133 lines
3.7 KiB
TypeScript
133 lines
3.7 KiB
TypeScript
import { useEffect, useRef } from 'react';
|
||
|
||
interface SkinViewerProps {
|
||
width?: number;
|
||
height?: number;
|
||
skinUrl?: string;
|
||
capeUrl?: string;
|
||
walkingSpeed?: number;
|
||
autoRotate?: boolean;
|
||
}
|
||
|
||
const DEFAULT_SKIN =
|
||
'https://static.planetminecraft.com/files/resource_media/skin/original-steve-15053860.png';
|
||
|
||
export default function SkinViewer({
|
||
width = 300,
|
||
height = 400,
|
||
skinUrl,
|
||
capeUrl,
|
||
walkingSpeed = 0.5,
|
||
autoRotate = true,
|
||
}: SkinViewerProps) {
|
||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||
const viewerRef = useRef<any>(null);
|
||
const animRef = useRef<any>(null);
|
||
|
||
// 1) Инициализируем viewer ОДИН РАЗ
|
||
useEffect(() => {
|
||
let disposed = false;
|
||
|
||
const init = async () => {
|
||
if (!canvasRef.current || viewerRef.current) return;
|
||
|
||
try {
|
||
const skinview3d = await import('skinview3d');
|
||
if (disposed) return;
|
||
|
||
const viewer = new skinview3d.SkinViewer({
|
||
canvas: canvasRef.current,
|
||
width,
|
||
height,
|
||
});
|
||
|
||
// базовая настройка
|
||
viewer.autoRotate = autoRotate;
|
||
|
||
// анимация ходьбы
|
||
const walking = new skinview3d.WalkingAnimation();
|
||
walking.speed = walkingSpeed;
|
||
viewer.animation = walking;
|
||
|
||
viewerRef.current = viewer;
|
||
animRef.current = walking;
|
||
|
||
// выставляем ресурсы сразу
|
||
const finalSkin = skinUrl?.trim() ? skinUrl : DEFAULT_SKIN;
|
||
await viewer.loadSkin(finalSkin);
|
||
|
||
if (capeUrl?.trim()) {
|
||
await viewer.loadCape(capeUrl);
|
||
} else {
|
||
viewer.cape = null;
|
||
}
|
||
} catch (e) {
|
||
console.error('Ошибка при инициализации skinview3d:', e);
|
||
}
|
||
};
|
||
|
||
init();
|
||
|
||
return () => {
|
||
disposed = true;
|
||
if (viewerRef.current) {
|
||
viewerRef.current.dispose();
|
||
viewerRef.current = null;
|
||
animRef.current = null;
|
||
}
|
||
};
|
||
// ⚠️ пустой deps — создаём один раз
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, []);
|
||
|
||
// 2) Обновляем размеры (не пересоздаём viewer)
|
||
useEffect(() => {
|
||
const viewer = viewerRef.current;
|
||
if (!viewer) return;
|
||
viewer.width = width;
|
||
viewer.height = height;
|
||
}, [width, height]);
|
||
|
||
// 3) Обновляем автоповорот
|
||
useEffect(() => {
|
||
const viewer = viewerRef.current;
|
||
if (!viewer) return;
|
||
viewer.autoRotate = autoRotate;
|
||
}, [autoRotate]);
|
||
|
||
// 4) Обновляем скорость анимации
|
||
useEffect(() => {
|
||
const walking = animRef.current;
|
||
if (!walking) return;
|
||
walking.speed = walkingSpeed;
|
||
}, [walkingSpeed]);
|
||
|
||
// 5) Обновляем скин
|
||
useEffect(() => {
|
||
const viewer = viewerRef.current;
|
||
if (!viewer) return;
|
||
|
||
const finalSkin = skinUrl?.trim() ? skinUrl : DEFAULT_SKIN;
|
||
|
||
// защита от кеша: добавим “bust” только если URL уже имеет query — не обязательно, но помогает
|
||
const url = finalSkin.includes('?') ? `${finalSkin}&t=${Date.now()}` : `${finalSkin}?t=${Date.now()}`;
|
||
|
||
viewer.loadSkin(url).catch((e: any) => console.error('loadSkin error:', e));
|
||
}, [skinUrl]);
|
||
|
||
// 6) Обновляем плащ
|
||
useEffect(() => {
|
||
const viewer = viewerRef.current;
|
||
if (!viewer) return;
|
||
|
||
if (capeUrl?.trim()) {
|
||
const url = capeUrl.includes('?') ? `${capeUrl}&t=${Date.now()}` : `${capeUrl}?t=${Date.now()}`;
|
||
viewer.loadCape(url).catch((e: any) => console.error('loadCape error:', e));
|
||
} else {
|
||
viewer.cape = null;
|
||
}
|
||
}, [capeUrl]);
|
||
|
||
return <canvas ref={canvasRef} width={width} height={height} style={{ display: 'block' }} />;
|
||
}
|