Permalink
Find file
5f0ff4b Mar 12, 2014
576 lines (450 sloc) 20.6 KB

How I Learned to Walk

Over the last couple weeks in my spare time, I decided to seriously mess around with CSS3. In particular webkit animations and transformations.

See the experiment here.

Idea(s) and Inspiration:

The walking experiment came from the need to work on something other than javascript for a while. Initially I thought I would do some kind of hybrid experiment mixing css animations and very minimal javascript, but eventually settled on using only CSS3 and html.

I threw around ideas of doing some kind of chaotic pendulum, or maybe a cleaner implementation of the robotic arm. The idea of doing the arm again, reminded me of the CSS3 AT-AT I saw a while ago. I remembered the first time I saw it, thinking, "This is incredible", before realizing just how limited the animation was. I thought maybe I could take it further and create something a little more extreme.

Structure:

When I first started working on the walking man, I didn’t really know where it was going to go so I ended up going through several iterations of markup for the actual skeleton. But in each iteration the css and overall structure of the man got better and better, so I think it worked out for the best.

The key idea behind all of this is the fact that a css transformation applied to an element also applies to all of its children. Without this, I would have had to use all kinds of math to figure out how to make the joints match up. Instead all that work is done for me, all I had to do was animate each limb independently.

Ultimately, I landed more or less on the structure below:

<div class="torso">

    <div class="left bicep">
        <div class="left forearm"></div>
    </div> <!-- left arm -->

    <div class="right bicep">
        <div class="right forearm"></div>
    </div> <!-- right arm -->

    <div class="left thigh">
        <div class="left shin">
            <div class="left foot">
                <div class="left toes"></div>
            </div>
        </div>
    </div> <!-- left leg -->

    <div class="right thigh">
        <div class="right shin">
            <div class="right foot">
                <div class="right toes"></div>
            </div>
        </div>
    </div> <!-- right leg -->

</div> <!-- torso -->

As you can see from the html above, I would need to use z-indexes to push the left arm as well as the legs behind the torso, as well as move the shins and forearms behind the thighs and biceps, respectively. Z-Indexes turned out to be my biggest source of grief, and almost pushed me to aborting this experiment all together. First off, I'll completely admit that I had only a vague understanding of stacking contexts when I first set out.

With my limited knowledge, I thought I could just give negative z-indexes to things I wanted floated behind... But as it turns out, you can only push a child element behind its parent if its parent does not have a z-index set. This means if I set the z-index of the left bicep to -1, I would never be able to move the left forearm behind the left bicep. The other problem that I found was that the moment I started animating anything, webkit would basically destroy the stacking contexts.

In the end, I found that if I added a little more markup, not only would it make more semantic sense, but also solve all of the above problems:

<div class="me">
    <div class="torso">

        <div class="left leg">
            <div class="left thigh">
                <div class="left shin">
                    <div class="left foot">
                        <div class="left toes"></div>
                    </div>
                </div>
            </div>
        </div> <!-- left leg -->

        <div class="right leg">
            <div class="right thigh">
                <div class="right shin">
                    <div class="right foot">
                        <div class="right toes"></div>
                    </div>
                </div>
            </div>
        </div> <!-- right leg -->

        <div class="left arm">
            <div class="left bicep">
                <div class="left forearm"></div>
            </div>
        </div> <!-- left arm -->

        <div class="right arm">
            <div class="right bicep">
                <div class="right forearm"></div>
            </div>
        </div> <!-- right arm -->

    </div> <!-- torso -->
</div> <!-- me -->

Note that I only really changed two things. First I wrapped each limb in another div, this was the main solution. I also moved the definition of the arms to be below the legs. I had originally done this in order to make sure that the arms would always display above the legs thus saving me a little bit of css (but not really).

Animating:

One goal of this experiment was to create something that would work extremely well on iPhones, iPads and iPods. To do this, all I needed to do was make sure I animated only a certain subset of properties. The key one being -webkit-transform (for rotation and translation), as it has been optimized in mobile safari.

When I first started animating, I thought that the only option was to create two separate animations for every type of appendage. So I set to work just trying to animate one side at a time (eg, the right side), and then translating it over to the other, such that the animation was offset by 50%. So for example, I would animate the right leg such that it would start out in front, then define a second nearly identical animation for the left side that would start the leg in the back position.

This became troublesome when I started using keyframes at strange percentages (20%, 40%, 70%). Translating this offset by 50% was sometimes difficult, and would often look awful when I was playing around with different timing functions.

Eventually I discovered that the -webkit-animation-delay property accepted negative values that would basically start an animation at some arbitrary point. So for example if I had the right arm swinging with a duration of 2 seconds, I could use that same animation for the left arm, but just specify the -webkit-animation-delay to be -1 second. Worked like a charm. Kinda.

Unfortunately, mobile safari apparently is not as well equipped as its desktop sibling. Namely my clever -webkit-animation-delay solution did not work at all and both arms and legs would animate in unison...

So after all that work to get things working so beautifully as a single animation, I reverted back to doing separate animations for the left and right sides. Although this time, to maintain my sanity, I decided to use only 0%, 25%, 50%, 75% and 100% as my keyframes. This made things nice and easy to offset.

Little Tidbit:

Just to kind of show off the fact that everything in the experiment is a div, I decided at one point that I would like to add a little feature to show this off. The easy solution would be to add a little bit of javascript that would add a border or background color to every div in the "canvas". But I thought this would be cheating a little on my goal of keeping the experiment free of javascript. The solution to this actually came from my coworker Scott Kyle (www.appden.com).

At the bottom of the page, you can see that there are two links to show and hide the element borders. One simply has an href of "#canvas" and the other "#", respectively. Then leveraging the target pseudo selector, I could just add a few more lines of css. This is what it looks like:

<style type="text/css" media="screen">

    #canvas:target div:not(.overlay) {
        border: 1px solid black;
    }

    #canvas:target div.me div{
        background: rgba(255, 255, 255, 0.25);
    }

</style>

<p style="text-align: center">
    <a href="#canvas">Show</a> /
    <a href="#">Hide</a>
    Element Borders.
</p>

Final Thoughts:

Thats more or less it. In the end its quite amazing to see something with so much motion in a browser, yet not having any code. I also find it to be pretty incredible that it works just as well on my iPhone 3G as on my iPad or laptop.

In case you're interested, here is the complete source of the experiment:

<style type="text/css" media="screen">
    #canvas {
        height: 600px;
        margin: 0;
        padding: 0;
        position: relative;
        overflow: hidden;
    }

    #canvas div {
        position: absolute;
        -webkit-animation-iteration-count: infinite;
        -webkit-animation-timing-function: linear;
    }

    #canvas:target div:not(.overlay) {
        border: 1px solid black;
    }

    #canvas:target div.me div{
        background: rgba(255, 255, 255, 0.25);
    }

    .me {
        top: 50px; left: 350px;
        -webkit-animation-name: me;
    }

    .me, .me div {
        background-repeat: no-repeat;
        -webkit-animation-duration: 1750ms;
    }

    .torso {
        width: 86px; height: 275px;
        background-image: url({{ page.resource_url }}me/torso.png);
    }

    .arm {
        left: 12px;
        -webkit-transform-origin: 20px 10px;
    }

    .right.arm {
        top: 93px;
        -webkit-animation-name: right-bicep;
    }
    .left.arm {
        top: 87px;
        -webkit-animation-name: left-bicep;
    }

    .bicep {
        height: 124px; width: 51px;
    }

    .right.bicep { background-image: url({{ page.resource_url }}me/right-bicep.png); }
    .left.bicep { background-image: url({{ page.resource_url }}me/left-bicep.png); }

    .forearm {
        top: 108px; left: 14px;
        width: 36px; height: 121px;
        -webkit-transform-origin: 3px 7px;
    }

    .right.forearm {
        background-image: url({{ page.resource_url }}me/right-forearm.png);
        -webkit-animation-name: right-forearm;
    }

    .left.forearm {
        background-image: url({{ page.resource_url }}me/left-forearm.png);
        -webkit-animation-name: left-forearm;
    }

    .leg {
        left: 6px;
        -webkit-transform-origin: 30px 20px;
        -webkit-animation-name: thigh;
    }

    .right.leg {
        top: 235px;
        -webkit-animation-name: right-thigh;
    }

    .left.leg {
        top: 225px;
        -webkit-animation-name: left-thigh;
    }

    .thigh {
        width: 70px; height: 145px;
    }

    .left.thigh { background-image: url({{ page.resource_url }}me/left-thigh.png); }
    .right.thigh { background-image: url({{ page.resource_url }}me/right-thigh.png); }

    .shin {
        top: 115px;
        width: 50px; height: 170px;
        background-image: url({{ page.resource_url }}me/shin.png);
        -webkit-transform-origin: 30px 25px;
    }

    .right.shin { -webkit-animation-name: right-shin; }
    .left.shin { -webkit-animation-name: left-shin; }

    .foot {
        top: 155px; left: 2px;
        width: 67px; height: 34px;
        background-image: url({{ page.resource_url }}me/foot.png);
        -webkit-transform-origin: 0 50%;
    }

    .right.foot { -webkit-animation-name: right-foot; }
    .left.foot { -webkit-animation-name: left-foot; }

    .toes {
        top: 9px; left: 66px;
        width: 28px; height: 25px;
        background-image: url({{ page.resource_url }}me/toes.png);
        -webkit-transform-origin: 0% 100%;
    }

    .right.toes { -webkit-animation-name: right-toes; }
    .left.toes { -webkit-animation-name: left-toes; }

    .shadow {
        width: 200px; height: 70px;
        left: 270px; bottom: 5px;
        background-image: url({{ page.resource_url }}misc/shadow.png);
        -webkit-animation-name: shadow;
    }

    /* setting proper z-indexes so that limbs show up correctly */

    div.right.arm { z-index: 1; }
    div.left.arm { z-index: -3; }
    div.arm > div.bicep > div.forearm { z-index: -1; }

    div.right.leg { z-index: -1; }
    div.left.leg { z-index: -2; }
    div.leg > div.thigh > div.shin { z-index: -1; }

    /* animations */

    @-webkit-keyframes me {
        0% { -webkit-transform:   rotate(5deg) translate( 5px,   0px); }
        25% { -webkit-transform:  rotate(5deg) translate(-5px, -14px); }
        50% { -webkit-transform:  rotate(5deg) translate( 5px,   0px); }
        75% { -webkit-transform:  rotate(5deg) translate(-5px, -14px); }
        100% { -webkit-transform: rotate(5deg) translate( 5px,   0px); }
    }

    @-webkit-keyframes right-bicep {
        0%   { -webkit-transform: rotate(26deg); }
        50%  { -webkit-transform: rotate(-20deg); }
        100% { -webkit-transform: rotate(26deg); }
    }

    @-webkit-keyframes left-bicep {
        0%   { -webkit-transform: rotate(-20deg); }
        50%  { -webkit-transform: rotate(26deg); }
        100% { -webkit-transform: rotate(-20deg); }
    }

    @-webkit-keyframes right-forearm {
        0%   { -webkit-transform: rotate(-10deg); }
        50%  { -webkit-transform: rotate(-45deg); }
        100% { -webkit-transform: rotate(-10deg); }
    }

    @-webkit-keyframes left-forearm {
        0%   { -webkit-transform: rotate(-45deg); }
        50%  { -webkit-transform: rotate(-10deg); }
        100% { -webkit-transform: rotate(-45deg); }
    }

    @-webkit-keyframes right-thigh {
        0%   { -webkit-transform: rotate(-45deg); }
        50%  { -webkit-transform: rotate(10deg); }
        100% { -webkit-transform: rotate(-45deg); }
    }

    @-webkit-keyframes left-thigh {
        0%   { -webkit-transform: rotate(10deg); }
        50%  { -webkit-transform: rotate(-45deg); }
        100% { -webkit-transform: rotate(10deg); }
    }

    @-webkit-keyframes right-shin {
        0%   { -webkit-transform: rotate(30deg); }
        25%  { -webkit-transform: rotate(20deg); }
        50%  { -webkit-transform: rotate(20deg); }
        75%  { -webkit-transform: rotate(85deg); }
        100% { -webkit-transform: rotate(30deg); }
    }

    @-webkit-keyframes left-shin {
        0%   { -webkit-transform: rotate(20deg); }
        25%  { -webkit-transform: rotate(85deg); }
        50%  { -webkit-transform: rotate(30deg); }
        75%  { -webkit-transform: rotate(20deg); }
        100% { -webkit-transform: rotate(20deg); }
    }

    @-webkit-keyframes right-foot {
        0%   { -webkit-transform: rotate(-5deg); }
        25%  { -webkit-transform: rotate(-7deg); }
        50%  { -webkit-transform: rotate(-16deg); }
        75%  { -webkit-transform: rotate(-10deg); }
        100% { -webkit-transform: rotate(-5deg); }
    }

    @-webkit-keyframes left-foot {
        0%   { -webkit-transform: rotate(-16deg); }
        25%  { -webkit-transform: rotate(-10deg); }
        50%  { -webkit-transform: rotate(-5deg); }
        75%  { -webkit-transform: rotate(-7deg); }
        100% { -webkit-transform: rotate(-16deg); }
    }

    @-webkit-keyframes right-toes {
        0%   { -webkit-transform: rotate(0deg); }
        25%  { -webkit-transform: rotate(-10deg); }
        50%  { -webkit-transform: rotate(-10deg); }
        75%  { -webkit-transform: rotate(-25deg); }
        100% { -webkit-transform: rotate(0deg); }
    }

    @-webkit-keyframes left-toes {
        0%   { -webkit-transform: rotate(-10deg); }
        25%  { -webkit-transform: rotate(-25deg); }
        50%  { -webkit-transform: rotate(0deg); }
        75%  { -webkit-transform: rotate(-10deg); }
        100% { -webkit-transform: rotate(-10deg); }
    }

    @-webkit-keyframes shadow {
        0%   { opacity: 1; }
        25%  { opacity: 0.75; }
        50%  { opacity: 1; }
        75%  { opacity: 0.75; }
        100% { opacity: 1; }
    }

    .overlay {
        width: 100%; height: 100%;
        background: url({{ page.resource_url }}misc/gradient-left.png) repeat-y top left,
                    url({{ page.resource_url }}misc/gradient-right.png) repeat-y top right;
    }

    .sky div {
        background-repeat: no-repeat;
        -webkit-animation-name: prop-1200;
    }

    .cloud-1, .cloud-2 {
        width: 82px; height: 90px;
        background-image: url({{ page.resource_url }}clouds/1.png);
        -webkit-animation-duration: 120s;
    }

    .cloud-3, .cloud-4 {
        top: 70px;
        width: 159px; height: 90px;
        background-image: url({{ page.resource_url }}clouds/2.png);
        -webkit-animation-duration: 80s;
    }

    .cloud-5, .cloud-6 {
        top: 30px;
        width: 287px; height: 62px;
        background-image: url({{ page.resource_url }}clouds/3.png);
        -webkit-animation-duration: 140s;
    }

    .cloud-7, .cloud-8 {
        top: 100px;
        width: 94px; height: 81px;
        background-image: url({{ page.resource_url }}clouds/4.png);
        -webkit-animation-duration: 90s;
    }


    .cloud-1 { left: 0px; }
    .cloud-2 { left: 1200px; }

    .cloud-3 { left: 250px; }
    .cloud-4 { left: 1450px; }

    .cloud-5 { left: 500px; }
    .cloud-6 { left: 1700px; }

    .cloud-7 { left: 950px; }
    .cloud-8 { left: 2150px; }

    .horizon {
        top: 350px;
        width: 1800px; height: 50px;
        background: url({{ page.resource_url }}misc/horizon.png) repeat-x;
        -webkit-animation-name: prop-600;
        -webkit-animation-duration: 40s;
    }

    .ground div {
        background-repeat: no-repeat;
        -webkit-animation-name: prop-2000;
    }

    .sign-all-css {
        width: 160px; height: 188px;
        top: 325px; left: 1600px;
        background-image: url({{ page.resource_url }}signs/all-css.png);
        -webkit-animation-duration: 35s;
    }

    .sign-lots-of-divs {
        width: 102px; height: 120px;
        top: 345px; left: 1150px;
        background-image: url({{ page.resource_url }}signs/lots-of-divs.png);
        -webkit-animation-duration: 56s;
    }

    .sign-no-js {
        width: 65px; height: 77px;
        top: 348px; left: 1150px;
        background-image: url({{ page.resource_url }}signs/no-js.png);
        -webkit-animation-duration: 71s;
    }

    .sign-best {
        width: 43px; height: 50px;
        top: 350px; left: 1000px;
        background-image: url({{ page.resource_url }}signs/best.png);
        -webkit-animation-duration: 95s;
    }

    @-webkit-keyframes prop-600 {
        0%   { -webkit-transform: translateX(0px); }
        100% { -webkit-transform: translateX(-600px); }
    }

    @-webkit-keyframes prop-1200 {
        0%   { -webkit-transform: translateX(0px); }
        100% { -webkit-transform: translateX(-1200px); }
    }

    @-webkit-keyframes prop-2000 {
        0%   { -webkit-transform: translateX(0px); }
        100% { -webkit-transform: translateX(-2000px); }
    }

</style>

<p style="text-align: center">This experiment is only designed to work <b>only in webkit browsers</b> (chrome and safari).</p>

<div id="canvas">
    <div class="sky">
        <div class="cloud-1"></div>
        <div class="cloud-2"></div>
        <div class="cloud-3"></div>
        <div class="cloud-4"></div>
        <div class="cloud-5"></div>
        <div class="cloud-6"></div>
        <div class="cloud-7"></div>
        <div class="cloud-8"></div>
    </div>

    <div class="horizon"></div>

    <div class="ground">
        <div class="sign-best"></div>
        <div class="sign-no-js"></div>
        <div class="sign-lots-of-divs"></div>
        <div class="sign-all-css"></div>
    </div>

    <!-- skeleton and shadow -->
    <div class='shadow'></div>

    <div class='me'>

        <div class="torso">
            <div class="left leg">
                <div class="left thigh">
                    <div class="left shin">
                        <div class="left foot">
                            <div class="left toes"></div>
                        </div>
                    </div>
                </div>
            </div> <!-- left leg -->

            <div class="right leg">
                <div class="right thigh">
                    <div class="right shin">
                        <div class="right foot">
                            <div class="right toes"></div>
                        </div>
                    </div>
                </div>
            </div> <!-- right leg -->

            <div class="left arm">
                <div class="left bicep">
                    <div class="left forearm"></div>
                </div>
            </div> <!-- left arm -->

            <div class="right arm">
                <div class="right bicep">
                    <div class="right forearm"></div>
                </div>
            </div> <!-- right arm -->

        </div> <!-- torso -->
    </div> <!-- me -->

    <div class="overlay"></div>

</div> <!-- canvas -->

<p style="text-align: center"><a href="#canvas">Show</a> / <a href="#">Hide</a> Element Borders.</p>