rails3 + html5 canvasでお絵かき投稿サイトを作ろう! このエントリをはてなブックマークに登録

2011年11月14日

takurutakuru / , , ,

はじめましてこんにちは。
KRAYアルバイトの浅海です。

html5のcanvasを使ってお絵かき投稿サイトを作ってみようと思います。
初めてブログ記事を書くということで気合が入りました。
ちょっと長めですがお付き合い下さい。

機能

お絵かき投稿サイトの必要最低限な機能って?
ざっと下のような機能を入れてみます。

完成見本はこちら完成見本の公開は終了致しました。

絵を描ける

HTML5のcanvasにマウスの軌道に線を引いていくわけです。
canvasでのマウス軌道の描き方は、
・mousemoveイベント発生時に点をプロット
という手段が真っ先に思い浮かぶと思いますが、これは、以下の様になります。

これではお話になりません。
なので、点ではなく線を引くことにします。
これは下のようになります。

この方法でもまだ、曲線の時に粗が目立ちます。

そこで、線と線の間に点をプロットする、今までの2つの方法を合わせて使います。
すると下のように、綺麗な線が描けます。

※canvasのプロパティに、lineJoinというものがありまして、これに’round’を入れておくとパスとパスの間を滑らかに連結してくれるようです。
しかし、考えるのが面倒だったため全ての線は個別に描画しました。

※11/21
.lineCapでいけそうです・・

controller

railsアプリを作成してからpictures controllerを作ります。

$ rails new canvas
$ cd canvas
$ rails generate controller

ルーティング

ルーティングを設定します。
pictures controllerのnew actionで絵を書くことにします。

# config/routes.rb
  root :to => 'pictures#new'

erb

何の変哲もないcanvasタグを書きます。
CSSだけでwidthとheightの設定をすると調子が悪いので、
必ずcanvasタグ内でサイズ指定して下さい。

# app/views/pictures/new.html.erb
<canvas id="draw-area" width="300" height="300"></canvas>

coffee script

canvasのcontextを取得して、点を描画する機能と線を描画する機能を追加します。

// app/assets/javascripts/pictures.js.coffee
  canvas = $('#draw-area')
  ctx = canvas[0].getContext('2d')
  ctx.lineWidth = 1

  ctx.putPoint = (x, y)-> # x,yに点を描画
    @.beginPath()
    @.arc(x, y, @.lineWidth / 2.0, 0, Math.PI*2, false)
    @.fill()
    @.closePath()
  ctx.drawLine = (sx, sy, ex, ey)-> # 始点(sx, sy) から 終点(ex, ey)に線を描画
    @.beginPath()
    @.moveTo(sx, sy)
    @.lineTo(ex, ey)
    @.stroke()
    @.closePath()

そして、canvasをクリックしたときに点を描画します。
mousedownというフラグ変数を用意して、マウスを押している状態か判定しています。

// app/assets/javascripts/pictures.js.coffee
  mousedown = false
  canvas.mousedown (e)->
    ctx.prevPos = getPointPosition(e)
    mousedown = true
    ctx.putPoint(ctx.prevPos.x, ctx.prevPos.y)

マウスが動いている間は点と点を線で繋ぎます。
始点は一つ前のイベント発生時の座標、
終点は現在のイベント発生時の座標です。

// app/assets/javascripts/pictures.js.coffee
  canvas.mousemove (e)->
    return unless mousedown
    nowPos = getPointPosition(e)
    ctx.drawLine(ctx.prevPos.x, ctx.prevPos.y, nowPos.x, nowPos.y)
    ctx.putPoint(nowPos.x, nowPos.y)
    ctx.prevPos = nowPos

マウスの押下をやめた時と、canvasからポインタが出た時に描画を終了します。

// app/assets/javascripts/pictures.js.coffee
  canvas.mouseup (e)->
    mousedown = false
  canvas.mouseout (e)->
    mousedown = false

絵を消せる

キャンバス上の絵を全消去できるようにします。

erb

消去のボタンを作成します。
CSSのデザイン設定等は省略します。

<!--  app/views/pictures/new.html.erb -->
<span id="clear-button">消去</span>

coffee script

#clear-buttonが押された時、context.clearRectでcanvas全体を指定して消去します。

// app/assets/javascripts/pictures.js.coffee
  $("#clear-button").click ->
    ctx.clearRect(0, 0, canvas.width(), canvas.height())

描画を一回分戻れる

人間は失敗するものです。
描画を一回分戻れるようにしてあげます。

erb

戻すボタンを作成します。

<!-- app/views/pictures/new.html.erb -->
<span id="return-button" class="controll-button">戻す</span>

coffee script

contextに現在の状態を保存する機能を追加します。
getImageDataを使えば指定領域の描画情報を取得することができます。

// app/assets/javascripts/pictures.js.coffee
  ctx.savePrevData = ->
    @.prevImageData = @.getImageData(0, 0, canvas.width(), canvas.height())

canvas.mousedownに描画前の状態を保存する以下の一行追加します。

// app/assets/javascripts/pictures.js.coffee
  canvas.mousedown (e)->
    ctx.savePrevData() # この一行を追加
    ctx.prevPos = getPointPosition(e)
    mousedown = true
    ctx.putPoint(ctx.prevPos.x, ctx.prevPos.y)

ボタンを押したとき、contextのputImageDataを使って、保存してあったprevImageDataをcanvasに上書きします。

// app/assets/javascripts/pictures.js.coffee
  $("#return-button").click ->
    ctx.putImageData(ctx.prevImageData, 0, 0)

これで描画をやり直せます。
思う存分間違って大丈夫です。

線の太さを変えられる

線の太さは現在1pxの固定になっています。
これではつまらない!
これをHTML5のスライダーを使って変更できるようにします。

erb

HTML5のスライダーは、Railsのrange_field_tagヘルパーがあるので使います。
初期値1、最小値1、最大値20で作成しました。
その下には、現在の太さを表示するための領域を用意しました。

<!-- app/views/pictures/show.html.erb -->
    <%= range_field_tag 'pen-width-slider', 1, :min => 1, :max => 20 %>
    <span id="show-pen-width">1</span>px

coffee script

スライダーの操作はchangeイベントで捕捉できます。

// app/assets/javascripts/pictures.js.coffee
  $("#pen-width-slider").change ->
    ctx.lineWidth = $(@).val()
    $("#show-pen-width").text(ctx.lineWidth)

これで線の太さを変えられるようになりました。

色を変えられる

「黒一色じゃつまらないです。いろんな色で書きたいです。」
「やりましょう。」

erb

R,G,Bそれぞれのスライダーで実装します。
#preview-colorという、色をプレビューするボックスも用意しまいした。

<!-- app/views/pictures/show.html.erb" -->
    <%= range_field_tag 'pen-color-red-slider', 0, :min => 0, :max => 255 %>
    <span class="red">R</span> <span id="show-pen-red">0</span><br />
    <%= range_field_tag 'pen-color-green-slider', 0, :min => 0, :max => 255 %>
    <span class="green">G</span> <span id="show-pen-green">0</span><br />
    <%= range_field_tag 'pen-color-blue-slider', 0, :min => 0, :max => 255 %>
    <span class="blue">B</span> <span id="show-pen-blue">0</span><br />
    <div id="preview-color"></div>

scss

現在の色をプレビューするボックスのスタイルです。
ただの四角です。

/* app/assets/stylesheets/pictures.css.scss */
#preview-color{
  width: 30px;
  height: 30px;
  background-color: rgb(0, 0, 0);
  border: solid 1px black;
}

coffee script

contextに色変更の機能を加えます。
strokeStyleとfillStyleにスライダーの色を設定するだけです。

// app/assets/javascripts/pictures.js.coffee
  ctx.setColor = ->
    color = "rgb(#{red_slider.val()},#{green_slider.val()},#{blue_slider.val()})"
    @.strokeStyle = color
    @.fillStyle = color
    preview_color.css('background-color', color)

スライダーの操作を取得します。

// app/assets/javascripts/pictures.js.coffee
  red_slider = $("#pen-color-red-slider")
  green_slider = $("#pen-color-green-slider")
  blue_slider = $("#pen-color-blue-slider")
  preview_color = $("#preview-color")

  red_slider.change ->
    ctx.setColor()
    $("#show-pen-red").text($(@).val())
  green_slider.change ->
    ctx.setColor()
    $("#show-pen-green").text($(@).val())
  blue_slider.change ->
    ctx.setColor()
    $("#show-pen-blue").text($(@).val())

これで色を変えられるようになりました。

絵を投稿できる

さて、ついに絵をサーバ側に保存します。
仕様は適当に、
・public/imagesディレクトリに保存
・100件まで。超えたら古いものから削除していく。
です。

erb

保存するボタンを作ります。

<!-- app/views/pictures/show.html.erb -->
<span id="save-button" class="controll-button">保存</span>

coffee script

canvasでtoDataURLすると、base64エンコードされた画像文字列が取得できます。
これをpictures controllerのcreate actionにポストします。

// app/assets/javascripts/pictures.js.coffee
  $("#save-button").click ->
    url = canvas[0].toDataURL()
    $.post '/pictures', {data: url}

ruby

画像データ受信元のルーティングを追加します。

# config/routes.rb
  resources :pictures, only: [:create] # この一行を追加

ピクチャーモデルを作成します。
実際はmysqlの設定などしていますが、省略です。
今回、カラムはidしか使わないためマイグレーションはいじらずにさっさとdb:migrateしました。

$ rails generate model picture
$ rake db:migrate

コントローラの動作です。
ブラウザから送られてきた画像データから、
最初の文字列を削除してbase64デコードすると画像として保存できます。

# app/controllers/pictures_controller.rb
  def create
    path = 'public/images/'
    picture = Picture.create
    File.open("#{Rails.root}/#{path}/#{picture.id}.png", "wb") { |f|
      f.write Base64.decode64(params[:data].sub!('data:image/png;base64,', ''))
    }
    if Picture.count > 100
      picture = Picture.order(:id).first
      begin
        File.unlink("#{Rails.root}/#{path}/#{picture.id}.png", "wb")
      rescue
        # 適当な処理
      end
      picture.destroy
    end

    render :nothing => true
  end

これで画像を投稿できるようになりました。

投稿された画像の一覧を表示できる

画像は投稿できるようになったけれども、
投稿された画像を見ることができないので見れるようにします。

erb

ただの表示用div要素です。

<!-- app/views/pictures/show.html -->
<div id="pictures"></div>

ruby

pictures controllerにcreate actionのルーティングを追加します。

# coufig/routes.rb
  resources :pictures, only: [:index, :create]

rails側でrenderして、HTMLをクライアントに返しても良いですが、
今回はディレクトリの構造が安易なのでRailsはidだけ渡して、残りはブラウザで処理してもらいます。

# app/controllers/pictures_controller.rb
  def index
    @response = ''.tap do |me|
      Picture.all.reverse.each do |picture|
        me << "#{picture.id},"
      end
    end
  end
<!-- app/views/pictures/index.html.erb">
<%= @response %>

coffee script

小さい画像を表示します。
実際にリサイズなどしていないので注意です。
画像をクリックすると、キャンバスにその画像が上書きされるようにしました。
これで人の描いた絵に落書きできますね。

coffee script

// app/assets/javascripts/pictures.js.coffee
  reloadPictures = ->
    $.get '/pictures', (result)->
      ids = result.split(',')
      pictures = $("#pictures")
      pictures.empty()
      ids.forEach (id, i)->
        if parseInt(id) > 0
          pictures.append("<img src=\"/images/#{id}.png\" class=\"thumbnail\" />")
      # ここからコピー処理
      thumb_pics = $("#pictures .thumbnail")
      thumb_pics.click ->
        image = new Image()
        image.src = $(@).attr('src')
        image.onload = ->
          ctx.clearRect(0, 0, canvas.width(), canvas.height())
          ctx.drawImage(image, 0, 0)
  reloadPictures()

画像を保存するボタンを押した時の処理に、投稿画像一覧を更新する処理を入れます。

// app/assets/javascripts/pictures.js.coffee
  $("#save-button").click ->
    url = canvas[0].toDataURL()
    $.post '/pictures', {data: url}, (data)-> # この(data) -> 以降を追加
      reloadPictures()

以上です。
これで安易すぎるお絵かき投稿サイトができました。
デザインなど、各自調整してみてください。
下に見本を置いておきます。

完成品

ご自由に投稿して下さいませ。
http://canvas.d-adv.net/

github

ご自由にcloneして下さいませ。
git://github.com/ttakuru88/canvas_post.git
git@github.com:ttakuru88/canvas_post.git

色々書いたよ

クレイについてもっと知りたい方は…

  1. クレイの3つの強みを見てみる。
  2. WEBシステムのことなら何でもご相談ください。

「いいね!」で応援よろしくお願いします!

このエントリーに対するコメント

  1. 初めまして、だいぶ古い記事に対しての質問なのですがよろしいでしょうか?
    このシステムの画像保存部分なのですが、画像のファイル番号はどうやって決定しているのでしょうか?
    見たところデータベースで管理しているわけではないようなので、picture.idの振り方がわかりません。
    よろしければ教えていただけませんか?

    Joshua

    2013年01月23日, 11:22 AM

  2. コメントありがとうございます。
    記事中にある

    $ rails generate model picture

    というコマンドを実行したときに、idだけ持っているpicturesテーブルが自動で生成されます。
    idは勝手にユニークでインクリメンタルな値になるので、特にきにせずにpicture.idを使っています。

    takuru

    2013年01月23日, 8:23 PM

  3. なるほど、そんな機能があったんですね。
    すいません後1つあるんですが、
    このシステムを完成させた時、画像の保存をかけると1つ前の画像が保存されてしまいます。
    詳しく説明すると、先を2本描くと1本目を描いた状態のものが保存されます。その後連続して保存をかけると今度は2本描かれたものがちゃんと保存できるのですが、これはやはりcanvasのプログラムが悪いのでしょうか?
    もし、原因をご存知でしたら教えていただけないでしょうか?

    Joshua

    2013年01月24日, 2:58 AM

  4. うーん、なんでしょうね・・。
    こちらでは発生していない現象なので、コードを書いた順番などが間違っているのかもしれません。
    私の書いたコードはgithubにあるので見比べてみてください。

    https://github.com/asaumi/canvas_post

    takuru

    2013年01月25日, 9:28 AM

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)


トラックバック
  1. 編集後記:rails3 + html5 canvasでお絵かき投稿サイトを作ろう! | このコードわからん2011/11/17, 12:09 AM

    […] ついこの間、 rails3 + html5 canvasでお絵かき投稿サイトを作ろう! って記事を会社のブログに書いた。 […]

  2. あずみ.net » [Bookmark]rails3 + html5 canvasでお絵かき投稿サイトを作ろう! | KRAY Inc2013/02/19, 10:17 AM

    […] rails3 + html5 canvasでお絵かき投稿サイトを作ろう! | KRAY Inc […]

we use!!Ruby on RailsAmazon Web Services

このページの先頭へ