KEMBAR78
Stupid Canvas Tricks | KEY
Stupid Canvas
    Tricks
Who Am I?




 Dean Hudson, @deanero
Bit Twiddler at Massively Fun
We make games with
open web technology.
    We’re hiring!
What this is?

• Tips tricks and about the Canvas API
• 2D Context
• Focus on game programming
• Focus on bitmaps
• Me v. Me Lightning Talks
What this isn’t

• A talk about WebGL
• Terribly deep on any one topic
• A JavaScript or Canvas API tutorial
But first: the
dumbest trick I
could think of
Pro Tip: Use HSL
 instead of RGB
      when
  interpolating
      color.
1) Basic Tools
A Simple
Animation Loop
Outline

• Call requestAnimationFrame
• Handle queued UI events
• Call update on your game entities
• Render!
Outline

• Call requestAnimationFrame
• Handle queued UI events
• Call update on your game entities
• Render!
// game loop
gameLoop = {
    run: function () {
        this.id = requestAnimationFrame(this.run);
        update();
        draw();
    },
    stop: function () {
        cancelAnimationFrame(this.id);
    }
};

gameLoop.run();
RAF Shim

• Lots of examples on the internet
• Set RAF to whichever your browser has
• Fallback to set timeout at ~16ms
buildRAFPolyfill = function () {
  window.requestAnimationFrame =
    window.requestAnimationFrame         ||
    window.webkitRequestAnimationFrame   ||
    window.mozRequestAnimationFrame      ||
    window.oRequestAnimationFrame        ||
    window.msRequestAnimationFrame       ||
    function (cb, elt) {
       window.setTimeout(function() {
         cb(+new Date());
       }, 1000 / 60);
    };
}
Profiling
chrome://tracing

• Wrap things you care about in
  console.time() / console.timeEnd() calls
• Bring up tracing in a tab
• Downside: console spam
var profile = function (name, cb) {
  console.time(name);
  cb();
  console.timeEnd(name);
}

// then later...
profile('render', function () {
  // my render call.
  render();
});
Testing?
Testing Canvas is
       hard.

• PhantomJS (A little scary...)
• Stub out the Canvas API
Testing Canvas is
       hard.

• PhantomJS (A little scary...)
• Stub out the Canvas API
2) Caching Tricks
Basic Technique
• Create a canvas element in your code
• Get its context
• Write pixels to that context
• Use the canvas in a drawImage() call on
  another canvas context
• YOU DON’T ADD THE CACHED CANVAS
  TO THE DOM!
var canvasCache = document.createElement('canvas'),
    cacheCtx;

canvasCache.setAttribute('width', 400);
canvasCache.setAttribute('height', 400);

cacheCtx = canvasCache.getContext('2d');
cacheCtx.drawImage(someImage, 0, 0);

// later...
mainCtx.drawImage(canvasCache, 0, 0);
Double Buffering
Basic Idea
• drawImage() (to screen) is expensive
• Build a whole screen cache in a back buffer
  (in memory)
• Draw entities to that cache with
  drawImage() during render
• Write the entire cache to the screen with a
  single drawImage() call
Basic Idea
• drawImage() (to screen) is expensive
• Build a whole screen cache in a back buffer
  (in memory)
• Draw entities to that cache with
  drawImage() during render
• Write the entire cache to the screen with a
  single drawImage() call
var backBuffer = document.createElement('canvas'),
    backBufferCtx;
backBufferCtx = canvasCache.getContext('2d');

// later...
var render = function () {
    var i, ent;
    mainCtx.drawImage(backBuffer, 0, 0);

    for (i = 0; i > entities.length; i++) {
        ent = entities[i];
        // this is not quite what you'd do but...
        backBufferCtx.drawImage(ent.cache, ent.x, ent.y)
    }
}
Layered Images
Similar thing...
• Save drawImage() calls by compositing into a
  cache
• Draw multiple images to the same cache in
  order
• Attach the cache to a single game entity
• Write the composited cache to your frame
  buffer in your draw call.
=
+
...and much more!
3) Your Friend,
  ImageData
WTF is
         ImageData?
• ImageData is what you get back from a
  getImageData() call
• A single byte Array with ALL your pixel
  data
• Pixel data is stored as R, G, B, A tuples
Single pixel Array
     offsets
   0        1                  2        3

 R        G                  B        A
 8 bits   8 bits             8 bits   8 bits



                   4 bytes
Hit Detection
What we want
Strategy

• If (x, y) is in the bounding box for the image,
  check pixels.
• getImageData() to get a pixel data array for
  your sprite at pixel x, y.
• Check byte values for pixel.
var isPixelPainted = function (x, y) {
  ctx = this.imageCache;   // this must be a canvas!
  thresh = this.transparencyThreshold || 100;

    // Grab a single pixel at x, y: if the alpha channel is greater
    // than the threshold, we're in business. idata acts like a byte
    // array with each pixel occupying 4 slots (R, G, B , A).
    idata = ctx.getImageData(x, y, 1, 1);
    return idata.data[3] > thresh;
}
DON’T FORGET:
 each idata.data
value is between
     0-255!
Image Filters
Since ImageData
   acts like a byte
       array....
• We can iterate through the pixels and do
  arithmetic or bitwise manipulation on the
  data.
• Use XOR, NOT, etc. Get weird!
var filter = function (x, y, w, h) {
    if (!this._cache) return;
    var ctx = this._cache.getContext('2d'),
        idata = ctx.getImageData(x, y, w, h),
        i;
    for (i = 0; i < idata.data.length; i++) {
        idata.data[i] = (idata.data[i]++) % 255;
    }
};
var filter = function (x, y, w, h) {
    if (!this._cache) return;
    var ctx = this._cache.getContext('2d'),
        idata = ctx.getImageData(x, y, w, h),
        i;
    for (i = 0; i < idata.data.length; i++) {
        if (! i % 4 === 3) continue;
        idata.data[i] = (idata.data[i]++) % 255;
    }
Also possible, (but
 not necessarily
  recommended)
• Masking (if the pixel is colored in one
  image...)
• Pixel-wise composition.
• Etc.!
Caveats

• Cross-domain issues: if your data came from
  somewhere untrusted, the browser will not
  send it to the GPU.
• Canvas calls exist for some of these things.
• You can easily produce garbage.
Questions?
Thanks!



Slides: http://j.mp/stupidcanvastricks

dean@massivelyfun.com, @deanero
   http://massivelyfun.com/jobs

Stupid Canvas Tricks

  • 1.
  • 2.
    Who Am I? Dean Hudson, @deanero Bit Twiddler at Massively Fun
  • 3.
    We make gameswith open web technology. We’re hiring!
  • 4.
    What this is? •Tips tricks and about the Canvas API • 2D Context • Focus on game programming • Focus on bitmaps • Me v. Me Lightning Talks
  • 5.
    What this isn’t •A talk about WebGL • Terribly deep on any one topic • A JavaScript or Canvas API tutorial
  • 6.
    But first: the dumbesttrick I could think of
  • 8.
    Pro Tip: UseHSL instead of RGB when interpolating color.
  • 9.
  • 10.
  • 11.
    Outline • Call requestAnimationFrame •Handle queued UI events • Call update on your game entities • Render!
  • 12.
    Outline • Call requestAnimationFrame •Handle queued UI events • Call update on your game entities • Render!
  • 13.
    // game loop gameLoop= { run: function () { this.id = requestAnimationFrame(this.run); update(); draw(); }, stop: function () { cancelAnimationFrame(this.id); } }; gameLoop.run();
  • 14.
    RAF Shim • Lotsof examples on the internet • Set RAF to whichever your browser has • Fallback to set timeout at ~16ms
  • 15.
    buildRAFPolyfill = function() { window.requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function (cb, elt) { window.setTimeout(function() { cb(+new Date()); }, 1000 / 60); }; }
  • 16.
  • 17.
    chrome://tracing • Wrap thingsyou care about in console.time() / console.timeEnd() calls • Bring up tracing in a tab • Downside: console spam
  • 18.
    var profile =function (name, cb) { console.time(name); cb(); console.timeEnd(name); } // then later... profile('render', function () { // my render call. render(); });
  • 20.
  • 21.
    Testing Canvas is hard. • PhantomJS (A little scary...) • Stub out the Canvas API
  • 22.
    Testing Canvas is hard. • PhantomJS (A little scary...) • Stub out the Canvas API
  • 23.
  • 24.
    Basic Technique • Createa canvas element in your code • Get its context • Write pixels to that context • Use the canvas in a drawImage() call on another canvas context • YOU DON’T ADD THE CACHED CANVAS TO THE DOM!
  • 25.
    var canvasCache =document.createElement('canvas'), cacheCtx; canvasCache.setAttribute('width', 400); canvasCache.setAttribute('height', 400); cacheCtx = canvasCache.getContext('2d'); cacheCtx.drawImage(someImage, 0, 0); // later... mainCtx.drawImage(canvasCache, 0, 0);
  • 26.
  • 27.
    Basic Idea • drawImage()(to screen) is expensive • Build a whole screen cache in a back buffer (in memory) • Draw entities to that cache with drawImage() during render • Write the entire cache to the screen with a single drawImage() call
  • 28.
    Basic Idea • drawImage()(to screen) is expensive • Build a whole screen cache in a back buffer (in memory) • Draw entities to that cache with drawImage() during render • Write the entire cache to the screen with a single drawImage() call
  • 29.
    var backBuffer =document.createElement('canvas'), backBufferCtx; backBufferCtx = canvasCache.getContext('2d'); // later... var render = function () { var i, ent; mainCtx.drawImage(backBuffer, 0, 0); for (i = 0; i > entities.length; i++) { ent = entities[i]; // this is not quite what you'd do but... backBufferCtx.drawImage(ent.cache, ent.x, ent.y) } }
  • 30.
  • 31.
    Similar thing... • SavedrawImage() calls by compositing into a cache • Draw multiple images to the same cache in order • Attach the cache to a single game entity • Write the composited cache to your frame buffer in your draw call.
  • 32.
  • 33.
  • 34.
  • 35.
    WTF is ImageData? • ImageData is what you get back from a getImageData() call • A single byte Array with ALL your pixel data • Pixel data is stored as R, G, B, A tuples
  • 36.
    Single pixel Array offsets 0 1 2 3 R G B A 8 bits 8 bits 8 bits 8 bits 4 bytes
  • 37.
  • 38.
  • 39.
    Strategy • If (x,y) is in the bounding box for the image, check pixels. • getImageData() to get a pixel data array for your sprite at pixel x, y. • Check byte values for pixel.
  • 40.
    var isPixelPainted =function (x, y) { ctx = this.imageCache; // this must be a canvas! thresh = this.transparencyThreshold || 100; // Grab a single pixel at x, y: if the alpha channel is greater // than the threshold, we're in business. idata acts like a byte // array with each pixel occupying 4 slots (R, G, B , A). idata = ctx.getImageData(x, y, 1, 1); return idata.data[3] > thresh; }
  • 41.
    DON’T FORGET: eachidata.data value is between 0-255!
  • 42.
  • 43.
    Since ImageData acts like a byte array.... • We can iterate through the pixels and do arithmetic or bitwise manipulation on the data. • Use XOR, NOT, etc. Get weird!
  • 44.
    var filter =function (x, y, w, h) { if (!this._cache) return; var ctx = this._cache.getContext('2d'), idata = ctx.getImageData(x, y, w, h), i; for (i = 0; i < idata.data.length; i++) { idata.data[i] = (idata.data[i]++) % 255; } };
  • 45.
    var filter =function (x, y, w, h) { if (!this._cache) return; var ctx = this._cache.getContext('2d'), idata = ctx.getImageData(x, y, w, h), i; for (i = 0; i < idata.data.length; i++) { if (! i % 4 === 3) continue; idata.data[i] = (idata.data[i]++) % 255; }
  • 46.
    Also possible, (but not necessarily recommended) • Masking (if the pixel is colored in one image...) • Pixel-wise composition. • Etc.!
  • 47.
    Caveats • Cross-domain issues:if your data came from somewhere untrusted, the browser will not send it to the GPU. • Canvas calls exist for some of these things. • You can easily produce garbage.
  • 48.
  • 49.