Tutorial hero
Lesson icon

Using the HTML5 Canvas Element in Sencha Touch: Part 4

Originally published January 01, 2015 Time 19 mins

This is the fourth part of a tutorial series where we will be building a drawing application using the HTML5 canvas element in Sencha Touch. Click here for the first part.

Last time we built an interface to interact with the canvas component we created. This allowed us to change the size of the brush, the colour of the brush and also reset the canvas.

In this part (which will likely be the last part) we will implement functionality that will allow the user to save whatever they have created within the application to the photo gallery on their device (on iOS or Android).

We will be using a PhoneGap Build plugin to achieve this, so you will first need to set up your application to use PhoneGap Build.

Using the Canvas 2 Image Plugin

We will be using the Canvas 2 Image plugin by devgeeks.org to handle saving images for us. We can’t save directly to the users photos on the device with plain web technology, so we need to use this plugin to interface with their Photos in iOS or Gallery in Android.

1. Include the plugin in your config.xml

To make this plugin available in your project you will need to first put it in your config.xml. Add the following line to your config.xml file:

<gap:plugin name="org.devgeeks.canvas2imageplugin" version="0.6.0" />

2. Add the functionality to the CanvasDraw component

To make use of the plugin, we will be adding an additional function to our CanvasDraw.js component called saveCanvas:

saveCanvas: function(){
	var me = this,
		canvas = me.element.dom.firstChild;

	if(window.canvas2ImagePlugin){
		var canvas2ImagePlugin = window.canvas2ImagePlugin;

		canvas2ImagePlugin.saveImageDataToLibrary(function(msg){
			console.log(msg);
		},
		function(err){
			console.log(err);
		}, canvas);
	}
},

This plugin is fortunately very easy to use. All we have to do is call the saveImageDataToLibrary() function on the plugin, and supply a reference to the canvas that we are trying to save. We also provide two callback functions, one for if the image is saved successfully and one for if it is not. For now we are just logging the message but you could change this to alert the user of any problems.

3. Add a save button

We now have the functionality of that plugin available to us within our application, and we have added the functionality to the drawing component. Now we just need to trigger it.

We will do this in the same way that we have been previously interacting with the component (to set the brush size, brush colour etc.). We will add a save button to our colours toolbar, which will trigger the saveCanvas function:

{
    xtype: 'button',
    text: 'Save',
    handler: function(){
        this.up('main').saveCanvas();
    }
}

of course make sure to add the corresponding function outside of the config of the Main.js file as well:

saveCanvas: function(){
    var me = this;
    me.down('canvasdraw').saveCanvas();
},

Now when that button is clicked it will trigger the saveCanvas function in our component, which utilises the plugin we installed to export whatever is currently on the canvas as an image to the photo gallery.

4. Install the app on your device

To actually see this in action you will need to install it on your iOS or Android device. Since we are making use of PhoneGap functionality, it won’t work until we actually build the project with PhoneGap.

Installing on Android is easy enough since you can install debug applications without any additional steps, but to install on an iOS device you will need to create a provisioning profile and .p12 file.

There’s more we could do with this application (like making it look prettier and allowing users to import photos as well as export) but I will leave it at that for now unless there’s interest in extending it more. Having completed this tutorial series you should now have a basic level of understanding of:

  • How to create custom Sencha Touch components
  • How to use the canvas element in a Sencha Touch application
  • Creating an interface in Sencha Touch

If there’s anything that you need help with, feel free to leave a comment below and I’ll try to help you as best I can. I’ve also added the completed source code for the Main.js file and CanvasDraw.js below (don’t forget to set up the plugin as well as described in this post).

Main.js

Ext.define('SenchaDraw.view.Main', {
  extend: 'Ext.Container',
  xtype: 'main',
  requires: ['Ext.ux.CanvasDraw'],
  config: {
    items: [
      {
        title: 'Draw',
        xtype: 'container',
        layout: {
          type: 'vbox',
        },
        items: [
          {
            xtype: 'toolbar',
            items: [
              {
                xtype: 'button',
                text: 'Small',
                handler: function () {
                  this.up('main').setBrushSize(1);
                },
              },
              {
                xtype: 'button',
                text: 'Medium',
                handler: function () {
                  this.up('main').setBrushSize(5);
                },
              },
              {
                xtype: 'button',
                text: 'Big',
                handler: function () {
                  this.up('main').setBrushSize(10);
                },
              },
              {
                xtype: 'button',
                text: 'HUGE',
                handler: function () {
                  this.up('main').setBrushSize(30);
                },
              },
            ],
          },
          {
            xtype: 'toolbar',
            ui: 'plain',
            items: [
              {
                xtype: 'button',
                text: 'Reset',
                handler: function () {
                  this.up('main').resetCanvas();
                },
              },
              {
                xtype: 'button',
                style: 'background-color: #000; background-image: none;',
                handler: function () {
                  this.up('main').setBrushColor('#000');
                },
              },
              {
                xtype: 'button',
                style: 'background-color: #ff0c06; background-image: none;',
                handler: function () {
                  this.up('main').setBrushColor('#ff0c06');
                },
              },
              {
                xtype: 'button',
                style: 'background-color: #ffe50c; background-image: none;',
                handler: function () {
                  this.up('main').setBrushColor('#ffe50c');
                },
              },
              {
                xtype: 'button',
                style: 'background-color: #10ff10; background-image: none;',
                handler: function () {
                  this.up('main').setBrushColor('#10ff10');
                },
              },
              {
                xtype: 'button',
                style: 'background-color: #15c5ff; background-image: none;',
                handler: function () {
                  this.up('main').setBrushColor('#15c5ff');
                },
              },
              {
                xtype: 'button',
                style: 'background-color: #fc1fff; background-image: none;',
                handler: function () {
                  this.up('main').setBrushColor('#fc1fff');
                },
              },
              {
                xtype: 'spacer',
              },
              {
                xtype: 'button',
                text: 'Save',
                handler: function () {
                  this.up('main').saveCanvas();
                },
              },
            ],
          },
          {
            xtype: 'canvasdraw',
          },
        ],
      },
    ],
  },

  resetCanvas: function () {
    var me = this;
    me.down('canvasdraw').resetCanvas();
  },

  saveCanvas: function () {
    var me = this;
    me.down('canvasdraw').saveCanvas();
  },

  setBrushSize: function (size) {
    var me = this;
    me.down('canvasdraw').setBrushSize(size);
  },

  setBrushColor: function (color) {
    var me = this;
    me.down('canvasdraw').setBrushColor(color);
  },
});

CanvasDraw.js

Ext.define('Ext.ux.CanvasDraw', {
  extend: 'Ext.Component',
  xtype: 'canvasdraw',

  template: [
    {
      reference: 'canvas',
      tag: 'canvas',
      classList: [Ext.baseCSSPrefix + 'canvas'],
    },
  ],

  config: {
    /**
     * @cfg brushColor
     * The color that will be used for drawing
     * @accessor
     */
    brushColor: '#000',

    /**
     * @cfg brushSize
     * Used to determine line width, which will
     * control the thickness of the "brush"
     * @accessor
     */
    brushSize: 5,

    /**
     * @cfg brushType
     * The type of brush to be used
     * can be set to 'eraser' or 'normal'
     * @accessor
     */
    brushType: 'normal',
  },

  initialize: function () {
    var me = this;
    me.callParent();

    me.on('painted', 'initCanvas', me);

    me.element.on({
      touchstart: 'onTouchStart',
      touchend: 'onTouchEnd',
      touchmove: 'onTouchMove',
      scope: me,
    });
  },

  initCanvas: function () {
    var me = this,
      canvas = me.element.dom.firstChild;

    me.painting = false;
    me.setSize();

    if (canvas) {
      me.canvas = canvas;
      canvas.setAttribute('width', me.width);
      canvas.setAttribute('height', me.height);
      me.context = canvas.getContext('2d');
      me.context.fillStyle = me.brushColor;
    }
  },

  setSize: function (width, height) {
    var me = this;

    if (!width) {
      var totalOffsetX = 0;
      var totalOffsetY = 0;
      var canvasX = 0;
      var canvasY = 0;
      var currentElement = me.element.dom.firstChild;

      do {
        totalOffsetX += currentElement.offsetLeft;
        totalOffsetY += currentElement.offsetTop;
      } while ((currentElement = currentElement.offsetParent));

      me.width = window.innerWidth - totalOffsetX;
      me.height = window.innerHeight - totalOffsetY;
    } else {
      me.width = width;
      me.height = height;
    }
  },

  relMouseCoords: function (e) {
    var me = this;
    var totalOffsetX = 0;
    var totalOffsetY = 0;
    var canvasX = 0;
    var canvasY = 0;
    var currentElement = me.element.dom.firstChild;

    do {
      totalOffsetX += currentElement.offsetLeft;
      totalOffsetY += currentElement.offsetTop;
    } while ((currentElement = currentElement.offsetParent));

    canvasX = e.pageX - totalOffsetX;
    canvasY = e.pageY - totalOffsetY;

    return { x: canvasX, y: canvasY };
  },

  onTouchStart: function (e) {
    var me = this,
      coords = me.relMouseCoords(e);

    me.painting = true;
    me.iLastX = coords.x;
    me.iLastY = coords.y;

    me.context.lineJoin = 'round';
    me.context.beginPath();
    me.context.moveTo(me.iLastX, me.iLastY);
    me.context.lineTo(me.iLastX + 1, me.iLastY);
    me.context.closePath();
    if (me.getBrushType() == 'eraser') {
      me.context.strokeStyle = '#fff';
    } else {
      me.context.strokeStyle = me.getBrushColor();
    }
    me.context.lineWidth = me.getBrushSize();
    me.context.stroke();
  },

  onTouchEnd: function (e) {
    var me = this;

    me.paining = false;
    me.iLastX = -1;
    me.iLastY = -1;
  },

  onTouchMove: function (e) {
    var me = this;

    if (me.painting) {
      var coords = me.relMouseCoords(e),
        iX = coords.x,
        iY = coords.y;

      me.context.lineJoin = 'round';
      me.context.beginPath();
      me.context.moveTo(me.iLastX, me.iLastY);
      me.context.lineTo(iX, iY);
      me.context.closePath();
      if (me.getBrushType() == 'eraser') {
        me.context.strokeStyle = '#fff';
      } else {
        me.context.strokeStyle = me.getBrushColor();
      }
      me.context.lineWidth = me.getBrushSize();
      me.context.stroke();
      me.iLastX = iX;
      me.iLastY = iY;
    }
  },

  resetCanvas: function () {
    var me = this,
      canvas = me.element.dom.firstChild,
      context = canvas.getContext('2d');

    context.clearRect(0, 0, canvas.width, canvas.height);
    context.beginPath();
  },

  saveCanvas: function () {
    var me = this,
      canvas = me.element.dom.firstChild;

    if (window.canvas2ImagePlugin) {
      var canvas2ImagePlugin = window.canvas2ImagePlugin;

      canvas2ImagePlugin.saveImageDataToLibrary(
        function (msg) {
          console.log(msg);
        },
        function (err) {
          console.log(err);
        },
        canvas
      );
    }
  },
});
Learn to build modern Angular apps with my course