Independent Web and Educational Software Developer
Mouse-over or touch/drag to interact with Bridge over a Pond of Water Lilies. Or, view fullscreen.
Bridge over a Pond of Water Lilies, painted in 1899, is one of my favorite impressionist paintings. It was done by the French painter Claude Monet, the leader of the French impressionist movement. He was a man after my own heart - the garden that you see in this painting is his own. He was an avid gardener and bought land with a pond, populating it with a landscape for his paintings. It shows up many times - this series has around 18 paintings in it.
One of the most fascinating things about Claude Monet, in my opinion, was his struggle with cataracts. This article has a lot of simulations of what Monet would have seen with his cataracts and how it affected his paintings. He also experimented with his own eyes, painting the same scene before and after his cataract surgery. Apparently he was so enraged by how his vision had changed that he sadly destroyed many of his older paintings.
This interactive turned out to be much harder to produce than I originally thought it would be. What you see here is the result of many failures and a lot of experimentation. The first three things I tried were not anywhere near as satisfying as I had hoped, so I scrapped them. Here's what didn't work:
Group the lily-pads and have them float together towards the front as if it were a river under the bridge.
This didn't work because the painting is called "Bridge over a POND ..." I scrapped it immediately.
When the lily-pads were generated, create them in patches and then have them float circularly and also spring around a home point. The patches worked rather well, but for some reason I had a world of trouble creating elliptical patches AND the interaction looked stupid. Lily-pads don't really spring exactly.
What I finally settled on, that required quite a bit of fine tuning, is a rudimentary flocking behavior.
The behavior of the lily-pads has a few components that I'll talk about. Feel free to skip this part if you're not into trigonometry or physics!
The basic "physics" of the lily-pads simply that they each have a position, velocity, and acceleration. The acceleration is the only thing that I modify, except to slow down the lily if it goes too fast. We don't want rogue lily-pads flying around.
Bounce off a neighbor. I loop through each pair of lilies without having duplicates:
// Bouncing behavior
for (var i = 0; i < lilypads.length - 1; i++) {
for (var j = i + 1; j < lilypads.length; j++) {
adjustLilies(lilypads[i], lilypads[j]);
}
}
And this is the adjustLilies function:
function adjustLilies(lily1, lily2) {
var dis = distance(lily1.x, lily1.y,
lily2.x, lily2.y);
var angle = Math.atan2(lily1.y - lily2.y,
lily1.x - lily2.x);
// Keep track of the distances and angles
if (lily1.nearestNeighbor.distance > dis) {
lily1.nearestNeighbor.distance = dis;
lily1.nearestNeighbor.angle = angle + Math.PI;
}
if (lily2.nearestNeighbor.distance > dis) {
lily2.nearestNeighbor.distance = dis;
lily2.nearestNeighbor.angle = angle;
}
if (dis > maxDist/3) {
// Too far away to worry about
return;
} else {
// if too close, bounce off eachother
//console.log('bonk',dis, maxDist);
lily1.ax += 0.09 * Math.cos(angle);
lily1.ax += 0.09 * Math.sin(angle);
lily1.hits++;
lily2.ax -= 0.09 * Math.cos(angle);
lily2.ay -= 0.09 * Math.sin(angle);
lily2.hits++;
}
}
What's going on here is that first, we keep track of the nearest neighbor to each lily. That will come in handy later. The angle between them is calculated by the Math.atan2() function. Next, the lilies bounce off of each other. In order that they bounce off at the correct angle, we take the sin and cosine of that angle and either add or subtract it, depending on the lily. I wound up having to play around with this. If you switch the += and the -=, you wind up having them accelerate past each other, which looks really strange. Pro tip.
Slow down the lily when it's not being hit or average the hit accelerations.
if (lily.hits > 0) {
// if it gets hit, average the acceleration
lily.ax /= lily.hits;
lily.ay /= lily.hits;
} else {
// slow down the velocity
//console.log("slowing down.");
lily.ax = -lily.vx/20;
lily.ay = -lily.vy/20;
}
Allow the user to interact with the lily/lily-pad. The lily-pad is repelled away from the mouse/touch point. You'll notice that the force is subtracted from the acceleration.
if (trackLoc.x > 0) {
// If tracking, jostle it!
var dist = distance(trackLoc.x, trackLoc.y,
lily.x, lily.y);
if (dist < maxDist) {
var intAngle = Math.atan2(trackLoc.y - lily.y,
trackLoc.x - lily.x);
lily.ax -= (maxDist - dist)/200 * Math.cos(intAngle);
lily.ay -= (maxDist - dist)/200 * Math.sin(intAngle);
}
}
Seek out a nearest neighbor if the lily is too far away. This is why we were keeping track of that information earlier. The force is attractive, rather than repulsive with the interaction.
// Seek out the nearest neighbor if you're lonely
if (lily.nearestNeighbor.distance > maxDist/2) {
//console.log("Finding a friend.");
lily.ax += maxDist/100 *
Math.cos(lily.nearestNeighbor.angle);
lily.ay += maxDist/100 *
Math.sin(lily.nearestNeighbor.angle);
}
Keep the lily in bounds
if (!pondBoundContains({x: lily.x, y:lily.y})) {
var centerAngle = Math.atan2(pondCenter.y - lily.y,
pondCenter.x- lily.x);
//console.log('Go back to center', lily.y, angle);
lily.ax += 0.5 * Math.cos(centerAngle);
lily.ay += 0.5 * Math.sin(centerAngle);
}
Float the lily. This code causes the lily to bounce up and down a little bit, and not at the same time as all the rest of them.
var yRatio = (lily.y * 10 - pondY)/pondHeight;
lily.ay += lily.baseSpeed *
0.005 * Math.sin(frame + yRatio);
Update the velocity, position, and scale (for faux 3D look).
lily.vx += lily.ax;
lily.vy += lily.ay;
// Scale it
var yScale = scaleAtY(lily.y);
lily.scale.set(lily.flip * yScale, yScale);
// Speed limit
if (Math.abs(lily.vx) > maxVelocity) {
lily.vx = Math.sign(lily.vx) * maxVelocity;
}
if (Math.abs(lily.vy) > maxVelocity) {
lily.vy = Math.sign(lily.vy) * maxVelocity;
}
lily.x += lily.vx;
lily.y += lily.vy;
// Reset acceleration.
lily.ax = 0;
lily.ay = 0;
Whew! That's pretty much it. There's also other stuff happening, but if you're curious about that, either look at the source code or email me.
Apparently my appreciation for this work goes back a long way. When I was in 3rd grade, I was a part of Odyssey of the Mind and we did a 4-foot-tall rendition of this painting to use as a backdrop. Awesome.
Bridge over a Pond of Water Lilies as painted by 9-year-olds.