diff --git a/app/components/Input.tsx b/app/components/Input.tsx index eff8cbab3a..a8ce2232f9 100644 --- a/app/components/Input.tsx +++ b/app/components/Input.tsx @@ -1,20 +1,34 @@ -import React from "react"; +import { twMerge } from "tailwind-merge"; +import { IconDefinition } from "@fortawesome/pro-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Label } from "./Typography"; interface InputProps extends React.InputHTMLAttributes { label?: string; className?: string; + icon?: IconDefinition } -export function Input({ label, className, ...rest }: InputProps) { +export function Input({label, icon, className, ...rest }: InputProps) { return ( -
+
{label && } - +
+ {icon && ( + + )} + +
); } diff --git a/app/components/InputVector.tsx b/app/components/InputVector.tsx index 51604f05b9..0a10274524 100644 --- a/app/components/InputVector.tsx +++ b/app/components/InputVector.tsx @@ -37,6 +37,7 @@ export const InputVector = ({ x, y, z, onChange }: InputVectorProps) => { > { > { > ( -

- {children} -

+

{children}

); export const H2 = ({ className, children }: TypoProps) => ( -

- {" "} - {children}{" "} -

+

{children}

); export const H3 = ({ className, children }: TypoProps) => ( -

- {" "} - {children}{" "} -

+

{children}

); export const H4 = ({ className, children }: TypoProps) => ( -

- {" "} - {children}{" "} -

+

{children}

); export const Label = ({ className, children }: TypoProps) => ( - + ); export const H5 = ({ className, children }: TypoProps) => ( -
- {" "} - {children}{" "} -
+
{children}
); export const H6 = ({ className, children }: TypoProps) => ( -
- {" "} - {children}{" "} -
+
{children}
); +export const Link = ({className, ...rest}:NavLinkProps)=> + + export const P = ({ className, children }: TypoProps) => (

{children}

); diff --git a/app/components/index.tsx b/app/components/index.tsx index d3c627d58c..8b3be492ec 100644 --- a/app/components/index.tsx +++ b/app/components/index.tsx @@ -4,7 +4,7 @@ import { ButtonLink } from "./ButtonLink"; import { ButtonIconSelect } from "./ButtonIconSelect"; import { Input } from "./Input"; import { InputVector } from "./InputVector"; -import { H1, H2, H3, H4, H5, H6, P, Label} from "./Typography"; +import { H1, H2, H3, H4, H5, H6, P, Label, Link} from "./Typography"; import { ItemPicker } from "./ItemPicker"; import { Textarea } from "./Textarea"; import { LoadingBar } from "./LoadingBar"; @@ -15,7 +15,7 @@ export { ButtonIcon, ButtonLink, ButtonIconSelect, - H1,H2,H3,H4,H5,H6,P,Label, + H1,H2,H3,H4,H5,H6,P,Label,Link, ItemPicker, Textarea, Input, diff --git a/app/contexts/Authentication/Authentication.tsx b/app/contexts/Authentication/Authentication.tsx new file mode 100644 index 0000000000..27d0d7594f --- /dev/null +++ b/app/contexts/Authentication/Authentication.tsx @@ -0,0 +1,98 @@ +import { + createContext, + useEffect, + useCallback, + useState, + ReactNode, + Dispatch, + SetStateAction +} from "react"; +import { useCookies } from 'react-cookie'; + +import { + SessionResponse, + AuthState, +} from "./types"; + +import { CreateSession, DestroySession, GetSession } from "./utilities"; + +export const AuthenticationContext = createContext<{ + authState?: AuthState, + setAuthState?: Dispatch>, + persistLoginOrOut?: ()=>void, + loginAndGetUserInfo?: ( + usernameOrEmail: string, + password:string, + failureCallback?:()=>void + )=>void; + logout?: ()=>void; +}>({}); + +export const AuthenticationProvider = ({children}:{children:ReactNode})=>{ + const [authCookies, setAuthCookie, removeAuthCookie] = useCookies(['userInfo']); + const [authState, setAuthState] = useState(undefined); + + const persistLoginOrOut = useCallback(()=>{ + GetSession().then(( + res: SessionResponse + )=>{ + if(res.success && res.user){ + setAuthCookie('userInfo', res.user); + setAuthState({ + isLoggedIn: true, + userInfo: res.user + }); + }else{ + removeAuthCookie('userInfo'); + } + }); + }, []); + + const loginAndGetUserInfo = (usernameOrEmail: string, password:string, failureCallback?:()=>void)=>{ + CreateSession({usernameOrEmail, password}) + .then((respond)=>{ + GetSession().then(( + res: SessionResponse + )=>{ + if(res.success && res.user && setAuthState){ + setAuthCookie('userInfo', res.user); + setAuthState({ + isLoggedIn: true, + userInfo: res.user + }); + }else{ + if(failureCallback) failureCallback(); + } + }); + }); + } + + const logout = ()=>{ + DestroySession().then((res)=>{ + if (res && setAuthState) { + setAuthState({ + isLoggedIn: false, + userInfo: undefined + }); + } + }); + } + useEffect(()=>{ + if (authCookies !== undefined && authState===undefined){ + persistLoginOrOut(); + } + },[]); + + return( + + {children} + + ) +} + \ No newline at end of file diff --git a/app/contexts/Authentication/index.tsx b/app/contexts/Authentication/index.tsx new file mode 100644 index 0000000000..0074994891 --- /dev/null +++ b/app/contexts/Authentication/index.tsx @@ -0,0 +1,6 @@ +import { AuthenticationContext, AuthenticationProvider } from "./Authentication"; +import { UserInfo, AUTH_STATUS } from "./types"; + + +export type { UserInfo }; +export { AUTH_STATUS, AuthenticationContext, AuthenticationProvider }; diff --git a/app/contexts/Authentication/types.tsx b/app/contexts/Authentication/types.tsx new file mode 100644 index 0000000000..2daea795e5 --- /dev/null +++ b/app/contexts/Authentication/types.tsx @@ -0,0 +1,59 @@ +export enum AUTH_STATUS { + LOGGED_IN = "logged_in", + LOGGING = "logging", + LOGGED_OUT = "logged_out", +} + +export type AuthState = { + isLoggedIn: boolean; + userInfo?: UserInfo; +} +export interface UserInfo { + user_token: string, + username: string, + display_name: string, + email_gravatar_hash: string, + + // Rollout feature flags + can_access_studio: boolean, + maybe_feature_flags: string[], + + // Usage + can_use_tts: boolean, + can_use_w2l: boolean, + can_delete_own_tts_results: boolean, + can_delete_own_w2l_results: boolean, + can_delete_own_account: boolean, + + // Contribution + can_upload_tts_models: boolean, + can_upload_w2l_templates: boolean, + can_delete_own_tts_models: boolean, + can_delete_own_w2l_templates: boolean, + + // Moderation + can_approve_w2l_templates: boolean, + can_edit_other_users_profiles: boolean, + can_edit_other_users_tts_models: boolean, + can_edit_other_users_w2l_templates: boolean, + can_delete_other_users_tts_models: boolean, + can_delete_other_users_tts_results: boolean, + can_delete_other_users_w2l_templates: boolean, + can_delete_other_users_w2l_results: boolean, + can_ban_users: boolean, + can_delete_users: boolean, +} + +// Responses from the `/session` endpoint. +export interface SessionResponse { + // API call was successful + success: boolean, + // Whether the user is logged in + logged_in?: boolean, + // Extended user details (only if logged in) + user?: UserInfo | undefined | null, + + // if success is false, there will be an error_reason + error_reason?: string, +} + diff --git a/app/contexts/Authentication/utilities.tsx b/app/contexts/Authentication/utilities.tsx new file mode 100644 index 0000000000..ccd7915e30 --- /dev/null +++ b/app/contexts/Authentication/utilities.tsx @@ -0,0 +1,78 @@ + +import { + SessionResponse, +} from "./types"; + +const apiHost = "https://api.fakeyou.com"; +// const createAccount = `${apiHost}}/create_account`; +const login = `${apiHost}/v1/login`; +const getSession = `${apiHost}/v1/session`; +const logout = `${apiHost}/v1/logout`; + +export const CreateSession = async ({ + usernameOrEmail, password +}:{ + usernameOrEmail: string; + password: string; +})=> { + //TODO: This SHOULD NOT be sent in plaintext + const request = { + username_or_email: usernameOrEmail, + password: password, + } + + return await fetch(login,{ + method: 'POST', + headers: { + "Accept": "application/json", + "Content-Type": "application/json", + }, + credentials: 'include', + body: JSON.stringify(request) + }) + .then(res => res.json()) + .then(res => { return res; }) + .catch(e => { + return { + success: false, + error_reason: "frontend_error", + }; + }); +} +export const GetSession = async () => { + return await fetch(getSession, { + method: 'GET', + headers: { + 'Accept': 'application/json', + }, + credentials: 'include', + }) + .then(res => res.json()) + .then(res => { + const response : SessionResponse = res; + return response; + }) + .catch(e => { + return { + success: false, + error_reason: "frontend_error", + }; + }); +} +export async function DestroySession() : Promise { + return await fetch(logout, { + method: 'POST', + headers: { + 'Accept': 'application/json', + }, + credentials: 'include', + }) + .then(res => res.json()) + .then(res => { + const response : SessionResponse = res; + return response.success; + }) + .catch(e => { + return false; + }); +} \ No newline at end of file diff --git a/app/contexts/TopBarInner.tsx b/app/contexts/TopBarInner.tsx index 39f08848ca..7e6653dba0 100644 --- a/app/contexts/TopBarInner.tsx +++ b/app/contexts/TopBarInner.tsx @@ -1,6 +1,12 @@ import { createContext } from "react"; export const TopBarInnerContext = createContext<{ - TopBarInner: JSX.Element | null; - setTopBarInner: (el: JSX.Element | null)=>void; + TopBarInner: { + location: string, + node: React.ReactNode, + } | null; + setTopBarInner: (el: { + location: string, + node: React.ReactNode, + } | null)=>void; } | null>(null); \ No newline at end of file diff --git a/app/helpers/index.tsx b/app/helpers/index.tsx deleted file mode 100644 index 465c33be52..0000000000 --- a/app/helpers/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { joinClassNames } from "./joinCassNames"; - -export { - joinClassNames -} \ No newline at end of file diff --git a/app/helpers/joinCassNames.tsx b/app/helpers/joinCassNames.tsx deleted file mode 100644 index ca112a52bc..0000000000 --- a/app/helpers/joinCassNames.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export function joinClassNames(...classes:string[]) { - return classes.filter(Boolean).join(' ') -}; \ No newline at end of file diff --git a/app/modules/TopBar/AuthButtons.tsx b/app/modules/TopBar/AuthButtons.tsx new file mode 100644 index 0000000000..972cc9e2df --- /dev/null +++ b/app/modules/TopBar/AuthButtons.tsx @@ -0,0 +1,24 @@ +import { useContext, } from "react"; + +import { AuthenticationContext } from "~/contexts/Authentication"; +import { Button, ButtonLink } from "~/components"; + +export const AuthButtons = ()=>{ + const {authState, logout} = useContext(AuthenticationContext); + const handleLogout = ()=>{ + if(logout)logout(); + } + if ( authState && authState.isLoggedIn ) { + return( + + ); + }else{ + return( + <> + Login + Sign Up + + ); + } +} + diff --git a/app/modules/TopBar/TopBar.tsx b/app/modules/TopBar/TopBar.tsx index 8bc1183c97..40632259ef 100644 --- a/app/modules/TopBar/TopBar.tsx +++ b/app/modules/TopBar/TopBar.tsx @@ -1,13 +1,16 @@ -import { useContext } from "react"; -import { Button } from "~/components/Button"; -import { - faChevronLeft, - faRightToBracket, -} from "@fortawesome/pro-solid-svg-icons"; +import { useContext, } from "react"; +import { useLocation } from "@remix-run/react"; + +import { faChevronLeft } from "@fortawesome/pro-solid-svg-icons"; + import { TopBarInnerContext } from "~/contexts/TopBarInner"; import { ButtonLink } from "~/components"; +import { AuthButtons } from "./AuthButtons"; + export const TopBar = () => { + const currentLocation = useLocation().pathname; + return (
- - Back to Dashboard - + {currentLocation !== "/" && + + Back to Dashboard + + }
- +
-
- +
+
@@ -45,7 +50,11 @@ export const TopBar = () => { ); }; -const TopBarInner = () => { +const TopBarInner = ({currentLocation}:{currentLocation:string}) => { const { TopBarInner } = useContext(TopBarInnerContext) || {}; - return TopBarInner; + if(!TopBarInner || TopBarInner.location !== currentLocation){ + return null; + } + return TopBarInner.node; }; + diff --git a/app/modules/TopBarHelmet/TopBarHelmet.tsx b/app/modules/TopBarHelmet/TopBarHelmet.tsx index c626dd4417..b73b655f39 100644 --- a/app/modules/TopBarHelmet/TopBarHelmet.tsx +++ b/app/modules/TopBarHelmet/TopBarHelmet.tsx @@ -1,3 +1,5 @@ +import { useLocation } from "@remix-run/react"; + import { useContext, useEffect, @@ -8,7 +10,7 @@ import { TopBarInnerContext } from "~/contexts/TopBarInner"; export const TopBarHelmet = (props:{ - children: JSX.Element | null; + children: React.ReactNode | null; })=>{ const [showChild, setShowChild] = useState(false); // Wait until after client-side hydration to show @@ -24,13 +26,17 @@ export const TopBarHelmet = (props:{ } const LazyLoadTopBarInnerContent = (props:{ - children: JSX.Element | null; + children: React.ReactNode | null; })=>{ const { setTopBarInner } = useContext(TopBarInnerContext) || {}; + const location = useLocation().pathname; useLayoutEffect(()=>{ if(setTopBarInner){ - setTopBarInner(props.children); + setTopBarInner({ + location: location, + node: props.children + }); } },[setTopBarInner]); diff --git a/app/pages/PageEnigma/comps/ControlsTopButtons/ControlsTopButtons.tsx b/app/pages/PageEnigma/comps/ControlsTopButtons/ControlsTopButtons.tsx index b830d9823c..7a064d7f7a 100644 --- a/app/pages/PageEnigma/comps/ControlsTopButtons/ControlsTopButtons.tsx +++ b/app/pages/PageEnigma/comps/ControlsTopButtons/ControlsTopButtons.tsx @@ -75,7 +75,7 @@ export const ControlsTopButtons = () => { type: APPUI_ACTION_TYPES.SHOW_CONTROLPANELS_SCENEOBJECT, payload: { group: ClipGroup.OBJECT, - object_name: "", + object_name: "TEST BUTTON", object_uuid: "", version: "1", objectVectors: { diff --git a/app/pages/PageEnigma/contexts/TrackContext/TrackProvider.tsx b/app/pages/PageEnigma/contexts/TrackContext/TrackProvider.tsx index d04d28548e..6c6e69a40d 100644 --- a/app/pages/PageEnigma/contexts/TrackContext/TrackProvider.tsx +++ b/app/pages/PageEnigma/contexts/TrackContext/TrackProvider.tsx @@ -103,7 +103,7 @@ export const TrackProvider = ({ children }: Props) => { }, { version: 1, - media_id: uuid.v4(), + media_id: "m_w5t517krrf63f3tj2288vsffmj87zw", type: ClipType.ANIMATION, length: 25, name: "Walk", @@ -126,7 +126,7 @@ export const TrackProvider = ({ children }: Props) => { }, { version: 1, - media_id: uuid.v4(), + media_id: "m_403phjvjkbbaxxbz8y7r6qjay07mfd", type: ClipType.AUDIO, length: 25, name: "Talk", diff --git a/app/pages/PageEnigma/datastructures/clips/lipsync_clip.ts b/app/pages/PageEnigma/datastructures/clips/lipsync_clip.ts index 6ad94b7fbf..77fdd5f6f6 100644 --- a/app/pages/PageEnigma/datastructures/clips/lipsync_clip.ts +++ b/app/pages/PageEnigma/datastructures/clips/lipsync_clip.ts @@ -35,6 +35,7 @@ export class LipSyncClip { audio_data: AudioData | undefined; lipsync: LipSync; blendshape_helper: BlendShapeHelper; + faces: THREE.Mesh[] constructor(version: number, media_id: string, volume: number) { this.version = version; @@ -47,6 +48,7 @@ export class LipSyncClip { this.blendshape_helper = new BlendShapeHelper(0, 0, 0); // we might need 3 of these one for each character ... this.lipsync = new LipSync(); + this.faces = []; } // lip sync will be generated through TTS @@ -81,8 +83,8 @@ export class LipSyncClip { const blendShapeIndexA = c.morphTargetDictionary["aa"]; if (blendShapeIndexE != null) { this.blendshape_helper = new BlendShapeHelper(blendShapeIndexA, blendShapeIndexE, blendShapeIndexO); + this.faces.push(c); resolve(c); - return; } } } @@ -91,42 +93,48 @@ export class LipSyncClip { } -async play(object: THREE.Object3D) { - if (this.audio_data?.audioBuffer == null) { await this.download_audio(); } - if (this.lipsync.face == null) { - this.lipsync = new LipSync(await this._detect_face(object)); - this.lipsync.startLipSyncFromAudioBuffer(this.audio_data?.audioBuffer); + async play(object: THREE.Object3D) { + if (this.audio_data?.audioBuffer == null) { await this.download_audio(); } + if (this.lipsync.face == null) { + this.lipsync = new LipSync(await this._detect_face(object)); + this.lipsync.startLipSyncFromAudioBuffer(this.audio_data?.audioBuffer); + } + } + + stop() { + if (this.lipsync == null) { return; } + this.lipsync.destroy(); + } + + setBlends(ah: number, ee: number, oh: number) { + this.faces.forEach((element: THREE.Mesh) => { + if (element.morphTargetInfluences) { + element.morphTargetInfluences[this.blendshape_helper.ee] = ee; + element.morphTargetInfluences[this.blendshape_helper.ah] = ah; + element.morphTargetInfluences[this.blendshape_helper.oh] = oh; + } + }); + } + + step() { + if (this.lipsync == null) { return; } + const positions = this.lipsync.update(); + this.setBlends(positions["ee"], positions["ah"], positions["oh"]); + } + + reset() { + if (this.lipsync.face != undefined) { + this.setBlends(0,0,0); + } + this.lipsync = new LipSync(); + } + + toJSON(): string { + return JSON.stringify({ + version: this.version, + media_id: this.media_id, + type: this.type, + volume: this.volume, + }); } } - -stop() { - if (this.lipsync == null) { return; } - this.lipsync.destroy(); -} - -step() { - if (this.lipsync == null) { return; } - const positions = this.lipsync.update(); - this.lipsync.face.morphTargetInfluences[this.blendshape_helper.ee] = positions["ee"]; - this.lipsync.face.morphTargetInfluences[this.blendshape_helper.ah] = positions["ah"]; - this.lipsync.face.morphTargetInfluences[this.blendshape_helper.oh] = positions["oh"]; -} - -reset() { - if (this.lipsync.face != undefined) { - this.lipsync.face.morphTargetInfluences[this.blendshape_helper.ee] = 0; - this.lipsync.face.morphTargetInfluences[this.blendshape_helper.ah] = 0 - this.lipsync.face.morphTargetInfluences[this.blendshape_helper.oh] = 0; - } - this.lipsync = new LipSync(); -} - -toJSON(): string { - return JSON.stringify({ - version: this.version, - media_id: this.media_id, - type: this.type, - volume: this.volume, - }); -} -} diff --git a/app/pages/PageEnigma/datastructures/clips/transform_clip.ts b/app/pages/PageEnigma/datastructures/clips/transform_clip.ts index 66659207d7..df7fe39603 100644 --- a/app/pages/PageEnigma/datastructures/clips/transform_clip.ts +++ b/app/pages/PageEnigma/datastructures/clips/transform_clip.ts @@ -1,18 +1,34 @@ import * as THREE from 'three'; +export class TransformFrame { + position: THREE.Vector3 + rotation: THREE.Vector3 + scale: THREE.Vector3 + offset: number + keyframe_uuid: string + + constructor(position: THREE.Vector3, rotation: THREE.Vector3, scale: THREE.Vector3, offset: number, keyframe_uuid: string) { + this.position = position; + this.rotation = rotation; + this.scale = scale; + this.offset = offset; + this.keyframe_uuid = keyframe_uuid; + } +} + export class TransformClip { version: number; media_id: string; object_uuid: string; type: "transform"; - positions: THREE.Vector3[]; - rotations: THREE.Vector3[]; - scales: THREE.Vector3[]; + keyframes: TransformFrame[]; length: number; step_frame: number; looping: boolean; + offset: number; + constructor(version: number, object_uuid: string, length: number, media_id: string = "") { this.version = version; this.media_id = media_id; @@ -21,40 +37,93 @@ export class TransformClip { this.length = length; - this.positions = []; - this.rotations = []; - this.scales = []; + this.keyframes = []; this.step_frame = 0; + this.offset = 0; + this.looping = false; } + private findNextNumber(location: number) { + // Sort the transform frames based on their offset in ascending order + this.keyframes.sort((a, b) => a.offset - b.offset); + + let currentKeyframe: TransformFrame | undefined; + let nextKeyframe: TransformFrame | undefined; + + this.length = this.keyframes[this.keyframes.length-1].offset+this.offset; + + for (let frame of this.keyframes) { + if (frame.offset <= location) { + currentKeyframe = frame; + } else if (frame.offset > location) { + nextKeyframe = frame; + break; + } + } + + return { currentKeyframe, nextKeyframe }; + } + + public setOffset(keyframe_uuid: string, keyframe_offset: number) { + this.keyframes.forEach(item => { + if (item.keyframe_uuid === keyframe_uuid) { + item.offset = keyframe_offset; + console.log("Set keyframe!!"); + return; + } + }); + } + + public removeKeyframe(keyframe_uuid: string) { + this.keyframes = this.keyframes.filter(item => item.keyframe_uuid !== keyframe_uuid); + } + step(object: THREE.Object3D, offset: number, frame: number) { - this.step_frame = frame-offset; - if (this.step_frame < 0) { this.step_frame = 0; } - if (this.step_frame >= this.length) { return; } // Reached max frames. - if(this.positions.length < 2) { return; } // If there are enough points in the scene. + this.offset = offset; + this.step_frame = frame - offset; + //if (this.step_frame < 0) { this.step_frame = 0; } + //if (this.step_frame >= this.length) { return; } // Reached max frames. + if (this.keyframes.length < 2) { return; } // If there are enough points in the scene. - let time_frame = (this.step_frame/this.length); + // Find the current and next keyframes based on time_frame + let { currentKeyframe, nextKeyframe } = this.findNextNumber(this.step_frame); + if (nextKeyframe != undefined && currentKeyframe != undefined) { + let small_step_frame = this.step_frame - currentKeyframe.offset; + let location = (small_step_frame / (nextKeyframe.offset - currentKeyframe.offset)); + if (location < 0) { + location = 0; + } + let pos_s = currentKeyframe?.position; + let pos_e = nextKeyframe?.position; + let points_pos = [new THREE.Vector3(pos_s.x, pos_s.y, pos_s.z), new THREE.Vector3(pos_e.x, pos_e.y, pos_e.z)] + let curve_pos = new THREE.CatmullRomCurve3(points_pos); + let point_pos = curve_pos.getPoint(location); + object.position.copy(point_pos); - let curve = new THREE.CatmullRomCurve3(this.positions); - let point = curve.getPoint(time_frame); - object.position.copy(point); + let rot_s = currentKeyframe?.rotation; + let rot_e = nextKeyframe?.rotation; + let quat_s = new THREE.Quaternion().setFromEuler(new THREE.Euler(rot_s.x, rot_s.y, rot_s.z)); + let quat_e = new THREE.Quaternion().setFromEuler(new THREE.Euler(rot_e.x, rot_e.y, rot_e.z)); + let quaternion = new THREE.Quaternion(); + quaternion.slerpQuaternions(quat_s, quat_e, location); + object.quaternion.copy(quaternion); - let curve_rot = new THREE.CatmullRomCurve3(this.rotations); - let point_rot = curve_rot.getPoint(time_frame); - object.rotation.set(point_rot.x, point_rot.y, point_rot.z); - - let curve_scale = new THREE.CatmullRomCurve3(this.scales); - let point_scale = curve_scale.getPoint(time_frame); - object.scale.copy(point_scale); + let scale_s = currentKeyframe?.scale; + let scale_e = nextKeyframe?.scale; + let points_scale = [new THREE.Vector3(scale_s.x, scale_s.y, scale_s.z), new THREE.Vector3(scale_e.x, scale_e.y, scale_e.z)] + let curve_scale = new THREE.CatmullRomCurve3(points_scale); + let point_scale = curve_scale.getPoint(location); + object.scale.copy(point_scale); + } } reset(object: THREE.Object3D) { - if (this.positions.length > 0) { - let first_pos = this.positions[0]; - let first_rot = this.rotations[0]; - let first_scl = this.scales[0]; + if (this.keyframes.length > 0) { + let first_pos = this.keyframes[0].position; + let first_rot = this.keyframes[0].rotation; + let first_scl = this.keyframes[0].scale; object.position.copy(first_pos); object.rotation.set(first_rot.x, first_rot.y, first_rot.z); object.scale.copy(first_scl); @@ -62,34 +131,13 @@ export class TransformClip { } } - add_position(position: THREE.Vector3) { - this.positions.push(new THREE.Vector3(position.x, position.y, position.z)); - } - - remove_position(position: THREE.Vector3) { - this.positions = this.positions.filter(positions => { - return !position.equals(position); - }); - } - - add_rotation(rotation: THREE.Vector3) { - this.rotations.push(new THREE.Vector3(rotation.x, rotation.y, rotation.z)); - } - - remove_rotation(rotation: THREE.Vector3) { - this.rotations = this.rotations.filter(rotations => { - return !rotation.equals(rotation); - }); - } - - add_scale(scale: THREE.Vector3) { - this.scales.push(new THREE.Vector3(scale.x, scale.y, scale.z)); - } - - remove_scale(scale: THREE.Vector3) { - this.scales = this.scales.filter(scales => { - return !scale.equals(scale); - }); + add_frame(position: THREE.Vector3, rotation: THREE.Vector3, scale: THREE.Vector3, offset: number, keyframe_uuid: string) { + this.keyframes.push(new TransformFrame( + position, + rotation, + scale, + offset, + keyframe_uuid)); } toJSON(): string { @@ -99,9 +147,7 @@ export class TransformClip { object_uuid: this.object_uuid, length: this.length, type: this.type, - position: this.positions, - rotation: this.rotations, - scale: this.scales, + keyframes: this.keyframes, }); } } diff --git a/app/pages/PageEnigma/js/animation_engine.ts b/app/pages/PageEnigma/js/animation_engine.ts index 1ec3dc1bf7..ba64f6c54d 100644 --- a/app/pages/PageEnigma/js/animation_engine.ts +++ b/app/pages/PageEnigma/js/animation_engine.ts @@ -18,6 +18,8 @@ export class AnimationEngine { object_uuid, 1.0, 1.0, clip_name); + this.clips[object_uuid]._load_animation(); + } } diff --git a/app/pages/PageEnigma/js/editor.ts b/app/pages/PageEnigma/js/editor.ts index dd22bc5222..367670be48 100644 --- a/app/pages/PageEnigma/js/editor.ts +++ b/app/pages/PageEnigma/js/editor.ts @@ -104,6 +104,8 @@ class Editor { positive_prompt: string; negative_prompt: string; art_style: ArtStyle; + + last_scrub: number; // Default params. constructor() { console.log( @@ -169,6 +171,7 @@ class Editor { this.playback = false; this.playback_location = 0; this.max_length = 10; + this.last_scrub = 0; // Audio Engine Test. this.render_width = 1280; @@ -344,8 +347,6 @@ class Editor { ); } - console.log(this.cam_obj); - this.dispatchAppUiState({ type: APPUI_ACTION_TYPES.UPDATE_EDITOR_LOADINGBAR, payload: { @@ -511,6 +512,7 @@ class Editor { return; } this.last_selected = this.selected; + this.selected = undefined; this.control.detach(); this.activeScene.scene.remove(this.control); this.outlinePass.selectedObjects = []; @@ -519,7 +521,7 @@ class Editor { async _serialize_timeline() { // note the database from the server is the source of truth for all the data. // Test code here - const object: any = await this.activeScene.load_glb( + const object: THREE.Object3D = await this.activeScene.load_glb( "./resources/models/fox/fox.glb", ); @@ -649,9 +651,10 @@ class Editor { async _test_demo() { // note the database from the server is the source of truth for all the data. // Test code here - const object: any = await this.activeScene.load_glb( - "./resources/models/fox/fox.glb", + const object: THREE.Object3D = await this.activeScene.load_glb( + "m_9f3d3z94kk6m25zywyz6an3p43fjtw", ); + object.uuid = "CH1"; // Load timeline creates the the clips from the datastructure and loads them in here. // load object into the engine for lip syncing @@ -841,8 +844,11 @@ class Editor { if (this.cameraViewControls && this.camera_person_mode) { this.cameraViewControls.update(5 * delta_time); - if (this.cam_obj) { - if (this.timeline.is_playing == false) { + if(this.cam_obj){ + if (this.last_scrub != this.timeline.scrubber_frame_position) { + this.camera.position.copy(this.cam_obj.position); + this.camera.rotation.copy(this.cam_obj.rotation); + } else if(this.timeline.is_playing == false) { this.cam_obj.position.copy(this.camera.position); this.cam_obj.rotation.copy(this.camera.rotation); } else { @@ -857,7 +863,13 @@ class Editor { this.render_camera.rotation.copy(this.cam_obj.rotation); } - this.timeline.update(); + this.updateSelectedUI(); + + if(this.timeline.is_playing){ + this.timeline.update(); + } + + this.last_scrub = this.timeline.scrubber_frame_position; this.renderScene(); } @@ -1010,9 +1022,9 @@ class Editor { URL.revokeObjectURL(url); document.body.removeChild(downloadLink); - const data = await this.api_manager.uploadMedia(blob, "tmp.wav"); - console.log(data); + const data = await this.api_manager.uploadMedia(blob, "render.mp4"); // Create a link to download the file + } async generateFrame() { @@ -1282,7 +1294,6 @@ class Editor { } this.activeScene.selected = this.selected; - this.updateSelectedUI(); // this.update_properties() this.activeScene.scene.add(this.control); this.control.attach(this.selected); diff --git a/app/pages/PageEnigma/js/scene.ts b/app/pages/PageEnigma/js/scene.ts index 81367ce85f..0228012696 100644 --- a/app/pages/PageEnigma/js/scene.ts +++ b/app/pages/PageEnigma/js/scene.ts @@ -77,7 +77,7 @@ class Scene { } _create_camera_obj() { - this.load_glb("/resources/models/camera/camera.glb", false).then((cam_obj) => { + this.load_glb("m_cxh4asqhapdz10j880755dg4yevshb", false).then((cam_obj) => { cam_obj.userData["name"] = "::CAM::"; cam_obj.name = "::CAM::"; cam_obj.position.set(0, 0.6, 1.5); @@ -97,7 +97,45 @@ class Scene { } } - async load_glb(filepath: string, auto_add: boolean = true): Promise { //: Promise { + async getMediaURL(media_id: string) { + //This is for prod when we have the proper info on the url. + let api_base_url = "https://api.fakeyou.com"; + let url = `${api_base_url}/v1/media_files/file/${media_id}` + let responce = await fetch(url); + let json = await JSON.parse(await responce.text()); + let bucketPath = json["media_file"]["public_bucket_path"]; + let media_base_url = "https://storage.googleapis.com/vocodes-public" + let media_url = `${media_base_url}${bucketPath}` + return media_url; + } + + async load_glb(media_id: string, auto_add: boolean = true): Promise { //: Promise { + return new Promise(async (resolve) => { + let glbLoader = new GLTFLoader(); + glbLoader.load(await this.getMediaURL(media_id), (glb) => { + glb.scene.children.forEach(child => { + child.traverse((c: THREE.Object3D) => { + if (c instanceof THREE.Mesh) { + c.material.metalness = 0.0; + c.material.specular = 0.5; + c.castShadow = true; + c.receiveShadow = true; + c.material.transparent = false; + } + }); + if (child.type == "Group") { + if (auto_add) { this.scene.add(child.children[0]); } + resolve(child.children[0]); + return; + } + if (auto_add) { this.scene.add(child); } + resolve(child); + }); + }); + }); + } + + async load_glb_absolute(filepath: string, auto_add: boolean = true): Promise { //: Promise { return new Promise((resolve) => { let glbLoader = new GLTFLoader(); glbLoader.load(filepath, (glb) => { diff --git a/app/pages/PageEnigma/js/timeline.ts b/app/pages/PageEnigma/js/timeline.ts index 3bf8545c4b..3c5c83a3e9 100644 --- a/app/pages/PageEnigma/js/timeline.ts +++ b/app/pages/PageEnigma/js/timeline.ts @@ -11,7 +11,7 @@ import Queue from "~/pages/PageEnigma/Queue/Queue"; import { QueueNames } from "../Queue/QueueNames"; import { toEngineActions } from "../Queue/toEngineActions"; import { fromEngineActions } from "../Queue/fromEngineActions"; -import { ClipType } from "~/pages/PageEnigma/models/track"; +import { ClipGroup, ClipType } from "~/pages/PageEnigma/models/track"; // Every object uuid / entity has a track. export class TimelineCurrentState { @@ -39,6 +39,7 @@ export class TimeLine { timeline_items: ClipUI[]; timeline_limit: number; + absolute_end: number; scrubber_frame_position: number; is_playing: boolean; @@ -63,7 +64,8 @@ export class TimeLine { scene: Scene, ) { this.timeline_items = []; - this.timeline_limit = 60 * 5; // 5 seconds + this.absolute_end = 60 * 12; + this.timeline_limit = this.absolute_end; // 5 seconds this.is_playing = false; this.scrubber_frame_position = 0; // in frames into the tl @@ -104,6 +106,12 @@ export class TimeLine { case toEngineActions.ADD_KEYFRAME: await this.addKeyFrame(data); break; + case toEngineActions.UPDATE_KEYFRAME: + await this.updateKeyFrame(data); + break; + case toEngineActions.DELETE_KEYFRAME: + await this.deleteKeyFrame(data); + break; case toEngineActions.ADD_CLIP: await this.addClip(data); break; @@ -135,18 +143,41 @@ export class TimeLine { // rotation: XYZ; // scale: XYZ; // selected?: boolean; + const data_json = data["data"]; + const uuid = data_json["object_uuid"]; + + let object_name = this.scene.get_object_by_uuid(uuid)?.name; + if (object_name == undefined) { + object_name = "undefined"; + } + + const new_item = this.transform_engine.addFrame( + uuid, + this.absolute_end, + data_json["position"], + data_json["rotation"], + data_json["scale"], + data_json["offset"], + data_json["keyframe_uuid"], + ); + if (new_item) { + await this.addPlayableClip( + new ClipUI( + data_json["version"], + ClipType.TRANSFORM, + data_json["group"], + object_name, + "", + uuid, + 0, + this.absolute_end, + ), + ); + } } public async addClip(data: any) { - let object_uuid = data["data"]["object_uuid"]; - - if (object_uuid.length < 6) { - object_uuid = this.scene.selected?.uuid; //data['data']['object_uuid']; - if (object_uuid == undefined) { - return; - } - } - + const object_uuid = data["data"]["object_uuid"]; const media_id = data["data"]["media_id"]; const name = data["data"]["name"]; const group = data["data"]["group"]; @@ -155,6 +186,37 @@ export class TimeLine { const offset = data["data"]["offset"]; const end_offset = data["data"]["length"] + offset; + switch (type) { + case "animation": + this.animation_engine.load_object(object_uuid, media_id, name); + break; + case "transform": + this.transform_engine.loadObject(object_uuid, data["data"]["length"]); + break; + case "audio": + if (group == "character") { + this.lipSync_engine.load_object(object_uuid, media_id); + // media id for this as well it can be downloaded + this.addPlayableClip( + new ClipUI( + version, + ClipType.AUDIO, + group, + name, + media_id, + object_uuid, + offset, + end_offset, + ), + ); + return; + } else { + console.log("Audio!"); + this.audio_engine.loadClip(media_id); + } + break; + } + // media id for this as well it can be downloaded this.addPlayableClip( new ClipUI( @@ -168,19 +230,39 @@ export class TimeLine { end_offset, ), ); - - switch (type) { - case ClipType.ANIMATION: - this.animation_engine.load_object(object_uuid, media_id, name); - break; - case ClipType.TRANSFORM: - this.transform_engine.loadObject(object_uuid, data["data"]["length"]); - break; - } } + + public async deleteKeyFrame(data: any) { + const keyframe_uuid = data["data"]["keyframe_uuid"]; + const object_uuid = data["data"]["object_uuid"]; + this.transform_engine.clips[object_uuid].removeKeyframe(keyframe_uuid); + } + + public async updateKeyFrame(data: any) { + const keyframe_uuid = data["data"]["keyframe_uuid"]; + const keyframe_offset = data["data"]["offset"]; + const object_uuid = data["data"]["object_uuid"]; + this.transform_engine.clips[object_uuid].setOffset( + keyframe_uuid, + keyframe_offset, + ); + } + public async updateClip(data: any) { // only length and offset changes here. console.log(data); + + const object_uuid = data["data"]["object_uuid"]; + const media_id = data["data"]["media_id"]; + const offset = data["data"]["offset"]; + const length = data["data"]["length"] + offset; + + for (const element of this.timeline_items) { + if (element.media_id == media_id && element.object_uuid == object_uuid) { + element.length = length; + element.offset = offset; + } + } } public async deleteClip(data: any) { console.log(data); @@ -220,6 +302,7 @@ export class TimeLine { if (this.is_playing) { return; } + this.update(); this.setScrubberPosition(data["data"]["currentTime"]); } @@ -294,34 +377,40 @@ export class TimeLine { // element.play() // remove the element from the list const object = this.scene.get_object_by_uuid(element.object_uuid); - if (element.type == "transform") { + if (element.type === ClipType.TRANSFORM) { if (object && this.transform_engine.clips[element.object_uuid]) { - this.transform_engine.clips[element.object_uuid].length = - element.length - element.offset; this.transform_engine.clips[element.object_uuid].step( object, element.offset, this.scrubber_frame_position, ); + element.length = + this.transform_engine.clips[element.object_uuid].length; } - } else if (element.type == "audio") { + } else if ( + element.group !== ClipGroup.CHARACTER && + element.type === ClipType.AUDIO + ) { if (this.scrubber_frame_position + 1 >= element.length) { this.audio_engine.stopClip(element.media_id); } else { this.audio_engine.playClip(element.media_id); } - } else if (element.type == "lipsync") { + } else if ( + element.group === ClipGroup.CHARACTER && + element.type === ClipType.AUDIO + ) { if (this.scrubber_frame_position + 1 >= element.length) { this.lipSync_engine.clips[element.object_uuid].stop(); } else if (object) { await this.lipSync_engine.clips[element.object_uuid].play(object); this.lipSync_engine.clips[element.object_uuid].step(); } - } else if (element.type == "animation") { + } else if (element.type === ClipType.ANIMATION) { if (object) { await this.animation_engine.clips[object.uuid].play(object); this.animation_engine.clips[object.uuid].step( - this.scrubber_frame_position / 60, + this.scrubber_frame_position / 120, // Double FPS for best result. ); } } else { diff --git a/app/pages/PageEnigma/js/transform_engine.ts b/app/pages/PageEnigma/js/transform_engine.ts index e1e29ccf6e..ba501957ad 100644 --- a/app/pages/PageEnigma/js/transform_engine.ts +++ b/app/pages/PageEnigma/js/transform_engine.ts @@ -15,13 +15,14 @@ class TransformEngine { return this.clips[object_uuid] } - addFrame(object: THREE.Object3D, clip_length:number = 2) { - if(this.clips[object.uuid] == null) { - this.clips[object.uuid] = new TransformClip(this.version, object.uuid, clip_length); + addFrame(object_uuid: string, clip_length:number = 2, position: THREE.Vector3, rotation: THREE.Vector3, scale: THREE.Vector3, offset: number, keyframe_uuid: string): boolean{ + if(this.clips[object_uuid] == null) { + this.clips[object_uuid] = new TransformClip(this.version, object_uuid, clip_length); + this.clips[object_uuid].add_frame(position, rotation, scale, offset, keyframe_uuid); + return false; } - this.clips[object.uuid].add_position(object.position); - this.clips[object.uuid].add_rotation(new THREE.Vector3(object.rotation.x, object.rotation.y, object.rotation.z)); - this.clips[object.uuid].add_scale(object.scale); + this.clips[object_uuid].add_frame(position, rotation, scale, offset, keyframe_uuid); + return true; } } diff --git a/app/root.tsx b/app/root.tsx index 855484c581..1017fbe028 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -1,4 +1,5 @@ -import { useState, useEffect } from "react"; +import React, { useState, useEffect } from "react"; +import { CookiesProvider } from "react-cookie"; import { Transition } from "@headlessui/react"; import { LinksFunction } from "@remix-run/deno"; import { @@ -16,6 +17,7 @@ import baseCss from "./styles/base.css?url"; import { LoadingDotsBricks } from "~/components"; import { TopBar } from "./modules/TopBar"; import { TopBarInnerContext } from "~/contexts/TopBarInner"; +import { AuthenticationProvider, UserInfo } from "./contexts/Authentication"; export const links: LinksFunction = () => [ { @@ -47,7 +49,10 @@ export const links: LinksFunction = () => [ export default function App() { const [topBarInnerComponent, setTopBarInnerComponent] = - useState(null); + useState<{ + location: string, + node: React.ReactNode, + } | null>(null); const [showLoader, setShowLoader] = useState(true); useEffect(()=>{ setTimeout(()=>setShowLoader(false), 2500); @@ -63,16 +68,20 @@ export default function App() { - -
- - - + + + +
+ + + + + diff --git a/app/routes/IdealEnigma.tsx b/app/routes/IdealEnigma.tsx index 6fe36a7f25..087f2297a7 100644 --- a/app/routes/IdealEnigma.tsx +++ b/app/routes/IdealEnigma.tsx @@ -1,40 +1,5 @@ -import { useState } from "react"; import { PageEnigma } from "../pages/PageEnigma"; -import { Button } from "~/components/Button"; - export default function IdealEnigma() { - //const [riddle, setRiddle] = useState(""); - //const [submitted, setSubmitted] = useState(false); - - //const handleInput = (event:React.ChangeEvent)=>{ - // setRiddle(event.target.value); - //} - - //const handleSubmit = () => { - // const state = riddle.length > 5 && riddle[riddle.length-1] === "!"; - // setSubmitted(state); - //} - return ; - // if(submitted){ - // return - // }else{ - // return( - //
- //
- // - // - //
- // - //
- //
- - // ); - // } } diff --git a/app/routes/Login.tsx b/app/routes/Login.tsx new file mode 100644 index 0000000000..4843521b2a --- /dev/null +++ b/app/routes/Login.tsx @@ -0,0 +1,95 @@ +import { useContext, useEffect, useState, useRef } from "react"; +import { useNavigate } from "@remix-run/react"; +import { useCookies } from 'react-cookie'; +import { faKey, faUser } from "@fortawesome/pro-solid-svg-icons"; + +import { AuthenticationContext } from "~/contexts/Authentication"; + +import { + Button, + H1, + Input, + Link, + P, +} from '~/components'; + +import { LoadingDots } from "~/components"; + +export default function LoginScreen() { + const navigate = useNavigate(); + + const [showLoader, setShowLoader] = useState(undefined); + const formRef = useRef(null); + const [authCookies, setAuthCookie, removeAuthCookie] = useCookies(['userInfo']); + const {authState, loginAndGetUserInfo} = useContext(AuthenticationContext); + + + const handleOnSumbit = (ev: React.FormEvent)=>{ + ev.preventDefault(); + if(formRef.current){ + const form = new FormData(formRef.current); + const usernameOrEmail = form.get("usernameOrEmail")?.toString(); + const password = form.get("password")?.toString(); + if( usernameOrEmail && password && loginAndGetUserInfo){ + setShowLoader("Authenticating"); + loginAndGetUserInfo( + usernameOrEmail, + password, + ()=>{ + setShowLoader(undefined); + }); + } + } + }// end handleOnSubmit + + useEffect(()=>{ + if(authState && authState.isLoggedIn) + navigate("/"); + },[authState]); + + return ( +
+
+

Login to FakeYou

+
+
+
+ +
+ +
+
+

Don't have an account?

+ Sign Up +
+ +
+ +
+
+ ); +} \ No newline at end of file diff --git a/app/routes/Signup.tsx b/app/routes/Signup.tsx new file mode 100644 index 0000000000..4cf1646bc2 --- /dev/null +++ b/app/routes/Signup.tsx @@ -0,0 +1,71 @@ +import { Form } from "@remix-run/react"; +import { faEnvelope, faKey, faUser } from "@fortawesome/pro-solid-svg-icons"; +import { + Button, + H1, + Input, + Link, + P, +} from '~/components'; + +export default function LoginScreen() { + return ( +
+
+

Sign Up to FakeYou

+
+
+
+ +
+ +
+ +
+ +
+
+ +
+
+

Already have an account?

+ Log in instead +
+
+
+
+ ); +} diff --git a/app/styles/base.css b/app/styles/base.css index 129268f61d..aa1208206a 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -21,11 +21,11 @@ body { font-family: "Fira Sans", sans-serif; color: #fff; + color-scheme: dark; } .topbar-spacer { height: 68px; - width: 100vw; } diff --git a/package-lock.json b/package-lock.json index ec56002429..72ba4e14ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "ccapture.js": "github:aceslowman/ccapture.js#npm-fix", "isbot": "^4.1.0", "react": "^18.2.0", + "react-cookie": "^7.1.4", "react-dom": "^18.2.0", "react-hot-toast": "^2.4.1", "tailwind-merge": "^2.2.2", @@ -2334,6 +2335,15 @@ "@types/unist": "^2" } }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", + "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2391,14 +2401,12 @@ "node_modules/@types/prop-types": { "version": "15.7.11", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", - "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", - "dev": true + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" }, "node_modules/@types/react": { "version": "18.2.57", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.57.tgz", "integrity": "sha512-ZvQsktJgSYrQiMirAN60y4O/LRevIV8hUzSOSNB6gfR3/o3wCBFQx3sPwIYtuDMeiVgsSS3UzCV26tEzgnfvQw==", - "dev": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -2417,8 +2425,7 @@ "node_modules/@types/scheduler": { "version": "0.16.8", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", - "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", - "dev": true + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==" }, "node_modules/@types/semver": { "version": "7.5.7", @@ -6120,6 +6127,14 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, "node_modules/hosted-git-info": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.1.tgz", @@ -10385,6 +10400,19 @@ "node": ">=0.10.0" } }, + "node_modules/react-cookie": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-7.1.4.tgz", + "integrity": "sha512-wDxxa/HYaSXSMlyWJvJ5uZTzIVtQTPf1gMksFgwAz/2/W3lCtY8r4OChCXMPE7wax0PAdMY97UkNJedGv7KnDw==", + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.5", + "hoist-non-react-statics": "^3.3.2", + "universal-cookie": "^7.0.0" + }, + "peerDependencies": { + "react": ">= 16.3.0" + } + }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -12478,6 +12506,15 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/universal-cookie": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-7.1.4.tgz", + "integrity": "sha512-Q+DVJsdykStWRMtXr2Pdj3EF98qZHUH/fXv/gwFz/unyToy1Ek1w5GsWt53Pf38tT8Gbcy5QNsj61Xe9TggP4g==", + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^0.6.0" + } + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", diff --git a/package.json b/package.json index bbd869f7cc..171f13c63e 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "ccapture.js": "github:aceslowman/ccapture.js#npm-fix", "isbot": "^4.1.0", "react": "^18.2.0", + "react-cookie": "^7.1.4", "react-dom": "^18.2.0", "react-hot-toast": "^2.4.1", "tailwind-merge": "^2.2.2", diff --git a/public/resources/models/pose/blend_pose.glb b/public/resources/models/pose/blend_pose.glb new file mode 100644 index 0000000000..3cce1fa632 Binary files /dev/null and b/public/resources/models/pose/blend_pose.glb differ