Edit in JSFiddle

/**
 * イメージにいろんなフィルタをかける
 */
 class ImageFilter {
  /**
   * @param {Object} param パラメータ
   * @param {string} param.el 画像を表示する要素のセレクタ
   * @param {string} param.imageSrc 画像ファイルのURL
   * @param {number} param.width Canvasの幅
   * @param {number} param.height Canvasの高さ
   */
  constructor(param) {
    this.el = document.querySelector(param.el);
    this.src = param.imageSrc;
    this.canvasWidth = param.width || 200;
    this.canvasHeight = param.height || 200;

    this.filters = {
      'nofilter'    : this._nofilter.bind(this),
      'grayscale'   : this._grayscale.bind(this),
      'inversion'   : this._inversion.bind(this),
      'binarization': this._binarization.bind(this),
      'gamma'       : this._gamma.bind(this),
      'blur'        : this._blur.bind(this),
      'sharpen'     : this._sharpen.bind(this),
      'median'      : this._median.bind(this),
      'emboss'      : this._emboss.bind(this),
      'mosaic'      : this._mosaic.bind(this)
    };
  }

  /**
   * 初期化
   */
  init() {
    // Canvas
    const original = this._createCanvas('original');
    const preview = this._createCanvas('preview');

    // 描画順調整のため遅延
    setTimeout(() => {
      this.el.appendChild(original);
      this.el.appendChild(preview);

      this.draw(original, this._nofilter);
      this.draw(preview, this._nofilter);
    }, 100);

    // Radios
    const radios = document.createElement('div');
    this.el.appendChild(radios);

    Object.keys(this.filters).forEach(type => {
      const radio = this._createRadios(type, preview);
      radios.appendChild(radio);
    });
  }

  /**
   * ラジオボタンを生成する
   * @param {string} filterType this.filtersのフィルタタイプ
   * @param {HTMLCanvasElement} canvas ラジオボタンがクリックされたときにフィルタを適用するCanvas
   * @return {HTMLLabelElement} labelタグでラップしたラジオボタン
   */
  _createRadios(filterType, canvas) {
    const input = document.createElement('input');
    input.type = 'radio';
    input.name = 'filters';
    input.checked = filterType === 'nofilter';
    input.value = filterType;

    const label = document.createElement('label');
    const span = document.createElement('span');
    span.innerText = filterType;

    label.appendChild(input);
    label.appendChild(span);

    input.addEventListener('change', e => {
      this.draw(canvas, this.filters[filterType]);
    });

    return label;
  }

  /**
   * Canvasを生成する
   * @param id 生成するCanvasのID
   * @return {HTMLCanvasElement} 生成したCanvas
   */
  _createCanvas(id) {
    const canvas = document.createElement('canvas');
    canvas.id = id;
    canvas.className = 'canvas'
    canvas.width = this.canvasWidth;
    canvas.height = this.canvasHeight;

    return canvas;
  }

  /**
   * Canvasに画像を描画し、フィルタを適用する
   * @param {HTMLCanvasElement} canvas 描画するCanvas
   * @param {Function} imageFilter フィルタ関数
   */
  draw(canvas, imageFilter) {
    const ctx = canvas.getContext('2d');
    ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight);

    const img = new Image();
    img.crossOrigin = 'anonymous';
    img.src = this.src;
    img.onload = () => {
      ctx.drawImage(img, 0, 0, this.canvasWidth, this.canvasHeight);

      const imageData = ctx.getImageData(0, 0, this.canvasWidth, this.canvasHeight);
      const data = imageData.data;

      // filter
      imageFilter(data);

      ctx.putImageData(imageData, 0, 0);
    };

  }

  //----------------------------------------------
  // フィルタ関数
  //----------------------------------------------

  /**
   * フィルタなし
   * @param {Array<Number>} data ImageData.dataの配列(dataを書き換える)
   */
  _nofilter(data) {
    /* nop */
    return data;
  }

  /**
   * グレースケール
   * @param {Array<Number>} data ImageData.dataの配列(dataを書き換える)
   */
  _grayscale(data) {
    for (let i = 0; i < data.length; i += 4) {
      // (r+g+b)/3
      const color = (data[i] + data[i+1] + data[i+2]) / 3;
      data[i] = data[i+1] = data[i+2] = color;
    }

    return data;
  }

  /**
   * 階調反転
   * @param {Array<Number>} data ImageData.dataの配列(dataを書き換える)
   */
  _inversion(data) {
    for (let i = 0; i < data.length; i += 4) {
      // 255-(r|g|b)
      data[i]   = Math.abs(255 - data[i])  ;
      data[i+1] = Math.abs(255 - data[i+1]);
      data[i+2] = Math.abs(255 - data[i+2]);
    }

    return data;
  }

  /**
   * 二値化
   * @param {Array<Number>} data ImageData.dataの配列(dataを書き換える)
   */
   _binarization(data) {
    const threshold = 255 / 2;

    const getColor = (data, i) => {
      // threshold < rgbの平均
      const avg = (data[i] + data[i+1] + data[i+2]) / 3;
      if (threshold < avg) {
        // white
        return 255;
      } else {
        // black
        return 0;
      }
    };

    for (let i = 0; i < data.length; i += 4) {
      const color = getColor(data, i);
      data[i] = data[i+1] = data[i+2] = color;
    }

    return data;
  }

  /**
   * ガンマ補正
   * @param {Array<Number>} data ImageData.dataの配列(dataを書き換える)
   */
  _gamma(data) {
    // 補正値(1より小さい:暗くなる、1より大きい明るくなる)
    const gamma = 2.0;
    // 補正式
    const correctify = val => 255 * Math.pow(val / 255, 1 / gamma);

    for (let i = 0; i < data.length; i += 4) {
      data[i]   = correctify(data[i]);
      data[i+1] = correctify(data[i+1]);
      data[i+2] = correctify(data[i+2]);
    }

    return data;
  }

  /**
   * ぼかし(3x3)
   * @param {Array<Number>} data ImageData.dataの配列(dataを書き換える)
   */
  _blur(data) {
    const _data = data.slice();
    const avgColor = (color, i) => {
      const prevLine = i - (this.canvasWidth * 4);
      const nextLine = i + (this.canvasWidth * 4);

      const sumPrevLineColor = _data[prevLine-4+color] + _data[prevLine+color] + _data[prevLine+4+color];
      const sumCurrLineColor = _data[i       -4+color] + _data[i       +color] + _data[i       +4+color];
      const sumNextLineColor = _data[nextLine-4+color] + _data[nextLine+color] + _data[nextLine+4+color];

      return (sumPrevLineColor + sumCurrLineColor + sumNextLineColor) / 9
    };

    // 2行目〜n-1行目
    for (let i = this.canvasWidth * 4; i < data.length - (this.canvasWidth * 4); i += 4) {
      // 2列目〜n-1列目
      if (i % (this.canvasWidth * 4) === 0 || i % ((this.canvasWidth * 4) + 300) === 0) {
        // nop
      } else {
        data[i]   = avgColor(0, i);
        data[i+1] = avgColor(1, i);
        data[i+2] = avgColor(2, i);
      }
    }

    return data;
  }

  /**
   * シャープ化
   * @param {Array<Number>} data ImageData.dataの配列(dataを書き換える)
   */
  _sharpen(data) {
    const _data = data.slice();
    const sharpedColor = (color, i) => {
      // 係数
      //  -1, -1, -1
      //  -1, 10, -1
      //  -1, -1, -1
      const sub = -1;
      const main = 10;

      const prevLine = i - (this.canvasWidth * 4);
      const nextLine = i + (this.canvasWidth * 4);

      const sumPrevLineColor = (_data[prevLine-4+color] * sub)  +  (_data[prevLine+color] * sub )  +  (_data[prevLine+4+color] * sub);
      const sumCurrLineColor = (_data[i       -4+color] * sub)  +  (_data[i       +color] * main)  +  (_data[i       +4+color] * sub);
      const sumNextLineColor = (_data[nextLine-4+color] * sub)  +  (_data[nextLine+color] * sub )  +  (_data[nextLine+4+color] * sub);

      return (sumPrevLineColor + sumCurrLineColor + sumNextLineColor) / 2
    };

    // 2行目〜n-1行目
    for (let i = this.canvasWidth * 4; i < data.length - (this.canvasWidth * 4); i += 4) {
      // 2列目〜n-1列目
      if (i % (this.canvasWidth * 4) === 0 || i % ((this.canvasWidth * 4) + 300) === 0) {
        // nop
      } else {
        data[i]   = sharpedColor(0, i);
        data[i+1] = sharpedColor(1, i);
        data[i+2] = sharpedColor(2, i);
      }
    }

    return data;
  }

  /**
   * メディアンフィルタ
   * @param {Array<Number>} data ImageData.dataの配列(dataを書き換える)
   */
  _median(data) {
    const _data = data.slice();
    const getMedian = (color, i) => {
      // 3x3の中央値を取得
      const prevLine = i - (this.canvasWidth * 4);
      const nextLine = i + (this.canvasWidth * 4);

      const colors = [
        _data[prevLine-4+color], _data[prevLine+color], _data[prevLine+4+color],
        _data[i       -4+color], _data[i       +color], _data[i       +4+color],
        _data[nextLine-4+color], _data[nextLine+color], _data[nextLine+4+color],
      ];

      colors.sort((a, b) => a - b);
      return colors[Math.floor(colors.length / 2)];
    };

    // 2行目〜n-1行目
    for (let i = this.canvasWidth * 4; i < data.length - (this.canvasWidth * 4); i += 4) {
      // 2列目〜n-1列目
      if (i % (this.canvasWidth * 4) === 0 || i % ((this.canvasWidth * 4) + 300) === 0) {
        // nop
      } else {
        data[i]   = getMedian(0, i);
        data[i+1] = getMedian(1, i);
        data[i+2] = getMedian(2, i);
      }
    }

    return data;
  }

  /**
   * エンボス
   * @param {Array<Number>} data ImageData.dataの配列(dataを書き換える)
   */
  _emboss(data) {
    const _data = data.slice();
    const embossColor = (color, i) => {
      // 係数
      //  -1,  0,  0
      //   0,  1,  0
      //   0,  0,  0
      // → + (255 / 2)

      const prevLine = i - (this.canvasWidth * 4);
      return ((_data[prevLine-4+color] * -1) + _data[i+color]) + (255 / 2);
    };

    // 2行目〜n-1行目
    for (let i = this.canvasWidth * 4; i < data.length - (this.canvasWidth * 4); i += 4) {
      // 2列目〜n-1列目
      if (i % (this.canvasWidth * 4) === 0 || i % ((this.canvasWidth * 4) + 300) === 0) {
        // nop
      } else {
        data[i]   = embossColor(0, i);
        data[i+1] = embossColor(1, i);
        data[i+2] = embossColor(2, i);
      }
    }

    return data;
  }

  /**
   * モザイク
   * @param {Array<Number>} data ImageData.dataの配列(dataを書き換える)
   */
  _mosaic(data) {
    const _data = data.slice();

    const avgColor = (i, j, color) => {
      // 3x3の平均値
      const prev = (((i - 1) * this.canvasWidth) + j) * 4;
      const curr = (( i      * this.canvasWidth) + j) * 4;
      const next = (((i + 1) * this.canvasWidth) + j) * 4;

      const sumPrevLineColor = _data[prev-4+color] + _data[prev+color] + _data[prev+4+color];
      const sumCurrLineColor = _data[curr-4+color] + _data[curr+color] + _data[curr+4+color];
      const sumNextLineColor = _data[next-4+color] + _data[next+color] + _data[next+4+color];

      return (sumPrevLineColor + sumCurrLineColor + sumNextLineColor) / 9;
    };

    // 3x3ブロックずつ色をぬる
    for (let i = 1; i < this.canvasWidth; i += 3) {
      for (let j = 1; j < this.canvasHeight; j += 3) {

        const prev = (((i - 1) * this.canvasWidth) + j) * 4;
        const curr = (( i      * this.canvasWidth) + j) * 4;
        const next = (((i + 1) * this.canvasWidth) + j) * 4;

        ['r', 'g', 'b'].forEach((_, color) => {
          data[prev-4+color] = data[prev+color] = data[prev+4+color] = avgColor(i, j, color);
          data[curr-4+color] = data[curr+color] = data[curr+4+color] = avgColor(i, j, color);
          data[next-4+color] = data[next+color] = data[next+4+color] = avgColor(i, j, color);
        });
      }
    }

    return data;
  }
}


// main
function main() {
  const imageFilter = new ImageFilter({
    el: '#app',
    imageSrc: 'https://upload.wikimedia.org/wikipedia/commons/5/52/Sinsinawa_640_480.jpg',
    width: 320,
    height: 240
  });
  imageFilter.init();
}
main();