前面两篇介绍了在 Web 中应用 Spine 以及使用的总结。但是关于 viewport 还是没有介绍清楚,在动画设计师和前端开发之间,还存在着诸多问题,动画设计师做出来的动画有没有大小的概念?前端如何设置尺寸?
页面有全屏的背景图,Spine 动画也和页面大小一致。假设页面的区域大小为:2040 * 1140。
视口问题
SpinePlayer 封装如下:
<template>
<div>
<div ref="spineContainer" class="spine h-full w-full"></div>
</div>
</template>
<script setup lang="ts">
import {
SpinePlayer,
type SpinePlayerConfig,
} from '@esotericsoftware/spine-player'
interface Layout {
x: number
y: number
width: number
height: number
}
interface Props {
jsonPath: string
atlasPath: string
premultipliedAlpha?: boolean
layout?: Layout
}
const props = defineProps<Props>()
const emit = defineEmits(['success', 'complete'])
const spineContainer = ref<HTMLElement | null>(null)
let player: SpinePlayer | null = null
onMounted(() => {
if (!spineContainer.value) return
const premultipliedAlpha = props.premultipliedAlpha ?? true
const layout = props.layout ?? {}
// 动画配置
const config: SpinePlayerConfig = {
jsonUrl: props.jsonPath,
atlasUrl: props.atlasPath,
alpha: true,
premultipliedAlpha,
backgroundColor: '#00000000',
preserveDrawingBuffer: false,
showControls: false,
showLoading: false,
defaultMix: 0,
viewport: {
debugRender: true,
padTop: 0,
padLeft: 0,
padBottom: 0,
padRight: 0,
...layout,
},
// 加载完成回调函数
success: (player) => {
// 监听动画完成事件
emit('success')
player.animationState?.addListener({
complete: function (entry) {
// console.log('complete: ', entry)
emit('complete', entry)
},
})
},
// 加载错误回调函数
error: function (reason) {
console.error('spine animation load error', reason)
},
}
// 创建 player
player = new SpinePlayer(spineContainer.value, config)
// 清理函数,当组件卸载时销毁 player
return () => {
player?.dispose()
}
})
onBeforeUnmount(() => {
player?.dispose()
player = null
spineContainer.value = null
})
onUnmounted(() => {
player?.dispose()
player = null
spineContainer.value = null
})
/**
* @function playAnimation
* @description 播放动画
* @param animationName 动画名称
*/
function playAnimation(animationName: string, loop: boolean): void {
if (player) {
player.setAnimation(animationName, loop)
player.play()
}
}
defineExpose({
playAnimation,
})
</script>
说明:红线框为 Spine 动画的 viewport 视口。
Web Player总是试图填充它所嵌入的容器元素. 当选中了一个动画(或在配置中指定), Web Player会确保该动画在Web Player可用的空间内完全可见.
页面中的动画, 当不设置大小时,默认大小 300 * 150 px
<!-- 动画 -->
<SpinePlayer
ref="spinePlayer"
class="player absolute"
json-path="./xx.json"
atlas-path="./xx.atlas"
/>
(图1)
在设置动画全屏显示之后:
<!-- 动画 -->
<SpinePlayer
ref="spinePlayer"
class="player absolute inset-0"
json-path="./xx.json"
atlas-path="./xx.atlas"
/>
(图2)
设置用于所有动画的全局视口:
<!-- 动画 -->
<SpinePlayer
ref="spinePlayer"
class="player absolute inset-0"
json-path="./xx.json"
atlas-path="./xx.atlas"
:layout="{
x: 0,
y: 0,
width: 2040,
height: 1140,
}"
/>
(图3)
可以看到红框表示的视口虽然在页面范围中,但是动画本身却向左偏移了。
设计师动画编辑器中的坐标:
(图4)
通过调整坐标偏移位置可以使动画进入视口中:
<!-- 动画 -->
<SpinePlayer
ref="spinePlayer"
class="player absolute inset-0"
json-path="./xx.json"
atlas-path="./xx.atlas"
:layout="{
x: -800,
y: 0,
width: 2040,
height: 1140,
}"
/>
但是这个偏移量我们只能一点点尝试,不能得到准确值。
然而设计师也不能确定左上角的坐标,在将左下角的位置水平平移到原点之后(x 轴已经在坐标线上),重新导出:
(图5)
坐标还是 x = 0, y = 0:
<!-- 动画 -->
<SpinePlayer
ref="spinePlayer"
class="player absolute inset-0"
json-path="./xx.json"
atlas-path="./xx.atlas"
:layout="{
x: 0,
y: 0,
width: 2040,
height: 1140,
}"
/>
就能看到动画重合了背景图片。
回过头来,重新考虑为什么图2是那样显示?如果动画容器的大小和页面大小一致,为什么自适应的视口不是页面的尺寸大小?说明当前动画尺寸比页面的尺寸更大,所以为了适应动画完全在容器中展示,进行了缩小。看另一个动画比较明显,这是沸腾的动画:
(图6)
设计师在这个背景图上设计动画,动画的范围超出了背景图的大小。我们虽然只需要展示背景图大小区域的动画,但是这个 Spine 动画的大小本身是比较大的,所以在页面中进行了自适应缩放。
所以这里应该明白,动画的大小和我们要展示的区域大小不是同一个概念,它们可以不一样。有时候我们不需要把动画的效果完全展示出来(这里只展示页面大小范围内的)
宽高:宽高是需要设计师提供的,这里动画要展示的宽高也就是页面的大小。
总结
视口的 x, y 表示编辑器世界坐标相对原点的位置,以这个位置作为的视口左下角,width、height 表示要展示的动画尺寸大小,它们作为视口的宽高。以此确定了视口截取的世界坐标区域。
当设置用于所有动画的全局视口时,一个动画资源中的所有动画视口全部一致。所以当不同的动画尺寸不一致时,视口的大小应该取较大者的范围。
x、y、width、height 必须全部设置,任何一个缺失,就会使用自动行为。
动画初始帧的问题
解决了以上问题之后,因为页面加载是由背景图切入动画,但是设计师做的动画不能和背景图完全吻合,也就是切换时会有一个明显的抖动。
背景图用的设计师用作动画的序列帧的第1帧。虽然图和动画展示的大小一致,但是序列帧的图片像是放大一点之后,截取的同样尺寸的图片,所以有背景图切换到动画时,画面像是缩小了一点。
针对这个问题,因为没时间找设计师深究,就自己一点点调整效果。方法是再使用一个元素包裹播放器容器,大小为页面大小,超出隐藏。再对 spine 容器 SpinePlayer 稍微放大,这样页面区域能够与背景图重合。
<!-- 动画 -->
<div class="absolute h-full w-full overflow-hidden">
<SpinePlayer
ref="spinePlayer"
class="player absolute"
json-path="./xx.json"
atlas-path="./xx.atlas"
:layout="{
x: 0,
y: 0,
width: 2040,
height: 1140,
}"
/>
</div>
<style scoped>
.player {
left: -8px;
right: -16px;
top: -9px;
bottom: -7px;
}
</style>