• Runtimes
  • How spine web player switch animations with diffrent sizes?

hi, I am using the Spine Web player in the vue project, and the Spine material contains several animations, each of which has different sizes. For example, if there are animations move and idle, my demand is to play animation move first and then idle. Because they are different sizes, I need to set the size of the container.

Here's some of my code:

SpinePlayer component:

<template>
  <div ref="spineContainer" class="spine"></div>
</template>

<script setup lang="ts">
import {
  SpinePlayer,
  type SpinePlayerConfig,
} from '@esotericsoftware/spine-player'

interface Props {
  jsonPath: string
  atlasPath: string
  animations: string[]
}

const props = defineProps<Props>()
const emit = defineEmits(['complete'])
const spineContainer = ref<HTMLElement | null>(null)
let player: SpinePlayer | null = null

onMounted(() => {
  if (!spineContainer.value) return
  const config: SpinePlayerConfig = {
    jsonUrl: props.jsonPath,
    atlasUrl: props.atlasPath,
    animations: props.animations,
    alpha: true,
    backgroundColor: '#00000000',
    preserveDrawingBuffer: false,
    showControls: false,
    success: (player) => {
      player.animationState?.setEmptyAnimation(0, 0)
      player.animationState?.addListener({
        complete: function (entry) {
          emit('complete', entry)
        },
      })
    },
  }

  player = new SpinePlayer(spineContainer.value, config)

  return () => {
    player?.dispose()
  }
})

function playAnimation(animationName, loop) {
  if (player) {
    player.animationState?.clearTracks()
    player.setAnimation(animationName, loop)
    player.play()
  }
}

defineExpose({
  playAnimation,
})
</script>

vue page:

<Role
  ref="role"
  json-path="./spine/role.json"
  atlas-path="./spine/role.atlas"
  :animations="[]"
  @complete="OnRoleComplete"
/>

function setRoleSize(animation) {
  if (animation === 'move') {
    role.style.width = '295px'
    role.style.width = '222px'
  } else if (animation === 'idle') {
    role.style.width = '127px'
    role.style.width = '185px'
  }
}

// this doesn't matter. Just changing role position
function setRolePosition() {}

function rolePlay(animation) {
  setRoleSize(animation)
  setRolePosition()
  role.value.playAnimation(animation, false)
}

function OnRoleComplete(entry: any): void {
  if (entry.animation.name === 'move') {
    animateSkyPlay('idle')
  }
}

rolePlay('move')

Start with move animation, and after move is complete, start idle animation.

However, the problem is that when rolePlay('idle') is started after the move is finished, because need set size first, the move will be displayed on the idle size canvas (that is, 127*185) first, and then play the idle animation, there will be a process of role reduction and then enlargement. How can I show the idle animation directly after the move animation?

Is it right to change containers to meet this requirement?
How do I switch between two different sized animations?

    Related Discussions
    ...

    To handle animations of different sizes in the Spine Web Player, you can use the viewport configuration to specify different viewports for each animation. This way, the player will automatically adjust the viewport size when switching between animations, ensuring that each animation is displayed correctly without resizing issues.

    Here's how you can modify your SpinePlayer component to include viewport configurations for each animation:

    <template>
      <div ref="spineContainer" class="spine"></div>
    </template>
    
    <script setup lang="ts">
    import {
      SpinePlayer,
      type SpinePlayerConfig,
    } from '@esotericsoftware/spine-player'
    
    interface Props {
      jsonPath: string
      atlasPath: string
      animations: string[]
    }
    
    const props = defineProps<Props>()
    const emit = defineEmits(['complete'])
    const spineContainer = ref<HTMLElement | null>(null)
    let player: SpinePlayer | null = null
    
    onMounted(() => {
      if (!spineContainer.value) return
      const config: SpinePlayerConfig = {
        jsonUrl: props.jsonPath,
        atlasUrl: props.atlasPath,
        animations: props.animations,
        alpha: true,
        backgroundColor: '#00000000',
        preserveDrawingBuffer: false,
        showControls: false,
        viewport: {
          animations: {
            move: {
              width: 295,
              height: 222,
            },
            idle: {
              width: 127,
              height: 185,
            },
          },
        },
        success: (player) => {
          player.animationState?.setEmptyAnimation(0, 0)
          player.animationState?.addListener({
            complete: function (entry) {
              emit('complete', entry)
            },
          })
        },
      }
    
      player = new SpinePlayer(spineContainer.value, config)
    
      return () => {
        player?.dispose()
      }
    })
    
    function playAnimation(animationName, loop) {
      if (player) {
        player.animationState?.clearTracks()
        player.setAnimation(animationName, loop)
        player.play()
      }
    }
    
    defineExpose({
      playAnimation,
    })
    </script>

    In your Vue page, you can then call the playAnimation function to switch between animations:

    <Role
      ref="role"
      json-path="./spine/role.json"
      atlas-path="./spine/role.atlas"
      :animations="[]"
      @complete="OnRoleComplete"
    />
    
    <script setup lang="ts">
    import { ref } from 'vue'
    import Role from './SpinePlayerComponent.vue'
    
    const role = ref(null)
    
    function OnRoleComplete(entry) {
      if (entry.animation.name === 'move') {
        role.value.playAnimation('idle', false)
      }
    }
    
    function rolePlay(animation) {
      role.value.playAnimation(animation, false)
    }
    
    rolePlay('move')
    </script>

    By specifying the viewport sizes for each animation in the SpinePlayer configuration, the player will automatically adjust the viewport when switching between animations, ensuring that each animation is displayed correctly without resizing issues.

    zangbianxuegu

    Hello 🙂
    I'm not sure I've fully understood your question. Be aware that you can write using you own langauge because on the bottom right of each message a translate button will quickly translate the message.

    As Nate indicated to you in this post 3 days ago, to control the player viewport, you can use the viewport configuration as explained in here.

    As far as I understood, you are asking in the following paragraph:

    However, the problem is that when rolePlay('idle') is started after the move is finished, because need set size first, the move will be displayed on the idle size canvas (that is, 127*185) first, and then play the idle animation, there will be a process of role reduction and then enlargement. How can I show the idle animation directly after the move animation?

    If you can show the animation right after without any viewport transition. If that is the question, you can supply the following config to the player

    viewport: { transitionTime: 0 },

    As the documentation explains:

    When the player switches between animations, it interpolates the viewports of the old and new animation for 0.2 seconds. This viewport transition time can be set via the transitionTime property.

    Regarding the other two questions:

    Is it right to change containers to meet this requirement?

    You should change the container size. The player calculates the viewport size to contain each animation. If you want to change the viewport size, just pass the desired global viewport size or the viewport size for each specific animation, as the documentation shows.
    As Nate mentioned, you could also call directly the setViewport method on the player.

    How do I switch between two different sized animations?

    You just call the player.setAnimation as you already do.

    If this does not answer your questions, please try to exaplin better your problem 👍

    viewport: { transitionTime: 0 }, can reduce the animation transition time, but there is still a flashing process. I think it is because the container size changed.

      zangbianxuegu

      If the canvas changes size, the player will invoke a resize of the webgl viewport that can cause the flashing effect you mentioned.

      I just tried without resizing the container and I don't see an flashing effect.

      Why do you change the container size? Please, try without changing the container size. That should solve your problem.

        Davide but animations have different sizes, if don't change container size, if I use viewport like this, it doesn't work!

        first idle, then move, the move animation viewport width is idle's 127,so it gets smaller.
        What's the problem? Thank you very much!

        viewport: {
        debugRender: true,
        padLeft: '0%',
        padRight: '0%',
        padTop: '0%',
        padBottom: '0%',
        transitionTime: 0,
        animations: {
        move: {
        width: 295,
        height: 222,
        x: -60,
        y: -100,
        padLeft: '0%',
        padRight: '0%',
        padTop: '0%',
        padBottom: '0%',
        },
        idle: {
        width: 127,
        height: 185,
        x: -63,
        y: -96,
        padLeft: '0%',
        padRight: '0%',
        padTop: '0%',
        padBottom: '0%',
        },
        },
        },

          zangbianxuegu

          If you want a fixed viewport, don't set a viewport for each animation, but set a global viewport as explained in the documentation:

            Davide

            也许我应该用自己的语言描述问题,就像你上面提到的,不过我没看到翻译按钮,如果有任何疑问,我可以再补充。非常感谢您的回复!

            动画师提供的导出文件中,包含有多个动画,就像官网 https://en.esotericsoftware.com/spine-player#Putting-it-all-together 展示的 Spineboy 动画一样,包含 walk、jump 等等。就像这里展示的,如果想要从动画 walk 切换到 jump,会发现角色人物变小,但是真实的需求中,肯定需要保持人物的大小一致,来制作连贯的动画。假如用这个例子来实现 walk-> jump,该如何做呢?

            我理解 viewport(https://en.esotericsoftware.com/spine-player#Viewports)中提到的绿框,就是动画的大小(bounding box)、动画自动适应 container 的大小。动画本来有一个大小,所以当 container 设置为一个固定的大小时,动画的大小可能会缩小放大。

            在我的例子中,我设置 viewport pad 全为 0%,动画 idle 的原始大小是 127 * 185,动画 move 的原始大小是 295 * 222,在这样的大小下,这两个动画中的角色是一样大小的。如果我设置 container 为 127 * 185,动画 idle 会以本来的大小展示,当 setAnimation move 的时候,move 就会自动适应 container,进行相应的缩小。

            所以还是上面的问题,如果要实现 Spineboy 的 walk 动画到 jump 动画的连贯展示(人物是一样的大小),该怎么做呢?

            另外,抱歉我感觉 viewport 有点难用,我不知道 viewport 配置的具体含义,文档说 width 和 height 指定视口的尺寸,但是我这样设置:

            <!doctype html>
            <html lang="en">
              <head>
                <meta charset="UTF-8" />
                <meta name="viewport" content="width=device-width, initial-scale=1.0" />
                <title>Spine boy</title>
                <script src="https://unpkg.com/@esotericsoftware/spine-player@4.2.*/dist/iife/spine-player.js"></script>
                <link rel="stylesheet" href="https://unpkg.com/@esotericsoftware/spine-player@4.2.*/dist/spine-player.css">
                <style>
                  body {
                    background-color: bisque;;
                  }
                </style>
              </head>
              <body>
                <div id="player-container" style="width: 640px; height: 480px;"></div>
                <script>
                  new spine.SpinePlayer("player-container", {
                    skeleton: "https://esotericsoftware.com/files/examples/4.2/spineboy/export/spineboy-pro.json",
                    atlas: "https://esotericsoftware.com/files/examples/4.2/spineboy/export/spineboy-pma.atlas",
                    alpha: true,
                    backgroundColor: '#00000000',
                    animation: 'jump',
                    viewport: {
                      debugRender: true,
                      x: 0,
                      y: 0,
                      width: 200,
                      height: 300,
                      padLeft: "10%",
                      padRight: "10%",
                      padTop: "10%",
                      padBottom: "10%"
                    }
                  });
                </script>
              </body>
            </html>

            jump 动画不在绿框或红框中,而且高度上是撑满 container 480 的,不太明白。为什么视口不是 200* 300 的大小、似乎这个值又进行了自适应 container?为什么动画不在视口中?有详细的使用 demo 吗?

              zangbianxuegu

              The viewport coordinates and dimensions work as in the Spine editor.

              The bottom left corner of the viewport (the green rectangle) corresponds to the x, y coordinates you specify.
              In this case, since you specified the viewport being in x: 0, y:0, the bottom left coordinate of the viewport is centered with the world origin. In spineboy skeleton, the world origin and the root bone are in the same coordinates, so the root bone is centered with the bottom left corner of the viewport.

              Then, you specify width: 200, height: 300 and as you can see we have a rectangle with a proportion of 2/3.
              10% define the red rectangle that is 10% bigger.

              Once the viewport is defined, the spine player fit it horizontally or vertically in canvas. In the center of your canvas you have the coordinate (100;150) of the world origin.

              The piece of code you provided above set a fixed global viewport. If you change animation, no shrink/enlargment occurs. That happens only if no global view is provided, or specific animation viewports are provided.

              Even if you do not have the editor, once you know where the world origin is, than is super easy to set the desired viewport. You know that the bottom-left corner is the x,y coordinate you provide. Just proceed by incremental steps:

              • Set x,y to 0,0, width/height to a reasonable size, and paddings to 0%. See where the bottom left coordinate of the rectangle that represent the viewport is placed.
              • Increase the width by a certain amount. You will see the the bottom-left coordinate remain in the same place, but the rectangle is larger.
              • If you want to move the rectangle, focus on the bottom-left corder. For example, move it a little left by subtracting a certain amount to the x coordinate.

              Iterate over the coordinates and width/height until you find the viewport with your desired size. That will be kept consistent across all animation.

                Davide 谢谢您,抱歉回复晚了,因为我又遇到新的问题。我之前没有发现上面我的例子中,动画之间已经能保持角色的大小一致了,只要设置了全局的 x, y,width,height。

                现在不断调整这些值,能够把所有动画完整显示。

                Davide 我遇到了另外一个问题,希望您有时间帮忙看看~

                我使用的 viewport 设置展示所有的动画,动画包含:

                代码如下:

                <!DOCTYPE html>
                <html lang="en">
                  <head>
                    <meta charset="UTF-8" />
                    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
                    <title>spine</title>
                    <script src="https://unpkg.com/@esotericsoftware/spine-player@4.2.*/dist/iife/spine-player.js"></script>
                    <link
                      rel="stylesheet"
                      href="https://unpkg.com/@esotericsoftware/spine-player@4.2.*/dist/spine-player.css"
                    />
                    <style>
                      body {
                        margin: 0;
                        padding: 0;
                        background-color: bisque;
                      }
                      .grid {
                        position: relative;
                        left: 150px;
                        top: 400px;
                        display: flex;
                        flex-wrap: wrap;
                        width: 600px;
                        height: 60px;
                      }
                      .item {
                        width: 80px;
                        height: 60px;
                        box-sizing: border-box;
                        border: 1px solid green;
                      }
                      .container {
                        position: absolute;
                        left: -30px;
                        top: -10px;
                        width: 300px;
                        height: 200px
                      }
                      .btn {
                        position: absolute;
                        left: 200px;
                        top: 600px;
                        width: 100px;
                        height: 40px;;
                      }
                    </style>
                  </head>
                  <body>
                    <div class="grid">
                      <div class="item"></div>
                      <div class="item"></div>
                      <div class="item"></div>
                      <div class="item"></div>
                      <div class="item"></div>
                      <div class="item"></div>
                      <div class="item"></div>
                      <div class="item"></div>
                      <div class="item"></div>
                      <div class="item"></div>
                      <div class="item"></div>
                      <div class="item"></div>
                      <div class="item"></div>
                      <div class="item"></div>
                      <div id="container" class="container"></div>
                    </div>
                    <div>
                      <button type="button" class="btn" onclick="handleMove()">Move</button>
                    </div>
                    <script>
                      const coords = [
                        {
                          x: -30,
                          y: -10,
                          direction: 'right',
                        },
                        {
                          x: 50,
                          y: -10,
                          direction: 'front',
                        },
                        {
                          x: 130,
                          y: -10,
                          direction: 'right',
                        },
                        {
                          x: 210,
                          y: -10,
                          direction: 'right',
                        },
                        {
                          x: 290,
                          y: -10,
                          direction: 'right',
                        },
                        {
                          x: 370,
                          y: -10,
                          direction: 'right',
                        },
                        {
                          x: 370,
                          y: -70,
                          direction: 'front',
                        },
                      ]
                      const player = new spine.SpinePlayer("container", {
                        skeleton: "./spine/yuyan/yuyan.json",
                        atlas: "./spine/yuyan/yuyan.atlas",
                        alpha: true,
                        backgroundColor: "#00000000",
                        showControls: false,
                        viewport: {
                          debugRender: true,
                          padTop: 0,
                          padLeft: 0,
                          padBottom: 0,
                          padRight: 0,
                          x: -300,
                          y: -200,
                          width: 600,
                          height: 400,
                        },
                        success: (player) => {
                          player.animationState?.setEmptyAnimation(0, 0)
                          player.animationState?.addListener({
                            complete: function (entry) {
                              if (entry.animation.name.includes('move')) {
                                remainingSteps--
                                curPos++
                                if (remainingSteps > 0) {
                                  const animation = coords[curPos].direction + '_move'
                                  handlePosAction(curPos, animation)
                                } else {
                                  const animation = coords[curPos].direction + '_idle'
                                  handlePosAction(curPos, animation)
                                }
                              }
                            },
                          });
                        },
                      });
                
                      let curPos = 0
                      let remainingSteps = 0
                
                      function setPosition(pos) {
                        container.style.left = pos.x + "px";
                        container.style.top = pos.y + "px";
                      }
                
                      function playAnimation(animation) {
                        if (player) {
                          player.animationState.clearTracks();
                          let loop = false
                          if (animation.includes('idle')) {
                            loop = true
                          } else {
                            loop = false
                          }
                          player.setAnimation(animation, loop)
                          player.play();          
                        }
                      }
                
                      function handlePosAction(pos, animation) {
                        setTimeout(() => {
                          setPosition(coords[pos])
                          playAnimation(animation)
                        }, 0)
                      }
                
                      function handleMove() {
                        remainingSteps = 1
                        handlePosAction(curPos, 'right_move')
                      }
                
                      window.onload = () => {
                        const animation = coords[curPos].direction + '_idle'
                        handlePosAction(curPos, animation)
                      }
                    </script>
                  </body>
                </html>

                希望不会有太多的代码干扰问题。当我点击按钮时,container 根据坐标设置位置,以及根据坐标 play 相应的动画。从坐标 0 -> 1,正常 right_move,当 1->2时,front_idle 显示为:

                角色不能显示,这是什么原因呢?

                如果注释掉:player.animationState.clearTracks(); 更够显示出动画,但是动画会回到初始 pose。

                  zangbianxuegu Unfortunately, Davide is currently on vacation so it will take some time for him to respond.

                  By the way, the code you showed us is too long and probably even contains lines that are not directly related to the problem you are experiencing, so we would appreciate it if you could show us the code with a minimum of content. Instead, if you have no problem sharing the full code, it would be faster for us to check if you could email us a minimal project file that can reproduce the problem: contact@esotericsoftware.com
                  Please include the URL of this forum thread in the email so we know the context. Then we can take a look at what's wrong.

                    Misaki hello, I have sent an email, please check, thank you very much!

                      zangbianxuegu Thanks for submitting your project file! I understand your problem. You want to do something like root movement. I'm sure there will be advice from another staff member later on how to achieve this, but I just want to say that the project you sent me did not match the version of the runtime with the version of the skeleton data, so I would recommend fixing that. (The project you sent us was using spine-pixi 4.2 for the runtime, but the skeleton data was exported from 4.1.16.)

                      The major and minor version for the Spine editor used to export JSON or binary data must always match the Spine Runtimes version. Somehow it seems to have worked this time, but there may be problems in the future, so please upgrade your editor version and export the skeleton data again.

                        Misaki 感谢这么快回复!我的真实的项目中使用的是"@esotericsoftware/spine-player": "^4.2.58",,邮件中使用CDN链接只是为了演示。我可以将项目中使用的 spine-player 降为 4.1.16 来解决这个问题吗?

                          zangbianxuegu Sorry for the mistake, indeed what you are using is spine-player!

                          Note that only major and minor versions need to be matched with the editor version. There is no need to have matching patch versions, so you can use the latest 4.1 runtime by writing as follows

                          <script src="https://unpkg.com/@esotericsoftware/spine-player@4.1.*/dist/iife/spine-player.js"></script>
                          <link rel="stylesheet" href="https://unpkg.com/@esotericsoftware/spine-player@4.1.*/dist/spine-player.css">

                          However, if you are already using the newer runtime, it is better to upgrade the editor version than to downgrade the runtime version. If you cannot use the Spine Editor yourself, ask the person who created the animation to upgrade the Editor version and re-export the skeleton for you.

                            Misaki 谢谢,如果使用:

                            <script src="https://unpkg.com/@esotericsoftware/spine-player@4.1.*/dist/iife/spine-player.js"></script>
                            <link rel="stylesheet" href="https://unpkg.com/@esotericsoftware/spine-player@4.1.*/dist/spine-player.css">

                            会有同样的问题,你可以直接在 email 的代码中修改,查看。

                            另外,我也在要求动画师升级editor到 4.2.x,导出文件,我再尝试一下。

                              zangbianxuegu Sorry for the confusion, but the display problem itself has nothing to do with version mismatch. I'm sure another staff member can answer you about that, so please wait a while for a more detailed answer!