feat(game): 实现精灵图渲染优化游戏性能
- 新增 AnimalSpriteImage 组件用于精灵图渲染 - 集成精灵图渲染到桌面版动物组件 - 集成精灵图渲染到移动版动物组件 - 重构历史记录组件使用精灵图渲染 - 更新动物覆盖层组件使用精灵图渲染 - 移除旧的单个图片资源引用 - 添加精灵图生成脚本和相关配置 - 优化启动资源加载排除精灵图源文件 - 添加精灵图位置计算工具函数
This commit is contained in:
61
src/features/game/components/shared/animal-sprite-image.tsx
Normal file
61
src/features/game/components/shared/animal-sprite-image.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { HTMLAttributes } from 'react'
|
||||
import animalSpriteUrl from '@/assets/animal/animal-sprite.webp'
|
||||
import {
|
||||
ANIMAL_SPRITE_CELL_ASPECT_RATIO,
|
||||
ANIMAL_SPRITE_COLUMNS,
|
||||
ANIMAL_SPRITE_ROWS,
|
||||
getAnimalSpritePosition,
|
||||
} from '@/features/game/shared'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface AnimalSpriteImageProps
|
||||
extends Omit<HTMLAttributes<HTMLSpanElement>, 'children'> {
|
||||
animalId: number
|
||||
alt?: string
|
||||
fit?: 'cover' | 'fill'
|
||||
imageClassName?: string
|
||||
}
|
||||
|
||||
export function AnimalSpriteImage({
|
||||
animalId,
|
||||
alt = '',
|
||||
className,
|
||||
fit = 'fill',
|
||||
imageClassName,
|
||||
style,
|
||||
...props
|
||||
}: AnimalSpriteImageProps) {
|
||||
const position = getAnimalSpritePosition(animalId)
|
||||
const accessibilityProps = alt
|
||||
? ({ 'aria-label': alt, role: 'img' } as const)
|
||||
: ({ 'aria-hidden': 'true' } as const)
|
||||
|
||||
return (
|
||||
<span
|
||||
{...accessibilityProps}
|
||||
{...props}
|
||||
className={cn('relative block overflow-hidden', className)}
|
||||
style={style}
|
||||
>
|
||||
{position ? (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
'block bg-no-repeat',
|
||||
fit === 'cover'
|
||||
? 'absolute left-1/2 top-1/2 h-full min-w-full -translate-x-1/2 -translate-y-1/2'
|
||||
: 'absolute inset-0',
|
||||
imageClassName,
|
||||
)}
|
||||
style={{
|
||||
aspectRatio:
|
||||
fit === 'cover' ? ANIMAL_SPRITE_CELL_ASPECT_RATIO : undefined,
|
||||
backgroundImage: `url(${animalSpriteUrl})`,
|
||||
backgroundPosition: `${position.xPercent}% ${position.yPercent}%`,
|
||||
backgroundSize: `${ANIMAL_SPRITE_COLUMNS * 100}% ${ANIMAL_SPRITE_ROWS * 100}%`,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { DataLoadingIndicator } from '@/components/ui/data-loading-indicator'
|
||||
import { AnimalSpriteImage } from '@/features/game/components/shared/animal-sprite-image'
|
||||
import type { PeriodHistoryDisplayItem } from '@/hooks/use-period-history-vm'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
@@ -95,16 +96,12 @@ export function PeriodHistoryList({
|
||||
{item.displayResultNumber}
|
||||
</span>
|
||||
<span className="flex h-design-50 w-design-72 shrink-0 items-center justify-end overflow-hidden">
|
||||
{item.image ? (
|
||||
<img
|
||||
src={item.image}
|
||||
alt={item.displayResultNumber}
|
||||
draggable={false}
|
||||
className="h-full w-auto max-w-none shrink-0 object-contain"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-design-13 text-[#668C92]">--</span>
|
||||
)}
|
||||
<AnimalSpriteImage
|
||||
animalId={item.animalId}
|
||||
alt={item.displayResultNumber}
|
||||
fit="cover"
|
||||
className="h-full w-full"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
52
src/features/game/components/shared/reward-sprite-image.tsx
Normal file
52
src/features/game/components/shared/reward-sprite-image.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { HTMLAttributes } from 'react'
|
||||
import rewardSpriteUrl from '@/assets/reward/reward-sprite.webp'
|
||||
import {
|
||||
getRewardSpritePosition,
|
||||
REWARD_SPRITE_CELL_ASPECT_RATIO,
|
||||
REWARD_SPRITE_COLUMNS,
|
||||
REWARD_SPRITE_ROWS,
|
||||
} from '@/features/game/shared'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface RewardSpriteImageProps
|
||||
extends Omit<HTMLAttributes<HTMLSpanElement>, 'children'> {
|
||||
alt?: string
|
||||
imageClassName?: string
|
||||
rewardId: number
|
||||
}
|
||||
|
||||
export function RewardSpriteImage({
|
||||
alt = '',
|
||||
className,
|
||||
imageClassName,
|
||||
rewardId,
|
||||
style,
|
||||
...props
|
||||
}: RewardSpriteImageProps) {
|
||||
const position = getRewardSpritePosition(rewardId)
|
||||
const accessibilityProps = alt
|
||||
? ({ 'aria-label': alt, role: 'img' } as const)
|
||||
: ({ 'aria-hidden': 'true' } as const)
|
||||
|
||||
return (
|
||||
<span
|
||||
{...accessibilityProps}
|
||||
{...props}
|
||||
className={cn('relative block overflow-hidden', className)}
|
||||
style={style}
|
||||
>
|
||||
{position ? (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={cn('absolute inset-0 block bg-no-repeat', imageClassName)}
|
||||
style={{
|
||||
aspectRatio: REWARD_SPRITE_CELL_ASPECT_RATIO,
|
||||
backgroundImage: `url(${rewardSpriteUrl})`,
|
||||
backgroundPosition: `${position.xPercent}% ${position.yPercent}%`,
|
||||
backgroundSize: `${REWARD_SPRITE_COLUMNS * 100}% ${REWARD_SPRITE_ROWS * 100}%`,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user