0

Aside from some basic transitions, i'm pretty much a novice when it comes to animations in css/JS. For a Project that starts next week, i have commited to a specific animation that i find somewhat challenging. Here's the Animation explained:

  • There should be 10-30 Images that rotate/circulate around a Ring/circle.
    • The Images should also randomly have a small offset away from the Ring, as to give a "scattered" effect
  • The Images should be spread around the ring to "fill" it.
  • The Images should have a "bounce" effect or similar

Image of what i'd hope to achieve: Animation Image

What i've tried so far: I've been researching some JS Libraries for Animations. Those that stood out to me were animeJS (https://animejs.com/) and MoJS (https://mojs.github.io/). I've decided to test out AnimeJS.

Here is a CodePen: CodePen

const imgContainer = document.getElementById("imgContainer");
const svgContainer = document.getElementById("svgContainer");

const imgURL =
  "https://images.unsplash.com/photo-1678833823181-ec16d450d8c1?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=774&q=80";

function generateCircleWithRandomSizes(index) {
  const randomSize = generateRandomNumberBetween(450, 520);
  return `<div class="svgWrapper">
<svg width="${randomSize}" height="${randomSize}" fill="none" stroke="none" id="svgContainer">
            <path id="path${index + 1}" fill="none" d="
                m ${(800 - randomSize) / 2}, ${800 - randomSize}
                a 1,1 0 1,1 ${randomSize},0
                a 1,1 0 1,1 -${randomSize},0
                " />
        </svg>
</div>`;
}

function generateRandomNumberBetween(min, max) {
  return Math.floor(Math.random() * (max - min + 1) + min);
}

for (let i = 0; i < 30; i++) {
  imgContainer.innerHTML += `<img class="image" id="img${i + 1}" src="${imgURL}" />`;

  svgContainer.innerHTML += generateCircleWithRandomSizes(i);

  let path = anime.path(`#path${i + 1}`);

  anime({
    targets: `#img${i + 1}`,
    translateX: path("x"),
    translateY: path("y"),
    easing: "linear",
    duration: 10000,
    loop: true
  });
}

Here's the approach i'm currently trying, but as the CodePen shows, i'm running into multiple issues. I'm generating as many Circles as i have images and want target 1 Image per Path. This approach would give me the scattered effect i'm trying to get. However, as you can see, it seems that animeJS only animates one of the Images, and that it seems to follow the path, but offset to the top left. I'm assuming this has something to do with my CSS and how i "center" SVG/Path, which ultimately shows another issue i have. I'm not quite sure how to dynamically generate theses rings and always center them.

I'm having a bit of a hard time to put my finger on how to best solve this. Am i using the best library for this use case? Do i even need a library? Should i got at it from a completely different angle?

I'd really love to get some help on this.

Moritz Ringler
  • 9,772
  • 9
  • 21
  • 34
SimCode1
  • 5
  • 3
  • I posted a solution below. But I don't know if you were drawing the svg circle just for demonstrating to see what was going on, or if you goal was to follow a specific svg path (which is entirely different that what I did) If its just to go in a circle, it's simple enough. – Phaelax z Mar 15 '23 at 15:16
  • See also this approach using [svg SMIL animations](https://stackoverflow.com/questions/73573273/offseting-animation-start-point-for-svg-animatemotion/73575652#73575652) – herrstrietzel Mar 15 '23 at 15:40

2 Answers2

0

Here's a simple approach. It doesn't do the 'bounce' or small offsets, but this should be a good starter for you.

var radius = 150;
var offsetX = 300;
var offsetY = 200;
var offsetA = 0;

setInterval(updateSpin, 30);


updateSpin();

function updateSpin(){  
  var imgs = document.querySelectorAll( '#imgContainer img' );
  

    // Evenly distribute images around circle
  slice = 360.0 / imgs.length; 


    imgs.forEach((e,index) => {
    // Convert angle to radians
    radians = (slice*index + offsetA) * (Math.PI/180);
    
    // Some simple pythagora geometry
    x = offsetX + Math.cos(radians) * radius;
    y = offsetY + Math.sin(radians) * radius;
    
    // Update image position
    e.style.top  = y+'px';
    e.style.left = x+'px';
  });

    // Increment angle to images rotate on next update
    offsetA += 0.5;
}
.container{background:white;}

.imgContainer img{position:fixed;width:100px;height:80px}
<div class="container">
  <div class="inner">
    <div class="imgContainer" id="imgContainer">
      <img src="">
      <img src="">
      <img src="">
      <img src="">
      <img src="">
      <img src="">
      <img src="">
    </div>
  </div>
</div>
Phaelax z
  • 1,814
  • 1
  • 7
  • 19
  • I like this approach. The issue i see with this is, when i want the animation to be slow, it will look choppy. If i want it to be really fast, i'm limited to 1ms Interval. I will experiment a bit with this and see if its a viable approach Thanks – SimCode1 Mar 15 '23 at 17:05
0

You current anime.js code only animates the last element as you're breaking the previous element bindings by using innerHTML().

As an alternative you could use insertAdjacentHTML() as explained here "Is it possible to append to innerHTML without destroying descendants' event listeners?"

  imgContainer.insertAdjacentHTML('beforeend', `<img class="image" id="img${
    i + 1}" src="${imgURL}" />`) ;

  svgContainer.insertAdjacentHTML('beforeend', generateCircleWithRandomSizes(i)); 

Example: anime.js

const ns = "http://www.w3.org/2000/svg";
const svgContainer = document.getElementById("svgContainer");
const svgEl = document.getElementById("svg");
const defs = document.getElementById("defs");
const imgContainer = document.getElementById("imgContainer");

const imgURL =
  "https://images.unsplash.com/photo-1678833823181-ec16d450d8c1?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=774&q=80";

function generateCircleWithRandomSizes(index) {
  let randomSize = generateRandomNumberBetween(400, 500);
  let newMpath = document.createElementNS(ns, "path");
  let d = `M ${(800 - randomSize) / 2}, ${800 - randomSize}
                a 1,1 0 1,1 ${randomSize},0
                a 1,1 0 1,1 -${randomSize},0z`;
  newMpath.setAttribute("d", d);
  newMpath.id = `path${index}`;
  defs.appendChild(newMpath);
}

function generateRandomNumberBetween(min, max) {
  return Math.floor(Math.random() * (max - min + 1) + min);
}

for (let i = 0; i < 10; i++) {
  generateCircleWithRandomSizes(i);

  let newImg = document.createElement("img");
  newImg.id = "img" + i;
  newImg.src = imgURL;
  newImg.classList.add("image");
  imgContainer.appendChild(newImg);

  let path = anime.path(`#path${i}`);
  let target = document.querySelector(`#img${i}`);


  anime({
    targets: target,
    translateX: path("x"),
    translateY: path("y"),
    easing: "linear",
    duration: 10000,
    loop: true
  });
}
body {
  margin: 0;
}

.container {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
  width: 100vw;
  background-color: lightblue;
}

.inner {
  height: 1000px;
  width: 1000px;
  display: flex;
  justify-content: center;
  align-items: center;
  background: lightpink;
}

.imgContainer img {
  width: 10%;
  height: 15%;
  position: absolute;
  top: -100px;
  left: -50px;
}

#svgContainer {
  position: relative;
  height: 800px;
  width: 800px;
}

.svgWrapper {
  position: absolute;
  top: 0;
  left: 0;
}

path {
  stroke: black;
  stroke-width: 1px;
  fill: none;
}

@keyframes bouncing {
  0% {
    bottom: 0;
    box-shadow: 0 0 5px rgba(250, 250, 0, 0.3);
  }
  100% {
    bottom: 50px;
    box-shadow: 0 50px 50px rgba(250, 0, 0, 0.2);
  }
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js"></script>

<body>
  <div class="container">
    <div class="inner">
      <div class="imgContainer" id="imgContainer">

      </div>

      <div id="svgContainer">
        <svg viewBox="0 0 800 800" fill="none" stroke="#000" id="svg">
          <g id="defs"></g>
      </div>
    </div>
  </div>
</body>

Alternative: SVG SMIL <AnimateMotion>

const ns = "http://www.w3.org/2000/svg";
const svgContainer = document.getElementById("svgContainer");
const svgEl = document.getElementById("svg");
const defs = document.getElementById("defs");
const duration = 6;

// image array
let images = [
  { src: "https://placehold.co/100x100/green/FFF", width: 100, height: 100 },
  { src: "https://placehold.co/100x100/orange/FFF", width: 100, height: 100 },
  { src: "https://placehold.co/100x100/red/FFF", width: 100, height: 100 }
];

for (let i = 0; i < images.length; i++) {
  
  // generate random motion paths
  generateCircleWithRandomSizes(i);
  
  // create svg <image> elements
  let img = images[i];
  let newImage = document.createElementNS(ns, "image");
  newImage.setAttribute("x", -img.width / 2);
  newImage.setAttribute("y", -img.height / 2);
  newImage.setAttribute("width", img.width);
  newImage.setAttribute("height", img.height);
  newImage.setAttribute("href", img.src);
  newImage.id = `img${i}`;

  // define animation
  let animateMotion = document.createElementNS(ns, "animateMotion");
  animateMotion.setAttribute("begin", `-${(duration / images.length) * i} `);
  animateMotion.setAttribute("dur", `${duration}`);
  animateMotion.setAttribute("repeatCount", "indefinite");
  let mpath = document.createElementNS(ns, "mpath");
  mpath.setAttribute("href", `#path${i}`);

  // append elements
  animateMotion.appendChild(mpath);
  newImage.appendChild(animateMotion);
  svg.appendChild(newImage);

}

function generateCircleWithRandomSizes(index) {
  let randomSize = generateRandomNumberBetween(400, 500);
  let newMpath = document.createElementNS(ns, "path");
  let d = `M ${(800 - randomSize) / 2}, ${800 - randomSize}
                a 1,1 0 1,1 ${randomSize},0
                a 1,1 0 1,1 -${randomSize},0z`;
  newMpath.setAttribute("d", d);
  newMpath.id = `path${index}`;
  defs.appendChild(newMpath);
}

function generateRandomNumberBetween(min, max) {
  return Math.floor(Math.random() * (max - min + 1) + min);
}
svg{
  width:50%;
  border:1px solid #ccc
}

image{
   animation: 0.5s bouncing forwards infinite;
}


@keyframes bouncing {
  0% {
    transform: scale(1)
  }
  50% {
    transform: translate(10px, 10px) scale(1)
  }
  100% {
    transform: scale(1)
  }
}
    <div id="svgContainer">
      <svg viewBox="0 0 800 800" fill="none" stroke="#000" id="svg">
        <g id="defs"></g>
      </svg>
    </div>

In this example you could append your images as <image> elements to your <svg> element.

<!-- define motion path -->
<defs>
  <path d="M 175, 350a 1,1 0 1,1 450,0a 1,1 0 1,1 -450,0z" id="path0"/>
</defs>
<image x="-50" y="-50" width="100" height="100" href="https://placehold.co/100x100/green/FFF" id="img0">
    <animateMotion begin="-10 " dur="6" repeatCount="indefinite">
        <!-- reference motion path -->
        <mpath href="#path0"></mpath>
    </animateMotion>
</image>

The path offset can be achieved by a negative begin value.
As explained here "Offseting animation start point for SVG AnimateMotion".

Alternative 2: offset-path

Disclaimer: Currently not fully implemented by a lot of browsers (especially webkit /safari).

const duration = 6;

// image array
let images = [
  { src: "https://placehold.co/100x100/green/FFF", width: 100, height: 100 },
  { src: "https://placehold.co/100x100/orange/FFF", width: 100, height: 100 },
  { src: "https://placehold.co/100x100/red/FFF", width: 100, height: 100 }
];

for (let i = 0; i < images.length; i++) {
  //generate random motion paths
  let d = generateCircleWithRandomSizes(i, 400, 520);

  // create svg <image> elements
  let img = images[i];
  let newImage = document.createElement("img");
  newImage.classList.add('image');
  newImage.setAttribute("width", img.width);
  newImage.setAttribute("height", img.height);
  newImage.setAttribute("src", img.src);
  newImage.id = `img${i}`;


  // append elements
  imgContainer.appendChild(newImage);
  
  // define offset path
  newImage.style["offset-path"] = `path('${d}')`;  
  newImage.style["offset-rotate"] = `0deg`;
  let delay = (100 / images.length) * i;

  newImage.animate(
    [
      { offsetDistance: `${0 + delay}%` },
      { offsetDistance: `${100 + delay}%` }
    ],
    {
      duration: duration*1000,
      iterations: Infinity
    }
  );
}

function generateCircleWithRandomSizes(index, r1=400, r2=500) {
  let randomSize = generateRandomNumberBetween(r1, r2);
  let d = `M ${(800 - randomSize) / 2}, ${
    800 - randomSize
  }a 1,1 0 1,1 ${randomSize},0a 1,1 0 1,1 -${randomSize},0z`;
  return d;
}

function generateRandomNumberBetween(min, max) {
  return Math.floor(Math.random() * (max - min + 1) + min);
}
svg{
  width:50%;
  border:1px solid #ccc
}

.image{
  position:absolute;
  animation: 0.5s bouncing forwards infinite;
}


@keyframes bouncing {
  0% {
    transform: scale(1)
  }
  50% {
    transform: translate(10px, 10px) scale(1)
  }
  100% {
    transform: scale(1)
  }
}
<div class="imgContainer" id="imgContainer">
</div>

Hopefully, we see better support in the near future. The main benefits:

  • specify a motion path in css - no need of a svg element.
  • Starting offsets can easily controlled in css via offset-distance property.
herrstrietzel
  • 11,541
  • 2
  • 12
  • 34
  • Sorry for the late reply. I tried all 3 approaches (fixed animeJS, SVG AnimateMotion & offset-path) I've sticked to the SVG AnimateMotion. I wasn't aware of its existence and it works perfectly for what i'm trying to achieve. I also needed the ability to pause/restart animations, which it also offers. Thanks for your help, really appreciated :) – SimCode1 Mar 31 '23 at 08:40