Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URLs added here will be added as <link>s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Auto Save

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <!-- Instructions. -->

<h1> Overflow-based masking demo </h1>
<div class="instructions">
  <label>
    <input type="checkbox" onchange="OverflowDemo.showInstructionsChanged()">
    Show Instructions
  </label>
  <div id="instructionsContent" class="hidden">
    <ul>
      <li>
        <b>Mask Shape:</b> The shape of the mask. Rectangle and Circle both use overflow: hidden, and a star-shaped mask can be set using clip-path. If no mask is set, then the DOM hierarchy is not changed, and in particular, mask animation (and counter-animation) is still generated.
      </li>
      <li>
        <b>Mask Size:</b> Use either a smaller or larger mask of the selected shape.
      </li>
      <li>
        <b>Maskee:</b> The content within the mask.
      </li>
      <li>
        <b>Animation Easing:</b> Whether successive keyframes are interpolated with linear easing or an ease-in/ease-out curve.
      </li>
      <li>
        <b>Animated Values:</b> Which styles, if any, are animated.
        <ul>
          <li>
            None: No animation is generated.
          </li>
          <li>
            Transform: Transforms are animated.
          </li>
          <li>
            Left/Top/Width/Height: Animation is applied to left, top, width, and/or height, and rotation animation is suppressed.
          </li>
        </ul>
        If you change other settings and the animation suddenly looks crazily wrong, try setting this to None and then back to the styles of interest, since this will force the animation to be regenerated.
      </li>
      <li>
        <b>Show Mask Outline:</b> If checked, the outline of the mask element is drawn. If clip-path is used, this is not equivalent to the mask shape.
      </li>
      <li>
        <b>Translate, Rotate, and Scale:</b> Select which transform channels are animated. To see the animation, Animated Values must not be None.
      </li>
      <li>
        <b>Use Small Amplitude Animation:</b> If selected, the numeric distance between each transform channel's keys is reduced.
      </li>
    </ul>
  </div>
</div>

<br>

<!-- Demo settings. -->
<div class="control-block">
  <div>
    <span class="setting"> Mask Shape: </span>
    <span>
      <select id="maskShape" name="maskShape" onchange="OverflowDemo.maskShapeChanged()">
        <option value="none" autocomplete="off"> None </option>
        <option value="rectangle" autocomplete="off" selected> Rectangle</option>
        <option value="circle" autocomplete="off"> Circle (using border-radius) </option>
        <option value="starClip" autocomplete="off"> Star (using clip-path) </option>
      </select>
    </span>
  </div>
  <div>
    <span class="setting"> Mask Size: </span>
    <span>
      <select id="maskSize" name="maskSize" onchange="OverflowDemo.maskSizeChanged()">
        <option value="small" autocomplete="off"> Small </option>
        <option value="large" autocomplete="off" selected> Large </option>
      </select>
    </span>
  </div>
  <div>
    <span class="setting"> Maskee: </span>
    <span>
      <select id="maskee" name="maskee" onchange="OverflowDemo.maskeeChanged()">
        <option value="car" autocomplete="off" selected> Car </option>
        <option value="car-animated" autocomplete="off"> Car (Animated) </option>
        <option value="text" autocomplete="off"> Text </option>
      </select>
    </span>
  </div>
  <div>
    <span class="setting"> Animation Easing: </span>
    <span>
      <select id="easing" name="easing" onchange="OverflowDemo.easingChanged()">
        <option value="linear" autocomplete="off" selected> Linear </option>
        <option value="ease-in-out" autocomplete="off"> Ease In and Out </option>
      </select>
    </span>
  </div>
  <div>
    <span class="setting"> Animated Values: </span>
    <span>
      <select id="animatedValues" name="animatedValues"
              onchange="OverflowDemo.animatedValuesChanged()">
        <option value="none" autocomplete="off"> None </option>
        <option value="transform" selected autocomplete="off"> Transform </option>
        <option value="ltwh" autocomplete="off"> Left/Top/Width/Height </option>
      </select>
    </span>
  </div>
</div>

<div class="control-block">
  <div>
    <label>
      <input id="showMaskOutlineToggle" type="checkbox" onchange="OverflowDemo.maskOutlineChanged()">
      Show Mask Outline
    </label>
  </div>

  <div>
    <label>
      <input id="translateXToggle" type="checkbox" onchange="OverflowDemo.animChanged()">
      Translate X
    </label>
  </div>

  <div>
    <label>
      <input id="translateYToggle" type="checkbox" onchange="OverflowDemo.animChanged()">
      Translate Y
    </label>
  </div>

  <div>
    <label>
      <input id="rotateToggle" type="checkbox" onchange="OverflowDemo.animChanged()">
      Rotate Z
    </label>
  </div>

  <div>
   <label>
      <input id="scaleXToggle" type="checkbox" checked onchange="OverflowDemo.animChanged()">
      Scale X
    </label>
  </div>

  <div>
    <label>
      <input id="scaleYToggle" type="checkbox" onchange="OverflowDemo.animChanged()">
      Scale Y
    </label>
  </div>

  <div>
    <label>
      <input id="animateSmallToggle" type="checkbox" onchange="OverflowDemo.animChanged()">
      Use Small Amplitude Animation
    </label>
  </div>
</div>

<!-- Mask and maskee. -->
<div class="container">
  <div id="Mask" class="mask large mask-active">
    <div id="MaskInverseScaleX" class="gwd-mask">
      <div id="MaskInverseScaleY" class="gwd-mask">
        <div id="MaskContent" class="gwd-mask">
          <img data-content-type="car" class="content selected" src="https://services.google.com/fh/files/misc/color_grey.jpg">
          <img data-content-type="car-animated" class="content car-anim" src="https://services.google.com/fh/files/misc/color_grey.jpg">
          <div data-content-type="text" class="content">
            <div class="text-background">
              <div class="text"> Example Text </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>

<!-- Reference content -->
  <div id="MaskOutline" class="mask"></div>
  <div class="reference">
    <img data-content-type="car" class="content selected" src="https://services.google.com/fh/files/misc/color_grey.jpg">
    <img data-content-type="car-animated" class="content car-anim" src="https://services.google.com/fh/files/misc/color_grey.jpg">
    <div data-content-type="text" class="content">
      <div class="text-background">
        <div class="text"> Example Text </div>
      </div>
    </div>
  </div>
</div>

<!-- Inlined SVG for use with clip-path. -->
<svg width="0" height="0">
  <defs>
    <clipPath id="clipPathStar">
      <polygon points="100,10 40,198 190,78 10,78 160,198"/>
    </clipPath>
    <clipPath id="clipPathStarLarge">
      <polygon points="150,15 60,297 285,117 15,117 240,297"/>
    </clipPath>
  </defs>
</svg>

              
            
!

CSS

              
                /** Demo Settings */
body {
  font-family: sans-serif;
}

.instructions {
  font-size: 14px;
  width: 800px;
}

.hidden {
  display: none
}

.setting {
  padding: 0 5px 10px 20px;
  width: 130px;
  display: inline-block;
}

.control-block {
  display: inline-block;
  padding-right: 50px;
  padding-bottom: 20px;
  vertical-align: top;
}


/** Mask styling. */
.container {
  position: absolute;
  transform-style: preserve-3d;
  transform: perspective(1400px);
}

.container img {
  width: 400px;
  top: -50px;
}

.mask {
  position: absolute;
  width: 200px;
  height: 200px;
  transform-style: preserve-3d;
}

.mask.outline {
  border: 1px solid black;
}

.mask.large {
  width: 300px;
  height: 300px;
}

.mask-active {
  overflow: hidden;
}

.mask.circle {
  border-radius: 300px;
}

.gwd-mask {
  transform-style: preserve-3d;
  position: absolute;
  width: 100%;
  height: 100%;
}


/** Maskee and reference content styling. */
.content {
  position: absolute;
  transform-style: preserve-3d;
  display: none;
}

.reference {
  position: absolute;
  left: 500px;
}

.star-clip-path {
  -webkit-clip-path: url(#clipPathStar);
  clip-path: url(#clipPathStar);
}

.star-clip-path.large {
  -webkit-clip-path: url(#clipPathStarLarge);
  clip-path: url(#clipPathStarLarge);
}

.content.selected {
  display: block;
}

.text-background {
  position: absolute;
  width: 340px;
  height: 280px;
  background-color: #c4c4c4;
}

.text {
  position: absolute;
  left: 50px;
  top: 100px;
  font-size: 40px;
}

.mask-anim {
  animation: mask_keys 4s linear 0s infinite normal forwards;
  -moz-animation: mask_keys 4s linear 0s infinite normal forwards;
  -webkit-animation: mask_keys 4s linear 0s infinite normal forwards;
}

.mask-content-anim {
  animation: mask_content_keys 4s linear 0s infinite normal forwards;
  -moz-animation: mask_content_keys 4s linear 0s infinite normal forwards;
  -webkit-animation: mask_content_keys 4s linear 0s infinite normal forwards;
}

.mask-inverse-scale-x-anim {
  animation: mask_inverse_scale_x_keys 4s linear 0s infinite normal forwards;
  -moz-animation: mask_inverse_scale_x_keys 4s linear 0s infinite normal forwards;
  -webkit-animation: mask_inverse_scale_x_keys 4s linear 0s infinite normal forwards;
}

.mask-inverse-scale-y-anim {
  animation: mask_inverse_scale_y_keys 4s linear 0s infinite normal forwards;
  -moz-animation: mask_inverse_scale_y_keys 4s linear 0s infinite normal forwards;
  -webkit-animation: mask_inverse_scale_y_keys 4s linear 0s infinite normal forwards;
}

.car-anim {
  animation: car_keys 3s linear 0s infinite normal forwards;
  -moz-animation: car_keys 3s linear 0s infinite normal forwards;
  -webkit--animation: car_keys 3s linear 0s infinite normal forwards;
}

@keyframes car_keys {
  0% {
    transform: rotateY(-30deg) translate3d(-50px, 0px, 0px);
  }
  50% {
    transform: rotateY(30deg) translate3d(50px, 0px, 0px);
  }
  100% {
    transform: rotateY(-30deg) translate3d(-50px, 0px, 0px);
  }
}

@-moz-keyframes car_keys {
  0% {
    -moz-transform: rotateY(-30deg) translate3d(-50px, 0px, 0px);
  }
  50% {
    -moz-transform: rotateY(30deg) translate3d(50px, 0px, 0px);
  }
  100% {
    -moz-transform: rotateY(-30deg) translate3d(-50px, 0px, 0px);
  }
}

@-webkit-keyframes car_keys {
  0% {
    -moz-transform: rotateY(-30deg) translate3d(-50px, 0px, 0px);
  }
  50% {
    -moz-transform: rotateY(30deg) translate3d(50px, 0px, 0px);
  }
  100% {
    -moz-transform: rotateY(-30deg) translate3d(-50px, 0px, 0px);
  }
}

              
            
!

JS

              
                window.OverflowDemo = {};

document.addEventListener('DOMContentLoaded', function initialize() {
  document.removeEventListener('DOMContentLoaded', initialize, false);
  OverflowDemo.animChanged();
}, false);


/** @const {number} Minimum allows value for a scale channel */
OverflowDemo.MIN_SCALE = 0.01;


/**
 * Maximum max width, used to compute the error of an inverse scale
 * approximation.
 * @const {number}
 */
OverflowDemo.MAX_MASK_WIDTH = 500;


/** @const {number} Width of the smaller mask element. */
OverflowDemo.MASK_WIDTH = 200;


/** @const {number} Width of the larger mask element. */
OverflowDemo.MASK_WIDTH_LARGE = 300;


/** @const {number} Translation amount for use in animation. */
OverflowDemo.ANIM_TRANSLATE = 100;


/** @const {number} Smaller translation amount for use in animation. */
OverflowDemo.ANIM_TRANSLATE_SMALL = 10;


/** @const {number} Rotation amount for use in animation. */
OverflowDemo.ANIM_ROTATE_Z_DEG = 90;


/** @const {number} Smaller rotation amount for use in animation. */
OverflowDemo.ANIM_ROTATE_Z_DEG_SMALL = 10;


/** @const {number} Scale amount for use in animation. */
OverflowDemo.ANIM_SCALE = OverflowDemo.MIN_SCALE;


/** @const {number} More modest scaling for use in animation. */
OverflowDemo.ANIM_SCALE_SMALL = 0.9;


/** @type {string} The type of easing to use when animating the mask. */
OverflowDemo.EASING = 'linear';


/**
 * Which values, if any, are animated. Possible values are none, transform, and
 * ltwh (for left, top, width, and height).
 * @type {string}
 */
OverflowDemo.ANIMATED_VALUES = 'transform';


/**
 * Translation, rotation, and scale channels for a 3D transform.
 * @typedef {{
 *   translate: !Array<number>,
 *   rotate: !Array<number>,
 *   scale: !Array<number>
 * }}
 */
OverflowDemo.Transform_;


/**
 * Key for a rotate, translate, or scale channel.
 *   - time: A number between 0 and 1 indicating the time of the key as a
 *     fraction of the animation's duration.
 *   - value: The channel's value.
 *   - easing: The easing curve between this key and the next.
 * @typedef {{
 *   time: number,
 *   value: number,
 *   easing: !OverflowDemo.EasingCurve
 * }}
 */
OverflowDemo.ChannelKey_;


/**
 * An inverse scale animation.
 * @typedef {{
 *   xAnim: !Array<!OverflowDemo.ChannelKey_>,
 *   yAnim: !Array<!OverflowDemo.ChannelKey_>
 * }}
 */
OverflowDemo.InverseScaleAnim_;


/**
 * The base transform for the mask.
 * @private {!OverflowDemo.Transform_}
 */
OverflowDemo.MASK_TRANSFORM_ = {
  translate: [0, 0, 0],
  rotate: [0, 0, 0],
  scale: [1, 1, 1]
};


/**
 * The second key in the mask's animation. There are three keys total, and the
 * first and third keys are always MASK_TRANSFORM_.
 * @private {!OverflowDemo.Transform_}
 */
OverflowDemo.MASK_KEY_ = {
  translate: [0, 0, 0],
  rotate: [0, 0, 0],
  scale: [1, 1, 1]
};


/**
 * Linearly interpolates two values, or two arrays of values.
 * @param {number|!Array<number>} a The first value/array.
 * @param {number|!Array<number>} b The second value/array.
 * @param {number} w The interpolation weight.
 * @return {number|!Array<number>} The interpolated value/array.
 * @private
 */
OverflowDemo.lerp_ = function(a, b, w) {
  if (Array.isArray(a)) {
    var result = [];
    for (var i = 0; i < a.length; i++) {
      result.push(a[i] * (1 - w) + b[i] * w);
    }
    return result;
  } else {
    return a * (1 - w) + b * w;
  }
};


/**
 * Callback invoked when the mask's shape is set.
 */
OverflowDemo.maskShapeChanged = function() {
  var maskElem = document.getElementById('Mask');
  var maskOutlineElem = document.getElementById('MaskOutline');
  var shape = document.getElementById('maskShape').value;

  maskElem.classList.remove('mask-active');
  maskElem.classList.remove('circle');
  maskOutlineElem.classList.remove('circle');
  maskElem.classList.remove('star-clip-path');

  if (shape == 'rectangle') {
    maskElem.classList.add('mask-active');
  } else if (shape == 'circle') {
    maskElem.classList.add('mask-active');
    maskElem.classList.add('circle');
    maskOutlineElem.classList.add('circle');
  } else if (shape == 'starClip') {
    maskElem.classList.add('mask-active');
    maskElem.classList.add('star-clip-path');
  }
};


/**
 * Callback invoked when the mask's size is set.
 */
OverflowDemo.maskSizeChanged = function() {
  var maskElem = document.getElementById('Mask');
  var maskOutlineElem = document.getElementById('MaskOutline');
  var maskSize = document.getElementById('maskSize').value;
  if (maskSize == 'small') {
    maskElem.classList.remove('large');
    maskOutlineElem.classList.remove('large');
  } else if (maskSize == 'large') {
    maskElem.classList.add('large');
    maskOutlineElem.classList.add('large');
  }
};


/**
 * Callback invoked when an animation control is altered.
 */
OverflowDemo.animChanged = function() {
  var hasAnimation =
      document.getElementById('translateXToggle').checked ||
      document.getElementById('translateYToggle').checked ||
      document.getElementById('rotateToggle').checked ||
      document.getElementById('scaleXToggle').checked ||
      document.getElementById('scaleYToggle').checked;

  if (hasAnimation && OverflowDemo.ANIMATED_VALUES != 'none') {
    OverflowDemo.updateAnimKey_();
    OverflowDemo.setAnimation_();
  } else {
    OverflowDemo.clearAnimation_();
  }
};


/**
 * Callback invoked when the selected maskee changes.
 */
OverflowDemo.maskeeChanged = function() {
  var elems = document.querySelectorAll('.content');
  var maskee = document.getElementById('maskee').value;
  for (var i = 0, len = elems.length; i < len; i++) {
    if (elems[i].getAttribute('data-content-type') == maskee) {
      elems[i].classList.add('selected');
    } else {
      elems[i].classList.remove('selected');
    }
  }
};


/**
 * Callback invoked when the selected mask easing type changes.
 */
OverflowDemo.easingChanged = function() {
  OverflowDemo.EASING = document.getElementById('easing').value;
  OverflowDemo.animChanged();
};


/**
 * Callback invoked when the visibility of the mask outline changes.
 */
OverflowDemo.maskOutlineChanged = function() {
  var outlineElem = document.getElementById('MaskOutline');
  var isVisible = document.getElementById('showMaskOutlineToggle').checked;
  if (isVisible) {
    outlineElem.classList.add('outline');
  } else {
    outlineElem.classList.remove('outline');
  }
};


/**
 * Callback invoked when the type of animated values (transform versus
 * left/top/width/height) changes.
 */
OverflowDemo.animatedValuesChanged = function() {
  OverflowDemo.ANIMATED_VALUES =
      document.getElementById('animatedValues').value;
  OverflowDemo.animChanged();
};


/**
 * Callback invoked when the demo instructions are shown or hidden.
 */
OverflowDemo.showInstructionsChanged = function() {
  document.getElementById('instructionsContent').classList.toggle('hidden');
};


/**
 * Sets the value of the second animation key based on the animation controls.
 * @private
 */
OverflowDemo.updateAnimKey_ = function() {
  var useSmallAmplitudeAnimation =
      document.getElementById('animateSmallToggle').checked;

  if (document.getElementById('translateXToggle').checked) {
    if (useSmallAmplitudeAnimation) {
      OverflowDemo.MASK_KEY_.translate[0] = OverflowDemo.ANIM_TRANSLATE_SMALL;
    } else {
      OverflowDemo.MASK_KEY_.translate[0] = OverflowDemo.ANIM_TRANSLATE;
    }
  } else {
    OverflowDemo.MASK_KEY_.translate[0] = 0;
  }

  if (document.getElementById('translateYToggle').checked) {
    if (useSmallAmplitudeAnimation) {
      OverflowDemo.MASK_KEY_.translate[1] = OverflowDemo.ANIM_TRANSLATE_SMALL;
    } else {
      OverflowDemo.MASK_KEY_.translate[1] = OverflowDemo.ANIM_TRANSLATE;
    }
  } else {
    OverflowDemo.MASK_KEY_.translate[1] = 0;
  }

  if (document.getElementById('rotateToggle').checked) {
    if (useSmallAmplitudeAnimation) {
      OverflowDemo.MASK_KEY_.rotate[2] = OverflowDemo.ANIM_ROTATE_Z_DEG_SMALL;
    } else {
      OverflowDemo.MASK_KEY_.rotate[2] = OverflowDemo.ANIM_ROTATE_Z_DEG;
    }
  } else {
    OverflowDemo.MASK_KEY_.rotate[2] = 0;
  }

  if (document.getElementById('scaleXToggle').checked) {
    if (useSmallAmplitudeAnimation) {
      OverflowDemo.MASK_KEY_.scale[0] = OverflowDemo.ANIM_SCALE_SMALL;
    } else {
      OverflowDemo.MASK_KEY_.scale[0] = OverflowDemo.ANIM_SCALE;
    }
  } else {
    OverflowDemo.MASK_KEY_.scale[0] = 1;
  }

  if (document.getElementById('scaleYToggle').checked) {
    if (useSmallAmplitudeAnimation) {
      OverflowDemo.MASK_KEY_.scale[1] = OverflowDemo.ANIM_SCALE_SMALL;
    } else {
      OverflowDemo.MASK_KEY_.scale[1] = OverflowDemo.ANIM_SCALE;
    }
  } else {
    OverflowDemo.MASK_KEY_.scale[1] = 1;
  }
};


/**
 * Updates and applies the keyframes rules for the mask and the mask content
 * container.
 * @private
 */
OverflowDemo.setAnimation_ = function() {
  var MASK_TRANSFORM_ = OverflowDemo.MASK_TRANSFORM_;
  var MASK_KEY_ = OverflowDemo.MASK_KEY_;
  var styleSheet = document.styleSheets[0];

  OverflowDemo.clearAnimation_();

  var easing = 'animation-timing-function: ' + OverflowDemo.EASING + '; ';

  if (OverflowDemo.ANIMATED_VALUES == 'transform') {
    // Animate transform values.
    var maskAnimText = '@keyframes mask_keys {' +
        '0% { transform: ' +
        OverflowDemo.getTransformString_(MASK_TRANSFORM_) + '; '+ easing +
        '} 50% { transform: ' +
        OverflowDemo.getTransformString_(MASK_KEY_) + '; ' + easing + '} ' +
        '100% { transform: ' +
        OverflowDemo.getTransformString_(MASK_TRANSFORM_) + '; '+ easing + '}}';
    styleSheet.insertRule(maskAnimText, styleSheet.cssRules.length);

    var maskContentAnimText = OverflowDemo.getInverseRigidAnimation_([
      MASK_TRANSFORM_, MASK_KEY_, MASK_TRANSFORM_
    ]);
    styleSheet.insertRule(maskContentAnimText, styleSheet.cssRules.length);

    var maskInverseScaleAnimText = OverflowDemo.getInverseScaleAnimation_([
      MASK_TRANSFORM_, MASK_KEY_, MASK_TRANSFORM_
    ]);
    for (var i = 0; i < maskInverseScaleAnimText.length; i++) {
      styleSheet.insertRule(maskInverseScaleAnimText[i],
          styleSheet.cssRules.length);
    }
  } else if (OverflowDemo.ANIMATED_VALUES == 'ltwh') {
    // Animate left, top, width, and height.
    var values = [MASK_TRANSFORM_, MASK_KEY_, MASK_TRANSFORM_];
    var times = ['0%', '50%', '100%'];
    var hasLeft = MASK_KEY_.translate[0] != 0;
    var hasTop = MASK_KEY_.translate[1] != 0;
    var hasWidth = MASK_KEY_.scale[0] != 1;
    var hasHeight = MASK_KEY_.scale[1] != 1;

    var maskSize = OverflowDemo.MASK_WIDTH;
    if (document.getElementById('Mask').classList.contains('large')) {
      var maskSize = OverflowDemo.MASK_WIDTH_LARGE;
    };

    var maskAnimText = '@keyframes mask_keys {';
    var maskInverseAnimText = '@keyframes mask_content_keys {'
    for (var i = 0; i < values.length; i++) {
      var val = values[i];
      maskAnimText += times[i] + ' {\n';
      maskInverseAnimText += times[i] + ' {\n';
      if (hasLeft) {
        maskAnimText += 'left: ' + val.translate[0] + 'px;\n'
        maskInverseAnimText += 'left: ' + -val.translate[0] + 'px;\n'
      }
      if (hasTop) {
        maskAnimText += 'top: ' + val.translate[1] + 'px;\n'
        maskInverseAnimText += 'top: ' + -val.translate[1] + 'px;\n'
      }
      if (hasWidth) {
        maskAnimText += 'width: ' + val.scale[0] * maskSize + 'px;\n'
        maskInverseAnimText += 'width: ' + val.scale[0] * maskSize + 'px;\n'
      }
      if (hasHeight) {
        maskAnimText += 'height: ' + val.scale[1] * maskSize + 'px;\n'
        maskInverseAnimText += 'height: ' + val.scale[1] * maskSize + 'px;\n'
      }
      maskAnimText += easing + '}';
      maskInverseAnimText += easing + '}';
    }
    maskAnimText += '\n}';
    maskInverseAnimText += '\n}';
    styleSheet.insertRule(maskAnimText, styleSheet.cssRules.length);
    styleSheet.insertRule(maskInverseAnimText, styleSheet.cssRules.length);
  }

  // Apply classes that enable mask animation and counter-animation.
  document.getElementById('Mask').classList.add('mask-anim');
  document.getElementById('MaskOutline').classList.add('mask-anim');
  document.getElementById('MaskInverseScaleX').classList.add(
      'mask-inverse-scale-x-anim');
  document.getElementById('MaskInverseScaleY').classList.add(
      'mask-inverse-scale-y-anim');
  document.getElementById('MaskContent').classList.add('mask-content-anim');
};


/**
 * Unapplies the keyframes rules for the mask and the mask content container.
 * @private
 */
OverflowDemo.clearAnimation_ = function() {
  // Delete all keyframes rules affecting the mask.
  OverflowDemo.deleteKeyframesRuleWithName_('mask_keys');
  OverflowDemo.deleteKeyframesRuleWithName_('mask_content_keys');
  OverflowDemo.deleteKeyframesRuleWithName_('mask_inverse_scale_x_keys');
  OverflowDemo.deleteKeyframesRuleWithName_('mask_inverse_scale_y_keys');

  // Remove classes that enable mask animation and counter-animation.
  document.getElementById('Mask').classList.remove('mask-anim');
  document.getElementById('MaskOutline').classList.remove('mask-anim');
  document.getElementById('MaskInverseScaleX').classList.remove(
      'mask-inverse-scale-x-anim');
  document.getElementById('MaskInverseScaleY').classList.remove(
      'mask-inverse-scale-y-anim');
  document.getElementById('MaskContent').classList.remove('mask-content-anim');
};


/**
 * Returns the CSS keyframes text for the inverse of the rigid component of the
 * animation defined by a list of transform keys. The keys are assumed to be
 * uniformly spaced in time.
 * @param {!Array<!OverflowDemo.Transform_>} keys The transform keys.
 * @return {string} The keyframes rule text for the inverse animation.
 */
OverflowDemo.getInverseRigidAnimation_ = function(keys) {
  var result = '@keyframes mask_content_keys {';
  var easing = 'animation-timing-function: ' + OverflowDemo.EASING + '; ';
  for (var i = 0; i < keys.length; i++) {
    var t = keys[i].translate;
    var r = keys[i].rotate;
    var time = i / (keys.length - 1);
      result += (time * 100) + '% { transform: ' +
          'rotateZ(' + -r[2] + 'deg) ' +
          'rotateY(' + -r[1] + 'deg) ' +
          'rotateX(' + -r[0] + 'deg) ' +
          'translate3d(' + (-t[0]) + 'px, ' + (-t[1]) + 'px, ' + (-t[2]) +
          'px);\n ' + easing + '}\n';
  }
  result += '}';
  return result;
};


/**
 * Given an animation defined by a list of transform keys, returns the CSS
 * keyframes text for the inverse of the scaling component of the animation. The
 * keys are assumed to be uniformly spaced in time.
 * @param {!Array<!OverflowDemo.Transform_>} keys The transform keys.
 * @return {!Array<string>} The keyframes rule text for the inverse animations.
 * @private
 */
OverflowDemo.getInverseScaleAnimation_ = function(keys) {
  var xKeys = [];
  var yKeys = [];

  var easing;
  if (OverflowDemo.EASING == 'linear') {
    easing = new OverflowDemo.EasingCurve(0, 0, 1, 1);
  } else if (OverflowDemo.EASING == 'ease-in-out') {
    easing = new OverflowDemo.EasingCurve(0.42, 0, 0.58, 1);
  }

  // Gather the keys for the inverse scaling animation for each interval.
  for (var i = 1; i < keys.length; i++) {
    var startTime = (i - 1) / (keys.length - 1);
    var endTime = i / (keys.length - 1);
    var inverseAnim = OverflowDemo.getInverseScaleKeysNonUniformLinear_(
        startTime, keys[i-1].scale, endTime, keys[i].scale, easing);
    for (var j = 0; j < inverseAnim.xAnim.length; j++) {
      xKeys.push(inverseAnim.xAnim[j]);
    }
    for (var j = 0; j < inverseAnim.yAnim.length; j++) {
      yKeys.push(inverseAnim.yAnim[j]);
    }
  }

  // Insert the initial keyframes for the x and y channels of the inverse
  // scaling animation.
  xKeys.push({
    time: 1,
    value: OverflowDemo.inverseScale_(keys[keys.length - 1].scale[0]),
    easing: new OverflowDemo.EasingCurve(0, 0, 1, 1)
  });
  yKeys.push({
    time: 1,
    value: OverflowDemo.inverseScale_(keys[keys.length - 1].scale[1]),
    easing: new OverflowDemo.EasingCurve(0, 0, 1, 1)
  });

  var inverseScaleAnim = /** @type {!OverflowDemo.InverseScaleAnim_} */ ({
    xAnim: xKeys,
    yAnim: yKeys
  });

  // Convert the keyframe lists to CSS.
  var resultX = '@keyframes mask_inverse_scale_x_keys {';
  for (var i = 0; i < xKeys.length; i++) {
    resultX += (xKeys[i].time * 100) + '% { transform: scale3d(' +
        xKeys[i].value + ', 1, 1);\n' + 'animation-timing-function: ' +
        xKeys[i].easing.getCssText() + ';\n}\n';
  }
  resultX += '}';

  var resultY = '@keyframes mask_inverse_scale_y_keys {';
  for (var i = 0; i < yKeys.length; i++) {
    resultY += (yKeys[i].time * 100) + '% { transform: scale3d(1, ' +
      yKeys[i].value + ', 1);\n' + 'animation-timing-function: ' +
      yKeys[i].easing.getCssText() + ';\n}\n';
  }
  resultY += '}';

  return [resultX, resultY];
};


/**
 * A node in a tree representing hierarchical subdivision of an animation curve.
 * @typedef {{
 *   startTime: number,
 *   startValue: number,
 *   endTime: number,
 *   endValue: number
 *   easing: ?OverflowDemo.EasingCurve,
 *   children: !Array<!OverflowDemo.AnimNode_>
 * }}
 * @private
 */
OverflowDemo.AnimNode_;


/**
 * Given a scale animation defined by two keys, returns a list of keys defining
 * the inverse scale animation, which spans the same time range. The keys are
 * nonuniformly spaced in time and connected with linear eases.
 * @param {number} t0 The time of the first scale key.
 * @param {!Array<number>} s0 The value (a 3-vector) of the first scale key.
 * @param {number} t1 The time of the second scale key.
 * @param {!Array<number>} s1 The value (a 3-vector) of the second scale key.
 * @param {!OverflowDemo.EasingCurve} easing The easing curve between the keys.
 * @return {!OverflowDemo.InverseScaleAnim_} The inverse scale animation.
 * @private
 */
OverflowDemo.getInverseScaleKeysNonUniformLinear_ =
    function(t0, s0, t1, s1, easing) {
  var xNode = /** @type {!OverflowDemo.AnimNode_} */ ({
    startTime: t0,
    startValue: s0[0],
    endTime: t1,
    endValue: s1[0],
    easing: easing,
    children: []
  });

  var yNode = /** @type {!OverflowDemo.AnimNode_} */ ({
    startTime: t0,
    startValue: s0[1],
    endTime: t1,
    endValue: s1[1],
    easing: easing,
    children: []
  });

  OverflowDemo.subdivideScaleInterval_(xNode, easing);
  OverflowDemo.subdivideScaleInterval_(yNode, easing);

  var xKeys = [];
  var yKeys = [];
  OverflowDemo.gatherNonUniformScaleKeys_(xNode, xKeys);
  OverflowDemo.gatherNonUniformScaleKeys_(yNode, yKeys);

  return /** @type {!OverflowDemo.InverseScaleAnim_} */ ({
    xAnim: xKeys,
    yAnim: yKeys
  });
};


/**
 * If necessary, subdivides an interval of a scale animation.
 * @param {!OverflowDemo.AnimNode_} node The node representing the animation
 *     interval. If subdivision occurs, two children are added to the node and
 *     recursively subdivided.
 * @private
 */
OverflowDemo.subdivideScaleInterval_ = function(node) {
  var SAMPLE_SPACING = 0.005;
  var ERROR_TOL = 0.5;

  // Don't subdivide if the interval is already very small.
  var timeSpan = node.endTime - node.startTime;
  if (timeSpan <= SAMPLE_SPACING) {
    return;
  }

  // Get the inverse scale at the boundaries of the interval.
  var invStartScale = OverflowDemo.inverseScale_(node.startValue);
  var invEndScale = OverflowDemo.inverseScale_(node.endValue);

  // Sample the interval to see if there are any points where lerping the
  // inverse scale is insufficiently close to the inverse of the lerped scale.
  var numSamples = Math.floor(timeSpan / SAMPLE_SPACING);
  var needsSubdivision = false;
  for (var i = 0; i < numSamples; i++) {
    var timeFraction = (i + 1) / (numSamples + 1);
    var alpha = node.easing.evaluateOutput(
        node.easing.getCurveParameterAtInput(timeFraction));

    var lerpedScale = OverflowDemo.lerp_(node.startValue, node.endValue, alpha);
    var lerpedInvScale =
        OverflowDemo.lerp_(invStartScale, invEndScale, timeFraction);

    var error = OverflowDemo.getInverseScaleError_(lerpedScale, lerpedInvScale);
    if (error > ERROR_TOL) {
      needsSubdivision = true;
      break;
    }
  }

  // Subdivide the interval if necessary.
  if (needsSubdivision) {
    var alphaT = node.easing.evaluateInput(0.5);
    var alphaV = node.easing.evaluateOutput(0.5);
    var midTime = node.startTime + alphaT * (node.endTime - node.startTime);
    var midValue = node.startValue + alphaV * (node.endValue - node.startValue);

    var splitEasing = node.easing.subdivide(0.5);
    var easings = [];
    for (var j = 0; j < splitEasing.length; j++) {
      var ctrlPts = splitEasing[j].getCtrlPts();
      easings.push(new OverflowDemo.EasingCurve(
        (ctrlPts[2] - ctrlPts[0]) / (ctrlPts[6] - ctrlPts[0]),
        (ctrlPts[3] - ctrlPts[1]) / (ctrlPts[7] - ctrlPts[1]),
        (ctrlPts[4] - ctrlPts[0]) / (ctrlPts[6] - ctrlPts[0]),
        (ctrlPts[5] - ctrlPts[1]) / (ctrlPts[7] - ctrlPts[1])));
    }

    node.children.push({
      startTime: node.startTime,
      startValue: node.startValue,
      endTime: midTime,
      endValue: midValue,
      easing: easings[0],
      children: []
    });

    node.children.push({
      startTime: midTime,
      startValue: midValue,
      endTime: node.endTime,
      endValue: node.endValue,
      easing: easings[1],
      children: []
    });

    OverflowDemo.subdivideScaleInterval_(node.children[0]);
    OverflowDemo.subdivideScaleInterval_(node.children[1]);
  }
};


/**
 * Walks an animation subdivision tree and gathers a list of keys, in order of
 * increasing time.
 * @param {!OverflowDemo.AnimNode_} node The parent node of the tree.
 * @param {!Array<!OverflowDemo.ChannelKey_>} keys Array of keys. This is
 *     initially empty, and filled when the function returns.
 * @private
 */

OverflowDemo.gatherNonUniformScaleKeys_ = function(node, keys) {
  if (node.children.length == 0) {
    keys.push({
      time: node.startTime,
      value: OverflowDemo.inverseScale_(node.startValue),
      easing: new OverflowDemo.EasingCurve(0, 0, 1, 1)
    });
  } else {
    for (var i = 0; i < node.children.length; i++) {
      OverflowDemo.gatherNonUniformScaleKeys_(node.children[i], keys);
    }
  }
};


/**
 * Returns an error measure for an approximation to the inverse of a scale
 * value. This represents the error in the position of a point at the edge of
 * the max.
 * @param {number} scale The scale being inverted.
 * @param {number} approxInverse The approximation of the scale's inverse.
 * @return {number} The error.
 * @private
 */
OverflowDemo.getInverseScaleError_ = function(scale, approxInverse) {
  return Math.abs(1 / scale - approxInverse) *
         (scale * scale * OverflowDemo.MAX_MASK_WIDTH);
};


/**
 * @param {!OverflowDemo.Transform_} transform A transform.
 * @return {string} The equivalent CSS style.
 * @private
 */
OverflowDemo.getTransformString_ = function(transform) {
  var t = transform.translate;
  var r = transform.rotate;
  var s = transform.scale;

  return 'translate3d(' + t[0] + 'px, ' + t[1] + 'px, ' + t[2] + 'px) ' +
    'rotateX(' + r[0] + 'deg) ' +
    'rotateY(' + r[1] + 'deg) ' +
    'rotateZ(' + r[2] + 'deg) ' +
    'scale3d(' + s[0] + ', ' + s[1] +', ' + s[2] + ')';
};


/**
 * @param {number|!Array<number>} scale A scale value, or an array of scales.
 * @return {number|!Array<number>} The inverse scale value(s), clamped to a
 *     maximum for small inputs.
 * @private
 */
OverflowDemo.inverseScale_ = function(scale) {
  if (Array.isArray(scale)) {
    var result = [];
    for (var i = 0; i < scale.length; i++) {
      result.push(1 / Math.max(scale[i], OverflowDemo.MIN_SCALE));
    }
    return result;
  } else {
    return 1 / Math.max(scale, OverflowDemo.MIN_SCALE);
  }
};


/**
 * @param {string} selector A CSS selector
 * @return {?CSSStyleRule} The style rule in the default style sheet with the
 *     given selector, or null if there is no such rule.
 * @private
 */
OverflowDemo.getRuleWithSelector_ = function(selector) {
  var rules = document.styleSheets[0].cssRules;
  for (var i = 0, len = rules.length; i < len; i++) {
    if (rules[i].selectorText && rules[i].selectorText == selector) {
      return rules[i];
    }
  }

  return null;
};


/**
 * Deletes the keyframes rule with a given name.
 * @param {string} name The rule's name.
 * @private
 */
OverflowDemo.deleteKeyframesRuleWithName_ = function(name) {
  var rules = document.styleSheets[0].cssRules;
  for (var i = 0, len = rules.length; i < len; i++) {
    if (rules[i].name && rules[i].name == name) {
      document.styleSheets[0].deleteRule(i);
      return;
    }
  }
};


/**
 * Representation of a 2D cubic Bezier curve.
 * @param {!Array<number>} ctrlPts Array of eight values containing the four
 *     2D control points.
 * @constructor
 */
OverflowDemo.Bezier = function(ctrlPts) {
  /** @private {!Array<number>} */
  this.ctrlPts_ = ctrlPts.slice(0);
};


/**
 * @return {!Array<number>} An eight-element array containing the control point
 *     coordinates.
 */
OverflowDemo.Bezier.prototype.getCtrlPts = function() {
  return this.ctrlPts_;
};


/**
 * Evaluates the x value at a given curve parameter.
 * @param {number} u The curve parameter, which is between 0 and 1.
 * @return {number} The x value.
 */
OverflowDemo.Bezier.prototype.evaluateX = function(u) {
  // Lerp the original control points.
  var p0 = OverflowDemo.lerp_(this.ctrlPts_[0], this.ctrlPts_[2], u);
  var p1 = OverflowDemo.lerp_(this.ctrlPts_[2], this.ctrlPts_[4], u);
  var p2 = OverflowDemo.lerp_(this.ctrlPts_[4], this.ctrlPts_[6], u);

  // Lerp the control points from the previous step.
  p0 = OverflowDemo.lerp_(p0, p1, u);
  p1 = OverflowDemo.lerp_(p1, p2, u);

  // Evaluate x.
  return OverflowDemo.lerp_(p0, p1, u);
};


/**
 * Evaluates the y value at a given curve parameter.
 * @param {number} u The curve parameter, which is between 0 and 1.
 * @return {number} The y value.
 */
OverflowDemo.Bezier.prototype.evaluateY = function(u) {
  // Lerp the original control points.
  var p0 = OverflowDemo.lerp_(this.ctrlPts_[1], this.ctrlPts_[3], u);
  var p1 = OverflowDemo.lerp_(this.ctrlPts_[3], this.ctrlPts_[5], u);
  var p2 = OverflowDemo.lerp_(this.ctrlPts_[5], this.ctrlPts_[7], u);

  // Lerp the control points from the previous step.
  p0 = OverflowDemo.lerp_(p0, p1, u);
  p1 = OverflowDemo.lerp_(p1, p2, u);

  // Evaluate y.
  return OverflowDemo.lerp_(p0, p1, u);
};


/**
 * Subdivides the curve at a u-value strictly between 0 and 1.
 * @param {number} u The u-value.
 * @return {!Array<!OverflowDemo.Bezier>} An array containining the lower and
 *     upper halves of the subdivision. The curve parameter of each output
 *     Bezier runs from 0 to 1.
 */
OverflowDemo.Bezier.prototype.subdivide = function(u) {
  // Interpolate each successive pair of control points.
  var ix0 = OverflowDemo.lerp_(this.ctrlPts_[0], this.ctrlPts_[2], u);
  var iy0 = OverflowDemo.lerp_(this.ctrlPts_[1], this.ctrlPts_[3], u);

  var ix1 = OverflowDemo.lerp_(this.ctrlPts_[2], this.ctrlPts_[4], u);
  var iy1 = OverflowDemo.lerp_(this.ctrlPts_[3], this.ctrlPts_[5], u);

  var ix2 = OverflowDemo.lerp_(this.ctrlPts_[4], this.ctrlPts_[6], u);
  var iy2 = OverflowDemo.lerp_(this.ctrlPts_[5], this.ctrlPts_[7], u);

  // Interpolate each of the interpolated points.
  var jx0 = OverflowDemo.lerp_(ix0, ix1, u);
  var jy0 = OverflowDemo.lerp_(iy0, iy1, u);

  var jx1 = OverflowDemo.lerp_(ix1, ix2, u);
  var jy1 = OverflowDemo.lerp_(iy1, iy2, u);

  // Interpolate the last pair of points.
  var kx0 = OverflowDemo.lerp_(jx0, jx1, u);
  var ky0 = OverflowDemo.lerp_(jy0, jy1, u);

  // Return the lower and upper halves of the subdivision.
  var ctrlPtsLower =
      [this.ctrlPts_[0], this.ctrlPts_[1], ix0, iy0, jx0, jy0, kx0, ky0];
  var ctrlPtsUpper =
      [kx0, ky0, jx1, jy1, ix2, iy2, this.ctrlPts_[6], this.ctrlPts_[7]];

  return [
    new OverflowDemo.Bezier(ctrlPtsLower),
    new OverflowDemo.Bezier(ctrlPtsUpper)
  ];
};


/**
 * Determines the curve parameter at a given value of X, assuming X is strictly
 * increasing or decreasing.
 * @param {number} x The x value.
 * @return {number} The curve parameter.
 */
OverflowDemo.Bezier.prototype.getCurveParameterAtX = function(x) {
  return this.getCurveParameter_(x, true);

};


/**
 * Determines the curve parameter at a given value of Y, assuming Y is strictly
 * increasing or decreasing.
 * @param {number} y The y value.
 * @return {number} The curve parameter.
 */
OverflowDemo.Bezier.prototype.getCurveParameterAtY = function(y) {
  return this.getCurveParameter_(y, false);
};


/**
 * Determines the curve parameter at a given value of X or Y, assuming that
 * coordinate is strictly increasing or decreasing.
 * @param {number} val The coordinate value.
 * @param {boolean} isX Whether the coordinate is X.
 * @return {number} The curve parameter.
 * @private
 */
OverflowDemo.Bezier.prototype.getCurveParameter_ = function(val, isX) {
  // Use binary search the curve parameter at the given position.
  var MAX_ITER = 32;
  var TOL = 1e-4;
  var uMin = 0;
  var uMax = 1;
  var isCurveIncreasing = isX && (this.ctrlPts_[0] < this.ctrlPts_[6]) ||
                          !isX && (this.ctrlPts_[1] < this.ctrlPts_[7]);
  for (var i = 0; i < MAX_ITER; i++) {
    var u = (uMin + uMax) / 2;
    var currentValue = isX ? this.evaluateX(u) : this.evaluateY(u);
    if (Math.abs(currentValue - val) < TOL) {
      return u;
    } else if ((currentValue < val && isCurveIncreasing) ||
               (currentValue > val && !isCurveIncreasing)) {
      uMin = u;
    } else {
      uMax = u;
    }
  }

  return u;
};


/**
 * An easing curve for a CSS3 animation.
 * @param {number} x0 The x (input) coordinate of the first control point.
 * @param {number} y0 The y (output) coordinate of the first control point.
 * @param {number} x1 The x (input) coordinate of the second control point.
 * @param {number} y1 The y (output) coordinate of the second control point.
 */
OverflowDemo.EasingCurve = function(x0, y0, x1, y1) {
  /** @private {!OverflowDemo.Bezier} */
  this.bezier_ = new OverflowDemo.Bezier([0, 0, x0, y0, x1, y1, 1, 1]);

  /** @type {number} */
  this.x0 = x0;

  /** @type {number} */
  this.y0 = y0;

  /** @type {number} */
  this.x1 = x1;

  /** @type {number} */
  this.y1 = y1;
};


/**
 * @return {string} The CSS text equivalent to this easing curve.
 */
OverflowDemo.EasingCurve.prototype.getCssText = function() {
  if (this.x0_ == 0 && this.y0_ == 0 && this.x1_ == 1 && this.y1_ == 1) {
    return 'linear';
  } else {
    return 'cubic-bezier(' + this.x0_ + ',' + this.y0_ + ',' + this.x1_ + ',' +
        this.y1_ + ')';
  }
};


/**
 * Subdivides the easing curve at a u-value strictly between 0 and 1.
 * @param {number} u The u-value.
 * @return {!Array<!OverflowDemo.Bezier>} An array containining the lower and
 *     upper halves of the subdivision. The curve parameter of each output
 *     Bezier runs from 0 to 1.
 */
OverflowDemo.EasingCurve.prototype.subdivide = function(u) {
  return this.bezier_.subdivide(u);
};


/**
 * Evaluates the input value at a given curve parameter.
 * @param {number} u The curve parameter, which is between 0 and 1.
 * @return {number} The input value.
 */
OverflowDemo.EasingCurve.prototype.evaluateInput = function(u) {
  return this.bezier_.evaluateX(u);
};


/**
 * Evaluates the output value at a given curve parameter.
 * @param {number} u The curve parameter, which is between 0 and 1.
 * @return {number} The output value.
 */
OverflowDemo.EasingCurve.prototype.evaluateOutput = function(u) {
  return this.bezier_.evaluateY(u);
};


/**
 * Determines the curve parameter at which the input is a given value.
 * @param {number} inputVal The input value.
 * @return {number} The curve parameter.
 */
OverflowDemo.EasingCurve.prototype.getCurveParameterAtInput =
    function(inputVal) {
  return this.bezier_.getCurveParameterAtX(inputVal);
};

              
            
!
999px

Console