Card Expansion Effect with SVG clipPath

A morphing card expansion effect enhanced by a low poly background animation, using SVG clipPath and Trianglify.

Today we are going to create a experimental grid layout with “cards” where we’ll animate the card expansion with a morphing effect on the cover image and follow its motion with a delightful animation of a low-poly pattern used as the background of the page.

We are using Trianglify by Quinn Rohlf to generate the background pattern, SVG’s clipPath to morph the card image, and GSAP to animate and control the whole sequence.

We could have used the CSS clip-path property to clip and morph the image but since the support is currently quite limited and inconsistent across browsers, we’ll be using SVG clipPath instead. This way we can make the demo work across all browsers, including IE9.

CardExpansionGif

Card Expansion

Let’s start by taking a look at how the card expansion works.

Since what we want is to only move the selected card without affecting the flow of other siblings, we’ll actually move the container inside by fixing it to a certain position on click and then animate its sizes and position to fill the screen. This way we’ll never cause a flickering of the cards.

As you can see on this screenshot the ‘card’ element is still at the same position while the card is open.

CardExpansion01

The very first thing that we need to do, is to build the markup and style for the card in its two states. We build the card the way it is in its opened state:


    <div class="card">
      <div class="card__container">
        <svg class="card__image">
          <!-- SVG image... -->
        </svg>
        <div class="card__content">
          <i class="card__btn-close fa fa-times"></i>
          <div class="card__caption">
            <h2 class="card__title">Title...</h2>
            <p class="card__subtitle">Subtitle...</p>
          </div>
          <p class="card__copy">
            Lorem ipsum dolor sit amet...
          </p>
        </div>
      </div>
    </div>
    

Here is the style for the card and the nested container.


.card {
  position: relative;
  float: left;
  width: 29%;
  height: 0;
  margin: 2%;
  padding-bottom: 20%;
}

.card__container {
  position: fixed;
  top: 0;
  left: 0;
  overflow-x: hidden;
  overflow-y: auto;
  width: 100%;
  height: 100%;
  -webkit-overflow-scrolling: touch;
}
/* You can check out the other styles in the CSS files */

For the closed state, we set the position of the card container to ‘absolute’:


.card__container--closed {
  position: absolute;
  overflow: hidden;
}

To animate the card between the two states, we’ll simply get the card position on the viewport, stick the container to it and then tween each property to the full screen size.

Notice that the ‘x’ property is tweened to the center of the screen. This is done because in order to animate the background, we need to keep track of the card’s center during each frame. We’ll go into more detail later on.

    
    /**
     * Float card to final position.
     * @param {Function} callback The callback `onCardMove`.
     * @private
     */
    Card.prototype._floatContainer = function(callback) {

      $(document.body).css('overflow', 'hidden');

      var TL = new TimelineLite;

      // Get the card position on the viewport.
      var rect = this._container.getBoundingClientRect();
      var windowW = window.innerWidth;

      var track = {
        width: 0,
        x: rect.left + (rect.width / 2),
        y: rect.top + (rect.height / 2),
      };

      // Fix the container to the card position (start point).
      TL.set(this._container, {
        width: rect.width,
        height: rect.height,
        x: rect.left,
        y: rect.top,
        position: 'fixed',
        overflow: 'hidden'
      });

      // Tween the container (and the track values) to full screen (end point).
      TL.to([this._container, track], 2, {
        width: windowW, // This value must be in px in order to correctly update the track width.
        height: '100%',
        x: windowW / 2,
        y: 0,
        xPercent: -50,
        ease: Expo.easeInOut,
        clearProps: 'all',
        className: '-=' + CLASSES.containerClosed,
        onUpdate: callback.bind(this, track)
      });

      return TL;
    };
    

Tiny break: 📬 Want to stay up to date with frontend and trends in web design? Subscribe and get our Collective newsletter twice a tweek.

Image Clip

In order to clip the image with cross browser support, we’re using the SVG clipPath. The respective image is set as an SVG Image Element with the definition of the clip as follow:

    
    <!-- In this case preserveAspectRatio set the image as background cover, like in CSS. -->
    <svg class="card__image" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1920 500" preserveAspectRatio="xMidYMid slice">
      <defs>
        <clipPath id="clipPath1">
          <!-- r = 992 = hyp = Math.sqrt(960*960+250*250) -->
          <circle class="clip" cx="960" cy="250" r="992"></circle>
        </clipPath>
      </defs>
      <!-- the clip-path url reference to the id above -->
      <image clip-path="url(#clipPath1)" width="1920" height="500" xlink:href="img/a.jpg"></image>
    </svg>
    

When using a circle, we just need to tween the ‘radius’ attribute to a smaller value (60px in this case) with TweenLite:


    /**
     * Clip image in.
     * @private
     */
    Card.prototype._clipImageIn = function() {

      // Circle.
      var tween = TweenLite.to(this._clip, 1, {
        attr: {
          r: 60
        },
        ease: Expo.easeInOut
      });

      return tween;
    };
    

If instead we want to use a polygon like we do in demo 2, 3 and 4, we need to tween each point of the polygon. In the ‘start and ‘end’ array you can see a sub-array of coordinates [x, y] for each point.

As shown here we attach an ‘onUpdate’ callback to each end point, and populate an initially empty array called ‘points’ with the values for each point. Once the length is equal to 4 (the number of points that we’re using) the SVG clipPath is updated and the ‘points’ array is reset.

    
    var TL = new TimelineLite; // Polygon

    var start = [
      [0, 500],
      [0, 0],
      [1920, 0],
      [1920, 500]
    ];

    var end = [
      [1025, 330],
      [1117, 171],
      [828, 206],
      [913, 260]
    ];

    var points = [];

    // Create a tween for each point

    start.forEach(function(point, i) {

      var tween = TweenLite.to(point, 1.5, end[i]);

      end[i].onUpdate = function() {

        points.push(point.join());

        // Every 4 point update clip-path.
        if (points.length === end.length) {
          $(this._clip).attr('points', points.join(' '));
          // Reset.
          points = [];
        };

      }.bind(this);

      tween.vars.ease = Expo.easeInOut;

      // Add the tween for each point at the start of the timeline.
      TL.add(tween, 0);

    }, this);

    return TL;
    

For demo purposes and to keep things simple, the points are hardcoded. It would be interesting to push this effect forward and set a different clipping shape for each card, or dynamically get the points from the pattern on the background.

Polygonal Background

To complete the effect let’s combine the card floating animation with the background pattern. To animate the SVG we just use CSS transitions (opacity and scale transform).

Unfortunately Firefox doesn’t support transform-origin on SVG elements, so we just change the polygon opacity otherwise the pattern transition will slow down the whole animation since the browser has to translate each path from the upper left corner through the whole screen.

In demo.js you’ll find the full code. This are the main steps:

  • Generate an SVG pattern with Trianglify.
  • Append the generated element to the container provided. Notice that the container is already hidden with a CSS class, so we’ll not show the pattern until all of its polygons are set for the animation.
  • Loop the SVG children paths and get the center position of each element on the viewport.
  • Store a reference of the path element and its points which we’ll use later to find out which polygon is hovered by the card.
    
    /**
     * Map of svg paths and points.
     */
    var polygonMap = {
      paths: null,
      points: null
    };

    ...

    /**
     * Store path elements, map coordinates and sizes.
     * @param {Element} pattern The SVG Element generated with Trianglify.
     * @private
     */
    function _mapPolygons(pattern) {

      // Append SVG to pattern container.
      $(SELECTORS.pattern).append(pattern);

      // Convert nodelist to array,
      // Used `.childNodes` because IE doesn't support `.children` on SVG.
      polygonMap.paths = [].slice.call(pattern.childNodes);

      polygonMap.points = [];

      polygonMap.paths.forEach(function(polygon) {

        // Hide polygons by adding CSS classes to each svg path (used attrs because of IE).
        $(polygon).attr('class', CLASSES.polygon + ' ' + CLASSES.polygonHidden);

        var rect = polygon.getBoundingClientRect();

        var point = {
          x: rect.left + rect.width / 2,
          y: rect.top + rect.height / 2
        };

        polygonMap.points.push(point);
      });

      // All polygons are hidden now, display the pattern container.
      $(SELECTORS.pattern).removeClass(CLASSES.patternHidden);
    };

    ...
    

Notice that we do all this computation ahead, so during the animation we’ll not look at the DOM anymore but instead we’ll just loop the ‘polygonMap’ to find out to which element we need to add or remove CSS classes.

Consistent Choreography

Now, the key to make the animation choreographically consistent is to have a hierarchy among the animated elements, since the primary elements is the card we’ll let the floating drive the background animation.

To do so we’ll pass a callback ‘_onCardMove’ (demo.js) to the ‘_floatContainer’ method (Card.js) that will be called during the tween ‘onUpdate’ event.

In order to detect which polygon needs to be animated, we test if the center of the path polygon previously stored in ‘polygonMap.points’ is inside an imaginary circle where its radius is defined by the card width and the center by the card center x, y.

These parameters are computed in the variable ‘track’ inside the ‘_floatContainer’ method and passed as an argument to the callback (_onCardMove).

    
      /**
       * Callback to be executed on Tween update, whatever a polygon
       * falls into a circular area defined by the card width the path's
       * CSS class will change accordingly.
       * @param {Object} track The card sizes and position during the floating.
       * @private
       */
      function _onCardMove(track) {

        var radius = track.width / 2;

        var center = {
          x: track.x,
          y: track.y
        };

        polygonMap.points.forEach(function(point, i) {

          if (_detectPointInCircle(point, radius, center)) {

            // Notice that since the points array has been previously generated
            // from the paths array we can safely use the index to get the correct path.
            $(polygonMap.paths[i]).attr('class', CLASSES.polygon);

          } else {
            $(polygonMap.paths[i]).attr('class', CLASSES.polygon + ' ' + CLASSES.polygonHidden);
          }
        });
      }

      /**
       * Detect if a point is inside a circle area.
       * @param {object} point The point to test.
       * @param {number} radius The width of the card.
       * @param {object} center The center of the card.
       * @private
       */
      function _detectPointInCircle(point, radius, center) {

        var xp = point.x;
        var yp = point.y;

        var xc = center.x;
        var yc = center.y;

        var d = radius * radius;

        var isInside = Math.pow(xp - xc, 2) + Math.pow(yp - yc, 2) <= d;

        return isInside;
      };
    

In the method ‘_onCardMove’ we can make variations to the background animation. For example, by swapping the if/else statement we inverted the pattern in demos 3 and 4.

To help you understand the background animation in the source files you’ll find a prototype with just the background animation and the circle displayed that you can move around with the mouse (hover.html).

Have a look at the demos and dig into the source, we hope you find this effect useful and inspiring!

Claudio Calautti

Claudio is a freelance creative developer based in London. He develops modern, highly interactive websites with cutting edge technologies. In his spare time he has fun with web experiments and enjoys travelling.

Stay in the loop: Get your dose of frontend twice a week

Fresh news, inspo, code demos, and UI animations—zero fluff, all quality. Make your Mondays and Thursdays creative!

Feedback 85

Comments are closed.
  1. Hey man awesome tutorial . But in your Demo 4 I have facing some scroll issue . I can’t scroll the article using my keyboard or mouse until I click on the article part . Otherwise it’s a great work

    • I think its not because of the click, the scroll region is defined only for the article area and the other part is fixed height and is a background, so when your mouse is in focus to the article the page scrolls and when you are outside it is not..

  2. Looks really awesome, but not really working on mobile. Otherwise it could have been useful!

  3. The fourth one is awesome. My only complaint is the scrollbar appearing and disappearing makes the animation feel janky. It would be better to always keep it than have it come and go and bump the content around.

  4. too close to game dev. whats next? opengl GLSL? i think we’re losing the web…

  5. This is unreal… I can’t stand up for next few minutes *awkward* Super work

  6. Amazing demo! Could you explain, why fixed positioned card__container behaves as not fixed positioned when the article is opened?

  7. This is a great article, the effect is astonishing.
    I like also the cross browsers support.
    Thank you for sharing!

  8. Hi Claudio,

    Amazing work! I would like to use this on my WordPress website and wondering how to transfer this into WordPress scenerio. Especially how to build the markup..

    Best regards,
    Art

    • Art,

      You will want to attach all the necessary files and replace aspects of the markup with php code to loop in WordPress content. Hope that helps put you on the right track.

      I have a working example live on one of my WordPress installations.

      -Steve

  9. This is cool, but I just don’t really know what is it for.
    It involves fairly complex math, it can slow your browser down to crawl, and, above this all, it serves absolutely no purpose except for making the end user frustrated because of the waiting time.
    I’m all for meaningful animations and I think almost all examples on this site are exceptional – but this one? I doubt.
    Also, I have seen some absolutely awful implementations of solutions presented on codrops. It makes me quite worried what might come out of this one when it’s unleashed on the web.

  10. Amazingly awesome. Demo4 is brilliant, with the very unusual image placement and masking greatly impacting the design. Major congrats!

  11. Love your articles, when are we going to see wordpress plugins of these beautiful effects ?
    Only you people can make this happen !!!

  12. This is a great piece of work. Looks amazing!
    Just one Q: I can’t get it to work on Samsung Galaxy Tablet?

  13. Hello , i was wondering why on my Macbook 13″ retina display works pretty sluggish , ¿is that normal or safari isn’t optimised for this kind of effects?

  14. Awesome work Claudio. It’s greta to see you really pushing web animation and transitional effects. It would be good practice to tighten up your timing for the transitions as they are a little on the long side. I look forward to reading the article properly.

    • You need change line 52 in …/js/demo-4.js

      Default is x_colors: 'Purples'

      Change ‘Purples‘ on one of this: ‘YlGn’, ‘YlGnBu’, ‘GnBu’, ‘BuGn’, ‘PuBuGn’, ‘PuBu’, ‘BuPu’, ‘RdPu’, ‘PuRd’, ‘OrRd’, ‘YlOrRd’, ‘YlOrBr’, ‘Purples’, ‘Blues’, ‘Greens’, ‘Oranges’, ‘Reds’, ‘Greys’, ‘PuOr’, ‘BrBG’, ‘PRGn’, ‘PiYG’, ‘RdBu’, ‘RdGy’, ‘RdYlBu’, ‘Spectral’, ‘RdYlGn’.

      Or U can make your own color in …/js/vendors/trianglify.min.js

  15. Hey Claudio,

    This is great! I’d love to use it, but there is a conflict when using that cash.js library and jQuery. Cash.js makes no attempts to be compatible with jQuery. Our site is currently relying on jQuery and $. Can you recommend a way that I can use both libraries, and continue to use $ throughout our custom functions?

    You help would be greatly appreciated.

    Thank you,
    Molly

    • I’ve got the same problem. Cash and jquery got conflicted. Is there a way to use both without conflict?

  16. Claudio, congratulations and thank you for a trully great effect!
    One short question, how could I make this loop?

    Thanks again!

  17. Hi, very awesome effect !!

    But I’ve got a big issue, on the same page I would like this effect with another one : the “Perspective Page View Navigation”, but I think the menu effect makes the card expansion effect bug on Safari (and mobile). After some research to find out the conflict, I think this is caused by the “perspective”, “container” and “wrapper” elements which are in “position: relative”, and on Safari the position behaviors are not the same … here is a link the page with the issue.

    Can anyone help me please ?

  18. Love this demo and I was wondering, how could I integrate stuff like this in my wordpress website? I can’t really find a guide on implementing custom html/css/js into a wordpress website. For instance I would like to build a custom menu using code from tympanus.. Anybody who can help?

  19. Hello, how can I change color of this amazing pattern that shows afer clicking a picture (demo 2)?