HTML5 Canvas Zoom and Pan Image

by bc_rikko

HTML

<h1>Image Editor</h1>

<form class="container">
  <h2>Input</h2>
  <input type="text" id="image-url" value="https://ja.wikipedia.org/static/images/project-logos/jawiki.png">
  <button type="button" id="load">load</button>
  <h2>How to use</h2>
  <ol>
    <li>画像URLを入力し、loadボタンで画像を読み込む(※ CORSが許可されている画像のみ)</li>
    <li>zoom-in, zoom-out, ドラッグ&ドロップで画像を編集する</li>
    <li>exportボタンで画像を出力する</li>
  </ol>
</form>

<div class="container">
  <h2>Editor</h2>
  <div class="form">
    <button type="button" id="export">export</button>
  </div>
  <div id="edit-image"></div>
</div>

<div class="container">
  <h2>Output</h2>
  <div id="output-image"></div>
</div>

CSS

img, canvas {
  border: 4px dashed rgb(221, 221, 221);
}

.container {
  width: 90%;
  margin: 10px;
  padding: 10px;
  background-color: white;
  box-shadow: 0 1px 2px rgba(66, 66, 66, 0.7);
}

#output-image > img {
  margin: 5px;
}

JavaScript

class ImageEditor {
  /**
   * Canvas操作
   * @param {Object} opt 
   * @param {string} opt.imageSrc イメージのURL
   * @param {string} opt.canvasId canvasタグのid(デフォルト: image-for-edit)
   * @param {Number} opt.canvasSize canvasのサイズ(デフォルト: 128px)
   * @param {Number} opt.scaleStep 拡大縮小の倍率(デフォルト: 0.25)
   */
  constructor(opt = {}) {
    this.src = opt.imageSrc || 'https://ja.wikipedia.org/static/images/project-logos/jawiki.png';
    this.id = opt.canvasId || 'image-for-edit';
    this.size = opt.canvasSize || 128;
    this.scaleStep = opt.scaleStep || 0.25;

    this.scale = 1;
    this.dragInfo = {
      isDragging: false,
      startX: 0,
      startY: 0,
      diffX: 0,
      diffY: 0,
      canvasX: 0,
      canvasY: 0
    };
  }

  /**
   * canvasを挿入する
   * @param {HTMLElement} el canvasを挿入する親要素
   * @return {void}
   */
  insertTo(el) {
    const container = document.createElement('div');
    el.appendChild(container);

    // slider
    const zoomSlider = document.createElement('input');
    zoomSlider.type = 'range';
    zoomSlider.min = 0.01;
    zoomSlider.max = 2;
    zoomSlider.value = 1;
    zoomSlider.step = 'any';
    zoomSlider.addEventListener('input', this.zoom.bind(this));
    container.appendChild(zoomSlider);

    // canvas
    this.canvas = document.createElement('canvas');
    this.ctx = this.canvas.getContext('2d');

    this.canvas.id = this.id;
    this.canvas.width = this.canvas.height = this.size;

    this.img = new Image();
    this.img.crossOrigin = 'anonymous';   // 「Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported.」というエラーになるため
    this.img.src = this.src;
    this.img.onload = () => {
      this.ctx.drawImage(this.img, 0, 0);
    };
    this.img.onerror = e => {
      [...el.children].forEach(a => a.remove());
      alert('画像読み込み失敗');
    };

    // mouse event
    this.canvas.addEventListener('mousedown', this.dragStart.bind(this));
    this.canvas.addEventListener('mousemove',...