JavaScript shrinking/growing circle transition
TL:DR
How do I even begin to describe this one? It would be a lot easier if the CSS 2 clip standard supported anything besides a "rect" value, namely a "circle" or "ellipse" but... since that doesn't exist, I've done my best to piece something together that will do what you're asking. The caveats are many. One is that this is only going to work on something with a solid color background in the event you wanted the picture to clip to the background. Another is that while I've tried to account for the CSS update timing across browsers, the rendering still isn't "perfect." My initial approach was to simply animate the clip on the image that was getting replaced, but that didn't work due to the way updates were made to the clipping via the easing function in the plugin I located. The final approach is below.
The Approach
The concept is to set the image as a background-image
property of a container like a <div>
with a background-position
of center center
, and the position
of the container to relative
, or anything non-static. The next is to generate the clipping elements as children of the container. The first is a position: absolute
clipping circle image of the color of your background, either transparent PNG or GIF (I prefer the former), and the next four are divs, also with absolute
positions that have left
, right
, top
, and bottom
attributes set to 0 for each of the respective sides they will clip. The idea is to animate the top
, left
, width
, and height
of the clipping circle image and synch up the width and height of the clipping divs using the step callback option of the .animate() call by matching them to the current left
and top
values. Between animations, you change the background-image
of the container to the new image and then start the animation back in the opposite direction.
This required a little finessing in IE7, 8, and Webkit browsers as the animation clipped much more cleanly in Firefox and IE9. This would be the adjust
variable you'll see in the working demo.
The sample code is below:
The Markup
<div class="imageContainer image1">
<img class="clip" src="clipCircle.png" />
<div class="top fill"></div>
<div class="left fill"></div>
<div class="right fill"></div>
<div class="bottom fill"></div>
</div>
The CSS
div.imageContainer
{
background-position: center;
width: 300px;
height: 300px;
position: relative;
}
img.clip
{
width: 100%;
height: 100%;
position: absolute;
}
div.fill
{
position: absolute;
background-color: White;
}
div.left, div.right
{
height: 100%;
top: 0;
width: 0;
}
div.left
{
left: 0;
}
div.right
{
right: 0;
}
div.top, div.bottom
{
width: 100%;
left: 0;
height: 0;
}
div.top
{
top: 0;
}
div.bottom
{
bottom: 0;
}
The Script
var speed = 1000;
$clip = $("img.clip");
$clip.animate({
top: $clip.parent().height() / 2,
left: $clip.parent().width() / 2,
width: 0,
height: 0
}, {
duration: speed,
step: function(now, fx) {
switch (fx.prop) {
case "top":
$("div.top").css("height", now);
$("div.bottom").css("height", now + adjust);
break;
case "left":
$("div.left").css("width", now);
$("div.right").css("width", now + adjust);
}
},
complete: function() {
$(this).parent().addClass("image2");
$(this).animate({
top: 0,
left: 0,
width: $clip.parent().width(),
height: $clip.parent().height()
}, {
duration: speed,
step: function(now, fx) {
switch (fx.prop) {
case "top":
$("div.top").css("height", now);
$("div.bottom").css("height", now + adjust);
break;
case "left":
$("div.left").css("width", now);
$("div.right").css("width", now + adjust);
}
},
complete: function() {
$("div.imageContainer > *").removeAttr("style");
}
});
}
});
EDIT:
The CSS3 Solution
When cross-browser compatibility is less of a concern, CSS3 is an option (although I'd probably suggest seeing what can be done with the new HTML5 Canvas for this kind of animation). There are a couple things to note:
- The image must be inside a container in order to allow us to clip toward its center rather than its top left corner.
- The border-radius attribute will not clip the child images inside a container. For this reason, the image must become the background-image attribute of the container.
- jQuery does not currently animate border-radius correctly. You can either replace the current jQuery animate functionality for that attribute or build a custom border-radius animation object to make jQuery more well-behaved. I have opted for the latter. Each corner's border-radius must be animated separately.
- The animation in or out consists of two separate segments, and as a result the "linear" easing function is probably best used for cleanest results.
The method is commented inline below:
The Markup
<div class="imageContainer image1">
</div>
The CSS
div.imageContainer
{
background-position: 0px 0px;
background-repeat: no-repeat;
width: 300px;
height: 300px;
position: absolute;
top: 0;
left: 0;
}
div.image1
{
background-image: url(/images/myFirstImage.png);
}
div.image2
{
background-image: url(/images/mySecondImage.png);
}
The Script
// Total animation speed in or out will be speed * 1.5
var speed = 600;
// Store a reference to the object to be clipped
var $clip = $("div")
// A function to build a mapping object for border radius parameters
var buildRadiusObj = function(value) {
// Dimension an option object
var opts = {};
// Use specialized Mozilla CSS attributes when needed
var attributes = $.browser.mozilla ?
["-moz-border-radius-topleft",
"-moz-border-radius-bottomleft",
"-moz-border-radius-topright",
"-moz-border-radius-bottomright"] :
["border-top-left-radius",
"border-bottom-left-radius",
"border-top-right-radius",
"border-bottom-right-radius"];
// Build the option object
$.each(attributes, function(i, key) {
opts[key] = value;
});
// Return the result
return opts;
}
$clip.animate(buildRadiusObj($clip.width() * 0.5), { // Animate the border radius until circular
duration: speed * 0.5,
easing: "linear"
}).animate({ // Resize and reposition the container
width: 0,
left: $clip.width() / 2,
height: 0,
top: $clip.height() / 2
}, {
duration: speed,
easing: "linear",
step: function(now, fx) { // Synch up the background-position
if (fx.prop == "top") {
$(this).css("background-position", "-" + $(this).css("top") + " -" + $(this).css("left"));
}
},
complete: function() { // Swap the image
$(this).addClass("image2");
}
}).animate({ // Restore position and size
width: $clip.width(),
left: 0,
height: $clip.height(),
top: 0
}, {
duration: speed,
easing: "linear",
step: function(now, fx) { // Synch the background-position
if (fx.prop == "top") {
$(this).css("background-position", "-" + $(this).css("top") + " -" + $(this).css("left"));
}
},
complete: function() { // Remove inline styles but reapply border-radius
$(this).removeAttr("style").css(buildRadiusObj($clip.width() * 0.5));
}
}).animate(buildRadiusObj(0), { // Restore border-radius to block
duration: speed * 0.5,
easing: "linear",
complete: function() {
$(this).removeAttr("style"); // Remove inline styles
}
});
Again, the demo is located here.