Project 13: Draw It with Bitcasa

The HTML5 canvas feature has been on my bucket list to explore. For some reason, being able to draw on a webpage with built-in browser capabilities is what excites me about the future of web development. Perhaps it is the possibilities of what the browser is capable of doing when given the opportunity. Perhaps it is the fact that it is yet another puzzle piece I can use to make engaging experiences.

For my thirteeth project in my 15 projects in 30 days challenge, I’m going to build a simple drawing app that saves images into Bitcasa, a cloud storage service.

Bitcasa

One of the products Bitcasa offers is a cloud file storage service. They offer free accounts with 25gb of storage, plenty of space for drawings. Sign up for a CloudFS developer account, create an app, and add a test user.

drawit

Next, generate an access token for the test user. This test user will store all the drawings that are saved in the app.

drawit2

Copy the API Endpoint domain and the Access Token for later.

Setup

There are three files for this project. app.js is the Node.js app that handles communicating with Bitcasa to store images and to retrieve images. index.html and drawit.js make up the AngularJS app which shows the drawing tool and communicates with the Node.js app to save and display images.

In the app.js file, insert the Access Token from earlier into the BITCASA_ACCESS_TOKEN variable and the API Endpoint into the BITCASA_ENDPOINT variable. You can change the value for FOLDER_NAME, which is a name for the directory where the drawings will be stored.

// Filename: app.js

var BITCASA_ACCESS_TOKEN = '';
var BITCASA_ENDPOINT = '';
var FOLDER_NAME = 'drawit';
var PORT = 8080;

var express = require('express');
var bodyParser = require('body-parser');
var request = require('request');
var url = require('url');

var folderId = '';

var app = express();
app.use(bodyParser());
app.listen(PORT);

// Locate the folder to store the drawings in.
if(folderId == '') {
  request({
    url: 'https://'+BITCASA_ENDPOINT+'/v2/folders/?operation=create',
    headers: {
      'Authorization': 'Bearer '+BITCASA_ACCESS_TOKEN,
      'Content-Type': 'application/x-www-form-urlencoded; charset=utf8'
    },
    form: {
      name: FOLDER_NAME,
      exists: 'fail'
    },
    method: 'POST'},function(error, response, body) {
      if(error) {
        console.log(error);
        process.exit(1);
      }

      var js = JSON.parse(body);
      
      if(js.error) {
        if(js.error.code == 2042) {
          folderId = js.error.data.conflicting_id;  
        } else {
          console.log('Cannot find drawing folder: '+js.error.message);
          process.exit(1);
        }
      } else {
        folderId = js.result.items[0].id;
      }

      console.log('Using folder: '+folderId);
  });
}

app.post('/api/upload', function(req, res) {
  var base64Data = req.body.image.replace(/^data:image\/png;base64,/,'');
  var binaryData = new Buffer(base64Data, 'base64');

  var timeNow = new Date();
  request({
    url: 'https://'+BITCASA_ENDPOINT+'/v2/files/'+folderId,
    headers: {
      'Authorization': 'Bearer '+BITCASA_ACCESS_TOKEN,
      'Content-Type': 'multipart/form-data'
    },
    formData: {
      file: {
        value: binaryData,
        options: {
          filename: 'drawing_'+timeNow.valueOf()+'.png',
          contentType: 'image/png'
        }
      }
    },
    method: 'POST'
  }, function(error, response, body) {
    if(error) {
      res.send({error: 'Unable to save image'});
      return;
    }

    var js = JSON.parse(body);

    if(js.error) {
      res.send({error: js.error.message});
      return;
    }
    
    res.send({id: js.result.id});
  });
});

app.get('/image', function(req, res) {
  var query = url.parse(req.url, true).query;
  var body = '';

  var r = request({
    url: 'https://'+BITCASA_ENDPOINT+'/v2/files/'+folderId+'/'+query.id,
    headers: {
      'Authorization': 'Bearer '+BITCASA_ACCESS_TOKEN,
      'Content-Type': 'application/x-www-form-urlencoded; charset=utf8 '
    },
    method: 'GET'
  });
  r.end();
  
  r.on('response', function(response) {
    response.setEncoding('binary');
    response.on('end', function() {
      if(response.statusCode == 404) {
        res.status(404);
        res.send('Not found');
        return;
      }

      res.setHeader('Content-Type', response.headers['content-type']);
      res.send(new Buffer(body, 'binary'));
    });
    response.on('data', function (chunk) {
      if(response.statusCode == 200) body += chunk;
    });
  });
});

app.use(express.static(__dirname + '/public'));

console.log('Application listening on port '+PORT);
// Filename: public/drawit.js

angular.module('DrawItApp', [])
.controller('DrawItCtrl', ['$scope', '$http', function($scope, $http) {
  $scope.colors = [
    {name: 'Red', hex: 'FF0000'},
    {name: 'Blue', hex: '0000FF'},
    {name: 'Green', hex: '00FF00'},
    {name: 'Yellow', hex: 'FFFF00'},
    {name: 'Purple', hex: '800080'},
    {name: 'White', hex: 'FFFFFF'},
    {name: 'Black', hex: '000000'},
  ];

  $scope.history = [];

  var board = document.getElementById('board');
  var canvasWidth = 800;
  var canvasHeight = 400;
  var clickX = new Array();
  var clickY = new Array();
  var clickDrag = new Array();
  var clickColor = new Array();
  var curColor = $scope.colors[0].hex;
  var paint;

  canvas = document.createElement('canvas');
  canvas.setAttribute('width', canvasWidth);
  canvas.setAttribute('height', canvasHeight);
  canvas.setAttribute('id', 'canvas');
  canvas.setAttribute('style', 'border:1px solid black');
  board.appendChild(canvas);
  context = canvas.getContext("2d");

  $('#canvas').mousedown(function(e) {
    var mouseX = e.pageX - this.offsetLeft;
    var mouseY = e.pageY - this.offsetTop;
      
    paint = true;
    addClick(e.pageX - this.offsetLeft, e.pageY - this.offsetTop);
    redraw();
  });

  $('#canvas').mousemove(function(e) {
    if(paint){
      addClick(e.pageX - this.offsetLeft, e.pageY - this.offsetTop, true);
      redraw();
    }
  });

  $('#canvas').mouseup(function(e) {
    paint = false;
  });  

  $('#canvas').mouseleave(function(e) {
    paint = false;
  });

  function addClick(x, y, dragging) {
    clickX.push(x);
    clickY.push(y);
    clickDrag.push(dragging);
    clickColor.push(curColor)
  }

  function redraw() {
    context.clearRect(0, 0, context.canvas.width, context.canvas.height); // Clears the canvas
    context.lineJoin = 'round';
    context.lineWidth = 5;
        
    for(var i=0; i < clickX.length; i++) {    
      context.beginPath();
      if(clickDrag[i] && i) {
        context.moveTo(clickX[i-1], clickY[i-1]);
       } else {
         context.moveTo(clickX[i]-1, clickY[i]);
       }

       context.lineTo(clickX[i], clickY[i]);
       context.closePath();
       context.strokeStyle = '#'+clickColor[i];
       context.stroke();
    }
  }

  $scope.save = function() {
    var dataURL = canvas.toDataURL();
    $http.post('/api/upload', {image: dataURL}).then(function(response) {
      if(response.data.error) {
        alert('Error: '+response.data.error);
        return;
      }

      console.log(response);
      $scope.history.push(response.data);
    });
  }

  $scope.setColor = function(hex) {
    curColor = hex;
  }

  $scope.clearCanvas = function() {
    clickX = new Array();
    clickY = new Array();
    clickDrag = new Array();
    clickColor = new Array();

    context.clearRect(0, 0, context.canvas.width, context.canvas.height); // Clears the canvas
  }
}]);
<!-- Filename: public/index.html -->
<html ng-app="DrawItApp">
  <head>
    <title>Draw It</title>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.16/angular.min.js"></script>
    <script src="drawit.js"></script>
  </head>
  <body ng-controller="DrawItCtrl" style="font-family:Arial">
    <h2>Draw It</h2>
    <div id="board"></div>
    <input type="button" ng-click="save()" value="Save" />
    <input type="button" ng-click="clearCanvas()" value="Clear" />

    Colors: <input type="button" ng-repeat="color in colors" value="{{color.name}}" ng-click="setColor(color.hex)" />

    <div ng-show="history.length > 0">
      <h3>Saved drawings</h3>
      <a ng-repeat="image in history" href="/image?id={{image.id}}" target="_blank"><img ng-src="/image?id={{image.id}}" width="100" style="border: 1px solid black" /></a>
    </div>
  </body>
</html>
// package.json
{
  "name": "draw-it",
  "description": "Draw It application for Node.js",
  "version": "0.0.1",
  "private": true,
  "dependencies": {
    "express": "*",
    "url": "*",
    "body-parser": "*",
    "request": "*"
  }
}

To install the Node.js dependencies, run the command:

npm install

And to start the Node.js app, run the command

nodejs app.js

Draw It

Get out your crayons and get ready to color! Okay, um, open the index.html file in a modern browser like Google Chrome.

drawit3

Pick a color and start drawing! You can switch colors by clicking on another color. Here’s my first drawing.

drawit4

When you’re ready, click on the Save button. This will take the image data, upload it to the Node.js app, and to the Bitcasa API where it will be stored in the folder you specified in FOLDER_NAME.

drawit5

You can either add more to this drawing, or you can click on Clear to wipe the canvas clean. Everytime the Save button is clicked, the image is uploaded and creates another saved image. They are available at the bottom of the page. Clicking on one of the thumbnails will load the image in a new tab.

drawit6

That’s it for this project. There are plenty of things that could be added to this project.

  • You can add more painting features. William Malone has more awesome examples on how to add brush sizes and other fun features to the HTML5 canvas.
  • The images are saved into Bitacasa with a timestamp. Add functionality to save the images with specific names and the option to choose which folder the images are saved into.
  • Saving images into Bitcasa is only half the fun. How about loading images from Bitcasa to use in the drawing app?
  • Add the ability to login with a Bitcasa account and have the drawings saved in that particular account, instead of just one global account.

Source Code

You can find the repo on GitHub.

Post Mortem

HTML5 gets a bad rap for not being very powerful compared to native apps in iOS and Android. Using the canvas feature shows that progress is being made to make the browser stronger.

This project also dived into how easy it is to pull the image data from the canvas and how to transmit the base 64 encoded binary data through Node.js and to Bitcasa. It felt harder than it really was. This project also got me thinking of drawing games that could be based on this example. Never a dull moment in a developer’s creative mind.

15 Projects in 30 Days Challenge

This blog post is part of my 15 projects in 30 days challenge. I’m hacking together 15 projects with different APIs, services, and technologies that I’ve had little to no exposure to. If my code isn’t completely efficient or accurate, please understand it isn’t meant to be complete and bulletproof. When something is left out, I try to mention it. Reach out to me and kindly teach me if I go towards the dark side. ?

This challenge serves a couple of purposes. First, I’ve always enjoyed hacking new things together and using APIs. And I haven’t had the chance (more like a reason) to dive in head first with things like AngularJS, Node.js, MongoDB, and Eventbrite. This project demonstrated AngularJS, Node.js, MongoDB, and Eventbrite.

This entry was posted in 15 Projects in 30 days. Bookmark the permalink.