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.
This commit is contained in:
318
docs/skinview3d-usage.md
Normal file
318
docs/skinview3d-usage.md
Normal file
@@ -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';
|
||||||
|
|
||||||
|
<SkinViewer
|
||||||
|
skinUrl="https://example.com/skin.png"
|
||||||
|
capeUrl="https://example.com/cape.png" // 可选
|
||||||
|
isSlim={false} // 是否为细臂模型
|
||||||
|
width={300}
|
||||||
|
height={300}
|
||||||
|
autoRotate={true} // 自动旋转
|
||||||
|
walking={false} // 步行动画
|
||||||
|
running={false} // 跑步动画
|
||||||
|
jumping={false} // 跳跃动画
|
||||||
|
rotation={true} // 允许手动旋转
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置选项
|
||||||
|
|
||||||
|
### 基本配置
|
||||||
|
|
||||||
|
| 参数 | 类型 | 默认值 | 说明 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| `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/)
|
||||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -26,7 +26,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.0.7",
|
"eslint-config-next": "16.0.7",
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.0.7",
|
"eslint-config-next": "16.0.7",
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ export default function EnhancedButton({
|
|||||||
initial: { scale: 1 },
|
initial: { scale: 1 },
|
||||||
hover: {
|
hover: {
|
||||||
scale: 1.02,
|
scale: 1.02,
|
||||||
transition: { duration: 0.2, ease: "easeOut" }
|
transition: { duration: 0.2 }
|
||||||
},
|
},
|
||||||
tap: {
|
tap: {
|
||||||
scale: 0.98,
|
scale: 0.98,
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ interface EnhancedInputProps {
|
|||||||
pattern?: string;
|
pattern?: string;
|
||||||
validate?: (value: string) => string | null;
|
validate?: (value: string) => string | null;
|
||||||
onValidationChange?: (isValid: boolean) => void;
|
onValidationChange?: (isValid: boolean) => void;
|
||||||
|
size?: 'sm' | 'md' | 'lg';
|
||||||
}
|
}
|
||||||
|
|
||||||
const EnhancedInput = forwardRef<HTMLInputElement, EnhancedInputProps>(({
|
const EnhancedInput = forwardRef<HTMLInputElement, EnhancedInputProps>(({
|
||||||
@@ -64,6 +65,7 @@ const EnhancedInput = forwardRef<HTMLInputElement, EnhancedInputProps>(({
|
|||||||
pattern,
|
pattern,
|
||||||
validate,
|
validate,
|
||||||
onValidationChange,
|
onValidationChange,
|
||||||
|
size = 'md',
|
||||||
...props
|
...props
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const [isFocused, setIsFocused] = useState(false);
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
@@ -151,11 +153,11 @@ const EnhancedInput = forwardRef<HTMLInputElement, EnhancedInputProps>(({
|
|||||||
initial: { scale: 1 },
|
initial: { scale: 1 },
|
||||||
focus: {
|
focus: {
|
||||||
scale: 1.02,
|
scale: 1.02,
|
||||||
transition: { duration: 0.2, ease: "easeOut" }
|
transition: { duration: 0.2 }
|
||||||
},
|
},
|
||||||
blur: {
|
blur: {
|
||||||
scale: 1,
|
scale: 1,
|
||||||
transition: { duration: 0.2, ease: "easeOut" }
|
transition: { duration: 0.2 }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -164,12 +166,12 @@ const EnhancedInput = forwardRef<HTMLInputElement, EnhancedInputProps>(({
|
|||||||
focus: {
|
focus: {
|
||||||
y: -20,
|
y: -20,
|
||||||
scale: 0.85,
|
scale: 0.85,
|
||||||
transition: { duration: 0.2, ease: "easeOut" }
|
transition: { duration: 0.2 }
|
||||||
},
|
},
|
||||||
blur: {
|
blur: {
|
||||||
y: internalValue ? -20 : 0,
|
y: internalValue ? -20 : 0,
|
||||||
scale: internalValue ? 0.85 : 1,
|
scale: internalValue ? 0.85 : 1,
|
||||||
transition: { duration: 0.2, ease: "easeOut" }
|
transition: { duration: 0.2 }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -259,7 +261,7 @@ const EnhancedInput = forwardRef<HTMLInputElement, EnhancedInputProps>(({
|
|||||||
>
|
>
|
||||||
<motion.div
|
<motion.div
|
||||||
animate={{ rotate: 360 }}
|
animate={{ rotate: 360 }}
|
||||||
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
|
transition={{ duration: 1, repeat: Infinity }}
|
||||||
className="w-4 h-4 border-2 border-orange-500 border-t-transparent rounded-full"
|
className="w-4 h-4 border-2 border-orange-500 border-t-transparent rounded-full"
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@@ -80,8 +80,7 @@ export function LoadingDots({
|
|||||||
y: [-8, 0, -8],
|
y: [-8, 0, -8],
|
||||||
transition: {
|
transition: {
|
||||||
duration: 0.6,
|
duration: 0.6,
|
||||||
repeat: Infinity,
|
repeat: Infinity
|
||||||
ease: "easeInOut"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -132,8 +131,7 @@ export function Skeleton({
|
|||||||
x: '100%',
|
x: '100%',
|
||||||
transition: {
|
transition: {
|
||||||
duration: 1.5,
|
duration: 1.5,
|
||||||
repeat: Infinity,
|
repeat: Infinity
|
||||||
ease: "linear"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -222,8 +220,7 @@ export function LoadingProgressBar({
|
|||||||
initial={{ width: 0 }}
|
initial={{ width: 0 }}
|
||||||
animate={{ width: `${Math.min(Math.max(progress, 0), 100)}%` }}
|
animate={{ width: `${Math.min(Math.max(progress, 0), 100)}%` }}
|
||||||
transition={{
|
transition={{
|
||||||
duration: animated ? 0.5 : 0,
|
duration: animated ? 0.5 : 0
|
||||||
ease: "easeOut"
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -264,7 +261,6 @@ export function PulseLoader({
|
|||||||
transition: {
|
transition: {
|
||||||
duration: 1.5,
|
duration: 1.5,
|
||||||
repeat: Infinity,
|
repeat: Infinity,
|
||||||
ease: "easeInOut"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -126,13 +126,13 @@ export default function Navbar() {
|
|||||||
ref={navbarRef}
|
ref={navbarRef}
|
||||||
style={{
|
style={{
|
||||||
y: isHidden ? navbarY : 0,
|
y: isHidden ? navbarY : 0,
|
||||||
opacity: navbarOpacity
|
opacity: navbarOpacity,
|
||||||
|
willChange: 'transform, opacity'
|
||||||
}}
|
}}
|
||||||
initial={{ y: 0 }}
|
initial={{ y: 0 }}
|
||||||
animate={{ y: isHidden ? -100 : 0 }}
|
animate={{ y: isHidden ? -100 : 0 }}
|
||||||
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
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"
|
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' }}
|
|
||||||
>
|
>
|
||||||
{/* 滚动进度条 */}
|
{/* 滚动进度条 */}
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
@@ -78,8 +78,7 @@ export default function PageTransition({ children }: PageTransitionProps) {
|
|||||||
rotateX: 0,
|
rotateX: 0,
|
||||||
transition: {
|
transition: {
|
||||||
duration: 0.5,
|
duration: 0.5,
|
||||||
ease: [0.25, 0.46, 0.45, 0.94],
|
type: "spring" as const,
|
||||||
type: "spring",
|
|
||||||
stiffness: 100,
|
stiffness: 100,
|
||||||
damping: 15
|
damping: 15
|
||||||
}
|
}
|
||||||
@@ -91,7 +90,6 @@ export default function PageTransition({ children }: PageTransitionProps) {
|
|||||||
rotateX: 15,
|
rotateX: 15,
|
||||||
transition: {
|
transition: {
|
||||||
duration: 0.3,
|
duration: 0.3,
|
||||||
ease: "easeIn"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -109,7 +107,6 @@ export default function PageTransition({ children }: PageTransitionProps) {
|
|||||||
y: 0,
|
y: 0,
|
||||||
transition: {
|
transition: {
|
||||||
duration: 0.3,
|
duration: 0.3,
|
||||||
ease: "easeOut"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
exit: {
|
exit: {
|
||||||
@@ -118,7 +115,6 @@ export default function PageTransition({ children }: PageTransitionProps) {
|
|||||||
y: -20,
|
y: -20,
|
||||||
transition: {
|
transition: {
|
||||||
duration: 0.2,
|
duration: 0.2,
|
||||||
ease: "easeIn"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -142,8 +138,8 @@ export default function PageTransition({ children }: PageTransitionProps) {
|
|||||||
scale: [1, 1.1, 1]
|
scale: [1, 1.1, 1]
|
||||||
}}
|
}}
|
||||||
transition={{
|
transition={{
|
||||||
rotate: { duration: 1, repeat: Infinity, ease: "linear" },
|
rotate: { duration: 1, repeat: Infinity },
|
||||||
scale: { duration: 1.5, repeat: Infinity, ease: "easeInOut" }
|
scale: { duration: 1.5, repeat: Infinity }
|
||||||
}}
|
}}
|
||||||
className="w-12 h-12 border-4 border-orange-500 border-t-transparent rounded-full mx-auto mb-4"
|
className="w-12 h-12 border-4 border-orange-500 border-t-transparent rounded-full mx-auto mb-4"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -78,8 +78,7 @@ export default function SkinCard({
|
|||||||
transition: {
|
transition: {
|
||||||
duration: 0.6,
|
duration: 0.6,
|
||||||
delay: index * 0.1,
|
delay: index * 0.1,
|
||||||
ease: [0.25, 0.46, 0.45, 0.94],
|
type: "spring" as const,
|
||||||
type: "spring",
|
|
||||||
stiffness: 100,
|
stiffness: 100,
|
||||||
damping: 15
|
damping: 15
|
||||||
}
|
}
|
||||||
@@ -90,7 +89,6 @@ export default function SkinCard({
|
|||||||
rotateX: 5,
|
rotateX: 5,
|
||||||
transition: {
|
transition: {
|
||||||
duration: 0.3,
|
duration: 0.3,
|
||||||
ease: "easeOut"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -103,7 +101,7 @@ export default function SkinCard({
|
|||||||
transition: {
|
transition: {
|
||||||
duration: 0.2,
|
duration: 0.2,
|
||||||
delay: 0.1,
|
delay: 0.1,
|
||||||
type: "spring",
|
type: "spring" as const ,
|
||||||
stiffness: 300,
|
stiffness: 300,
|
||||||
damping: 20
|
damping: 20
|
||||||
}
|
}
|
||||||
@@ -122,7 +120,7 @@ export default function SkinCard({
|
|||||||
transition: {
|
transition: {
|
||||||
duration: 0.3,
|
duration: 0.3,
|
||||||
delay: 0.2 + index * 0.05,
|
delay: 0.2 + index * 0.05,
|
||||||
type: "spring",
|
type: "spring" as const,
|
||||||
stiffness: 200,
|
stiffness: 200,
|
||||||
damping: 15
|
damping: 15
|
||||||
}
|
}
|
||||||
@@ -163,8 +161,8 @@ export default function SkinCard({
|
|||||||
scale: [1, 1.1, 1]
|
scale: [1, 1.1, 1]
|
||||||
}}
|
}}
|
||||||
transition={{
|
transition={{
|
||||||
rotate: { duration: 2, repeat: Infinity, ease: "linear" },
|
rotate: { duration: 2, repeat: Infinity },
|
||||||
scale: { duration: 1.5, repeat: Infinity, ease: "easeInOut" }
|
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"
|
className="w-12 h-12 border-4 border-orange-300 dark:border-orange-600 border-t-transparent rounded-full"
|
||||||
/>
|
/>
|
||||||
@@ -183,21 +181,19 @@ export default function SkinCard({
|
|||||||
} ${isHovered ? 'scale-110' : ''}`}
|
} ${isHovered ? 'scale-110' : ''}`}
|
||||||
autoRotate={isHovered}
|
autoRotate={isHovered}
|
||||||
walking={false}
|
walking={false}
|
||||||
onLoad={() => setImageLoaded(true)}
|
onImageLoaded={() => setImageLoaded(true)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
<motion.div
|
<motion.div
|
||||||
className="text-center"
|
className="text-center"
|
||||||
animate={isHovered ? { y: [-5, 5, -5] } : {}}
|
animate={isHovered ? { y: [-5, 5, -5] } : {}}
|
||||||
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
|
|
||||||
>
|
>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="w-24 h-24 mx-auto mb-2 bg-white dark:bg-gray-800 rounded-xl shadow-lg flex items-center justify-center"
|
className="w-24 h-24 mx-auto mb-2 bg-white dark:bg-gray-800 rounded-xl shadow-lg flex items-center justify-center"
|
||||||
whileHover={{ scale: 1.1, rotate: 10 }}
|
whileHover={{ scale: 1.1, rotate: 10 }}
|
||||||
transition={{ type: 'spring', stiffness: 300 }}
|
transition={{ type: 'spring' as const, stiffness: 300 }}
|
||||||
animate={imageLoaded ? {} : { scale: [0.8, 1, 0.8] }}
|
animate={imageLoaded ? {} : { scale: [0.8, 1, 0.8] }}
|
||||||
transition={imageLoaded ? {} : { duration: 1.5, repeat: Infinity }}
|
|
||||||
>
|
>
|
||||||
<span className="text-2xl">🧥</span>
|
<span className="text-2xl">🧥</span>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -232,7 +228,7 @@ export default function SkinCard({
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
</motion.button>
|
</motion.button>
|
||||||
|
|
||||||
{onDownload !== false && (
|
{onDownload && (
|
||||||
<motion.button
|
<motion.button
|
||||||
variants={getActionButtonVariants()}
|
variants={getActionButtonVariants()}
|
||||||
initial="initial"
|
initial="initial"
|
||||||
@@ -250,7 +246,7 @@ export default function SkinCard({
|
|||||||
{isDownloading ? (
|
{isDownloading ? (
|
||||||
<motion.div
|
<motion.div
|
||||||
animate={{ rotate: 360 }}
|
animate={{ rotate: 360 }}
|
||||||
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
|
transition={{ duration: 1, repeat: Infinity }}
|
||||||
>
|
>
|
||||||
<ArrowDownTrayIcon className="w-5 h-5" />
|
<ArrowDownTrayIcon className="w-5 h-5" />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -288,7 +284,7 @@ export default function SkinCard({
|
|||||||
<motion.div
|
<motion.div
|
||||||
initial={{ scale: 0 }}
|
initial={{ scale: 0 }}
|
||||||
animate={{ scale: 1 }}
|
animate={{ scale: 1 }}
|
||||||
transition={{ type: "spring", stiffness: 300 }}
|
transition={{ type: "spring" as const , stiffness: 300 }}
|
||||||
>
|
>
|
||||||
<HeartIconSolid className="w-5 h-5" />
|
<HeartIconSolid className="w-5 h-5" />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -408,13 +404,13 @@ export default function SkinCard({
|
|||||||
</motion.span>
|
</motion.span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-400">
|
<div className="text-xs text-gray-400">
|
||||||
{texture.uploader && (
|
{texture.uploader_id && (
|
||||||
<motion.span
|
<motion.span
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
transition={{ delay: index * 0.1 + 0.6 }}
|
transition={{ delay: index * 0.1 + 0.6 }}
|
||||||
>
|
>
|
||||||
by {texture.uploader.username}
|
by User #{texture.uploader_id}
|
||||||
</motion.span>
|
</motion.span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -109,8 +109,7 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr
|
|||||||
y: 0,
|
y: 0,
|
||||||
transition: {
|
transition: {
|
||||||
duration: 0.5,
|
duration: 0.5,
|
||||||
ease: [0.25, 0.46, 0.45, 0.94],
|
type: "spring" as const,
|
||||||
type: "spring",
|
|
||||||
stiffness: 100,
|
stiffness: 100,
|
||||||
damping: 15
|
damping: 15
|
||||||
}
|
}
|
||||||
@@ -121,8 +120,7 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr
|
|||||||
rotateX: 15,
|
rotateX: 15,
|
||||||
y: 50,
|
y: 50,
|
||||||
transition: {
|
transition: {
|
||||||
duration: 0.3,
|
duration: 0.3
|
||||||
ease: "easeIn"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
minimized: {
|
minimized: {
|
||||||
@@ -132,8 +130,7 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr
|
|||||||
width: 280,
|
width: 280,
|
||||||
height: 150,
|
height: 150,
|
||||||
transition: {
|
transition: {
|
||||||
duration: 0.4,
|
duration: 0.4
|
||||||
ease: "easeInOut"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -145,7 +142,7 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr
|
|||||||
opacity: 1,
|
opacity: 1,
|
||||||
transition: {
|
transition: {
|
||||||
duration: 0.2,
|
duration: 0.2,
|
||||||
type: "spring",
|
type: "spring" as const,
|
||||||
stiffness: 300,
|
stiffness: 300,
|
||||||
damping: 20
|
damping: 20
|
||||||
}
|
}
|
||||||
@@ -162,7 +159,7 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr
|
|||||||
scale: 1.02,
|
scale: 1.02,
|
||||||
transition: {
|
transition: {
|
||||||
duration: 0.2,
|
duration: 0.2,
|
||||||
type: "spring",
|
type: "spring" as const,
|
||||||
stiffness: 400,
|
stiffness: 400,
|
||||||
damping: 25
|
damping: 25
|
||||||
}
|
}
|
||||||
@@ -195,14 +192,14 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr
|
|||||||
animate={isMinimized ? "minimized" : "animate"}
|
animate={isMinimized ? "minimized" : "animate"}
|
||||||
exit="exit"
|
exit="exit"
|
||||||
transition={{ type: "spring", damping: 20, stiffness: 300 }}
|
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]'
|
isMinimized ? 'fixed bottom-4 right-4' : 'w-full max-w-6xl h-[90vh]'
|
||||||
}`}
|
}`}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<motion.div
|
<motion.div
|
||||||
className="absolute top-0 left-0 right-0 bg-gradient-to-r from-white/90 to-orange-50/90 dark:from-gray-800/90 dark:to-gray-700/90 backdrop-blur-md border-b border-orange-200/50 dark:border-gray-600/50 p-4 z-10"
|
className="absolute top-0 left-0 right-0 bg-gradient-to-r from-white/95 via-orange-50/95 to-amber-50/95 dark:from-gray-800/95 dark:via-gray-800/95 dark:to-gray-700/95 backdrop-blur-xl border-b border-orange-200/60 dark:border-gray-600/60 p-4 z-10 shadow-lg"
|
||||||
initial={{ y: -100 }}
|
initial={{ y: -100 }}
|
||||||
animate={{ y: 0 }}
|
animate={{ y: 0 }}
|
||||||
transition={{ delay: 0.2, duration: 0.4 }}
|
transition={{ delay: 0.2, duration: 0.4 }}
|
||||||
@@ -210,7 +207,7 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<motion.h2
|
<motion.h2
|
||||||
className="text-2xl font-bold bg-gradient-to-r from-orange-500 to-amber-500 bg-clip-text text-transparent"
|
className="text-3xl font-bold bg-gradient-to-r from-orange-500 via-amber-500 to-yellow-500 bg-clip-text text-transparent drop-shadow-sm"
|
||||||
initial={{ opacity: 0, x: -20 }}
|
initial={{ opacity: 0, x: -20 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
transition={{ delay: 0.3 }}
|
transition={{ delay: 0.3 }}
|
||||||
@@ -219,10 +216,10 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr
|
|||||||
</motion.h2>
|
</motion.h2>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<motion.span
|
<motion.span
|
||||||
className={`px-3 py-1 text-sm rounded-full font-medium ${
|
className={`px-4 py-2 text-sm rounded-full font-semibold shadow-md ${
|
||||||
texture.type === 'SKIN'
|
texture.type === 'SKIN'
|
||||||
? 'bg-gradient-to-r from-orange-100 to-amber-100 text-orange-800 dark:from-orange-900/30 dark:to-amber-900/30 dark:text-orange-300'
|
? 'bg-gradient-to-r from-orange-100 to-amber-100 text-orange-800 dark:from-orange-900/40 dark:to-amber-900/40 dark:text-orange-300 border border-orange-200/50 dark:border-orange-700/30'
|
||||||
: 'bg-gradient-to-r from-purple-100 to-pink-100 text-purple-800 dark:from-purple-900/30 dark:to-pink-900/30 dark:text-purple-300'
|
: 'bg-gradient-to-r from-purple-100 to-pink-100 text-purple-800 dark:from-purple-900/40 dark:to-pink-900/40 dark:text-purple-300 border border-purple-200/50 dark:border-purple-700/30'
|
||||||
}`}
|
}`}
|
||||||
initial={{ scale: 0, opacity: 0 }}
|
initial={{ scale: 0, opacity: 0 }}
|
||||||
animate={{ scale: 1, opacity: 1 }}
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
@@ -232,7 +229,7 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr
|
|||||||
</motion.span>
|
</motion.span>
|
||||||
{texture.is_slim && (
|
{texture.is_slim && (
|
||||||
<motion.span
|
<motion.span
|
||||||
className="px-3 py-1 bg-gradient-to-r from-pink-100 to-rose-100 text-pink-800 dark:from-pink-900/30 dark:to-rose-900/30 text-sm rounded-full font-medium"
|
className="px-4 py-2 bg-gradient-to-r from-pink-100 to-rose-100 text-pink-800 dark:from-pink-900/40 dark:to-rose-900/40 text-sm rounded-full font-semibold shadow-md border border-pink-200/50 dark:border-pink-700/30"
|
||||||
initial={{ scale: 0, opacity: 0 }}
|
initial={{ scale: 0, opacity: 0 }}
|
||||||
animate={{ scale: 1, opacity: 1 }}
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
transition={{ delay: 0.5, type: "spring", stiffness: 200 }}
|
transition={{ delay: 0.5, type: "spring", stiffness: 200 }}
|
||||||
@@ -259,9 +256,9 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr
|
|||||||
</motion.button>
|
</motion.button>
|
||||||
<motion.button
|
<motion.button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="p-2 text-gray-500 hover:text-orange-500 dark:text-gray-400 dark:hover:text-orange-400 rounded-full hover:bg-orange-100 dark:hover:bg-orange-900/20 transition-all duration-200"
|
className="p-3 text-gray-500 hover:text-red-500 dark:text-gray-400 dark:hover:text-red-400 rounded-full hover:bg-red-100 dark:hover:bg-red-900/30 transition-all duration-300 shadow-md hover:shadow-lg"
|
||||||
whileHover={{ scale: 1.1, rotate: 90 }}
|
whileHover={{ scale: 1.15, rotate: 90 }}
|
||||||
whileTap={{ scale: 0.9 }}
|
whileTap={{ scale: 0.85 }}
|
||||||
initial={{ opacity: 0, rotate: -180 }}
|
initial={{ opacity: 0, rotate: -180 }}
|
||||||
animate={{ opacity: 1, rotate: 0 }}
|
animate={{ opacity: 1, rotate: 0 }}
|
||||||
transition={{ delay: 0.7 }}
|
transition={{ delay: 0.7 }}
|
||||||
@@ -276,7 +273,7 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr
|
|||||||
<div className="flex h-full pt-20">
|
<div className="flex h-full pt-20">
|
||||||
{/* 3D 预览区域 */}
|
{/* 3D 预览区域 */}
|
||||||
<motion.div
|
<motion.div
|
||||||
className="flex-1 flex items-center justify-center p-8 bg-gradient-to-br from-orange-50/30 to-amber-50/30 dark:from-gray-900/30 dark:to-gray-800/30"
|
className="flex-1 flex items-center justify-center p-8 bg-gradient-to-br from-orange-50/40 via-amber-50/40 to-yellow-50/40 dark:from-gray-900/40 dark:via-gray-800/40 dark:to-gray-700/40"
|
||||||
initial={{ opacity: 0, scale: 0.9 }}
|
initial={{ opacity: 0, scale: 0.9 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
transition={{ delay: 0.3, duration: 0.5 }}
|
transition={{ delay: 0.3, duration: 0.5 }}
|
||||||
@@ -284,20 +281,22 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr
|
|||||||
<div className="w-full h-full max-w-2xl max-h-2xl">
|
<div className="w-full h-full max-w-2xl max-h-2xl">
|
||||||
<motion.div
|
<motion.div
|
||||||
animate={{
|
animate={{
|
||||||
rotateY: autoRotate ? 360 : 0,
|
scale: isPlaying ? 1.02 : 1,
|
||||||
scale: isPlaying ? 1.02 : 1
|
y: isPlaying ? -5 : 0
|
||||||
}}
|
}}
|
||||||
transition={{
|
transition={{
|
||||||
rotateY: { duration: 10, repeat: Infinity, ease: "linear" },
|
scale: { duration: 0.2 },
|
||||||
scale: { duration: 0.2 }
|
y: { duration: 0.2 }
|
||||||
}}
|
}}
|
||||||
|
className="relative"
|
||||||
>
|
>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-br from-orange-400/20 to-amber-400/20 dark:from-orange-500/10 dark:to-amber-500/10 rounded-2xl blur-xl"></div>
|
||||||
<SkinViewer
|
<SkinViewer
|
||||||
skinUrl={texture.url}
|
skinUrl={texture.url}
|
||||||
isSlim={texture.is_slim}
|
isSlim={texture.is_slim}
|
||||||
width={600}
|
width={600}
|
||||||
height={600}
|
height={600}
|
||||||
className="w-full h-full rounded-xl shadow-2xl border-2 border-white/50 dark:border-gray-700/50"
|
className="w-full h-full rounded-2xl shadow-2xl border-2 border-white/60 dark:border-gray-600/60 relative z-10"
|
||||||
autoRotate={autoRotate}
|
autoRotate={autoRotate}
|
||||||
walking={currentAnimation === 'walking'}
|
walking={currentAnimation === 'walking'}
|
||||||
running={currentAnimation === 'running'}
|
running={currentAnimation === 'running'}
|
||||||
@@ -310,7 +309,7 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr
|
|||||||
|
|
||||||
{/* 控制面板 */}
|
{/* 控制面板 */}
|
||||||
<motion.div
|
<motion.div
|
||||||
className="w-80 bg-gradient-to-b from-orange-50/80 to-amber-50/80 dark:from-gray-900/80 dark:to-gray-800/80 backdrop-blur-md border-l border-orange-200/50 dark:border-gray-600/50 p-6 space-y-6 overflow-y-auto custom-scrollbar"
|
className="w-80 bg-gradient-to-b from-orange-50/90 via-amber-50/90 to-yellow-50/90 dark:from-gray-900/90 dark:via-gray-800/90 dark:to-gray-700/90 backdrop-blur-xl border-l border-orange-200/60 dark:border-gray-600/60 p-6 space-y-6 overflow-y-auto custom-scrollbar shadow-inner"
|
||||||
initial={{ opacity: 0, x: 50 }}
|
initial={{ opacity: 0, x: 50 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
transition={{ delay: 0.4, duration: 0.5 }}
|
transition={{ delay: 0.4, duration: 0.5 }}
|
||||||
@@ -318,12 +317,12 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr
|
|||||||
{/* 动画控制 */}
|
{/* 动画控制 */}
|
||||||
<motion.div className="space-y-4">
|
<motion.div className="space-y-4">
|
||||||
<motion.h3
|
<motion.h3
|
||||||
className="text-lg font-semibold bg-gradient-to-r from-orange-500 to-amber-500 bg-clip-text text-transparent flex items-center"
|
className="text-xl font-bold bg-gradient-to-r from-orange-500 via-amber-500 to-yellow-500 bg-clip-text text-transparent flex items-center drop-shadow-sm"
|
||||||
initial={{ opacity: 0, y: -10 }}
|
initial={{ opacity: 0, y: -10 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ delay: 0.5 }}
|
transition={{ delay: 0.5 }}
|
||||||
>
|
>
|
||||||
<PlayIcon className="w-5 h-5 mr-2" />
|
<PlayIcon className="w-6 h-6 mr-3" />
|
||||||
动画控制
|
动画控制
|
||||||
</motion.h3>
|
</motion.h3>
|
||||||
|
|
||||||
@@ -337,18 +336,17 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr
|
|||||||
<motion.button
|
<motion.button
|
||||||
key={anim.key}
|
key={anim.key}
|
||||||
variants={getAnimationButtonVariants(currentAnimation === anim.key)}
|
variants={getAnimationButtonVariants(currentAnimation === anim.key)}
|
||||||
initial="initial"
|
|
||||||
animate={currentAnimation === anim.key ? "active" : "animate"}
|
animate={currentAnimation === anim.key ? "active" : "animate"}
|
||||||
whileHover="hover"
|
whileHover="hover"
|
||||||
whileTap="tap"
|
whileTap="tap"
|
||||||
onClick={() => handleAnimationChange(anim.key as any)}
|
onClick={() => 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
|
currentAnimation === anim.key
|
||||||
? 'bg-gradient-to-r from-orange-500 to-amber-500 text-white shadow-lg transform scale-105'
|
? '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 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-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 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: 0.6 + i * 0.1 }}
|
transition={{ delay: 0.6 + i * 0.1 }}
|
||||||
>
|
>
|
||||||
{anim.label}
|
{anim.label}
|
||||||
@@ -364,18 +362,18 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr
|
|||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
transition={{ delay: 0.8 }}
|
transition={{ delay: 0.8 }}
|
||||||
>
|
>
|
||||||
<h3 className="text-lg font-semibold bg-gradient-to-r from-orange-500 to-amber-500 bg-clip-text text-transparent">
|
<h3 className="text-xl font-bold bg-gradient-to-r from-orange-500 via-amber-500 to-yellow-500 bg-clip-text text-transparent drop-shadow-sm">
|
||||||
皮肤信息
|
皮肤信息
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{texture.description && (
|
{texture.description && (
|
||||||
<motion.div
|
<motion.div
|
||||||
className="bg-white/50 dark:bg-gray-700/50 rounded-lg p-3"
|
className="bg-white/70 dark:bg-gray-700/70 rounded-xl p-4 shadow-md border border-white/30 dark:border-gray-600/30"
|
||||||
initial={{ opacity: 0, y: 10 }}
|
initial={{ opacity: 0, y: 10 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ delay: 0.9 }}
|
transition={{ delay: 0.9 }}
|
||||||
>
|
>
|
||||||
<p className="text-sm text-gray-700 dark:text-gray-300 leading-relaxed">
|
<p className="text-sm text-gray-700 dark:text-gray-300 leading-relaxed font-medium">
|
||||||
{texture.description}
|
{texture.description}
|
||||||
</p>
|
</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -432,13 +430,21 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr
|
|||||||
|
|
||||||
{/* 快捷键提示 */}
|
{/* 快捷键提示 */}
|
||||||
<motion.div
|
<motion.div
|
||||||
className="bg-white/30 dark:bg-gray-700/30 rounded-lg p-3 text-xs text-gray-600 dark:text-gray-400"
|
className="bg-gradient-to-r from-orange-100/50 to-amber-100/50 dark:from-gray-700/50 dark:to-gray-600/50 rounded-xl p-4 text-xs text-gray-600 dark:text-gray-400 border border-orange-200/30 dark:border-gray-600/30"
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
transition={{ delay: 1.4 }}
|
transition={{ delay: 1.4 }}
|
||||||
>
|
>
|
||||||
<p className="font-medium mb-1">快捷键:</p>
|
<p className="font-bold mb-2 text-orange-700 dark:text-orange-300 flex items-center">
|
||||||
<p>空格 - 播放/暂停 | 1-4 - 切换动画 | M - 最小化 | ESC - 关闭</p>
|
<span className="w-2 h-2 bg-orange-500 rounded-full mr-2"></span>
|
||||||
|
快捷键
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||||
|
<div className="flex items-center"><kbd className="px-2 py-1 bg-white/70 dark:bg-gray-800/70 rounded border border-gray-300 dark:border-gray-600 text-xs mr-2">空格</kbd>播放/暂停</div>
|
||||||
|
<div className="flex items-center"><kbd className="px-2 py-1 bg-white/70 dark:bg-gray-800/70 rounded border border-gray-300 dark:border-gray-600 text-xs mr-2">1-4</kbd>切换动画</div>
|
||||||
|
<div className="flex items-center"><kbd className="px-2 py-1 bg-white/70 dark:bg-gray-800/70 rounded border border-gray-300 dark:border-gray-600 text-xs mr-2">M</kbd>最小化</div>
|
||||||
|
<div className="flex items-center"><kbd className="px-2 py-1 bg-white/70 dark:bg-gray-800/70 rounded border border-gray-300 dark:border-gray-600 text-xs mr-2">ESC</kbd>关闭</div>
|
||||||
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
@@ -453,11 +459,7 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr
|
|||||||
transition={{ delay: 0.2 }}
|
transition={{ delay: 0.2 }}
|
||||||
>
|
>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<motion.div
|
<div className="w-20 h-20 mx-auto mb-2">
|
||||||
animate={{ rotate: autoRotate ? 360 : 0 }}
|
|
||||||
transition={{ rotate: { duration: 5, repeat: Infinity, ease: "linear" } }}
|
|
||||||
className="w-20 h-20 mx-auto mb-2"
|
|
||||||
>
|
|
||||||
<SkinViewer
|
<SkinViewer
|
||||||
skinUrl={texture.url}
|
skinUrl={texture.url}
|
||||||
isSlim={texture.is_slim}
|
isSlim={texture.is_slim}
|
||||||
@@ -468,7 +470,7 @@ export default function SkinDetailModal({ isOpen, onClose, texture, isExternalPr
|
|||||||
running={currentAnimation === 'running'}
|
running={currentAnimation === 'running'}
|
||||||
jumping={currentAnimation === 'swimming'}
|
jumping={currentAnimation === 'swimming'}
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</div>
|
||||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-300 truncate">
|
<p className="text-sm font-medium text-gray-700 dark:text-gray-300 truncate">
|
||||||
{texture.name}
|
{texture.name}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ interface SkinViewerProps {
|
|||||||
jumping?: boolean; // 新增:跳跃动画
|
jumping?: boolean; // 新增:跳跃动画
|
||||||
rotation?: boolean; // 新增:旋转控制
|
rotation?: boolean; // 新增:旋转控制
|
||||||
isExternalPreview?: boolean; // 新增:是否为外部预览
|
isExternalPreview?: boolean; // 新增:是否为外部预览
|
||||||
|
onImageLoaded?: () => void; // 新增:图片加载完成回调
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SkinViewer({
|
export default function SkinViewer({
|
||||||
@@ -31,6 +32,7 @@ export default function SkinViewer({
|
|||||||
jumping = false,
|
jumping = false,
|
||||||
rotation = true,
|
rotation = true,
|
||||||
isExternalPreview = false, // 新增:默认为false
|
isExternalPreview = false, // 新增:默认为false
|
||||||
|
onImageLoaded, // 新增:图片加载完成回调
|
||||||
}: SkinViewerProps) {
|
}: SkinViewerProps) {
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const viewerRef = useRef<SkinViewer3D | null>(null);
|
const viewerRef = useRef<SkinViewer3D | null>(null);
|
||||||
@@ -53,6 +55,9 @@ export default function SkinViewer({
|
|||||||
console.log('皮肤图片加载成功:', skinUrl);
|
console.log('皮肤图片加载成功:', skinUrl);
|
||||||
setImageLoaded(true);
|
setImageLoaded(true);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
if (onImageLoaded) {
|
||||||
|
onImageLoaded();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
img.onerror = (error) => {
|
img.onerror = (error) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user