@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;
}