Welcome to my recap of Day 8 of the free JavaScript 30 online video course by Wes Bos. In case you really have not yet heard of the course, quickly head over to it and get started right away! 🙂
Please refer to the JavaScript 30 archive for all recap posts.
Objective
Day 8 is fun! 😀 The task is to use JavaScript in order to paint with your mouse on an HTML5 <canvas>
element.
The provided starter files include not more than a single HTML file that again contains the canvas.
Today was different (for me), because I watched the video during a train ride (which was two weeks ago). This means that I saw already the final result of Wes—which was good, to be honest, for there was not really any detailed description of what should be accomplished. 🙂
Conception
Painting with the mouse should work as follows:
- moving the mouse should only result in painting when the pen is on the canvas (i.e., the left mouse button is clicked);
- leaving the canvas with the cursor stops painting (i.e., when leaving and re-entering again while still having the button pressed, no painting happens).
In addition to the basic functionality listed above, there are lots of possibilities regarding the fun and experimental part of today’s task (e.g., manipulating color and thickness of the pen).
Implementation
Setting things up is straightforward, and follows pretty much exactly what Wes did. We have our canvas that we use to listen for several events to realize the bullet list from the conception. We also cache the canvas context, which is where the painting will happen. The according code looks like this:
const canvas = document.getElementById( 'draw' );
const context = canvas.getContext( '2d' );
let penDown;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
context.lineCap = 'round';
context.lineJoin = 'round';
function paint( position ) {
if ( ! penDown ) {
return;
}
// Painting code goes here...
}
canvas.addEventListener( 'mousemove', ( e ) => paint( [ e.offsetX, e.offsetY ] ) );
canvas.addEventListener( 'mousedown', () => penDown = true );
canvas.addEventListener( 'mouseup', () => penDown = false );
canvas.addEventListener( 'mouseleave', () => penDown = false );
Since we only need the position data from the mousemove
event, we don’t pass the (whole) object but only the two offset values, combined in a single array.
We now have to take care of initializing the starting position both when starting to paint (i.e., right after pressing the left mouse button) and while painting. Setting the position after a mouse click means we have to give the arrow function a body now. The updated code then looks like so:
let lastPosition;
function paint( position ) {
if ( ! penDown ) {
return;
}
// Painting code goes here...
lastPosition = position;
}
canvas.addEventListener( 'mousedown', ( e ) => {
penDown = true;
lastPosition = [ e.offsetX, e.offsetY ];
} );
The actual painting consists of preparing pen and canvas, and drawing a line from the last to the current position. In code, this would be the following:
function paint( position ) {
if ( ! penDown ) {
return;
}
context.beginPath();
context.moveTo( ...lastPosition );
context.lineTo( ...position );
context.stroke();
lastPosition = position;
}
In the above code, you can see two times spreading into a function (e.g., passing the two-item array lastPosition
to the moveTo()
method, which actually has two parameters). ES6 ftw! 😀
What’s missing now is the fun, right? 😉
I followed Wes’s example and manipulated both color and thickness of the pen, by using two separate (arrow) functions that are executed in individual intervals. Like so:
let hue = 0;
setInterval( () => penDown || ( context.strokeStyle = `hsl( ${hue = ( hue + 1 ) % 360}, 50%, 50%)` ), 25 );
setInterval( () => penDown || ( context.lineWidth = ( context.lineWidth % 72 ) + 1 ), 50 );
The color is manipulated by means of its hue—which is updated on the fly inside the template string—while using a fixed value of 50% for both saturation and lightness. The thickness of the pen, also updated on the fly, ranges from 1 to 72 pixels.
The expression penDown || something()
is short for if ( ! penDown ) something();
, and by writing it like this, all fits in a single line, without the need for a function body. The idea behind this is that I do not want to manipulate any further while drawing—which is different from what Wes did.
All in all, the code looks like the following:
const canvas = document.getElementById( 'draw' );
const context = canvas.getContext( '2d' );
let hue = 0;
let lastPosition;
let penDown;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
context.lineCap = 'round';
context.lineJoin = 'round';
function paint( position ) {
if ( ! penDown ) {
return;
}
context.beginPath();
context.moveTo( ...lastPosition );
context.lineTo( ...position );
context.stroke();
lastPosition = position;
}
canvas.addEventListener( 'mousemove', ( e ) => paint( [ e.offsetX, e.offsetY ] ) );
canvas.addEventListener( 'mousedown', ( e ) => {
penDown = true;
lastPosition = [ e.offsetX, e.offsetY ];
} );
canvas.addEventListener( 'mouseup', () => penDown = false );
canvas.addEventListener( 'mouseleave', () => penDown = false );
setInterval( () => penDown || ( context.strokeStyle = `hsl( ${hue = ( hue + 1 ) % 360}, 50%, 50%)` ), 25 );
setInterval( () => penDown || ( context.lineWidth = ( context.lineWidth % 72 ) + 1 ), 50 );
Refinement
I didn’t (have/want to) do any refinement, but there are a few (minor) differences between my code and the suggested solution. First of all, instead of mouseout
, I listen to the mouseleave
event. In this context, there isn’t really a difference (regarding the functionality).
Second, when I query an element with an ID, I (still) use .getElementById()
—because it works, does exactly what I want in this case, and it is faster than using .querySelector()
.
Like mentioned before, my fun part is a little different—otherwise it wouldn’t be fun at all, right? 🙂
I didn’t use two variables to keep track of the last position, but only a single array, and thus spreading.
One major difference is that my painting or drawing function is not directly attach to the (mouse) event, but called from within the arrow function that serves as event handler. The reason is that I can now also directly call paint()
with an arbitrary position array, and everything works just fine (if in painting mode). You could therefore also use the function to realize touch- or swipe-based painting. The touchmove
event, for example, does not have offset*
properties, so you cannot use Wes’s current function to handle that.
Live Demo
Want to see this in action? Well, here is a live demo. 🙂
Retrospection
I never interacted with an HTML5 canvas element before, so that was cool.
Besides that, there wasn’t much news. But every (no matter how small!) project means learning, internalizing, and practice. And I really like it to find appropriate use cases for all the great things ES6 has to offer.
And You?
JavaScript 30 is free! So, unless you already did this on your own, what’s your reason not to? 🙂
Leave a Reply