@use 'sass:map'; @use 'sass:math'; @function roundTo($num, $places: 2) { $factor: 100 * $places; @return math.round($num * $factor) / $factor; } @mixin bounce( $acceleration: null, $bounciness: .5, $direction: "bottom", $distance: 100, $name: bounce, $timeFrom: 0, $timeTotal: null, ) { @if $bounciness >= 1 or $bounciness < 0 { @error "$bounciness must be < 1 and > 0."; } @if ($acceleration == null) { $acceleration: 28.9 - (4.8 * math.log($distance)); // Used trial and error to figure out what looks good at different distances } $acceleration: $distance * $acceleration; $distanceMin: ($distance / 100); $steps: ( ( "elapsed": 0, "to": 0, "direction": "down", ), ); $step: 0; $distanceThisBounce: $distance; $timeThisBounce: 1; @while $timeThisBounce >= .01 and $distanceThisBounce > 0 { $timeThisBounce: math.sqrt((2 * $distanceThisBounce) / $acceleration); $step: $step + $timeThisBounce; $steps: append($steps, ( "elapsed": $step, "to": $distanceThisBounce, "direction": up, )); $step: $step + $timeThisBounce; $steps: append($steps, ( "elapsed": $step, "to": 0, "direction": down, )); $distanceThisBounce: $distanceThisBounce * $bounciness; $stepPrevious: $step; } $duration: $step; @if $timeTotal == null { $timeTotal: $duration; } @if ($duration + $timeFrom) > $timeTotal { @error "Animation duration of #{$timeTotal} is less than required bounce duration of #{$duration + $timeFrom}."; } @keyframes #{$name} { 0% { #{$direction}: 0; } @each $step in $steps { $elapsed: map.get($step, "elapsed"); $elapsed: (($elapsed + $timeFrom) / $timeTotal); $moveTo: map.get($step, "to") / 100; #{roundTo(percentage($elapsed), 2)} { #{$direction}: roundTo(percentage($moveTo), 2); @if map.get($step, "direction") == up { animation-timing-function: ease-in; } @else { animation-timing-function: ease-out; } } } 100% { #{$direction}: 0; } } .animation-#{$name} { animation: $name #{$timeTotal}s linear infinite both; } } @include bounce( $direction: "left", $distance: 100, $timeFrom: 1, $timeTotal: 5, ); .box { bottom: 0; height: 500px; outline: 1px solid black; position: absolute; width: 100px; } .ball { background: #f00; border-radius: 25px; bottom: 0; height: 50px; left: 25px; position: absolute; width: 50px; }