<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: imai</title>
    <description>The latest articles on Forem by imai (@masaakiimai).</description>
    <link>https://forem.com/masaakiimai</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2945436%2F03ad6b73-0f5e-49cc-a16d-0ae7032b91d2.jpeg</url>
      <title>Forem: imai</title>
      <link>https://forem.com/masaakiimai</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/masaakiimai"/>
    <language>en</language>
    <item>
      <title>How to Make Your Favorite Anime Character Dance in 3D on the Web (Three.js + DeepMotion)</title>
      <dc:creator>imai</dc:creator>
      <pubDate>Sat, 15 Mar 2025 11:12:02 +0000</pubDate>
      <link>https://forem.com/masaakiimai/how-to-make-your-favorite-anime-character-dance-in-3d-on-the-web-threejs-deepmotion-57fp</link>
      <guid>https://forem.com/masaakiimai/how-to-make-your-favorite-anime-character-dance-in-3d-on-the-web-threejs-deepmotion-57fp</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fladh8ae4ic1jqgxfpaoi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fladh8ae4ic1jqgxfpaoi.png" alt="Image description" width="800" height="610"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As an anime fan, I wanted to make my favorite anime characters dance freely to popular songs, so I implemented this using Three.js.&lt;br&gt;
*It only took 2 days to create and isn't particularly difficult!&lt;/p&gt;

&lt;p&gt;Check out the demo here 👇 (Please click "Dance Motion"! 🙇)&lt;/p&gt;

&lt;p&gt;Demo Site&lt;br&gt;
&lt;a href="https://3d-anime-mu.vercel.app/" rel="noopener noreferrer"&gt;https://3d-anime-mu.vercel.app/&lt;/a&gt;&lt;br&gt;
Github&lt;br&gt;
&lt;a href="https://github.com/masaaki-imai/3D-anime" rel="noopener noreferrer"&gt;https://github.com/masaaki-imai/3D-anime&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you like it, please give it a "like"! 😊&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;📌 Technologies &amp;amp; Tools Used&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Three.js&lt;/strong&gt; (3D display library)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DeepMotion&lt;/strong&gt; (SaaS for generating motion from videos)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vroid Studio&lt;/strong&gt; (Anime character creation)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cobalt&lt;/strong&gt; (YouTube video downloader)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Veed&lt;/strong&gt; (Video editing)&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;📌 Workflow to Completion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Follow these steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Choose a dance motion video&lt;/li&gt;
&lt;li&gt;Download and edit the video&lt;/li&gt;
&lt;li&gt;Generate motion with DeepMotion&lt;/li&gt;
&lt;li&gt;Create a character with Vroid Studio and convert to GLB format&lt;/li&gt;
&lt;li&gt;Animate the character with Three.js&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;strong&gt;① Choose a Dance Motion Video&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For this project, I used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Music&lt;/strong&gt;: YOASOBI "Idol"&lt;br&gt;
&lt;a href="https://www.youtube.com/watch?v=ZRtdQ81jPUQ" rel="noopener noreferrer"&gt;YouTube Link&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dance Motion&lt;/strong&gt;: Mio Sakura [Juring]&lt;br&gt;
&lt;a href="https://www.youtube.com/shorts/gYZzVHGrRcA" rel="noopener noreferrer"&gt;YouTube Link&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;② Download and Edit the Video&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To download YouTube videos, you can use services like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://cobalt.tools/" rel="noopener noreferrer"&gt;Cobalt&lt;/a&gt; (recommended)&lt;/li&gt;
&lt;li&gt;Any other YouTube download tool will work too&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Video Editing Requirements&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To generate motion with DeepMotion, your video must meet these conditions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Length: &lt;strong&gt;Under 20 seconds&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Frame rate: &lt;strong&gt;30 FPS&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Recommended video editing tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.veed.io/" rel="noopener noreferrer"&gt;Veed&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Any other video editing software will work&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;③ Generate Motion with DeepMotion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;DeepMotion is a convenient service that automatically generates 3D motion from videos.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a free account at &lt;a href="https://www.deepmotion.com/" rel="noopener noreferrer"&gt;DeepMotion's official site&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Upload your edited video and generate the motion&lt;/li&gt;
&lt;li&gt;Download the generated motion in GLB format&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;*Note: DeepMotion also offers an API, so you can automate this process with code (recommended for long-term use).&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;④ Create a Character with Vroid Studio and Convert to GLB&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Vroid Studio is a convenient tool for creating anime characters.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Download it for free from &lt;a href="https://vroid.com/en/studio" rel="noopener noreferrer"&gt;Vroid Studio's official site&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Export your created character in VRM format&lt;/li&gt;
&lt;li&gt;Convert the VRM format to GLB using an online conversion tool&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;⑤ Animate the Character with Three.js&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use Three.js to animate your character in the browser.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sample Project Structure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3d-app
├── public
│   └── 3d
│       ├── idle.mp3
│       ├── kakeruze.glb
│       ├── kawaii22.glb
│       ├── motion.glb
│       └── original_movie.mp4
├── src
│   └── app
│       ├── globals.css
│       ├── layout.tsx
│       └── page.tsx
├── package.json
└── tsconfig.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;








&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;'use client';

import React, { useState, useRef, useEffect, forwardRef, useImperativeHandle } from 'react';
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';

// Type extension (adding isBone property to THREE.Object3D)
declare module 'three' {
  interface Object3D {
    isBone?: boolean;
  }
}

// Type definitions
interface ModelData {
  scene: THREE.Group;
  animations: THREE.AnimationClip[];
}

interface Actions {

}

// ThreeScene component handle type definition
export interface ThreeSceneHandle {
  playDanceAnimation: (index: number) =&amp;gt; void;
}

// Model and motion path constants
const MODEL_PATHS = {
  CHARACTER: '/3d/kawaii22.glb',  // Main character model
  DANCE_MOTION: '/3d/motion.glb',  // Dance motion
  DANCE_MUSIC: '/3d/idle.mp3',     // Dance BGM
  ORIGINAL_VIDEO: '/3d/original_movie.mp4' // オリジナルビデオ
} as const;

// ThreeScene component property type definition
interface ThreeSceneProps {
  onModelLoaded: (loaded: boolean, error?: string) =&amp;gt; void;
}

// Bone name mapping table
const boneMapping: { [key: string]: string } = {
  // motion bone name: kawaii bone name
  'hips_JNT': 'J_Bip_C_Hips',
  'spine_JNT': 'J_Bip_C_Spine',
  'spine1_JNT': 'J_Bip_C_Chest',
  'spine2_JNT': 'J_Bip_C_UpperChest',
  'neck_JNT': 'J_Bip_C_Neck',
  'head_JNT': 'J_Bip_C_Head',

  // Left arm
  'l_shoulder_JNT': 'J_Bip_L_Shoulder',
  'l_arm_JNT': 'J_Bip_L_UpperArm',
  'l_forearm_JNT': 'J_Bip_L_LowerArm',
  'l_hand_JNT': 'J_Bip_L_Hand',

  // Right arm
  'r_shoulder_JNT': 'J_Bip_R_Shoulder',
  'r_arm_JNT': 'J_Bip_R_UpperArm',
  'r_forearm_JNT': 'J_Bip_R_LowerArm',
  'r_hand_JNT': 'J_Bip_R_Hand',

  // Left leg
  'l_upleg_JNT': 'J_Bip_L_UpperLeg',
  'l_leg_JNT': 'J_Bip_L_LowerLeg',
  'l_foot_JNT': 'J_Bip_L_Foot',
  'l_toebase_JNT': 'J_Bip_L_ToeBase',

  // Right leg
  'r_upleg_JNT': 'J_Bip_R_UpperLeg',
  'r_leg_JNT': 'J_Bip_R_LowerLeg',
  'r_foot_JNT': 'J_Bip_R_Foot',
  'r_toebase_JNT': 'J_Bip_R_ToeBase',

  // Finger (Left hand)
  'l_handThumb1_JNT': 'J_Bip_L_Thumb1',
  'l_handThumb2_JNT': 'J_Bip_L_Thumb2',
  'l_handThumb3_JNT': 'J_Bip_L_Thumb3',
  'l_handIndex1_JNT': 'J_Bip_L_Index1',
  'l_handIndex2_JNT': 'J_Bip_L_Index2',
  'l_handIndex3_JNT': 'J_Bip_L_Index3',
  'l_handMiddle1_JNT': 'J_Bip_L_Middle1',
  'l_handMiddle2_JNT': 'J_Bip_L_Middle2',
  'l_handMiddle3_JNT': 'J_Bip_L_Middle3',
  'l_handRing1_JNT': 'J_Bip_L_Ring1',
  'l_handRing2_JNT': 'J_Bip_L_Ring2',
  'l_handRing3_JNT': 'J_Bip_L_Ring3',
  'l_handPinky1_JNT': 'J_Bip_L_Little1',
  'l_handPinky2_JNT': 'J_Bip_L_Little2',
  'l_handPinky3_JNT': 'J_Bip_L_Little3',

  // Finger (Right hand)
  'r_handThumb1_JNT': 'J_Bip_R_Thumb1',
  'r_handThumb2_JNT': 'J_Bip_R_Thumb2',
  'r_handThumb3_JNT': 'J_Bip_R_Thumb3',
  'r_handIndex1_JNT': 'J_Bip_R_Index1',
  'r_handIndex2_JNT': 'J_Bip_R_Index2',
  'r_handIndex3_JNT': 'J_Bip_R_Index3',
  'r_handMiddle1_JNT': 'J_Bip_R_Middle1',
  'r_handMiddle2_JNT': 'J_Bip_R_Middle2',
  'r_handMiddle3_JNT': 'J_Bip_R_Middle3',
  'r_handRing1_JNT': 'J_Bip_R_Ring1',
  'r_handRing2_JNT': 'J_Bip_R_Ring2',
  'r_handRing3_JNT': 'J_Bip_R_Ring3',
  'r_handPinky1_JNT': 'J_Bip_R_Little1',
  'r_handPinky2_JNT': 'J_Bip_R_Little2',
  'r_handPinky3_JNT': 'J_Bip_R_Little3'
};

// Model load duplicate prevention flag - maintained at module level
let isLoading = false;

// ThreeScene component
const ThreeScene = forwardRef&amp;lt;ThreeSceneHandle, ThreeSceneProps&amp;gt;((props, ref) =&amp;gt; {
  const mountRef = useRef&amp;lt;HTMLDivElement&amp;gt;(null);
  const audioRef = useRef&amp;lt;HTMLAudioElement | null&amp;gt;(null);

  // Animation-related state
  const animationRef = useRef&amp;lt;number&amp;gt;(0);
  const sceneRef = useRef&amp;lt;THREE.Scene | null&amp;gt;(null);
  const cameraRef = useRef&amp;lt;THREE.PerspectiveCamera | null&amp;gt;(null);
  const rendererRef = useRef&amp;lt;THREE.WebGLRenderer | null&amp;gt;(null);
  const controlsRef = useRef&amp;lt;OrbitControls | null&amp;gt;(null);
  const mixerRef = useRef&amp;lt;THREE.AnimationMixer | null&amp;gt;(null);
  const clockRef = useRef&amp;lt;THREE.Clock&amp;gt;(new THREE.Clock());
  const modelRef = useRef&amp;lt;THREE.Group | null&amp;gt;(null);
  const actionsRef = useRef&amp;lt;Actions&amp;gt;({});
  const currentActionRef = useRef&amp;lt;THREE.AnimationAction | null&amp;gt;(null);
  const danceModelRef = useRef&amp;lt;ModelData | null&amp;gt;(null);
  const danceAnimationsRef = useRef&amp;lt;THREE.AnimationClip[]&amp;gt;([]);
  const isComponentMounted = useRef(false);

  // GLB model paths
  const modelPath = MODEL_PATHS.CHARACTER;
  const dancePath = MODEL_PATHS.DANCE_MOTION;

  // Animation retargeting (for when bone structures are different)
  const retargetAnimation = (clip: THREE.AnimationClip): THREE.AnimationClip =&amp;gt; {
    const newClip = THREE.AnimationClip.parse(THREE.AnimationClip.toJSON(clip));
    const newTracks: THREE.KeyframeTrack[] = [];

    newClip.tracks.forEach(track =&amp;gt; {
      const [boneName, property] = track.name.split('.');
      if (boneMapping[boneName]) {
        const newTrack = new THREE.KeyframeTrack(
          `${boneMapping[boneName]}.${property}`,
          track.times,
          track.values.slice()
        );
        newTracks.push(newTrack);
      }
    });

    return new THREE.AnimationClip(clip.name, clip.duration, newTracks);
  };

  // Function to process external animations
  const processExternalAnimations = () =&amp;gt; {
    if (!danceAnimationsRef.current.length || !modelRef.current || !mixerRef.current) {
      console.warn('Missing data required for animation processing');
      return;
    }

    try {
      danceAnimationsRef.current.forEach((clip, index) =&amp;gt; {
        const retargetedClip = retargetAnimation(clip);
        const action = mixerRef.current!.clipAction(retargetedClip);
        actionsRef.current[`dance_${index}`] = action;
      });
    } catch (error) {
      console.error('Failed to process external animations:', error);
    }
  };

  // Function to load models
  const loadModels = async () =&amp;gt; {
    if (isLoading) return;
    isLoading = true;

    try {
      const loader = new GLTFLoader();

      // Load main model
      const characterGltf = await loader.loadAsync(modelPath);
      modelRef.current = characterGltf.scene;

      // Adjust model scale and position
      modelRef.current.scale.set(1.5, 1.5, 1.5);
      modelRef.current.position.set(0, 0, 0);

      // Add model to scene
      if (sceneRef.current) {
        sceneRef.current.add(modelRef.current);
      }

      // Set up animation mixer
      mixerRef.current = new THREE.AnimationMixer(modelRef.current);

      // Set up original model animations
      if (characterGltf.animations &amp;amp;&amp;amp; characterGltf.animations.length &amp;gt; 0) {
        characterGltf.animations.forEach((clip, index) =&amp;gt; {
          const action = mixerRef.current!.clipAction(clip);
          actionsRef.current[`original_${index}`] = action;
        });
      }

      // Load dance model
      const danceGltf = await loader.loadAsync(dancePath);
      danceModelRef.current = danceGltf;
      danceAnimationsRef.current = danceGltf.animations;

      // Process dance animations
      if (danceAnimationsRef.current.length &amp;gt; 0) {
        processExternalAnimations();
      }

      props.onModelLoaded(true);
    } catch (error) {
      console.error('Failed to load model:', error);
      const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
      props.onModelLoaded(false, errorMessage);
    } finally {
      isLoading = false;
    }
  };

  // Animation loop
  const animate = () =&amp;gt; {
    if (!mountRef.current) return;

    const delta = clockRef.current.getDelta();

    // Update mixer
    if (mixerRef.current) {
      mixerRef.current.update(delta);
    }

    // Update controls
    if (controlsRef.current) {
      controlsRef.current.update();
    }

    // Rendering
    if (rendererRef.current &amp;amp;&amp;amp; sceneRef.current &amp;amp;&amp;amp; cameraRef.current) {
      rendererRef.current.render(sceneRef.current, cameraRef.current);
    }

    animationRef.current = requestAnimationFrame(animate);
  };

  // Scene initialization
  useEffect(() =&amp;gt; {
    if (!mountRef.current || isComponentMounted.current) return;
    isComponentMounted.current = true;

    // Scene setup
    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0x000022);
    sceneRef.current = scene;

    // Camera setup
    const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
    camera.position.set(0, 1.5, 3);
    cameraRef.current = camera;

    // Renderer setup
    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.shadowMap.enabled = true;
    mountRef.current.appendChild(renderer.domElement);
    rendererRef.current = renderer;

    // Controls setup
    const controls = new OrbitControls(camera, renderer.domElement);
    controls.target.set(0, 1.82, 0.1);
    controls.update();
    controlsRef.current = controls;

    // Light setup
    const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
    scene.add(ambientLight);

    const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
    directionalLight.position.set(5, 5, 5);
    directionalLight.castShadow = true;
    scene.add(directionalLight);

    // Grid helper
    const gridHelper = new THREE.GridHelper(10, 10);
    scene.add(gridHelper);

    // Floor creation (with reflection effect)
    const floorGeometry = new THREE.CircleGeometry(10, 64);
    const floorMaterial = new THREE.MeshStandardMaterial({
      color: 0x6666aa,
      metalness: 0.9,
      roughness: 0.1,
    });
    const floor = new THREE.Mesh(floorGeometry, floorMaterial);
    floor.rotation.x = -Math.PI / 2;
    floor.receiveShadow = true;
    scene.add(floor);

    // Fog addition
    scene.fog = new THREE.Fog(0x000022, 1, 15);

    // Load models
    loadModels();

    // Animation start
    animate();

    // Resize handler
    const handleResize = () =&amp;gt; {
      if (!cameraRef.current || !rendererRef.current) return;

      cameraRef.current.aspect = window.innerWidth / window.innerHeight;
      cameraRef.current.updateProjectionMatrix();
      rendererRef.current.setSize(window.innerWidth, window.innerHeight);
    };

    window.addEventListener('resize', handleResize);

    // Cleanup
    return () =&amp;gt; {
      window.removeEventListener('resize', handleResize);

      if (animationRef.current) {
        cancelAnimationFrame(animationRef.current);
      }

      if (mountRef.current &amp;amp;&amp;amp; rendererRef.current) {
        mountRef.current.removeChild(rendererRef.current.domElement);
      }

      if (mixerRef.current) {
        mixerRef.current.stopAllAction();
      }

      if (sceneRef.current) {
        // Scene cleanup
        sceneRef.current.traverse((object) =&amp;gt; {
          if (object instanceof THREE.Mesh) {
            object.geometry.dispose();
            if (object.material instanceof THREE.Material) {
              object.material.dispose();
            } else if (Array.isArray(object.material)) {
              object.material.forEach(material =&amp;gt; material.dispose());
            }
          }
        });
      }

      if (rendererRef.current) {
        rendererRef.current.dispose();
      }

      // Audio stop and cleanup
      if (audioRef.current) {
        audioRef.current.pause();
        audioRef.current.src = '';
      }

      isComponentMounted.current = false;
    };
  }, []);

  // Method exposure
  useImperativeHandle(ref, () =&amp;gt; ({
    // Play dance animation
    playDanceAnimation: (index: number) =&amp;gt; {
      console.log(`playDanceAnimation(${index}) called`);

      // Stop existing animation
      if (currentActionRef.current) {
        currentActionRef.current.fadeOut(0.5);
        currentActionRef.current.stop();
      }

      // Music playback - 削除：ビデオの音声と重複するため無効化
      // if (!audioRef.current) {
      //   audioRef.current = new Audio(MODEL_PATHS.DANCE_MUSIC);
      //   audioRef.current.loop = true;
      // }
      // audioRef.current.play().catch(error =&amp;gt; {
      //   console.warn('Failed to play music:', error);
      // });

      // Play specified dance animation
      const animationName = `dance_${index}`;
      if (mixerRef.current &amp;amp;&amp;amp; actionsRef.current[animationName]) {
        currentActionRef.current = actionsRef.current[animationName];
        currentActionRef.current.reset();
        currentActionRef.current.fadeIn(0.5);
        currentActionRef.current.play();
        currentActionRef.current.setLoop(THREE.LoopRepeat, Infinity);
        console.log(`Playing dance animation ${index}`);
      } else {
        console.warn(`Dance animation ${animationName} not found`);
      }
    }
  }));

  return (
    &amp;lt;div
      ref={mountRef}
      className="w-full h-full"
      style={{ overflow: 'hidden' }}
    /&amp;gt;
  );
});

ThreeScene.displayName = 'ThreeScene';

// VideoOverlay component
interface VideoOverlayProps {
  isVisible: boolean;
  videoSrc: string;
  onClose: () =&amp;gt; void;
}

const VideoOverlay: React.FC&amp;lt;VideoOverlayProps&amp;gt; = ({ isVisible, videoSrc, onClose }) =&amp;gt; {
  if (!isVisible) return null;

  return (
    &amp;lt;div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80"&amp;gt;
      &amp;lt;div className="relative max-w-5xl w-full"&amp;gt;
        &amp;lt;button
          onClick={onClose}
          className="absolute -top-10 right-0 text-white text-2xl hover:text-red-500"
        &amp;gt;
          ✕
        &amp;lt;/button&amp;gt;
        &amp;lt;video
          controls
          autoPlay
          className="w-full rounded-lg shadow-2xl"
        &amp;gt;
          &amp;lt;source src={videoSrc} type="video/mp4" /&amp;gt;
          お使いのブラウザはビデオタグをサポートしていません。
        &amp;lt;/video&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

// PictureInPictureVideo component
interface PiPVideoProps {
  isVisible: boolean;
  videoSrc: string;
  onClose: () =&amp;gt; void;
}

const PictureInPictureVideo: React.FC&amp;lt;PiPVideoProps&amp;gt; = ({ isVisible, videoSrc, onClose }) =&amp;gt; {
  const videoRef = useRef&amp;lt;HTMLVideoElement&amp;gt;(null);

  // ビデオが表示/非表示になったときの処理
  useEffect(() =&amp;gt; {
    if (isVisible &amp;amp;&amp;amp; videoRef.current) {
      // ビデオが表示されたら再生を確実に開始
      const playVideo = async () =&amp;gt; {
        try {
          // 先にビデオをロードしておく
          videoRef.current!.load();
          // 少し待ってから再生開始 (ダンスアニメーションとの同期を改善)
          await new Promise(resolve =&amp;gt; setTimeout(resolve, 100));
          await videoRef.current!.play();
        } catch (error) {
          console.warn('Failed to play video:', error);
        }
      };

      playVideo();
    } else if (!isVisible &amp;amp;&amp;amp; videoRef.current) {
      // 非表示になったら一時停止
      videoRef.current.pause();
    }
  }, [isVisible]);

  if (!isVisible) return null;

  return (
    &amp;lt;div className="fixed bottom-5 right-5 z-20 w-48 md:w-64 lg:w-80 shadow-xl rounded-lg overflow-hidden"&amp;gt;
      &amp;lt;div className="relative bg-black"&amp;gt;
        &amp;lt;button
          onClick={onClose}
          className="absolute top-2 right-2 text-white text-xl bg-black/50 rounded-full w-6 h-6 flex items-center justify-center hover:bg-red-500/80 z-10"
        &amp;gt;
          ✕
        &amp;lt;/button&amp;gt;
        &amp;lt;video
          ref={videoRef}
          controls
          autoPlay
          loop
          muted
          className="w-full"
          preload="auto"
          playsInline
        &amp;gt;
          &amp;lt;source src={videoSrc} type="video/mp4" /&amp;gt;
          お使いのブラウザはビデオタグをサポートしていません。
        &amp;lt;/video&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

// Credits modal component
interface CreditsModalProps {
  isVisible: boolean;
  onClose: () =&amp;gt; void;
}

const CreditsModal: React.FC&amp;lt;CreditsModalProps&amp;gt; = ({ isVisible, onClose }) =&amp;gt; {
  if (!isVisible) return null;

  return (
    &amp;lt;div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80"&amp;gt;
      &amp;lt;div className="relative bg-gray-900 p-8 rounded-lg max-w-2xl w-full mx-4"&amp;gt;
        &amp;lt;button
          onClick={onClose}
          className="absolute -top-10 right-0 text-white text-2xl hover:text-red-500"
        &amp;gt;
          ✕
        &amp;lt;/button&amp;gt;
        &amp;lt;h2 className="text-2xl font-bold mb-4 text-white"&amp;gt;Credits&amp;lt;/h2&amp;gt;
        &amp;lt;div className="text-gray-300 space-y-4"&amp;gt;
          &amp;lt;div&amp;gt;
            &amp;lt;h3 className="text-xl font-semibold mb-2"&amp;gt;Dance Motion&amp;lt;/h3&amp;gt;
            &amp;lt;p&amp;gt;三桜じゅり【じゅりんぐる】&amp;lt;/p&amp;gt;
            &amp;lt;a
              href="https://www.youtube.com/shorts/gYZzVHGrRcA"
              target="_blank"
              rel="noopener noreferrer"
              className="text-blue-400 hover:text-blue-300"
            &amp;gt;
              Dance Reference Video
            &amp;lt;/a&amp;gt;
          &amp;lt;/div&amp;gt;
          &amp;lt;div&amp;gt;
            &amp;lt;h3 className="text-xl font-semibold mb-2"&amp;gt;Music&amp;lt;/h3&amp;gt;
            &amp;lt;p&amp;gt;YOASOBI「アイドル」 Official Music&amp;lt;/p&amp;gt;
            &amp;lt;a
              href="https://www.youtube.com/watch?v=ZRtdQ81jPUQ"
              target="_blank"
              rel="noopener noreferrer"
              className="text-blue-400 hover:text-blue-300"
            &amp;gt;
              YouTube Video
            &amp;lt;/a&amp;gt;
          &amp;lt;/div&amp;gt;
          &amp;lt;div&amp;gt;
            &amp;lt;h3 className="text-xl font-semibold mb-2"&amp;gt;Anime Character&amp;lt;/h3&amp;gt;
            &amp;lt;p&amp;gt;Created using VRoid Studio&amp;lt;/p&amp;gt;
            &amp;lt;a
              href="https://vroid.com/en/studio"
              target="_blank"
              rel="noopener noreferrer"
              className="text-blue-400 hover:text-blue-300"
            &amp;gt;
              VRoid Studio Website
            &amp;lt;/a&amp;gt;
          &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

// Controls component
interface ControlsProps {
  onIntergalactiaDance: () =&amp;gt; void;
  isModelLoaded: boolean;
  githubUrl?: string;
}

const Controls: React.FC&amp;lt;ControlsProps&amp;gt; = ({
  onIntergalactiaDance,
  isModelLoaded,
  githubUrl = "https://github.com"
}) =&amp;gt; {
  const [showCredits, setShowCredits] = useState(false);

  // Common button class
  const buttonClass = `
    px-5 py-2.5
    rounded-lg
    font-medium
    text-white
    shadow-lg
    transition-all
    duration-200
    disabled:opacity-50
    disabled:cursor-not-allowed
    transform hover:-translate-y-1 hover:shadow-xl
    focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-black focus:ring-opacity-50
  `;

  const handleReset = () =&amp;gt; {
    window.location.reload();
  };

  const handleGithub = () =&amp;gt; {
    window.open(githubUrl, '_blank');
  };

  return (
    &amp;lt;&amp;gt;
      &amp;lt;div className="absolute bottom-5 left-5 bg-black/70 p-4 rounded-lg text-white backdrop-blur-md shadow-lg z-10 flex flex-wrap gap-3 md:flex-row"&amp;gt;
        &amp;lt;button
          onClick={onIntergalactiaDance}
          disabled={!isModelLoaded}
          className={`${buttonClass} bg-green-600 hover:bg-green-700 focus:ring-green-500`}
        &amp;gt;
          Dance Motion
        &amp;lt;/button&amp;gt;

        &amp;lt;button
          onClick={handleReset}
          className={`${buttonClass} bg-gray-600 hover:bg-gray-700 focus:ring-gray-500`}
        &amp;gt;
          Reset
        &amp;lt;/button&amp;gt;

        &amp;lt;button
          onClick={handleGithub}
          className={`${buttonClass} bg-purple-600 hover:bg-purple-700 focus:ring-purple-500 flex items-center justify-center gap-2`}
        &amp;gt;
          &amp;lt;svg
            className="w-5 h-5"
            viewBox="0 0 24 24"
            fill="currentColor"
            xmlns="http://www.w3.org/2000/svg"
          &amp;gt;
            &amp;lt;path d="M12 0C5.37 0 0 5.37 0 12c0 5.3 3.44 9.8 8.2 11.38.6.12.83-.26.83-.57v-2c-3.34.73-4.03-1.6-4.03-1.6-.55-1.4-1.34-1.77-1.34-1.77-1.08-.74.08-.73.08-.73 1.2.08 1.83 1.23 1.83 1.23 1.07 1.84 2.8 1.3 3.5 1 .1-.78.42-1.3.76-1.6-2.67-.3-5.47-1.34-5.47-5.93 0-1.3.47-2.38 1.24-3.22-.14-.3-.54-1.52.1-3.18 0 0 1-.32 3.3 1.23a11.5 11.5 0 016 0c2.28-1.55 3.3-1.23 3.3-1.23.64 1.66.24 2.88.12 3.18.76.84 1.23 1.9 1.23 3.22 0 4.6-2.8 5.63-5.48 5.92.42.36.8 1.1.8 2.2v3.3c0 .3.2.7.82.57C20.56 21.8 24 17.3 24 12c0-6.63-5.37-12-12-12z"/&amp;gt;
          &amp;lt;/svg&amp;gt;
          GitHub
        &amp;lt;/button&amp;gt;

        &amp;lt;button
          onClick={() =&amp;gt; setShowCredits(true)}
          className={`${buttonClass} bg-blue-600 hover:bg-blue-700 focus:ring-blue-500 flex items-center justify-center gap-2`}
        &amp;gt;
          &amp;lt;svg
            className="w-5 h-5"
            viewBox="0 0 24 24"
            fill="none"
            stroke="currentColor"
            xmlns="http://www.w3.org/2000/svg"
          &amp;gt;
            &amp;lt;path
              strokeLinecap="round"
              strokeLinejoin="round"
              strokeWidth={2}
              d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
            /&amp;gt;
          &amp;lt;/svg&amp;gt;
          Credits
        &amp;lt;/button&amp;gt;
      &amp;lt;/div&amp;gt;

      &amp;lt;CreditsModal
        isVisible={showCredits}
        onClose={() =&amp;gt; setShowCredits(false)}
      /&amp;gt;
    &amp;lt;/&amp;gt;
  );
};

// ErrorNotice component
interface ErrorNoticeProps {
  isVisible: boolean;
  message?: string;
}

const ErrorNotice: React.FC&amp;lt;ErrorNoticeProps&amp;gt; = ({ isVisible, message }) =&amp;gt; {
  if (!isVisible) return null;

  return (
    &amp;lt;div className="absolute top-16 left-5 right-5 bg-red-700/80 p-4 rounded-lg text-white text-sm leading-relaxed max-w-3xl z-10 animate-fade-in backdrop-blur-md shadow-lg"&amp;gt;
      &amp;lt;h3 className="mt-0 text-lg font-bold mb-2 text-pink-100"&amp;gt;⚠️ An Error Occurred&amp;lt;/h3&amp;gt;

      {message ? (
        &amp;lt;p&amp;gt;{message}&amp;lt;/p&amp;gt;
      ) : (
        &amp;lt;&amp;gt;
          &amp;lt;p&amp;gt;The following GLB files are required to run this demo:&amp;lt;/p&amp;gt;
          &amp;lt;code className="bg-black/30 px-2 py-1 rounded font-mono inline-block m-1"&amp;gt;{MODEL_PATHS.CHARACTER}&amp;lt;/code&amp;gt;
          &amp;lt;code className="bg-black/30 px-2 py-1 rounded font-mono inline-block m-1"&amp;gt;{MODEL_PATHS.DANCE_MOTION}&amp;lt;/code&amp;gt;
          &amp;lt;p&amp;gt;Please place the GLB files in the correct location and refresh the page.&amp;lt;/p&amp;gt;
        &amp;lt;/&amp;gt;
      )}
    &amp;lt;/div&amp;gt;
  );
};

// Main Home component
const Home = () =&amp;gt; {
  const [isModelLoaded, setIsModelLoaded] = useState(false);
  const [errorMessage, setErrorMessage] = useState('');
  const [loadingStatus, setLoadingStatus] = useState('Loading...');
  const [showVideo, setShowVideo] = useState(false);
  const threeSceneRef = useRef&amp;lt;ThreeSceneHandle&amp;gt;(null);
  const loadStartTime = useRef&amp;lt;number&amp;gt;(Date.now());
  const isFirstLoad = useRef&amp;lt;boolean&amp;gt;(true);
  const videoPreloadRef = useRef&amp;lt;HTMLVideoElement | null&amp;gt;(null);
  const audioRef = useRef&amp;lt;HTMLAudioElement | null&amp;gt;(null);

  // ページロード時にビデオを事前にロード
  useEffect(() =&amp;gt; {
    // ビデオを事前にロードしておく
    videoPreloadRef.current = new Audio(MODEL_PATHS.ORIGINAL_VIDEO) as unknown as HTMLVideoElement;
    videoPreloadRef.current.preload = 'auto';
    videoPreloadRef.current.load();

    return () =&amp;gt; {
      if (videoPreloadRef.current) {
        videoPreloadRef.current.src = '';
      }
    };
  }, []);

  // Animation function
  const handleIntergalactiaDance = () =&amp;gt; {
    if (threeSceneRef.current) {
      setShowVideo(true);

      // 音楽を最初から再生
      if (!audioRef.current) {
        audioRef.current = new Audio(MODEL_PATHS.DANCE_MUSIC);
        audioRef.current.loop = true;
      }
      audioRef.current.play().catch(error =&amp;gt; {
        console.warn('Failed to play music:', error);
      });

      setTimeout(() =&amp;gt; {
        threeSceneRef.current!.playDanceAnimation(0);
      }, 50);
    }
  };

  // ビデオを閉じる関数
  const handleCloseVideo = () =&amp;gt; {
    setShowVideo(false);

    // 動画を閉じた時に音楽を再生
    if (!audioRef.current) {
      audioRef.current = new Audio(MODEL_PATHS.DANCE_MUSIC);
      audioRef.current.loop = true;
    }
    audioRef.current.play().catch(error =&amp;gt; {
      console.warn('Failed to play music:', error);
    });
  };

  // Model load state handler
  const handleModelLoad = (loaded: boolean, error?: string) =&amp;gt; {
    console.log("Model load state:", { loaded, error });

    // Avoid duplicate processing if already loaded
    if (isModelLoaded &amp;amp;&amp;amp; !error &amp;amp;&amp;amp; !isFirstLoad.current) {
      console.log("Model is already loaded");
      return;
    }

    isFirstLoad.current = false;

    if (error) {
      setErrorMessage(error.includes("Not Found") ?
        "Model files not found. Please place GLB files in '/public/3d/'." :
        error
      );
      setLoadingStatus('Model loading error');
    } else {
      const loadTime = Date.now() - loadStartTime.current;
      console.log(`Model load completion time: ${loadTime}ms`);
      setIsModelLoaded(loaded);
      setLoadingStatus('');
      setErrorMessage('');
    }
  };

  // Model load progress display
  useEffect(() =&amp;gt; {
    loadStartTime.current = Date.now();

    const loadingTimer = setTimeout(() =&amp;gt; {
      if (!isModelLoaded &amp;amp;&amp;amp; !errorMessage) {
        setLoadingStatus('Loading...(please wait)');
      }
    }, 3000);

    return () =&amp;gt; clearTimeout(loadingTimer);
  }, [isModelLoaded, errorMessage]);

  // Inline styles (Tailwind compatibility workaround)
  const forceStyles = {
    container: {
      position: 'relative' as const,
      width: '100%',
      height: '100vh',
      overflow: 'hidden',
      zIndex: 0,
      background: '#111'
    },
    title: {
      textShadow: '0 0 10px rgba(255,255,255,0.5)'
    },
    threeContainer: {
      position: 'absolute' as const,
      top: 0,
      left: 0,
      width: '100%',
      height: '100%',
      zIndex: 1
    }
  };

  return (
    &amp;lt;div style={forceStyles.container} className="relative w-full h-screen overflow-hidden"&amp;gt;
      {!isModelLoaded &amp;amp;&amp;amp; !errorMessage &amp;amp;&amp;amp; (
        &amp;lt;div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-black/70 p-4 rounded-lg text-white backdrop-blur-md z-20"&amp;gt;
          &amp;lt;p className="text-center"&amp;gt;{loadingStatus}&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
      )}

      &amp;lt;ErrorNotice
        isVisible={!!errorMessage}
        message={errorMessage}
      /&amp;gt;

      &amp;lt;Controls
        onIntergalactiaDance={handleIntergalactiaDance}
        isModelLoaded={isModelLoaded}
        githubUrl="https://github.com/masaaki-imai/3D-anime/blob/main/src/app/page.tsx"
      /&amp;gt;

      &amp;lt;div
        style={forceStyles.threeContainer}
        className="absolute inset-0 z-1"
      &amp;gt;
        &amp;lt;ThreeScene
          ref={threeSceneRef}
          onModelLoaded={handleModelLoad}
        /&amp;gt;
      &amp;lt;/div&amp;gt;

      &amp;lt;PictureInPictureVideo
        isVisible={showVideo}
        videoSrc={MODEL_PATHS.ORIGINAL_VIDEO}
        onClose={handleCloseVideo}
      /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

export default Home;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;🎉 Conclusion&lt;/strong&gt;&lt;br&gt;
By applying these steps, you can freely animate your favorite anime characters as you wish.&lt;br&gt;
Please try creating your own original work!&lt;br&gt;
That's all for now!&lt;/p&gt;

</description>
      <category>theejs</category>
      <category>nextjs</category>
      <category>typescript</category>
      <category>animation</category>
    </item>
  </channel>
</rss>
