11"use client" ;
2- import { AiOutlineLoading3Quarters } from "react-icons/ai" ;
3- import React , { useState , useEffect } from "react" ;
2+ import { useState , useEffect , useRef , FC } from "react" ;
43import { Canvas , useFrame } from "@react-three/fiber" ;
54// @ts -ignore
65import { GLTF , GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader" ;
6+ import { AnimationAction } from "three" ;
77import { VRMLoaderPlugin } from "@pixiv/three-vrm" ;
88import {
99 VRMAnimationLoaderPlugin ,
1010 createVRMAnimationClip ,
1111} from "@pixiv/three-vrm-animation" ;
1212import { AnimationMixer , LoopOnce , LoopRepeat } from "three" ;
13+ import { PiPlayPause , PiRepeat } from "react-icons/pi" ;
1314
1415const animations = [
15- { url : "https://cdn.mikn.dev/vroid/shikanoko.vrma" , loop : true , percentage : 10 } ,
16- { url : "https://cdn.mikn.dev/vroid/hi.vrma" , loop : false , percentage : 70 } ,
17- { url : "https://cdn.mikn.dev/vroid/uishig.vrma" , loop : true , percentage : 10 } ,
18- { url : "https://cdn.mikn.dev/vroid/tetoris.vrma" , loop : false , percentage : 10 } ,
16+ {
17+ url : "https://cdn.mikn.dev/vroid/shikanoko.vrma" ,
18+ loop : true ,
19+ percentage : 10 ,
20+ } ,
21+ { url : "https://cdn.mikn.dev/vroid/hi.vrma" , loop : false , percentage : 50 } ,
22+ {
23+ url : "https://cdn.mikn.dev/vroid/uishig.vrma" ,
24+ loop : false ,
25+ percentage : 10 ,
26+ } ,
27+ {
28+ url : "https://cdn.mikn.dev/vroid/tetoris.vrma" ,
29+ loop : false ,
30+ percentage : 10 ,
31+ } ,
32+ {
33+ url : "https://cdn.mikn.dev/vroid/telepathy.vrma" ,
34+ loop : false ,
35+ percentage : 10 ,
36+ } ,
37+ {
38+ url : "https://cdn.mikn.dev/vroid/soware.vrma" ,
39+ loop : false ,
40+ percentage : 10 ,
41+ } ,
1942] ;
2043
21- export const VRMModel : React . FC < {
44+ export const VRMModel : FC < {
2245 vrm : import ( "@pixiv/three-vrm" ) . VRM | null ;
2346 mixer : AnimationMixer | null ;
2447} > = ( { vrm, mixer } ) => {
@@ -43,6 +66,7 @@ export function VRM() {
4366 const [ vrm , setVrm ] = useState < import ( "@pixiv/three-vrm" ) . VRM | null > ( null ) ;
4467 const [ mixer , setMixer ] = useState < AnimationMixer | null > ( null ) ;
4568 const [ isLoaded , setIsLoaded ] = useState ( false ) ;
69+ const actionRef = useRef < AnimationAction | null > ( null ) ;
4670
4771 useEffect ( ( ) => {
4872 const loader = new GLTFLoader ( ) ;
@@ -73,7 +97,10 @@ export function VRM() {
7397 } ;
7498
7599 function pickAnimation ( ) {
76- const total = animations . reduce ( ( sum , anim ) => sum + anim . percentage , 0 ) ;
100+ const total = animations . reduce (
101+ ( sum , anim ) => sum + anim . percentage ,
102+ 0 ,
103+ ) ;
77104 const rand = Math . random ( ) * total ;
78105 let acc = 0 ;
79106 for ( const anim of animations ) {
@@ -96,8 +123,11 @@ export function VRM() {
96123 vrmAnimations [ 0 ] ,
97124 loadedVrm ,
98125 ) ;
99- const animationMixer = new AnimationMixer ( loadedVrm . scene ) ;
100- const action = animationMixer . clipAction ( animationClip ) ;
126+ const animationMixer = new AnimationMixer (
127+ loadedVrm . scene ,
128+ ) ;
129+ const action =
130+ animationMixer . clipAction ( animationClip ) ;
101131
102132 if ( loop ) {
103133 action . setLoop ( LoopRepeat , Infinity ) ;
@@ -108,14 +138,20 @@ export function VRM() {
108138
109139 action . play ( ) ;
110140 setMixer ( animationMixer ) ;
141+ actionRef . current = action ;
111142 } else {
112- console . error ( "VRM model or humanoid is not loaded correctly." ) ;
143+ console . error (
144+ "VRM model or humanoid is not loaded correctly." ,
145+ ) ;
113146 }
114147 }
115148 } ,
116149 undefined ,
117150 ( error : Error ) => {
118- console . error ( "An error occurred while loading the animation:" , error ) ;
151+ console . error (
152+ "An error occurred while loading the animation:" ,
153+ error ,
154+ ) ;
119155 } ,
120156 ) ;
121157 } ;
@@ -131,19 +167,52 @@ export function VRM() {
131167 } , [ ] ) ;
132168
133169 return (
134- < div className = "flex justify-center items-center w-96 h-96" >
135- { ! isLoaded ? (
136- < div className = "flex items-center justify-center p-4" >
137- < span
138- className = "loading loading-xl loading-spinner text-primary"
139- />
140- </ div >
141- ) : (
142- < Canvas camera = { { position : [ 0 , 0 , 3 ] } } >
143- < ambientLight intensity = { 1.7 } />
144- < VRMModel vrm = { vrm } mixer = { mixer } />
145- </ Canvas >
146- ) }
170+ < div >
171+ < div className = "flex flex-col justify-center items-center w-96 h-96" >
172+ { ! isLoaded ? (
173+ < div className = "flex items-center justify-center p-4" >
174+ < span className = "loading loading-xl loading-spinner text-primary" />
175+ </ div >
176+ ) : (
177+ < a
178+ href = { "https://youtube.com/@mikndotdev" }
179+ target = "_blank"
180+ className = { "block w-full h-full" }
181+ >
182+ < Canvas camera = { { position : [ 0 , 0 , 3 ] } } >
183+ < ambientLight intensity = { 1.7 } />
184+ < VRMModel vrm = { vrm } mixer = { mixer } />
185+ </ Canvas >
186+ </ a >
187+ ) }
188+ </ div >
189+ < div className = "flex gap-2 mt-5 w-full justify-center" >
190+ < PiPlayPause
191+ className = "text-primary w-10 h-10 cursor-pointer"
192+ onClick = { ( ) => {
193+ if ( ! actionRef . current ) return ;
194+ if ( actionRef . current ?. paused ) {
195+ actionRef . current . paused = false ;
196+ } else {
197+ actionRef . current . paused = true ;
198+ }
199+ } }
200+ >
201+ Pause
202+ </ PiPlayPause >
203+ < PiRepeat
204+ className = "text-primary w-10 h-10 cursor-pointer"
205+ onClick = { ( ) => {
206+ if ( actionRef . current ) {
207+ actionRef . current . reset ( ) ;
208+ actionRef . current . paused = false ;
209+ actionRef . current . play ( ) ;
210+ }
211+ } }
212+ >
213+ Restart
214+ </ PiRepeat >
215+ </ div >
147216 </ div >
148217 ) ;
149218}
0 commit comments