Independent Web and Educational Software Developer
Mouse-over or touch/drag to interact with the Eight of Combustion. Or, view fullscreen.
Collage Artwork © Paula Millet 2017.
One of my friends/former coworkers, Paula Millet, created a set of amazing tarot cards from collages of public domain artwork - she calls them Boadicea's Tarot of Earthly Delights. Ever since she started making them, I've had the itch to animate one. They're all beautiful and astounding pieces of art. After making sure it was okay with her, I settled on the Eight of Combustion, although plenty more would work.
Here's what Paula says:
You may as well shoot for the moon because things are certainly taking off!
The suite of Combustion corresponds to the element fire, inspiration and energy, as well as hard-working, forceful people who are capable of turning ideas into actions.
Eights represent a plateau of accomplishment and thorough understanding: a level both stabile, yet complex.
Interesting coincidence, because I feel like that is what I aspire to, and I picked the piece before I even read the blurb.
When I first saw this card, I was really drawn to the moon and the fireworks. Getting the moon to work was no big deal, and I've already covered the following eyes tricks a couple times.
The fireworks/meteors proved to be more problematic than I had originally anticipated. I spent a lot of time trying to get the same delicate appearance that they had in the original collage, but also have the motion that a shooting streamer would have. Therefore, I started with the physics of a projectile launching at a given speed at a given angle:
x(t) = velocity0 * cosine(launch angle) * t
y(t) = velocity0 * sine(launch angle) * t - 1/2 gt2
In order to get them to follow the arc of their path, I used the PIXI rope mesh for each one of the meteor images. If I didn't do that, it would look really stupid. So, each point of the meteor's mesh (there's 10 on each one) has a different "time" going along with it.
BUT that wasn't enough for me. I also needed to have some interaction. The distance from the mouse alternatively attracts and repels points on the mouse, which makes them zig-zag, and it also changes the instantaneous "launch angle" at each frame.
Finally, I gave each meteor a glowing ember at the front of it that begins to appear whenever it starts to fall back to earth. The ember appears for a set number of frames, eventually burning out and disappearing. This means the update function for each projectile is pretty complicated. Here's an abbreviated version with some pseudocode because the real function is embarrassingly long (120 lines - I swear I don't usually do that).
function updatemeteor(meteor) {
var meteorIntForce = {x: 0, y: 0, phi:1};
for (var i = 0; i < meteor.points.length; i++) {
// Interact with the meteor
if (trackLoc.x > 0) {
var adjustedTrac = {x: trackLoc.x - meteor.x,
y: trackLoc.y - meteor.y};
// If tracking, jostle it!
var dist = distance(adjustedTrac.x, adjustedTrac.y,
meteor.points[i].x, meteor.points[i].y);
if (dist < maxDist) {
var intAngle = Math.atan2(adjustedTrac.y - meteor.points[i].y,
adjustedTrac.x - meteor.points[i].x);
var distanceMod = Math.sqrt(maxDist - dist);
var signInteraction = i % 2 == 0 ? 1 : -1;
meteorIntForce.x = signInteraction * distanceMod/5
* Math.cos(intAngle);
meteorIntForce.y = signInteraction * distanceMod/5
* Math.sin(intAngle);
// Also adjust the v0, but only use the first point's value
if (i == 0) {
signInteraction = Math.sign(meteor.points[i].y - trackLoc.y);
meteorIntForce.phi = signInteraction * distanceMod/500 + 1;
}
}
}
// Yay trajectory!
var t = (frame - meteor.startFrame) - (i*20);
meteor.points[i].x = meteor.v0 * t
* Math.cos(meteor.startAngle * meteorIntForce.phi)
+ meteorIntForce.x;
meteor.points[i].y = meteor.v0 * t
* Math.sin(meteor.startAngle * meteorIntForce.phi)
- 0.5 * gravity * t * t + meteorIntForce.y;
// Yay calculus! Take the derivative of position to find the velocity. I haven't done that in a while. :-)
var yVel = meteor.v0 * Math.sin(meteor.startAngle) - gravity * t;
if (i == 0 && !meteor.hasGlow && Math.abs(yVel) < 0.01) {
// -- Make a new meteor glow sprite --
}
}
if (meteor.hasGlow) {
if (meteor.glow.frames < maxGlowFrames) {
meteor.glow.x = meteor.points[0].x;
meteor.glow.y = meteor.points[0].y;
meteor.glow.scale.set(0.25 + meteor.glow.frames/300);
var adjF = Math.abs(maxGlowFrames/2 - meteor.glow.frames);
// Lerp up and down
meteor.glow.alpha = glowM * adjF + 0.85;
meteor.glow.frames++;
}
else {
meteor.removeChild(meteor.glow);
meteor.hasGlow = false;
meteor.glow = null;
}
}
if (yVel > 1.4 && meteor.alpha > 0) {
// Fade it out
meteor.alpha = meteor.alpha - 0.01;
}
if (meteor.alpha <= 0) {
// Reset the meteor back to its start point //
}
}
That wound up being so long because I kept playing with all different parameters until I was happy with how it looked.