Yandy_Viera
Yandy_Viera

Reputation: 4380

How to add physics to CSS animations?

I'm just making a loading screen using CSS and I want it to have physically accurate behavior. I'm trying with the animation-timing-function: cubic-bezier(1, 0, 1, 1), looks fine but not as real like I want, at first because I don't know how do cubic-bezier parameters really work, I found this site and just played around with them until I got something nice.

To sum up, how can I add physically accurate behavior to my animation? I am looking for a CSS-only solution, but JavaScript is fine too if it's impossible.

Here you have an example:

body{
    background-color: #02a2bb;
}

.wrapper {
    padding: 50px;
    text-align: center;
}
.content {
    height: 125px;
    margin: 0 auto;
    position: relative;
    display: inline-block;
}
.ball {
    width: 25px;
    height: 25px;
    display: inline-block;
    border-radius: 50%;
    bottom: 0;
    position: relative;
    background-color: #fff;
    z-index: 1;
}
.ball-shadow {
    width: 20px;
    height: 6px;
    border-radius: 50%;
    position: absolute;
    bottom: 9px;
    left: 50%;
    -webkit-transform: translateX(-50%);
    -moz-transform: translateX(-50%);
    transform: translateX(-50%);
}
.animated {
    -webkit-animation-duration: 1s;
    -moz-animation-duration: 1s;
    -ms-animation-duration: 1s;
    -o-animation-duration: 1s;
    animation-duration: 1s;
    -webkit-animation-fill-mode: both;
    -moz-animation-fill-mode: both;
    -ms-animation-fill-mode: both;
    -o-animation-fill-mode: both;
    animation-fill-mode: both;
    -webkit-animation-iteration-count: infinite;
    -moz-animation-iteration-count: infinite;
    -ms-animation-iteration-count: infinite;
    -o-animation-iteration-count: infinite;
    animation-iteration-count: infinite;
}
.animated.jump, .animated.displace, .animated.diffuse-scale {
    -webkit-animation-duration: 3s;
    -moz-animation-duration: 3s;
    -ms-animation-duration: 3s;
    -o-animation-duration: 3s;
    animation-duration: 3s;
}
@-webkit-keyframes jump {
    0% {
        opacity: 1;
        -webkit-animation-timing-function: cubic-bezier(1, 0, 1, 1);
        -webkit-transform: translate(0, 0);
    }
    15% {
        opacity: 1;
        -webkit-transform: translate(0, 100px) scale(1.1, 0.9);
    }
    30% {
        opacity: 1;
        -webkit-animation-timing-function: cubic-bezier(1, 0, 1, 1);
        -webkit-transform: translate(0, 15px);
    }
    45% {
        opacity: 1;
        -webkit-transform: translate(0, 100px) scale(1.08, 0.92);
    }
    60% {
        opacity: 1;
        -webkit-animation-timing-function: cubic-bezier(1, 0, 1, 1);
        -webkit-transform: translate(0, 45px);
    }
    70% {
        opacity: 1;
        -webkit-transform: translate(0, 100px) scale(1.05, 0.95);
    }
    80% {
        opacity: 1;
        -webkit-animation-timing-function: cubic-bezier(1, 0, 1, 1);
        -webkit-transform: translate(0, 65px);
    }
    85% {
        opacity: 1;
        -webkit-transform: translate(0, 100px) scale(1.03, 0.97);
    }
    90% {
        opacity: 1;
        -webkit-animation-timing-function: cubic-bezier(1, 0, 1, 1);
        -webkit-transform: translate(0, 80px);
    }
    95% {
        opacity: 1;
        -webkit-transform: translate(0, 100px) scale(1.01, 0.99);
    }
    97% {
        opacity: 1;
        -webkit-animation-timing-function: cubic-bezier(1, 0, 1, 1);
        -webkit-transform: translate(0, 95px);
    }
    100% {
        opacity: 0;
        -webkit-transform: translate(0, 100px);
    }
}

@keyframes jump {
    0% {
        opacity: 1;
        -moz-animation-timing-function: cubic-bezier(1, 0, 1, 1);
        animation-timing-function: cubic-bezier(1, 0, 1, 1);
        -moz-transform: translate(0, 0);
        transform: translate(0, 0);
    }
    15% {
        opacity: 1;
        -moz-transform: translate(0, 100px) scale(1.1, 0.9);
        transform: translate(0, 100px) scale(1.1, 0.9);
    }
    30% {
        opacity: 1;
        -moz-animation-timing-function: cubic-bezier(1, 0, 1, 1);
        animation-timing-function: cubic-bezier(1, 0, 1, 1);
        -moz-transform: translate(0, 15px);
        transform: translate(0, 15px);
    }
    45% {
        opacity: 1;
        -moz-transform: translate(0, 100px)scale(1.08, 0.92);
        transform: translate(0, 100px)scale(1.08, 0.92);
    }
    60% {
        opacity: 1;
        -moz-animation-timing-function: cubic-bezier(1, 0, 1, 1);
        animation-timing-function: cubic-bezier(1, 0, 1, 1);
        -moz-transform: translate(0, 45px);
        transform: translate(0, 45px);
    }
    70% {
        opacity: 1;
        -moz-transform: translate(0, 100px)scale(1.05, 0.95);
        transform: translate(0, 100px)scale(1.05, 0.95);
    }
    80% {
        opacity: 1;
        -moz-animation-timing-function: cubic-bezier(1, 0, 1, 1);
        animation-timing-function: cubic-bezier(1, 0, 1, 1);
        -moz-transform: translate(0, 65px);
        transform: translate(0, 65px);
    }
    85% {
        opacity: 1;
        -moz-transform: translate(0, 100px) scale(1.03, 0.97);
        transform: translate(0, 100px) scale(1.03, 0.97);
    }
    90% {
        opacity: 1;
        -moz-animation-timing-function: cubic-bezier(1, 0, 1, 1);
        animation-timing-function: cubic-bezier(1, 0, 1, 1);
        -moz-transform: translate(0, 80px);
        transform: translate(0, 80px);
    }
    95% {
        opacity: 1;
        -moz-transform: translate(0, 100px) scale(1.01, 0.99);
        transform: translate(0, 100px) scale(1.01, 0.99);
    }
    97% {
        opacity: 1;
        -moz-animation-timing-function: cubic-bezier(1, 0, 1, 1);
        animation-timing-function: cubic-bezier(1, 0, 1, 1);
        -moz-transform: translate(0, 95px);
        transform: translate(0, 95px);
    }
    100% {
        opacity: 0;
        -moz-transform: translate(0, 100px);
        transform: translate(0, 100px);
    }
}

@-webkit-keyframes diffuse-scale {
    0% {
        box-shadow: 0 14px 8px rgba(0, 0, 0, 0.5);
        -webkit-animation-timing-function: cubic-bezier(1, 0, 1, 1);
        -webkit-transform: scale(1.5, 1) translateX(-50%);
    }
    15% {
        box-shadow: 0 14px 2px rgba(0, 0, 0, 0.5);
        -webkit-transform: scale(1, 1) translateX(-50%);
    }
    30% {
        box-shadow: 0 14px 7px rgba(0, 0, 0, 0.5);
        -webkit-animation-timing-function: cubic-bezier(1, 0, 1, 1);
        -webkit-transform: scale(1.4, 1) translateX(-50%);
    }
    45% {
        box-shadow: 0 14px 2px rgba(0, 0, 0, 0.5);
        -webkit-transform: scale(1, 1) translateX(-50%);    }
    60% {
        box-shadow: 0 14px 5px rgba(0, 0, 0, 0.5);
        -webkit-animation-timing-function: cubic-bezier(1, 0, 1, 1);
        -webkit-transform: scale(1.3, 1) translateX(-50%);    }
    70% {
        box-shadow: 0 14 2px rgba(0, 0, 0, 0.5);
        -webkit-transform: scale(1, 1) translateX(-50%);
    }
    80% {
        box-shadow: 0 14px 4px rgba(0, 0, 0, 0.5);
        -webkit-animation-timing-function: cubic-bezier(1, 0, 1, 1);
        -webkit-transform: scale(1.2, 1) translateX(-50%);
    }
    85% {
        box-shadow: 0 14px 2px rgba(0, 0, 0, 0.5);
        -webkit-transform: scale(1, 1) translateX(-50%);
    }
    90% {
        box-shadow: 0 14px 2px rgba(0, 0, 0, 0.5);
        -webkit-animation-timing-function: cubic-bezier(1, 0, 1, 1);
        -webkit-transform: scale(1.1, 1) translateX(-50%);
    }
    95% {
        box-shadow: 0 14px 3px rgba(0, 0, 0, 0.5);
        -webkit-transform: scale(1, 1) translateX(-50%);
    }
    97% {
        box-shadow: 0 14px 2px rgba(0, 0, 0, 0.5);
        -webkit-animation-timing-function: cubic-bezier(1, 0, 1, 1);
        -webkit-transform: scale(1.05, 1) translateX(-50%);
    }
    100% {
        -webkit-transform: scale(1, 1) translateX(-50%);
    }
}
@keyframes diffuse-scale {
    0% {
        box-shadow: 0 14px 8px rgba(0, 0, 0, 0.5);
        -moz-animation-timing-function: cubic-bezier(1, 0, 1, 1);
        animation-timing-function: cubic-bezier(1, 0, 1, 1);
        -moz-transform: scale(1.5, 1) translateX(-50%);
        transform: scale(1.5, 1) translateX(-50%);
    }
    15% {
        box-shadow: 0 14px 2px rgba(0, 0, 0, 0.5);
        -moz-transform: scale(1, 1) translateX(-50%);
        transform: scale(1, 1) translateX(-50%);
    }
    30% {
        box-shadow: 0 14px 7px rgba(0, 0, 0, 0.5);
        -moz-animation-timing-function: cubic-bezier(1, 0, 1, 1);
        animation-timing-function: cubic-bezier(1, 0, 1, 1);
        -moz-transform: scale(1.4, 1) translateX(-50%);
        transform: scale(1.4, 1) translateX(-50%);
    }
    45% {
        box-shadow: 0 14px 2px rgba(0, 0, 0, 0.5);
        -moz-transform: scale(1, 1) translateX(-50%);
        transform: scale(1, 1) translateX(-50%);
    }
    60% {
        box-shadow: 0 14px 5px rgba(0, 0, 0, 0.5);
        -moz-animation-timing-function: cubic-bezier(1, 0, 1, 1);
        animation-timing-function: cubic-bezier(1, 0, 1, 1);
        -moz-transform: scale(1.3, 1) translateX(-50%);
        transform: scale(1.3, 1) translateX(-50%);
    }
    70% {
        box-shadow: 0 14px 2px rgba(0, 0, 0, 0.5);
        -moz-transform: scale(1, 1) translateX(-50%);
        transform: scale(1, 1) translateX(-50%);
    }
    80% {
        box-shadow: 0 14px 4px rgba(0, 0, 0, 0.5);
        -moz-animation-timing-function: cubic-bezier(1, 0, 1, 1);
        animation-timing-function: cubic-bezier(1, 0, 1, 1);
        -moz-transform: scale(1.2, 1) translateX(-50%);
        transform: scale(1.2, 1) translateX(-50%);
    }
    85% {
        box-shadow: 0 14px 2px rgba(0, 0, 0, 0.5);
        -webkit-transform: scale(1, 1) translateX(-50%);
        -moz-transform: scale(1, 1) translateX(-50%);
        transform: scale(1, 1) translateX(-50%);
    }
    90% {
        box-shadow: 0 14px 2px rgba(0, 0, 0, 0.5);
        -moz-animation-timing-function: cubic-bezier(1, 0, 1, 1);
        animation-timing-function: cubic-bezier(1, 0, 1, 1);
        -moz-transform: scale(1.1, 1) translateX(-50%);
        transform: scale(1.1, 1) translateX(-50%);
    }
    95% {
        box-shadow: 0 14px 3px rgba(0, 0, 0, 0.5);
        -moz-transform: scale(1, 1) translateX(-50%);
        transform: scale(1, 1) translateX(-50%);
    }
    97% {
        box-shadow: 0 14px 2px rgba(0, 0, 0, 0.5);
        -moz-animation-timing-function: cubic-bezier(1, 0, 1, 1);
        animation-timing-function: cubic-bezier(1, 0, 1, 1);
        -moz-transform: scale(1.05, 1) translateX(-50%);
        transform: scale(1.05, 1) translateX(-50%);
    }
    100% {
        -moz-transform: scale(1, 1) translateX(-50%);
        transform: scale(1, 1) translateX(-50%);
    }
}

@-webkit-keyframes displace {
    from {
        -webkit-animation-timing-function: linear;
        -webkit-transform: translateX(0);
    }
    to {
        -webkit-transform: translateX(100px);
    }
}
@keyframes displace {
    from {
        -moz-animation-timing-function: linear;
        animation-timing-function: linear;
        -moz-transform: translateX(0);
        transform: translateX(0);
    }
    to {
        -moz-transform: translateX(100px);
        transform: translateX(100px);
    }
}
.jump {
    -webkit-animation-name: jump;
    -moz-animation-name: jump;
    -ms-animation-name: jump;
    -o-animation-name: jump;
    animation-name: jump;
}
.diffuse-scale {
    -webkit-animation-name: diffuse-scale;
    -moz-animation-name: diffuse-scale;
    -ms-animation-name: diffuse-scale;
    -o-animation-name: diffuse-scale;
    animation-name: diffuse-scale;
}
.displace {
    -webkit-animation-name: displace;
    -moz-animation-name: displace;
    -ms-animation-name: displace;
    -o-animation-name: displace;
    animation-name: displace;
}
<div class="wrapper">
    <div class="content animated infinite displace">
        <span class="ball animated infinite jump"></span>
        <span class="ball-shadow animated infinite diffuse-scale"></span>
    </div>
</div>

Suggestion

Something like a less or SCSS with constant physical variables that are defined, or values that you can add to the function and sumule the physical behavior may even have already mixins that simulates certain behavior, I do not know something simple and only CSS.

Upvotes: 17

Views: 5747

Answers (1)

user1693593
user1693593

Reputation:

You can use CSS-only but you will spend a lot of time figuring out the numbers for the Bezier, keyframe positions, scale and so on, and on top of that: a slight change in your layout, "gravity", dimensions, distance and you have to start "all over" (for the above part at least).

CSS animations are nice, but you will gain a better result with a little JavaScript code, not to mention have a lot more flexibility if you need to change something -

  • Define a vector for the ball
  • Define a arbitrary gravity
  • Calculate the vector and bounce
  • Bind resulting values to DOM element using transforms (gives smoother result compared to position).
  • Animate using requestAnimationFrame which syncs to monitor and gives just as smooth animations as CSS animations does.

Example

This example shows the basic, does not include the shadow, but that is left as an exercise for the reader.

var div = document.querySelector("div"),
    v = {x: 2.3, y: 1},       // some vector
    pos = {x: 100, y: 20},    // some position
    g = 0.5,                  // some gravity
    absorption = 0.7,         // friction/absorption
    bottom = 150,             // floor collision
    frames = 0;               // to reset animation (for demo)

// main calculation of the animation using a particle and a vector
function calc() {
  pos.x += v.x;               // update position with vector
  pos.y += v.y;
  v.y += g;                   // update vector with gravity
  if (pos.y > bottom) {       // hit da floor, bounce
    pos.y = bottom;           // force position = max bottom
    v.y = -v.y * absorption;  // reduce power with absorption
  }
  if (pos.x < 0 || pos.x > 620) v.x = -v.x;
}

// animate
(function loop() {
  calc();
  move(div, pos);
 
  if (++frames > 220) {       // tweak, use other techniques - just to reset bounce
    frames = 0; pos.y = 20;
  }
  requestAnimationFrame(loop)
})();

function move(el, p) {
  el.style.transform = el.style.webkitTransform = "translate("+p.x+"px,"+p.y+"px)";
}
div {
  width:20px;
  height:20px;
  background:rgb(0, 135, 222);
  border-radius:50%;
  position:fixed;
}
<div></div>

If you want a more accurate bounce of the floor, you can use the diff of actual position to reflect that as well:

if (pos.y > bottom) {
    var diff = pos.y - bottom;
    pos.y = bottom - diff;
    ...

And if you need this for several element, just create an instantiate-able object which embeds a reference to the element to animate, the calculations etc.

If you now want to change direction, start point, gravity and so on, you just update the respective values and everything works smooth when replayed.

Example intermediate step to produce CSS key-frames

You can modify the code above to crunch numbers for a CSS-animation.

Use number of frames and normalize the sequence range, run the calculations by counting frames. Then extract values per, lets say every 10 frames as well as every bounce, finally format numbers as key-frames.

Ideally you will always include top and bottom position - you can detect this by monitoring the direction of the vector's y-value (the sign), not shown here.

This will work as an intermediate step to produce the CSS-rule which we will use later:

var v = {x: 2.3, y: 1},       // some vector
    pos = {x: 100, y: 20},    // some position
    g = 0.5,                  // some gravity
    absorption = 0.7,         // friction/absorption
    bottom = 150,             // floor collision
    frames = 0,               // to reset animation (for demo)
    maxFrames = 220,          // so we can normalize
    step = 10,                // grab every nth + bounce
    heights = [],             // collect in an array as step 1
    css = "";                 // build CSS animation

// calc CSS-frames
for(var i = 0; i <= maxFrames; i++) {
  var t = i / maxFrames;
  pos.x += v.x;               // update position with vector
  pos.y += v.y;
  v.y += g;                   // update vector with gravity

  if (pos.y > bottom) {
    pos.y = bottom;
    v.y = -v.y * absorption;
    heights.push({pst: t * 100, y: pos.y});
  }  
  else if (!(i % step)) {heights.push({pst: t * 100, y: pos.y})}  
}

// step 2: format height-array into CSS
css += "@keyframes demo {\n";
for(i = 0; i < heights.length; i++) {
  var e = heights[i];
  css += "  " + e.pst.toFixed(3) + "% {transform: translateY(" + e.y.toFixed(3) + "px)}\n";
}
css += "}";

document.write("<pre>" + css + "</pre>");

If we grab the result from that and uses it as CSS for our final page, we get this result (sorry, non-prefixed version only in this demo):

(You will of course have to tweak and fine-tune this, but you'll get the gist.)

div  {
  animation: demo 3s linear infinite;
  width:20px;
  height:20px;
  border-radius:50%;
  background:rgb(0, 148, 243);
  position:fixed;
  left:100px;
}

@keyframes demo {
  0.000% {transform: translateY(21.000px)}
  4.545% {transform: translateY(58.500px)}
  9.091% {transform: translateY(146.000px)}
  9.545% {transform: translateY(150.000px)}
  13.636% {transform: translateY(92.400px)}
  18.182% {transform: translateY(75.900px)}
  22.727% {transform: translateY(109.400px)}
  25.455% {transform: translateY(150.000px)}
  27.273% {transform: translateY(127.520px)}
  31.818% {transform: translateY(106.320px)}
  36.364% {transform: translateY(135.120px)}
  37.727% {transform: translateY(150.000px)}
  40.909% {transform: translateY(125.563px)}
  45.455% {transform: translateY(133.153px)}
  47.273% {transform: translateY(150.000px)}
  50.000% {transform: translateY(134.362px)}
  54.545% {transform: translateY(148.299px)}
  55.000% {transform: translateY(150.000px)}
  59.091% {transform: translateY(138.745px)}
  61.818% {transform: translateY(150.000px)}
  63.636% {transform: translateY(141.102px)}
  67.727% {transform: translateY(150.000px)}
  68.182% {transform: translateY(147.532px)}
  72.727% {transform: translateY(150.000px)}
  77.273% {transform: translateY(150.000px)}
  81.818% {transform: translateY(150.000px)}
  86.364% {transform: translateY(150.000px)}
  90.909% {transform: translateY(150.000px)}
  95.455% {transform: translateY(150.000px)}
  100.000% {transform: translateY(150.000px)}
}
<div></div>

Personally I would recommend the JavaScript support though as it is more accurate for these type of animations, and as mentioned, it can easily adopt to new requirements.

Upvotes: 18

Related Questions