【JavaScript】ドット絵のおもちゃ

## はじめに
Amazonでドット絵の<a href="https://www.amazon.co.jp/Divoom-%E3%82%A2%E3%83%97%E3%83%AA%E3%81%A8Divoom-Pixoo%E3%83%87%E3%82%B8%E3%82%BF%E3%83%AB%E3%83%95%E3%83%AC%E3%83%BC%E3%83%A0%E3%81%AF16%C3%9716%E3%81%AELED%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%92%E5%88%B6%E5%BE%A1%EF%BC%88%E3%83%96%E3%83%A9%E3%83%83%E3%82%AF%EF%BC%89/dp/B07HHXWN3C/ref=pd_day0fbt_img_sccl_2/355-3375078-0092017?pd_rd_w=26xYL&content-id=amzn1.sym.a46a2d2a-0d7e-4eb6-a098-4bb0691a3c33&pf_rd_p=a46a2d2a-0d7e-4eb6-a098-4bb0691a3c33&pf_rd_r=0KMNXWNGE6ZMG2DPXXJG&pd_rd_wg=uYrhn&pd_rd_r=41efe17a-b9fa-4a95-b4b2-d73b11db325f&pd_rd_i=B07HHXWN3C&psc=1">おもちゃ</a>見つけたんですが、なんかそれっぽいものJavaScriptで書いてみようってことで書いてみました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163605/95636ff0-d699-14c2-bd35-764d35e92b15.png)


## 本ライブラリの使い方
名前はPixelJSとしました。
ImageDataを受け取り、それをドット絵のおもちゃっぽくして返します。
ドットのサイズやドットの間隔を指定できます。
ドットぽくするプログラムはちょっといい加減なので、ご不満な方は直してご使用ください。
ImageDataを返すだけなので、アニメーションをつける場合などは自分でプログラムしてください。
下の画像は、今回作成したライブラリを利用して作ったものです。

![ダウンロード.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163605/83fa1d1a-52f4-f54b-c627-7158153383a2.gif)


## ソース
ライブラリとライブラリを呼び出すサンプルプログラムです。
サンプルでは画像データをjsに埋め込んでいますが、ImageDataは自由に作成してください。

```html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>PixelJS Demo</title>
<script>
// このPixelJSというクラスを定義してください。別ファイルにしてもよいし、コピペでも何でもよいです。
class PixelJS {
    /**
     * 設定(ピクセルや枠を設定します)
     * @param {number} pixelSize - ピクセルのサイズ
     * @param {number} frameSize - 枠のサイズ
     * @param {number[4]} frameColor - 枠の色情報
     * @return {void}
     */
    static setting(pixelSize = 16, frameSize = 4, frameColor = [0, 0, 0, 255]) {
        this.pixelSize = pixelSize > 0 ? pixelSize : 16;
        this.frameSize = frameSize > 0 ? frameSize : 4;
        this.frameColor = frameColor ? frameColor : [0, 0, 0, 255];
    }

    /**
     * イメージデータを受け取りイメージデータを返します
     * @return {ImageData} ドット絵っぽいImageData
     */
    static getImageData(imgData) {
        const width = imgData.width,
            height = imgData.height;
        const canvas = document.createElement('canvas');
        canvas.width = this.pixelSize * width + this.frameSize * (width + 1);
        canvas.height = this.pixelSize * height + this.frameSize * (height + 1);
        const ctx = canvas.getContext('2d');

        // 枠の色で塗る
        ctx.fillStyle = `rgba(${this.frameColor[0]},${this.frameColor[1]},${this.frameColor[2]},${this.frameColor[3]})`;
        ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);

        // パレットを作成する。
        const palette = [];
        const paretteCanvas = [];

        for(let y = 0; y < height; y += 1) {
            for(let x = 0; x < width; x += 1) {
                const index = y * width + x;
                // 色を取得
                const r = imgData.data[index * 4 + 0],
                    g = imgData.data[index * 4 + 1],
                    b = imgData.data[index * 4 + 2],
                    a = imgData.data[index * 4 + 3];
                if(a === 0) { continue; }
                const tmpColor = [ r, g, b, a ];
                let paletteIndex = palette.findIndex(elm => elm.toString() === tmpColor.toString());
                if(paletteIndex === -1) {// 登録されていない色
                    palette.push(tmpColor);
                    const pCanvas = document.createElement('canvas');
                    pCanvas.width = this.pixelSize;
                    pCanvas.height = this.pixelSize;
                    const pctx = pCanvas.getContext('2d');
                    // ピクセルの塗りつぶしのロジックがいい加減なのでこの辺を直した方がよいかもしれません
                    pctx.globalAlpha = 0.9;
                    pctx.fillStyle = `rgba(${r},${g},${b},${a})`;
                    //pctx.fillRect(0, 0, pctx.canvas.width, pctx.canvas.height);
                    pctx.beginPath();
                    pctx.arc(this.pixelSize / 2, this.pixelSize / 2, this.pixelSize * 0.5 * 1.3, 0, Math.PI * 2, true);
                    pctx.fill();

                    pctx.fillStyle = '#FFF';
                    pctx.globalAlpha = 0.2;
                    for (let i = 0; i < 3; i += 1) {
                      pctx.beginPath();
                      pctx.arc(this.pixelSize / 2, this.pixelSize / 2, this.pixelSize / 4 + this.pixelSize / 10 * i, 0, Math.PI * 2, true);
                      pctx.fill();
                    }
                    paretteCanvas.push(pCanvas);
                    paletteIndex = palette.length - 1;
                } 
                const currentCanvas = paretteCanvas[paletteIndex];

                ctx.fillStyle = `rgba(${palette[paletteIndex][0]},${palette[paletteIndex][1]},${palette[paletteIndex][2]},${palette[paletteIndex][3]})`;
                
                const px = this.pixelSize * x + this.frameSize * (x + 1),
                    py = this.pixelSize * y + this.frameSize * (y + 1);
                ctx.drawImage(paretteCanvas[paletteIndex], px, py);

            }
        }

        return ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
    }
}
// ここからがPixelJS呼び出しのサンプルプログラム
document.addEventListener('DOMContentLoaded', e => {
    const pixelSize = 32,// ピクセルのサイズ
        frameSize = 4, // フレームのサイズ
        frameColor = [0, 0, 0, 255], // フレームの色
        srcImgWidth = 16,
        srcImgHeight = 16;

    const srcPalette = [
        [ 255, 255, 255, 0 ],
        [ 204, 0, 0, 255 ],
        [ 114, 105, 0, 255],
        [ 249, 172, 2, 255],
    ];

    const srcImg0 = [
        0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
        0,0,0,0,0,1,1,1,1,1,0,0,0,0,0,0,
        0,0,0,0,1,1,1,1,1,1,1,1,1,0,0,0,
        0,0,0,0,2,2,2,3,3,2,3,0,0,0,0,0,
        0,0,0,2,3,2,3,3,3,2,3,3,3,0,0,0,
        0,0,0,2,3,2,2,3,3,3,2,3,3,3,0,0,
        0,0,0,2,2,3,3,3,3,2,2,2,2,0,0,0,
        0,0,0,0,0,3,3,3,3,3,3,3,0,0,0,0,
        0,0,0,0,2,2,2,2,1,2,0,3,0,0,0,0,
        0,0,0,3,2,2,2,2,2,2,3,3,3,0,0,0,
        0,0,3,3,1,2,2,2,2,2,3,3,0,0,0,0,
        0,0,2,2,1,1,1,1,1,1,1,0,0,0,0,0,
        0,0,2,1,1,1,1,1,1,1,1,0,0,0,0,0,
        0,2,2,1,1,1,0,1,1,1,0,0,0,0,0,0,
        0,2,0,0,0,0,2,2,2,0,0,0,0,0,0,0,
        0,0,0,0,0,0,2,2,2,2,0,0,0,0,0,0,
    ];

    const srcImg1 = [
        0,0,0,0,0,1,1,1,1,1,0,0,0,0,0,0,
        0,0,0,0,1,1,1,1,1,1,1,1,1,0,0,0,
        0,0,0,0,2,2,2,3,3,2,3,0,0,0,0,0,
        0,0,0,2,3,2,3,3,3,2,3,3,3,0,0,0,
        0,0,0,2,3,2,2,3,3,3,2,3,3,3,0,0,
        0,0,0,2,2,3,3,3,3,2,2,2,2,0,0,0,
        0,0,0,0,0,3,3,3,3,3,3,3,0,0,0,0,
        0,0,2,2,2,2,1,1,2,2,0,0,0,0,0,0,
        3,3,2,2,2,2,1,1,1,2,2,2,3,3,3,0,
        3,3,3,0,2,2,1,3,1,1,1,2,2,3,3,0,
        3,3,0,0,1,1,1,1,1,1,1,0,0,2,0,0,
        0,0,0,1,1,1,1,1,1,1,1,1,2,2,0,0,
        0,0,1,1,1,1,1,1,1,1,1,1,2,2,0,0,
        0,2,2,1,1,1,0,0,0,1,1,1,2,2,0,0,
        0,2,2,2,0,0,0,0,0,0,0,0,0,0,0,0,
        0,0,2,2,2,0,0,0,0,0,0,0,0,0,0,0,
    ];

    // 表示用のcanvasのサイズを初期化します。
    const dispCanvas = document.getElementById('disp-canvas'); 
    dispCanvas.width = pixelSize * srcImgWidth + frameSize * (srcImgWidth + 1);
    dispCanvas.height = pixelSize * srcImgHeight + frameSize * (srcImgHeight + 1);
    const dispCtx = dispCanvas.getContext('2d');

    // PixelJSの設定(ピクセルや枠を設定します)
    PixelJS.setting(pixelSize, frameSize, frameColor);
    
    // 動的にcanvasを作成し、そこに絵を描く
    // 最終的に渡すのはImageDataなので、canvas作成は必須ではありません。
    const canvas = document.createElement('canvas');
    canvas.width = srcImgWidth;
    canvas.height = srcImgHeight;
    const ctx = canvas.getContext('2d');
    const srcImgData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);

    // debug用にsrcImg0を表示してみる
    for(let i = 0; i < srcImgWidth * srcImgHeight; i += 1) {
        for(let j = 0; j < 4; j += 1) {
            srcImgData.data[i * 4 + j] = srcPalette[srcImg0[i]][j];
        }        
    }

    // 
    const dstImgData0 = PixelJS.getImageData(srcImgData);

    // debug用にsrcImg1を表示してみる
    for(let i = 0; i < srcImgWidth * srcImgHeight; i += 1) {
        for(let j = 0; j < 4; j += 1) {
            srcImgData.data[i * 4 + j] = srcPalette[srcImg1[i]][j];
        }        
    }

    const dstImgData1 = PixelJS.getImageData(srcImgData);

    let count = 0;
    setInterval(() => {
        const imgData = !(count % 2) ? dstImgData0 : dstImgData1;
        dispCtx.putImageData(imgData, 0, 0);
        count++;
    }, 300);
});
</script>
</head>
<body>  
<canvas id="disp-canvas"></canvas>
</body>
</html>
```