Sunday, December 9, 2012

Creating a Perspective based Animated 3D Ribbon Effect

css3-ribbon-effect
It seems that I've seen a lot of uses of this ribbon effect springing up lately. Maybe they've been there and I just haven't really noticed. On RedHat's careers page, it has them every where. I even saw them in a Popular Science article I was reading the other day. The effect does make headings stand out better by offsetting them from the main page alignment. I figured someone probably found a way to do this with just CSS3 and sure enough, I found this article with a solution.

What's really nice about this approach is that it doesn't require any additional markup. However, there are some limits to using :before and :after to create the extra content required for the effect. If you look at the available variations, you need to create content for the wrapping part with either the :before or :after. This leaves you with one pseudo-element to either create a wrap on the other side or one of those tail effects (on either side). If you want a complete ribbon with left-tail, left-wrap, right-wrap, and right-tail, you'll need at least one more element in the markup to attach these styles to. Additionally, animating the styles created by the pseudo-elements would be a challenge. Notice that the image above has two different wrap effects for the top (Apples) and bottom (Bananas). If I wanted to transition from the top to the bottom, it would not be possible with content generated by :before and :after because I need to animate those little triangles that create the wrap effect.  

Those triangles are actually another CSS trick that creatively uses the border style to make the correct shape. Since I want to change the border style based on the position of the element to maintain the illusion of perspective, I'll need to add markup around my original block and style it similarly.  While this approach does add extra content, it does turn this solution into something that possibly more flexible and easier to debug.

Before digging into the calculations to position and size the border, we need to create the base markup and static styling:

<div class="wrapper">
<div id="move"></div>

<div id="mv1" class="banner">
<div class="wrap"></div>
<div class="main">Apples</div>
</div>
</div>


The wrapper element is the guide the banner will appear to be wrapping around. It can be styled with any kind of height/width/color you'd like. The important thing to note is the relative positioning and the padding. The banner element will need to be positioned accordingly based on that setup:


.wrapper {
position: relative;
width: 100px;
height: 360px;
margin: 30px;
padding: 30px;
background-color: #ddd;
border-radius: 5px;
}


Now, for the actual banner/ribbon element. Since our intention is to move this element around, it needs to be absolutely positioned. I setup the padding of the wrapper to be 30px above so I'll initialize the top to 30px to match. I want the banner to be 10px left of the wrapper but the padding is 30px so I need to move it -40px. Everything else needs to be moved around accordingly so it fits together properly. The .main class will hold the text and the .wrap class implements the triangle border trick to complete the ribbon effect.


.banner {
position: absolute;
top: 30px;
margin-left: -40px;
}

.banner .main {
cursor: pointer;
position: relative;
padding: 10px;
padding-left: 20px;
padding-right: 20px;
min-width: 75px;
font-weight: bold;
color: white;
background-color: gray;
border-radius: 0px 5px 5px 0px;
}

.banner .wrap {
position: absolute;
top: 0px;
left: 0;
width: 0;
height: 100%;
border-width: 0px 10px 5px 0px;
border-style: solid;
border-color: transparent #aaa transparent transparent;
}


The wrap class is set to the initial position of the banner which is at the top with the wrap appearing on the bottom of the banner (the Apples one in the image above). We only need the right border on this element and will adjust the top/bottom widths to manipulate the shape of the triangle formed by the visible corner of the border. Since the wrap element is behind the main element, we'll carefully move the wrap element to only show the triangle portion of the border.

As a test, I originally setup a slider control to see the result of my calculations at different positions. The top point is at 30px and the bottom extreme is at 330px (creating a 300px range). Over that 300px range, the wrap element needs to adjust the bottom triangle from 5px high to 0px and then adjust the top triangle from 0px to 5px tall. An important point to notice here is that both top and bottom borders do not change at the same time. At our fictitious eye level, we should not see the wrapping since it should be directly behind the main element. So the full range is really 10px that will be traveled from top to bottom. The first calculation is to scale the current position of the banner to the range of the wrap element. To simplify the logic, I move it so the range is from -5 to 5. The final adjustment is to shift the wrap element up and down 5px to properly expose either the top or bottom border triangle:



var p = Math.round(10 / 300 * (ui.value - 30)) - 5;

$('#mv1')
.css('top', ui.value)
.find('.wrap')
.css({
top: (p >= 0 ? -p : 0),
borderTopWidth: (p >= 0 ? p : 0),
borderBottomWidth: (p <= 0 ? -p : 0)
})
.end()
.find('.main')
.css({
boxShadow: '3px '+(-p/2)+'px 3px 0 #aaa'
});



After successfully figuring out that logic, I moved it to an animation which will occur when the banner is clicked. It will transition up and down from the top to the bottom of the wrapper on each click:



$(function() {

var dir = 1;
$('#mv2')
.click(function ( e ) {

// Cache these for use in the step function
var $w = $('#mv2 .wrap'),
$m = $('#mv2 .main');

// Animate the position of the whole banner element but
// use the step callback to manipulate the position of the
// wrap element based on the current position.
$(this)
.animate({top: (dir == 1 ? 330 : 30)}, {
step: function (now, fx) {

var p = Math.round(10 / 300 * (now - 30)) - 5;

$w.css({
top: (p >= 0 ? -p : 0),
borderTopWidth: (p >= 0 ? p : 0),
borderBottomWidth: (p <= 0 ? -p : 0)
});

$m.css('box-shadow', '3px '+(-p/2)+'px 3px 0 #aaa');
}
});

dir *= -1;

});

// jQuery makes this easy to apply for all browser flavors.
$('#mv2 .main').css('box-shadow', '3px 3px 3px 0 #aaa');

});


I setup a demonstration of this code on my sandbox with both the slider implementation and the animated version. Following along the same approach, you can add more elements to the wrapper and position their wrapper to maintain the correct perspective that occurs during the animation. Add some more logic and could build an accordion style menu or content container.