Search code examples
animationsvgsprite

Animated SVG sprite in IMG tag vibrates in horizontal direction


See code snippet for bouncing circle in 24 sprite frames.

<text y='12'>n:0</text> displays the sprite framenr

How can I get rid of the horizontal jitter movement, is it SVG or CSS?

The offset is less at less frames in sprite, and gets worse and worse at more frames per sprite

It is less obvious in Chromium, and more obvious in FireFox

I tried https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/preserveAspectRatio

<style>
  #bounce {
    --w: 200px;
    width: var(--w);
    height: var(--w);
    overflow: hidden;
    display: inline-flex;
    background: lightgreen;
  }

  img {
    position: relative;
    left: 0;
    animation: moveX 1s steps(23) infinite;
  }

  @keyframes moveX {
    to {
      transform: translate(-100%);
      left: 100%;
    }
  }

</style>
<div id="bounce"><img id=svgimg src="SVG injected here"></div>
<script>
  window.onload = () => svgimg.src = `data:image/svg+xml,` + svg.innerHTML;
</script>
<template id=svg>
  <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 2400 100' height='200'>
    <style>
      ellipse {
        fill: none;
        stroke: green;
        stroke-width: 5;
      }

    </style>
    <g transform='translate(0 0)'>
      <ellipse cx='50' cy='32' rx='30' ry='30'></ellipse><text y='12'>n:0</text>
    </g>
    <g transform='translate(100 0)'>
      <ellipse cx='50' cy='32.3' rx='30' ry='30'></ellipse><text y='12'>n:1</text>
    </g>
    <g transform='translate(200 0)'>
      <ellipse cx='50' cy='33.2' rx='30' ry='30'></ellipse><text y='12'>n:2</text>
    </g>
    <g transform='translate(300 0)'>
      <ellipse cx='50' cy='34.5' rx='30' ry='30'></ellipse><text y='12'>n:3</text>
    </g>
    <g transform='translate(400 0)'>
      <ellipse cx='50' cy='36.5' rx='30' ry='30'></ellipse><text y='12'>n:4</text>
    </g>
    <g transform='translate(500 0)'>
      <ellipse cx='50' cy='39' rx='30' ry='30'></ellipse><text y='12'>n:5</text>
    </g>
    <g transform='translate(600 0)'>
      <ellipse cx='50' cy='42' rx='30' ry='30'></ellipse><text y='12'>n:6</text>
    </g>
    <g transform='translate(700 0)'>
      <ellipse cx='50' cy='45.7' rx='30' ry='30'></ellipse><text y='12'>n:7</text>
    </g>
    <g transform='translate(800 0)'>
      <ellipse cx='50' cy='49.8' rx='30' ry='30'></ellipse><text y='12'>n:8</text>
    </g>
    <g transform='translate(900 0)'>
      <ellipse cx='50' cy='54.5' rx='31.3' ry='30'></ellipse><text y='12'>n:9</text>
    </g>
    <g transform='translate(1000 0)'>
      <ellipse cx='50' cy='59.8' rx='33.9' ry='30'></ellipse><text y='12'>n:10</text>
    </g>
    <g transform='translate(1100 0)'>
      <ellipse cx='50' cy='65.7' rx='36.9' ry='30'></ellipse><text y='12'>n:11</text>
    </g>
    <g transform='translate(1200 0)'>
      <ellipse cx='50' cy='72' rx='40' ry='30'></ellipse><text y='12'>n:12</text>
    </g>
    <g transform='translate(1300 0)'>
      <ellipse cx='50' cy='65.7' rx='36.9' ry='30'></ellipse><text y='12'>n:13</text>
    </g>
    <g transform='translate(1400 0)'>
      <ellipse cx='50' cy='59.8' rx='33.9' ry='30'></ellipse><text y='12'>n:14</text>
    </g>
    <g transform='translate(1500 0)'>
      <ellipse cx='50' cy='54.5' rx='31.3' ry='30'></ellipse><text y='12'>n:15</text>
    </g>
    <g transform='translate(1600 0)'>
      <ellipse cx='50' cy='49.8' rx='30' ry='30'></ellipse><text y='12'>n:16</text>
    </g>
    <g transform='translate(1700 0)'>
      <ellipse cx='50' cy='45.7' rx='30' ry='30'></ellipse><text y='12'>n:17</text>
    </g>
    <g transform='translate(1800 0)'>
      <ellipse cx='50' cy='42' rx='30' ry='30'></ellipse><text y='12'>n:18</text>
    </g>
    <g transform='translate(1900 0)'>
      <ellipse cx='50' cy='39' rx='30' ry='30'></ellipse><text y='12'>n:19</text>
    </g>
    <g transform='translate(2000 0)'>
      <ellipse cx='50' cy='36.5' rx='30' ry='30'></ellipse><text y='12'>n:20</text>
    </g>
    <g transform='translate(2100 0)'>
      <ellipse cx='50' cy='34.5' rx='30' ry='30'></ellipse><text y='12'>n:21</text>
    </g>
    <g transform='translate(2200 0)'>
      <ellipse cx='50' cy='33.2' rx='30' ry='30'></ellipse><text y='12'>n:22</text>
    </g>
    <g transform='translate(2300 0)'>
      <ellipse cx='50' cy='32.3' rx='30' ry='30'></ellipse><text y='12'>n:23</text>
    </g>
  </svg></template>

Note

I can not use a library, I am generating (and changing) spritesheets Client-Side

Template for this circle is a template-literal:

<template id="bounce">
  <ellipse cx='50' 
           cy='${72-1*ease(40)}' 
           rx='${minmax(30,40-ease(20))}' 
           ry='30' fill='none' stroke='black' stroke-width='5'>
  </ellipse>
  <text y='12'>n:${framenr}</text>
</template>

A Web Component then (re)creates the sprite and IMG, and everything to display the sprite

<svg-spriter do="bounce" steps="24" duration="1s" animation="infinite"></svg-spriter>


Solution

  • I have made some changes to the fiddle, and it seems to work. One point seems to be that the last left coordinate in the transformation should be the total length of the svg minus the length of the window. In the fiddle, you can change

    transform: translate(calc(-1 * var(--steps) * var(--w)));
    

    to

    transform: translate(calc(-1 * calc(var(--steps) - 1) * var(--w)));
    

    In your snippet, the transform: translate(-100%); percentage should be (2300/2400)*100.

    More importantly, I have added the width='2400' property to the svg. The matter of setting a missing length property for an svg is quite complex, and I do not think that every browser behaves the same way.

    In the fiddle, I have changed the --w property to 100, which is the height and width of each svg frame. If you want to set it to 200, you would need to change the size of the svg and the coordinates of each frame proportionally.