Edit in JSFiddle

<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>
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;
}
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', this.drag.bind(this));
    this.canvas.addEventListener('mouseup', this.dragEnd.bind(this));

    el.appendChild(this.canvas);
  }

  /**
   * 再描画する
   * @private
   * @return {void}
   */
  _redraw() {
    // canvasをクリア
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    // リサイズ
    this.ctx.scale(this.scale, this.scale);
    // 再描画
    this.ctx.drawImage(this.img, this.dragInfo.diffX, this.dragInfo.diffY);
    // 変形マトリクスを元に戻す
    this.ctx.scale(1 / this.scale, 1 / this.scale);
  }

  /**
   * 拡大/縮小する
   * @param {Event} event イベント
   * @return {void}
   */
  zoom(event) {
    this.scale = event.target.value;
    this._redraw();
  }

  /**
   * 拡大(ズームイン)する
   * @return {void}
   */
  zoomIn() {
    this.scale += this.scaleStep;
    this._redraw();
  }
  /**
   * 縮小(ズームアウト)する
   * @return {void}
   */
  zoomOut() {
    this.scale -= this.scaleStep;
    this._redraw();
  }

  /**
   * ドラッグ開始
   * @param {MouseEvent} event マウスイベント
   * @return {void}
   */
  dragStart(event) {
    this.dragInfo.isDragging = true;
    this.dragInfo.startX = event.clientX;
    this.dragInfo.startY = event.clientY;
  }
  /**
   * ドラッグで画像を移動する
   * @param {MouseEvent} event マウスイベント
   * @return {void}
   */
  drag(event) {
    if (this.dragInfo.isDragging) {
      // 開始位置 + 差分 / スケール (画像の大きさによる移動距離の補正のためスケールで割る)
      this.dragInfo.diffX = this.dragInfo.canvasX + (event.clientX - this.dragInfo.startX) / this.scale;
      this.dragInfo.diffY = this.dragInfo.canvasY + (event.clientY - this.dragInfo.startY) / this.scale;

      this._redraw();
    }
  }
  /**
   * ドラッグ終了
   * @param {MouseEvent} event マウスイベント
   * @return {void}
   */
  dragEnd(event) {
    this.dragInfo.isDragging = false;
    // mousedown時のカクつきをなくすため
    this.dragInfo.canvasX = this.dragInfo.diffX;
    this.dragInfo.canvasY = this.dragInfo.diffY;
  }

  /**
   * canvasを出力する
   * @return {Canvas}
   */
  getCanvas() {
    return this.canvas;
  }

  /**
   * imgを出力する
   * @return {Image}
   */
  getImage() {
    const img = new Image();
    const data = this.canvas.toDataURL('image/png');
    img.src = data;

    return img;
  }
}

function createImageEditor() {
  const input = document.getElementById('image-url');
  const editImage = document.getElementById('edit-image');

  if (editImage.hasChildNodes()) {
    // clear
    [...editImage.children].forEach(a => a.remove());
  }

  const imageEditor = new ImageEditor({
    imageSrc: input.value,
    canvasSize: 128
  });

  imageEditor.insertTo(editImage);

  // イベントを上書きする
  document.getElementById('export').onclick = () => {
    document.getElementById('output-image').appendChild(imageEditor.getImage());
  };

}

function main() {
  document.getElementById('load').addEventListener('click', createImageEditor);
}

main();