124 lines
5.6 KiB
React
124 lines
5.6 KiB
React
|
|
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;
|