Create a digital Etch A Sketch

In this tutorial, we take the mechanical drawing toy Etch A Sketch as an inspiration and attempt to implement these features for modern devices, with web technologies. Using the (aptly named) canvas, we first focus on tablets, which are akin in shape to the authentic toy. We can take advantage of touch events to control the dials, and device motion events to erase the content. Not leaving phones out, we will also explore how to use WebSockets to enhance the possibilities by splitting the controls and drawing area.

01. Get the assets

This tutorial will use Node.js. Before we get started, go to FileSilo, select Free Stuff and Free Content next to the tutorial – here you can download the assets you need for the tutorial. Then run the commands below, which will install the dependencies and launch the server. We're using Node to create a localhost, and it will also serve us later for WebSockets.

npm install
node server/index.js

02. Use the draw() function

In main.js, the draw() function will be the centre point of our application. We use canvas to draw a line between two points; the origin (x1, y1), being where we last left our drawing, and the destination (x2, y2), the new point we want to reach. We now need to trigger this function to observe any form of drawing.

function draw(x1, y1, x2, y2) {
  //context is set globally in init()
  context.beginPath();
  context.moveTo(x1, y1);
  context.lineTo(x2, y2);
  context.stroke();
}

03. Implement keyboard events

Before we implement dials, let's quickly add a keyboard listener that can trigger our draw function. You've already been provided with the different keyCodes in the example, but you will need to amend the listener slightly to trigger the draw() function we defined previously. Now refresh your browser and see what you can draw with the arrow keys.

document.addEventListener('keydown', function(e) {
  /*keyCode switch goes here*/
  draw(Math.floor(prev_horizontal), Math.floor(prev_vertical), Math.floor(horizontal), Math.floor(vertical));
  prev_vertical = vertical;
  prev_horizontal = horizontal;
});

04. Resize the canvas

You may have noticed that our canvas element doesn't have a size assigned to it yet. For our drawing board, we will want a bigger space, maybe even the whole window. The code below takes care of the resize event, but don't forget to call adjustFrame() in init() as well.

function adjustFrame() {
  //canvas is defined globally in init()
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
}
window.addEventListener('resize', adjustFrame);

05. Add a frame

We want the application to look like the original toy as much as possible, so we want to add a frame around the drawing area. To do so, we can define a margin value and amend the CSS for #sketch to margin: 20px auto; to centre the canvas horizontally and keep a bigger space at the bottom for the dials.

var frameMarginVertical = 122;
var frameMarginHorizontal = 62;
function adjustFrame() {
  canvas.width = window.innerWidth - frameMarginHorizontal;
  canvas.height = window.innerHeight - frameMarginVertical;
}

06. Create the dials

We've already given you the CSS for the dials in public/css/styles.css, so feel free to have a look. Next, add two <div> tags under the <canvas> in the HTML file, as described below. As a convention, we will use the left dial for horizontal drawing, and the right for vertical. We're also adding new variables to the init() function to prepare for touch events.

<div id="dialHorizontal" class="dial"></div>
<div id="dialVertical" class="dial"></div>
var targetLeft = document.getElementById('dialHorizontal');
var regionLeft = new ZingTouch.Region(targetLeft);
var targetRight = document.getElementById('dialVertical');
var regionRight = new ZingTouch.Region(targetRight);

07. Use ZingTouch

The canvas with added dials, tied to the draw() function

ZingTouch is a JavaScript library capable of detecting various touch gestures, and will also handle mouse events. It is provided for you in the /public/lib/ folder, as we use it to control our dials. Below is the implementation for the left control; you will need to replicate and amend it for the other side.

regionLeft.bind(targetLeft, 'rotate', function(e) {
  if(e.detail.distanceFromLast < 0) {
  --horizontal;
  } else {
  ++horizontal;
  }
  angleHorizontal += e.detail.distanceFromLast;
  targetLeft.style.transform = 'rotate(' + angleHorizontal + 'deg)';
  draw(Math.floor(prev_horizontal), Math.floor(prev_vertical), Math.floor(horizontal), Math.floor(prev_vertical));
  prev_horizontal = horizontal;
});

08. Implement dial bounds

To block the lines from going off-screen, we use the canDraw() function, which returns a boolean. We pass it the direction, either 'horizontal' or 'vertical', and the value of either the horizontal or vertical variable. We call this function in the 'rotate' listener of both dials, and only if 'true' do we increment the angle and call the draw() function.

function canDraw(direction, value) {
  var max = (direction==='horizontal')?(canvas.width):(canvas.height);
  if(value < 2 || value > max - 2) {
  return false;
  }
  return true;
}

09. Avoid dial problems

With the bounds we've just implemented, there is a chance the dial might get stuck at one end if the value goes over the limit, even by a decimal point. To avoid this situation, we should handle the case where canDraw() is false and reset the value to a previously valid one, as shown here for the horizontal controller:

if(canDraw('horizontal', horizontal)) {
  angleHorizontal += e.detail.distanceFromLast;
  targetLeft.style.transform = 'rotate(' + angleHorizontal + 'deg)';
  draw(Math.floor(prev_horizontal), Math.floor(prev_vertical), Math.floor(horizontal), Math.floor(prev_vertical));        
  prev_horizontal = horizontal;
} else {
  horizontal = prev_horizontal;
}

10. Get the drawing board on your tablet

It is always recommended to test on your targeted devices as early as possible. Our application is now in a good shape, and can respond to touch events. Follow the steps on accessing localhost remotely to get the drawing board on your tablet.

Next, we will use Safari and the Develop menu to inspect the application on an iPad. For Android devices, use chrome://inspect.

11. Test the accelerometer

Testing the accelerometer in Safari [click the icon to enlarge]

Connect your tablet to your computer via USB and inspect the application using the developer tools.

With the code below in place, you should be able to see the various acceleration values, as you move your device around. In order to reset the canvas, we've decided to consider an acceleration on the x axis over 5, and slowly decrease the opacity (eraseRate).

var eraseRate = 1; /*define as a global variable*/
window.addEventListener('devicemotion', function(event) {
  console.log('Acceleration::', event.acceleration);
  if(event.acceleration.x > 5) {
  eraseRate -= Math.abs(event.acceleration.x/100);
  console.log('Erase::', eraseRate);
  }
});

12. Shake to delete

We've seen in the previous step how to check for motion and acceleration. We now need to call fadeDrawing() when our condition is met. In this instance, we redraw an exact copy of the canvas at a different opacity.

Reset the globalAlpha to 1 in draw() and set the globalCompositeOperation back to source-over.

function fadeDrawing() {
  if(eraseRate < 0) {
  context.clearRect(0, 0, canvas.width, canvas.height);
  eraseRate = 1;
  return;
  }
  context.globalAlpha = eraseRate;
  context.globalCompositeOperation='copy';
  context.drawImage(canvas, 0, 0);
}

13. Make it look like the real deal

Our application with shake-to-delete functionality

So far, our application looks quite bland and flat. In order to give it some depth, we will add a frame colour, a shadow inside the frame and a bit of volume on the dials. The CSS for the dial shadows is already provided, but you will need to add those two elements at the end of the body. 

Complete the CSS for the elements suggested here:

<div id="dialShadowHorizontal" class="shadow"></div>
<div id="dialShadowVertical" class="shadow"></div>
body {
  background: #09cbf7;
}
#sketch {
  box-shadow: 2px 2px 10px rgba(0, 0, 0, .25) inset;
}

14. Use WebSockets

At the beginning of this tutorial, we briefly mentioned using WebSockets through our Node server. Now that you have a standalone drawing pad for tablet, we will look at making it available for your phone as well. However, phones might be too small to display both the screen and controls. We are therefore using sockets to communicate between phone and computer screen.

15. Detect the device size

In the main HTML file, replace main.js with extra.js. The latter contains all we've done so far, with modifications to handle devices and sockets, which we'll inspect in the following steps. Have a look at detectDevice() – this method now gets called on load instead of init() and will decide which 'mode' to handle for the application.

Below is the particular case of a phone being detected:

if(window.innerWidth < 768) {
  socket = io.connect();
  document.querySelector('#sketch').remove();
  var dials = document.querySelectorAll('.dial, .shadow');
  [].forEach.call(dials, function(item) {
  item.classList.add('big');
  });
  isControls = true;
  frameMarginVertical = 62;
  socket.emit('ready', {'ready': 'controls'});
}

16. From phone to computer

From phone to computer, remotely drawing through sockets

Throughout extra.js you will notice bits of code such as socket.emit() or socket.on(). These are the emitters and listeners for our controls (phone) and screen (computer) instances. Every emitted event needs to go through the server to be re-distributed to all connected sockets. In server\index.js add a few more listeners in the 'connection' function and restart the Node server.

socket.on('draw', function(data){
  io.sockets.emit('draw', data);
});
socket.on('erase', function(data){
  io.sockets.emit('erase', data);
});
socket.on('adjustFrame', function(data){
  screenWidth = data.screenWidth;
  screenHeight = data.screenHeight;
  io.sockets.emit('adjustFrame', data);
});

17. Fix phone orientation

Visit the localhost on your computer, while accessing it remotely with your phone (like you did previously from your tablet). You should now see a line being drawn on your screen while turning the dials on your phone. You will notice, however, that the dials don't fit properly if the phone is in portrait mode.

We can fix this with some CSS:

@media screen and (orientation: portrait) {
  .dial.big#dialVertical, .shadow.big#dialShadowVertical {
  right: calc(50% - 75px);
  bottom: 20px;
  top: auto;
  }
  .dial.big#dialHorizontal, .shadow.big#dialShadowHorizontal {
  left: calc(50% - 75px);
  top: 20px;
  }
}

18. Make the toy more realistic

Touching your tablet leaves some temporary fingerprints

Let's get back to our tablet version. Sadly, the Vibration API is not available on iOS, so we can't implement haptic feedback when the dials are turned. In the original toy, though, you could leave temporary black fingerprint marks on the screen if you pushed it. We can add a touch event on the device to replicate this feature. 

Set these listeners in init() and explore the functions they call:

if(type === 'all') {
  canvas.addEventListener('touchstart', function(e){
  e.preventDefault();
  drawFingerPrint(e.layerX, e.layerY, true);
  });
  canvas.addEventListener('touchend', function(e){
  hideFingerPrint(e.layerX, e.layerY);
  });
}

19. Save a copy of the canvas

In the drawFingerPrint() method, before we do anything else, we save a copy of the current state of the canvas to a hidden element that we use to restore our drawing when clearing the print. That only happens on first touch, and not on the subsequent calls that increase the size of the print every 100ms.

function drawFingerPrint(xPos, yPos, saveCanvas) {
  /*partial function, refer to extra.js*/
  if(saveCanvas) {
  hiddenCanvas = document.createElement('canvas');
  var hiddenContext = hiddenCanvas.getContext('2d');
  hiddenCanvas.width = canvas.width;
  hiddenCanvas.height = canvas.height;
  hiddenContext.drawImage(canvas, 0, 0);
  }
}

20. Run the application offline

You could now make the application truly standalone by saving it to your tablet as a Home Screen app. We won't be able to do the same for the phone, as it requires connection to the server. In /public, locate the file named sketch.appcache and replace all the instances of 'localhost' by your IP address.

Now, amend the HTML to read as follows:

<html lang="en" manifest="sketch.appcache">

21. Save the application

Now visit the application again on your tablet and select the Add to Home Screen option. A new icon should appear on your desktop. Open it once while still being connected to your localhost remotely. The cache manifest we set up previously will download all the necessary files for offline use in the background. Turn the Wi-Fi off and open the app again. Voilà!

This article originally appeared in Web Designer magazine issue 263. Buy it here

Read more: