#Floating Letters

This is a demo I put on the homepage of my old portfolio site, found here. This is a simple particle system demo, with a noticable catch: clicking the "unlock" button adds every letter on the homepage into the simulation.

The cool thing about this demo is that I don't hardcode in the position of these letters. There's a function that procedurally generates recreates each letter as an entity in the simulation.

Basic Concepts

Particle System Physics

I've addressed the basic physics behind particle systems at a number of other pages, including the Fabric demo page.

Here's a quick rundown of how it works. We know from basic physics that acceleration is a change in velocity and that velocity is a change in position. Since we're running a simulation, we can throw away all units and just deal with these values from an abstract standpoint.

Therefore, all we need to do to obtain a basic physics simulation is store position, velocity, and acceleration values, and each frame we add acceleration to velocity and add velocity to position.

For instance, we might have something like this:

objects = [...];

loop() {
    for(var i=0; i<objects.length; i++) {
        object[i].x+=object[i].vx;
        object[i].y+=object[i].vy;
        object[i].vx+=object[i].ax;
        object[i].vy+=object[i].ay;

        drawObject(object[i].x, object[i].y);
    }

    loop();
}

We'll get into the actual implementation later.

CSS Positioning

The primary difficulty with this demo as opposed to other particle demos is that we want each object to still work as an HTML element. We're not just using canvas to draw text objects. We want this:

letter gif

This means that we have to use CSS to handle positioning.

CSS positioning is a little bit odd. If we say top:10px - which top is that? Is that top relative to the parent element? Which parent? Relative to the document?

Here's the key rule: whenever position:absolute is set, the position is set relative to the closest parent element with position set. That's a bit of a mouthful. Let's use the following HTML as an example:

<body>
    <div>
        <p>Hello!</p>
    </div>
</body>

Now take the following CSS:

body {
    position:relative;
}
div {
    margin-top:100px;
}
p {
    position:absolute;
    top:100px;
}

In this case, the <p> will render at 100px because the closest parent with position set is the <body>.

Now consider the following example:

body {
    /* same result as if we set:
    position:relative;
     */
}
div {
    position:relative;
    margin-top:100px;
}
p {
    position:absolute;
    top:100px;
}

In this case, the <p> will render at 200px because 100px will be relative to the margin of 100px set by the <div>, which is the closest element with position set. The same is true no matter what we set for position of <body>, because the <div> is a closer parent.

With this information in mind, we can finally move to the architecture of the demo.

Demo Architecture

Here's what the final code looks like:

var locations=[];
$(".wrapper li a").each(function(){
    locations.push($(this).attr("href"));
});
//this is complicated
function unlock() {
    var unlinked=[];
    var links=[];
    locked=false;
    if(!unlockedbefore) {
        $(".objects").show();
        var ocount=0;
        //parse and generate objects
        var contents = [];
        var iterator=0;
        $(".wrapper li").each(function() {
            //create spans around every non-space character
            var str = $(this).text();
            //if the first character is a space, just put the entire thing into a span
            $(this).text("");
            if(str.substring(0,1)==" ") {
                var span = $('<span>'+str+"</span>");
                span.attr("id",ocount);
                $(this).append(span);
                unlinked.push(span.attr("id"));
                ocount++;
            } else { //if it's not, create a span for every character
                var chars = str.split("");
                var link=[];
                link.push(locations[iterator]);
                console.log(iterator,locations[iterator]);
                iterator++;
                for(var i=0; i<chars.length; i++) {
                    var span = $('<span>'+chars[i]+"</span>");
                    span.addClass("colored");
                    span.attr("id",ocount);
                    link.push(span.attr("id"));
                    $(this).append(span);
                    ocount++;
                }
                links.push(link);
            }
        });

        //do the same for h1
        var logochars = $(".wrapper h1").text().split("");
        $(".wrapper h1").text("");
        for(var i=0; i<logochars.length; i++) {
            var span = $('<span>'+logochars[i]+"</span>");
            span.attr("id",ocount);
            span.addClass("bigtext");
            unlinked.push(span.attr("id"));
            $(".wrapper h1").append(span);
            ocount++;
        }
        var bigger=[];
        var dashes=[];

        // parse object data into arrays
        $("span").each(function() {
            if($(this).text()!=" ") {
                contents.push($(this).text());
                var oposition = $(this).offset();
                var ox=oposition.left;
                var oy=oposition.top;
                objects.push([$(this).attr("id"),[ox, oy]]);
                if($(this).hasClass("bigtext")) { 
                    bigger.push($(this).attr("id"));
                }
                $(this).attr("id","null");
                objectvelocities.push([0,0]);
            }
        });

        //parse arrays into document objects
        var linkcount=0;
        for(var i=0; i<objects.length; i++) {
            var object=$("<span>"+contents[i]+"</span>");
            object.attr("id",objects[i][0]);
            object.addClass("object");

            if(contents[i]!=" - ") {
                object.addClass("colored");
            } 
            else { 
                dashes.push(object.attr("id"));
                $(".objects").append(object);
            }
            $("#"+objects[i][0]).css("left",objects[i][1][0]+"px").css("top",objects[i][1][1]+"px");

            $(".objects").append(object);
        }
        //insert links around spans
        for(var i=0; i<links.length; i++) {
            var currentlink = $('<a href="'+links[i][0]+'" id="l'+linkcount+'"></a>');
            $(".objects").append(currentlink);
            for(var j=1; j<links[i].length; j++) {
                $("#l"+linkcount).append($("#"+links[i][j]));
            }
            linkcount++;
        }

        //style the added elements
        for(var i=0; i<bigger.length; i++) {
            $("#"+bigger[i]).css("font-size","4em").css("color","white")
                .css("font-family","'Raleway'").css("font-weight","300");
        }
        for(var i=0; i<dashes.length; i++) {
            $("#"+dashes[i]).css("font-weight","400");
        }

        $(".wrapper").hide();
        unlockedbefore=true;
        originalobjects=copyObjects(objects);
    }
}

The magic of this is that we can define a document using normal HTML:

<div class="wrapper noselect">
    <h1>ALAN LUO</h1>
    <ul class="social">
        <li><a href="#">GITHUB</a></li><li> - </li>
        <li><a href="#">LINKEDIN</a></li><li> - </li>
        <li><a href="#">FACEBOOK</a></li><li> - </li>
        <li><a href="#">TWITTER</a></li><li> - </li>
        <li><a href="#">EMAIL</a></li>
    </ul>
    <ul class="professional">
        <li><a href="#">CODE GALLERY</a></li><li> - </li>
        <li><a href="#">CV</a></li>
    </ul>
</div>

And the code will make everything just fit together. This makes it easier if I ever want to make changes to the text.

Here's a basic psuedocode explanation of how the code works:

First, at the layer of the document relative to which we want to place the elements (the parent element that has position:relative set), we duplicate this element and use it as an empty container.

Then, in the original container, we take every character and wrap it in <span> elements.

Then, for each of these resulting <span> elements, we calculate the offset in the original container, and create a new element in the duplicate container with position:absolute using this calculated offset.

Then, we wrap these resulting spans in <a> tags in order to make the links work when the characters are separated.

Finally, we hide the original container, leaving only the duplicate container with absolutely positioned <span> elements. From here, we can handle the <span> elements the same way we would handle particles.