feat(game): 实现精灵图渲染优化游戏性能

- 新增 AnimalSpriteImage 组件用于精灵图渲染
- 集成精灵图渲染到桌面版动物组件
- 集成精灵图渲染到移动版动物组件
- 重构历史记录组件使用精灵图渲染
- 更新动物覆盖层组件使用精灵图渲染
- 移除旧的单个图片资源引用
- 添加精灵图生成脚本和相关配置
- 优化启动资源加载排除精灵图源文件
- 添加精灵图位置计算工具函数
This commit is contained in:
JiaJun
2026-06-10 16:38:00 +08:00
parent da8adc336d
commit c3059549e6
20 changed files with 652 additions and 135 deletions

View 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>
)
}

View File

@@ -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>
))}

View 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>
)
}