Siv3D でドット絵エディタを作ろう

![siv3d_dotedit.gif](https://qiita-image-store.s3.amazonaws.com/0/35599/1d0bce99-680b-3760-4df2-bf3cef979625.gif)

# はじめに

[Siv3D](http://play-siv3d.hateblo.jp/) を使うと、C++ でゲームやツールを簡単に作成することができます。Siv3D の便利なクラス群を活用して、ドット絵エディタを作成してみましょう。

今回作成するドット絵エディタでは、こんな機能を実装してみます。

* ドット絵を表示する
* ドット絵のグリッドを表示する
* クリックしたドットの色を変える
* ドット絵を保存する
* パレットから色を選ぶ

# ドット絵を表示しよう、その前に

まず最初に、エディタの外観を大まかに決めます。幅 16 × 高さ 16 のドット絵を、1 ドットあたり 32 ピクセルのサイズに拡大して表示することにします。

あとはウィンドウの大きさ、背景色を自分好みに設定します。
:information_source: [ウィンドウと背景](https://github.com/Siv3D/Reference-JP/wiki/%E3%82%A6%E3%82%A3%E3%83%B3%E3%83%89%E3%82%A6%E3%81%A8%E8%83%8C%E6%99%AF)

```cpp:Main.cpp
#include <Siv3D.hpp>

void Main()
{
    Window::Resize(640, 530);

    Graphics::SetBackground(Palette::Peachpuff);

    // 1マスの大きさ
    const int GridSize = 32;
    
    // 横方向のマスの数
    const int W = 16;

    // 縦方向のマスの数
    const int H = 16;

    while (System::Update())
    {
    }
}
```



# ドット絵を表示しよう

Siv3D には、画像を編集するための [Image](https://github.com/Reputeless/Siv3D-Reference/blob/master/Programming%20Guide/Headers/Siv3D/Image.hpp) クラスがあります。今回はこのクラスを使ってドット絵を編集することにしましょう。
:information_source: [画像編集](https://github.com/Siv3D/Reference-JP/wiki/%E7%94%BB%E5%83%8F%E7%B7%A8%E9%9B%86)

幅 16 × 高さ 16 で、中身が真っ白の画像を作成します。

```cpp
    Image image(W, H, Palette::White);
```

ただ、Image クラスは画像の編集はできても表示はできません。画像を表示するには [Texture](https://github.com/Reputeless/Siv3D-Reference/blob/master/Programming%20Guide/Headers/Siv3D/Texture.hpp) クラスか、または描き換えが可能な [DynamicTexture](https://github.com/Reputeless/Siv3D-Reference/blob/master/Programming%20Guide/Headers/Siv3D/DynamicTexture.hpp) を使用します。今回は頻繁にテクスチャを書き換える事になりそうので、 DynamicTexture を使うことにします。
:information_source: [テクスチャを描く](https://github.com/Siv3D/Reference-JP/wiki/%E3%83%86%E3%82%AF%E3%82%B9%E3%83%81%E3%83%A3%E3%82%92%E6%8F%8F%E3%81%8F) ／ [スケッチ](https://github.com/Siv3D/Reference-JP/wiki/%E3%82%B9%E3%82%B1%E3%83%83%E3%83%81)

以下のようにして、既存の画像からテクスチャを生成します。

```cpp
    DynamicTexture texture(image);
```

テクスチャの表示には `Texture::draw()` 関数を使用します。また、今回はドット絵を `GridSize` 倍に拡大して表示するので、`draw()` の前に `Texture::scale()` 関数で拡大しましょう。

以下のようにして、左から 10px、上から 10px の位置に、拡大されたテクスチャを表示します。

```cpp
    texture.scale(GridSize).draw(10, 10);
```

:warning: テクスチャを拡大表示すると、ぼやけた感じになると思います。これをくっきりさせたい時は、[サンプラーステートの変更](https://github.com/Siv3D/Reference-JP/wiki/2D%E3%81%AE%E3%83%AC%E3%83%B3%E3%83%80%E3%83%BC%E3%82%B9%E3%83%86%E3%83%BC%E3%83%88#%E3%82%B5%E3%83%B3%E3%83%97%E3%83%A9%E3%83%BC%E3%82%B9%E3%83%86%E3%83%BC%E3%83%88%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AA%E3%83%B3%E3%82%B0%E3%83%A2%E3%83%BC%E3%83%89) をします。

```cpp
    Graphics2D::SetSamplerState(SamplerState::WrapPoint);
```

ここまでをまとめると、ソースコードは以下のようになります。真っ白な画像が表示されたでしょうか。`Palette::White` のところを編集して、画像の色が変わることを確認してみましょう。

```cpp
#include <Siv3D.hpp>

void Main()
{
    Window::Resize(640, 530);

    Graphics::SetBackground(Palette::Peachpuff);

    Graphics2D::SetSamplerState(SamplerState::WrapPoint);

    // 1マスの大きさ
    const int GridSize = 32;

    // 横方向のマスの数
    const int W = 16;

    // 縦方向のマスの数
    const int H = 16;

    // ドット絵
    Image image(W, H, Palette::White);

    // ドット絵表示用のテクスチャ
    DynamicTexture texture(image);

    while (System::Update())
    {
        // ドット絵を描画
        texture.scale(GridSize).draw(10, 10);
    }
}
```

![00.png](https://qiita-image-store.s3.amazonaws.com/0/35599/b65b1a4b-aff2-a5b9-8961-4754f5f256cb.png)

# グリッドを表示しよう

次に、ドット絵エディタらしくグリッドを表示してみましょう。大きさが `GridSize` の矩形（Rect = [Rectangle](https://github.com/Reputeless/Siv3D-Reference/blob/master/Programming%20Guide/Headers/Siv3D/Rectangle.hpp) クラス）を敷き詰めればよさそうです。

`Rectangle::drawFrame()` 関数を使って、灰色の枠を描いています。
:information_source: [長方形の枠を描く](https://github.com/Siv3D/Reference-JP/wiki/%E5%9B%B3%E5%BD%A2%E3%82%92%E6%8F%8F%E3%81%8F#%E9%95%B7%E6%96%B9%E5%BD%A2%E3%81%AE%E6%9E%A0%E3%82%92%E6%8F%8F%E3%81%8F)

```cpp
    for (int i : step(H))
    {
        for (int j : step(W))
        {
            // グリッドを描画
            Rect dot(10 + j*GridSize, 10 + i*GridSize, GridSize);
            dot.drawFrame(1.0, 1.0, Palette::Gray);
        }
    }
```

![01.png](https://qiita-image-store.s3.amazonaws.com/0/35599/a0484b5e-dbe1-6165-6fec-b0e56875635a.png)

# クリックしたドットの色を変えてみよう

Rectangle のような図形のクラスには、クリックされたかどうかを知るためのプロパティ `Rectangle::leftClicked`、`Rectangle::rightClicked` がありますので、それを利用します。
:information_source: [図形とクリック](https://github.com/Siv3D/Reference-JP/wiki/%E5%9B%B3%E5%BD%A2%E3%81%AE%E5%BD%93%E3%81%9F%E3%82%8A%E5%88%A4%E5%AE%9A#%E5%9B%B3%E5%BD%A2%E3%81%A8%E3%82%AF%E3%83%AA%E3%83%83%E3%82%AF)

クリックの判定には、グリッド表示に使った Rect を流用すると良さそうですね。左クリックされたドットを黒に、右クリックされたドットを白にしてみましょう。

画像のピクセルの色を変更するときは、`Image::operator[]` を使用します。
:information_source: [画像のピクセルに色を書き込む](https://github.com/Siv3D/Reference-JP/wiki/%E7%94%BB%E5%83%8F%E7%B7%A8%E9%9B%86#%E7%94%BB%E5%83%8F%E3%81%AE%E3%83%94%E3%82%AF%E3%82%BB%E3%83%AB%E3%81%AB%E8%89%B2%E3%82%92%E6%9B%B8%E3%81%8D%E8%BE%BC%E3%82%80)

:warning: 画像を編集したあとは、忘れずに表示用の `texture` 変数を描き換えましょう。テクスチャに画像を描き込むには、`DynamicTexture::fill()` 関数を使用します。

```cpp
    for (int i : step(H))
    {
        for (int j : step(W))
        {
            // グリッドを描画
            Rect dot(10 + j*GridSize, 10 + i*GridSize, GridSize);
            dot.drawFrame(1.0, 1.0, Palette::Gray);

            // ドットがクリックされていたら、ドットの色を変更する
            if (dot.leftClicked)
            {
                image[i][j] = Palette::Black;
                texture.fill(image);
            }
            else if (dot.rightClicked)
            {
                image[i][j] = Palette::White;
                texture.fill(image);
            }
        }
    }
```

![bw.gif](https://qiita-image-store.s3.amazonaws.com/0/35599/358c6d3f-5140-94cb-29dd-dbc0ecadd85c.gif)

# ドット絵を保存してみよう

ではここで画像を保存できるようにしてみましょう。「保存」ボタンを表示しておいて、クリックされたら「名前をつけて保存」ダイアログを出し、指定されたファイル名で保存するようにします。

まずは「保存」ボタンを表示します。ボタン本体の表示には Rect を、「保存」の文字の表示には [Font](https://github.com/Reputeless/Siv3D-Reference/blob/master/Programming%20Guide/Headers/Siv3D/Font.hpp) クラスを使います。

下記のようにして、画面右上のスペースにボタンを描きます。`Font::drawCenter()` 関数を使うと、指定した座標が中心になるように文字を描くことができるので、矩形の中心 `Rectangle::center` を指定して、ボタンの中央に文字を表示します。
:information_source: [テキストを描く#中央に表示する](https://github.com/Siv3D/Reference-JP/wiki/%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88%E3%82%92%E6%8F%8F%E3%81%8F#%E4%B8%AD%E5%A4%AE%E3%81%AB%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B)

```cpp
    //ボタンのラベル用フォント
    Font font(12);

    // ･･･途中省略･･･

        // 保存ボタン
        Rect saveButton(531, 10, 100, 30);
        saveButton.draw(Palette::Whitesmoke).drawFrame(1.0, 1.0, Palette::Black);
        font(L"保存").drawCenter(saveButton.center, Palette::Black);
```

このボタンがクリックされた時に、「名前をつけて保存」ダイアログを出し、指定されたファイル名で画像を保存するには以下のようにします。
:information_source: [ダイアログ#名前を付けて画像を保存する](https://github.com/Siv3D/Reference-JP/wiki/%E3%83%80%E3%82%A4%E3%82%A2%E3%83%AD%E3%82%B0#%E5%90%8D%E5%89%8D%E3%82%92%E4%BB%98%E3%81%91%E3%81%A6%E7%94%BB%E5%83%8F%E3%82%92%E4%BF%9D%E5%AD%98%E3%81%99%E3%82%8B)


```cpp
        if (saveButton.leftClicked)
        {
            const auto save = Dialog::GetSaveImage();
            if (save)
            {
                image.save(save.value());
            }
        }
```

ここまでをまとめると、ソースコードは以下のようになります。

```cpp
#include <Siv3D.hpp>

void Main()
{
    Window::Resize(640, 530);

    Graphics::SetBackground(Palette::Peachpuff);

    Graphics2D::SetSamplerState(SamplerState::WrapPoint);

    // 1マスの大きさ
    const int GridSize = 32;

    // 横方向のマスの数
    const int W = 16;

    // 縦方向のマスの数
    const int H = 16;

    // ドット絵
    Image image(W, H, Palette::White);

    // ドット絵表示用のテクスチャ
    DynamicTexture texture(image);

    // ボタンのラベル用フォント
    Font font(12);

    while (System::Update())
    {
        // ドット絵を描画
        texture.scale(GridSize).draw(10, 10);

        for (int i : step(H))
        {
            for (int j : step(W))
            {
                // グリッドを描画
                Rect dot(10 + j*GridSize, 10 + i*GridSize, GridSize);
                dot.drawFrame(1.0, 1.0, Palette::Gray);

                // ドットがクリックされていたら、ドットの色を変更する
                if (dot.leftClicked)
                {
                    image[i][j] = Palette::Black;
                    texture.fill(image);
                }
                else if (dot.rightClicked)
                {
                    image[i][j] = Palette::White;
                    texture.fill(image);
                }
            }
        }

        // 保存ボタン
        Rect saveButton(531, 10, 100, 30);
        saveButton.draw(Palette::Whitesmoke).drawFrame(1.0, 1.0, Palette::Black);
        font(L"保存").drawCenter(saveButton.center, Palette::Black);

        if (saveButton.leftClicked)
        {
            const auto save = Dialog::GetSaveImage();
            if (save)
            {
                image.save(save.value());
            }
        }

    }
}
```

# パレットから色を選べるようにしてみよう

さて、この段階では使える色が白と黒の 2 つしかありません。右側にパレットを表示して、色を選択できるようにしてみましょう。

その前に、現在選択している色を表示することにします。`leftColor`、`rightColor` という 2 つの変数を用意し、現在選択している色をここに格納するようにします。

```cpp
    Color leftColor = Palette::Black;
    Color rightColor = Palette::White;

    // ･･･途中省略･･･

        // 現在選択中の色
        Rect(531, 470, 50, 50).draw(leftColor).drawFrame(1.0, 1.0, Palette::Black);
        Rect(581, 470, 50, 50).draw(rightColor).drawFrame(1.0, 1.0, Palette::Black);
```

そしてグリッドがクリックされたとき、これらの変数を使うようにします。

```cpp
                if (dot.leftClicked)
                {
                    image[i][j] = leftColor;
                    texture.fill(image);
                }
                else if (dot.rightClicked)
                {
                    image[i][j] = rightColor;
                    texture.fill(image);
                }
```

![02.png](https://qiita-image-store.s3.amazonaws.com/0/35599/a7555299-b293-d04e-4ace-992a1a7a486e.png)

ではパレットを作ります。[Array](https://github.com/Reputeless/Siv3D-Reference/blob/master/Programming%20Guide/Headers/Siv3D/Array.hpp) に色をいくつか入れて、右側のスペースに表示し、それらがクリックされたら選択中の色（`leftColor`、`rightColor`）を変更します。

```cpp
    Array<Color> palette{ Palette::White, Palette::Black, Palette::Red, Palette::Blue, Palette::Green };

    // ･･･途中省略･･･

        // パレット
        for (int i = 0; i < palette.size(); i++)
        {
            // パレットのボタン
            Rect palButton(531, 100 + i * 30, 100, 30);
            palButton.draw(palette[i]).drawFrame(1.0, 1.0, Palette::Black);

            // パレットのボタンがクリックされたら、現在の色を変える
            if (palButton.leftClicked)
            {
                leftColor = palette[i];
            }
            else if (palButton.rightClicked)
            {
                rightColor = palette[i];
            }
        }
```

![03.png](https://qiita-image-store.s3.amazonaws.com/0/35599/c9a09d84-58ca-9203-742f-5f7330933d33.png)


# 完成

今回の記事ではここでひとまず完成とします。最終的にほぼ 100 行に収まりましたね。マウスやダイアログを使うような複雑な処理がありましたが、Siv3D の便利なクラス群のおかげで、短いソースコードで記述することができました。

```cpp
#include <Siv3D.hpp>

void Main()
{
    Window::Resize(640, 530);

    Graphics::SetBackground(Palette::Peachpuff);

    Graphics2D::SetSamplerState(SamplerState::WrapPoint);

    // 1マスの大きさ
    const int GridSize = 32;

    // 横方向のマスの数
    const int W = 16;

    // 縦方向のマスの数
    const int H = 16;

    // ドット絵
    Image image(W, H, Palette::White);

    // ドット絵表示用のテクスチャ
    DynamicTexture texture(image);

    // ボタンのラベル用フォント
    Font font(12);

    // 選択中の色
    Color leftColor = Palette::Black;
    Color rightColor = Palette::White;

    // パレット
    Array<Color> palette{ Palette::White, Palette::Black, Palette::Red, Palette::Blue, Palette::Green };

    while (System::Update())
    {
        // ドット絵を描画
        texture.scale(GridSize).draw(10, 10);

        for (int i : step(H))
        {
            for (int j : step(W))
            {
                // グリッドを描画
                Rect dot(10 + j*GridSize, 10 + i*GridSize, GridSize);
                dot.drawFrame(1.0, 1.0, Palette::Gray);

                // ドットがクリックされていたら、ドットの色を変更する
                if (dot.leftClicked)
                {
                    image[i][j] = leftColor;
                    texture.fill(image);
                }
                else if (dot.rightClicked)
                {
                    image[i][j] = rightColor;
                    texture.fill(image);
                }
            }
        }

        // 保存ボタン
        Rect saveButton(531, 10, 100, 30);
        saveButton.draw(Palette::Whitesmoke).drawFrame(1.0, 1.0, Palette::Black);
        font(L"保存").drawCenter(saveButton.center, Palette::Black);

        if (saveButton.leftClicked)
        {
            const auto save = Dialog::GetSaveImage();
            if (save)
            {
                image.save(save.value());
            }
        }

        // 現在選択中の色
        Rect(531, 470, 50, 50).draw(leftColor).drawFrame(1.0, 1.0, Palette::Black);
        Rect(581, 470, 50, 50).draw(rightColor).drawFrame(1.0, 1.0, Palette::Black);

        // パレット
        for (int i = 0; i < palette.size(); i++)
        {
            // パレットのボタン
            Rect palButton(531, 100 + i * 30, 100, 30);
            palButton.draw(palette[i]).drawFrame(1.0, 1.0, Palette::Black);

            // パレットのボタンがクリックされたら、現在の色を変える
            if (palButton.leftClicked)
            {
                leftColor = palette[i];
            }
            else if (palButton.rightClicked)
            {
                rightColor = palette[i];
            }
        }
    }
}
```

# 今後の課題

このプログラムにさらに機能を追加するとしたら、どんな機能が考えられるでしょうか。

* 画像を読み込めるようにする
* マウスが乗っているグリッドを強調する
* グリッドのサイズを変えられるようにする
* 画像のサイズを変えられるようにする
* マウスをドラッグして連続して線を引けるようにする
* etc.