Search code examples
csssvg

scale with transformY moves element in the wrong direction


I try to animate the small bubbles in my svg. But when scale is applied it looks like it's applied to the distances between the bubbles rather than the bubbles only. It makes the buble to move to the side rather then up only as I'd like them. Why? And how to fix it? The transform-origin only changes the direction all of them moves.

svg {
position: absolute;
left: 200px;
top: 200px;
overflow: visible;
}

@keyframes bubble {
        0% {
            opacity: 1;
            transform: translateY(0) scale(1);
        }
        50% {
            opacity: 1;
            transform: translateY(-50px) scale(0.5);
        }
        100% {
            opacity: 0;
            transform: translateY(-100px) scale(0);
        }
    }

#circle1,
#circle2,
#circle3,
#circle4 {

transform-origin: top;
  animation: bubble 4s ease-in-out infinite;
}
<svg width="500" height="1000" xmlns="http://www.w3.org/2000/svg">
  <circle id="circle1" cx="30" cy="30" r="10" fill="red"/>
  <circle id="circle2" cx="70" cy="30" r="10" fill="blue"/>
  <circle id="circle3" cx="30" cy="70" r="10" fill="green"/>
  <circle id="circle4" cx="70" cy="70" r="10" fill="yellow"/>
</svg>


Solution

  • Update

    You can use transform-box: fill-box to make the circles behave like HTML elements

    svg {
      contain: paint;
    }
    
    circle {
      animation: bubble 4s ease-in-out infinite;
      transform-box: fill-box;
      transform-origin: 50%;
    }
    
    @keyframes bubble {
      50% {
        opacity: 1;
        transform: translateY(-50px) scale(.5);
      }
      100% {
        opacity: 0;
        transform: translateY(-100px) scale(0);
      }
    }
    <svg width="100" viewBox="0 -100 100 200">
      <circle cx="30" cy="30" r="10" fill="red"/>
      <circle cx="70" cy="30" r="10" fill="blue"/>
      <circle cx="30" cy="70" r="10" fill="green"/>
      <circle cx="70" cy="70" r="10" fill="yellow"/>
    </svg>


    Old answer

    transform-origin: top sets the transform origin to the middle of the top edge of the <svg> element (in your case 250px 0), so all transforms happen in relation to that point. You probably want something like this:

    svg {
      background: pink; /* visualize bounds */
      margin-top: 100px;
      overflow: visible;
    }
    
    @keyframes bubble {
      0% {
        transform: translateY(0) scale(1);
      }
      50% {
        opacity: 1;
        transform: translateY(-50px) scale(0.5);
      }
      100% {
        opacity: 0;
        transform: translateY(-100px) scale(0);
      }
    }
    
    circle {
      animation: bubble 4s ease-in-out infinite;
    }
    
    
    /* solution 2 */
    #circle1 { transform-origin: 30px 30px }
    #circle2 { transform-origin: 70px 30px }
    #circle3 { transform-origin: 30px 70px }
    #circle4 { transform-origin: 70px 70px }
    <!-- solution 1 -->
    <svg width="100" height="100">
      <g transform="translate(30, 30)">
        <circle r="10" fill="red"/>
      </g>
      <g transform="translate(70, 30)">
        <circle r="10" fill="blue"/>
      </g>
      <g transform="translate(30, 70)">
        <circle r="10" fill="green"/>
      </g>
      <g transform="translate(70, 70)">
        <circle r="10" fill="yellow"/>
      </g>
    </svg>
    
    <!-- solution 2 (see css) -->
    <svg width="100" height="100">
      <circle id="circle1" cx="30" cy="30" r="10" fill="red"/>
      <circle id="circle2" cx="70" cy="30" r="10" fill="blue"/>
      <circle id="circle3" cx="30" cy="70" r="10" fill="green"/>
      <circle id="circle4" cx="70" cy="70" r="10" fill="yellow"/>
    </svg>

    You can then position the <svg> however you want.

    If you want to optimize the rendering, you should avoid using overflow: visible and instead expand the SVG to accommodate for the animation, and maybe use contain: paint, like so:

    svg {
      background: pink; /* visualize bounds */
      contain: paint;
      margin-top: -100%;
    }
    
    @keyframes bubble {
      0% {
        transform: translateY(0) scale(1);
      }
      50% {
        opacity: 1;
        transform: translateY(-50px) scale(0.5);
      }
      100% {
        opacity: 0;
        transform: translateY(-100px) scale(0);
      }
    }
    
    circle {
      animation: bubble 4s ease-in-out infinite;
    }
    
    /* example container */
    div {
      display: grid;
      min-height: 180px;
      outline: pink solid;
      place-content: center;
    }
    <div>
      <svg width="50" height="100" viewBox="0 -100 100 200">
        <g transform="translate(30, 30)">
          <circle r="10" fill="red"/>
        </g>
        <g transform="translate(70, 30)">
          <circle r="10" fill="blue"/>
        </g>
        <g transform="translate(30, 70)">
          <circle r="10" fill="green"/>
        </g>
        <g transform="translate(70, 70)">
          <circle r="10" fill="yellow"/>
        </g>
      </svg>
    </div>

    Here we're "raising the ceiling" in the <svg>, and lowering it again with margin-top: -100%, so the image can be centered properly. You can also scale the SVG using width/height, as long as you provide a viewBox