├── .vscode └── settings.json ├── LICENSE.md ├── README.md ├── docs └── assets │ ├── demo.gif │ ├── icon.png │ ├── logo.png │ └── tryMe.gif └── examples ├── blendshapes-only ├── .env ├── .gitignore ├── .nvmrc ├── README.md ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.css │ ├── App.test.tsx │ ├── App.tsx │ ├── avatar │ │ └── predictions.ts │ ├── index.css │ ├── index.tsx │ ├── logo.svg │ ├── react-app-env.d.ts │ ├── reportWebVitals.ts │ └── setupTests.ts ├── tsconfig.json └── yarn.lock ├── hallway-rendering-tools ├── .env ├── .gitignore ├── README.md ├── package.json ├── public │ ├── backgrounds │ │ └── venice_sunset_1k.hdr │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ ├── models │ │ ├── Smiley_eye.glb │ │ ├── hannah.glb │ │ ├── headphones_2.glb │ │ ├── kevin.glb │ │ ├── mozilla.glb │ │ ├── rose.glb │ │ └── rpm.glb │ └── robots.txt ├── src │ ├── App.css │ ├── App.test.tsx │ ├── App.tsx │ ├── assets │ │ ├── avatar_thumbs │ │ │ ├── emoji.png │ │ │ ├── hannah.png │ │ │ ├── kevin.png │ │ │ └── mozilla.png │ │ └── icons │ │ │ ├── art.svg │ │ │ └── backgrounds.svg │ ├── avatar │ │ ├── AvatarOptions.ts │ │ ├── BackgroundOptions.ts │ │ ├── avatarLayout.module.scss │ │ └── avatarLayout.tsx │ ├── components │ │ ├── controls │ │ │ ├── backgroundButton.module.scss │ │ │ ├── backgroundButton.tsx │ │ │ ├── characterButton.module.scss │ │ │ ├── characterButton.tsx │ │ │ ├── customizationButton.module.scss │ │ │ ├── customizationButton.tsx │ │ │ └── popovers │ │ │ │ ├── backgroundPopover.module.scss │ │ │ │ ├── backgroundPopover.tsx │ │ │ │ ├── characterPopover.module.scss │ │ │ │ ├── characterPopover.tsx │ │ │ │ ├── customizationPopover.module.scss │ │ │ │ └── customizationPopover.tsx │ │ └── shared │ │ │ ├── loader.module.scss │ │ │ ├── loader.tsx │ │ │ ├── menuSelect.module.scss │ │ │ ├── menuSelect.tsx │ │ │ ├── splotch.module.scss │ │ │ ├── splotch.tsx │ │ │ ├── switch.module.scss │ │ │ └── switch.tsx │ ├── index.css │ ├── index.tsx │ ├── logo.svg │ ├── react-app-env.d.ts │ ├── reportWebVitals.ts │ ├── setupTests.ts │ └── utils │ │ └── browser.ts ├── tsconfig.json └── yarn.lock ├── react-app-with-threejs ├── .env ├── .gitignore ├── README.md ├── package.json ├── public │ ├── backgrounds │ │ └── venice_sunset_1k.hdr │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ ├── models │ │ ├── Smiley_eye.glb │ │ ├── hannah.glb │ │ ├── headphones_2.glb │ │ ├── kevin.glb │ │ ├── mozilla.glb │ │ ├── rose.glb │ │ └── rpm.glb │ └── robots.txt ├── src │ ├── App.css │ ├── App.test.tsx │ ├── App.tsx │ ├── avatar │ │ ├── avatarLayout.module.scss │ │ ├── avatarLayout.tsx │ │ ├── components │ │ │ ├── loader.module.scss │ │ │ ├── loader.tsx │ │ │ ├── menuSelect.module.scss │ │ │ ├── menuSelect.tsx │ │ │ ├── switch.module.scss │ │ │ └── switch.tsx │ │ ├── utils │ │ │ └── browser.ts │ │ └── world │ │ │ ├── components │ │ │ ├── camera.ts │ │ │ ├── lights.ts │ │ │ └── scene.ts │ │ │ ├── globalCanvas.ts │ │ │ ├── models │ │ │ ├── emoji.ts │ │ │ ├── index.ts │ │ │ ├── mozilla.ts │ │ │ └── readyPlayerMe.ts │ │ │ ├── renderLoop.ts │ │ │ ├── systems │ │ │ ├── controls.ts │ │ │ ├── environmentLoader.ts │ │ │ ├── loadModel.ts │ │ │ └── webGLRenderer.ts │ │ │ └── world.ts │ ├── index.css │ ├── index.tsx │ ├── logo.svg │ ├── react-app-env.d.ts │ ├── reportWebVitals.ts │ └── setupTests.ts ├── tsconfig.json └── yarn.lock ├── ready-player-me-tutorials ├── .gitignore ├── README.md ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── AvatarView.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ ├── reportWebVitals.js │ └── setupTests.js └── yarn.lock └── render-multiple-avatars ├── .env ├── .gitignore ├── README.md ├── package.json ├── public ├── backgrounds │ └── venice_sunset_1k.hdr ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json ├── models │ ├── Smiley_eye.glb │ ├── hannah.glb │ ├── headphones_2.glb │ ├── kevin.glb │ ├── mozilla.glb │ ├── rose.glb │ └── rpm.glb └── robots.txt ├── src ├── App.css ├── App.test.tsx ├── App.tsx ├── avatar │ ├── avatarsLayout.module.scss │ ├── avatarsLayout.tsx │ ├── components │ │ ├── loader.module.scss │ │ ├── loader.tsx │ │ ├── menuSelect.module.scss │ │ ├── menuSelect.tsx │ │ ├── switch.module.scss │ │ └── switch.tsx │ ├── utils │ │ └── browser.ts │ └── world │ │ ├── components │ │ ├── camera.ts │ │ ├── lights.ts │ │ └── scene.ts │ │ ├── globalCanvas.ts │ │ ├── models │ │ ├── emoji.ts │ │ ├── index.ts │ │ ├── mozilla.ts │ │ └── readyPlayerMe.ts │ │ ├── renderLoop.ts │ │ ├── systems │ │ ├── controls.ts │ │ ├── environmentLoader.ts │ │ ├── loadModel.ts │ │ └── webGLRenderer.ts │ │ └── world.ts ├── index.css ├── index.tsx ├── logo.svg ├── react-app-env.d.ts ├── reportWebVitals.ts └── setupTests.ts ├── tsconfig.json └── yarn.lock /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "renderable", 4 | "renderables" 5 | ] 6 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | ARCHIVED - AvatarWebKit by Hallway 5 |

6 |

7 | 8 | **This repository is no longer active and SDK token access has been suspended. Our SDK is no longer publically available. Thank you for using AvatarWebKit.** 9 | 10 | AvatarWebKit is an SDK, developed by Hallway, optimized for the web that provides real-time blend shapes from a camera feed, video or image. The SDK also gives head X/Y position, depth (Z) and rotation (pitch, roll, yaw) for each frame. AvatarWebKit runs at 60 FPS and provides ARKit-compatible 52 blend shapes. 11 | 12 | In the future, the SDK will be able to provide rigid body frame and hand positions as well. 13 | 14 |

15 | 16 |

17 | 18 | Hallway drives our avatar technology using Machine Learning models that predict highly accurate blend shapes from images & video feeds in real-time. The ML pipeline is optimized for real-time video to achieve both high framerate and lifelike animations. 19 | 20 | Our vision for the future is an "open metaverse" where you can take your character with you anywhere. We believe tools like AvatarWebKit will help pave that road. The models we've provided here are available to use in your applications for free. [Contact us](#contact-us) to get in touch about making your characters compatible with Hallway! 21 | 22 | ## Installation 23 | 24 | ```bash 25 | # yarn 26 | yarn add @quarkworks-inc/avatar-webkit 27 | 28 | # npm 29 | npm install @quarkworks-inc/avatar-webkit 30 | ``` 31 | 32 | ## First Steps 33 | 34 | 1. [Get an API token](https://joinhallway.com/sdk/) 35 | 36 | 2. Start your predictor: 37 | 38 | ```ts 39 | import { AUPredictor } from '@quarkworks-inc/avatar-webkit' 40 | // ... 41 | 42 | let predictor = new AUPredictor({ 43 | apiToken: , 44 | shouldMirrorOutput: true, 45 | }) 46 | 47 | let stream = await navigator.mediaDevices.getUserMedia({ 48 | audio: false, 49 | video: { 50 | width: { ideal: 640 }, 51 | height: { ideal: 360 }, 52 | facingMode: 'user' 53 | } 54 | }) 55 | 56 | predictor.onPredict = (results => { 57 | console.log(results) 58 | }) 59 | 60 | // or if you like RX 61 | predictor.dataStream.subscribe(results => { 62 | console.log(results) 63 | }) 64 | 65 | predictor.start({ stream }) 66 | ``` 67 | 68 | # More Docs 69 | 70 | * https://docs.google.com/document/d/16c3qSYvMi_5l2zXdrsykb2xH6XneOqxTd2wwnEVawxY/edit# 71 | 72 | # Example Projects 73 | 74 | ### Using AvatarWebKit 75 | - [Basic example running predictor w/o rendering](examples/blendshapes-only) 76 | - [Predictor + React + Three.js (basic)](examples/react-app-with-threejs) 77 | - [Video Call Style UI](examples/render-multiple-avatars) 78 | - [Using our rendering kit module](examples/hallway-rendering-tools) 79 | 80 | ### Popular model integrations 81 | 82 | - [ReadyPlayerMe Examples](examples/ready-player-me-tutorials) 83 | 84 | # FAQ 85 | 86 | ### API Token? What is that and why do I need it? 87 | 88 | An API key is your unique identifier that will allow your code to authenticate with our server when using the SDK. [You can sign up for one here.](https://joinhallway.com/sdk) 89 | 90 | ### What browsers are supported? 91 | 92 | We recommend Chromium based browsers for best performance, but all other major browsers are supported. We are currently working on performance improvements for Safari, Firefox and Edge. 93 | 94 | ### Is mobile supported? 95 | 96 | The models will currently run on mobile but need to be optimized. We are working on configuration options which will allow you to choose to run lighter models. 97 | 98 | ### Do you have any native SDKs? 99 | 100 | We do not have an official SDK yet, but our ML pipeline is native-first and the models are used in our Mac OS app [Hallway Tile](https://joinhallway.com). We have the capability to create SDKs for most common platforms (eg macOS/Windows/Linux, iOS/Android). Each SDK will follow the same data standard for BlendShapes/predictions and will include encoders for portability between environments. This means you can do some creative things between native, web, etc! 101 | 102 | If you are interested in native SDKs, we'd love to hear from you! 103 | 104 | ### Is this production ready? 105 | 106 | Yes, depending on your needs. There may be a couple rough edges at the moment, but the SDK has been in use internally at our company for over a year and in production with several pilot companies. 107 | 108 | We are currently making no SLAs for the SDK, but we are happy to cooperate with you on any improvements you need to get it going in production. 109 | 110 | ### Can I make feature requests? 111 | 112 | YES!!! We are in an open beta currently and would love to hear your feedback. [Contact us](#contact-us) on Discord or by email. 113 | 114 | ### What’s the best place to reach out for support? 115 | 116 | We are active daily on our and can help with any problems you may have! If discord doesn’t work for you, reach out to 117 | 118 | # Contact Us 119 | 120 | Our team is primarily in U.S. timezones, but we are pretty active on Discord and over email! We've love to hear your thoughts, feedback, ideas or provide any support you need. 121 | 122 | * [Discord](https://discord.gg/jYCHaMASz7) 123 | 124 | * [contact@joinhallway.com](mailto:contact@joinhallway.com) 125 | 126 | * [support@joinhallway.com](mailto:support@joinhallway.com) 127 | 128 | # Other Hallway Tools 129 | 130 | * https://github.com/Hallway-Inc/AvatarWebKit-Rendering 131 | 132 | If you are using Three.js, we've released this open source tooling module you can import freely. This pairs especially well with video-call style apps, as we provide a three world setup that works well for rendering multiple avatars on screen at once Zoom-style. 133 | 134 | # TODO 135 | 136 | More coming :) 137 | -------------------------------------------------------------------------------- /docs/assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hallway-Inc/AvatarWebKit/c55a6aa000c46b6b8d7b1b17a288e59316776d5b/docs/assets/demo.gif -------------------------------------------------------------------------------- /docs/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hallway-Inc/AvatarWebKit/c55a6aa000c46b6b8d7b1b17a288e59316776d5b/docs/assets/icon.png -------------------------------------------------------------------------------- /docs/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hallway-Inc/AvatarWebKit/c55a6aa000c46b6b8d7b1b17a288e59316776d5b/docs/assets/logo.png -------------------------------------------------------------------------------- /docs/assets/tryMe.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hallway-Inc/AvatarWebKit/c55a6aa000c46b6b8d7b1b17a288e59316776d5b/docs/assets/tryMe.gif -------------------------------------------------------------------------------- /examples/blendshapes-only/.env: -------------------------------------------------------------------------------- 1 | REACT_APP_AVATAR_WEBKIT_AUTH_TOKEN=${AVATAR_WEBKIT_AUTH_TOKEN} -------------------------------------------------------------------------------- /examples/blendshapes-only/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /examples/blendshapes-only/.nvmrc: -------------------------------------------------------------------------------- 1 | 16.14.0 2 | -------------------------------------------------------------------------------- /examples/blendshapes-only/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `yarn start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `yarn test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `yarn build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `yarn eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /examples/blendshapes-only/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blendshapes-only", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@quarkworks-inc/avatar-webkit": "0.9.3", 7 | "@testing-library/jest-dom": "^5.14.1", 8 | "@testing-library/react": "^12.0.0", 9 | "@testing-library/user-event": "^13.2.1", 10 | "@types/jest": "^27.0.1", 11 | "@types/node": "^16.7.13", 12 | "@types/react": "^17.0.20", 13 | "@types/react-dom": "^17.0.9", 14 | "react": "^17.0.2", 15 | "react-dom": "^17.0.2", 16 | "react-scripts": "5.0.0", 17 | "typescript": "^4.4.2", 18 | "web-vitals": "^2.1.0" 19 | }, 20 | "resolutions": { 21 | "react-error-overlay": "6.0.9" 22 | }, 23 | "scripts": { 24 | "start": "react-scripts start", 25 | "build": "react-scripts build", 26 | "test": "react-scripts test", 27 | "eject": "react-scripts eject" 28 | }, 29 | "eslintConfig": { 30 | "extends": [ 31 | "react-app", 32 | "react-app/jest" 33 | ] 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /examples/blendshapes-only/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hallway-Inc/AvatarWebKit/c55a6aa000c46b6b8d7b1b17a288e59316776d5b/examples/blendshapes-only/public/favicon.ico -------------------------------------------------------------------------------- /examples/blendshapes-only/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /examples/blendshapes-only/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hallway-Inc/AvatarWebKit/c55a6aa000c46b6b8d7b1b17a288e59316776d5b/examples/blendshapes-only/public/logo192.png -------------------------------------------------------------------------------- /examples/blendshapes-only/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hallway-Inc/AvatarWebKit/c55a6aa000c46b6b8d7b1b17a288e59316776d5b/examples/blendshapes-only/public/logo512.png -------------------------------------------------------------------------------- /examples/blendshapes-only/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /examples/blendshapes-only/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /examples/blendshapes-only/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /examples/blendshapes-only/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/blendshapes-only/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import logo from './logo.svg'; 3 | import './App.css'; 4 | import { AvatarPredictions } from './avatar/predictions'; 5 | 6 | class App extends React.Component { 7 | 8 | private avatarPredictions = new AvatarPredictions() 9 | 10 | componentDidMount() { 11 | this.avatarPredictions.start() 12 | .then(() => console.log('Avatar predictions started')) 13 | .catch(err => console.error('Avatar predictions failed to start', err)) 14 | } 15 | 16 | componentWillUnmount() { 17 | this.avatarPredictions.stop() 18 | .then(() => console.log('Avatar predictions stopped')) 19 | .catch(err => console.error('Avatar predictions failed to stop', err)) 20 | } 21 | 22 | render() { 23 | return ( 24 |
25 |
26 | logo 27 |

28 | Edit src/App.tsx and save to reload. 29 |

30 | 36 | Learn React 37 | 38 |
39 |
40 | ); 41 | } 42 | } 43 | 44 | export default App; 45 | -------------------------------------------------------------------------------- /examples/blendshapes-only/src/avatar/predictions.ts: -------------------------------------------------------------------------------- 1 | import { AUPredictor, AvatarPrediction } from '@quarkworks-inc/avatar-webkit' 2 | 3 | export class AvatarPredictions { 4 | videoStream?: MediaStream 5 | predictor?: AUPredictor 6 | 7 | async start() { 8 | this.videoStream = await navigator.mediaDevices.getUserMedia({ 9 | audio: false, 10 | video: { 11 | width: { ideal: 640 }, 12 | height: { ideal: 360 }, 13 | facingMode: 'user' 14 | } 15 | }) 16 | 17 | this.predictor = new AUPredictor({ 18 | apiToken: process.env.REACT_APP_AVATAR_WEBKIT_AUTH_TOKEN! 19 | }) 20 | 21 | this.predictor.onPredict = ((results: AvatarPrediction) => { 22 | console.log(results) 23 | }) 24 | 25 | return this.predictor.start({ 26 | stream: this.videoStream 27 | }) 28 | } 29 | 30 | async stop() { 31 | return this.predictor?.stop() 32 | } 33 | } -------------------------------------------------------------------------------- /examples/blendshapes-only/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /examples/blendshapes-only/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /examples/blendshapes-only/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/blendshapes-only/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/blendshapes-only/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /examples/blendshapes-only/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /examples/blendshapes-only/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx", 22 | "baseUrl": "src" 23 | }, 24 | "include": [ 25 | "src" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/.env: -------------------------------------------------------------------------------- 1 | REACT_APP_AVATAR_WEBKIT_AUTH_TOKEN=${AVATAR_WEBKIT_AUTH_TOKEN} -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `yarn start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `yarn test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `yarn build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `yarn eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hallway-rendering-tools", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@quarkworks-inc/avatar-webkit": "0.9.3", 7 | "@quarkworks-inc/avatar-webkit-rendering": "0.8.1", 8 | "@testing-library/jest-dom": "^5.14.1", 9 | "@testing-library/react": "^12.0.0", 10 | "@testing-library/user-event": "^13.2.1", 11 | "@types/jest": "^27.0.1", 12 | "@types/node": "^16.7.13", 13 | "@types/react": "^17.0.20", 14 | "@types/react-dom": "^17.0.9", 15 | "classnames": "^2.3.1", 16 | "react": "^17.0.2", 17 | "react-color": "^2.19.3", 18 | "react-dom": "^17.0.2", 19 | "react-scripts": "5.0.0", 20 | "rsuite": "^5.7.0", 21 | "sass": "^1.49.9", 22 | "typescript": "^4.4.2", 23 | "web-vitals": "^2.1.0" 24 | }, 25 | "scripts": { 26 | "start": "react-scripts start", 27 | "build": "react-scripts build", 28 | "test": "react-scripts test", 29 | "eject": "react-scripts eject" 30 | }, 31 | "eslintConfig": { 32 | "extends": [ 33 | "react-app", 34 | "react-app/jest" 35 | ] 36 | }, 37 | "browserslist": { 38 | "production": [ 39 | ">0.2%", 40 | "not dead", 41 | "not op_mini all" 42 | ], 43 | "development": [ 44 | "last 1 chrome version", 45 | "last 1 firefox version", 46 | "last 1 safari version" 47 | ] 48 | }, 49 | "devDependencies": { 50 | "@types/classnames": "^2.3.1", 51 | "@types/react-color": "^3.0.6" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/public/backgrounds/venice_sunset_1k.hdr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hallway-Inc/AvatarWebKit/c55a6aa000c46b6b8d7b1b17a288e59316776d5b/examples/hallway-rendering-tools/public/backgrounds/venice_sunset_1k.hdr -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hallway-Inc/AvatarWebKit/c55a6aa000c46b6b8d7b1b17a288e59316776d5b/examples/hallway-rendering-tools/public/favicon.ico -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hallway-Inc/AvatarWebKit/c55a6aa000c46b6b8d7b1b17a288e59316776d5b/examples/hallway-rendering-tools/public/logo192.png -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hallway-Inc/AvatarWebKit/c55a6aa000c46b6b8d7b1b17a288e59316776d5b/examples/hallway-rendering-tools/public/logo512.png -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/public/models/Smiley_eye.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hallway-Inc/AvatarWebKit/c55a6aa000c46b6b8d7b1b17a288e59316776d5b/examples/hallway-rendering-tools/public/models/Smiley_eye.glb -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/public/models/hannah.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hallway-Inc/AvatarWebKit/c55a6aa000c46b6b8d7b1b17a288e59316776d5b/examples/hallway-rendering-tools/public/models/hannah.glb -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/public/models/headphones_2.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hallway-Inc/AvatarWebKit/c55a6aa000c46b6b8d7b1b17a288e59316776d5b/examples/hallway-rendering-tools/public/models/headphones_2.glb -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/public/models/kevin.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hallway-Inc/AvatarWebKit/c55a6aa000c46b6b8d7b1b17a288e59316776d5b/examples/hallway-rendering-tools/public/models/kevin.glb -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/public/models/mozilla.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hallway-Inc/AvatarWebKit/c55a6aa000c46b6b8d7b1b17a288e59316776d5b/examples/hallway-rendering-tools/public/models/mozilla.glb -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/public/models/rose.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hallway-Inc/AvatarWebKit/c55a6aa000c46b6b8d7b1b17a288e59316776d5b/examples/hallway-rendering-tools/public/models/rose.glb -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/public/models/rpm.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hallway-Inc/AvatarWebKit/c55a6aa000c46b6b8d7b1b17a288e59316776d5b/examples/hallway-rendering-tools/public/models/rpm.glb -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/src/App.css: -------------------------------------------------------------------------------- 1 | .container { 2 | justify-content: space-around; 3 | display: flex; 4 | flex-wrap: wrap; 5 | align-items: center !important; 6 | flex-direction: row; 7 | } 8 | -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './App.css'; 3 | import AvatarLayout from './avatar/avatarLayout'; 4 | 5 | class App extends React.Component { 6 | render() { 7 | return ( 8 |
9 | 10 |
11 | ) 12 | } 13 | } 14 | 15 | export default App; 16 | -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/src/assets/avatar_thumbs/emoji.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hallway-Inc/AvatarWebKit/c55a6aa000c46b6b8d7b1b17a288e59316776d5b/examples/hallway-rendering-tools/src/assets/avatar_thumbs/emoji.png -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/src/assets/avatar_thumbs/hannah.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hallway-Inc/AvatarWebKit/c55a6aa000c46b6b8d7b1b17a288e59316776d5b/examples/hallway-rendering-tools/src/assets/avatar_thumbs/hannah.png -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/src/assets/avatar_thumbs/kevin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hallway-Inc/AvatarWebKit/c55a6aa000c46b6b8d7b1b17a288e59316776d5b/examples/hallway-rendering-tools/src/assets/avatar_thumbs/kevin.png -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/src/assets/avatar_thumbs/mozilla.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hallway-Inc/AvatarWebKit/c55a6aa000c46b6b8d7b1b17a288e59316776d5b/examples/hallway-rendering-tools/src/assets/avatar_thumbs/mozilla.png -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/src/avatar/AvatarOptions.ts: -------------------------------------------------------------------------------- 1 | import { hallwayPublicCDNUrl, ModelType } from "@quarkworks-inc/avatar-webkit-rendering" 2 | 3 | import emojiIcon from '../assets/avatar_thumbs/emoji.png' 4 | import mozillaIcon from '../assets/avatar_thumbs/mozilla.png' 5 | import hannahIcon from '../assets/avatar_thumbs/hannah.png' 6 | import kevinIcon from '../assets/avatar_thumbs/kevin.png' 7 | 8 | export class AvatarOptions { 9 | readonly id: string 10 | readonly name: string 11 | readonly thumbnail: string 12 | readonly modelType: ModelType 13 | readonly modelUrl?: string 14 | 15 | private constructor(id: string, name: string, thumbnail: string, modelType: ModelType, modelUrl?: string) { 16 | this.id = id 17 | this.name = name 18 | this.thumbnail = thumbnail 19 | this.modelType = modelType 20 | this.modelUrl = modelUrl 21 | } 22 | 23 | public static readonly emoji = new AvatarOptions('emoji', 'Mr. Emoji', emojiIcon, 'emoji') 24 | public static readonly mozilla = new AvatarOptions('mozilla', 'Mozilla', mozillaIcon, 'mozilla', hallwayPublicCDNUrl('models/mozilla.glb')) 25 | public static readonly rpmHannah = new AvatarOptions('rpm_hannah', 'ReadyPlayerMe (Hannah)', hannahIcon, 'readyPlayerMe', hallwayPublicCDNUrl('models/hannah.glb')) 26 | public static readonly rpmKevin = new AvatarOptions('rpm_kevin', 'ReadyPlayerMe (Kevin)', kevinIcon, 'readyPlayerMe', hallwayPublicCDNUrl('models/kevin.glb')) 27 | 28 | public static readonly all: AvatarOptions[] = [ 29 | AvatarOptions.emoji, 30 | AvatarOptions.mozilla, 31 | AvatarOptions.rpmHannah, 32 | AvatarOptions.rpmKevin 33 | ] 34 | } -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/src/avatar/BackgroundOptions.ts: -------------------------------------------------------------------------------- 1 | import { hallwayPublicCDNUrl } from "@quarkworks-inc/avatar-webkit-rendering" 2 | 3 | export class BackgroundOptions { 4 | readonly id: string 5 | readonly name: string 6 | // readonly thumbnail: string 7 | readonly url: string 8 | readonly size: string 9 | 10 | private constructor(id: string, name: string, url: string, size: string) { 11 | this.id = id 12 | this.name = name 13 | this.url = url 14 | this.size = size 15 | } 16 | 17 | // 1k HDRs 18 | static readonly venice_sunset_1k = new BackgroundOptions("venice_sunset_1k", "Venice Sunset (1k)", hallwayPublicCDNUrl('backgrounds/venice_sunset_1k.hdr'), '1.3 MB') 19 | static readonly aerodynamics_workshop_1k = new BackgroundOptions("aerodynamics_workshop_1k", "Aerodynamics Workshop (1k)", hallwayPublicCDNUrl('backgrounds/aerodynamics_workshop_1k.hdr'), '1.4 MB') 20 | static readonly missle_launch_facility_1k = new BackgroundOptions("missle_launch_facility_1k", "Missle Launch Facility (1k)", hallwayPublicCDNUrl('backgrounds/missile_launch_facility_01_1k.hdr'), '1.6 MB') 21 | 22 | // 4k EXRs 23 | // Sorted by size increasing 24 | static readonly aerodynamics_workshop_4k = new BackgroundOptions("aerodynamics_workshop_4k", "Aerodynamics Workshop (4k)", hallwayPublicCDNUrl('backgrounds/aerodynamics_workshop_4k.exr'), '18.6 MB') 25 | static readonly small_cathedral_4k = new BackgroundOptions("small_cathedral_4k", "Small Cathedral (4k)", hallwayPublicCDNUrl('backgrounds/small_cathedral_02_4k.exr'), '18.8 MB') 26 | static readonly spruit_sunrise_4k = new BackgroundOptions("spruit_sunrise_4k", "Spruit Sunrise (4k)", hallwayPublicCDNUrl('backgrounds/spruit_sunrise_4k.exr'), '20.1 MB') 27 | static readonly konzerthaus_4k = new BackgroundOptions("konzerthaus_4k", "Konzerthaus (4k)", hallwayPublicCDNUrl('backgrounds/konzerthaus_4k.exr'), '21.0 MB') 28 | static readonly teufelsberg_lookout_4k = new BackgroundOptions("teufelsberg_lookout_4k", "Teufelsberg Lookout (4k)", hallwayPublicCDNUrl('backgrounds/teufelsberg_lookout_4k.exr'), '24.1 MB') 29 | static readonly blaubeuren_church_square_4k = new BackgroundOptions("blaubeuren_church_square_4k", "Blaubeuren Church Square (4k)", hallwayPublicCDNUrl('backgrounds/blaubeuren_church_square_4k.exr'), '83.7 MB') 30 | static readonly phalzer_forest_4k = new BackgroundOptions("phalzer_forest_4k", "Phalzer Forest (4k)", hallwayPublicCDNUrl('backgrounds/phalzer_forest_01_4k.exr'), '88.7 MB') 31 | 32 | // Collections 33 | static readonly all_1k: BackgroundOptions[] = [this.venice_sunset_1k, this.aerodynamics_workshop_1k, this.missle_launch_facility_1k] 34 | static readonly all_4k: BackgroundOptions[] = [this.aerodynamics_workshop_4k, this.small_cathedral_4k, this.spruit_sunrise_4k, this.konzerthaus_4k, this.teufelsberg_lookout_4k, this.blaubeuren_church_square_4k, this.phalzer_forest_4k] 35 | static readonly all: BackgroundOptions[] = [...this.all_1k, ...this.all_4k] 36 | } -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/src/avatar/avatarLayout.module.scss: -------------------------------------------------------------------------------- 1 | .app { 2 | width: 100%; 3 | height: 100%; 4 | background-color: rgba(0, 0, 0, 0); 5 | overflow: hidden; 6 | } 7 | 8 | .container { 9 | align-items: center; 10 | display: flex; 11 | flex-direction: column; 12 | flex-wrap: wrap; 13 | height: 100%; 14 | justify-content: center; 15 | justify-items: center; 16 | position: relative; 17 | width: 100%; 18 | } 19 | 20 | .controlsContainer { 21 | display: flex; 22 | flex-direction: row; 23 | flex-wrap: nowrap; 24 | align-items: center; 25 | justify-items: center; 26 | } 27 | 28 | .videoContainer { 29 | width: 1280px; 30 | height: 720px; 31 | position: relative; 32 | } 33 | 34 | .video { 35 | position: absolute; 36 | top: 16px; 37 | right: 16px; 38 | max-width: 160px; 39 | height: auto; 40 | } 41 | 42 | .inActiveContainer { 43 | position: absolute; 44 | top: 0; 45 | left: 0; 46 | width: 100%; 47 | height: 100%; 48 | display: flex; 49 | flex-direction: column; 50 | justify-content: center; 51 | align-items: center; 52 | } 53 | 54 | .inActiveContainer video { 55 | max-width: 100%; 56 | max-height: 100%; 57 | } 58 | 59 | .loadingContainer { 60 | position: absolute; 61 | top: 0; 62 | left: 0; 63 | width: 100%; 64 | height: 100%; 65 | display: flex; 66 | flex-direction: column; 67 | justify-content: center; 68 | align-items: center; 69 | } 70 | 71 | .buttonContainer { 72 | position: absolute; 73 | width: 100%; 74 | bottom: 10px; 75 | display: flex; 76 | align-items: center; 77 | justify-content: center; 78 | } 79 | 80 | .switchContainer { 81 | position: relative; 82 | } 83 | 84 | .deviceSelectContainer { 85 | position: relative; 86 | margin-left: 20px; 87 | } 88 | 89 | .zStack { 90 | position: relative; 91 | } 92 | 93 | .zStack > * { 94 | position: absolute; 95 | width: 100%; 96 | height: 100%; 97 | } 98 | 99 | @media screen and (max-width: 768px) { 100 | .app { 101 | display: none; 102 | } 103 | 104 | .inActiveContainer { 105 | display: none; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/src/components/controls/backgroundButton.module.scss: -------------------------------------------------------------------------------- 1 | .button { 2 | display: inline-block; 3 | width: 70px; 4 | height: 70px; 5 | margin: 8px; 6 | background-color: #2f2f2f; 7 | border-radius: 40px; 8 | 9 | // prevent drag/select 10 | -webkit-user-drag: none; 11 | user-select: none; 12 | -moz-user-select: none; 13 | -webkit-user-select: none; 14 | 15 | svg { 16 | width: 100%; 17 | height: auto; 18 | padding: 6px; 19 | 20 | // https://codepen.io/sosuke/pen/Pjoqqp 21 | filter: invert(100%) sepia(99%) saturate(5%) hue-rotate(101deg) brightness(103%) contrast(102%); 22 | } 23 | 24 | &:hover { 25 | // filter: brightness(0.95); 26 | background-color: #434343; 27 | cursor: pointer; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/src/components/controls/backgroundButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Whisper } from 'rsuite' 3 | 4 | import styles from './backgroundButton.module.scss' 5 | import { ReactComponent as BackgroundsSvg } from '../../assets/icons/backgrounds.svg' 6 | import { BackgroundOptions } from '../../avatar/BackgroundOptions' 7 | import { BackgroundPopover } from './popovers/backgroundPopover' 8 | 9 | const tooltipId = 'BackgroundButton__tooltip' 10 | 11 | type Props = { 12 | selectedBackground: BackgroundOptions 13 | onBackgroundSelected: (background: BackgroundOptions) => void 14 | } 15 | 16 | export class BackgroundButton extends React.PureComponent { 17 | render() { 18 | const popover = 19 | 20 | return <> 21 | 27 |
28 | 29 |
30 |
31 | 32 | } 33 | } -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/src/components/controls/characterButton.module.scss: -------------------------------------------------------------------------------- 1 | .button { 2 | display: inline-block; 3 | width: 80px; 4 | height: 80px; 5 | margin: 20px; 6 | 7 | // prevent drag/select 8 | -webkit-user-drag: none; 9 | user-select: none; 10 | -moz-user-select: none; 11 | -webkit-user-select: none; 12 | 13 | img { 14 | width: 80px; 15 | height: 80px; 16 | border: 1px solid rgba(0, 0, 0, 0.2); 17 | border-radius: 40px; 18 | } 19 | 20 | &:hover { 21 | filter: brightness(0.95); 22 | cursor: pointer; 23 | } 24 | } -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/src/components/controls/characterButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Whisper } from 'rsuite' 3 | 4 | import styles from './characterButton.module.scss' 5 | 6 | import { CharacterPopover } from './popovers/characterPopover' 7 | import { AvatarOptions } from '../../avatar/AvatarOptions' 8 | 9 | const tooltipId = 'CharacterButton__tooltip' 10 | 11 | type Props = { 12 | selectedAvatar: AvatarOptions 13 | onAvatarSelected: (avatar: AvatarOptions) => void 14 | } 15 | 16 | export class CharacterButton extends React.PureComponent { 17 | render() { 18 | const popover = 19 | 20 | return <> 21 | 27 |
28 | Choose character 29 |
30 |
31 | 32 | } 33 | } -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/src/components/controls/customizationButton.module.scss: -------------------------------------------------------------------------------- 1 | .button { 2 | display: inline-block; 3 | width: 70px; 4 | height: 70px; 5 | margin: 8px; 6 | background-color: #2f2f2f; 7 | border-radius: 40px; 8 | 9 | // prevent drag/select 10 | -webkit-user-drag: none; 11 | user-select: none; 12 | -moz-user-select: none; 13 | -webkit-user-select: none; 14 | 15 | svg { 16 | width: 100%; 17 | height: 100%; 18 | padding: 14px; 19 | 20 | // https://codepen.io/sosuke/pen/Pjoqqp 21 | filter: invert(100%) sepia(99%) saturate(5%) hue-rotate(101deg) brightness(103%) contrast(102%); 22 | } 23 | 24 | &:hover { 25 | // filter: brightness(0.95); 26 | background-color: #434343; 27 | cursor: pointer; 28 | } 29 | } 30 | 31 | .disabled { 32 | opacity: 0.25; 33 | 34 | &:hover { 35 | background-color: #2f2f2f; 36 | cursor: auto; 37 | } 38 | } -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/src/components/controls/customizationButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import classNames from 'classnames' 3 | import { Whisper } from 'rsuite' 4 | import { ModelSettings } from '@quarkworks-inc/avatar-webkit-rendering' 5 | 6 | import styles from './customizationButton.module.scss' 7 | 8 | import { ReactComponent as ArtSvg } from '../../assets/icons/art.svg' 9 | import { CustomizationPopover } from './popovers/customizationPopover' 10 | 11 | const tooltipId = 'CustomizationButton__tooltip' 12 | 13 | type Props = { 14 | settings: ModelSettings 15 | onSettingsDidUpdate: (settings: ModelSettings) => void 16 | } 17 | 18 | type State = { 19 | isPopoverOpen: boolean 20 | } 21 | 22 | export class CustomizationButton extends React.Component { 23 | 24 | state: State = { 25 | isPopoverOpen: false 26 | } 27 | 28 | get isDisabled(): boolean { 29 | return Object.keys(this.props.settings ?? {}).length === 0 30 | } 31 | 32 | componentDidMount(): void { 33 | this.onWindowClicked = this.onWindowClicked.bind(this) 34 | window.addEventListener('click', this.onWindowClicked) 35 | } 36 | 37 | componentWillUnmount(): void { 38 | window.removeEventListener('click', this.onWindowClicked) 39 | } 40 | 41 | onWindowClicked() { 42 | this.setState({ isPopoverOpen: false }) 43 | } 44 | 45 | buttonWasClicked(e: React.MouseEvent) { 46 | e.stopPropagation() 47 | const { isPopoverOpen } = this.state 48 | 49 | if (isPopoverOpen) { 50 | this.setState({ isPopoverOpen: false }) 51 | } else { 52 | // Prevents opening when it should be disabled 53 | if (!this.isDisabled) { 54 | this.setState({ isPopoverOpen: true }) 55 | } 56 | } 57 | } 58 | 59 | render() { 60 | const { settings, onSettingsDidUpdate } = this.props 61 | 62 | const popover = ( 63 | 67 | ) 68 | 69 | // Clicks are handled manually for this popover component because it contains a child popover that overflows 70 | return <> 71 | this.buttonWasClicked(e)} 75 | placement="top" 76 | controlId={tooltipId} 77 | speaker={popover} 78 | > 79 |
83 | 84 |
85 |
86 | 87 | } 88 | } -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/src/components/controls/popovers/backgroundPopover.module.scss: -------------------------------------------------------------------------------- 1 | .flexContainer { 2 | display: flex; 3 | flex-direction: row; 4 | flex-wrap: wrap; 5 | justify-content: space-evenly; 6 | } 7 | 8 | .backgroundContainer { 9 | display: flex; 10 | width: 120px; 11 | height: auto; 12 | margin: 10px; 13 | flex-direction: column; 14 | flex-wrap: nowrap; 15 | align-items: center; 16 | justify-items: center; 17 | } 18 | 19 | .background { 20 | display: inline-block; 21 | width: 80px; 22 | height: 80px; 23 | border-radius: 40px; 24 | transition: 0.05s ease-in; 25 | 26 | &:hover { 27 | filter: brightness(0.95); 28 | cursor: pointer; 29 | } 30 | } 31 | 32 | .label { 33 | font-size: 16px; 34 | font-weight: 400; 35 | text-align: center; 36 | margin-top: 6px; 37 | } -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/src/components/controls/popovers/backgroundPopover.tsx: -------------------------------------------------------------------------------- 1 | import classnames from 'classnames' 2 | import React from 'react' 3 | import { Popover } from 'rsuite' 4 | import { BackgroundOptions } from '../../../avatar/BackgroundOptions' 5 | 6 | import styles from './backgroundPopover.module.scss' 7 | 8 | type Props = { 9 | onBackgroundSelected: (background: BackgroundOptions) => void 10 | } 11 | 12 | class PopoverContent extends React.PureComponent { 13 | render() { 14 | return ( 15 |
16 | {BackgroundOptions.all.map(background => ( 17 |
18 | 19 |
20 | ))} 21 |
22 | ) 23 | } 24 | } 25 | 26 | export const BackgroundPopover = React.forwardRef(({ onBackgroundSelected, ...props }, ref) => { 27 | return ( 28 |
29 | 30 | 31 | 32 |
33 | ) 34 | }) -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/src/components/controls/popovers/characterPopover.module.scss: -------------------------------------------------------------------------------- 1 | .flexContainer { 2 | display: flex; 3 | flex-direction: row; 4 | flex-wrap: wrap; 5 | justify-content: space-evenly; 6 | } 7 | 8 | .avatarContainer { 9 | display: flex; 10 | width: 120px; 11 | height: auto; 12 | margin: 10px; 13 | flex-direction: column; 14 | flex-wrap: nowrap; 15 | align-items: center; 16 | justify-items: center; 17 | } 18 | 19 | .avatar { 20 | display: inline-block; 21 | width: 80px; 22 | height: 80px; 23 | border-radius: 40px; 24 | transition: 0.05s ease-in; 25 | 26 | &:hover { 27 | filter: brightness(0.95); 28 | cursor: pointer; 29 | } 30 | } 31 | 32 | .label { 33 | font-size: 16px; 34 | font-weight: 400; 35 | text-align: center; 36 | margin-top: 6px; 37 | } -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/src/components/controls/popovers/characterPopover.tsx: -------------------------------------------------------------------------------- 1 | import classnames from 'classnames' 2 | import React from 'react' 3 | import { Popover } from 'rsuite' 4 | import { AvatarOptions } from '../../../avatar/AvatarOptions' 5 | 6 | import styles from './characterPopover.module.scss' 7 | 8 | type Props = { 9 | onAvatarSelected: (avatar: AvatarOptions) => void 10 | } 11 | 12 | class PopoverContent extends React.PureComponent { 13 | render() { 14 | return ( 15 |
16 | {AvatarOptions.all.map(avatar => ( 17 |
18 | {avatar.name} this.props.onAvatarSelected(avatar)} 23 | /> 24 | 25 | 26 |
27 | ))} 28 |
29 | ) 30 | } 31 | } 32 | 33 | export const CharacterPopover = React.forwardRef(({ onAvatarSelected, ...props }, ref) => { 34 | return ( 35 |
36 | 37 | 38 | 39 |
40 | ) 41 | }) -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/src/components/controls/popovers/customizationPopover.module.scss: -------------------------------------------------------------------------------- 1 | .flexContainer { 2 | display: flex; 3 | flex-direction: row; 4 | flex-wrap: wrap; 5 | justify-content: space-evenly; 6 | } 7 | 8 | .splotchContainer { 9 | display: flex; 10 | width: 120px; 11 | height: auto; 12 | margin: 10px; 13 | flex-direction: column; 14 | flex-wrap: nowrap; 15 | align-items: center; 16 | justify-items: center; 17 | } 18 | -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/src/components/controls/popovers/customizationPopover.tsx: -------------------------------------------------------------------------------- 1 | import classnames from 'classnames' 2 | import React from 'react' 3 | import { Popover } from 'rsuite' 4 | import { ModelColorSetting, ModelSettings, ModelSettingType } from '@quarkworks-inc/avatar-webkit-rendering' 5 | import { ColorResult } from 'react-color' 6 | 7 | import { Splotch } from '../../shared/splotch' 8 | 9 | import styles from './characterPopover.module.scss' 10 | 11 | type Props = { 12 | settings: ModelSettings 13 | onSettingsDidUpdate: (settings: ModelSettings) => void 14 | } 15 | 16 | class PopoverContent extends React.PureComponent { 17 | 18 | private _onColorSelected(settingKey: string, newValue: ColorResult) { 19 | const { settings } = this.props 20 | settings[settingKey].value = newValue.hex 21 | 22 | this.props.onSettingsDidUpdate(settings) 23 | } 24 | 25 | render() { 26 | const { settings } = this.props 27 | 28 | return ( 29 |
30 | {Object.keys(settings).map(settingKey => { 31 | const anySetting = settings[settingKey] 32 | 33 | if (anySetting.type !== ModelSettingType.color) { 34 | console.warn('Nothing to handle non color options yet.') 35 | return undefined 36 | } 37 | 38 | const setting = anySetting as ModelColorSetting 39 | 40 | switch (setting.type) { 41 | case ModelSettingType.color: 42 | return ( 43 |
44 | this._onColorSelected(settingKey, color)} 49 | /> 50 |
51 | ) 52 | default: return undefined 53 | } 54 | 55 | })} 56 |
57 | ) 58 | } 59 | } 60 | 61 | export const CustomizationPopover = React.forwardRef(({ settings, onSettingsDidUpdate, ...props }, ref) => { 62 | return ( 63 |
64 | e.stopPropagation()}> 65 | 66 | 67 |
68 | ) 69 | }) -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/src/components/shared/loader.module.scss: -------------------------------------------------------------------------------- 1 | // @import '../../styles/colors.scss'; 2 | 3 | .loaderContainer { 4 | align-items: center; 5 | display: flex; 6 | flex-direction: column; 7 | justify-content: center; 8 | left: 0; 9 | top: 0; 10 | text-align: center; 11 | } 12 | 13 | .fullSize { 14 | height: 100%; 15 | width: 100%; 16 | } 17 | 18 | .loader { 19 | animation: animate 0.75s 0s infinite linear; 20 | animation-fill-mode: both; 21 | background: transparent !important; 22 | border-radius: 100%; 23 | border: 4px solid; 24 | border-color: blue; 25 | border-bottom-color: transparent; 26 | display: inline-block; 27 | height: 20px; 28 | margin: 0 5px; 29 | width: 20px; 30 | } 31 | 32 | .loaderText { 33 | color: white; 34 | font-size: 32px; 35 | font-weight: 600; 36 | margin: 1rem; 37 | text-align: center; 38 | } 39 | 40 | .version { 41 | color: blue; 42 | font-size: 0.7rem; 43 | margin: 1rem; 44 | text-align: center; 45 | } 46 | 47 | @keyframes animate { 48 | 0% { 49 | transform: rotate(0deg) scale(1); 50 | } 51 | 50% { 52 | transform: rotate(180deg) scale(0.8); 53 | } 54 | 100% { 55 | transform: rotate(360deg) scale(1); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/src/components/shared/loader.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames' 2 | import React from 'react' 3 | import { Property } from 'csstype' 4 | 5 | import styles from './loader.module.scss' 6 | 7 | type Props = { 8 | position: 'absolute' | 'relative' 9 | width: number 10 | height: number 11 | color?: Property.Color 12 | thickness?: 1 | 2 13 | subtext?: JSX.Element | string 14 | versionLabel?: string 15 | } 16 | 17 | export class Loader extends React.PureComponent { 18 | render() { 19 | const { position, width, height, color, thickness, subtext, versionLabel } = this.props 20 | return ( 21 |
28 |
37 | {versionLabel &&
{versionLabel}
} 38 | {subtext &&
{subtext}
} 39 |
40 | ) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/src/components/shared/menuSelect.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: row; 4 | flex-wrap: nowrap; 5 | align-items: center; 6 | } 7 | 8 | .select { 9 | background-color: white; 10 | border-color: none; 11 | border-radius: 35px; 12 | border: 1px solid; 13 | color: #4a57b9; 14 | cursor: pointer; 15 | font-family: 'Open Sans Semi Bold', sans-serif; 16 | font-size: 14px; 17 | font-weight: 500; 18 | padding: 13px 40px 13px 40px; 19 | position: relative; 20 | text-transform: capitalize; 21 | transition: 0.2s ease; 22 | z-index: 1; 23 | } 24 | 25 | .label { 26 | flex-grow: 0; 27 | margin-left: 10px; 28 | font-size: 0.8rem; 29 | color: #444; 30 | display: none; 31 | } 32 | 33 | .errorMessage { 34 | color: red; 35 | font-size: 0.7rem; 36 | } 37 | -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/src/components/shared/menuSelect.tsx: -------------------------------------------------------------------------------- 1 | import React, { ChangeEvent } from 'react' 2 | 3 | import styles from './menuSelect.module.scss' 4 | 5 | type Props = { 6 | errorMessage?: string 7 | label: string 8 | options: { value: string; label: string }[] 9 | permission?: boolean 10 | value: string 11 | onChange: (value: string) => void 12 | } 13 | 14 | const _onSelectChange = (onChange: (value: string) => void, event: ChangeEvent) => { 15 | event.stopPropagation() 16 | onChange(event.target.value) 17 | } 18 | 19 | export const MenuSelect = (props: Props) => { 20 | const { errorMessage, label, permission = true, onChange, options, value } = props 21 | 22 | return ( 23 |
24 | {!permission ? ( 25 | {errorMessage} 26 | ) : ( 27 | 36 | )} 37 | 38 |
39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/src/components/shared/splotch.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | width: 120px; 4 | height: auto; 5 | flex-direction: column; 6 | flex-wrap: nowrap; 7 | align-content: center; 8 | justify-content: center; 9 | } 10 | 11 | .splotch { 12 | display: inline-block; 13 | width: 80px; 14 | height: 80px; 15 | margin: 20px; 16 | border-radius: 40px; 17 | transition: 0.05s ease-in; 18 | 19 | &:hover { 20 | filter: brightness(0.95); 21 | cursor: pointer; 22 | } 23 | } 24 | 25 | .label { 26 | font-size: 18px; 27 | font-weight: 300; 28 | text-align: center; 29 | } -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/src/components/shared/splotch.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react' 2 | import { Color, ColorResult, SketchPicker } from 'react-color' 3 | import classnames from 'classnames' 4 | 5 | import styles from './splotch.module.scss' 6 | import { Popover, Whisper } from 'rsuite' 7 | 8 | type Props = { 9 | id: string 10 | label: string 11 | color: Color 12 | className?: string 13 | style?: CSSProperties 14 | onChangeComplete: (color: ColorResult) => void 15 | } 16 | 17 | type State = { 18 | color: Color 19 | } 20 | 21 | export class Splotch extends React.PureComponent { 22 | 23 | constructor(props: Props) { 24 | super(props) 25 | this.state = { 26 | color: props.color 27 | } 28 | } 29 | 30 | get tooltipId(): string { 31 | return `tooltip_${this.props.id}` 32 | } 33 | 34 | render() { 35 | const popover = ( 36 | 37 | this.setState({ color: color.hex })} 40 | onChangeComplete={this.props.onChangeComplete} 41 | /> 42 | 43 | ) 44 | 45 | return ( 46 |
47 | 48 | 54 |
60 | 61 | 62 | 63 |
64 | ) 65 | } 66 | } -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/src/components/shared/switch.module.scss: -------------------------------------------------------------------------------- 1 | .checkbox { 2 | height: 0; 3 | width: 0; 4 | visibility: hidden; 5 | display: none; 6 | } 7 | 8 | .label { 9 | display: flex; 10 | align-items: center; 11 | justify-content: space-between; 12 | cursor: pointer; 13 | width: 100px; 14 | height: 46px; 15 | background: grey; 16 | border-radius: 100px; 17 | position: relative; 18 | transition: background-color 0.2s; 19 | margin-bottom: 0; 20 | } 21 | 22 | .label .button { 23 | content: ''; 24 | position: absolute; 25 | top: 2px; 26 | left: 2px; 27 | width: 45px; 28 | height: 41px; 29 | border-radius: 41px; 30 | transition: 0.2s; 31 | background: #fff; 32 | box-shadow: 0 0 2px 0 rgba(10, 10, 10, 0.29); 33 | } 34 | 35 | .checkbox:checked + .label .button { 36 | left: calc(100% - 2px); 37 | transform: translateX(-100%); 38 | } 39 | 40 | .label:active .button { 41 | width: 60px; 42 | } 43 | -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/src/components/shared/switch.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import styles from './switch.module.scss' 4 | 5 | type Props = { 6 | isOn: boolean 7 | handleToggle: () => void 8 | onColor: string 9 | } 10 | 11 | export class Switch extends React.Component { 12 | render() { 13 | return ( 14 | <> 15 | 22 | 29 | 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import 'rsuite/dist/rsuite.min.css' 5 | import './index.css'; 6 | 7 | import App from './App'; 8 | import reportWebVitals from './reportWebVitals'; 9 | 10 | ReactDOM.render( 11 | 12 | 13 | , 14 | document.getElementById('root') 15 | ); 16 | 17 | // If you want to start measuring performance in your app, pass a function 18 | // to log results (for example: reportWebVitals(console.log)) 19 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 20 | reportWebVitals(); 21 | -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/src/utils/browser.ts: -------------------------------------------------------------------------------- 1 | // Source: https://github.com/w-okada/image-analyze-workers/blob/728898121e5062fdfc639c99e8cf2d1e1935fd2e/002_facemesh-worker-js/src/BrowserUtil.ts 2 | 3 | export enum BrowserType { 4 | 'MSIE', 5 | 'EDGE', 6 | 'CHROME', 7 | 'SAFARI', 8 | 'FIREFOX', 9 | 'OPERA', 10 | 'BRAVE', 11 | 'OTHER' 12 | } 13 | 14 | export const getBrowserType = () => { 15 | const userAgent = window.navigator.userAgent.toLowerCase() 16 | 17 | // @ts-expect-error non-supported 18 | if (navigator?.brave) { 19 | return BrowserType.BRAVE 20 | } else if (userAgent.indexOf('msie') !== -1 || userAgent.indexOf('trident') !== -1) { 21 | return BrowserType.MSIE 22 | } else if (userAgent.indexOf('opera') !== -1 || userAgent.indexOf('opr') !== -1) { 23 | return BrowserType.OPERA 24 | } else if (userAgent.indexOf('edge') !== -1) { 25 | return BrowserType.EDGE 26 | } else if (userAgent.indexOf('chrome') !== -1) { 27 | return BrowserType.CHROME 28 | } else if (userAgent.indexOf('safari') !== -1) { 29 | return BrowserType.SAFARI 30 | } else if (userAgent.indexOf('firefox') !== -1) { 31 | return BrowserType.FIREFOX 32 | } else { 33 | return BrowserType.OTHER 34 | } 35 | } 36 | 37 | export const avatarsAvailable = () => { 38 | const types = [BrowserType.CHROME, BrowserType.OPERA, BrowserType.BRAVE, BrowserType.FIREFOX, BrowserType.SAFARI] 39 | 40 | return types.includes(getBrowserType()) 41 | } 42 | 43 | export const gestureDetectionDefault = () => { 44 | const types = [BrowserType.CHROME, BrowserType.OPERA, BrowserType.BRAVE, BrowserType.FIREFOX, BrowserType.SAFARI] 45 | 46 | return types.includes(getBrowserType()) 47 | } 48 | -------------------------------------------------------------------------------- /examples/hallway-rendering-tools/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": false, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /examples/react-app-with-threejs/.env: -------------------------------------------------------------------------------- 1 | REACT_APP_AVATAR_WEBKIT_AUTH_TOKEN=${AVATAR_WEBKIT_AUTH_TOKEN} -------------------------------------------------------------------------------- /examples/react-app-with-threejs/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /examples/react-app-with-threejs/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `yarn start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `yarn test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `yarn build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `yarn eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /examples/react-app-with-threejs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-app-with-threejs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@quarkworks-inc/avatar-webkit": "0.9.3", 7 | "@testing-library/jest-dom": "^5.14.1", 8 | "@testing-library/react": "^12.0.0", 9 | "@testing-library/user-event": "^13.2.1", 10 | "@types/jest": "^27.0.1", 11 | "@types/node": "^16.7.13", 12 | "@types/react": "^17.0.20", 13 | "@types/react-dom": "^17.0.9", 14 | "classnames": "^2.3.1", 15 | "react": "^17.0.2", 16 | "react-dom": "^17.0.2", 17 | "react-scripts": "5.0.0", 18 | "sass": "^1.49.9", 19 | "three": "^0.138.0", 20 | "typescript": "^4.4.2", 21 | "web-vitals": "^2.1.0" 22 | }, 23 | "scripts": { 24 | "start": "react-scripts start", 25 | "build": "react-scripts build", 26 | "test": "react-scripts test", 27 | "eject": "react-scripts eject" 28 | }, 29 | "eslintConfig": { 30 | "extends": [ 31 | "react-app", 32 | "react-app/jest" 33 | ] 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | }, 47 | "devDependencies": { 48 | "@types/classnames": "^2.3.1", 49 | "@types/three": "^0.137.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/react-app-with-threejs/public/backgrounds/venice_sunset_1k.hdr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hallway-Inc/AvatarWebKit/c55a6aa000c46b6b8d7b1b17a288e59316776d5b/examples/react-app-with-threejs/public/backgrounds/venice_sunset_1k.hdr -------------------------------------------------------------------------------- /examples/react-app-with-threejs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hallway-Inc/AvatarWebKit/c55a6aa000c46b6b8d7b1b17a288e59316776d5b/examples/react-app-with-threejs/public/favicon.ico -------------------------------------------------------------------------------- /examples/react-app-with-threejs/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /examples/react-app-with-threejs/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hallway-Inc/AvatarWebKit/c55a6aa000c46b6b8d7b1b17a288e59316776d5b/examples/react-app-with-threejs/public/logo192.png -------------------------------------------------------------------------------- /examples/react-app-with-threejs/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hallway-Inc/AvatarWebKit/c55a6aa000c46b6b8d7b1b17a288e59316776d5b/examples/react-app-with-threejs/public/logo512.png -------------------------------------------------------------------------------- /examples/react-app-with-threejs/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /examples/react-app-with-threejs/public/models/Smiley_eye.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hallway-Inc/AvatarWebKit/c55a6aa000c46b6b8d7b1b17a288e59316776d5b/examples/react-app-with-threejs/public/models/Smiley_eye.glb -------------------------------------------------------------------------------- /examples/react-app-with-threejs/public/models/hannah.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hallway-Inc/AvatarWebKit/c55a6aa000c46b6b8d7b1b17a288e59316776d5b/examples/react-app-with-threejs/public/models/hannah.glb -------------------------------------------------------------------------------- /examples/react-app-with-threejs/public/models/headphones_2.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hallway-Inc/AvatarWebKit/c55a6aa000c46b6b8d7b1b17a288e59316776d5b/examples/react-app-with-threejs/public/models/headphones_2.glb -------------------------------------------------------------------------------- /examples/react-app-with-threejs/public/models/kevin.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hallway-Inc/AvatarWebKit/c55a6aa000c46b6b8d7b1b17a288e59316776d5b/examples/react-app-with-threejs/public/models/kevin.glb -------------------------------------------------------------------------------- /examples/react-app-with-threejs/public/models/mozilla.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hallway-Inc/AvatarWebKit/c55a6aa000c46b6b8d7b1b17a288e59316776d5b/examples/react-app-with-threejs/public/models/mozilla.glb -------------------------------------------------------------------------------- /examples/react-app-with-threejs/public/models/rose.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hallway-Inc/AvatarWebKit/c55a6aa000c46b6b8d7b1b17a288e59316776d5b/examples/react-app-with-threejs/public/models/rose.glb -------------------------------------------------------------------------------- /examples/react-app-with-threejs/public/models/rpm.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hallway-Inc/AvatarWebKit/c55a6aa000c46b6b8d7b1b17a288e59316776d5b/examples/react-app-with-threejs/public/models/rpm.glb -------------------------------------------------------------------------------- /examples/react-app-with-threejs/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /examples/react-app-with-threejs/src/App.css: -------------------------------------------------------------------------------- 1 | .container { 2 | justify-content: space-around; 3 | display: flex; 4 | flex-wrap: wrap; 5 | align-items: center !important; 6 | flex-direction: row; 7 | } 8 | -------------------------------------------------------------------------------- /examples/react-app-with-threejs/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/react-app-with-threejs/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './App.css'; 3 | import AvatarLayout from './avatar/avatarLayout'; 4 | 5 | class App extends React.Component { 6 | render() { 7 | return ( 8 |
9 | 10 |
11 | ) 12 | } 13 | } 14 | 15 | export default App; 16 | -------------------------------------------------------------------------------- /examples/react-app-with-threejs/src/avatar/avatarLayout.module.scss: -------------------------------------------------------------------------------- 1 | .app { 2 | width: 100%; 3 | height: 100%; 4 | background-color: rgba(0, 0, 0, 0); 5 | overflow: hidden; 6 | } 7 | 8 | .container { 9 | align-items: center; 10 | display: flex; 11 | flex-direction: column; 12 | flex-wrap: wrap; 13 | height: 100%; 14 | justify-content: center; 15 | position: relative; 16 | width: 100%; 17 | } 18 | 19 | .videoContainer { 20 | width: 1280px; 21 | height: 720px; 22 | position: relative; 23 | } 24 | 25 | .video { 26 | position: absolute; 27 | top: 16px; 28 | right: 16px; 29 | max-width: 160px; 30 | height: auto; 31 | } 32 | 33 | .inActiveContainer { 34 | position: absolute; 35 | top: 0; 36 | left: 0; 37 | width: 100%; 38 | height: 100%; 39 | display: flex; 40 | flex-direction: column; 41 | justify-content: center; 42 | align-items: center; 43 | } 44 | 45 | .inActiveContainer video { 46 | max-width: 100%; 47 | max-height: 100%; 48 | } 49 | 50 | .loadingContainer { 51 | position: absolute; 52 | top: 0; 53 | left: 0; 54 | width: 100%; 55 | height: 100%; 56 | display: flex; 57 | flex-direction: column; 58 | justify-content: center; 59 | align-items: center; 60 | } 61 | 62 | .buttonContainer { 63 | position: absolute; 64 | width: 100%; 65 | bottom: 10px; 66 | display: flex; 67 | align-items: center; 68 | justify-content: center; 69 | } 70 | 71 | .switchContainer { 72 | position: relative; 73 | } 74 | 75 | .deviceSelectContainer { 76 | position: relative; 77 | margin-left: 20px; 78 | } 79 | 80 | .zStack { 81 | position: relative; 82 | } 83 | 84 | .zStack > * { 85 | position: absolute; 86 | width: 100%; 87 | height: 100%; 88 | } 89 | 90 | @media screen and (max-width: 768px) { 91 | .app { 92 | display: none; 93 | } 94 | 95 | .inActiveContainer { 96 | display: none; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /examples/react-app-with-threejs/src/avatar/avatarLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | // eslint-disable-next-line 4 | import { AUPredictor, AvatarPrediction } from '@quarkworks-inc/avatar-webkit' 5 | 6 | import { Loader } from './components/loader' 7 | import { Switch } from './components/switch' 8 | import { MenuSelect } from './components/menuSelect' 9 | import { RenderLoop } from './world/renderLoop' 10 | import { EnvironmentLoader } from './world/systems/environmentLoader' 11 | import { AvatarWorld } from './world/world' 12 | 13 | import styles from './avatarLayout.module.scss' 14 | import { modelFactory } from './world/models' 15 | 16 | const CAMERA_WIDTH = 640 17 | const CAMERA_HEIGHT = 360 18 | 19 | const AVATAR_WIDTH = 1280 20 | const AVATAR_HEIGHT = 720 21 | 22 | type ComponentState = 'loading' | 'running' | 'paused' 23 | 24 | type Props = {} 25 | type State = { 26 | flipped: boolean 27 | avatarState: ComponentState 28 | videoInDevices: MediaDeviceInfo[] 29 | selectedVideoInDeviceId?: string 30 | } 31 | 32 | class AvatarLayout extends React.Component { 33 | private renderLoop: RenderLoop 34 | private environmentLoader: EnvironmentLoader 35 | private world?: AvatarWorld 36 | 37 | private predictor!: AUPredictor 38 | 39 | private node: HTMLDivElement 40 | private videoRef = React.createRef() 41 | private avatarCanvas = React.createRef() 42 | 43 | state: State = { 44 | flipped: true, 45 | avatarState: 'loading', 46 | videoInDevices: [] 47 | } 48 | 49 | async componentDidMount() { 50 | this.predictor = new AUPredictor({ 51 | apiToken: process.env.REACT_APP_AVATAR_WEBKIT_AUTH_TOKEN, 52 | shouldMirrorOutput: true 53 | }) 54 | 55 | const videoInDevices = await this.fetchVideoDevices() 56 | const selectedVideoInDeviceId = videoInDevices[0]?.deviceId ?? undefined 57 | 58 | this.setState({ videoInDevices, selectedVideoInDeviceId }) 59 | 60 | this.start() 61 | } 62 | 63 | componentWillUnmount(): void { 64 | this.stop() 65 | } 66 | 67 | async start() { 68 | this.setState({ 69 | avatarState: 'loading' 70 | }) 71 | 72 | const videoElement = this.videoRef.current 73 | videoElement.width = CAMERA_WIDTH 74 | videoElement.height = CAMERA_HEIGHT 75 | videoElement.srcObject = this.predictor.stream 76 | videoElement.play() 77 | 78 | this.predictor.dataStream.subscribe(this.updateScene.bind(this)) 79 | 80 | await this._initWorlds() 81 | await this._startAvatar() 82 | } 83 | 84 | async stop() { 85 | this.renderLoop.stop() 86 | this.renderLoop.canvas.remove() 87 | this.predictor.stop() 88 | this.world = undefined 89 | } 90 | 91 | async _initWorlds() { 92 | if (this.world) return 93 | 94 | let avatarCanvas = this.avatarCanvas.current 95 | if (!avatarCanvas) return 96 | 97 | this.renderLoop = new RenderLoop({ canvas: avatarCanvas }) 98 | this.environmentLoader = new EnvironmentLoader(this.renderLoop.webGLRenderer) 99 | 100 | this.world = new AvatarWorld({ 101 | container: avatarCanvas, 102 | environmentLoader: this.environmentLoader 103 | }) 104 | 105 | const model = await modelFactory('emoji') 106 | await this.world.loadScene(model) 107 | 108 | this.renderLoop.updatables.push(this.world) 109 | this.renderLoop.renderables.push(this.world) 110 | 111 | this.renderLoop.start() 112 | } 113 | 114 | private async _startAvatar() { 115 | const { selectedVideoInDeviceId: deviceId } = this.state 116 | 117 | const constraints = { 118 | width: 640, 119 | height: 360, 120 | deviceId: deviceId ? { exact: deviceId } : undefined 121 | } 122 | 123 | const stream = await navigator.mediaDevices.getUserMedia({ video: constraints }) 124 | 125 | await this.predictor.start({ stream }) 126 | 127 | const videoElement = this.videoRef.current 128 | videoElement.srcObject = this.predictor.stream 129 | videoElement.play() 130 | 131 | // Update device list 132 | // We may have just asked for video permission for the first time 133 | const videoInDevices = await this.fetchVideoDevices() 134 | const selectedVideoInDeviceId = stream.getVideoTracks()[0].getSettings().deviceId 135 | 136 | this.setState({ videoInDevices, selectedVideoInDeviceId }) 137 | } 138 | 139 | private async _stopAvatar() { 140 | this.predictor.stop() 141 | } 142 | 143 | _videoInChange(deviceId: string) { 144 | this.setState({ 145 | selectedVideoInDeviceId: deviceId 146 | }, () => { 147 | this._startAvatar() 148 | }) 149 | } 150 | 151 | async fetchVideoDevices() { 152 | const devices = await navigator.mediaDevices.enumerateDevices() 153 | 154 | const videoDevices = [] 155 | devices.forEach(function (device) { 156 | if (device.kind === 'videoinput') { 157 | videoDevices.push(device) 158 | } 159 | }) 160 | 161 | return videoDevices 162 | } 163 | 164 | updateScene(results: AvatarPrediction) { 165 | const { avatarState } = this.state 166 | 167 | // End loading state 168 | if (avatarState !== 'running' && avatarState !== 'paused') { 169 | this.setState({ 170 | avatarState: 'running' 171 | }) 172 | } 173 | 174 | this.world?.updateFromResults(results) 175 | } 176 | 177 | handleToggle = () => { 178 | const newState = this.state.avatarState === 'running' ? 'paused' : 'running' 179 | 180 | newState === 'running' ? this._startAvatar() : this._stopAvatar() 181 | this.setState({ 182 | avatarState: newState 183 | }) 184 | } 185 | 186 | render() { 187 | const { avatarState, videoInDevices } = this.state 188 | 189 | return ( 190 |
{ 193 | if (!this.node) { 194 | this.node = n 195 | } 196 | }} 197 | > 198 |
199 |
200 | 204 |
231 | {avatarState === 'loading' && ( 232 |
233 | 234 |
235 | )} 236 |
237 |
238 | ) 239 | } 240 | } 241 | 242 | export default AvatarLayout 243 | -------------------------------------------------------------------------------- /examples/react-app-with-threejs/src/avatar/components/loader.module.scss: -------------------------------------------------------------------------------- 1 | // @import '../../styles/colors.scss'; 2 | 3 | .loaderContainer { 4 | align-items: center; 5 | display: flex; 6 | flex-direction: column; 7 | justify-content: center; 8 | left: 0; 9 | top: 0; 10 | text-align: center; 11 | } 12 | 13 | .fullSize { 14 | height: 100%; 15 | width: 100%; 16 | } 17 | 18 | .loader { 19 | animation: animate 0.75s 0s infinite linear; 20 | animation-fill-mode: both; 21 | background: transparent !important; 22 | border-radius: 100%; 23 | border: 4px solid; 24 | border-color: blue; 25 | border-bottom-color: transparent; 26 | display: inline-block; 27 | height: 20px; 28 | margin: 0 5px; 29 | width: 20px; 30 | } 31 | 32 | .loaderText { 33 | color: white; 34 | font-size: 32px; 35 | font-weight: 600; 36 | margin: 1rem; 37 | text-align: center; 38 | } 39 | 40 | .version { 41 | color: blue; 42 | font-size: 0.7rem; 43 | margin: 1rem; 44 | text-align: center; 45 | } 46 | 47 | @keyframes animate { 48 | 0% { 49 | transform: rotate(0deg) scale(1); 50 | } 51 | 50% { 52 | transform: rotate(180deg) scale(0.8); 53 | } 54 | 100% { 55 | transform: rotate(360deg) scale(1); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /examples/react-app-with-threejs/src/avatar/components/loader.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames' 2 | import React from 'react' 3 | import { Property } from 'csstype' 4 | 5 | import styles from './loader.module.scss' 6 | 7 | type Props = { 8 | position: 'absolute' | 'relative' 9 | width: number 10 | height: number 11 | color?: Property.Color 12 | thickness?: 1 | 2 13 | subtext?: JSX.Element | string 14 | versionLabel?: string 15 | } 16 | 17 | export class Loader extends React.PureComponent { 18 | render() { 19 | const { position, width, height, color, thickness, subtext, versionLabel } = this.props 20 | return ( 21 |
28 |
37 | {versionLabel &&
{versionLabel}
} 38 | {subtext &&
{subtext}
} 39 |
40 | ) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/react-app-with-threejs/src/avatar/components/menuSelect.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: row; 4 | flex-wrap: nowrap; 5 | align-items: center; 6 | } 7 | 8 | .select { 9 | background-color: white; 10 | border-color: none; 11 | border-radius: 35px; 12 | border: 1px solid; 13 | color: #4a57b9; 14 | cursor: pointer; 15 | font-family: 'Open Sans Semi Bold', sans-serif; 16 | font-size: 14px; 17 | font-weight: 500; 18 | padding: 13px 40px 13px 40px; 19 | position: relative; 20 | text-transform: capitalize; 21 | transition: 0.2s ease; 22 | z-index: 1; 23 | } 24 | 25 | .label { 26 | flex-grow: 0; 27 | margin-left: 10px; 28 | font-size: 0.8rem; 29 | color: #444; 30 | display: none; 31 | } 32 | 33 | .errorMessage { 34 | color: red; 35 | font-size: 0.7rem; 36 | } 37 | -------------------------------------------------------------------------------- /examples/react-app-with-threejs/src/avatar/components/menuSelect.tsx: -------------------------------------------------------------------------------- 1 | import React, { ChangeEvent } from 'react' 2 | 3 | import styles from './menuSelect.module.scss' 4 | 5 | type Props = { 6 | errorMessage?: string 7 | label: string 8 | options: { value: string; label: string }[] 9 | permission?: boolean 10 | value: string 11 | onChange: (value: string) => void 12 | } 13 | 14 | const _onSelectChange = (onChange: (value: string) => void, event: ChangeEvent) => { 15 | event.stopPropagation() 16 | onChange(event.target.value) 17 | } 18 | 19 | export const MenuSelect = (props: Props) => { 20 | const { errorMessage, label, permission = true, onChange, options, value } = props 21 | 22 | return ( 23 |
24 | {!permission ? ( 25 | {errorMessage} 26 | ) : ( 27 | 36 | )} 37 | 38 |
39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /examples/react-app-with-threejs/src/avatar/components/switch.module.scss: -------------------------------------------------------------------------------- 1 | .checkbox { 2 | height: 0; 3 | width: 0; 4 | visibility: hidden; 5 | display: none; 6 | } 7 | 8 | .label { 9 | display: flex; 10 | align-items: center; 11 | justify-content: space-between; 12 | cursor: pointer; 13 | width: 100px; 14 | height: 46px; 15 | background: grey; 16 | border-radius: 100px; 17 | position: relative; 18 | transition: background-color 0.2s; 19 | margin-bottom: 0; 20 | } 21 | 22 | .label .button { 23 | content: ''; 24 | position: absolute; 25 | top: 2px; 26 | left: 2px; 27 | width: 45px; 28 | height: 41px; 29 | border-radius: 41px; 30 | transition: 0.2s; 31 | background: #fff; 32 | box-shadow: 0 0 2px 0 rgba(10, 10, 10, 0.29); 33 | } 34 | 35 | .checkbox:checked + .label .button { 36 | left: calc(100% - 2px); 37 | transform: translateX(-100%); 38 | } 39 | 40 | .label:active .button { 41 | width: 60px; 42 | } 43 | -------------------------------------------------------------------------------- /examples/react-app-with-threejs/src/avatar/components/switch.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import styles from './switch.module.scss' 4 | 5 | type Props = { 6 | isOn: boolean 7 | handleToggle: () => void 8 | onColor: string 9 | } 10 | 11 | export class Switch extends React.Component { 12 | render() { 13 | return ( 14 | <> 15 | 22 | 29 | 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/react-app-with-threejs/src/avatar/utils/browser.ts: -------------------------------------------------------------------------------- 1 | // Source: https://github.com/w-okada/image-analyze-workers/blob/728898121e5062fdfc639c99e8cf2d1e1935fd2e/002_facemesh-worker-js/src/BrowserUtil.ts 2 | 3 | export enum BrowserType { 4 | 'MSIE', 5 | 'EDGE', 6 | 'CHROME', 7 | 'SAFARI', 8 | 'FIREFOX', 9 | 'OPERA', 10 | 'BRAVE', 11 | 'OTHER' 12 | } 13 | 14 | export const getBrowserType = () => { 15 | const userAgent = window.navigator.userAgent.toLowerCase() 16 | 17 | // @ts-expect-error non-supported 18 | if (navigator?.brave) { 19 | return BrowserType.BRAVE 20 | } else if (userAgent.indexOf('msie') !== -1 || userAgent.indexOf('trident') !== -1) { 21 | return BrowserType.MSIE 22 | } else if (userAgent.indexOf('opera') !== -1 || userAgent.indexOf('opr') !== -1) { 23 | return BrowserType.OPERA 24 | } else if (userAgent.indexOf('edge') !== -1) { 25 | return BrowserType.EDGE 26 | } else if (userAgent.indexOf('chrome') !== -1) { 27 | return BrowserType.CHROME 28 | } else if (userAgent.indexOf('safari') !== -1) { 29 | return BrowserType.SAFARI 30 | } else if (userAgent.indexOf('firefox') !== -1) { 31 | return BrowserType.FIREFOX 32 | } else { 33 | return BrowserType.OTHER 34 | } 35 | } 36 | 37 | export const avatarsAvailable = () => { 38 | const types = [BrowserType.CHROME, BrowserType.OPERA, BrowserType.BRAVE, BrowserType.FIREFOX, BrowserType.SAFARI] 39 | 40 | return types.includes(getBrowserType()) 41 | } 42 | 43 | export const gestureDetectionDefault = () => { 44 | const types = [BrowserType.CHROME, BrowserType.OPERA, BrowserType.BRAVE, BrowserType.FIREFOX, BrowserType.SAFARI] 45 | 46 | return types.includes(getBrowserType()) 47 | } 48 | -------------------------------------------------------------------------------- /examples/react-app-with-threejs/src/avatar/world/components/camera.ts: -------------------------------------------------------------------------------- 1 | import { PerspectiveCamera } from 'three' 2 | 3 | export const createCamera = (zoom = 3.75) => { 4 | const camera = new PerspectiveCamera(50, 1, 0.1, 100) 5 | 6 | camera.position.set(0, 0, zoom) 7 | 8 | return camera 9 | } 10 | -------------------------------------------------------------------------------- /examples/react-app-with-threejs/src/avatar/world/components/lights.ts: -------------------------------------------------------------------------------- 1 | import { DirectionalLight, HemisphereLight, AmbientLight } from 'three' 2 | 3 | function createLights() { 4 | const hemisphereLight = new HemisphereLight() 5 | const ambientLight = new AmbientLight(0xffffff, 0.1) 6 | 7 | const mainLight = new DirectionalLight(0xffffff, 0.2) 8 | mainLight.position.set(0.5, 0, 0.866) 9 | 10 | return { hemisphereLight, ambientLight, mainLight } 11 | } 12 | 13 | export { createLights } 14 | -------------------------------------------------------------------------------- /examples/react-app-with-threejs/src/avatar/world/components/scene.ts: -------------------------------------------------------------------------------- 1 | import { Color, Scene } from 'three' 2 | 3 | function createScene() { 4 | const scene = new Scene() 5 | 6 | scene.background = new Color('skyblue') 7 | 8 | return scene 9 | } 10 | 11 | export { createScene } 12 | -------------------------------------------------------------------------------- /examples/react-app-with-threejs/src/avatar/world/globalCanvas.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates a shared canvas across the entire screen that is rendered at a Z-depth below 3 | * everything else in the document body. 4 | * 5 | * This allows us to share one rendering context between multiple avatars. This is useful for 6 | * applications such as a video calls where you want to show multiple distinct "scenes", 7 | * all rendered client-side. 8 | */ 9 | export const tileGlobalCanvasId = 'hallway-tile-global-canvas' 10 | 11 | export const createGlobalCanvas = (zIndex: number = -1, prependToBody: boolean = true): HTMLCanvasElement => { 12 | const oldCanvas = document.getElementById(tileGlobalCanvasId) 13 | if (oldCanvas) { 14 | oldCanvas.remove() 15 | } 16 | 17 | const canvas = document.createElement('canvas') as HTMLCanvasElement 18 | canvas.setAttribute('id', 'hallway-tile-global-canvas') 19 | canvas.style.position = 'absolute' 20 | canvas.style.top = '0' 21 | canvas.style.left = '0' 22 | canvas.style.width = '100%' 23 | canvas.style.height = '100%' 24 | canvas.style.zIndex = `${zIndex}` 25 | 26 | if (prependToBody) { 27 | document.body.prepend(canvas) 28 | } 29 | 30 | return canvas 31 | } 32 | 33 | export const isGlobalCanvas = (canvas: HTMLCanvasElement) => canvas.id === tileGlobalCanvasId -------------------------------------------------------------------------------- /examples/react-app-with-threejs/src/avatar/world/models/emoji.ts: -------------------------------------------------------------------------------- 1 | import { AvatarPrediction, BlendShapeKeys, BlendShapes } from "@quarkworks-inc/avatar-webkit"; 2 | import { Group, Scene, Mesh, MeshStandardMaterial, MathUtils } from "three"; 3 | import { Model, ModelType } from "."; 4 | import { loadModel } from "../systems/loadModel"; 5 | 6 | const Y_OFFSET = -0.55 7 | 8 | export class EmojiModel implements Model { 9 | readonly type: ModelType = 'emoji' 10 | 11 | // Groups 12 | private model: Group 13 | private headphones?: Group 14 | 15 | // Model mesh components 16 | private face: Mesh 17 | private mouth: Mesh 18 | private tongue: Mesh 19 | private teeth: Mesh 20 | private rightEye: Mesh 21 | private leftEye: Mesh 22 | 23 | // TODO: fix dis 24 | private isMe = true 25 | 26 | static async init(): Promise { 27 | const model = new EmojiModel() 28 | return model.load() 29 | } 30 | 31 | private constructor() {} 32 | 33 | private async load(): Promise { 34 | this.model = await loadModel('models/Smiley_eye.glb') 35 | this.headphones = await loadModel('models/headphones_2.glb') 36 | 37 | this.model.add(this.headphones) 38 | 39 | this.model.position.y = Y_OFFSET 40 | 41 | this.leftEye = this.model.children[0] as Mesh 42 | this.rightEye = this.model.children[1] as Mesh 43 | const smileyGroup = this.model.children[2] 44 | 45 | this.face = smileyGroup.children[0] as Mesh 46 | this.mouth = smileyGroup.children[1] as Mesh 47 | this.tongue = smileyGroup.children[2] as Mesh 48 | this.teeth = smileyGroup.children[3] as Mesh 49 | 50 | if (this.face.material instanceof MeshStandardMaterial) { 51 | this.face.material.metalness = 0.1 52 | this.face.material.roughness = 0.5 53 | this.face.material.needsUpdate = true 54 | } 55 | 56 | return this 57 | } 58 | 59 | addToScene(scene: Scene) { 60 | scene.add(this.model) 61 | } 62 | 63 | removeFromScene(scene: Scene) { 64 | scene.remove(this.model) 65 | } 66 | 67 | getPosition = () => this.model.position 68 | 69 | updateFromResults(results: AvatarPrediction) { 70 | if (!this.model) return 71 | 72 | this.updateBlendShapes(results.blendShapes) 73 | this.updateHeadRotation(-results.rotation.pitch, -results.rotation.yaw, -results.rotation.roll) 74 | this.updatePosition(results.transform.x, results.transform.y, results.transform.z) 75 | } 76 | 77 | private updateBlendShapes(blendShapes: BlendShapes) { 78 | if (!this.face) return 79 | 80 | for (const key in blendShapes) { 81 | let value = blendShapes[key] 82 | 83 | if (key === BlendShapeKeys.browDown_L || key === BlendShapeKeys.browDown_R) { 84 | value = Math.min(Math.max(value - 0.0, 0), 1) 85 | } 86 | 87 | const morphIndex = this.face.morphTargetDictionary[key] 88 | 89 | this.face.morphTargetInfluences[morphIndex] = value 90 | this.mouth.morphTargetInfluences[morphIndex] = value 91 | this.teeth.morphTargetInfluences[morphIndex] = value 92 | this.tongue.morphTargetInfluences[morphIndex] = value 93 | } 94 | 95 | const eulerRight = [ 96 | blendShapes.eyeLookDown_L + -blendShapes.eyeLookUp_L, 97 | blendShapes.eyeLookOut_L + -blendShapes.eyeLookIn_L, 98 | 0.0 99 | ] 100 | const eulerLeft = [ 101 | blendShapes.eyeLookDown_R + -blendShapes.eyeLookUp_R, 102 | -blendShapes.eyeLookOut_R + blendShapes.eyeLookIn_R, 103 | 0.0 104 | ] 105 | const maxAngle = (1 / 57.3) * 30 106 | 107 | this.rightEye.rotation.x = eulerRight[0] * maxAngle 108 | this.rightEye.rotation.y = eulerRight[1] * maxAngle 109 | this.rightEye.rotation.z = eulerRight[2] * maxAngle 110 | 111 | this.leftEye.rotation.x = eulerLeft[0] * maxAngle 112 | this.leftEye.rotation.y = eulerLeft[1] * maxAngle 113 | this.leftEye.rotation.z = eulerLeft[2] * maxAngle 114 | } 115 | 116 | updateEyeGaze(value: number) { 117 | this.leftEye.rotation.y = -MathUtils.degToRad(value) 118 | this.rightEye.rotation.y = -MathUtils.degToRad(value) 119 | } 120 | 121 | updateHeadRotation(pitch: number, yaw: number, roll: number) { 122 | if (!this.model) return 123 | 124 | this.model.rotation.x = pitch 125 | 126 | // Inverse yaw & roll effects for yourself to give mirror effect 127 | this.model.rotation.y = this.isMe ? -yaw : yaw 128 | this.model.rotation.z = this.isMe ? roll : -roll 129 | } 130 | 131 | updatePosition(x: number, y: number, z: number) { 132 | this.model.position.x = x 133 | this.model.position.y = y 134 | // this.head.position.z = z 135 | } 136 | } -------------------------------------------------------------------------------- /examples/react-app-with-threejs/src/avatar/world/models/index.ts: -------------------------------------------------------------------------------- 1 | import { AvatarPrediction } from "@quarkworks-inc/avatar-webkit"; 2 | import { Mesh, Object3D, Scene, Vector3 } from "three"; 3 | import { EmojiModel } from "./emoji"; 4 | import { MozillaModel } from "./mozilla"; 5 | import { ReadyPlayerMeModel } from "./readyPlayerMe"; 6 | 7 | export interface Model { 8 | readonly type: ModelType 9 | addToScene(scene: Scene): void 10 | removeFromScene(scene: Scene): void 11 | getPosition(): Vector3 12 | updateFromResults(results: AvatarPrediction): void 13 | } 14 | 15 | export type ModelType = 'emoji' | 'readyPlayerMe' | 'mozilla' 16 | 17 | export const modelFactory = (type: ModelType, url?: string): Promise => { 18 | switch (type) { 19 | case 'emoji': return EmojiModel.init() 20 | case 'readyPlayerMe': return ReadyPlayerMeModel.init(url) 21 | case 'mozilla': return MozillaModel.init(url) 22 | default: return Promise.reject(`Unknown model type: ${type}`) 23 | } 24 | } 25 | 26 | export const object3DChildNamed = (object: Object3D, name: string) => object.children.find((child) => child.name === name) 27 | 28 | export const setMorphTarget = (mesh: Mesh | undefined, key: string, value: any) => { 29 | if (!mesh) return 30 | let idx = mesh.morphTargetDictionary[key] 31 | if (!idx) return 32 | mesh.morphTargetInfluences[idx] = value 33 | } 34 | -------------------------------------------------------------------------------- /examples/react-app-with-threejs/src/avatar/world/models/mozilla.ts: -------------------------------------------------------------------------------- 1 | import { AvatarPrediction, BlendShapes } from "@quarkworks-inc/avatar-webkit"; 2 | import { Group, SkinnedMesh, Scene } from "three"; 3 | import { Model, ModelType, object3DChildNamed, setMorphTarget } from "."; 4 | import { loadModel } from "../systems/loadModel"; 5 | 6 | const Y_OFFSET = -0.55 7 | 8 | export class MozillaModel implements Model { 9 | readonly type: ModelType = 'mozilla' 10 | 11 | private model: Group 12 | private combinedMesh: SkinnedMesh 13 | 14 | static async init(url: string): Promise { 15 | const model = new MozillaModel() 16 | return model.load(url) 17 | } 18 | 19 | private constructor() {} 20 | 21 | private async load(url: string): Promise { 22 | this.model = await loadModel(url) 23 | 24 | this.model.position.y = Y_OFFSET 25 | 26 | const object = this.model.children[0] 27 | this.combinedMesh = object3DChildNamed(object, "CombinedMesh") as SkinnedMesh 28 | 29 | return this 30 | } 31 | 32 | addToScene(scene: Scene) { 33 | scene.add(this.model) 34 | } 35 | 36 | removeFromScene(scene: Scene) { 37 | scene.remove(this.model) 38 | } 39 | 40 | getPosition = () => this.model.position 41 | 42 | updateFromResults(results: AvatarPrediction) { 43 | if (!this.model) return 44 | 45 | this.updateBlendShapes(results.blendShapes) 46 | this.updateHeadRotation(-results.rotation.pitch, -results.rotation.yaw, -results.rotation.roll) 47 | this.updatePosition(results.transform.x, results.transform.y, results.transform.z) 48 | } 49 | 50 | private updateBlendShapes(blendShapes: BlendShapes) { 51 | 52 | const blink = (blendShapes.eyeBlink_L + blendShapes.eyeBlink_R) / 2 53 | setMorphTarget(this.combinedMesh, 'Blink', blink) 54 | 55 | const eyeRotation = (blendShapes.browDown_L + blendShapes.browDown_R) / 2 56 | setMorphTarget(this.combinedMesh, 'Eye Rotation', eyeRotation) 57 | 58 | setMorphTarget(this.combinedMesh, 'MouthFlap', blendShapes.jawOpen) 59 | } 60 | 61 | private updateHeadRotation(pitch: number, yaw: number, roll: number) { 62 | 63 | // this.head.rotation.x = pitch 64 | 65 | // // Inverse yaw & roll effects for yourself to give mirror effect 66 | // this.head.rotation.y = this.isMe ? -yaw : yaw 67 | // this.head.rotation.z = this.isMe ? roll : -roll 68 | } 69 | 70 | private updatePosition(x: number, y: number, z: number) { 71 | // this.head.position.x = x 72 | // this.head.position.y = Y_OFFSET + y 73 | // this.head.position.z = z 74 | } 75 | } -------------------------------------------------------------------------------- /examples/react-app-with-threejs/src/avatar/world/models/readyPlayerMe.ts: -------------------------------------------------------------------------------- 1 | import { AvatarPrediction, BlendShapeKeys, BlendShapes } from "@quarkworks-inc/avatar-webkit" 2 | import { Group, Object3D, Scene, SkinnedMesh } from "three" 3 | import { Model, ModelType, object3DChildNamed, setMorphTarget } from "." 4 | import { loadModel } from "../systems/loadModel" 5 | 6 | const Y_OFFSET = -0.55 7 | 8 | export class ReadyPlayerMeModel implements Model { 9 | readonly type: ModelType = 'readyPlayerMe' 10 | 11 | // Model group 12 | private model: Group 13 | private avatarRoot: Object3D 14 | 15 | // Nodes for current RPM version 16 | private faceNode?: SkinnedMesh 17 | private teethNode?: SkinnedMesh 18 | 19 | // Nodes for old RPM versions 20 | private wolf3D_Avatar?: SkinnedMesh 21 | 22 | static async init(url: string): Promise { 23 | const model = new ReadyPlayerMeModel() 24 | return model.load(url) 25 | } 26 | 27 | private constructor() {} 28 | 29 | private async load(url: string): Promise { 30 | this.model = await loadModel(url) 31 | 32 | this.model.position.y = Y_OFFSET 33 | 34 | this.avatarRoot = object3DChildNamed(this.model, "AvatarRoot") 35 | 36 | this.faceNode = object3DChildNamed(this.avatarRoot, "Wolf3D_Head") as SkinnedMesh 37 | this.teethNode = object3DChildNamed(this.avatarRoot, "Wolf3D_Teeth") as SkinnedMesh 38 | 39 | this.wolf3D_Avatar = object3DChildNamed(this.avatarRoot, "Wolf3D_Avatar") as SkinnedMesh 40 | 41 | return this 42 | } 43 | 44 | addToScene(scene: Scene) { 45 | scene.add(this.model) 46 | } 47 | 48 | removeFromScene(scene: Scene) { 49 | scene.remove(this.model) 50 | } 51 | 52 | getPosition = () => this.model.position 53 | 54 | updateFromResults(results: AvatarPrediction) { 55 | if (!this.model) return 56 | 57 | this.updateBlendShapes(results.blendShapes) 58 | this.updateHeadRotation(-results.rotation.pitch, -results.rotation.yaw, -results.rotation.roll) 59 | this.updatePosition(results.transform.x, results.transform.y, results.transform.z) 60 | } 61 | 62 | private updateBlendShapes(blendShapes: BlendShapes) { 63 | for (const key in blendShapes) { 64 | const value = blendShapes[key] 65 | 66 | const arKitKey = BlendShapeKeys.toARKitConvention(key) 67 | 68 | setMorphTarget(this.faceNode, arKitKey, value) 69 | setMorphTarget(this.teethNode, arKitKey, value) 70 | setMorphTarget(this.wolf3D_Avatar, arKitKey, value) 71 | } 72 | } 73 | 74 | private updateHeadRotation(pitch: number, yaw: number, roll: number) { 75 | 76 | // this.head.rotation.x = pitch 77 | 78 | // // Inverse yaw & roll effects for yourself to give mirror effect 79 | // this.head.rotation.y = this.isMe ? -yaw : yaw 80 | // this.head.rotation.z = this.isMe ? roll : -roll 81 | } 82 | 83 | private updatePosition(x: number, y: number, z: number) { 84 | // this.head.position.x = x 85 | // this.head.position.y = Y_OFFSET + y 86 | // this.head.position.z = z 87 | } 88 | } -------------------------------------------------------------------------------- /examples/react-app-with-threejs/src/avatar/world/renderLoop.ts: -------------------------------------------------------------------------------- 1 | import { Clock, WebGLRenderer } from 'three' 2 | import { isGlobalCanvas } from './globalCanvas' 3 | import { createDefaultWebGLRenderer } from './systems/webGLRenderer' 4 | 5 | const clock = new Clock() 6 | 7 | export interface Updateable { 8 | tick(delta: number): void 9 | } 10 | 11 | export interface Renderable { 12 | render(renderer: WebGLRenderer): void 13 | getContainerRect(): DOMRect 14 | } 15 | 16 | export class RenderLoop { 17 | webGLRenderer: WebGLRenderer 18 | canvas: HTMLCanvasElement 19 | drawWhenOffscreen: boolean 20 | isRunning: boolean = false 21 | 22 | updatables: Updateable[] = [] 23 | renderables: Renderable[] = [] 24 | 25 | constructor({ 26 | canvas, 27 | webGLRenderer, 28 | } : { 29 | canvas: HTMLCanvasElement, 30 | webGLRenderer?: WebGLRenderer 31 | }) { 32 | this.canvas = canvas 33 | this.webGLRenderer = webGLRenderer || createDefaultWebGLRenderer(canvas) 34 | } 35 | 36 | start() { 37 | this.isRunning = true 38 | this.webGLRenderer.setAnimationLoop(() => { 39 | // tell every animated object to tick forward one frame 40 | const delta = clock.getDelta() 41 | for (const updatable of this.updatables) { 42 | updatable.tick(delta) 43 | } 44 | 45 | // translate to match scroll on page 46 | if (isGlobalCanvas(this.canvas)) { 47 | this.canvas.style.transform = `translate(${window.scrollX}px, ${window.scrollY}px)` 48 | } 49 | 50 | // Clear canvas 51 | this.webGLRenderer.setClearColor(0xffffff) 52 | this.webGLRenderer.clear() 53 | 54 | // Draw things 55 | for (const renderable of this.renderables) { 56 | this._withScissoredViewport(renderable, () => { 57 | renderable.render(this.webGLRenderer) 58 | }) 59 | } 60 | }) 61 | } 62 | 63 | private _isRenderableOffscreen(renderable: Renderable): boolean { 64 | const { top, left, bottom, right } = renderable.getContainerRect() 65 | 66 | return bottom < 0 || // above 67 | top > this.canvas.clientHeight || // below 68 | right < 0 || // left 69 | left > this.canvas.clientWidth // right 70 | } 71 | 72 | private _withScissoredViewport(renderable: Renderable, render: () => void) { 73 | if (!isGlobalCanvas(this.canvas)) { 74 | render() 75 | return 76 | } 77 | 78 | // For global canvas, we do some optimization to skip item drawing when offscreen 79 | if (this._isRenderableOffscreen(renderable)) { 80 | return 81 | } 82 | 83 | // For global canvas, we use scissoring technique to draw scenes to different locations on the canvas 84 | this.webGLRenderer.setScissorTest(true) 85 | 86 | const { width, height, left, bottom } = renderable.getContainerRect() 87 | 88 | const xPos = left 89 | const yPos = this.canvas.clientHeight - bottom 90 | 91 | this.webGLRenderer.setViewport(xPos, yPos, width, height) 92 | this.webGLRenderer.setScissor(xPos, yPos, width, height) 93 | 94 | render() 95 | 96 | this.webGLRenderer.setScissorTest(false) 97 | } 98 | 99 | stop() { 100 | this.isRunning = false 101 | this.webGLRenderer.setAnimationLoop(null) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /examples/react-app-with-threejs/src/avatar/world/systems/controls.ts: -------------------------------------------------------------------------------- 1 | import { PerspectiveCamera } from 'three' 2 | 3 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' 4 | 5 | import { Updateable } from '../renderLoop' 6 | 7 | export class UpdateableControls extends OrbitControls implements Updateable { 8 | tick(_delta: number): void { 9 | this.update() 10 | } 11 | } 12 | 13 | export const createControls = (camera: PerspectiveCamera, domElement?: HTMLElement) => { 14 | const controls = new UpdateableControls(camera, domElement) 15 | 16 | controls.enableDamping = true 17 | //controls.enableRotate = true 18 | 19 | return controls 20 | } 21 | -------------------------------------------------------------------------------- /examples/react-app-with-threejs/src/avatar/world/systems/environmentLoader.ts: -------------------------------------------------------------------------------- 1 | import { PMREMGenerator, Texture, WebGLRenderer } from "three"; 2 | import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader"; 3 | 4 | export class EnvironmentLoader { 5 | private pmremGenerator: PMREMGenerator 6 | 7 | constructor(renderer: WebGLRenderer) { 8 | this.pmremGenerator = new PMREMGenerator(renderer) 9 | this.pmremGenerator.compileEquirectangularShader() 10 | } 11 | 12 | load(url: string): Promise { 13 | // no envmap 14 | if (!url) return Promise.resolve(null) 15 | 16 | return new Promise((resolve, reject) => { 17 | new RGBELoader().load( 18 | url, 19 | texture => { 20 | const envMap = this.pmremGenerator.fromEquirectangular(texture).texture 21 | this.pmremGenerator.dispose() 22 | 23 | resolve(envMap) 24 | }, 25 | undefined, 26 | reject 27 | ) 28 | }) 29 | } 30 | } -------------------------------------------------------------------------------- /examples/react-app-with-threejs/src/avatar/world/systems/loadModel.ts: -------------------------------------------------------------------------------- 1 | import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader' 2 | 3 | async function loadModel(url: string) { 4 | const loader = new GLTFLoader() 5 | 6 | const [emojiData] = await Promise.all([loader.loadAsync(url)]) 7 | console.log(emojiData) 8 | const emoji = emojiData.scene 9 | emoji.position.set(0, 0, 0) 10 | 11 | return emoji 12 | } 13 | 14 | export { loadModel } 15 | -------------------------------------------------------------------------------- /examples/react-app-with-threejs/src/avatar/world/systems/webGLRenderer.ts: -------------------------------------------------------------------------------- 1 | import { WebGLRenderer, LinearToneMapping, sRGBEncoding } from "three" 2 | 3 | export const createDefaultWebGLRenderer = (canvas: HTMLCanvasElement) => { 4 | 5 | const renderer = new WebGLRenderer({ 6 | alpha: true, 7 | antialias: true, 8 | canvas: canvas, 9 | preserveDrawingBuffer: true 10 | }) 11 | 12 | renderer.setClearColor(0xffffff, 0) 13 | renderer.setSize(canvas.clientWidth, canvas.clientHeight) 14 | renderer.setPixelRatio(window.devicePixelRatio) 15 | 16 | renderer.physicallyCorrectLights = true 17 | renderer.toneMapping = LinearToneMapping 18 | renderer.toneMappingExposure = 0.8 19 | renderer.outputEncoding = sRGBEncoding 20 | 21 | return renderer 22 | } -------------------------------------------------------------------------------- /examples/react-app-with-threejs/src/avatar/world/world.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Color, 3 | PerspectiveCamera, 4 | Scene, 5 | WebGLRenderer, 6 | } from 'three' 7 | 8 | // eslint-disable-next-line 9 | import { AvatarPrediction } from '@quarkworks-inc/avatar-webkit' 10 | 11 | import { createCamera } from './components/camera' 12 | import { createLights } from './components/lights' 13 | import { createScene } from './components/scene' 14 | import { createControls, UpdateableControls } from './systems/controls' 15 | import { Renderable, Updateable } from './renderLoop' 16 | import { EnvironmentLoader } from './systems/environmentLoader' 17 | import { Model } from './models' 18 | 19 | const sceneBackgroundColor = new Color(0xffffff) 20 | 21 | export class AvatarWorld implements Updateable, Renderable { 22 | private container: HTMLElement 23 | private isMe: boolean 24 | private enableControls: boolean 25 | private environmentLoader: EnvironmentLoader 26 | 27 | private scene: Scene 28 | private camera: PerspectiveCamera 29 | private controls: UpdateableControls 30 | 31 | private controlsEnabled = true 32 | 33 | private model?: Model 34 | 35 | constructor({ 36 | container, 37 | isMe, 38 | enableControls, 39 | environmentLoader, 40 | }: { 41 | container: HTMLElement 42 | isMe?: boolean 43 | enableControls?: boolean 44 | environmentLoader: EnvironmentLoader 45 | }) { 46 | this.container = container 47 | this.isMe = isMe ?? true 48 | this.enableControls = enableControls ?? false 49 | this.environmentLoader = environmentLoader 50 | 51 | this.camera = createCamera() 52 | this.scene = createScene() 53 | this.scene.background = sceneBackgroundColor 54 | 55 | this.scene.rotateY((Math.PI / 180) * 0) 56 | 57 | this.controls = createControls(this.camera, this.container) 58 | this.controls.enabled = this.enableControls 59 | 60 | const { hemisphereLight, ambientLight, mainLight } = createLights() 61 | this.scene.add(hemisphereLight) 62 | this.camera.add(ambientLight, mainLight) 63 | 64 | this.scene.add(this.camera) 65 | 66 | this.resize() 67 | } 68 | 69 | async loadScene(model: Model) { 70 | this.model = model 71 | this.model.addToScene(this.scene) 72 | 73 | this.camera.position.x = 0 74 | this.camera.position.z = model.type === 'emoji' ? 1 : 0.6 75 | this.camera.position.y = 0 76 | 77 | this.environmentLoader.load('backgrounds/venice_sunset_1k.hdr').then(envMap => { 78 | this.scene.environment = envMap 79 | this.scene.background = envMap 80 | }) 81 | } 82 | 83 | cleanUp() { 84 | this.setAvatarEnabled(false) 85 | this.model = undefined 86 | } 87 | 88 | setAvatarEnabled(enabled: boolean) { 89 | if (enabled) { 90 | this.model?.addToScene(this.scene) 91 | } else { 92 | this.model?.removeFromScene(this.scene) 93 | } 94 | } 95 | 96 | resize() { 97 | if (!this.camera) return 98 | 99 | this.camera.aspect = this.container.clientWidth / this.container.clientHeight 100 | this.camera.updateProjectionMatrix() 101 | } 102 | 103 | tick(delta: number): void { 104 | this.controls.tick(delta) 105 | } 106 | 107 | getContainerRect(): DOMRect { 108 | return this.container.getBoundingClientRect() 109 | } 110 | 111 | render(renderer: WebGLRenderer): void { 112 | renderer.render(this.scene, this.camera) 113 | } 114 | 115 | updateFromResults(results: AvatarPrediction) { 116 | this.model?.updateFromResults(results) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /examples/react-app-with-threejs/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /examples/react-app-with-threejs/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /examples/react-app-with-threejs/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/react-app-with-threejs/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/react-app-with-threejs/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /examples/react-app-with-threejs/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /examples/react-app-with-threejs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": false, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /examples/ready-player-me-tutorials/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /examples/ready-player-me-tutorials/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!** 35 | 36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. 39 | 40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /examples/ready-player-me-tutorials/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ready-player-me-tutorials", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@quarkworks-inc/avatar-webkit": "0.9.3", 7 | "@testing-library/jest-dom": "^5.16.3", 8 | "@testing-library/react": "^12.1.4", 9 | "@testing-library/user-event": "^13.5.0", 10 | "react": "^18.0.0", 11 | "react-dom": "^18.0.0", 12 | "react-scripts": "5.0.0", 13 | "three": "^0.139.2", 14 | "web-vitals": "^2.1.4" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": [ 24 | "react-app", 25 | "react-app/jest" 26 | ] 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/ready-player-me-tutorials/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hallway-Inc/AvatarWebKit/c55a6aa000c46b6b8d7b1b17a288e59316776d5b/examples/ready-player-me-tutorials/public/favicon.ico -------------------------------------------------------------------------------- /examples/ready-player-me-tutorials/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /examples/ready-player-me-tutorials/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hallway-Inc/AvatarWebKit/c55a6aa000c46b6b8d7b1b17a288e59316776d5b/examples/ready-player-me-tutorials/public/logo192.png -------------------------------------------------------------------------------- /examples/ready-player-me-tutorials/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hallway-Inc/AvatarWebKit/c55a6aa000c46b6b8d7b1b17a288e59316776d5b/examples/ready-player-me-tutorials/public/logo512.png -------------------------------------------------------------------------------- /examples/ready-player-me-tutorials/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /examples/ready-player-me-tutorials/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /examples/ready-player-me-tutorials/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | 40 | .topBar { 41 | align-items: center; 42 | border-bottom: 2px solid #333; 43 | display: flex; 44 | height: 98px; 45 | justify-content: center; 46 | } 47 | 48 | .toggleButton { 49 | margin: 1rem; 50 | } 51 | 52 | .iFrame { 53 | border: none; 54 | width: 100%; 55 | height: calc(100vh - 100px); 56 | } 57 | -------------------------------------------------------------------------------- /examples/ready-player-me-tutorials/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react'; 2 | import './App.css'; 3 | import { AvatarView } from './AvatarView'; 4 | 5 | function App() { 6 | const subdomain = 'hallway' 7 | const iFrameRef = useRef(null) 8 | const [avatarUrl, setAvatarUrl] = useState('') 9 | const [showIFrame, setShowIFrame] = useState(true) 10 | const [predicting, setPredicting] = useState(false) 11 | 12 | useEffect(() => { 13 | let iFrame = iFrameRef.current 14 | if(iFrame) { 15 | iFrame.src = `https://${subdomain}.readyplayer.me/avatar?frameApi` 16 | } 17 | }) 18 | 19 | useEffect(() => { 20 | window.addEventListener('message', subscribe) 21 | document.addEventListener('message', subscribe) 22 | 23 | return () => { 24 | window.removeEventListener('message', subscribe) 25 | document.removeEventListener('message', subscribe) 26 | } 27 | }); 28 | 29 | function subscribe(event) { 30 | const json = parse(event) 31 | 32 | if (json?.source !== 'readyplayerme') { 33 | return; 34 | } 35 | 36 | // Subscribe to all events sent from Ready Player Me once frame is ready 37 | if (json.eventName === 'v1.frame.ready') { 38 | let iFrame = iFrameRef.current 39 | if(iFrame && iFrame.contentWindow) { 40 | iFrame.contentWindow.postMessage( 41 | JSON.stringify({ 42 | target: 'readyplayerme', 43 | type: 'subscribe', 44 | eventName: 'v1.**' 45 | }), 46 | '*' 47 | ); 48 | } 49 | } 50 | 51 | // Get avatar GLB URL 52 | if (json.eventName === 'v1.avatar.exported') { 53 | console.log(`Avatar URL: ${json.data.url}`); 54 | setAvatarUrl(json.data.url) 55 | setShowIFrame(false); 56 | setPredicting(true) 57 | } 58 | 59 | // Get user id 60 | if (json.eventName === 'v1.user.set') { 61 | console.log(`User with id ${json.data.id} set: ${JSON.stringify(json)}`); 62 | } 63 | } 64 | 65 | function parse(event) { 66 | try { 67 | return JSON.parse(event.data); 68 | } catch (error) { 69 | return null; 70 | } 71 | } 72 | 73 | return ( 74 |
75 |
76 | setShowIFrame(!showIFrame)} 79 | type="button" 80 | value={`${showIFrame ? 'Close': 'Open'} creator`} 81 | /> 82 | {avatarUrl && 83 | setPredicting(!predicting)} 86 | type="button" 87 | value={`${predicting ? 'Stop': 'Start'} predicting`} 88 | /> 89 | } 90 |

Avatar URL: {avatarUrl}

91 |
92 | {avatarUrl && } 93 |