The Canvas API for Rubyists  Cascadia Ruby, August 3rd, 2012       Harry Dean Hudson
Who Am I? Dean Hudson, @deaneroBit Twiddler at Massively Fun
We make games withopen web technology.  (And are hiring).http://massivelyfun.com    @massivelyfun
Word * Word
What is this talk?• Actually less about Canvas API and more   about patterns in which it can be used...• No Ruby! (Sorry R...
Canvas API:2D graphics in browser.   It is quite simple.
Canvas is• 2D, immediate mode, graphics API• Well supported in modern browsers• A collection of drawing calls• Not Flash!
(function () {    var ImageData = function (width, height) {         this.width     = width   || 10;         this.height =...
this.__openSubpath      = true;     this.__initTime         = new Date();     this.__lastUpdateTime = null;     this.__las...
CanvasRenderingContext2D.prototype.clip = function () {     return undefined;};CanvasRenderingContext2D.prototype.closePat...
return retImageData;};CanvasRenderingContext2D.prototype.isPointPath = function (x, y) {     return true;};CanvasRendering...
return undefined;    };    CanvasRenderingContext2D.prototype.restore = function () {         return undefined;    };    C...
The CanvasContext:It has all the calls to draw  Pixels to the screen.
Simple, no?cvs = document.getElementById("canvas")ctx = cvs.getContext("2D")# ctx has all your draw methods...ctx.fillText...
Simple, no?cvs = document.getElementById("canvas")ctx = cvs.getContext("2D")# ctx has all your draw methods...ctx.fillText...
dean@dean:~$ grep prototype canvas.js |                 wc -l                   38  You can learn the draw    API in a wee...
dean@dean:~$ grep prototype canvas.js |                 wc -l                   38  You can learn the draw     API in a we...
Browser event loopsAre not well suited for games   You must roll your own
Simple Game Loop • Handle queued UI/server events • Update state of “things” on screen. • Re-draw!
# a fake game loopgameLoop =  run: ->    @handleQueuedEvents()    @update()    @draw()    # loop somehow?
setInterval()?setTimeout()?
NO!
16 ms !=16.666...ms    Text (60 FPS)
For accurate loopsrequestAnimationFrame() Will bring you great joy
requestAnimationFrame allows the browser tomanage screen updates      efficiently...
requestAnimationFrame allows the browser tomanage screen updates      efficiently... ...but is not uniformly        suppor...
buildRAFPolyfill = ->  requestAnimationFrame =    requestAnimationFrame         ||    webkitRequestAnimationFrame   ||    ...
class GameLoop  constructor: (cvs) ->    @ctx       = cvs.getContext("2D")    @entities = []  addEntity: (entity) ->    @e...
class GameLoop  constructor: (cvs) ->    @ctx       = cvs.getContext("2D")    @entities = []  addEntity: (entity) ->    @e...
Entities and Rects:The largest things are built From just these pieces.
Simple Game   Entity• Respond to update call• Manage position, height, width• Delegate draw calls
# Game level objectclass Entity  # Rect is a drawing primitive  constructor: (options = {}) ->    @rect     = options.rect...
# Game level objectclass Entity  # Rect is a drawing primitive  constructor: (options = {}) ->    @rect     = options.rect...
# Game level objectclass Entity  # Rect is a drawing primitive  constructor: (options = {}) ->    @rect     = options.rect...
Simple Draw        Primitive• Associated with Entity• Responds to draw() call• Has actual CanvasContext2D drawing calls• Y...
## Rect: Drawing primitive for canvas.class Rect  constructor: (options = {}) ->    @width    = options?.width ? 1    @hei...
## Rect: Drawing primitive for canvas.class Rect  constructor: (options = {}) ->    @width    = options?.width ? 1    @hei...
Rect = require "rect"# Sometimes we just need the simple things.# Make a simple box subclass.class Box extends Rect  const...
Rect = require "rect"# Sometimes we just need the simple things.# Make a simple box subclass.class Box extends Rect  const...
Rect = require "rect"# Sometimes we just need the simple things.# Make a simple box subclass.class Box extends Rect  const...
Canvas tests are hard;The one true way is to cheat.Stub, Stub, stub the world.
CanvasRenderingContext2D.prototype.__update = function () {    var args = Array.prototype.slice.call(arguments);    this._...
Questions?
Thanks!dean@massivelyfun.com, @deanero     http://massivelyfun.com
Upcoming SlideShare
Loading in …5
×

The Canvas API for Rubyists

2,182 views

Published on

A short talk which covered more the core game loop/entity/drawing primitive pattern more than the actual Canvas API. Terrible Haiku as a bonus.

Published in: Technology, Business
0 Comments
0 Likes
Statistics
Notes
  • Be the first to comment

  • Be the first to like this

No Downloads
Views
Total views
2,182
On SlideShare
0
From Embeds
0
Number of Embeds
49
Actions
Shares
0
Downloads
5
Comments
0
Likes
0
Embeds 0
No embeds

No notes for slide
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • The Canvas API for Rubyists

    1. The Canvas API for Rubyists Cascadia Ruby, August 3rd, 2012 Harry Dean Hudson
    2. Who Am I? Dean Hudson, @deaneroBit Twiddler at Massively Fun
    3. We make games withopen web technology. (And are hiring).http://massivelyfun.com @massivelyfun
    4. Word * Word
    5. What is this talk?• Actually less about Canvas API and more about patterns in which it can be used...• No Ruby! (Sorry Rubyists).• Play along! https://github.com/massivelyfun/ canvas-playground• ...in 5 / 7 / 5.
    6. Canvas API:2D graphics in browser. It is quite simple.
    7. Canvas is• 2D, immediate mode, graphics API• Well supported in modern browsers• A collection of drawing calls• Not Flash!
    8. (function () { var ImageData = function (width, height) { this.width = width || 10; this.height = height || 10; numPixels = this.width * this.height; this.data = new Uint8Array(numPixels * 4); }; var CanvasGradient = function () {}; CanvasGradient.prototype.addColorStop = function (offset, color) { return undefined; }; var CanvasPattern = function (image, repetitionStyle) { this.image = image; this.repetitionStyle = repetitionStyle; }; var TextMetrics = function (ctx, text) { this... // quick and dirty style var fontSize = parseInt(ctx.font), chars = text.split().length; this.width = fontSize * chars; } var CanvasRenderingContext2D = function (canvas) { this.canvas = canvas; this.fillStyle = "rgb(0,0,0)"; this.font = "10px sans-serif"; this.globalAlpha = 1.0; this.globalCompositionOperation = "source-over"; this.lineCap = "butt"; this.lineJoin = "miter"; this.lineWidth = 1.0; this.miterLimit = 10; this.textAlign = "start"; this.textBaseLine = "alphabetic"; this.shadowBlur = 0; this.shadowColor = "rgba(0,0,0,0)"; this.shadowOffsetX = 0; this.shadowOffsetY = 0; this.strokeStyle = "rgb(0,0,0)"; this.__width = this.canvas.width; this.__height = this.canvas.height; this.__imageData = null; // dont do this until we need it, its a memory hog. // new ImageData(this.__width, this.__height); this.__curX = 0; this.__curY = 0;
    9. this.__openSubpath = true; this.__initTime = new Date(); this.__lastUpdateTime = null; this.__lastFillTime = null; this.__updateCount = 0; this.__fillCount = 0;};CanvasRenderingContext2D.prototype.__update = function () { var args = Array.prototype.slice.call(arguments); this.__lastUpdateTime = new Date(); this.__updateCount++;}CanvasRenderingContext2D.prototype.__fill = function () { var args = Array.prototype.slice.call(arguments); this.__lastFillTime = new Date(); this.__fillCount++;}// Stub out the real methods. Im explicitly returning undefined// in cases where the API calls for void return, so as to be clear// about the intent. This is a simple sub-set of the API focused// on operations for images. TODO: implement transforms and state.CanvasRenderingContext2D.prototype.arc = function (x, y, radius, startAngle, endAngle, counterClockwise) { this.__openSubpath = true;...plus this... return undefined;};CanvasRenderingContext2D.prototype.arcTo = function (x1, y1, x2, y2, radius) { this.__openSubpath = true; this.__curX = x2; this.__curY = y2; return undefined;};CanvasRenderingContext2D.prototype.beginPath = function () { this.__openSubpath = true; return undefined;};CanvasRenderingContext2D.prototype.bezierCurveTo = function (cpX1, cpY1, cpX2, cpY2, x, y) { this.__openSubpath = true; this.__curX = x; this.__curY = y; return undefined;};CanvasRenderingContext2D.prototype.clearRect = function () { return undefined;};CanvasRenderingContext2D.prototype.clip = function () { return undefined;};CanvasRenderingContext2D.prototype.closePath = function () { this.__openSubpath = false; return undefined;};CanvasRenderingContext2D.prototype.createImageData = function () { var args = Array.prototype.slice.call(arguments);
    10. CanvasRenderingContext2D.prototype.clip = function () { return undefined;};CanvasRenderingContext2D.prototype.closePath = function () { this.__openSubpath = false; return undefined;};CanvasRenderingContext2D.prototype.createImageData = function () { var args = Array.prototype.slice.call(arguments); if (args[0].hasOwnProperty(data) && typeof args[0].data !== undefined) { return new ImageData(args[0].data.length, 1); } else if (typeof args[0] === number && typeof args[1] === number) { return new ImageData(args[0], args[1]); } else { throw new Error("Invalid arguments. createImageData() takes 1 or 2 args."); }};CanvasRenderingContext2D.prototype.createLinearGradient = function (xStart, yStart, xEnd, yEnd) { return new CanvasGradient();};CanvasRenderingContext2D.prototype.createPattern = function (image, repetitionStyle) { return new CanvasPattern(image, repetitionStyle);};CanvasRenderingContext2D.prototype.createRadialGradient = function (xStart, yStart, radiusStart, xEnd, yEnd, radiusEnd) {...plus this... return new CanvasGradient();};CanvasRenderingContext2D.prototype.drawImage = function () { switch(arguments.length) { case 3: return CanvasRenderingContext2D.prototype.__drawImage3.apply(this, arguments); case 5: return CanvasRenderingContext2D.prototype.__drawImage5.apply(this, arguments); case 9: return CanvasRenderingContext2D.prototype.__drawImage9.apply(this, arguments); default: throw new Error("Invalid number of arguments. drawImage() takes, 3, 5, or 9 args."); }};// All that contortion and I dont do anything with it. Im stubbing// this out in case I want to at some point.CanvasRenderingContext2D.prototype.__drawImage3 = function (image, dx, dy) { return undefined;};CanvasRenderingContext2D.prototype.__drawImage5 = function (image, dx, dy, dw, dh) { return undefined;};CanvasRenderingContext2D.prototype.__drawImage9 = function (image, sx, sy, sw, sh, dx, dy, dw, dh) { return undefined;};CanvasRenderingContext2D.prototype.fill = function () { return undefined;};CanvasRenderingContext2D.prototype.fillRect = function (x, y, width, height) {
    11. return retImageData;};CanvasRenderingContext2D.prototype.isPointPath = function (x, y) { return true;};CanvasRenderingContext2D.prototype.lineTo = function (x, y) { this.__openSubpath = true; this.__curX = x; this.__curY = y; return undefined;};CanvasRenderingContext2D.prototype.measureText = function (text) { return new TextMetrics(this, text);};CanvasRenderingContext2D.prototype.moveTo = function (x, y) { this.__curX = x; this.__curY = y; return undefined;};CanvasRenderingContext2D.prototype.putImageData = function (insertData, dx, dy, sx, sy, sw, sh) { if (arguments.length !== 7) throw new Error("putImageData requires 7 arguments") var imageData = this.__imageData || new ImageData(this.__width, this.__height),...plus this... startAt = dx * dy * 4 + dx + 4, fromData, fromOffset = sx * sy * 4 + sx * 4, fromNumPixels = sw * sh * 4 + sw * 4, endAt = imageData.length - 1, howMany; if (typeof fromOffset === number && typeof fromNumPixels === number) { fromData = insertData.data.slice(fromOffset, fromOffset + fromNumPixels); } else { fromData = insertData.data; } startAt + fromData.length > endAt ? howMany = endAt - startAt : howMany = startAt + fromData.length; imageData.data.splice(startAt, howMany, fromData); return undefined;};CanvasRenderingContext2D.prototype.quadraticCurveTo = function (cpX, cpY, x, y) { this.__curX = x; this.__curY = y; return undefined;};CanvasRenderingContext2D.prototype.rect = function (x, y, width, height) { this.__curX = x; this.__curY = y; return undefined;};CanvasRenderingContext2D.prototype.restore = function () { return undefined;
    12. return undefined; }; CanvasRenderingContext2D.prototype.restore = function () { return undefined; }; CanvasRenderingContext2D.prototype.rotate = function (angle) { return undefined; }; CanvasRenderingContext2D.prototype.save = function () { return undefined; }; CanvasRenderingContext2D.prototype.scale = function (sx, sy) { return undefined; }; CanvasRenderingContext2D.prototype.setTransform = function (a, b, c, d, e, f) { return undefined; }; CanvasRenderingContext2D.prototype.stroke = function () { return undefined; }; CanvasRenderingContext2D.prototype.strokeRect = function (x, y, width, height) { return undefined; }; CanvasRenderingContext2D.prototype.strokeText = function (text, x, y, max) { return undefined; ...and this. }; CanvasRenderingContext2D.prototype.transform = function (a, b, c, d, e, f) { return undefined; }; CanvasRenderingContext2D.prototype.translate = function (dx, dy) { return undefined; }; var Canvas = function () { this.width = 10; // API default is 300 x 150, but that makes this.height = 10; // our ImageData a memory hog. }; Canvas.prototype.getContext = function (cxtType) { return new CanvasRenderingContext2D(this); }; Canvas.prototype.toDataURL = function () { var buf = new Buffer("Say hello to my little friend."); return "data:text/plain;base64," + buf.toString(base64); }; module.exports = Canvas;})();
    13. The CanvasContext:It has all the calls to draw Pixels to the screen.
    14. Simple, no?cvs = document.getElementById("canvas")ctx = cvs.getContext("2D")# ctx has all your draw methods...ctx.fillText("Hello!", 100, 100, 200)
    15. Simple, no?cvs = document.getElementById("canvas")ctx = cvs.getContext("2D")# ctx has all your draw methods...ctx.fillText("Hello!", 100, 100, 200) You’ll only see this from here out.
    16. dean@dean:~$ grep prototype canvas.js | wc -l 38 You can learn the draw API in a weekend!
    17. dean@dean:~$ grep prototype canvas.js | wc -l 38 You can learn the draw API in a weekend!...so, on to bigger things!
    18. Browser event loopsAre not well suited for games You must roll your own
    19. Simple Game Loop • Handle queued UI/server events • Update state of “things” on screen. • Re-draw!
    20. # a fake game loopgameLoop = run: -> @handleQueuedEvents() @update() @draw() # loop somehow?
    21. setInterval()?setTimeout()?
    22. NO!
    23. 16 ms !=16.666...ms Text (60 FPS)
    24. For accurate loopsrequestAnimationFrame() Will bring you great joy
    25. requestAnimationFrame allows the browser tomanage screen updates efficiently...
    26. requestAnimationFrame allows the browser tomanage screen updates efficiently... ...but is not uniformly supported.
    27. buildRAFPolyfill = -> requestAnimationFrame = requestAnimationFrame || webkitRequestAnimationFrame || mozRequestAnimationFrame || oRequestAnimationFrame || msRequestAnimationFrame || -> (cb, elt) setTimeout (cb) -> cb(+new Date()) , 1000 / 60
    28. class GameLoop constructor: (cvs) -> @ctx = cvs.getContext("2D") @entities = [] addEntity: (entity) -> @entities.push(entity) run: () => tick = @frameId = requestAnimationFrame(@run) # Update entities entity.update(tick) for entity in @entities # Draw! entity.draw(@ctx) for entity in @entities stop: -> root.cancelAnimationFrame(@frameId)
    29. class GameLoop constructor: (cvs) -> @ctx = cvs.getContext("2D") @entities = [] addEntity: (entity) -> @entities.push(entity) run: () => tick = @frameId = requestAnimationFrame(@run) # Update entities entity.update(tick) for entity in @entities # Draw! entity.draw(@ctx) for entity in @entities stop: -> root.cancelAnimationFrame(@frameId)requestAnimationFrame returns a int “id”
    30. Entities and Rects:The largest things are built From just these pieces.
    31. Simple Game Entity• Respond to update call• Manage position, height, width• Delegate draw calls
    32. # Game level objectclass Entity # Rect is a drawing primitive constructor: (options = {}) -> @rect = options.rect ? null {x, y} = options @position = {x: x, y: y} update: (tick) -> # override and do something interesting here. draw: (ctx) -> # delegate to your drawing primitive! @rect.draw(ctx)module.exports = Entity
    33. # Game level objectclass Entity # Rect is a drawing primitive constructor: (options = {}) -> @rect = options.rect ? null {x, y} = options @position = {x: x, y: y} update: (tick) -> # override and do something interesting here. draw: (ctx) -> # delegate to your drawing primitive! @rect.draw(ctx)module.exports = Entity Game level logic, once per tick
    34. # Game level objectclass Entity # Rect is a drawing primitive constructor: (options = {}) -> @rect = options.rect ? null {x, y} = options @position = {x: x, y: y} update: (tick) -> # override and do something interesting here. draw: (ctx) -> # delegate to your drawing primitive! @rect.draw(ctx)module.exports = Entity DELEGATE!!!!!
    35. Simple Draw Primitive• Associated with Entity• Responds to draw() call• Has actual CanvasContext2D drawing calls• You can determine containment here (image hit detection, for instance)
    36. ## Rect: Drawing primitive for canvas.class Rect constructor: (options = {}) -> @width = options?.width ? 1 @height = options?.height ? 1 {x, y} = options @position = {x: x, y: y} draw: (canvasCtx) -> throw new Error("Implement draw()")module.exports = Rect
    37. ## Rect: Drawing primitive for canvas.class Rect constructor: (options = {}) -> @width = options?.width ? 1 @height = options?.height ? 1 {x, y} = options @position = {x: x, y: y} draw: (canvasCtx) -> throw new Error("Implement draw()")module.exports = Rect Your *simple* interface
    38. Rect = require "rect"# Sometimes we just need the simple things.# Make a simple box subclass.class Box extends Rect constructor: (options = {}) -> super(options) draw: (ctx) -> {x, y} = @position.get() ctx.fillRect(x, y, @width, @height)module.exports = Box
    39. Rect = require "rect"# Sometimes we just need the simple things.# Make a simple box subclass.class Box extends Rect constructor: (options = {}) -> super(options) draw: (ctx) -> {x, y} = @position.get() ctx.fillRect(x, y, @width, @height)module.exports = Box Implement draw
    40. Rect = require "rect"# Sometimes we just need the simple things.# Make a simple box subclass.class Box extends Rect constructor: (options = {}) -> super(options) draw: (ctx) -> {x, y} = @position.get() ctx.fillRect(x, y, @width, @height)module.exports = Box Do work on CanvasContext2D
    41. Canvas tests are hard;The one true way is to cheat.Stub, Stub, stub the world.
    42. CanvasRenderingContext2D.prototype.__update = function () { var args = Array.prototype.slice.call(arguments); this.__lastUpdateTime = new Date(); this.__updateCount++;}CanvasRenderingContext2D.prototype.__fill = function () { var args = Array.prototype.slice.call(arguments); this.__lastFillTime = new Date(); this.__fillCount++;}// Stub out the real methods. Im explicitly returning undefined// in cases where the API calls for void return, so as to be clear// about the intent. This is a simple sub-set of the API focused// on operations for images. TODO: implement transforms and state.CanvasRenderingContext2D.prototype.arcTo = function (x1, y1, x2, y2, radius) { this.__openSubpath = true; this.__curX = x2; this.__curY = y2; return undefined; ...etc.};
    43. Questions?
    44. Thanks!dean@massivelyfun.com, @deanero http://massivelyfun.com

    ×