Close
Simone Seagle

Simone Seagle

Independent Web and Educational Software Developer

Site Information

Search

Mouse-over or touch/drag to interact with the trees in Magnolias and Irises. Or, view fullscreen.

About the Art

Louis Comfort Tiffany is well known for his amazingly colorful stained glass. He was born in February 1848, the son of the famous Charles Lewis Tiffany who started the eponymous jewelry company Tiffany & Co. Originally Tiffany wanted to be a painter, but soon found that his talents were put to better use making stained glass and other beautiful household objects. He was even hired as an interior designer for the White House before President Chester Arthur moved in in 1882! Sadly, FDR removed pretty much all of his designs in 1902.

This piece of art, called Magnolias and Irises (for obvious reasons) is part of the Metropolitan Museum of Art's Open Access Art Collection. It was designed by Tiffany and manufactured by his glass company, Tiffany Studios. It was originally installed in a mausoleum in a Brooklyn cemetery but was later donated anonymously to the museum.

About the Programming

Whew. I've actually been working on this piece on and off for almost two months. I'd work on it here and there, get frustrated, go away, and come back. Consequently the code for this one is much more complex than for most of the other interactive art pieces that I've done. The basic gist is that the trees are (roughly) fractal trees using a basic branching algorithm. They aren't true fractal trees because the branches are made from semi-random sprites. If you want perfect fractal trees, check out The Nature of Code.

I first fell in love with Fractal trees for my Algorithms class in 2015. We talked about the Nature of Code and how to create fractals, and I stumbled upon this video which I totally fell in love with:

That was the inspiration for Movie Trees project, which unfortunately isn't working now because I need to fork over some money to the OMDB foundation. Anyway, fast forward to 2017 and I found a Louis Comfort Tiffany featuring beautiful magnolia trees. I wondered if I could make his trees interactive as well, but obviously they're not perfect fractals... no trees are.

I wound up working with my original algorithm, still including the recursion, but using sprites that I cut out from the painting instead of drawing lines. It took a lot of trial and error to get the trees to appear the way I wanted them to. Honestly, I still feel like they need tweaking. The code is too complicated to include in its entirety, but here are a few snippets. Feel free to look at the source if you're curious.

First, the trees are created recursively using different sprites for the branches. I found early on that it was important to make sure the wider branches never came after thinner ones. Duh, I guess.

function drawBranch(parent, percent, angle) {
    branchCount++;
    // this selects only branch sprite images which are thinner than the parent 
    var whichImg = getRandomBranchImg(parent.branchWidth);
    var branch = PIXI.Sprite.fromImage('../assets/living-art/'
                           + branchData[whichImg].image);

    // This is so we can update recursively later
    parent.childBranches.push(branch);
    branch.parentBranch = parent;
    branch.percent = percent;
    branch.scale.set(scale);
    branch.anchor.set(0.5,1);

    // Place the branch correctly at the end of its parent.
    branch.x = parent.x + (parent.branchLength * percent) 
                    * Math.sin(parent.rotation);
    branch.y = parent.y - (parent.branchLength * percent)
                    * Math.cos(parent.rotation);

    // We need to pass this info down to the children
    branch.branchLength = branchData[whichImg].length * scale;
    branch.branchWidth = branchData[whichImg].width * scale;
    branch.childBranches = [];

    branch.baseRotation = angle;    
    // Constrain how much it can move
    branch.minimumRotation = angle - maxAngleDelta;
    branch.maximumRotation = angle + maxAngleDelta;                 
    branch.rotation = angle;

    // calculating the wind effect for the branch
    var branchWidthFraction = (maxBranchWidth - branch.branchWidth)/maxBranchWidth;
    branch.windEffect = windEffectFraction * branchWidthFraction * branchWidthFraction;

    treeContainer.addChild(branch);

    // Decide if this branch of the tree needs to be truncated or not
    var truncate = Math.random() < .4 && branch.branchWidth < 18 * scale 
                                ? true : false;
    var twoBranches = Math.random() < .8;
    if (branch.y > topBuffer 
        && !truncate 
        && branch.x < size.width + 200) {
        // if it's far enough from the top, draw two more branches
        var angleMod = 1 + (39 - branchData[whichImg].width) / 55;

        // Recursion FTW!
        drawBranch(branch, 0.9, -1*randomAngle()*angleMod);

        if (twoBranches)
            drawBranch(branch, 0.9, randomAngle()*angleMod);
    } else {
        // Add a flower at the last branch
        drawFlower(branch, randomAngle());
    }
}

Flowers are drawn pretty much the same way, so I'll skip that. Primary distance is that for obvious reasons, flowers don't have children so the branch ends there.

Branches and flowers are updated every frame according to windspeed and distance from interaction.

/**
 * Updates a branch or flower for each frame including the target angle
 * and the effect from the wind
 */
function updateBranch(branch) {
    branch.x = branch.parentBranch.x + 
        (branch.parentBranch.branchLength * branch.percent) 
                    * Math.sin(branch.parentBranch.rotation);
    branch.y = branch.parentBranch.y - 
        (branch.parentBranch.branchLength * branch.percent)
                    * Math.cos(branch.parentBranch.rotation);

    // Get either the base angle or the angle to the 
    var angle = getSpriteTargetAngle(branch);

    branch.rotation = angle + branch.windEffect * windSpeed;

    // Recursive updating
    branch.childBranches.forEach(updateBranch);
}

/**
 * Gets a target angle for the sprite (either branch or flower)
 *  to either follow a point or ease back to the base angle
 */
function getSpriteTargetAngle(sprite) {
    var targetAngle = sprite.baseRotation;

    // if the tracking location is active and close to the branch...
    if (trackLoc.x > 0) {
        if (distance(trackLoc.x, trackLoc.y, sprite.x, sprite.y) < maxDist) {

            var dx = trackLoc.x - sprite.x;
            var dy = trackLoc.y - sprite.y;

            // We don't want the branch to go completely straight, so keep it within some reasonable limits
            var intAngle = Math.atan2(dy, dx) + Math.PI/2;
            targetAngle = Math.min(sprite.maximumRotation, 
                                   Math.max(sprite.minimumRotation, intAngle));
        }

    }

    var delta = sprite.rotation - targetAngle;

    // ease into the target angle
    if (Math.abs(delta) > 0.01) {
        return sprite.rotation - delta/20;
    } else {
        return sprite.rotation;
    }
}

There is some more fiddly stuff in the code, but those functions contain most of the meat.