JavaScript + Canvas で特定の色だけ変更する

こんにちは。フリーランス・コンサルタント&エンジニアの 九保すこひ です。

さてさて、私はファッションが得意ではありませんが、少しでも多くの人に印象を残したいということでボトム(パンツ?)の色は赤で固定にしています。(赤は好きですし、ホント見た目に特徴がないので…)

これは、たくさんエネルギーを使うと言われている「意思決定」を日常から少しでも減らす役割もあります。(スティーブ・ジョブズが同じ服を着てたのは、これと同じ理由からだという説がありますよね)

それは良いのですが、先ほど言いましたとおりファッションに明るくないので、ネットショッピングをしていて困ることがあります。

それは・・・・・・「赤いボトムの写真」でしか、

仕上がりがイメージができない😅

ことです。

そのため、これまではいちいち画像をダウンロードして画像ソフトでボトムを赤に塗り直して確認していました。

しかし、それは果てしなくめんどくさいんです…。

そこで❗

せっかくエンジニアなので何かできないだろうか、と考えていたところ思いつきました。

Canvas を使えばできるじゃないか

と。

そこで❗

今回はJavaScript + Canvasで「指定した色だけ塗りつぶすことができる」機能をつくってみたいと思います。

ぜひ何かの参考になりましたら嬉しいです😄✨

「2021年の累計PVが
100万を超えました❗
皆さんホントにいつも
ありがとうございます✨」

開発環境: Vue 3

やりたいこと

では、まずはやりたいことですが、以下4点になります。

  • 画像ファイルを選択してプレビュー表示する
  • 対象にする色、塗りつぶす色は変更できる
  • 画像上をダブルクリックするとその色を取得できる
  • 選択した色に「近い色」だけ塗りつぶす

では、実際に作業に移りましょう❗

実際のコード

今回はJavaScript + HTMLだけですので、1ファイルで完了できます。
まずは実際のコードです。

<html>
<head>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet">
    <style>

        #canvas {

            cursor: crosshair;

        }

    </style>
</head>
<body>
<div id="app" class="p-5">
    <div class="h3">キャンバスの特定色を変更するサンプル</div>
    <label class="btn btn-primary">
        画像を選択する
        <input type="file" accept="image/*" class="d-none" @change="onFileChange">
    </label>
    <hr>
    プレビュー画像<br>
    <canvas
        id="canvas"
        class="mb-5"
        width="0"
        height="0"
        @dblclick="onCanvasDoubleClick($event)"
        @mousedown="onMouseDown"
        @mouseup="onMouseUp"
        @mousemove="onMouseMove($event)"></canvas>
    <div class="row">
        <div class="col-3">
            <div class="h5">どの色を?</div>
            <input type="color" v-model="colors.from">
            <br>
            <small>
                画像上をダブルクリックすると色を取得できます。
            </small>
        </div>
        <div class="col-3">
            <div class="h5">どの色へ変える?</div>
            <input type="color" v-model="colors.to">
        </div>
    </div>
</div>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.2/dist/umd/popper.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/js/bootstrap.min.js"></script>
<script src="https://unpkg.com/vue@3.1.1/dist/vue.global.prod.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js"></script>
<script>

    Vue.createApp({
        data() {
            return {
                canvas: null,
                ctx: null,
                colors: {
                    from: '#ffffff',
                    to: '#BC0C23'
                },
                threshold: 30, // 各色がプラスマイナスこの数字以内なら「似ている」と判断する,
                drawingLength: 30, // 色を変える範囲(px)
                isDragging: false
            }
        },
        methods: {
            isSimilarColor(color1, color2) { // ① 色が似ているかどうかチェックする

                const keys = ['red', 'green', 'blue'];
                let results = [];

                keys.forEach(key => {

                    const colorPart1 = color1[key];
                    const colorPart2 = color2[key];
                    const minColor = colorPart1 - this.threshold;
                    const maxColor = colorPart1 + this.threshold;
                    const result = (colorPart2 >= minColor && colorPart2 <= maxColor);
                    results.push(result);

                });

                return !results.includes(false);

            },
            onFileChange(e) { // ② ファイルが選択されたら画像をプレビュー表示する

                const files = e.target.files;

                if(files.length > 0) {

                    const file = files[0];
                    const reader = new FileReader();
                    reader.onload = e => {

                        let image = new Image();
                        image.onload = () => {

                            const width = image.width;
                            const height = image.height;
                            this.canvas.width = width;
                            this.canvas.height = height;
                            this.ctx.drawImage(image, 0, 0, width, height);

                        }
                        image.src = e.target.result;

                    };
                    reader.readAsDataURL(file);

                }

            },
            onCanvasDoubleClick(e) { // ③ プレビュー画像上をダブルクリックされたら、その位置の色を取得する

                const x = e.layerX;
                const y = e.layerY;
                const imageData = this.ctx.getImageData(x, y, 1, 1);
                this.colors.from = this.rgbToHex(imageData.data[0], imageData.data[1], imageData.data[2]);
                window.getSelection().empty();

            },
            onMouseDown() { // ④ マウスが押されたら、マウスドラッグ中

                this.isDragging = true;

            },
            onMouseUp() { // ④ マウスが離されたら、マウスドラッグ中ではない

                this.isDragging = false;

            },
            onMouseMove(e) { // ⑤ マウスドラッグ中に移動したら、対象の色のみ変更する

                if(this.isDragging === true) {

                    const drawingLength = this.drawingLength;
                    const mouseX = e.layerX;
                    const mouseY = e.layerY;
                    const minX = Math.max(0, mouseX - drawingLength);
                    const maxX = Math.min(this.canvas.width, mouseX + drawingLength);
                    const minY = Math.max(0, mouseY - drawingLength);
                    const maxY = Math.min(this.canvas.height, mouseY + drawingLength);

                    for(let x = minX ; x <= maxX ; x++) {

                        for(let y = minY ; y <= maxY ; y++) {

                            const imageData = this.ctx.getImageData(x, y, 1, 1);
                            const currentColors = {
                                red: imageData.data[0],
                                green: imageData.data[1],
                                blue: imageData.data[2]
                            };
                            const fromColors = this.hexToRgb(this.colors.from);
                            const toColors = this.hexToRgb(this.colors.to);

                            if(this.isSimilarColor(currentColors, fromColors)) {

                                imageData.data[0] = toColors.red;
                                imageData.data[1] = toColors.green;
                                imageData.data[2] = toColors.blue;
                                this.ctx.putImageData(imageData, x, y);

                            }

                        }

                    }

                }

            },

            // ⑥ 16進数 ↔ RGB 変換
            // from https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb
            hexToRgb(hex) {

                const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);

                if(result) {

                    return {
                        red: parseInt(result[1], 16),
                        green: parseInt(result[2], 16),
                        blue: parseInt(result[3], 16)
                    };

                }

                return null;

            },
            rgbToHex(r, g, b) {

                return '#' + ((1 << 24) + (r << 16) + (g << 8) + b)
                    .toString(16)
                    .slice(1);

            }
        },
        mounted() {

            // ⑦ どこからでもCanvasにアクセスできるように変数へ格納する
            this.canvas = document.querySelector('#canvas');
            this.ctx = this.canvas.getContext('2d');

        }
    }).mount('#app');

</script>
</body>
</html>

では、少しコードが長いのでひとつずつ見ていきましょう❗

① 色が似ているかどうかチェックする

ここで、color1color2が「近い色なのかどうか」をチェックしています。

実際には、RBG3つの値がしきい値(今回は±30)の範囲内に入っていれば似ていると判別しています。

そのため、thresholdを変更することでこの判別の強弱を設定することができます。

なお、最後の!results.includes()は、RBGチェックの結果 ひとつでもfalseが入っていれば、falseが帰るようになっています。

例えば、以下のようなパターンになります。

[false, false, false] → もちろん false
[false, true, false] → 2つも false があるので false
[false, true, true] → おしいけど、1つ false があるので false
[true, true, true] → これだけ true

② ファイルが選択されたら画像をプレビュー表示する

選択された画像をJavaScript内部で呼び出し、「横幅&高さ」を取得、さらに<canvas></canvas>タグにその画像を書き込んでいます。

なお、FileReaderは非同期処理になります。

③ プレビュー画像上をダブルクリックされたら、その位置の色を取得する

流れは以下のとおりです。

  1. クリックされた位置(X, Y)を取得
  2. その位置にある色をキャンバスから取得
  3. 変更対象の色としてセット

ちなみに、最後のwindow.getSelection().empty();はダブルクリックしたときにテキストが選択状態になるのが気になったのでつけていますが、なくても問題ありません。(これChromeだけかな??)

④ マウスが押されたら、マウスドラッグ中&マウスが離されたら、マウスドラッグ中ではない:切り替え

マウスが動いているのは、mousemoveイベントで取得できるのですが、マウスドラッグ(クリックしたままの状態でマウスを動かすこと)をしているかどうかをチェックするには、

  • マウスが押されたら isDragging を true にする
  • マウスが離されたら isDragging を false にする

という切り替えを利用します。

つまり、isDraggingtrueのときは「マウスのボタンをクリックしたまま離していない」ということになるので、つまり「マウスドラッグ中」ということになります。

⑤ マウスドラッグ中に移動したら、対象の色のみ変更する

ここが今回のメインになります。

まず、色を変更するエリアですがさすがに1x1ピクセルだけでは「エスパー伊東さんのホワイトボード塗りつぶしチャレンジ」のようにいつまで経っても終わらないので、ある程度範囲を広げています。(知らない方はスルーしてください😅)

その範囲の長さがdrawingLengthです。(実際にはクリックした場所から±30ピクセルの正方形【60 x 60 ピクセル】が塗りつぶし対象エリアになります)

そして、この範囲の色をひとつずつisSimilarColor()でチェックし、もし似ているなら別の色に塗りつぶす、という流れになっています。

⑥ 16進数 ↔ RGB 変換

今回の機能を実装するに当たって、16進数 ↔ RGBの変換をする必要があったのですが、正直私の頭では一生わからないのでstackoverflowさんに聞いてみました。(こういうコードがすらすら書ける人はどんな脳の構造なんでしょうね…😂)

参照元: https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb

⑦ どこからでもCanvasにアクセスできるように変数へ格納する

これはタイトルそのままです!

テストしてみる

では、実際にテストしてみましょう❗
利用する画像は以下です。


写真:lannyboy89

Pixabayさんいつもありがとうございます。ちなみに私のファッションはこんなにワイルドではありません。念のため😂

では、ブラウザで先ほどのコードへアクセスします。

【⚠ご注意】チェックしていませんが、もしかするとウェブサーバー経由(http://とかのURLですね)でないとうまく動かないかもしれません。

すると、以下のような表示になりますので、早速さきほどの画像を選択してみましょう。

すると・・・・・・

ワイルドな写真がプレビュー表示されました!

では、ボトムの色を赤へ変更したいのでダブルクリックして色を取得してみます。

どうなったでしょうか・・・・・・??

はい❗
最初は白だったのが、ダークグレーに変更になりました。

では、この状態でボトム部分をマウスドラッグしてみましょう。

結果は・・・・・・???

はい❗
何度か色の取得&描画を繰り返しましたが、ファッションのイメージをすることはできそうです。

成功です😄✨

デモをつくりました

せっかくなので、デモページをつくりました。
ぜひ以下からお試しください。(画像は一切アップロードされませんので、ご自身の写真でも安心して使っていただけます👍)

デモページ

企業様へのご提案

今回の機能を使えば、よりお客様の好みに沿ったイメージをしてもらうことができると考えています。

また、オーダーメイドなどこれから作るものをブラウザ上でシミュレートできるのも販売促進にはプラスになるんじゃないでしょうか。(どこかのメガネメーカーさんがパソコン上で、お客さんの顔写真にこれからつくるメガネを重ねて販売していました)

もしそういったシステムをご希望でしたら、お気軽にお問い合わせからご連絡ください。

お待ちしております。m(_ _)m

開発のご依頼お待ちしております
開発のご依頼はこちらから: お問い合わせ
どうぞよろしくお願いいたします! by 九保すこひ

おわりに

ということで、今回はJavaScript + Canvasで特定の色だけを変更してみました。

ちなみに、当初は画像の中にある特定の色すべてを変更する仕様だったのですが、他の場所にも似た色が存在していると、そこまで色が変更になってしまうので、今回のようにアップデートしました。(色が赤なので、とある画像だと「どう見ても事件性ありでしょ…」というような流血してるっぽい画像になってしまいました😅)

なお、今回塗りつぶす色は全て同じ色でしたが、選択した色と対象の色との差分が計算すればよりリアルな映像のまま色変更できると思います。

それにしても、こういう作業をするとやっぱり数学ができる人ってすごいなと感じます。

うーん、、、私は別の分野で勝負することにしますね。

ではでは〜❗

「電子レンジ待ってるときは、
ジークンドーのパンチ真似してます」

このエントリーをはてなブックマークに追加       follow us in feedly