Quickstart and Tutorial
Counterpoint is a library of data structures for developing animated, interactive canvas-based web interfaces. It’s best suited for people who are building data- driven interfaces with custom or non-standard rendering requirements, such as D3.js users looking for additional flexibility and simpler canvas support.
Installation
To install Counterpoint using NPM:
npm install -S counterpoint-vis
Then import in your code:
import { Mark, Attribute, ... } from 'counterpoint-vis';
To import Counterpoint in a JavaScript module, you can use similar syntax but refer to the full library URL:
import { Mark, Attribute } from 'https://cdn.jsdelivr.net/npm/counterpoint-vis@latest/dist/counterpoint-vis.es.js';
or use dynamic import syntax in vanilla JS:
import(
'https://cdn.jsdelivr.net/npm/counterpoint-vis@latest/dist/counterpoint-vis.es.js'
).then((Counterpoint) => {
// use Counterpoint as a module
});
Setting Up a Canvas
This example will walk you through creating a simple rendering of some circles that move across the screen when you click.
The first step to use Counterpoint is to set up an HTML5 Canvas element. We’ll place the canvas in our HTML and give it an id:
<body>
<canvas id="example-canvas" style="width: 300px; height: 300px;"></canvas>
</body>
Then we’ll add some JavaScript to get the Canvas element and draw to it:
<script type="text/javascript">
const canvas = document.getElementById('example-canvas');
canvas.width = canvas.offsetWidth * window.devicePixelRatio;
canvas.height = canvas.offsetHeight * window.devicePixelRatio;
function draw() {
const ctx = canvas.getContext('2d');
// scaling for 2x devices
ctx.resetTransform();
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
ctx.clearRect(0, 0, canvas.offsetWidth, canvas.offsetHeight);
ctx.fillStyle = 'blue';
ctx.beginPath();
ctx.ellipse(150, 150, 20, 20, 0, 0, 2 * Math.PI, false);
ctx.fill();
}
draw();
</script>
The canvas should now look like this:
Defining Marks and a Render Group
At this point, if we wanted to manually create multiple circles and have them animate, we would have to create a data structure to hold the point coordinates, then update that data structure every frame and redraw the canvas accordingly. That’s because unlike with SVG, objects on a canvas are not DOM elements so you cannot use CSS animations to animate them. This quickly becomes cumbersome when not all elements are animating at the same times, when adding or removing elements, or when you want to cancel one animation mid-flight and begin another one.
Counterpoint can help you achieve great animations as easily as with SVG, while getting the great performance and scalability of Canvas.
It does this by letting you express the contents of the canvas in terms of marks, or drawable units, that have animatable attributes. For instance, in a scatter plot, the marks might be points consisting of x and y attributes.
Let’s set up some marks in our script to represent two circles. Each Mark
is
constructed with an ID (any identifier) and a dictionary of attributes:
let marks = [
new Mark(0, { x: 50, y: 50 }),
new Mark(1, { x: 200, y: 100 }),
];
Attributes can also be initialized with functions that get called whenever the
attribute is needed. For example, we could set up a color
attribute that
changes depending on the marks’ x and y positions:
function getColor(mark) {
return `hsl(${mark.attr('x') * 360 / 500}, ${mark.attr('y') * 100 / 500}%, 40%)`;
}
let marks = [
new Mark(0, { x: 50, y: 50, color: getColor }),
new Mark(1, { x: 200, y: 100, color: getColor }),
];
Counterpoint also provides a container called MarkRenderGroup
which helps
manage animations and updates over a potentially large set of marks. Let’s use it
to wrap our array of marks:
let renderGroup = new MarkRenderGroup(marks);
Now that we’ve defined our marks and their attributes, we can use them to
re-implement the draw()
function we created above. Every time draw()
gets
called (which is still just once for now, until we add animations), we iterate
over the render group and get each mark’s coordinates using the Mark.attr()
method.
function draw() {
const ctx = canvas.getContext('2d');
// scaling for 2x devices
ctx.resetTransform();
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
ctx.clearRect(0, 0, canvas.offsetWidth, canvas.offsetHeight);
ctx.fillStyle = 'blue';
// iterate over the marks in the render group and draw them
renderGroup.forEach((mark) => {
ctx.beginPath();
ctx.fillStyle = mark.attr('color');
ctx.ellipse(mark.attr('x'), mark.attr('y'), 20, 20, 0, 0, 2 * Math.PI, false);
ctx.fill();
});
}
That’s great, but it still looks pretty basic. Let’s add some animations!
Simple Animations
Now that we’ve encoded our canvas objects as Mark
instances and placed them
in a render group, it’s easy to perform animations on the attributes we’ve
defined. As the animations play, our draw()
function will get called every
frame, and the values returned by the Mark.attr()
method will automatically
interpolate to the new values.
TIP: Keeping it Fast
Since the
draw()
function will get called about 60 times per second during animations, it’s important to make sure it runs fast and doesn’t perform any unnecessary calculations. Plus, you can configure Counterpoint to redraw only when needed, improving performance and saving energy.
To enable animations, we first have to create a ticker to keep track of our
animations’ timing. This Ticker
instance will keep track of the render group(s)
we give it, and we pass it a function to call when the state of the render group
changes:
ticker = new Ticker(renderGroup).onChange(draw);
Now all that’s left is to write the animations! For this example we’ll simply add a button that animates both circles’ locations to a random spot when clicked. The click handler will look like this:
function animateCircles() {
renderGroup
.animateTo('x', () => Math.random() * 300)
.animateTo('y', () => Math.random() * 300);
}
And we’ll add the click handler to a new button:
<button onclick="animateCircles">Animate</button>
Once completed, you should have something that looks like the following:
Although these are simple animations so far, you can already see that Counterpoint
has helped make our animations easy to create yet smooth. For example, if you click
the button multiple times quickly, you’ll see that the animations smoothly switch
from one to the next with no jitter. And we didn’t have to animate the color
property, since that was automatically computed from our animations to x
and y
.
Next Steps
From here, you can check out further documentation to learn about how to use attributes and marks effectively, make more complex animations, add and remove marks dynamically, make your canvases non-visually accessible, and more.
We’ve also provided some more complete examples: