feat: 实现多行多列雪碧图的动画加载
This commit is contained in:
112
src/components/Sprite/SpriteAnimator.vue
Normal file
112
src/components/Sprite/SpriteAnimator.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<view class="sprite-animator" :style="spriteStyle" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
src: string
|
||||
frameWidth: number
|
||||
frameHeight: number
|
||||
totalFrames: number
|
||||
columns: number
|
||||
fps?: number
|
||||
displayWidth?: number
|
||||
autoplay?: boolean
|
||||
loop?: boolean
|
||||
}>(), {
|
||||
fps: 12,
|
||||
autoplay: true,
|
||||
loop: true,
|
||||
})
|
||||
|
||||
/* JS 精确逐帧方案(使用 setInterval,确保整数偏移、无插值) */
|
||||
const currentFrame = ref(0)
|
||||
let intervalId: number | null = null
|
||||
|
||||
const play = () => {
|
||||
stop()
|
||||
const safeFps = Math.max(1, Math.floor(props.fps || 12))
|
||||
const durationPerFrame = 1000 / safeFps
|
||||
// 强制整数像素偏移,使用 setInterval 在各运行时更可靠
|
||||
const setter = (globalThis as any).setInterval || setInterval
|
||||
intervalId = setter(() => {
|
||||
const safeTotal = Math.max(1, Math.floor(props.totalFrames || 0))
|
||||
if (currentFrame.value >= safeTotal - 1) {
|
||||
if (props.loop) {
|
||||
currentFrame.value = 0
|
||||
} else {
|
||||
stop()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
currentFrame.value++
|
||||
}
|
||||
}, durationPerFrame) as unknown as number
|
||||
}
|
||||
|
||||
const stop = () => {
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId)
|
||||
intervalId = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
console.log('[SpriteAnimator] mounted', { autoplay: props.autoplay, fps: props.fps })
|
||||
if (props.autoplay) play()
|
||||
})
|
||||
|
||||
onUnmounted(() => stop())
|
||||
|
||||
const spriteStyle = computed(() => {
|
||||
const {
|
||||
src,
|
||||
frameWidth: rawFrameWidth,
|
||||
frameHeight: rawFrameHeight,
|
||||
totalFrames: rawTotalFrames,
|
||||
columns: rawColumns,
|
||||
} = props
|
||||
|
||||
const frameWidth = Math.max(1, Math.floor(rawFrameWidth || 0))
|
||||
const frameHeight = Math.max(1, Math.floor(rawFrameHeight || 0))
|
||||
const totalFrames = Math.max(1, Math.floor(rawTotalFrames || 0))
|
||||
let columns = Math.floor(Number(rawColumns) || 0)
|
||||
if (!columns || columns <= 0) columns = Math.min(1, totalFrames)
|
||||
columns = Math.min(columns, totalFrames)
|
||||
|
||||
const displayWidth = props.displayWidth || frameWidth
|
||||
|
||||
const rows = Math.ceil(totalFrames / columns)
|
||||
const imageWidth = columns * frameWidth
|
||||
const imageHeight = rows * frameHeight
|
||||
|
||||
const scale = displayWidth / frameWidth
|
||||
const displayHeight = Math.round(frameHeight * scale)
|
||||
|
||||
const scaledImageWidth = Math.round(imageWidth * scale)
|
||||
const scaledImageHeight = Math.round(imageHeight * scale)
|
||||
|
||||
if (currentFrame.value < 0) currentFrame.value = 0
|
||||
if (currentFrame.value >= totalFrames) currentFrame.value = totalFrames - 1
|
||||
const col = currentFrame.value % columns
|
||||
const row = Math.floor(currentFrame.value / columns)
|
||||
|
||||
const offsetX = Math.round(col * frameWidth * scale)
|
||||
const offsetY = Math.round(row * frameHeight * scale)
|
||||
|
||||
const styleStr = `width: ${Math.round(displayWidth)}px; height: ${displayHeight}px; background-image: url("${src}"); background-repeat: no-repeat; background-size: ${scaledImageWidth}px ${scaledImageHeight}px; background-position: -${offsetX}px -${offsetY}px;`
|
||||
return styleStr as any
|
||||
})
|
||||
|
||||
// 对外暴露
|
||||
defineExpose({ play, stop, currentFrame })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sprite-animator {
|
||||
display: block;
|
||||
will-change: background-position;
|
||||
}
|
||||
</style>
|
||||
@@ -3,7 +3,17 @@ v
|
||||
<view class="welcome-content border-box p-12">
|
||||
<view class="wrap rounded-20">
|
||||
<view class="flex flex-items-center flex-justify-between border-box pl-12 pr-12">
|
||||
<view class="ip" :style="getStyle"></view>
|
||||
<!-- <view class="ip" :style="getStyle"></view> -->
|
||||
<SpriteAnimator
|
||||
class="welcome-animator"
|
||||
:src="spriteStyle.welcomeImageUrl"
|
||||
:frameWidth="spriteStyle.frameWidth"
|
||||
:frameHeight="spriteStyle.frameHeight"
|
||||
:totalFrames="spriteStyle.totalFrames"
|
||||
:columns="spriteStyle.columns"
|
||||
:displayWidth="spriteStyle.displayWidth"
|
||||
:fps="16"
|
||||
/>
|
||||
<view class="welcome-text font-size-14 font-500 font-family-misans-vf color-171717 line-height-24">
|
||||
{{ welcomeContent }}
|
||||
</view>
|
||||
@@ -18,6 +28,7 @@ v
|
||||
import { defineProps, computed, getCurrentInstance, defineExpose } from "vue";
|
||||
import { getCurrentConfig } from "@/constant/base";
|
||||
import ChatMoreTips from "../ChatMoreTips/index.vue";
|
||||
import SpriteAnimator from "@/components/Sprite/SpriteAnimator.vue";
|
||||
|
||||
const props = defineProps({
|
||||
mainPageDataModel: {
|
||||
@@ -51,6 +62,30 @@ const initPageImages = computed(() => {
|
||||
|
||||
const config = getCurrentConfig();
|
||||
|
||||
const spriteStyle = computed(() => {
|
||||
const images = initPageImages.value;
|
||||
return {
|
||||
welcomeImageUrl: 'https://oss.nianxx.cn/mp/static/version_101/zn/zn_large.png',
|
||||
frameWidth: 395,
|
||||
frameHeight: 335,
|
||||
totalFrames: 71,
|
||||
columns: 1,
|
||||
displayWidth: 158,
|
||||
};
|
||||
});
|
||||
|
||||
// "ipLargeImage":"https://oss.nianxx.cn/mp/static/version_101/dh/dh_large.png",
|
||||
// "ipLargeImageWidth": 395,
|
||||
// "ipLargeImageHeight": 335,
|
||||
// "ipLargeTotalFrames": 71,
|
||||
// "ipLargeColumns": 1,
|
||||
|
||||
// "ipSmallImage":"https://oss.nianxx.cn/mp/static/version_101/dh/dh_small.png",
|
||||
// "ipSmallImageWidth": 395,
|
||||
// "ipSmallImageHeight": 335,
|
||||
// "ipSmallTotalFrames": 71,
|
||||
// "ipSmallColumns": 1
|
||||
|
||||
const getStyle = computed(() => {
|
||||
const images = initPageImages.value;
|
||||
const style = {
|
||||
|
||||
Reference in New Issue
Block a user