Disclaimer - Do not expect this code to magically work in your application. The code in this post is meant to explain the concepts behind how beams and bolts are rendered in Inventaverse, not as a copy paste resource.
1.21 Gigawatts!
There comes a time in one's life when it becomes necessary to obliterate the enemy with more than just beams. If we want to ensure that every atom of matter has been thoroughly and properly destroyed, we want to use 1.21 Gigawatts of power to do it!In this article I'll go over the concepts behind creating a simple laser beam in Javascript. Then I'll show you how we can spice up our laser beam by segmenting it and turning it into an awesome lightning bolt!
Lasers... an elegant weapon for a more civilized age
That's right, lasers! Or, in our case, a couple of lines that alternate colors rapidly so as to give the impression of being a laser. Here's how we do it:
With the grace of the great drawing artist Escher, we'll begin with a single line.
Our renderLaser function takes starting point, slope and distance. We'll describe our line using it's slope as a parameter instead of supplying the start x,y and end x,y because we're going to do something with that later.
Nothing to it, if you've wired up your canvas correctly, and you've correctly called this function then you should have a nice solid line that looks something like this line from Inventaverse:
Now, add one megajoule of pulsating laser power. We'll alter our renderLaser function slightly by alternating it's color every frame between two colors supplied as a parameter, by using the modulus operator % (secretly the most useful operator for sprite animation, in the universe).
You may have noticed an additional variable hanging off the window object here (which is generally a bad practice, but for the sake of this exercise, it's fine), frameCounter. This is just a count of the total number of frame drawn since the script was loaded. Simply increment this every time requestAnimationFrame calls your drawing function and your laser will alternate between two supplied colors quite nicely.
Let's animate one more property of our line, the width.
Here's what it ends up looking like! Not too shabby eh?
Obviously the slowest part is the iteration. Javascript is perfectly capable of executing 500 iterations in far less than a millisecond, so expecting it to do it in under 16.67 ms is perfectly reasonable. However, there is certainly room for improvement. Generating a random number is slow.
Back in the day, when we wanted to avoid doing expensive computations at run time, we would generate a pool of results ahead of time, and then perform a quick lookup. This is faster than generating individual random numbers inline by a degree of magnitude.
Also, a basic for loop executes about 10 times faster than a forEach. Sorry hipsters! Save your fancy ES6 array functions for one-time situations, not real time graphics scenarios.
Make these minor modifications to your lightning bolt and you'll shave the render time down to practically nothing!
One final note: you could make your renderLaser function a little easier to call into by wrapping all it's parameters into one laser configuration json object, and adding a default parameter object. Sometimes when you end up with 4+ parameters, things start looking a little crazy.
Using the techniques I've covered in this article, we can create some pretty amazing lightning effects, see for yourself:
At this point, the only question remaining is: How do you create a creature that is so awesome that it would necessitate using our badass new lightning bolt to destroy it?
But that my friends, is a story for anther day.
Until next time...
With the grace of the great drawing artist Escher, we'll begin with a single line.
Our renderLaser function takes starting point, slope and distance. We'll describe our line using it's slope as a parameter instead of supplying the start x,y and end x,y because we're going to do something with that later.
1 2 3 4 5 6 7 8 9 10 11 12 | function renderLaser(start, slope, distance) { // first, we'll calculate the endpoint. let end = { X : start.X * slope * distance, Y : start.Y * slope * distance } context.beginPath(); context.moveTo(start.X, start.Y); context.lineTo(end.X, end.Y); context.stroke(); } |
Nothing to it, if you've wired up your canvas correctly, and you've correctly called this function then you should have a nice solid line that looks something like this line from Inventaverse:

Now, add one megajoule of pulsating laser power. We'll alter our renderLaser function slightly by alternating it's color every frame between two colors supplied as a parameter, by using the modulus operator % (secretly the most useful operator for sprite animation, in the universe).
1 2 3 4 5 6 7 8 9 10 11 12 13 | function renderLaser(start, slope, distance, colors) { // first, we'll calculate the endpoint. let end = { X : start.X * slope * distance, Y : start.Y * slope * distance } context.beginPath(); context.strokeStyle = window.frameCounter % 2 === 0 ? colors[0] : colors[1]; context.moveTo(start.X, start.Y); context.lineTo(end.X, end.Y); context.stroke(); } |
You may have noticed an additional variable hanging off the window object here (which is generally a bad practice, but for the sake of this exercise, it's fine), frameCounter. This is just a count of the total number of frame drawn since the script was loaded. Simply increment this every time requestAnimationFrame calls your drawing function and your laser will alternate between two supplied colors quite nicely.
Let's animate one more property of our line, the width.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | function renderLaser(start, slope, distance, colors, widths) { // first, we'll calculate the endpoint. let end = { X : start.X * slope * distance, Y : start.Y * slope * distance } context.beginPath(); context.strokeStyle = window.frameCounter % 2 === 0 ? colors[0] : colors[1]; context.lineWidth = window.frameCounter %2 === 0 ? widths[0] : widths[1]; context.moveTo(start.X, start.Y); context.lineTo(end.X, end.Y); context.stroke(); } |
Now that we're all on the same page...
At this point, you're right about where I was at when I decided I wanted some of the weapons in Inventaverse to do more than just fire a beam. I wanted a little more sizzle, and some variety.
So, how do we turn lasers into lightning bolts?
Well, there's more than one way to discharge a massive bolt of electricity, and the way that made the most sense to me was this:
So, how do we turn lasers into lightning bolts?
Well, there's more than one way to discharge a massive bolt of electricity, and the way that made the most sense to me was this:
- break our beam up into lots of little line segments.
- at each of the points where the line segments touch, jitter the x and y coordinates such that the line is no longer straight.
- season to taste.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | function renderLightning(start, slope, distance, colors, widths) { const nodes = 500; const cracle = 16; let bn = []; let scalar = 0; // pick a bunch of points on our line and jitter them up! for(let n = 0; n < nodes; n++) { scalar += (Math.random() * ((distance-scalar)/nodes)); let r = { X : (Math.random() * crackle) - crackle/2, // jitter the x a little Y : (Math.random() * crackle) - crackle/2 // jitter the y a little }; bn.push({ X : slope * scalar + r.X, Y : slope * scalar + r.Y }); } context.beginPath(); context.strokeStyle = window.frameCounter % 2 === 0 ? colors[0] : colors[1]; context.lineWidth = window.frameCounter %2 === 0 ? widths[0] : widths[1]; context.moveTo(start.X, start.Y); bn.forEach((v, i) => { context.lineTo(v.X, v.Y); }); context.stroke(); } |
Here's what it ends up looking like! Not too shabby eh?
Lightning Speed
Alright, we have lightning, but it's... slow lightning... Let's see what we can do to speed this up.Obviously the slowest part is the iteration. Javascript is perfectly capable of executing 500 iterations in far less than a millisecond, so expecting it to do it in under 16.67 ms is perfectly reasonable. However, there is certainly room for improvement. Generating a random number is slow.
Back in the day, when we wanted to avoid doing expensive computations at run time, we would generate a pool of results ahead of time, and then perform a quick lookup. This is faster than generating individual random numbers inline by a degree of magnitude.
Also, a basic for loop executes about 10 times faster than a forEach. Sorry hipsters! Save your fancy ES6 array functions for one-time situations, not real time graphics scenarios.
Make these minor modifications to your lightning bolt and you'll shave the render time down to practically nothing!
One final note: you could make your renderLaser function a little easier to call into by wrapping all it's parameters into one laser configuration json object, and adding a default parameter object. Sometimes when you end up with 4+ parameters, things start looking a little crazy.
Using the techniques I've covered in this article, we can create some pretty amazing lightning effects, see for yourself:
At this point, the only question remaining is: How do you create a creature that is so awesome that it would necessitate using our badass new lightning bolt to destroy it?
But that my friends, is a story for anther day.
Until next time...