How to Build a Canvas Painting App with Ionic 4 Last update: 2019-11-26

How to Build a Canvas Painting App with Ionic 4

The canvas is a very mighty element, which you can use for all kind of functionalities ranging from image manipulation to creating a painting app.

Therefore we will today incorporate a canvas in our Ionic 4 app, and build a drawing app on top of it!

ionic-4-canvas-painting

We’ll add some different colours, set a background image, change the stroke width and finally implement the according functionality to export the canvas into a real image (or first base64) that you can then use for any other operation!

Starting our Canvas App

To start, we just need a blank application and install the base64toGallery plugin in case we want to export an image to the camera roll of the user, so go ahead and run:

ionic start devdacticCanvas blank --type=angular
cd ./devdacticCanvas

npm install @ionic-native/base64-to-gallery
ionic cordova plugin add cordova-base64-to-gallery

If you don’t need the device functionality you can also skip the plugin and the next step, but otherwise you need to make sure that you add this snippet to your config.xml inside the ios block:

<config-file parent="NSPhotoLibraryAddUsageDescription" target="*-Info.plist">
  <string>need to store photos to your library</string>
</config-file>

Otherwise your app will crash on iOS because of missing permissions!

Also, we need to import the plugin inside our app/app.module.ts like always:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';

import { Base64ToGallery } from '@ionic-native/base64-to-gallery/ngx';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
    Base64ToGallery
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

Now it’s time to get real.

Creating the Canvas App View

Let’s start with the view today, as this is actually the easier part of our tutorial.

We need to implement a view with a canvas and some additional functionality which is the selection of a color, the size of the line width and the buttons to set a background image and export the final canvas view.

For the canvas we can grab the events and call our own functionality at different stages, and there’s a slight difference between viewing the app on the browser and on a device: On the browser, only the mouse events will be fired, on a device only the touch events!

In detail this means:

  • mousedown / touchstart: Set the initial coordinates to start drawing and enable drawing mode
  • mousemove / touchmove: We moved the cursor/finger so we need to start drawing
  • mouseup / touchend: End the drawing action

Also, we can disable the bounce effect of our view which can be kinda annoying on iOS if you want to draw by setting forceOverscroll to false on our ion-content element!

The color iteration might not make sense to you yet, but we’ll create the according array of colors soon.

For now go ahead with your app/home/home.page.html and change it to:

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>
      Ionic Canvas Drawing
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content [forceOverscroll]="false">
  <ion-row>
    <ion-col *ngFor="let color of colors" [style.background]="color" class="color-block" tappable
      (click)="selectColor(color)"></ion-col>
  </ion-row>
  <ion-radio-group [(ngModel)]="selectedColor">
    <ion-row>
      <ion-col *ngFor="let color of colors" class="ion-text-center">
        <ion-radio [value]="color"></ion-radio>
      </ion-col>
    </ion-row>
  </ion-radio-group>

  <ion-range min="2" max="20" color="primary" [(ngModel)]="lineWidth">
    <ion-icon size="small" slot="start" name="brush"></ion-icon>
    <ion-icon slot="end" name="brush"></ion-icon>
  </ion-range>

  <canvas #imageCanvas (mousedown)="startDrawing($event)" (touchstart)="startDrawing($event)"
    (touchmove)="moved($event)" (mousemove)="moved($event)" (mouseup)="endDrawing()" (touchend)="endDrawing()"></canvas>

  <ion-button expand="full" (click)="setBackground()">
    <ion-icon slot="start" name="image"></ion-icon>
    Set Background Image
  </ion-button>

  <ion-button expand="full" (click)="exportCanvasImage()">
    <ion-icon slot="start" name="download"></ion-icon>
    Export Canvas Image
  </ion-button>
</ion-content>

We can also apply some easy CSS rules to make the canvas stand out, so simply change the app/home/home.page.scss to:

canvas {
  border: 1px solid rgb(187, 178, 178);
}

.color-block {
  height: 40px;
}

While you can use this canvas mechanism to capture a signature, there’s also another tutorial on exactly that case using another package made specifically for that:

https://devdactic.com/signature-drawpad-ionic-2/

Basics of the Canvas Element

Now we can start our canvas manipulation, and first of all we need to access to it by accessing it as a viewchild. We also add some general properties to our class which are needed to store some coordinates and our colors and line width.

We didn’t set the size of the canvas, so right now it looks kinda ugly. But we can manipulate the size directly in the ngAfterViewInit and set it to the size of the current platform. But using some CSS would work as well.

When we start the drawing (remember, connected to the start event of the canvas) we capture the x and y coordinates so we can then in the next step draw a line from one point to another.

Right now we can also add some more basic functions like setting the color, ending the draw mode and finally also loading an image into the background of the canvas. For this make sure to add an image to your assets folder, which is what we use in our case.

Go ahead by changing the app/home/home.page.ts to:

import { Component, ViewChild, AfterViewInit } from '@angular/core';
import { Platform, ToastController } from '@ionic/angular';
import { Base64ToGallery, Base64ToGalleryOptions } from '@ionic-native/base64-to-gallery/ngx';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss']
})
export class HomePage implements AfterViewInit {
  @ViewChild('imageCanvas', { static: false }) canvas: any;
  canvasElement: any;
  saveX: number;
  saveY: number;

  selectedColor = '#9e2956';
  colors = [ '#9e2956', '#c2281d', '#de722f', '#edbf4c', '#5db37e', '#459cde', '#4250ad', '#802fa3' ];

  drawing = false;
  lineWidth = 5;

  constructor(private plt: Platform, private base64ToGallery: Base64ToGallery, private toastCtrl: ToastController) {}

  ngAfterViewInit() {
    // Set the Canvas Element and its size
    this.canvasElement = this.canvas.nativeElement;
    this.canvasElement.width = this.plt.width() + '';
    this.canvasElement.height = 200;
  }

  startDrawing(ev) {
    this.drawing = true;
    var canvasPosition = this.canvasElement.getBoundingClientRect();

    this.saveX = ev.pageX - canvasPosition.x;
    this.saveY = ev.pageY - canvasPosition.y;
  }

  endDrawing() {
    this.drawing = false;
  }

  selectColor(color) {
    this.selectedColor = color;
  }

  setBackground() {
    var background = new Image();
    background.src = './assets/code.webp';
    let ctx = this.canvasElement.getContext('2d');

    background.onload = () => {
      ctx.drawImage(background,0,0, this.canvasElement.width, this.canvasElement.height);
    }
  }
}

You can of course use any other image, even from a different source. Just keep in mind that you are basically drawing the image on the canvas, so whatever was in it before is below the image, and everything that follows will be above!

Drawing on the Canvas Element

Now it’s time to perform our drawing action. We have created all the necessary things upfront, so right now this is a simple calculation to draw a line with a slected width and color.

To calculate the right positions you also have to count int the real position of the canvas on the screen, otherwise the points you are painting are at the wrong position due to the offset inside the view.

Performing operations on the canvas involves grabbing the current context, beginning a path from the previous spot to the current spot and then calling stroke().

By running this function over and over again whenever the mouse/touch moves we can create a nice line on our canvas, so again open the app/home/home.page.ts and append the following snippet below the already existing functionality:

moved(ev) {
  if (!this.drawing) return;

  var canvasPosition = this.canvasElement.getBoundingClientRect();
  let ctx = this.canvasElement.getContext('2d');

  let currentX = ev.pageX - canvasPosition.x;
  let currentY = ev.pageY - canvasPosition.y;

  ctx.lineJoin = 'round';
  ctx.strokeStyle = this.selectedColor;
  ctx.lineWidth = this.lineWidth;

  ctx.beginPath();
  ctx.moveTo(this.saveX, this.saveY);
  ctx.lineTo(currentX, currentY);
  ctx.closePath();

  ctx.stroke();

  this.saveX = currentX;
  this.saveY = currentY;
}

exportCanvasImage() {
  var dataUrl = this.canvasElement.toDataURL();

  // Clear the current canvas
  let ctx = this.canvasElement.getContext('2d');
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);


  if (this.plt.is('cordova')) {
    const options: Base64ToGalleryOptions = { prefix: 'canvas_', mediaScanner:  true };

    this.base64ToGallery.base64ToGallery(dataUrl, options).then(
      async res => {
        const toast = await this.toastCtrl.create({
          message: 'Image saved to camera roll.',
          duration: 2000
        });
        toast.present();
      },
      err => console.log('Error saving image to gallery ', err)
    );
  } else {
    // Fallback for Desktop
    var data = dataUrl.split(',')[1];
    let blob = this.b64toBlob(data, 'image.webp');

    var a = window.document.createElement('a');
    a.href = window.URL.createObjectURL(blob);
    a.download = 'canvasimage.webp';
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
  }
}

// https://forum.ionicframework.com/t/save-base64-encoded-image-to-specific-filepath/96180/3
b64toBlob(b64Data, contentType) {
  contentType = contentType || '';
  var sliceSize = 512;
  var byteCharacters = atob(b64Data);
  var byteArrays = [];

  for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) {
    var slice = byteCharacters.slice(offset, offset + sliceSize);

    var byteNumbers = new Array(slice.length);
    for (var i = 0; i < slice.length; i++) {
      byteNumbers[i] = slice.charCodeAt(i);
    }

    var byteArray = new Uint8Array(byteNumbers);

    byteArrays.push(byteArray);
  }

  var blob = new Blob(byteArrays, { type: contentType });
  return blob;
}

The previous snippet also contains the functions to export our canvas into a base64 string, that we can use in various ways.

In our case we implement two possibly scenarios:

  • Save the image directly to the camera roll of a user if we are inside a Cordova app (native app)
  • Create a dummy element and download the file, used inside a regular web application

If you want to store it to a local file you can also check out the previous canvas drawing tutorial or the recent explanation about the Ionic device file system.

Conclusion

Using the canvas allows you to work with images and paintings in an easy way across all platforms to implement various cases like a signature field, a simple drawing app or image manipulation for your users.

You can also find a video version of this tutorial below.