This document provides steps for building a recursive tree drawing on a canvas element. It begins with initializing the canvas and rendering loop. Subsequent steps describe drawing the initial line, converting it to a parameterized function, calling the function recursively to build the tree structure, adding animation and optimization. Later sections discuss using a library like stats.js to measure performance, drawing grids, and techniques for optimizing canvas drawing like using single paths.
6. @rrafols
Step 0 - Recursive tree
• Set up a <canvas>
• Initialize the rendering loop with requestAnimationFrame()
7. @rrafols
Step 0 - Recursive tree
let a = document.getElementsByTagName('canvas')[0]
let c = a.getContext('2d')
let w = window.innerWidth
let h = window.innerHeight
render = ts => {
requestAnimationFrame(render)
}
requestAnimationFrame(render)
8. @rrafols
Step 1 - Recursive tree
• Clear the <canvas> on each iteration
• Draw the initial line
9. @rrafols
Step 1 - Recursive tree
a.width = w
a.height = h
c.strokeStyle = '#fff'
c.beginPath()
c.moveTo(w/2, h)
c.lineTo(w/2, h - h/3)
c.stroke()
10. @rrafols
Step 2 - Recursive tree
• Convert the drawing into a parametrized function
• Lines direction modified by an angle
11. @rrafols
Step 2 - Recursive tree
split(w / 2, h, h / 3, Math.PI / 2)
split = (x, y, length, angle) => {
let x1 = x + length * Math.cos(angle)
let y1 = y - length * Math.sin(angle)
c.beginPath()
c.moveTo(x, y)
c.lineTo(x1, y1)
c.stroke()
}
12. @rrafols
Step 3 - Recursive tree
• Call the split function recursively
• Modify the angle in each iteration to build the tree structure
14. @rrafols
Step 4 - Recursive tree
• Add more branches / bifurcations
• Increase max number of iterations
• Count the number of lines drawn so we can estimate drawing
complexity
15. @rrafols
Step 4 - Recursive tree
lines++
…
split(x1, y1, length/1.5, angle - Math.PI/3, it + 1)
split(x1, y1, length/1.5, angle - Math.PI/8, it + 1)
split(x1, y1, length/1.5, angle + Math.PI/3, it + 1)
split(x1, y1, length/1.5, angle + Math.PI/8, it + 1)
…
c.fillStyle = '#fff'
c.fillText("lines: " + lines, w - 200, 20)
16. @rrafols
Step 5 - Recursive tree
• Animate tree movement
• Recursively displace branches for intensified effect
• Apply alpha for smoother effect
20. @rrafols
How can we measure performance?
Feels slower right?
To measure the frames per second we can do any of the following:
• Implement a simple frame counter
• Web Developer tools in Chrome & Firefox
• Available open source libraries such as stats.js
• https://github.com/mrdoob/stats.js/
• Use jsPerf to create test & run cases
• https://jsperf.com/
23. @rrafols
Using stats.js
1) Use npm to install it:
rrafols$ npm install stats.js
2) Include into the source file
<script src="node_modules/stats.js/build/stats.min.js"/>
3) Add it to the render function:
25. @rrafols
Step 7 – Recursive tree
Now that we know it is slower, what we can do about it? How
can we optimize it?
• …
26. @rrafols
Step 7 – Recursive tree
Now that we know it is slower, what we can do about it? How
can we optimize it?
• Draw one single path with all the lines in it instead of several
paths with one single line.
• …
28. @rrafols
Step 7 – Recursive tree
Now that we know it is slower, what we can do about it? How
can we optimize it?
• Draw one single path with all the lines in it instead of several
paths with one single line.
• Let’s increase the number of iterations to see how it behaves
now
• …
35. @rrafols
Grid
We have several ways of drawing a grid, let’s see some of
them:
• Using strokeRect directly on the context
• Adding rect to a path and stroking that path
• Generating a path with moveTo / lineTo instead of rect
• ...
36. @rrafols
Grid - strokeRect
for(let i = 0; i < h / GRID_SIZE; i++) {
for(let j = 0; j < w / GRID_SIZE; j++) {
let x = j * GRID_SIZE
let y = i * GRID_SIZE
c.strokeRect(x, y, GRID_SIZE, GRID_SIZE)
}
}
37. @rrafols
Grid – path & rect
let path = new Path2D()
…
for(let i = 0; i < h / GRID_SIZE; i++) {
for(let j = 0; j < w / GRID_SIZE; j++) {
let x = j * GRID_SIZE
let y = i * GRID_SIZE
path.rect(x, y, GRID_SIZE, GRID_SIZE)
}
}
…
c.stroke(path)
38. @rrafols
Grid – moveTo/lineTo
c.beginPath()
for(let i = 0; i < h / GRID_SIZE; i++) {
for(let j = 0; j < w / GRID_SIZE; j++) {
let x = j * GRID_SIZE
let y = i * GRID_SIZE
c.moveTo(x, y)
c.lineTo(x + GRID_SIZE, y)
c.lineTo(x + GRID_SIZE, y + GRID_SIZE)
c.lineTo(x, y + GRID_SIZE)
c.lineTo(x, y)
}
}
c.stroke()
39. @rrafols
Grid – moveTo/lineTo - path
let path = new Path2D()
for(let i = 0; i < h / GRID_SIZE; i++) {
for(let j = 0; j < w / GRID_SIZE; j++) {
let x = j * GRID_SIZE
let y = i * GRID_SIZE
path.moveTo(x, y)
path.lineTo(x + GRID_SIZE, y)
path.lineTo(x + GRID_SIZE, y + GRID_SIZE)
path.lineTo(x, y + GRID_SIZE)
path.lineTo(x, y)
}
}
c.stroke(path)
59. @rrafols
Conclusions
• Avoid allocating memory inside render loop – GC is “evil”!
• Group paths together rather than drawing multiple small paths
• Pre-calculate & store as much as possible
• Values & numbers
• Paths, Gradients, …
• Reduce render state machine changes
• Careful with canvas transformations
• Reset transformation with setTransform instead of save/restore.
• Always measure & profile. Check differences on several browsers.
60. @rrafols
Thank you
• More information:
https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial
• Source code:
https://github.com/rrafols/html_canvas_performance
• Contact: https://www.linkedin.com/in/raimonrafols/