/** * イメージにいろんなフィルタをかける */ 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();