Imperative vs Declarative drawing API

Many drawing libraries propose imperative API to render objects. E.g. d3, Raphaƫl, fabric.js, two.js, svg.js, snap.svg they all offer some sort of javascript API to draw scene and append objects to it.

If you programmed in both HTML and MFC you'll know how much easier it is to use declarative API to describe UI. Especially if you used angular.js or ember.js.

In this post I wanted to explore available options for declarative programming of graphics. As a reference we'll take really good library two.js.

I am not trying to say imperative API is bad. It has a lot of value to offer. Sole purpose of this post is to check alternative options with declarative API

Basic usage

Let's compare basic example from two.js and pure SVG implementation.

Imperative

// Make an instance of two and place it on the page.
var elem = document.getElementById('draw-shapes').children[0];
var params = { width: 280, height: 200 };
var two = new Two(params).appendTo(elem);

// two has convenience methods to create shapes.
var circle = two.makeCircle(72, 100, 50);
var rect = two.makeRectangle(150, 50, 100, 100);

// The object returned has many stylable properties:
circle.fill = '#FF8000';
circle.stroke = 'orangered'; // Accepts all valid css color
circle.linewidth = 5;

rect.fill = 'rgb(0, 200, 255)';
rect.opacity = 0.75;
rect.noStroke();

// Don't forget to tell two to render everything
// to the screen
two.update();
Declarative

<div id='#draw-shapes'>
  <svg width="280" height="200">
    <circle cx="72" cy="100" r="50" fill="#FF8000"
            stroke="orangered" stroke-width="5" />

    <rect x="150" y="50" width="100" height="100"
          fill="rgb(0, 200, 255)" opacity="0.75">
  </svg>
</div>

As you can see declarative description in this case is much shorter and is very explicit. In fact you don't even need any library. Code works in your browser:

Groups and Animation

Many svg rendering libraries offer grouping of elements and some sort of animation support. SVG standard has group element too. Animations are also part of the standard.

Imperative

var elem = document.getElementById('draw-animation').children[0];
var two = new Two({ width: 280, height: 200 }).appendTo(elem);

var circle = two.makeCircle(-70, 0, 50);
var rect = two.makeRectangle(20, -50, 100, 100);
circle.fill = '#FF8000';
rect.fill = 'rgba(0, 200, 255, 0.75)';

var group = two.makeGroup(circle, rect);
group.translation.set(two.width / 2, two.height / 2);
group.scale = 0;
group.noStroke();

// Bind a function to scale and rotate the group
// to the animation loop.
two.bind('update', function(frameCount) {
  // This code is called everytime two.update() is called.
  // Effectively 60 times per second.
  if (group.scale > 0.9999) {
    group.scale = group.rotation = 0;
  }
  var t = (1 - group.scale) * 0.125;
  group.scale += t;
  group.rotation += t * 4 * Math.PI;
}).play();  // Finally, start the animation loop
        
Declarative

<div id='#draw-shapes'>
  <svg width="280" height="200">
    <g transform="translate(140, 100)">
      <g>
        <circle cx="-70" cy="0" r="50" fill="#FF8000"></circle>
        <rect x="20" y="-50" width="100" height="100"
              fill="rgb(0, 200, 255)" opacity="0.75"></rect>

        <animateTransform attributeName="transform" type="rotate"
                    values="0 0 0;180 0 0;270 0 0;350 0 0; 360 0 0;"
                    keyTimes="0;0.25;0.5;0.9; 1" dur="1s"
                    repeatCount='indefinite' />
        <animateTransform attributeName='transform' type='scale'
                    keyTimes='0;0.3;0.75;1.0' values='0; 0.9;0.9;0.99;'
                    dur='1s' repeatCount='indefinite' additive='sum'/>
      </g>
    </g>
  </svg>
</div>
        

Declarative example works without any library in your browser:

It is more concise too. However it's arguably cleaner than imperative program, and it cheats by hardcoding values for transforms, and animations. Frankly, it was hard to create similar animation using animateTransform tag.

When declarative is not enough?

By nature imperative drawing is very well suited for dynamic programs. When you don't know beforehand how many circles you might have on your screen.

Imperative

var elem = document.getElementById('draw-shapes').children[0];
var params = { width: 280, height: 200 };
var two = new Two(params).appendTo(elem);

for (var i = 0; i < 100; ++i) {
  two.makeCircle(i * 5, Math.random() * 100, 5);
}

two.update();
Declarative

<div id='#draw-shapes'>
  <svg width="280" height="200">
<!-- ??? -->
  </svg>
</div>

Declarative API by definition cannot contain control flow, but can we have best of two worlds? Of course we can! Angular.js uses bindings and custom directives to achieve it. If only we could use something like this:


<svg width="280" height="200">
  <g items-source="{circles}">
    <item-template>
      <circle cx="{x}" cy="{y}" r="5" fill="#FF8000"></circle>
    </item-template>
  </g>
</svg>

Both items-source and item-template are not part of the SVG specification, but maybe a library could implement this?

Fork me on GitHub