• Runtimes
  • [ts-overlay-widget] scale and offset to a percentage of div dimensions

Hi there,

I'm using the beta ts-overlay-widget and what I'm trying to do is be able to scale my spine character based on the percentage of the div height, and also offset x and y by a percentage of the div width/height as well.

I see there is x-axis and y-axis for shifting position by a percentage of div width/height, but it does not seem to be working. Can you see if I'm doing anything wrong in my code?

      <spine-widget
        identifier={identifier}
        atlas="spine-dynamic-nopremult/char.atlas"
        skeleton="spine-dynamic-nopremult/char.skel"
        animation={animation}
        skin={"skin"}
        clip="true"
        mode="origin"
        scale="0.5"
        y-axis="-.5"
        x-axis="0.5"
        debug="true"
      />

Can you also advise on how to set scale percentage according to div height? The current "scale" attribute seems to scale based on skeleton dimensions, not the div dimensions. For example, I want my character to be 50% smaller than the div.

Thanks!

  • Davide ha risposto a questo messaggio
    Related Discussions
    ...

    To achieve scaling and offsetting your Spine character based on the percentage of the div dimensions using the ts-overlay-widget, you can use CSS to control the size and position of the widget itself. The scale attribute in the widget scales based on the skeleton dimensions, not the div dimensions. Here's how you can achieve your desired effect:

    1. Scaling based on div height:
      Use CSS to set the height of the widget to a percentage of the div height, and then use the scale attribute to adjust the size of the character within that height.

    2. Offsetting x and y by a percentage of div width/height:
      Use CSS to position the widget within the div.

    Here's an example of how you can achieve this:

    <div id="spine-container" style="position: relative; width: 100%; height: 100%;">
      <spine-widget
        identifier={identifier}
        atlas="spine-dynamic-nopremult/char.atlas"
        skeleton="spine-dynamic-nopremult/char.skel"
        animation={animation}
        skin={"skin"}
        clip="true"
        mode="origin"
        scale="1" <!-- Set scale to 1 and control size with CSS -->
        style="position: absolute; width: 50%; height: 50%; top: 25%; left: 25%;" <!-- Adjust width, height, top, and left to control size and position -->
        debug="true"
      />
    </div>

    In this example:

    • The spine-widget is set to 50% of the div's width and height.
    • The top and left properties are set to 25% to center the widget within the div.
    • The scale attribute is set to 1 to use the original skeleton dimensions, and the size is controlled by the CSS.

    This approach ensures that the widget scales and positions itself based on the div dimensions.

    If you need more precise control over the scaling and positioning, you might need to use JavaScript to dynamically calculate and set the scale, top, and left properties based on the div's dimensions.

    const container = document.getElementById('spine-container');
    const widget = container.querySelector('spine-widget');
    
    const updateWidgetSize = () => {
      const containerHeight = container.clientHeight;
      const containerWidth = container.clientWidth;
    
      // Set the widget size to 50% of the container height
      widget.style.height = `${containerHeight * 0.5}px`;
      widget.style.width = `${containerWidth * 0.5}px`;
    
      // Center the widget
      widget.style.top = `${containerHeight * 0.25}px`;
      widget.style.left = `${containerWidth * 0.25}px`;
    };
    
    window.addEventListener('resize', updateWidgetSize);
    updateWidgetSize();

    This script dynamically updates the size and position of the spine-widget based on the container's dimensions whenever the window is resized.

    • jojo ha risposto a questo messaggio

      Spinebot I'd like to scale it in the spine world though and not CSS, because most of the time want to scale bigger for a clipped zoomed in effect.

      jojo

      I see there is x-axis and y-axis for shifting position by a percentage of div width/height, but it does not seem to be working. Can you see if I'm doing anything wrong in my code?

      The x-axis and y-axis properties don't seem to be working when clip is enabled. I'll work on fixing that.

      I'm using the beta ts-overlay-widget and what I'm trying to do is be able to scale my spine character based on the percentage of the div height

      Currently, there's no attribute to achieve that. I'd suggest surrounding the hosting div with another one and adding padding to it. If you prefer not to do that, you can accomplish it with some code, but the first approach is definitely easier.

      I'll offer some advice. You're using mode="origin". This means you're in free mode, where the widget simply centers the skeleton within the parent container. Positioning the skeleton in this way can be quite complicated.

      If you instead use mode="inside" (the default mode, so no need to set it) and set fit="none", the widget centers the skeleton using the bounds object. This object is a rectangle with x, y, width, and height properties. The bounds are used to determine the position and scale for the skeleton. Note that bounds use the skeleton's world coordinates/size.
      By default, it's sized using the given skin and animation. The widget uses fit="contain" as default: this ensures the bounds are scaled as large as possible while still being entirely contained within the element container.
      From what I understand, your goal is to link the skeleton's scale (scaleX and scaleY) to half of the div's width and height (likely the smaller one to avoid stretching). This means you want the bounds to be sized as half of the div's width and height.
      Since the div size might change, you should do this continuously. You can implement all of this in the widget's beforeUpdateWorldTransforms method, ensuring that if the div changes size, it will be correctly rescaled accordingly.

      Since the CSS approach is quite straightforward, I don't think we need an additional feature for this. However, I might not have fully understood your goal. Indeed, your last message about scaling bigger for a clipped zoomed-in effect confused me a bit.
      Perhaps you could explain your final goal better or share some images illustrating it? If it proves to be useful, we might consider adding a new feature to support that.

      • jojo ha risposto a questo messaggio

        An illustration via screenshots/drawings would help @jojo

        Meanwhile I've fixed x-axis and y-axis not working in clip mode!
        Thanks for reporting it 🙂

        • jojo ha risposto a questo messaggio

          Davide

          Thanks for your responses! The use case is that we are creating stickers using the spine characters, and for some stickers, we want to zoom in quite a bit on the head of the character, and for other stickers, we want to zoom out on the whole body. The stickers should always look consistent when used by the user, so I want to scale it based on the div height.

          For example, the spine rig is of the whole body but for the crying sticker, we want it to display like this:

          Davide Amazing thanks!

          jojo

          It's still not entirely clear to me why you can't use the CSS padding approach. Perhaps you could help us better understand your problem by providing a sketch with a couple of rectangles showing the two different widgets (zoomed in and not zoomed in) and the issue with sizing.

          In any case, as I mentioned above, you can already achieve this by defining your personalized fit mode.

          First, let's understand the easiest way to focus on a specific part of your skeleton in the div: by using the bounds object.
          For example, if you want to zoom in on Spineboy's head, you can do it like this:

          <spine-widget
              identifier="boi"
              atlas="assets/spineboy-pma.atlas"
              skeleton="assets/spineboy-pro.skel"
              animation="walk"
              debug
              clip
          ></spine-widget>
          
          <script>
              (async () => {
                  const boi = spine.getSpineWidget("boi");
                  await boi.loadingPromise;
          
                  const myBound = {
                      x: -110,
                      y: 350,
                      width: 310,
                      height: 350,
                  };
                  boi.setBounds(myBound);
              })();
          </script>

          However, this won't add any padding since the skeleton is automatically scaled to fit using the default method, which is contain.
          There's no direct way to define your personalized fit mode, but you can set the fit mode to none and utilize the beforeUpdateWorldTransforms callback. The none fit mode doesn't automatically scale the skeleton.beforeUpdateWorldTransforms callback. The fit mode none does not autoscale the skeleton.

          In the beforeUpdateWorldTransforms callback, we want to change the scale of the skeleton based on the div size.
          We need to:

          • get the div size
          • determine the target width/height (1/2 of the div width/height) and transform it to the skeleton space
          • determine the smallest scale ratio
          • scale the skeleton accordingly
          <spine-widget
              identifier="boi"
              atlas="assets/spineboy-pma.atlas"
              skeleton="assets/spineboy-pro.skel"
              animation="walk"
              fit="none"
              debug
              clip
          ></spine-widget>
          
          <script>
              (async () => {
                  const boi = spine.getSpineWidget("boi");
                  const { skeleton } = await boi.loadingPromise;
          
                  const myBound = {
                      x: -110,
                      y: 350,
                      width: 310,
                      height: 350,
                  };
                  boi.setBounds(myBound);
          
                  boi.beforeUpdateWorldTransforms = (skeleton, state) => {
                      // get the div size
                      const containerBounds = boi.getHTMLElementReference().getBoundingClientRect();
          
                      // determine the target width/height (1/2 of the div width/height) and transform it to the skeleton space
                      const targetWidth = containerBounds.width / 2 * window.devicePixelRatio;
                      const targetHeight = containerBounds.height / 2 * window.devicePixelRatio;
          
                      // determine the smallest scale ratio
                      const bounds = boi.bounds;
                      const scaleW = targetWidth / bounds.width;
                      const scaleH = targetHeight / bounds.height;
                      const scale = Math.min(scaleW, scaleH);
          
                      // scale the skeleton accordingly
                      skeleton.scaleX = scale;
                      skeleton.scaleY = scale;
                  }
          
              })();
          
          </script>

          Clearly, this isn't the most straightforward approach, but it works.
          We might consider adding a padding option or an easier way to create a custom fit mode in the future.

          • jojo ha risposto a questo messaggio
          • jojo ha messo mi piace.

            Davide
            I'm trying to do something like the following, where there is a grid of zoomed in sticker emotes. I don't have good zoomed out examples right now though! The beforeUpdateWorldTransforms seems to work, thanks so much for the guidance there. I'll try to use it in combination with y-axis and x-axis next.

            • Davide ha risposto a questo messaggio

              jojo

              I don't see any internal padding here. In this case, a custom bounds + fit="contain" should do the job.

              Note that the approach I suggested works well, but is far from ideal. We have padding options in the Spine player, so we'll probably add them here as well.
              I'll let you know if and when they become available. 🙂

              • jojo ha messo mi piace.
              • Modificato

              We added the possibility to add a virtual padding to the element container of the widget.
              Basically, you can get rid of all the code I suggested above and set:

              <spine-widget
                  identifier="boi"
                  atlas="assets/spineboy-pma.atlas"
                  skeleton="assets/spineboy-pro.skel"
                  animation="walk"
                  debug
                  clip
                  pad-left=".25"
                  pad-right=".25"
                  pad-top=".25"
                  pad-bottom=".25"
              ></spine-widget>
              
              <script>
                  (async () => {
                      const boi = spine.getSpineWidget("boi");
                      await boi.loadingPromise;
                      boi.setBounds({
                          x: -110,
                          y: 350,
                          width: 310,
                          height: 350,
                      });
                  })();
              </script>

              I just want to add one thing. I've noticed that you use the clip attribute a lot.
              Be aware that it decreases the performance on the page. Without the clip attribute, all skeletons using the same texture are drawn by using a single draw call. Instead, if you use the clip attribute, each skeleton is drawn in a dedicated draw call.

              • jojo ha risposto a questo messaggio

                Davide
                Hey Davide,

                Thanks for making that change! The reason I am using clip is because we're trying to render stickers like the ones below, where we zoom into the character. As you can see, the bottom is clipped. Sometimes the sides and top are too.

                Ah thanks for the note about clipping performance. Let's say on a page, I show 30 stickers that are clipped and 30 that are not clipped, do you think there might be visible performance issues? We do need to clip somehow, so I'm just wondering what the impact is on the user and their device. Worst case, I can render the sticker as a png/webm instead via server or client side rendering but that is not as easy as using the spine runtime directly.

                On the topic of performance, I'm also wondering if you think there will be any performance issues with using spine-widget on a web page where we're also streaming a video.

                Thanks!

                • Davide ha risposto a questo messaggio

                  jojo
                  Performance depends on several factors specific to each use case. You might optimize your webpage as much as possible, but still have bad performance because of several other factors (have a read of this page).

                  In the example page of webcomponents, there are a lot of skeletons and on my devices it runs always at full FPS. Let's take a section as an example:

                  There are 15 skeletons on the screen. I have two screens, the one of my MacBook Pro M3 (120Hz), and an external one (60Hz). Then there is also my iPhone 12 mini (60Hz).
                  If I do not use the clip property, on all displays I get full FPS.
                  If I set the clip property for all of them, I get 60 FPS for all 60Hz displays, but 110 FPS for the MacBook Pro display. No one will ever notice this 10 FPS drop on 120Hz displays though.

                  In the extreme case where I'd need to optimize it, would I focus only on the clip property? No, because if you make a further analysis, we discover that there are 60 draw calls even with clip true! The Chibi example stickers as is are not very well optimized for this specific use case. The example has a different texture for each skin. Here we use 4 different textures (3 for the characters and 1 for the common elements). If the goal of this skeleton had been to render these three characters all together, we could have put all of them in a single texture, reducing drastically the number of draw calls.

                  We could go further and optimize it more. For example, Luke's chibi has a shadow on the face that uses the multiply blend mode. That breaks batching too.

                  I actually don't think that you will have big problems in displaying your skeletons.
                  Take also into consideration that the widgets run only if they are visible on the screen. So, if some of them are off screen, these don't affect performance.
                  Regarding the video + spine widget, there too it might depend on several factors. Maybe you can pause the video when it is off screen. But if eventually everything runs smoothly, you can avoid that.
                  My advice is to test this as early as possible. If there are performance issues, you can immediately try to identify the problems.

                  • jojo ha risposto a questo messaggio
                  • Misaki ha messo mi piace.

                    Davide
                    Thanks so much for taking the time to explain this thoroughly 👍

                    • Davide ha risposto a questo messaggio

                      jojo

                      We exposed some parameters to set the bounds from webcomponent's attributes.
                      The previouse code snipper can now be simplified as is:

                      <spine-widget
                          atlas="assets/spineboy-pma.atlas"
                          skeleton="assets/spineboy-pro.skel"
                          animation="walk"
                          debug
                          clip
                          pad-left=".25"
                          pad-right=".25"
                          pad-top=".25"
                          pad-bottom=".25"
                          bounds-x="-100"
                          bounds-y="350"
                          bounds-width="310"
                          bounds-height="350"
                      ></spine-widget>
                      • jojo ha risposto a questo messaggio

                        Davide great, thanks!