minecraft-web/frontend/src/components/SkinViewer.jsx

123 lines
5.6 KiB
JavaScript

import React, { useEffect, useRef } from 'react';
import * as skinview3d from 'skinview3d';
const STEVE_SKIN_BASE64 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAEwElEQVR4Xu1av2sUURgMqCABQQVBBK0SCdooMQQD5jSFkNgpKdIEwSZoZ6GYJohNUmlhqrSxsUlhYZM/If/TmdncrLOz3+7drcneXtyB4f36bvNm3vd2l32ZmOiD+3eudMHZqWtJyTrbn189LqVfb+xAwZ17N1LxqLsBH17MZniuDFDBXn5b6+TEkxjz640dXLSmP8r/wgDd+74VILJoC5wLA3TFNRsgHqRQv/mh7+v64vkwwO8BSoiE4KLSrzd2UNF+QwQpdnn6aobs9+s1DpraFDZ183Im5ZkFSjVBf6PX0WtEcSh9PrUjmiBIIVpX8V7yd5Foxvn10efzqR0qwifMvr03y91fn9a7v7+8S8qf79eSvkfT13OxINvs0zpNYJ/Pp3ZwQjpRXUkIhWAIp3gSY5Go6JqsM55tn0/t0ImxxCMM4iACQoGXT3ZS0agDaCMGffhNdC0aGhnUCAN8hShIyRV/+/RBQs0AJUX5yuv9QQ1B2+dTOzgRlFx5PsO/v15MX2jwaPu4NJdw6fZkJoaPPGaCXtMN8LbPp3bwrU3FuwkoIRomsKRoGqS/5fX0zfDurckM2e/zGTkWHu50lfPz8wlnZmYSenwOR0fp9lBj0Ycxv77TL5fDwUE34f5+Znt6WGX4hCicRnh8DsciVbyacNoGqNEeVhk+oSoZcNYGTB4epgY0MgPOdAscC9ct8M8Z8Gxurwvij6N8vvAjJftYkjp+cXc3Q0/PVHxv4kXxYKafQpW9uLS9tZWlxg4KChzUAB0HMdlL29sJU0E9oUxP74/iMyK9j/2RARsbJ6QBHB8UZQI9O3wchJALm5sJUdcURel9Hu8muFkZ8WJGMlZkADgoygQWGaB1iiH7CfL4UJi0c2mupHg1gRwULpDtqE/HSBeUWXEzQFc/MiC39/f/3uh4s9N6RI67zkK4Ab7CkSHa9pRODegJcQM83jMkFd9r66OON1Rtq+hKj8XIAN8GZeMURXoK66p6bBTvv3VhKj4ygKXrLASFFQnsZwAmqWKSDNAVZTb0BHl8aIDED2oAykoZ4OmtwpUeR1KQiqNwF8cYj8sYIOIR4yvrBqgRlQxYWVnpgn5zczLOqaIiM1Sgx6YGiGgVD6rgoizQLTC0AXi97XQ6GVGrq6s5oYjxWNR1i6hhmjk0gsI8RrePxiATIwM0K9wAlq6zEHzHpzgXSOFRHOpqAKnbiCVX1OP9XsMY9pcJ1/3vRrjOFi1atGjRosX4AR9Ai5h5fT44+ViajrVo0aJFixYtWtQN/5Q29OGqfELTDyEe1li4AUMfr4sBmZPlcYEbUCUD+JrLDBhrAypnwHFZ6airbujXX//I6WM6zpjcgaefEbDtbAoGEcls8DEwPOJWA9wEtpsCNwClG6Dj/QxID0vcCK83BS7QRfq49iUGlJ3xl5HQTOmZ498NnKdqoItnO+rTMdLP9/XG5wce2u/zGBmKBPtq67jGu1h99Hm7kY/FSGC/LaB0A4pEs5+lz2NkcIEoIwMic8DofM/Fq/DGZYCnv6+09kfbJNrnkXDv93mMDDxJpsAi+nE7WSY+Et44A3hcrqKG+f+CyAAV7Qaw7fMYGaL/G1CBFB7FoQ5BRfeByAiO+zyq4g8lK5z2I+oYkQAAAABJRU5ErkJggg==";
// 3D 스킨 뷰어 컴포넌트
const SkinViewer = ({ skinUrl }) => {
const containerRef = useRef(null);
const canvasRef = useRef(null);
const viewerRef = useRef(null);
const isDraggingRef = useRef(false);
const lastXRef = useRef(0);
useEffect(() => {
if (!canvasRef.current || !containerRef.current) return;
const viewer = new skinview3d.SkinViewer({
canvas: canvasRef.current,
width: containerRef.current.clientWidth,
height: containerRef.current.clientHeight,
skin: STEVE_SKIN_BASE64,
});
// 설정
viewer.fov = 70;
viewer.zoom = 0.9;
viewer.autoRotate = false;
viewer.animation = new skinview3d.IdleAnimation();
viewer.playerObject.rotation.y = Math.PI / 6;
viewer.playerObject.position.y = -0.5;
viewerRef.current = viewer;
// 수동 드래그 회전 구현
const canvas = canvasRef.current;
const handleMouseDown = (e) => {
isDraggingRef.current = true;
lastXRef.current = e.clientX;
canvas.style.cursor = 'grabbing';
};
const handleMouseMove = (e) => {
if (!isDraggingRef.current || !viewerRef.current) return;
const deltaX = e.clientX - lastXRef.current;
viewerRef.current.playerObject.rotation.y += deltaX * 0.01;
lastXRef.current = e.clientX;
};
const handleMouseUp = () => {
isDraggingRef.current = false;
canvas.style.cursor = 'grab';
};
// 터치 이벤트 지원
const handleTouchStart = (e) => {
if (e.touches.length === 1) {
isDraggingRef.current = true;
lastXRef.current = e.touches[0].clientX;
}
};
const handleTouchMove = (e) => {
if (!isDraggingRef.current || !viewerRef.current || e.touches.length !== 1) return;
e.preventDefault();
const deltaX = e.touches[0].clientX - lastXRef.current;
viewerRef.current.playerObject.rotation.y += deltaX * 0.01;
lastXRef.current = e.touches[0].clientX;
};
const handleTouchEnd = () => {
isDraggingRef.current = false;
};
canvas.addEventListener('mousedown', handleMouseDown);
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp);
canvas.addEventListener('touchstart', handleTouchStart, { passive: false });
canvas.addEventListener('touchmove', handleTouchMove, { passive: false });
canvas.addEventListener('touchend', handleTouchEnd);
// Resize Observer for responsive sizing
const resizeObserver = new ResizeObserver(entries => {
for (let entry of entries) {
const { width, height } = entry.contentRect;
viewer.setSize(width, height);
}
});
resizeObserver.observe(containerRef.current);
return () => {
resizeObserver.disconnect();
canvas.removeEventListener('mousedown', handleMouseDown);
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
canvas.removeEventListener('touchstart', handleTouchStart);
canvas.removeEventListener('touchmove', handleTouchMove);
canvas.removeEventListener('touchend', handleTouchEnd);
viewer.dispose();
};
}, []);
useEffect(() => {
if (viewerRef.current) {
// 로딩 상태를 보여주기 위해 스티브 스킨으로 초기화 (동기/즉시)
viewerRef.current.loadSkin(STEVE_SKIN_BASE64);
const targetSkin = skinUrl || STEVE_SKIN_BASE64;
if (targetSkin !== STEVE_SKIN_BASE64) {
// 그 다음 실제 스킨 로드
viewerRef.current.loadSkin(targetSkin);
}
}
}, [skinUrl]);
return (
<div ref={containerRef} className="w-full h-full flex items-center justify-center">
<canvas ref={canvasRef} style={{ cursor: 'grab' }} />
</div>
);
};
export default SkinViewer;