Developing an HTML5 canvas app ? Why you should use a model

This is aimed at folks who are starting out writing apps that use the HTML5 canvas using JavaScript.

There are many JavaScript canvas libs around to help you write visual apps and increase your overall productivity. This article is generic and about your planning of your app rather than being related to any one lib, although in case you are interested, I’m currently enjoying using the Konva lib, but many others are available.

When you start out coding a canvas-oriented app and find a lib that you can work with, it’s easy to focus on moving forward with your development and seeing where it takes you. But, without a model you will be relying on say, managing the types, positions and appearances of shapes within the lib simply because the lib provides methods to get those properties back after you created the shapes. So when it’s time to save (serialise) your diagram data you can simply call the libs serialise func and voila – everything you need, right ? No.

A typical visual app will have all kinds of use-case rules that it has to follow. Maybe the stacking of shapes, for example – keeping it generic, imagine a case where a red circle can only be placed on a red rectangle and not the other way around. Now you need to code a rule for that. Will the lib help – most likely not because the lib will be assisting you with drawing low-level shapes and event management, but not the aspects of the use-case rules for your specific app.

As you crack out the code, you’ll start out feeling good about how quickly you can get some shapes on the canvas, but as your app progresses you’ll start to realise that a reasonable list of features for a MVP app should include

  • undo-redo
  • cut & paste
  • save & load
  • use-case rules – e.g. shape hierarchy – circle A connects to rectangle B

And now you will see that your canvas lib will not provide these features and become aware of the work that lies ahead of you to include them.

Side note – It IS reasonable for the lib devs not to provide these features because they cannot hope to support all app use cases. The first three features in the list could be common, but the use-case rues item illustrates the point that virtually all graphical apps are imitating a physical world process and that will have rules specific either to the use-case or to the physical world behaviours of the situation the app represents.

Ok – maybe I need a model. Sounds complicated ?

Not really – a modal is fundamentally a list or hierarchy of the objects that relate to your use case, and the code to manage them. Imagine you are making a book editing app. Your model might comprise

  • Book object – the top level object of the app. Includes properties such as author & title, methods for addChaper() and deleteChapter(), and a list of Chapter objects.
  • Chapter object – a Book is composed of chapters. Chapter includes properties such as title & sequence no, and methods such as addPage(), deletePage(), movePage(), and a list of page objects.
  • Page object – a Chapter is composed of pages. Page includes a property for sequence no and methods such as addParagraph(), deleteParagraph(), moveParagraph() and a list of paragraph objects.
  • Paragraph object – a Page is composed of paragraphs…
  • Sentence object – a Paragraph is composed of sentences…

So the model for my book app will contain definitions for objects named Book, Chapter, Page, Paragraph and Sentence. And each level of that hierarchy will contain a list of its child objects so that a chapter has a list containing its pages, etc.

How does this relate to my canvas app ?

Getting back to the point that this is a visual app, exactly how these objects relate to the use case of your app will be self-evident.

For example, with my Book app, we could imagine a UI where there is some type of visual navigation from chapters to pages and that a selected page is then displayed on the canvas as our UI, with paragraphs and sentences shown as canvas shapes containing text that the user can edit somehow. Which all implies that the model is the internal ‘picture’ of the data and the canvas is the visualisation of it.

Who is in control here ?

While the model is the blueprint for how the visualisation on the canvas should look and behave, the user gets to interact with the visualisation – maybe dragging a paragraph from the bottom to the top of the page. You have to provide the code to track to those canvas events and inform the model of the changes. You could think of it as the model providing an API and your job is to link that API via code to the canvas lib’s methods, properties and events.

How does this relate to libs like Vue and React ?

Sure, those things have a model but they offer all kinds of features to achieve all kinds of specific functions that we don’t need right now. You can go down that route, or you can create your own simple model and avoid the learning curve and dependency chain that they introduce. Writing your own simple model will give the added benefit of insight around why and how they achieve what they do.

So, exactly what does a model look like ?

A code skeleton of the object model for the Book example could look something like this.

function Book(options){

  this.title = options.title;
  this.authorName = options.authorName;
  this.uid = newGuid();  
  this.chapters = [];  // list of Chapter objects
  
  this.addChapter = function(options){
    var newChapter = new Chapter({title: options.title});
    this.chapters.push(newChapter);
    return newChapter;
  }
  this.deleteChapter = function(options){
    chapters.splice(options.chapterNo, 1);
    return true;
  }  
  
  // Makes a Chapterobject 
  function Chapter(options){
    this.title = options.title;    
    this.uid = newGuid();      
    this.pages = [];  // list of Page objects

    this.addPage = function(options){
      var newPage = new Page();
      this.Pages.push(newPage);
      return newPage;
    }
  }
  
  function Page(options){
    // ...
  }
  
  // Helper funcs to make a unique id.
  function newGuid(){
    var empty="00000000-0000-0000-0000-000000000000";
    var fC=function () {
        return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1).toUpperCase();
      }
    return (fC() + fC() + "-" + fC() + "-" + fC() + "-" + fC() + "-" + fC() + fC() + fC());
  } 
}

We can create a new book model via…

var myBook = new Book({title: 'Wombats - a practical history of their lost civilisation', authorName: 'A Vanquished Wombat' });

console.log(myBook)

/* Output:
Book {
  title: "Wombats - a practical history of their lost civilisation", 
  authorName: "A Vanquished Wombat", 
  uid: "591A9893-4ACE-E959-2BD0-41DE4BE227FE",
  chapters: Array(0), 
  addChapter: ƒ, 
  deleteChapter: ƒ
  }
*/

So to recap, we’ve got a JavaScript object that represents the top-level object in our object model, and we’ve instantiated it into the myBook variable. From here we can make a new chapter with

var aChapter = myBook.addChapter({title: 'Let there be wombats'});

And so on to pages. paragraphs and sentences. All the code for our model lives inside of the Book object, meaning its outside global Window scope (which is good coding style), and each piece of code is close to the object it relates to giving good structure.

In the real world, we would expand this model with functions for save and load which would, for the save side generate JSON, and for the load would consume JSON and populate the objects into the model.

We would also build in undo-redo and cut & paste.

A note about unique ID’s

The newGuid() function in the code sample creates a unique id similar to a GUID. The reason for my providing this for you is that, later on in the process of developing your app, you will come to a number of points where you will need a unique ID. At that point your initial strategy might be to use a string made of, say, ‘objectname+seq no’, giving for chapter 6 the id ‘chaper-6’. This quickly becomes an issue if you have to keep the id’s unique across documents – for example how about cut & pasting ‘chapter-1’ between books ? Oh wait, the target book already has a ‘chapter-1’ object. Hmmmm.

So the recommendation of long-game experience is to definitely use globally unique identifiers for your objects.

Summary

When you start out on a canvas based app development its tempting to ignore the concept of a model. However, any MVP app will need a bunch of features that demand a model is used, and retro-fitting is a time cost to be avoided, since the benefits of having a model come immediately, and go on to give you the platform to develop sophisticated features like undo-redo. We’ve seen that a model can start simple and grow with your app, and that it leads you toward the benefits of good practice and structured code.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: