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

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:

    Related Discussions
    ...

    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.

            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!

                      Misaki 刚刚尝试了一下动画师用 4.2.x 版本导出的文件。问题还是存在。

                      我明白这个问题还需要另外的解释,只是告知一下。另外,也邮件发送了最新的文件。

                      Misaki hello,早上好!虽然知道需要一点时间,但我真的需要尽快知道解决方法,麻烦您帮我问一下大概的时间!谢谢您!

                        zangbianxuegu Sorry to keep you waiting. I have already asked Mario to review the project you sent us yesterday and this thread, but he has not had enough time to do so as he is providing very extensive support. I think he might respond today if he finds the time, but I can't make any promises. Please wait as I am sure he will see it in order.

                        我发现 setToSetupPose 可以解决问题:

                        player.animationState?.setEmptyAnimation(0, 0)
                        player.skeleton?.setToSetupPose()

                        但是也仍然有问题,会有两个动画之间的闪动

                        Your code does many things that can not work. Let's start with:

                            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);
                            };

                        When window.onload() is called, you call handlePosAction() which in turn calls playAnimation() in a setTimeout() handler. Depending on various server/browser related factors, the player and skeleton may not have been loaded at that time, resulting in an error, e.g.

                        Uncaught TypeError: Cannot read properties of null (reading 'data')
                            at SpinePlayer.setViewport (Player.ts:724:40)
                            at SpinePlayer.setAnimation (Player.ts:711:20)
                            at playAnimation ((index):167:16)
                            at (index):175:9

                        On my machine, I do not see the skeleton when I load the page because of that.

                        You need to move the logic from window.onload() into the success handler of the Spine player. This also allows us to remove the setTimeout() call in handlePosAction().

                        You also queue an empty animation. The complete listener will be called for that empty animation as well. Empty animations do not have their animation field set, so the handler code in complete fails. I added a check.

                        The default mix duration is set to something != 0. This means that when switching from one animation to the next, there will be interpolation. Your skeleton is extremely complicated and when previewing it in Spine in the preview pane, switching between animations, one can see that the transitions between animations are broken. This is not something the Spine player can fix. That is something you need to fix in your Spine project in the Spine Editor. @Misaki can possibly help you with that. In any case, I've set the default mix duration to 0.

                        Finally, in handlePosAction() you call setPosition() before playAnimation(). setPosition() will immediately move the <div> containing the <canvas>, while the player hasn't rendered a new frame with the new animation yet. This results in a noticable flicker. It can be fixed by first playing the new animation, and then moving the <div> in the next frame.

                        Here's the entire fixed code:

                        <!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.json",
                              atlas: "./spine/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) => {
                                const animation = coords[curPos].direction + "_idle";
                                handlePosAction(curPos, animation);
                                player.animationState.data.defaultMix = 0;
                        
                                player.animationState?.addListener({
                                  complete: function (entry) {
                                    if (!entry.animation) return; // empty animation, do nothing
                                    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) {
                                if (animation.includes("idle")) {
                                  loop = true;
                                } else {
                                  loop = false;
                                }
                                player.setAnimation(animation, loop);
                                player.play();
                              }
                            }
                        
                            function handlePosAction(pos, animation) {
                              playAnimation(animation);
                              requestAnimationFrame(() => setPosition(coords[pos]));
                            }
                        
                            function handleMove() {
                              remainingSteps = 1;
                              handlePosAction(curPos, "right_move");
                            }
                          </script>
                        </body>
                        
                        </html>

                        Now, this is clearly going to be some kind of game. I would STRONGLY suggest to not use Spine player for games. Please use spine-phaser or spine-pixi.

                          Mario 非常感谢!可惜现在才知道最后一点 :