HTML canvas で画像を切り取る(crop) Javascript サンプルコード

HTML canvas & Javascript を使った画像の切り抜き「Crop」のサンプルコードです。

ブラウザ上で画像を加工しようとしたら、canvas しかないわけですが、
いちばんやりたいのは「切り抜き：Crop」なんだけど、まぁ、シンプルなコードがほしかったのにみつから無いわけで。
無いときは自分で作るのがエンジニアの掟なわけです。

## 切り取る（crop） Demo

![Mar-02-2019 15-57-23.gif](https://qiita-image-store.s3.amazonaws.com/0/107592/26190d50-41c1-84ae-40ee-0bc0fa7c2fad.gif)
中央の枠の中を切り出すタイプ。先に切り出す枠を決めておいて、画像の方を確認しながら切り出す。
なんと言いますか、マウスで角から角へドラッグしたり、枠をつまんで移動するUIよりも、このほうが直感的でわかりやすいし、スマホとかタブレットにも向いているUIだと思うのです。

## Crop してますか？
実際に触ってみてもらうのが早いでしょう。⬇︎

- canvas 上で、マウスをドラッグすると切り抜く場所を変えられます。
- canvas 上で、マウスのホイールをグリグリすると拡大率が変わります。
- 「CROP」ボタンで、赤枠の部分が切り抜きされます。
- 上部の「A」「B」「C」のボタンで画像が変わります。


<p class="codepen" data-height="681" data-theme-id="0" data-default-tab="result" data-user="yamazaki3104" data-slug-hash="qvOyGL" style="height: 681px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid black; margin: 1em 0; padding: 1em;" data-pen-title="HTML crop canvas">
  <span>See the Pen <a href="https://codepen.io/yamazaki3104/pen/qvOyGL/">
  HTML crop canvas</a> by yamazaki.3104 (<a href="https://codepen.io/yamazaki3104">@yamazaki3104</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

## 実際のコードがこちら

Qiita を見ているひとたちに、細かい解説は不要だとは思うので、ざっと概要のみ書く

 - 主な canvas は編集用の id='cvs' と、ボタン「CROP」を押したあとに編集結果が入る id='out' の２つ
 - ボタン「A」「B」「C」は入力画像の変更を行う load_img() を呼ぶだけ。読み込まれた画像は img に入っていて、img は読み出すだけで加工はしない。
 - スライダーは id='scal' 画像の拡大縮小率を変えられるように用意したが、最終的にホイールイベントに対応したので、UIとしてはあまり役に立っていないかもしれない
 - 画像の切り抜き処理は drawImage() がやってます
 - イベントは、マウスとホイールとタッチを取っています（タッチイベントはスマホ＆タブレット対応）
 - あー、一番のポイントは、外部ライブラリは未使用で、素のJavascriptのみで実装している点


特に難しいことは何もやっていない、平凡なコードだと思っています。マウスのドラッグで画像の位置を変えるコードがいちばん大変だった、、、とはいっても、HTMLも入れて90行程度なので、、、まぁ、半日くらいの作業量でしょうか。

```HTML
<html>
<body>
<button onclick="load_img('https://lh3.ggpht.com/O0aW5qsyCkR2i7Bu-jUU1b5BWA_NygJ6ui4MgaAvL7gfqvVWqkOBscDaq4pn-vkwByUx=w300')">A</button>
<button onclick="load_img('https://www.mozilla.org/media/img/logos/firefox/logo-quantum-wordmark-white.bd1944395fb6.png')">B</button>
<button onclick="load_img('')">C</button><br>
<input id='scal' type='range' value='' min='10' max='400' oninput="scaling(value)" style='width: 300px;'><br>
<canvas id='cvs' width='300' height='400'></canvas><br>
<button onclick='crop_img()'>CROP</button><br>
<canvas id='out' width='200' height='200'></canvas>
<script>
    const cvs = document.getElementById( 'cvs' )
    const cw = cvs.width
    const ch = cvs.height
    const out = document.getElementById( 'out' )
    const oh = out.height
    const ow = out.width

    let ix = 0    // 中心座標
    let iy = 0
    let v = 1.0   // 拡大縮小率
    const img  = new Image()
    img.onload = function( _ev ){   // 画像が読み込まれた
        ix = img.width  / 2
        iy = img.height / 2
        let scl = parseInt( cw / img.width * 100 )
        document.getElementById( 'scal' ).value = scl
        scaling( scl )
    }
    function load_img( _url ){  // 画像の読み込み
        img.src = ( _url ? _url : 'https://1.bp.blogspot.com/-AoQB8vIvlcw/W8BOcXcEQ6I/AAAAAAABPZM/rXNbol90tXcxBZBlXsg__xix03b_F4nqwCLcBGAs/s800/pet_cat_omoi_sleep_man.png' )
    }
    load_img()
    function scaling( _v ) {        // スライダーが変った
        v = parseInt( _v ) * 0.01
        draw_canvas( ix, iy )       // 画像更新
    }

    function draw_canvas( _x, _y ){     // 画像更新
        const ctx = cvs.getContext( '2d' )
        ctx.fillStyle = 'rgb(200, 200, 200)'
        ctx.fillRect( 0, 0, cw, ch )    // 背景を塗る
        ctx.drawImage( img,
            0, 0, img.width, img.height,
            (cw/2)-_x*v, (ch/2)-_y*v, img.width*v, img.height*v,
        )
        ctx.strokeStyle = 'rgba(200, 0, 0, 0.8)'
        ctx.strokeRect( (cw-ow)/2, (ch-oh)/2, ow, oh ) // 赤い枠
    }
    function crop_img(){                // 画像切り取り
        const ctx = out.getContext( '2d' )
        ctx.fillStyle = 'rgb(200, 200, 200)'
        ctx.fillRect( 0, 0, ow, oh )    // 背景を塗る
        ctx.drawImage( img,
            0, 0, img.width, img.height,
            (ow/2)-ix*v, (oh/2)-iy*v, img.width*v, img.height*v,
        )
    }

    let mouse_down = false      // canvas ドラッグ中フラグ
    let sx = 0                  // canvas ドラッグ開始位置
    let sy = 0
    cvs.ontouchstart =
    cvs.onmousedown = function ( _ev ){     // canvas ドラッグ開始位置
        mouse_down = true
        sx = _ev.pageX
        sy = _ev.pageY
        return false // イベントを伝搬しない
    }
    cvs.ontouchend =
    cvs.onmouseout =
    cvs.onmouseup = function ( _ev ){       // canvas ドラッグ終了位置
        if ( mouse_down == false ) return
        mouse_down = false
        draw_canvas( ix += (sx-_ev.pageX)/v, iy += (sy-_ev.pageY)/v )
        return false // イベントを伝搬しない
    }
    cvs.ontouchmove =
    cvs.onmousemove = function ( _ev ){     // canvas ドラッグ中
        if ( mouse_down == false ) return
        draw_canvas( ix + (sx-_ev.pageX)/v, iy + (sy-_ev.pageY)/v )
        return false // イベントを伝搬しない
    }
    cvs.onmousewheel = function ( _ev ){    // canvas ホイールで拡大縮小
        let scl = parseInt( parseInt( document.getElementById( 'scal' ).value ) + _ev.wheelDelta * 0.05 )
        if ( scl < 10  ) scl = 10
        if ( scl > 400 ) scl = 400
        document.getElementById( 'scal' ).value = scl
        scaling( scl )
        return false // イベントを伝搬しない
    }
</script>
</body>
</html>
```

## 動作確認環境

いまのところ

 - Chrome バージョン: 72.0.3626.109（Official Build） （64 ビット）
 - Safari バージョン 12.0.2 (14606.3.4)
 - Edge 42.17134.1.0

でしか確認してません。ごめんなさい。
他の環境で確認できたら、追記します。

スマホ＆タブレット向けにタッチイベントに対応したつもり

 - iOS 版 Google Chrome 72.0.3626.101
 - iOS 版 Safari 12.0

で動作確認すみ

2019/03/03: 追記
Firefox 65.0.2 で確認したらホイールのイベントが取れてなかった、、、なぜ？？（原因不明の助）