<div class=bg></div>
<div class=q></div>
<div class=mario></div>
<div class=title>CodePen</div>
//
// the goal is to have a single sprite, but instead of having one giant image,
// only have the individual loops in sequence. 
// then call that loop however many times before moving to the next loop.
// that is controlled in the $settings map.

//
// some base variables
$sprite-w: 3840px;
$frame-h: 280px;
$frame-w: 240px;
$block-sprite-di: 160px;
$frame-count: $sprite-w / $frame-w;
// seconds per frame for consistent FPS
$spf: 80ms;
// max height of fly (bottom value)
$height: 80%;
// scale down elements
$scale: 0.5;


//
// the frame animation chain. this sequence requires unique keys.
// each sub map describes an animation in the chain.
// iterations: how many times to run the frame loop
// start: the start frame in the sprite
// stop: the stop frame in the sprite
// the order of the maps in this map dictate the sequence
// is converted over to new $keyframes map for this and calculated data
$settings: (
  run-1:   ( iterations:  6, start:  1, stop:  2, y: 0.0 ),
  p-run:   ( iterations:  6, start:  7, stop: 10, y: 0.0 ),
  takeoff: ( iterations:  1, start: 11, stop: 13, y: 0.0 ),
  fly:     ( iterations: 12, start: 14, stop: 16, y: 1.2 ),
  run-2:   ( iterations:  4, start:  1, stop:  2, y: 0.0 ),
  jump:    ( iterations:  2, start:  3, stop:  6, y: 0.25, block: true )
);

//
// function to quickly pull down values from our $settings map
@function setting($which, $val: null) {
  @if $val {
    @return map-get(map-get($settings, $which), $val);
  } @else {
    @return map-get($settings, $which);
  }
}

//
// function to quickly pull down values from our $keyframes map
@function keyframe($which, $val: null) {
  @if $val {
    @return map-get(map-get($keyframes, $which), $val);
  } @else {
    @return map-get($keyframes, $which);
  }
}

//
// creating the frame animation of mario.
// the value of the "animation" property for the element.
// looks like multiple animation declarations with different delays.
$frame-anims: ();
$ongoing-time: 0;
$total-steps: 0;
$block-in: 0;


//
// keyframes will be the fully compiled map we use later on 
// based on animations map settings
$keyframes: ();

// building out $keyframes
@each $anim, $vals in $settings {
  $start: map-get($vals, start);
  $stop: map-get($vals, stop);
  $iterations: map-get($vals, iterations);
  $y: map-get($vals, y);
  $speed: map-get($vals, speed);

  $block: map-get($vals, block);
  // if block, set in position to this frame
  @if $block { $block-in: $ongoing-time; }
  
  // quantity of frames for the animation
  $steps: $stop - $start + 1;
  // length of time relative to the quantity of frames for consistent fps
  $time: $steps * $spf;
  // when to start the anim
  $delay: $ongoing-time;
  // if zero, set delay to null to avoid bad css
  @if $delay == 0 { $delay: null; }
  
  // add values to keyframes
  $keyframes: map-merge($keyframes, ($anim: (
    start: $start, stop: $stop, iterations: $iterations,
    y: $y, spped: $speed, steps: $steps, time: $time, delay: $delay
  )));
  
  // increase total steps for later ref
  $s: setting($anim, iterations) * $steps;
  $total-steps: $total-steps + $s;
  // build out the animation strings, comma-separated
  $frame-anims: append($frame-anims, $anim $time steps($steps) $delay $iterations, comma);
  // increase ongoing time for use in the delay of next animation
  $ongoing-time: $ongoing-time + ($time * $iterations);
}


//
// the mario element
.mario {
  width: $frame-w;
  height: $frame-h;
  
  background-image: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/111863/mario-sprite.png);
  
  // positioning mario
  position: absolute;
  bottom: 0;
  left: 50%;
  transform-origin: 50% 100%;
  transform: translateX(-50%) translateY(-$block-sprite-di * $scale) scale($scale);
  
  animation: $frame-anims,
    y-axis $ongoing-time linear;
}


//
// create animation keyframes for each animation
@each $anim, $vals in $keyframes {
  $start: map-get($vals, start);
  $stop: map-get($vals, stop);
  @keyframes #{$anim} {
    from { background-position: ($start - 1) * -$frame-w; }
    to   { background-position: $stop * -$frame-w; }
  }
}

//
// create y axis animation
// each will always start and end at bottom
$ongoing-ratio: 0;
@keyframes y-axis {
  @each $anim, $vals in $keyframes {
    // get params
    $steps: keyframe($anim, steps);
    $iterations: keyframe($anim, iterations);
    $y: keyframe($anim, y);
    // set y to non null
    @if $y == null { $y: 0; }
    // starting % keyframe
    $start-ratio: $ongoing-ratio;
    // amount to increase by
    $stop-ratio: $steps * $iterations / $total-steps;
    // final % keyframe position
    $ongoing-ratio: $ongoing-ratio + $stop-ratio;
    // the middle % keyframe, or apex height
    $midpoint-ratio: $ongoing-ratio - ($stop-ratio / 2);
    // start: height of zero
    #{$start-ratio * 100%} { bottom: 0%; }
    #{$midpoint-ratio * 100%} { bottom: $y * $height; }
    #{$ongoing-ratio * 100%} { bottom: 0%; }
  }
}


//
// the q element
$q-loop: 6;
$q-in: $spf * 20;
$q-block-delay: 4 * $q-loop * $spf + $block-in - $q-in;
.q {
  position: absolute;
  bottom: calc(20% + #{($frame-h + $block-sprite-di) * $scale}); right: 0; left: calc(100% + #{$block-sprite-di * $scale / 2});
  width: $block-sprite-di * $scale;
  height: $block-sprite-di * $scale;
  transform: translate(-50%, 0%);
  background-image: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/111863/mario-q.png);
  // 5 in sprite, first 4 loop, last is post-bump.
  background-size: $block-sprite-di * $scale * 5 $block-sprite-di * $scale;
  animation: q-frames 4 * $spf steps(4) $block-in - $q-in $q-loop,
    q-block $spf steps(1) $q-block-delay forwards,
    q-in $q-in linear $block-in - $q-in forwards;
}

@keyframes q-frames {
  from { background-position: 0 0; }
  to   { background-position: $block-sprite-di * $scale * -4 0; }
}
@keyframes q-block {
  from { background-position: 0 0; }
  to   { background-position: $block-sprite-di * $scale * -4 0; }
}
@keyframes q-in {
  from { left: calc(100% + #{$block-sprite-di * $scale / 2}); }
  to   { left: 50%; }
}



//
// the background element
.bg {
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  background: #0076ba;
  background-image: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/111863/mario-ground.png);
  background-size: $block-sprite-di * $scale $block-sprite-di * $scale;
  background-repeat: repeat-x;
  background-position: 0 bottom;
  animation: ground $block-in linear forwards;
}

$ground-frames: $total-steps / 4;
$ground-decrement: -$block-sprite-di * $scale;
@keyframes ground {
  from { background-position: 0 bottom; }
  to   { background-position: $ground-decrement * $ground-frames bottom; }
}



@import url(https://fonts.googleapis.com/css?family=Press+Start+2P);

//
// the title at the end
.title {
  position: absolute;
  font-family: 'Press Start 2P', cursive;
  color: white;
  font-size: 4rem;
  text-align: center;
  bottom: calc(20% + #{($frame-h + $block-sprite-di) * $scale});
  width: 100%;
  text-shadow: 4px 4px 0px rgba(#000, 0.2);
  transform: scale(0);
  transform-origin: 50% 100%;
  animation: title 500ms steps(4) infinite,
    title-in 500ms steps(10) $q-block-delay forwards;
}

@keyframes title {
  0%, 100% { color: white; }
  50% { color: #CCC; }
}

@keyframes title-in {
  from { transform: scale(0); bottom: calc(30% + #{($frame-h + $block-sprite-di) * $scale}); }
  to   { transform: scale(1); bottom: calc(40% + #{($frame-h + $block-sprite-di) * $scale}); }
}
View Compiled
jakealbaughSignature('light');

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://codepen.io/jakealbaugh/pen/WvNjZB.js