Canvasで巨大な画像をアニメーションする際はImageに埋め込んで保存しておく

概要

  • canvas画面外にあるものはレンダリングしない
  • コストが高い画像を生成したならimg.src = canva.toDataURL()で突っ込んどく

encahnt.jsやeasel.jsのコードを読んでいたら似たような仕組みがあったので参考にした。
(enchant.js、call,applyを多用していてキモかったんだけど、素のJSで継承を実装するとそんなもんだっけ…)

準備

canvasのプロトタイプを少し拡張している

Color =
  Red : "rgb(255,0,0)"
  Blue : "rgb(0,0,255)"
  Green : "rgb(0,255,0)"
  White : "rgb(255,255,255)"
  Black : "rgb(0,0,0)"
  i : (r,g,b)->
    "rgb(#{r},#{g},#{b})"

Canvas = CanvasRenderingContext2D
Canvas::init = (color=Color.i(255,255,255),alpha=1)->
  @beginPath()
  @strokeStyle = color
  @fillStyle = color
  @globalAlpha = alpha


抽象クラス

class CanvasSprite
  constructor:(@size=32)->
    buffer = document.createElement('canvas')
    buffer.width = buffer.height = @size
    @shape buffer.getContext('2d')
    @img = new Image
    @img.src = buffer.toDataURL()
  @shape :->
  draw:(g,x,y)->
    dx = dy = ~~(@size/2)
    g.drawImage(@img, x-dx,y-dy)

32x32のキャンバスオブジェクトを生成して、書きこんでからCanvas#toDataURLを呼び、新しくイメージクラスを生成して流しこむ。

継承してshape関数をオーバーライド
具体的な形状を記述する

class CharSprite extends CanvasSprite
  shape: (g,color)->
    cx = cy = 16 
    g.init color or Color.i(128,128,255)
    g.arc( cx , cy-7 ,3 ,0 , 2*Math.PI )
    g.fill()

    g.beginPath()
    g.moveTo cx,cy
    g.lineTo(xx,yy) for [xx,yy] in [
      [cx-4  , cy-3 ]
      [cx+4  , cy-3 ]
    ]
    g.lineTo cx,cy
    g.fill()


使う。

char = new CharSprite 32
char.draw(ctx, 100,100)

アニメーションにおいては、生成するのはコンストラクタで、表示するのはメインループで

スクロール型ゲームのマップのレンダリング

画面スクロールタイプのゲームを作っていた。
これの厄介なところは、フレームごとにカメラ(プレーヤーの座標)を更新して、マップをレンダリングしなおす必要がある。
毎度生成していてはメモリを食いつぶしてしまう。この方式でやってたときは、手元のギャラタブだと全く動く気配なかった。

で、巨大な画像をまるごと生成して、座標指定して表示することにした。
注意するべき点としては、canvasに書き込んだり座標指定する時にマイナス座標を指定するとエラーが起こるので、プレフィックスをつけてバッファ内部でのレンダリング位置を調整している。

なんでそんなことを気にしているかというと、クオータビューにレンダリングするときに回転行列で座標を変換しているので、普通に書きこむとマイナスを指定することが多々ある。

# 640x480決め打ち
# 一応継承しているが、あまりつかい回せなかったたのでほとんどオーバーロードしている
class GroundSprite extends CanvasSprite
  constructor:(@map , @size=32)->
    @ip = [1280,960]
    buffer = document.createElement('canvas')
    [mx,my] = [@map.length*@size , @map[0].length*@size]
    lx = ~~((mx+my)/2)
    ly = ~~(mx/4)
    buffer.width = lx+1280
    buffer.height = ly+960
    @shape buffer.getContext('2d')
    @img = new Image
    @img.src = buffer.toDataURL()

  # アイソメトリック座標変換
  p2ism : (x,y)->
    [ix,iy]= @ip
    [(x+y)/2+ix
     (x-y)/4+iy
    ]

  shape: (g)->
    for i in [0 ... @map.length]
      for j in [0 ... @map[i].length]
        if @map[i][j]
          [vx,vy] = @p2ism i*@size ,j*@size
          g.init(Color.Black)
          g.moveTo vx,vy
          g.lineTo(x,y) for [x,y] in [
            @p2ism(  i  *@size,(j+1)*@size)
            @p2ism((i+1)*@size,(j+1)*@size)
            @p2ism((i+1)*@size,  j  *@size)
          ]
          g.lineTo vx,vy
          g.fill()

  draw:(g,cx,cy)->
    [ix,iy]= @ip
    # g.drawImage(@img, 0,0)
    g.drawImage(@img, cx-320+ix, cy+iy-240, 640, 480, 0 , 0 , 640, 480)

mapはバイナリの二次元配列
Canvas#drawImageの細かい引数の指定は ドキュメントを読むように。
drawImage() メソッド - Canvasリファレンス - HTML5.JP

手元のChromiumRendererだと、CPU消費率が80% -> 30%まで減少
GPUを使うようにするともっとへるかも(前試したときはバグがひどかったので)

まあこのように頑張っても、スマホだとスクロールさせるのはきつかったので、低スペック用にオミットした版のレンダリングが別途必要になる気がした。