SlideShare a Scribd company logo
1 of 61
@rrafols
Rendered art on the
web
A performance compendium
@rrafols
Disclaimer
Content is my own experimentation and might
differ on other environments / systems or as soon
as there is a browser update.
@rrafols
Uses
• Game development
• Customized widgets
• Interactivity
• It’s fun & creative
@rrafols
• JS1k - https://js1k.com/
• Demoscene - https://en.wikipedia.org/wiki/Demoscene
• P01 - http://www.p01.org/about/
• JS13k games - https://js13kgames.com/
• Creative JS - http://creativejs.com/
@rrafols
Let’s build something simple to begin with……
@rrafols
Step 0 - Recursive tree
• Set up a <canvas>
• Initialize the rendering loop with requestAnimationFrame()
@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)
@rrafols
Step 1 - Recursive tree
• Clear the <canvas> on each iteration
• Draw the initial line
@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()
@rrafols
Step 2 - Recursive tree
• Convert the drawing into a parametrized function
• Lines direction modified by an angle
@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()
}
@rrafols
Step 3 - Recursive tree
• Call the split function recursively
• Modify the angle in each iteration to build the tree structure
@rrafols
Step 3 - Recursive tree
c.beginPath()
c.moveTo(x, y)
c.lineTo(x1, y1)
c.stroke()
split(x1, y1, length/1.5, angle - Math.PI/4, it + 1)
split(x1, y1, length/1.5, angle + Math.PI/4, it + 1)
@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
@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)
@rrafols
Step 5 - Recursive tree
• Animate tree movement
• Recursively displace branches for intensified effect
• Apply alpha for smoother effect
@rrafols
Step 5 - Recursive tree
c.globalAlpha = 0.2
c.strokeStyle = '#fff'
let angle = Math.PI * Math.sin(ts * 0.0003)*0.15 +
Math.PI/2
split(w/2, h, h/3, angle, Math.cos(ts * 0.0004) *
Math.PI/8, 0)
c.globalAlpha = 1
@rrafols
Step 6 - Recursive tree
• Increase maximum number of iterations and see what happens…
@rrafols
<code>
@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/
@rrafols
@rrafols
@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:
@rrafols
Adding stats.js
var stats = new Stats()
stats.showPanel(0)
document.body.appendChild( stats.dom )
…
render = ts => {
stats.begin()
…
stats.end()
requestAnimationFrame(render)
}
@rrafols
Step 7 – Recursive tree
Now that we know it is slower, what we can do about it? How
can we optimize it?
• …
@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.
• …
@rrafols
Step 7 - Recursive tree
// c.beginPath()
c.moveTo(x, y)
c.lineTo(x1, y1)
// c.stroke()
…
c.beginPath()
let angle = Math.PI*Math.sin(ts * 0.0003)*0.15…
split(w/2, h, h/3, angle, Math.cos(ts * 0.0004)…
c.stroke()
@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
• …
@rrafols
<demo>
@rrafols
Performance Profiling
@rrafols
Performance Profiling
There might be a CPU bottleneck when calling recursively the
split function.
Let’s check if that is the issue…
@rrafols
Performance profiling
let path = new Path2D()
…
path.moveTo(x, y)
path.lineTo(x1, y1)
…
c.strokeStyle = '#fff'
c.stroke(path)
@rrafols
Performance Profiling
There is an impact in the
frame rate by function calling
overhead, but rendering
seems to be the bottleneck
@rrafols
Let’s try something else
@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
• ...
@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)
}
}
@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)
@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()
@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)
@rrafols
<demo>
@rrafols
Grid – transformation
c.save()
c.translate(w / 2, h / 2)
c.rotate(angle)
c.translate(-w / 2, -h / 2)
…
c.restore()
@rrafols
Grid – transformation
//c.save()
c.translate(w / 2, h / 2)
c.rotate(angle)
c.translate(-w / 2, -h / 2)
…
//c.restore()
c.setTransform(1, 0, 0, 1, 0, 0)
@rrafols
<code>
@rrafols
Grid
@rrafols
Grid – transformation
rotate = (x, y, angle) => {
x -= w/2
y -= h/2
return [
x * Math.cos(angle) - y * Math.sin(angle) + w/2,
y * Math.cos(angle) + x * Math.sin(angle) + h/2
]
}
@rrafols
<demo>
@rrafols
OKNOK
strokeRect
path.rect
path.lineT
o
c.lineTo
@rrafols
Grid
What about fill operations instead of stroke? Let’s fill the
rects to see the differences.
@rrafols
<demo>
@rrafols
OK NOK
fillRect
path.rect
path.lineT
o
c.lineTo
@rrafols
Grid
What about images?
ImageAtlas vs single images
Images: https://toen.itch.io/toens-medieval-strategy
@rrafols
ImageAtlas – drawing & clipping
c.save()
c.beginPath()
c.rect(j * GRID_SIZE, i * GRID_SIZE, GRID_SIZE, GRID_SIZE)
c.clip()
c.drawImage(image, j * GRID_SIZE - 64, i * GRID_SIZE)
@rrafols
<demo>
@rrafols
Grid – ImageAtlas vs single images
@rrafols
Grid – image smoothing
Browsers smooth images when drawing on decimal positions:
c.drawImage(image, 5.24, 10.23)
@rrafols
ImageAtlas – drawing on exact pixels
c.drawImage(imageLarge,
(w / 2 - imageLarge.naturalWidth / 2) |0,
(h / 2 - imageLarge.naturalHeight / 2) |0)
@rrafols
<demo>
@rrafols
Images: https://dragosha.com/free/adventure-tileset.html
@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.
@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/
@rrafols
<?>

More Related Content

What's hot

ECMAScript 6 major changes
ECMAScript 6 major changesECMAScript 6 major changes
ECMAScript 6 major changes
hayato
 

What's hot (20)

Enter The Matrix
Enter The MatrixEnter The Matrix
Enter The Matrix
 
Introduction to NumPy for Machine Learning Programmers
Introduction to NumPy for Machine Learning ProgrammersIntroduction to NumPy for Machine Learning Programmers
Introduction to NumPy for Machine Learning Programmers
 
Coscup2021 - useful abstractions at rust and it's practical usage
Coscup2021 - useful abstractions at rust and it's practical usageCoscup2021 - useful abstractions at rust and it's practical usage
Coscup2021 - useful abstractions at rust and it's practical usage
 
Running Free with the Monads
Running Free with the MonadsRunning Free with the Monads
Running Free with the Monads
 
Numpy Talk at SIAM
Numpy Talk at SIAMNumpy Talk at SIAM
Numpy Talk at SIAM
 
λ | Lenses
λ | Lensesλ | Lenses
λ | Lenses
 
Gremlin's Graph Traversal Machinery
Gremlin's Graph Traversal MachineryGremlin's Graph Traversal Machinery
Gremlin's Graph Traversal Machinery
 
Beyond Scala Lens
Beyond Scala LensBeyond Scala Lens
Beyond Scala Lens
 
30 分鐘學會實作 Python Feature Selection
30 分鐘學會實作 Python Feature Selection30 分鐘學會實作 Python Feature Selection
30 分鐘學會實作 Python Feature Selection
 
Statistical inference for (Python) Data Analysis. An introduction.
Statistical inference for (Python) Data Analysis. An introduction.Statistical inference for (Python) Data Analysis. An introduction.
Statistical inference for (Python) Data Analysis. An introduction.
 
1 seaborn introduction
1 seaborn introduction 1 seaborn introduction
1 seaborn introduction
 
Millionways
MillionwaysMillionways
Millionways
 
ECMAScript 6 major changes
ECMAScript 6 major changesECMAScript 6 major changes
ECMAScript 6 major changes
 
Intoduction to numpy
Intoduction to numpyIntoduction to numpy
Intoduction to numpy
 
Optics with monocle - Modeling the part and the whole
Optics with monocle - Modeling the part and the wholeOptics with monocle - Modeling the part and the whole
Optics with monocle - Modeling the part and the whole
 
ProgrammingwithGOLang
ProgrammingwithGOLangProgrammingwithGOLang
ProgrammingwithGOLang
 
Functional Patterns for the non-mathematician
Functional Patterns for the non-mathematicianFunctional Patterns for the non-mathematician
Functional Patterns for the non-mathematician
 
Abstracting over the Monad yielded by a for comprehension and its generators
Abstracting over the Monad yielded by a for comprehension and its generatorsAbstracting over the Monad yielded by a for comprehension and its generators
Abstracting over the Monad yielded by a for comprehension and its generators
 
D3.js workshop
D3.js workshopD3.js workshop
D3.js workshop
 
A Survey Of R Graphics
A Survey Of R GraphicsA Survey Of R Graphics
A Survey Of R Graphics
 

Similar to Rendering Art on the Web - A Performance compendium

Refactoring to Macros with Clojure
Refactoring to Macros with ClojureRefactoring to Macros with Clojure
Refactoring to Macros with Clojure
Dmitry Buzdin
 
ComputeFest 2012: Intro To R for Physical Sciences
ComputeFest 2012: Intro To R for Physical SciencesComputeFest 2012: Intro To R for Physical Sciences
ComputeFest 2012: Intro To R for Physical Sciences
alexstorer
 
Lock? We don't need no stinkin' locks!
Lock? We don't need no stinkin' locks!Lock? We don't need no stinkin' locks!
Lock? We don't need no stinkin' locks!
Michael Barker
 
Threaded Programming
Threaded ProgrammingThreaded Programming
Threaded Programming
Sri Prasanna
 

Similar to Rendering Art on the Web - A Performance compendium (20)

Introduction to Coding
Introduction to CodingIntroduction to Coding
Introduction to Coding
 
Proposals for new function in Java SE 9 and beyond
Proposals for new function in Java SE 9 and beyondProposals for new function in Java SE 9 and beyond
Proposals for new function in Java SE 9 and beyond
 
FreeBSD 2014 Flame Graphs
FreeBSD 2014 Flame GraphsFreeBSD 2014 Flame Graphs
FreeBSD 2014 Flame Graphs
 
Clojure And Swing
Clojure And SwingClojure And Swing
Clojure And Swing
 
Refactoring to Macros with Clojure
Refactoring to Macros with ClojureRefactoring to Macros with Clojure
Refactoring to Macros with Clojure
 
Raphaël and You
Raphaël and YouRaphaël and You
Raphaël and You
 
N flavors of streaming
N flavors of streamingN flavors of streaming
N flavors of streaming
 
Swift for tensorflow
Swift for tensorflowSwift for tensorflow
Swift for tensorflow
 
ComputeFest 2012: Intro To R for Physical Sciences
ComputeFest 2012: Intro To R for Physical SciencesComputeFest 2012: Intro To R for Physical Sciences
ComputeFest 2012: Intro To R for Physical Sciences
 
Getting Started with Keras and TensorFlow - StampedeCon AI Summit 2017
Getting Started with Keras and TensorFlow - StampedeCon AI Summit 2017Getting Started with Keras and TensorFlow - StampedeCon AI Summit 2017
Getting Started with Keras and TensorFlow - StampedeCon AI Summit 2017
 
Emerging Languages: A Tour of the Horizon
Emerging Languages: A Tour of the HorizonEmerging Languages: A Tour of the Horizon
Emerging Languages: A Tour of the Horizon
 
Locks? We Don't Need No Stinkin' Locks - Michael Barker
Locks? We Don't Need No Stinkin' Locks - Michael BarkerLocks? We Don't Need No Stinkin' Locks - Michael Barker
Locks? We Don't Need No Stinkin' Locks - Michael Barker
 
Lock? We don't need no stinkin' locks!
Lock? We don't need no stinkin' locks!Lock? We don't need no stinkin' locks!
Lock? We don't need no stinkin' locks!
 
Threaded Programming
Threaded ProgrammingThreaded Programming
Threaded Programming
 
Groovy
GroovyGroovy
Groovy
 
Scala Refactoring for Fun and Profit
Scala Refactoring for Fun and ProfitScala Refactoring for Fun and Profit
Scala Refactoring for Fun and Profit
 
Introduction To PostGIS
Introduction To PostGISIntroduction To PostGIS
Introduction To PostGIS
 
Cpp tutorial
Cpp tutorialCpp tutorial
Cpp tutorial
 
Building an ML Platform with Ray and MLflow
Building an ML Platform with Ray and MLflowBuilding an ML Platform with Ray and MLflow
Building an ML Platform with Ray and MLflow
 
Introductionto fp with groovy
Introductionto fp with groovyIntroductionto fp with groovy
Introductionto fp with groovy
 

More from Raimon Ràfols

More from Raimon Ràfols (12)

The bytecode gobbledygook
The bytecode gobbledygookThe bytecode gobbledygook
The bytecode gobbledygook
 
The bytecode mumbo-jumbo
The bytecode mumbo-jumboThe bytecode mumbo-jumbo
The bytecode mumbo-jumbo
 
The Digital Evolution of Dinosaurs - MWCS 2017
The Digital Evolution of Dinosaurs - MWCS 2017The Digital Evolution of Dinosaurs - MWCS 2017
The Digital Evolution of Dinosaurs - MWCS 2017
 
Android Custom Views
Android Custom ViewsAndroid Custom Views
Android Custom Views
 
Iterate & Learn 2017
Iterate & Learn 2017Iterate & Learn 2017
Iterate & Learn 2017
 
Iterate & Learn 2017 (català)
Iterate & Learn 2017 (català)Iterate & Learn 2017 (català)
Iterate & Learn 2017 (català)
 
The bytecode hocus pocus - JavaOne 2016
The bytecode hocus pocus - JavaOne 2016The bytecode hocus pocus - JavaOne 2016
The bytecode hocus pocus - JavaOne 2016
 
Iterate + learn - February 2016
Iterate + learn - February 2016Iterate + learn - February 2016
Iterate + learn - February 2016
 
Playing with camera preview buffers on BlackBerry 10
Playing with camera preview buffers on BlackBerry 10Playing with camera preview buffers on BlackBerry 10
Playing with camera preview buffers on BlackBerry 10
 
Improving Android Performance at Mobiconf 2014
Improving Android Performance at Mobiconf 2014Improving Android Performance at Mobiconf 2014
Improving Android Performance at Mobiconf 2014
 
Improving Android Performance at Droidcon UK 2014
Improving Android Performance at Droidcon UK 2014Improving Android Performance at Droidcon UK 2014
Improving Android Performance at Droidcon UK 2014
 
Improving Java performance at JBCNConf 2015
Improving Java performance at JBCNConf 2015Improving Java performance at JBCNConf 2015
Improving Java performance at JBCNConf 2015
 

Recently uploaded

Architecting Cloud Native Applications
Architecting Cloud Native ApplicationsArchitecting Cloud Native Applications
Architecting Cloud Native Applications
WSO2
 

Recently uploaded (20)

A Year of the Servo Reboot: Where Are We Now?
A Year of the Servo Reboot: Where Are We Now?A Year of the Servo Reboot: Where Are We Now?
A Year of the Servo Reboot: Where Are We Now?
 
Emergent Methods: Multi-lingual narrative tracking in the news - real-time ex...
Emergent Methods: Multi-lingual narrative tracking in the news - real-time ex...Emergent Methods: Multi-lingual narrative tracking in the news - real-time ex...
Emergent Methods: Multi-lingual narrative tracking in the news - real-time ex...
 
DBX First Quarter 2024 Investor Presentation
DBX First Quarter 2024 Investor PresentationDBX First Quarter 2024 Investor Presentation
DBX First Quarter 2024 Investor Presentation
 
Apidays Singapore 2024 - Building Digital Trust in a Digital Economy by Veron...
Apidays Singapore 2024 - Building Digital Trust in a Digital Economy by Veron...Apidays Singapore 2024 - Building Digital Trust in a Digital Economy by Veron...
Apidays Singapore 2024 - Building Digital Trust in a Digital Economy by Veron...
 
MINDCTI Revenue Release Quarter One 2024
MINDCTI Revenue Release Quarter One 2024MINDCTI Revenue Release Quarter One 2024
MINDCTI Revenue Release Quarter One 2024
 
Data Cloud, More than a CDP by Matt Robison
Data Cloud, More than a CDP by Matt RobisonData Cloud, More than a CDP by Matt Robison
Data Cloud, More than a CDP by Matt Robison
 
A Beginners Guide to Building a RAG App Using Open Source Milvus
A Beginners Guide to Building a RAG App Using Open Source MilvusA Beginners Guide to Building a RAG App Using Open Source Milvus
A Beginners Guide to Building a RAG App Using Open Source Milvus
 
TrustArc Webinar - Unlock the Power of AI-Driven Data Discovery
TrustArc Webinar - Unlock the Power of AI-Driven Data DiscoveryTrustArc Webinar - Unlock the Power of AI-Driven Data Discovery
TrustArc Webinar - Unlock the Power of AI-Driven Data Discovery
 
Axa Assurance Maroc - Insurer Innovation Award 2024
Axa Assurance Maroc - Insurer Innovation Award 2024Axa Assurance Maroc - Insurer Innovation Award 2024
Axa Assurance Maroc - Insurer Innovation Award 2024
 
Apidays New York 2024 - The Good, the Bad and the Governed by David O'Neill, ...
Apidays New York 2024 - The Good, the Bad and the Governed by David O'Neill, ...Apidays New York 2024 - The Good, the Bad and the Governed by David O'Neill, ...
Apidays New York 2024 - The Good, the Bad and the Governed by David O'Neill, ...
 
Strategies for Unlocking Knowledge Management in Microsoft 365 in the Copilot...
Strategies for Unlocking Knowledge Management in Microsoft 365 in the Copilot...Strategies for Unlocking Knowledge Management in Microsoft 365 in the Copilot...
Strategies for Unlocking Knowledge Management in Microsoft 365 in the Copilot...
 
FWD Group - Insurer Innovation Award 2024
FWD Group - Insurer Innovation Award 2024FWD Group - Insurer Innovation Award 2024
FWD Group - Insurer Innovation Award 2024
 
Corporate and higher education May webinar.pptx
Corporate and higher education May webinar.pptxCorporate and higher education May webinar.pptx
Corporate and higher education May webinar.pptx
 
EMPOWERMENT TECHNOLOGY GRADE 11 QUARTER 2 REVIEWER
EMPOWERMENT TECHNOLOGY GRADE 11 QUARTER 2 REVIEWEREMPOWERMENT TECHNOLOGY GRADE 11 QUARTER 2 REVIEWER
EMPOWERMENT TECHNOLOGY GRADE 11 QUARTER 2 REVIEWER
 
Apidays New York 2024 - The value of a flexible API Management solution for O...
Apidays New York 2024 - The value of a flexible API Management solution for O...Apidays New York 2024 - The value of a flexible API Management solution for O...
Apidays New York 2024 - The value of a flexible API Management solution for O...
 
Strategies for Landing an Oracle DBA Job as a Fresher
Strategies for Landing an Oracle DBA Job as a FresherStrategies for Landing an Oracle DBA Job as a Fresher
Strategies for Landing an Oracle DBA Job as a Fresher
 
Architecting Cloud Native Applications
Architecting Cloud Native ApplicationsArchitecting Cloud Native Applications
Architecting Cloud Native Applications
 
Real Time Object Detection Using Open CV
Real Time Object Detection Using Open CVReal Time Object Detection Using Open CV
Real Time Object Detection Using Open CV
 
Apidays Singapore 2024 - Modernizing Securities Finance by Madhu Subbu
Apidays Singapore 2024 - Modernizing Securities Finance by Madhu SubbuApidays Singapore 2024 - Modernizing Securities Finance by Madhu Subbu
Apidays Singapore 2024 - Modernizing Securities Finance by Madhu Subbu
 
Apidays New York 2024 - Scaling API-first by Ian Reasor and Radu Cotescu, Adobe
Apidays New York 2024 - Scaling API-first by Ian Reasor and Radu Cotescu, AdobeApidays New York 2024 - Scaling API-first by Ian Reasor and Radu Cotescu, Adobe
Apidays New York 2024 - Scaling API-first by Ian Reasor and Radu Cotescu, Adobe
 

Rendering Art on the Web - A Performance compendium

  • 1. @rrafols Rendered art on the web A performance compendium
  • 2. @rrafols Disclaimer Content is my own experimentation and might differ on other environments / systems or as soon as there is a browser update.
  • 3. @rrafols Uses • Game development • Customized widgets • Interactivity • It’s fun & creative
  • 4. @rrafols • JS1k - https://js1k.com/ • Demoscene - https://en.wikipedia.org/wiki/Demoscene • P01 - http://www.p01.org/about/ • JS13k games - https://js13kgames.com/ • Creative JS - http://creativejs.com/
  • 5. @rrafols Let’s build something simple to begin with……
  • 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
  • 13. @rrafols Step 3 - Recursive tree c.beginPath() c.moveTo(x, y) c.lineTo(x1, y1) c.stroke() split(x1, y1, length/1.5, angle - Math.PI/4, it + 1) split(x1, y1, length/1.5, angle + Math.PI/4, it + 1)
  • 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
  • 17. @rrafols Step 5 - Recursive tree c.globalAlpha = 0.2 c.strokeStyle = '#fff' let angle = Math.PI * Math.sin(ts * 0.0003)*0.15 + Math.PI/2 split(w/2, h, h/3, angle, Math.cos(ts * 0.0004) * Math.PI/8, 0) c.globalAlpha = 1
  • 18. @rrafols Step 6 - Recursive tree • Increase maximum number of iterations and see what happens…
  • 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:
  • 24. @rrafols Adding stats.js var stats = new Stats() stats.showPanel(0) document.body.appendChild( stats.dom ) … render = ts => { stats.begin() … stats.end() requestAnimationFrame(render) }
  • 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. • …
  • 27. @rrafols Step 7 - Recursive tree // c.beginPath() c.moveTo(x, y) c.lineTo(x1, y1) // c.stroke() … c.beginPath() let angle = Math.PI*Math.sin(ts * 0.0003)*0.15… split(w/2, h, h/3, angle, Math.cos(ts * 0.0004)… c.stroke()
  • 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 • …
  • 31. @rrafols Performance Profiling There might be a CPU bottleneck when calling recursively the split function. Let’s check if that is the issue…
  • 32. @rrafols Performance profiling let path = new Path2D() … path.moveTo(x, y) path.lineTo(x1, y1) … c.strokeStyle = '#fff' c.stroke(path)
  • 33. @rrafols Performance Profiling There is an impact in the frame rate by function calling overhead, but rendering seems to be the bottleneck
  • 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)
  • 41. @rrafols Grid – transformation c.save() c.translate(w / 2, h / 2) c.rotate(angle) c.translate(-w / 2, -h / 2) … c.restore()
  • 42. @rrafols Grid – transformation //c.save() c.translate(w / 2, h / 2) c.rotate(angle) c.translate(-w / 2, -h / 2) … //c.restore() c.setTransform(1, 0, 0, 1, 0, 0)
  • 45. @rrafols Grid – transformation rotate = (x, y, angle) => { x -= w/2 y -= h/2 return [ x * Math.cos(angle) - y * Math.sin(angle) + w/2, y * Math.cos(angle) + x * Math.sin(angle) + h/2 ] }
  • 48. @rrafols Grid What about fill operations instead of stroke? Let’s fill the rects to see the differences.
  • 51. @rrafols Grid What about images? ImageAtlas vs single images Images: https://toen.itch.io/toens-medieval-strategy
  • 52. @rrafols ImageAtlas – drawing & clipping c.save() c.beginPath() c.rect(j * GRID_SIZE, i * GRID_SIZE, GRID_SIZE, GRID_SIZE) c.clip() c.drawImage(image, j * GRID_SIZE - 64, i * GRID_SIZE)
  • 54. @rrafols Grid – ImageAtlas vs single images
  • 55. @rrafols Grid – image smoothing Browsers smooth images when drawing on decimal positions: c.drawImage(image, 5.24, 10.23)
  • 56. @rrafols ImageAtlas – drawing on exact pixels c.drawImage(imageLarge, (w / 2 - imageLarge.naturalWidth / 2) |0, (h / 2 - imageLarge.naturalHeight / 2) |0)
  • 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/