From a7dd3a4bc0fde06ff137387e8cf29d64d5bda4cd Mon Sep 17 00:00:00 2001 From: lafay <2021211506@stu.hit.edu.cn> Date: Fri, 9 Jan 2026 17:44:21 +0800 Subject: [PATCH] fix: update dependencies and improve animation transitions - Updated @types/react to version 19.2.7 in package.json and package-lock.json. - Refined animation transitions in EnhancedButton, EnhancedInput, LoadingStates, Navbar, PageTransition, SkinCard, SkinDetailModal, and SkinViewer components by removing unnecessary ease properties for smoother animations. - Added size prop to EnhancedInput for better customization. - Enhanced SkinDetailModal and SkinCard styles for improved UI consistency. --- docs/skinview3d-usage.md | 318 +++++++++++++++++++++++++++++ package-lock.json | 2 +- package.json | 2 +- src/components/EnhancedButton.tsx | 10 +- src/components/EnhancedInput.tsx | 12 +- src/components/LoadingStates.tsx | 10 +- src/components/Navbar.tsx | 4 +- src/components/PageTransition.tsx | 10 +- src/components/SkinCard.tsx | 30 ++- src/components/SkinDetailModal.tsx | 100 ++++----- src/components/SkinViewer.tsx | 5 + 11 files changed, 409 insertions(+), 94 deletions(-) create mode 100644 docs/skinview3d-usage.md diff --git a/docs/skinview3d-usage.md b/docs/skinview3d-usage.md new file mode 100644 index 0000000..60cedb3 --- /dev/null +++ b/docs/skinview3d-usage.md @@ -0,0 +1,318 @@ +# skinview3d 使用指南 + +skinview3d 是一个基于 Three.js 的 Minecraft 皮肤查看器,支持 3D 皮肤预览、动画效果、披风显示等功能。 + +## 安装 + +```bash +npm install skinview3d +``` + +## 基本用法 + +### 快速开始 + +```javascript +import { SkinViewer } from 'skinview3d'; + +// 创建皮肤查看器 +const skinViewer = new SkinViewer({ + canvas: document.getElementById('skin_container'), + width: 300, + height: 400, + skin: 'path/to/skin.png' +}); +``` + +### React 组件封装 + +项目中已经封装了 React 组件 `SkinViewer`: + +```tsx +import SkinViewer from '@/components/SkinViewer'; + + +``` + +## 配置选项 + +### 基本配置 + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `canvas` | HTMLCanvasElement | 必填 | 画布元素 | +| `width` | number | 300 | 画布宽度 | +| `height` | number | 400 | 画布高度 | +| `skin` | string | null | 皮肤图片URL | +| `cape` | string | null | 披风图片URL | +| `model` | string | 'default' | 模型类型:'default' 或 'slim' | +| `zoom` | number | 1.0 | 缩放比例 | + +### 背景设置 + +```javascript +// 设置背景颜色 +skinViewer.background = 0x5a76f3; + +// 设置背景图片 +skinViewer.loadBackground('path/to/background.png'); + +// 设置全景背景 +skinViewer.loadPanorama('path/to/panorama.png'); + +// 透明背景 +skinViewer.background = null; +``` + +### 相机设置 + +```javascript +// 设置视野角度 +skinViewer.fov = 70; + +// 设置缩放 +skinViewer.zoom = 0.5; +``` + +## 动画控制 + +### 内置动画类型 + +```javascript +import { + WalkingAnimation, + RunningAnimation, + FlyingAnimation, + IdleAnimation +} from 'skinview3d'; + +// 步行动画 +skinViewer.animation = new WalkingAnimation(); + +// 跑步动画 +skinViewer.animation = new RunningAnimation(); + +// 飞行动画(可用于游泳) +skinViewer.animation = new FlyingAnimation(); + +// 静止动画 +skinViewer.animation = new IdleAnimation(); +``` + +### 动画控制 + +```javascript +// 设置动画速度 +skinViewer.animation.speed = 3; + +// 暂停动画 +skinViewer.animation.paused = true; + +// 移除动画 +skinViewer.animation = null; +``` + +### 自动旋转 + +```javascript +// 启用自动旋转 +skinViewer.autoRotate = true; + +// 禁用自动旋转 +skinViewer.autoRotate = false; +``` + +## 交互控制 + +### 鼠标控制 + +```javascript +// 启用旋转控制 +skinViewer.controls.enableRotate = true; + +// 启用缩放控制 +skinViewer.controls.enableZoom = true; + +// 启用平移控制 +skinViewer.controls.enablePan = true; +``` + +## 光照设置 + +```javascript +// 设置相机光源强度 +skinViewer.cameraLight.intensity = 0.9; + +// 设置全局光源强度 +skinViewer.globalLight.intensity = 0.1; + +// 完全禁用阴影 +globalLight.intensity = 1.0; +cameraLight.intensity = 0.0; +``` + +## 高级功能 + +### 耳朵支持 + +skinview3d 支持两种耳朵纹理类型: + +1. **独立耳朵纹理**:14×7 像素的耳朵图片 +2. **皮肤内置耳朵**:皮肤纹理中包含耳朵(如 deadmau5 皮肤) + +```javascript +// 使用独立耳朵纹理 +skinViewer.loadEars('path/to/ears.png', { + textureType: 'standalone' +}); + +// 使用皮肤内置耳朵 +skinViewer.loadEars('path/to/skin.png', { + textureType: 'skin' +}); +``` + +### 名称标签 + +```javascript +import { NameTagObject } from 'skinview3d'; + +// 简单名称标签 +skinViewer.nameTag = "PlayerName"; + +// 自定义样式名称标签 +skinViewer.nameTag = new NameTagObject("PlayerName", { + textStyle: "yellow" +}); + +// 移除名称标签 +skinViewer.nameTag = null; +``` + +**注意**:要正确显示名称标签,需要加载 Minecraft 字体: + +```css +@font-face { + font-family: 'Minecraft'; + src: url('/path/to/minecraft.woff2') format('woff2'); +} +``` + +## 方法参考 + +### 皮肤管理 + +```javascript +// 加载皮肤 +skinViewer.loadSkin('path/to/skin.png'); + +// 加载皮肤并指定模型 +skinViewer.loadSkin('path/to/skin.png', { + model: 'slim' +}); + +// 重置皮肤 +skinViewer.resetSkin(); +``` + +### 披风管理 + +```javascript +// 加载披风 +skinViewer.loadCape('path/to/cape.png'); + +// 加载鞘翅(使用披风纹理) +skinViewer.loadCape('path/to/cape.png', { + backEquipment: 'elytra' +}); + +// 移除披风 +skinViewer.loadCape(null); +``` + +### 尺寸调整 + +```javascript +// 调整查看器尺寸 +skinViewer.setSize(600, 800); + +// 调整宽度 +skinViewer.width = 600; + +// 调整高度 +skinViewer.height = 800; +``` + +## 错误处理 + +项目中封装的 SkinViewer 组件包含了完整的错误处理: + +```typescript +// 图片加载失败处理 +const img = new Image(); +img.crossOrigin = 'anonymous'; + +img.onload = () => { + // 图片加载成功,初始化3D查看器 + setImageLoaded(true); +}; + +img.onerror = (error) => { + // 图片加载失败,显示错误状态 + setHasError(true); +}; +``` + +## 最佳实践 + +### 1. 预加载图片 +在初始化3D查看器之前,先预加载皮肤图片以确保可访问性。 + +### 2. 性能优化 +- 及时清理不再使用的查看器实例 +- 避免同时运行多个动画 +- 合理设置画布尺寸 + +### 3. 用户体验 +- 提供加载状态指示 +- 显示错误信息和重试选项 +- 支持键盘快捷键控制 + +### 4. 响应式设计 +监听容器尺寸变化,动态调整查看器大小: + +```javascript +useEffect(() => { + if (viewerRef.current && canvasRef.current) { + const rect = canvasRef.current.getBoundingClientRect(); + viewerRef.current.setSize(rect.width, rect.height); + } +}, [imageLoaded]); +``` + +## 项目中的使用示例 + +查看 `[src/components/SkinDetailModal.tsx](src/components/SkinDetailModal.tsx)` 了解完整的皮肤预览实现,包括: + +- 动画控制面板 +- 键盘快捷键支持 +- 最小化/最大化功能 +- 响应式布局 +- 错误处理和加载状态 + +## 相关资源 + +- [skinview3d GitHub 仓库](https://github.com/bs-community/skinview3d) +- [skinview3d 在线演示](https://skinview3d-demo.vercel.app) +- [Three.js 官方文档](https://threejs.org/docs/) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 66cd025..4de7cdf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "devDependencies": { "@tailwindcss/postcss": "^4", "@types/node": "^20", - "@types/react": "^19", + "@types/react": "^19.2.7", "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "16.0.7", diff --git a/package.json b/package.json index b6e4e62..885b32c 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "devDependencies": { "@tailwindcss/postcss": "^4", "@types/node": "^20", - "@types/react": "^19", + "@types/react": "^19.2.7", "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "16.0.7", diff --git a/src/components/EnhancedButton.tsx b/src/components/EnhancedButton.tsx index d7626b2..08e78af 100644 --- a/src/components/EnhancedButton.tsx +++ b/src/components/EnhancedButton.tsx @@ -147,15 +147,15 @@ export default function EnhancedButton({ const buttonVariants = { initial: { scale: 1 }, hover: { - scale: 1.02, - transition: { duration: 0.2, ease: "easeOut" } + scale: 1.02, + transition: { duration: 0.2 } }, tap: { - scale: 0.98, + scale: 0.98, transition: { duration: 0.1 } }, - loading: { - scale: 0.95, + loading: { + scale: 0.95, transition: { duration: 0.2 } } }; diff --git a/src/components/EnhancedInput.tsx b/src/components/EnhancedInput.tsx index 06eb4ac..aa91963 100644 --- a/src/components/EnhancedInput.tsx +++ b/src/components/EnhancedInput.tsx @@ -33,6 +33,7 @@ interface EnhancedInputProps { pattern?: string; validate?: (value: string) => string | null; onValidationChange?: (isValid: boolean) => void; + size?: 'sm' | 'md' | 'lg'; } const EnhancedInput = forwardRef(({ @@ -64,6 +65,7 @@ const EnhancedInput = forwardRef(({ pattern, validate, onValidationChange, + size = 'md', ...props }, ref) => { const [isFocused, setIsFocused] = useState(false); @@ -151,11 +153,11 @@ const EnhancedInput = forwardRef(({ initial: { scale: 1 }, focus: { scale: 1.02, - transition: { duration: 0.2, ease: "easeOut" } + transition: { duration: 0.2 } }, blur: { scale: 1, - transition: { duration: 0.2, ease: "easeOut" } + transition: { duration: 0.2 } } }; @@ -164,12 +166,12 @@ const EnhancedInput = forwardRef(({ focus: { y: -20, scale: 0.85, - transition: { duration: 0.2, ease: "easeOut" } + transition: { duration: 0.2 } }, blur: { y: internalValue ? -20 : 0, scale: internalValue ? 0.85 : 1, - transition: { duration: 0.2, ease: "easeOut" } + transition: { duration: 0.2 } } }; @@ -259,7 +261,7 @@ const EnhancedInput = forwardRef(({ > diff --git a/src/components/LoadingStates.tsx b/src/components/LoadingStates.tsx index 6828c56..0a5706e 100644 --- a/src/components/LoadingStates.tsx +++ b/src/components/LoadingStates.tsx @@ -80,8 +80,7 @@ export function LoadingDots({ y: [-8, 0, -8], transition: { duration: 0.6, - repeat: Infinity, - ease: "easeInOut" + repeat: Infinity } } }; @@ -132,8 +131,7 @@ export function Skeleton({ x: '100%', transition: { duration: 1.5, - repeat: Infinity, - ease: "linear" + repeat: Infinity } } }; @@ -222,8 +220,7 @@ export function LoadingProgressBar({ initial={{ width: 0 }} animate={{ width: `${Math.min(Math.max(progress, 0), 100)}%` }} transition={{ - duration: animated ? 0.5 : 0, - ease: "easeOut" + duration: animated ? 0.5 : 0 }} /> @@ -264,7 +261,6 @@ export function PulseLoader({ transition: { duration: 1.5, repeat: Infinity, - ease: "easeInOut" } } }; diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index ea930de..1c9ec06 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -126,13 +126,13 @@ export default function Navbar() { ref={navbarRef} style={{ y: isHidden ? navbarY : 0, - opacity: navbarOpacity + opacity: navbarOpacity, + willChange: 'transform, opacity' }} initial={{ y: 0 }} animate={{ y: isHidden ? -100 : 0 }} transition={{ duration: 0.3, ease: 'easeInOut' }} className="fixed top-0 left-0 right-0 z-50 transition-all duration-300 bg-white/80 dark:bg-gray-800/80 backdrop-blur-lg border-b border-gray-200/50 dark:border-gray-700/50" - style={{ willChange: 'transform' }} > {/* 滚动进度条 */} diff --git a/src/components/SkinCard.tsx b/src/components/SkinCard.tsx index f22a3de..d3216ae 100644 --- a/src/components/SkinCard.tsx +++ b/src/components/SkinCard.tsx @@ -78,8 +78,7 @@ export default function SkinCard({ transition: { duration: 0.6, delay: index * 0.1, - ease: [0.25, 0.46, 0.45, 0.94], - type: "spring", + type: "spring" as const, stiffness: 100, damping: 15 } @@ -90,7 +89,6 @@ export default function SkinCard({ rotateX: 5, transition: { duration: 0.3, - ease: "easeOut" } } }); @@ -103,7 +101,7 @@ export default function SkinCard({ transition: { duration: 0.2, delay: 0.1, - type: "spring", + type: "spring" as const , stiffness: 300, damping: 20 } @@ -122,7 +120,7 @@ export default function SkinCard({ transition: { duration: 0.3, delay: 0.2 + index * 0.05, - type: "spring", + type: "spring" as const, stiffness: 200, damping: 15 } @@ -163,8 +161,8 @@ export default function SkinCard({ scale: [1, 1.1, 1] }} transition={{ - rotate: { duration: 2, repeat: Infinity, ease: "linear" }, - scale: { duration: 1.5, repeat: Infinity, ease: "easeInOut" } + rotate: { duration: 2, repeat: Infinity }, + scale: { duration: 1.5, repeat: Infinity } }} className="w-12 h-12 border-4 border-orange-300 dark:border-orange-600 border-t-transparent rounded-full" /> @@ -183,23 +181,21 @@ export default function SkinCard({ } ${isHovered ? 'scale-110' : ''}`} autoRotate={isHovered} walking={false} - onLoad={() => setImageLoaded(true)} + onImageLoaded={() => setImageLoaded(true)} /> ) : (
- 🧥 + 🧥

披风

@@ -232,7 +228,7 @@ export default function SkinCard({ - {onDownload !== false && ( + {onDownload && ( @@ -288,7 +284,7 @@ export default function SkinCard({ @@ -408,13 +404,13 @@ export default function SkinCard({
- {texture.uploader && ( + {texture.uploader_id && ( - by {texture.uploader.username} + by User #{texture.uploader_id} )}
diff --git a/src/components/SkinDetailModal.tsx b/src/components/SkinDetailModal.tsx index d56bd0b..7858575 100644 --- a/src/components/SkinDetailModal.tsx +++ b/src/components/SkinDetailModal.tsx @@ -109,8 +109,7 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr y: 0, transition: { duration: 0.5, - ease: [0.25, 0.46, 0.45, 0.94], - type: "spring", + type: "spring" as const, stiffness: 100, damping: 15 } @@ -121,8 +120,7 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr rotateX: 15, y: 50, transition: { - duration: 0.3, - ease: "easeIn" + duration: 0.3 } }, minimized: { @@ -132,8 +130,7 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr width: 280, height: 150, transition: { - duration: 0.4, - ease: "easeInOut" + duration: 0.4 } } }); @@ -145,7 +142,7 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr opacity: 1, transition: { duration: 0.2, - type: "spring", + type: "spring" as const, stiffness: 300, damping: 20 } @@ -162,7 +159,7 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr scale: 1.02, transition: { duration: 0.2, - type: "spring", + type: "spring" as const, stiffness: 400, damping: 25 } @@ -195,14 +192,14 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr animate={isMinimized ? "minimized" : "animate"} exit="exit" transition={{ type: "spring", damping: 20, stiffness: 300 }} - className={`relative bg-white/95 dark:bg-gray-800/95 rounded-2xl shadow-2xl overflow-hidden border border-white/20 dark:border-gray-700/50 backdrop-blur-lg ${ + className={`relative bg-white/98 dark:bg-gray-800/98 rounded-2xl shadow-2xl overflow-hidden border border-white/40 dark:border-gray-700/60 backdrop-blur-2xl ${ isMinimized ? 'fixed bottom-4 right-4' : 'w-full max-w-6xl h-[90vh]' }`} onClick={(e) => e.stopPropagation()} > {/* Header */}
{texture.is_slim && ( - + 细臂 )} @@ -259,9 +256,9 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr {/* 3D 预览区域 */} +
- + 动画控制 @@ -337,18 +336,17 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr handleAnimationChange(anim.key as any)} - className={`p-3 rounded-lg text-sm font-medium transition-all duration-200 ${ + className={`p-4 rounded-xl text-sm font-semibold transition-all duration-300 transform ${ currentAnimation === anim.key - ? 'bg-gradient-to-r from-orange-500 to-amber-500 text-white shadow-lg transform scale-105' - : 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-orange-100 dark:hover:bg-orange-900/20 border border-orange-200 dark:border-gray-600 hover:border-orange-300 dark:hover:border-orange-500' + ? 'bg-gradient-to-r from-orange-500 via-amber-500 to-yellow-500 text-white shadow-2xl scale-105 ring-2 ring-orange-300 dark:ring-orange-500' + : 'bg-white/80 dark:bg-gray-700/80 text-gray-700 dark:text-gray-300 hover:bg-orange-50 dark:hover:bg-orange-900/30 border border-orange-200/50 dark:border-gray-600 hover:border-orange-300 dark:hover:border-orange-400 hover:scale-105 hover:shadow-lg' }`} initial={{ opacity: 0, y: 20 }} - animate={{ opacity: 1, y: 0 }} + transition={{ delay: 0.6 + i * 0.1 }} > {anim.label} @@ -364,18 +362,18 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr animate={{ opacity: 1 }} transition={{ delay: 0.8 }} > -

+

皮肤信息

{texture.description && ( -

+

{texture.description}

@@ -432,13 +430,21 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr {/* 快捷键提示 */} -

快捷键:

-

空格 - 播放/暂停 | 1-4 - 切换动画 | M - 最小化 | ESC - 关闭

+

+ + 快捷键 +

+
+
空格播放/暂停
+
1-4切换动画
+
M最小化
+
ESC关闭
+
@@ -453,11 +459,7 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr transition={{ delay: 0.2 }} >
- +
- +

{texture.name}

diff --git a/src/components/SkinViewer.tsx b/src/components/SkinViewer.tsx index a4b76d9..8355572 100644 --- a/src/components/SkinViewer.tsx +++ b/src/components/SkinViewer.tsx @@ -16,6 +16,7 @@ interface SkinViewerProps { jumping?: boolean; // 新增:跳跃动画 rotation?: boolean; // 新增:旋转控制 isExternalPreview?: boolean; // 新增:是否为外部预览 + onImageLoaded?: () => void; // 新增:图片加载完成回调 } export default function SkinViewer({ @@ -31,6 +32,7 @@ export default function SkinViewer({ jumping = false, rotation = true, isExternalPreview = false, // 新增:默认为false + onImageLoaded, // 新增:图片加载完成回调 }: SkinViewerProps) { const canvasRef = useRef(null); const viewerRef = useRef(null); @@ -53,6 +55,9 @@ export default function SkinViewer({ console.log('皮肤图片加载成功:', skinUrl); setImageLoaded(true); setIsLoading(false); + if (onImageLoaded) { + onImageLoaded(); + } }; img.onerror = (error) => {