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="welcome-content border-box p-12">
|
||||||
<view class="wrap rounded-20">
|
<view class="wrap rounded-20">
|
||||||
<view class="flex flex-items-center flex-justify-between border-box pl-12 pr-12">
|
<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">
|
<view class="welcome-text font-size-14 font-500 font-family-misans-vf color-171717 line-height-24">
|
||||||
{{ welcomeContent }}
|
{{ welcomeContent }}
|
||||||
</view>
|
</view>
|
||||||
@@ -18,6 +28,7 @@ v
|
|||||||
import { defineProps, computed, getCurrentInstance, defineExpose } from "vue";
|
import { defineProps, computed, getCurrentInstance, defineExpose } from "vue";
|
||||||
import { getCurrentConfig } from "@/constant/base";
|
import { getCurrentConfig } from "@/constant/base";
|
||||||
import ChatMoreTips from "../ChatMoreTips/index.vue";
|
import ChatMoreTips from "../ChatMoreTips/index.vue";
|
||||||
|
import SpriteAnimator from "@/components/Sprite/SpriteAnimator.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
mainPageDataModel: {
|
mainPageDataModel: {
|
||||||
@@ -51,6 +62,30 @@ const initPageImages = computed(() => {
|
|||||||
|
|
||||||
const config = getCurrentConfig();
|
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 getStyle = computed(() => {
|
||||||
const images = initPageImages.value;
|
const images = initPageImages.value;
|
||||||
const style = {
|
const style = {
|
||||||
|
|||||||
Reference in New Issue
Block a user