在 Web 中使用 Spine 动画(三)

997 阅读5分钟

前面两篇介绍了在 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"
/>

image.png (图1)

在设置动画全屏显示之后:

<!-- 动画 -->
<SpinePlayer
  ref="spinePlayer"
  class="player absolute inset-0"
  json-path="./xx.json"
  atlas-path="./xx.atlas"
/>

image.png (图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,
  }"
/>

image.png (图3)

可以看到红框表示的视口虽然在页面范围中,但是动画本身却向左偏移了。

设计师动画编辑器中的坐标:

{"anchor_href":"","base_size":"-1,-1","expected_size":"-1,-1","external_info":"","font_size_type_change_aware_":false,"id":"","image_margin":0,"image_url":"https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/c8496e59742646d6a429dee05a08c39b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY2hvcmVhdQ==:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiNTg4OTkzOTYyNDU1NDQ4In0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1730018584&x-orig-sign=HeNzARoJfbZc0LAz6ekLzMKpM6o%3D","original_name":"","original_path":"C:/Users/wb.chenzhao01/AppData/Local/netease/popo/users/wb.chenzhao01@mesg.corp.netease.com/image/18abc9ed5f563b0d587a2b6ec9d448d8.png","original_size":"-1,-1","press_can_drag":true,"show_in_image_viewer":true} (图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 轴已经在坐标线上),重新导出:

image.png (图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是那样显示?如果动画容器的大小和页面大小一致,为什么自适应的视口不是页面的尺寸大小?说明当前动画尺寸比页面的尺寸更大,所以为了适应动画完全在容器中展示,进行了缩小。看另一个动画比较明显,这是沸腾的动画:

image.png (图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>