Files
carrotskin/src/components/SkinViewer.tsx

272 lines
8.3 KiB
TypeScript
Raw Normal View History

'use client';
import { useEffect, useRef, useState } from 'react';
import { SkinViewer as SkinViewer3D, WalkingAnimation, RunningAnimation, FlyingAnimation, IdleAnimation } from 'skinview3d';
interface SkinViewerProps {
skinUrl: string;
capeUrl?: string;
isSlim?: boolean;
width?: number;
height?: number;
className?: string;
autoRotate?: boolean;
walking?: boolean;
running?: boolean; // 新增:跑步动画
jumping?: boolean; // 新增:跳跃动画
rotation?: boolean; // 新增:旋转控制
isExternalPreview?: boolean; // 新增:是否为外部预览
}
export default function SkinViewer({
skinUrl,
capeUrl,
isSlim = false,
width = 300,
height = 300,
className = '',
autoRotate = true,
walking = false,
running = false,
jumping = false,
rotation = true,
isExternalPreview = false, // 新增默认为false
}: SkinViewerProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const viewerRef = useRef<SkinViewer3D | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [hasError, setHasError] = useState(false);
const [imageLoaded, setImageLoaded] = useState(false);
// 预加载皮肤图片以检查是否可访问
useEffect(() => {
if (!skinUrl) return;
setIsLoading(true);
setHasError(false);
setImageLoaded(false);
const img = new Image();
img.crossOrigin = 'anonymous'; // 尝试跨域访问
img.onload = () => {
console.log('皮肤图片加载成功:', skinUrl);
setImageLoaded(true);
setIsLoading(false);
};
img.onerror = (error) => {
console.error('皮肤图片加载失败:', skinUrl, error);
setHasError(true);
setIsLoading(false);
};
// 开始加载图片
img.src = skinUrl;
return () => {
// 清理
img.onload = null;
img.onerror = null;
};
}, [skinUrl]);
// 初始化3D查看器
useEffect(() => {
if (!canvasRef.current || !imageLoaded || hasError) return;
try {
console.log('初始化3D皮肤查看器:', { skinUrl, isSlim, width, height });
// 使用canvas的实际尺寸参考blessingskin
const canvas = canvasRef.current;
const viewer = new SkinViewer3D({
canvas: canvas,
width: canvas.clientWidth || width,
height: canvas.clientHeight || height,
skin: skinUrl,
cape: capeUrl,
model: isSlim ? 'slim' : 'default',
zoom: 1.0, // 使用blessingskin的zoom方式
});
viewerRef.current = viewer;
// 设置背景和控制选项 - 参考blessingskin
viewer.background = null; // 透明背景
viewer.autoRotate = false; // 完全禁用自动旋转
// 外部预览时禁用所有动画和旋转
if (isExternalPreview) {
viewer.autoRotate = false;
viewer.controls.enableRotate = false; // 禁用旋转控制
viewer.controls.enableZoom = false; // 禁用缩放
} else {
viewer.autoRotate = autoRotate && !walking && !running && !jumping;
viewer.controls.enableRotate = rotation; // 根据参数控制旋转
viewer.controls.enableZoom = true; // 启用缩放
}
viewer.controls.enablePan = false; // 禁用平移
console.log('3D皮肤查看器初始化成功');
} catch (error) {
console.error('3D皮肤查看器初始化失败:', error);
setHasError(true);
}
// 清理函数
return () => {
if (viewerRef.current) {
try {
viewerRef.current.dispose();
viewerRef.current = null;
console.log('3D皮肤查看器已清理');
} catch (error) {
console.error('清理3D皮肤查看器失败:', error);
}
}
};
}, [skinUrl, capeUrl, isSlim, width, height, autoRotate, walking, running, jumping, rotation, imageLoaded, hasError, isExternalPreview]);
// 控制动画效果 - 参考 Blessing Skin 的实现
useEffect(() => {
if (!viewerRef.current) return;
const viewer = viewerRef.current;
// 外部预览时只使用静止动画,禁用所有其他动画
if (isExternalPreview) {
viewer.animation = new IdleAnimation();
viewer.autoRotate = false;
console.log('外部预览模式:启用静止动画,禁用旋转');
} else {
// 根据优先级设置动画 - 参考 Blessing Skin 的 animationFactories
if (running) {
// 跑步动画
viewer.animation = new RunningAnimation();
console.log('启用跑步动画');
} else if (walking) {
// 普通步行动画
viewer.animation = new WalkingAnimation();
console.log('启用步行动画');
} else if (jumping) {
// 飞行动画作为跳跃
viewer.animation = new FlyingAnimation();
console.log('启用跳跃动画');
} else {
// 静止动画
viewer.animation = new IdleAnimation();
console.log('启用静止动画');
}
// 更新自动旋转状态
viewer.autoRotate = autoRotate && !walking && !running && !jumping;
}
}, [walking, running, jumping, autoRotate, isExternalPreview]);
// 当皮肤URL改变时更新
useEffect(() => {
if (viewerRef.current && skinUrl && imageLoaded) {
try {
console.log('更新皮肤URL:', skinUrl);
viewerRef.current.loadSkin(skinUrl);
} catch (error) {
console.error('更新皮肤失败:', error);
}
}
}, [skinUrl, imageLoaded]);
// 监听容器尺寸变化并调整viewer大小 - 参考blessingskin
useEffect(() => {
if (viewerRef.current && canvasRef.current) {
const rect = canvasRef.current.getBoundingClientRect();
viewerRef.current.setSize(rect.width, rect.height);
}
}, [imageLoaded]); // 当图片加载完成后调整尺寸
// 当披风URL改变时更新
useEffect(() => {
if (viewerRef.current && capeUrl && imageLoaded) {
try {
console.log('更新披风URL:', capeUrl);
viewerRef.current.loadCape(capeUrl);
} catch (error) {
console.error('更新披风失败:', error);
}
} else if (viewerRef.current && !capeUrl && imageLoaded) {
try {
viewerRef.current.resetCape();
} catch (error) {
console.error('移除披风失败:', error);
}
}
}, [capeUrl, imageLoaded]);
// 当模型类型改变时更新
useEffect(() => {
if (viewerRef.current && skinUrl && imageLoaded) {
try {
console.log('更新模型类型:', isSlim ? 'slim' : 'default');
viewerRef.current.loadSkin(skinUrl, { model: isSlim ? 'slim' : 'default' });
} catch (error) {
console.error('更新模型失败:', error);
}
}
}, [isSlim, skinUrl, imageLoaded]);
// 错误状态显示
if (hasError) {
return (
<div
className={`${className} flex items-center justify-center bg-gradient-to-br from-red-50 to-orange-50 dark:from-red-900/20 dark:to-orange-900/20 rounded-xl border-2 border-dashed border-red-300 dark:border-red-700`}
style={{ width: width, height: height }}
>
<div className="text-center p-4">
<div className="text-4xl mb-2"></div>
<div className="text-sm text-red-600 dark:text-red-400 font-medium mb-1">
</div>
<div className="text-xs text-red-500 dark:text-red-500">
访
</div>
</div>
</div>
);
}
// 加载状态显示
if (isLoading) {
return (
<div
className={`${className} flex items-center justify-center bg-gradient-to-br from-orange-50 to-amber-50 dark:from-gray-700 dark:to-gray-600 rounded-xl`}
style={{ width: width, height: height }}
>
<div className="text-center">
<div className="animate-spin w-8 h-8 border-3 border-orange-500 border-t-transparent rounded-full mx-auto mb-3"></div>
<div className="text-sm text-orange-600 dark:text-orange-400 font-medium">
...
</div>
</div>
</div>
);
}
return (
<canvas
ref={canvasRef}
className={className}
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
width: '100%',
height: '100%'
}}
/>
);
}