Open9
Siv3Dでドット絵ツールを作るときのメモ

String EncodeImageToBase64(const Image& image)
{
const auto webp = image.encodeWebP(Lossless::Yes);
const auto compressedData = Zlib::Compress(webp, Zlib::MaxCompressionLevel);
return compressedData.base64Str();
}
Image DecodeImageFromBase64(StringView base64TextData)
{
const auto decodedData = Base64::Decode(base64TextData);
const auto decompressedData = Zlib::Decompress(decodedData);
return Image(MemoryReader(decompressedData));
}

//ドット絵としてぼやけないように const ScopedRenderStates2D sampler{ SamplerState::ClampNearest };

グリッドを描画
void DrawGrid(const Size& canvasSize, int32 scaling, const ColorF& color = ColorF{ 0.0, 0.3 })
{
for (auto i : step(canvasSize.x))
{
Rect{ i * scaling, 0, 1, canvasSize.x * scaling }.draw(color);
}
for (auto i : step(canvasSize.y))
{
Rect{ 0, i * scaling, canvasSize.y * scaling, 1 }.draw(color);
}
}

タイトル
//画面構成やレイアウトはJSONで変更できるように
// セーブデータはJSON+
//ドット絵のサイズは最初に選ばせるように 数値入力
//ドット絵の出力方法はいくつか選べるように(PNG、JSON、BMPなど)
//オリジナルのパレットも作れるように いくつかのプリセット スポイト
//レイヤーの機能は持たせるように 必須 半透明を
//ドット絵としてぼやけないように const ScopedRenderStates2D sampler{ SamplerState::ClampNearest };
//拡大率を変えられるように
//半透明の表現が必要 灰色と白のチェック画像の自動生成
//アンドゥ・リドゥ history
//グリッドの表示 8倍以降
//for (auto i : step(61))
//{
// Rect{ 0, i * 10, 600, 1 }.draw(ColorF{ 0.4 });
// Rect{ i * 10, 0, 1, 600 }.draw(ColorF{ 0.4 });
//}
# include <Siv3D.hpp>
String EncodeImageToBase64(const Image& image)
{
const auto webp = image.encodeWebP(Lossless::Yes);
const auto compressedData = Zlib::Compress(webp, Zlib::MaxCompressionLevel);
return compressedData.base64Str();
}
Image DecodeImageFromBase64(StringView base64TextData)
{
const auto decodedData = Base64::Decode(base64TextData);
const auto decompressedData = Zlib::Decompress(decodedData);
return Image(MemoryReader(decompressedData));
}
void DrawGrid(const Size& canvasSize, int32 scaling, const ColorF& color = ColorF{ 0.0, 0.1 })
{
for (auto i : step(canvasSize.x))
{
Rect{ i * scaling, 0, 1, canvasSize.x * scaling }.draw(color);
}
for (auto i : step(canvasSize.y))
{
Rect{ 0, i * scaling, canvasSize.y * scaling, 1 }.draw(color);
}
}
struct Layer
{
String index;
DynamicTexture texture;
};
Image GenerateBackgroundImage(const Size& canvasSize)
{
return Image(canvasSize, Arg::generator = [](const Point& p) {return (p.x + p.y) % 2 == 0 ? Color(255) : Color(230); });
}
class PixelArtData
{
public:
PixelArtData()
{
m_backgroundTexture = Texture(GenerateBackgroundImage(m_canvasSize));
}
Size m_canvasSize{ 32, 32 };
Texture m_backgroundTexture;
HashTable<String, Image> m_imageDataTable;
Array<Layer> m_layers;
size_t m_index = 0;
String generateImageIndex()
{
return ToString(++m_index);
}
void draw(int32 scaling) const
{
const ScopedRenderStates2D sampler{ SamplerState::ClampNearest };
// テクスチャを表示
m_backgroundTexture.scaled(scaling).draw();
for (const auto& layer : m_layers)
{
layer.texture.scaled(scaling).draw();
}
}
void addLayerImage()
{
const String index = generateImageIndex();
const Image image = Image(m_canvasSize, Color(255, 255, 255, 0));
m_imageDataTable[index] = image;
Layer layer;
layer.index = index;
layer.texture = DynamicTexture(image);
m_layers << layer;
}
void clearAllImage()
{
for (auto& image : m_imageDataTable)
{
image.second.fill(Color(255, 255, 255, 0));
}
}
void updateTexture()
{
for (auto& layer : m_layers)
{
layer.texture.fill(m_imageDataTable[layer.index]);
}
}
};
PixelArtData ImportArtDataFromJSON(const JSON& json)
{
//バージョン管理
//基本情報
//Image情報
//Layer情報
}
JSON ExportArtDataToJSON(const PixelArtData& art)
{
JSON json;
json[U"test"] = U"wow";
}
void Main()
{
int32 scaling = 16;
// ペンの太さ
constexpr int32 thickness = 1;
// ペンの色
Color penColor = Palette::Orange;
// 書き込み用の画像データを用意
//Image image = DecodeImageFromBase64(U"eNoBrgBR/1JJRkamAAAAV0VCUFZQOEyaAAAALz/ADwAPMP/zP//zH3iAW2uboowOIPQ6CKcwZMogpBDPCLcAZM5GG+GyPg/yDfZLHNH/CeCjqSvFSsqCmjZ3OiP1TC/UsMeEoUZf8MJAo9YEYalr8SHc7DMM+Xbj5RSjfOtjmR0tPGChDMHDDHgQHaoQCVOL/LncvquRSf5ByjaCahlh4jBP9KXBRJq/ogJJ7yS2vOI5+o5/LvgzT50=");
// 表示用のテクスチャ(内容を更新するので DynamicTexture)
//DynamicTexture texture{ image };
PixelArtData data;
data.addLayerImage();
data.addLayerImage();
data.addLayerImage();
String currentIndex = ToString(1);
while (System::Update())
{
if (Key1.down())
{
currentIndex = U"1";
}
if (Key2.down())
{
currentIndex = U"2";
}
if (Key3.down())
{
currentIndex = U"3";
}
if (MouseL.pressed())
{
auto& image = data.m_imageDataTable[currentIndex];
// 書き込む線の始点は直前のフレームのマウスカーソル座標
// (初回はタッチ操作時の座標のジャンプを防ぐため、現在のマウスカーソル座標にする)
const Point from = (MouseL.down() ? Cursor::Pos() / scaling : Cursor::PreviousPos() / scaling);
// 書き込む線の終点は現在のマウスカーソル座標
const Point to = Cursor::Pos() / scaling;
// image に線を書き込む
Line{ from, to }.overwrite(image, thickness, penColor, Antialiased::No);
data.updateTexture();
}
// 描いたものを消去するボタンが押されたら
if (SimpleGUI::Button(U"Clear", Vec2{ 640, 500}, 120))
{
data.clearAllImage();
data.updateTexture();
}
//if (SimpleGUI::Button(U"Save", Vec2{ 640, 140 }, 120))
//{
// Stopwatch stopwatch;
// stopwatch.start();
// Clipboard::SetText(EncodeImageToBase64(image));
// Print << stopwatch.usF();
//}
if (SimpleGUI::Button(U"Save", Vec2{ 640, 140 }, 120))
{
ExportArtDataToJSON(data);
}
HSV color;
if (SimpleGUI::ColorPicker(color, Vec2{ 640, 240 }, true))
{
penColor = color.toColor();
}
// テクスチャを表示
data.draw(scaling);
//layerごとのプレビュー
{
const ScopedRenderStates2D sampler{ SamplerState::ClampNearest };
//もっとリストとしてきれいに配置したい。インタラクティブに
for (size_t i = 0; i < data.m_layers.size(); ++i)
{
data.m_backgroundTexture.draw(Arg::topRight = Point(Scene::Width(), i * data.m_canvasSize.y));
data.m_layers[i].texture.draw(Arg::topRight = Point(Scene::Width(), i * data.m_canvasSize.y));
}
}
Rect(data.m_canvasSize).setPos(Arg::topRight = Point(Scene::Width(),( Parse<int32>(currentIndex)-1) * data.m_canvasSize.y)).drawFrame(2.0, Palette::Black);
//DrawGrid(data.m_canvasSize, scaling);
}
}

タイトル
//画面構成やレイアウトはJSONで変更できるように
// セーブデータはJSON+
//ドット絵のサイズは最初に選ばせるように 数値入力
//ドット絵の出力方法はいくつか選べるように(PNG、JSON、BMPなど)
//オリジナルのパレットも作れるように いくつかのプリセット スポイト
//レイヤーの機能は持たせるように 必須 半透明を
//ドット絵としてぼやけないように const ScopedRenderStates2D sampler{ SamplerState::ClampNearest };
//拡大率を変えられるように
//アンドゥ・リドゥ history
//グリッドの表示 8倍以降
//for (auto i : step(61))
//{
// Rect{ 0, i * 10, 600, 1 }.draw(ColorF{ 0.4 });
// Rect{ i * 10, 0, 1, 600 }.draw(ColorF{ 0.4 });
//}
//RGBパレットが必要
//右クリックでスポイト
# include <Siv3D.hpp>
String EncodeImageToBase64(const Image& image)
{
const auto webp = image.encodeWebP(Lossless::Yes);
const auto compressedData = Zlib::Compress(webp, Zlib::MaxCompressionLevel);
return compressedData.base64Str();
}
Image DecodeImageFromBase64(StringView base64TextData)
{
const auto decodedData = Base64::Decode(base64TextData);
const auto decompressedData = Zlib::Decompress(decodedData);
return Image(MemoryReader(decompressedData));
}
void DrawGrid(const Size& canvasSize, int32 scaling, const ColorF& color = ColorF{ 0.0, 0.1 })
{
for (auto i : step(canvasSize.x))
{
Rect{ i * scaling, 0, 1, canvasSize.x * scaling }.draw(color);
}
for (auto i : step(canvasSize.y))
{
Rect{ 0, i * scaling, canvasSize.y * scaling, 1 }.draw(color);
}
}
struct Layer
{
String index;
DynamicTexture texture;
};
//半透明の表現が必要 灰色と白のチェック画像の自動生成
Image GenerateBackgroundImage(const Size& canvasSize)
{
return Image(canvasSize, Arg::generator = [](const Point& p) {return (p.x + p.y) % 2 == 0 ? Color(255) : Color(230); });
}
class PixelArtData
{
public:
PixelArtData()
{
m_backgroundTexture = Texture(GenerateBackgroundImage(m_canvasSize));
m_palette = Image(32, 32, Palette::White);
}
Size m_canvasSize{ 32, 32 };
Texture m_backgroundTexture;
HashTable<String, Image> m_imageDataTable;
Array<Layer> m_layers;
size_t m_index = 0;
Image m_palette;
String generateImageIndex()
{
return ToString(++m_index);
}
void draw(int32 scaling) const
{
const ScopedRenderStates2D sampler{ SamplerState::ClampNearest };
// テクスチャを表示
m_backgroundTexture.scaled(scaling).draw();
for (const auto& layer : m_layers)
{
layer.texture.scaled(scaling).draw();
}
}
void addLayerImage()
{
const String index = generateImageIndex();
const Image image = Image(m_canvasSize, Color(255, 255, 255, 0));
m_imageDataTable[index] = image;
Layer layer;
layer.index = index;
layer.texture = DynamicTexture(image);
m_layers << layer;
}
void clearAllImage()
{
for (auto& image : m_imageDataTable)
{
image.second.fill(Color(255, 255, 255, 0));
}
}
void updateTexture()
{
for (auto& layer : m_layers)
{
layer.texture.fill(m_imageDataTable[layer.index]);
}
}
void drawLayers(size_t currentIndex) const
{
const ScopedRenderStates2D sampler{ SamplerState::ClampNearest };
//もっとリストとしてきれいに配置したい。インタラクティブに
for (size_t i = 0; i < m_layers.size(); ++i)
{
const size_t k = m_layers.size() - 1 - i;
m_backgroundTexture.draw(Point(0, k * m_canvasSize.y));
m_layers[i].texture.draw(Point(0, k * m_canvasSize.y));
}
const size_t j = m_layers.size() - 1 - currentIndex;
Rect(m_canvasSize).setPos(0, j* m_canvasSize.y).drawFrame(2.0, Palette::Black);
}
Image generateIntegratedImage()
{
if (m_layers.size() == 0)
{
return Image();
}
Image result = m_imageDataTable[m_layers[0].index];
for (size_t i = 1; i < m_layers.size(); ++i)
{
m_imageDataTable[m_layers[i].index].stamp(result,0, 0);
}
return result;
}
};
PixelArtData ImportArtDataFromJSON(const JSON& json)
{
//バージョン管理
//基本情報
//Image情報
//Layer情報
}
JSON ExportArtDataToJSON(const PixelArtData& art)
{
JSON json;
json[U"test"] = U"wow";
return json;
}
Transformer2D TranslateDrawingPos(const Vec2& pos)
{
return Transformer2D(Mat3x2::Translate(pos));
}
void Main()
{
int32 scaling = 16;
// ペンの太さ
constexpr int32 thickness = 1;
// ペンの色
Color penColor = Palette::Orange;
// 書き込み用の画像データを用意
//Image image = DecodeImageFromBase64(U"eNoBrgBR/1JJRkamAAAAV0VCUFZQOEyaAAAALz/ADwAPMP/zP//zH3iAW2uboowOIPQ6CKcwZMogpBDPCLcAZM5GG+GyPg/yDfZLHNH/CeCjqSvFSsqCmjZ3OiP1TC/UsMeEoUZf8MJAo9YEYalr8SHc7DMM+Xbj5RSjfOtjmR0tPGChDMHDDHgQHaoQCVOL/LncvquRSf5ByjaCahlh4jBP9KXBRJq/ogJJ7yS2vOI5+o5/LvgzT50=");
// 表示用のテクスチャ(内容を更新するので DynamicTexture)
//DynamicTexture texture{ image };
PixelArtData data;
data.addLayerImage();
data.addLayerImage();
data.addLayerImage();
size_t currentIndex = 0;
while (System::Update())
{
auto& image = data.m_imageDataTable[data.m_layers[currentIndex].index];
if (KeyUp.down())
{
currentIndex = (currentIndex + 1 == data.m_layers.size()) ? 0 : currentIndex + 1;
}
if (KeyDown.down())
{
currentIndex = (currentIndex == 0) ? data.m_layers.size() - 1 : currentIndex - 1;
}
if (MouseL.pressed())
{
//キャンバスの場合
// 書き込む線の始点は直前のフレームのマウスカーソル座標
// (初回はタッチ操作時の座標のジャンプを防ぐため、現在のマウスカーソル座標にする)
const Point from = (MouseL.down() ? Cursor::Pos() / scaling : Cursor::PreviousPos() / scaling);
// 書き込む線の終点は現在のマウスカーソル座標
const Point to = Cursor::Pos() / scaling;
// image に線を書き込む
Line{ from, to }.overwrite(image, thickness, penColor, Antialiased::No);
data.updateTexture();
}
if (MouseR.pressed())
{
// 書き込む線の終点は現在のマウスカーソル座標
const Point to = Cursor::Pos() / scaling;
penColor = image.getPixel(to, ImageAddressMode::Clamp);
}
if (KeyF.down())
{
const Point to = Cursor::Pos() / scaling;
image.floodFill(to, penColor);
data.updateTexture();
}
// 描いたものを消去するボタンが押されたら
if (SimpleGUI::Button(U"Clear", Vec2{ 640, 500 }, 120))
{
data.clearAllImage();
data.updateTexture();
}
//if (SimpleGUI::Button(U"Save", Vec2{ 640, 140 }, 120))
//{
// Stopwatch stopwatch;
// stopwatch.start();
// Clipboard::SetText(EncodeImageToBase64(image));
// Print << stopwatch.usF();
//}
if (SimpleGUI::Button(U"Save", Vec2{ 640, 140 }, 120))
{
//PNG
//ExportArtDataToJSON(data);
//レイヤーを統合する必要がある。
Image result = data.generateIntegratedImage();
if (const auto filePath = Dialog::SaveImage())
{
result.savePNG(filePath.value());
}
}
HSV color = penColor;
if (SimpleGUI::ColorPicker(color, Vec2{ 640, 240 }, true))
{
penColor = color.toColor();
}
// テクスチャを表示
{
data.draw(scaling);
if (scaling >= 16)
{
DrawGrid(data.m_canvasSize, scaling);
}
}
{
const auto t = TranslateDrawingPos({ 600,0 });
data.drawLayers(currentIndex);
}
{
const auto t = TranslateDrawingPos({ 550,0 });
data.draw(1);
}
Circle(Cursor::PosF(), 5.0).draw(penColor);
}
}

タイトル
//達成
//アンドゥ・リドゥ history
//ドット絵としてぼやけないように const ScopedRenderStates2D sampler{ SamplerState::ClampNearest };
//拡大率を変えられるように
//レイヤーの機能は持たせるように 必須 半透明を
//グリッドの表示 8倍以降
//for (auto i : step(61))
//{
// Rect{ 0, i * 10, 600, 1 }.draw(ColorF{ 0.4 });
// Rect{ i * 10, 0, 1, 600 }.draw(ColorF{ 0.4 });
//}
//RGBパレットが必要
//右クリックでスポイト
//TODO
//
//画面構成やレイアウトはJSONで変更できるように
// セーブデータはJSON+
//ドット絵のサイズは最初に選ばせるように 数値入力
//ドット絵の出力方法はいくつか選べるように(PNG、JSON、BMPなど)
//オリジナルのパレットも作れるように いくつかのプリセット スポイト
//範囲選択のコピー・カット・ペースト
//自作のポップアップウィンドウを使う
//import export new セッティング
# include <Siv3D.hpp>
String EncodeImageToBase64(const Image& image)
{
const auto webp = image.encodeWebP(Lossless::Yes);
const auto compressedData = Zlib::Compress(webp, Zlib::MaxCompressionLevel);
return compressedData.base64Str();
}
Image DecodeImageFromBase64(StringView base64TextData)
{
const auto decodedData = Base64::Decode(base64TextData);
const auto decompressedData = Zlib::Decompress(decodedData);
return Image(MemoryReader(decompressedData));
}
template<class Type>
Blob GenerateHistory(const Type& data)
{
return Zlib::Compress(Serializer<MemoryWriter>{}(data)->getBlob());
}
template<class Type>
void RecoverHistory(Type& data, const Blob& blob)
{
Deserializer<MemoryReader>{Zlib::Decompress(blob)}(data);
}
void DrawGrid(const Size& canvasSize, int32 scaling, const ColorF& color = ColorF{ 0.0, 0.1 })
{
for (auto i : step(canvasSize.x))
{
Rect{ i * scaling, 0, 1, canvasSize.x * scaling }.draw(color);
}
for (auto i : step(canvasSize.y))
{
Rect{ 0, i * scaling, canvasSize.y * scaling, 1 }.draw(color);
}
}
//半透明の表現が必要 灰色と白のチェック画像の自動生成
Image GenerateBackgroundImage(const Size& canvasSize)
{
return Image(canvasSize, Arg::generator = [](const Point& p) {return (p.x + p.y) % 2 == 0 ? Color(255) : Color(230); });
}
class PixelArtData
{
public:
struct Layer
{
String index;
DynamicTexture texture;
template <class Archive>
void SIV3D_SERIALIZE(Archive& archive)
{
archive(index);
}
};
PixelArtData()
{
m_backgroundTexture = Texture(GenerateBackgroundImage(m_canvasSize));
m_palette = Image(32, 32, Palette::White);
}
Texture m_backgroundTexture;
Size m_canvasSize{ 32, 32 };
HashTable<String, Image> m_imageDataTable;
Array<Layer> m_layers;
size_t m_index = 0;
Image m_palette;
template <class Archive>
void SIV3D_SERIALIZE(Archive& archive)
{
archive(m_canvasSize, m_imageDataTable, m_layers, m_index, m_palette);
}
String generateImageIndex()
{
return ToString(++m_index);
}
void draw() const
{
const ScopedRenderStates2D sampler{ SamplerState::ClampNearest };
// テクスチャを表示
m_backgroundTexture.draw();
for (const auto& layer : m_layers)
{
layer.texture.draw();
}
}
void addLayerImageWhite()
{
const String index = generateImageIndex();
const Image image = Image(m_canvasSize, Color(255, 255, 255, 255));
m_imageDataTable[index] = image;
Layer layer;
layer.index = index;
layer.texture = DynamicTexture(image);
m_layers << layer;
}
void addLayerImageTransparent()
{
const String index = generateImageIndex();
const Image image = Image(m_canvasSize, Color(255, 255, 255, 0));
m_imageDataTable[index] = image;
Layer layer;
layer.index = index;
layer.texture = DynamicTexture(image);
m_layers << layer;
}
void clearAllImage()
{
for (auto& image : m_imageDataTable)
{
image.second.fill(Color(255, 255, 255, 0));
}
if (m_layers.size() == 0)
{
return;
}
m_imageDataTable[m_layers[0].index].fill(Palette::White);
}
void updateTexture()
{
for (auto& layer : m_layers)
{
layer.texture.fill(m_imageDataTable[layer.index]);
}
}
void drawLayers(size_t currentIndex) const
{
const ScopedRenderStates2D sampler{ SamplerState::ClampNearest };
//もっとリストとしてきれいに配置したい。インタラクティブに
for (size_t i = 0; i < m_layers.size(); ++i)
{
const size_t k = m_layers.size() - 1 - i;
m_backgroundTexture.draw(Point(0, k * m_canvasSize.y));
m_layers[i].texture.draw(Point(0, k * m_canvasSize.y));
}
const size_t j = m_layers.size() - 1 - currentIndex;
Rect(m_canvasSize).setPos(0, j * m_canvasSize.y).drawFrame(2.0, Palette::Black);
}
Image generateIntegratedImage()
{
if (m_layers.size() == 0)
{
return Image();
}
Image result = m_imageDataTable[m_layers[0].index];
for (size_t i = 1; i < m_layers.size(); ++i)
{
m_imageDataTable[m_layers[i].index].stamp(result, 0, 0);
}
return result;
}
};
PixelArtData ImportArtDataFromJSON(const JSON& json)
{
//バージョン管理
//基本情報
//Image情報
//Layer情報
}
JSON ExportArtDataToJSON(const PixelArtData& art)
{
JSON json;
json[U"test"] = U"wow";
return json;
}
Transformer2D TranslateDrawingPos(const Vec2& pos)
{
return Transformer2D(Mat3x2::Translate(pos));
}
void Main()
{
// タイトル
Window::SetTitle(U" pixrus ");
int32 scaling = 16;
Scene::SetBackground(Color(125, 162, 163));
// ペンの太さ
constexpr int32 thickness = 1;
// ペンの色
Color penColor = Palette::Orange;
// 書き込み用の画像データを用意
//Image image = DecodeImageFromBase64(U"eNoBrgBR/1JJRkamAAAAV0VCUFZQOEyaAAAALz/ADwAPMP/zP//zH3iAW2uboowOIPQ6CKcwZMogpBDPCLcAZM5GG+GyPg/yDfZLHNH/CeCjqSvFSsqCmjZ3OiP1TC/UsMeEoUZf8MJAo9YEYalr8SHc7DMM+Xbj5RSjfOtjmR0tPGChDMHDDHgQHaoQCVOL/LncvquRSf5ByjaCahlh4jBP9KXBRJq/ogJJ7yS2vOI5+o5/LvgzT50=");
// 表示用のテクスチャ(内容を更新するので DynamicTexture)
//DynamicTexture texture{ image };
PixelArtData data;
data.addLayerImageWhite();
data.addLayerImageTransparent();
data.addLayerImageTransparent();
size_t currentIndex = 0;
Point canvasPos(50, 50);
const Point previewPos(0, 0);
const Point layerPos(600, 0);
Optional<Point> tCanvasMoveCursorPos;
Optional<Point> tCanvasPos;
Optional<String> lastAction;
Array<Blob> histories;
histories << GenerateHistory(data);
const size_t maxHisoryNum = 1000;
Font font(12);
Point colorTextBoxPos(640, 360);
TextEditState r;
TextEditState g;
TextEditState b;
while (System::Update())
{
auto& image = data.m_imageDataTable[data.m_layers[currentIndex].index];
if (KeyUp.down())
{
currentIndex = (currentIndex + 1 == data.m_layers.size()) ? 0 : currentIndex + 1;
}
if (KeyDown.down())
{
currentIndex = (currentIndex == 0) ? data.m_layers.size() - 1 : currentIndex - 1;
}
if (Mouse::Wheel() > 0.5)
{
scaling = Min(scaling + 2, 32);
}
if (Mouse::Wheel() < -0.5)
{
scaling = Max(scaling - 2, 1);
}
{
//キャンバスの場合
const auto t = Transformer2D(Mat3x2::Scale(scaling) * Mat3x2::Translate(canvasPos), TransformCursor::Yes);
if (MouseL.pressed())
{
// 書き込む線の始点は直前のフレームのマウスカーソル座標
// (初回はタッチ操作時の座標のジャンプを防ぐため、現在のマウスカーソル座標にする)
const Point from = (MouseL.down() ? Cursor::Pos()/* / scaling*/ : Cursor::PreviousPos()/* / scaling*/);
// 書き込む線の終点は現在のマウスカーソル座標
const Point to = Cursor::Pos();// / scaling;
// image に線を書き込む
Line{ from, to }.overwrite(image, thickness, penColor, Antialiased::No);
data.updateTexture();
}
if (MouseL.up())
{
lastAction = U"Drawed";
}
if (MouseR.pressed())
{
// 書き込む線の終点は現在のマウスカーソル座標
const Point to = Cursor::Pos();
penColor = image.getPixel(to, ImageAddressMode::Clamp);
}
if (MouseR.up())
{
lastAction = U"Picked";
}
if (KeyF.down())
{
const Point to = Cursor::Pos();
image.floodFill(to, penColor);
data.updateTexture();
lastAction = U"Filled";
}
}
//移動
if (MouseM.down())
{
tCanvasMoveCursorPos = Cursor::Pos();
tCanvasPos = canvasPos;
}
if (MouseM.up())
{
tCanvasMoveCursorPos.reset();
tCanvasPos.reset();
lastAction = U"Moved";
}
if (tCanvasMoveCursorPos.has_value())
{
Cursor::RequestStyle(CursorStyle::Hand);
const Point offset = Cursor::Pos() - tCanvasMoveCursorPos.value();
canvasPos = tCanvasPos.value() + offset;
}
if (KeyR.down())
{
canvasPos = Point(50, 50);
lastAction = U"Reset";
}
// 描いたものを消去するボタンが押されたら
if (SimpleGUI::Button(U"Clear", Vec2{ 640, 500 }, 120))
{
data.clearAllImage();
data.updateTexture();
lastAction = U"Cleared";
}
//if (SimpleGUI::Button(U"Save", Vec2{ 640, 140 }, 120))
//{
// Stopwatch stopwatch;
// stopwatch.start();
// Clipboard::SetText(EncodeImageToBase64(image));
// Print << stopwatch.usF();
//}
if (SimpleGUI::Button(U"Save", Vec2{ 640, 140 }, 120))
{
Image result = data.generateIntegratedImage();
if (const auto filePath = Dialog::SaveImage())
{
result.savePNG(filePath.value());
}
lastAction = U"Saved";
}
HSV color = penColor;
if (SimpleGUI::ColorPicker(color, Vec2{ 640, 240 }, true))
{
penColor = ColorF(color.toColor(), 1.0);
r.text = ToString(penColor.r);
g.text = ToString(penColor.g);
b.text = ToString(penColor.b);
}
if (SimpleGUI::TextBox(r, colorTextBoxPos, 50, 3)
|| SimpleGUI::TextBox(g, colorTextBoxPos + Vec2(55, 0), 50, 3)
|| SimpleGUI::TextBox(b, colorTextBoxPos + Vec2(110, 0), 50, 3))
{
penColor = Color(ParseOr<int32>(r.text, 0), ParseOr<int32>(g.text, 0), ParseOr<int32>(b.text, 0));
}
if (lastAction.has_value())
{
histories << GenerateHistory<PixelArtData>(data);
if (histories.size() > maxHisoryNum)
{
histories.pop_front();
}
//Print << histories.back().size_bytes();
lastAction.reset();
}
if (KeyZ.down())
{
if (histories.size() >= 2)
{
histories.pop_back();
RecoverHistory<PixelArtData>(data, histories.back());
data.updateTexture();
}
}
if (KeyC.down())
{
Clipboard::SetText(GenerateHistory<PixelArtData>(data).base64Str());
}
// テクスチャを表示
{
{
const auto t = Transformer2D(Mat3x2::Scale(scaling) * Mat3x2::Translate(canvasPos));
data.draw();
}
if (scaling >= 16)
{
const auto t = Transformer2D(Mat3x2::Translate(canvasPos));
DrawGrid(data.m_canvasSize, scaling);
}
}
{
const auto t = TranslateDrawingPos(layerPos);
data.drawLayers(currentIndex);
}
{
const auto t = TranslateDrawingPos(previewPos);
data.draw();
}
Circle(Cursor::PosF(), 5.0).draw(penColor);
}
}

たいとる
//達成
//アンドゥ・リドゥ history
//ドット絵としてぼやけないように const ScopedRenderStates2D sampler{ SamplerState::ClampNearest };
//拡大率を変えられるように
//レイヤーの機能は持たせるように 必須 半透明を
//グリッドの表示 8倍以降
//for (auto i : step(61))
//{
// Rect{ 0, i * 10, 600, 1 }.draw(ColorF{ 0.4 });
// Rect{ i * 10, 0, 1, 600 }.draw(ColorF{ 0.4 });
//}
//RGBパレットが必要
//右クリックでスポイト
//TODO
//
//画面構成やレイアウトはJSONで変更できるように
// セーブデータはJSON+
//ドット絵のサイズは最初に選ばせるように 数値入力
//ドット絵の出力方法はいくつか選べるように(PNG、JSON、BMPなど)
//オリジナルのパレットも作れるように いくつかのプリセット スポイト
//範囲選択のコピー・カット・ペースト
//自作のポップアップウィンドウを使う
//import export new セッティング
# include <Siv3D.hpp>
String EncodeImageToBase64(const Image& image)
{
const auto webp = image.encodeWebP(Lossless::Yes);
const auto compressedData = Zlib::Compress(webp, Zlib::MaxCompressionLevel);
return compressedData.base64Str();
}
Image DecodeImageFromBase64(StringView base64TextData)
{
const auto decodedData = Base64::Decode(base64TextData);
const auto decompressedData = Zlib::Decompress(decodedData);
return Image(MemoryReader(decompressedData));
}
template<class Type>
Blob GenerateHistory(const Type& data)
{
return Zlib::Compress(Serializer<MemoryWriter>{}(data)->getBlob());
}
template<class Type>
void RecoverHistory(Type& data, const Blob& blob)
{
Deserializer<MemoryReader>{Zlib::Decompress(blob)}(data);
}
void DrawGrid(const Size& canvasSize, int32 scaling, const ColorF& color = ColorF{ 0.0, 0.1 })
{
for (auto i : step(canvasSize.x))
{
Rect{ i * scaling, 0, 1, canvasSize.x * scaling }.draw(color);
}
for (auto i : step(canvasSize.y))
{
Rect{ 0, i * scaling, canvasSize.y * scaling, 1 }.draw(color);
}
}
//半透明の表現が必要 灰色と白のチェック画像の自動生成
Image GenerateBackgroundImage(const Size& canvasSize)
{
return Image(canvasSize, Arg::generator = [](const Point& p) {return (p.x + p.y) % 2 == 0 ? Color(255) : Color(230); });
}
Rect CalculateAABB(const Point& p1, const Point& p2)
{
const int32 minX = std::min(p1.x, p2.x);
const int32 maxX = std::max(p1.x, p2.x);
const int32 minY = std::min(p1.y, p2.y);
const int32 maxY = std::max(p1.y, p2.y);
return Rect(minX, minY, maxX - minX, maxY - minY);
}
Image GetAABBImage(const Image& originalImage, const Point& p1, const Point& p2)
{
const Rect aabb = CalculateAABB(p1, p2);
return originalImage.clipped(aabb);
}
class PixelArtData
{
public:
struct Layer
{
String index;
DynamicTexture texture;
template <class Archive>
void SIV3D_SERIALIZE(Archive& archive)
{
archive(index);
}
};
PixelArtData()
{
m_backgroundTexture = Texture(GenerateBackgroundImage(m_canvasSize));
m_palette = Image(32, 32, Palette::White);
}
Texture m_backgroundTexture;
Size m_canvasSize{ 32, 32 };
HashTable<String, Image> m_imageDataTable;
Array<Layer> m_layers;
size_t m_index = 0;
Image m_palette;
template <class Archive>
void SIV3D_SERIALIZE(Archive& archive)
{
archive(m_canvasSize, m_imageDataTable, m_layers, m_index, m_palette);
}
String generateImageIndex()
{
return ToString(++m_index);
}
void draw() const
{
const ScopedRenderStates2D sampler{ SamplerState::ClampNearest };
// テクスチャを表示
m_backgroundTexture.draw();
for (const auto& layer : m_layers)
{
layer.texture.draw();
}
}
void addLayerImageWhite()
{
const String index = generateImageIndex();
const Image image = Image(m_canvasSize, Color(255, 255, 255, 255));
m_imageDataTable[index] = image;
Layer layer;
layer.index = index;
layer.texture = DynamicTexture(image);
m_layers << layer;
}
void addLayerImageTransparent()
{
const String index = generateImageIndex();
const Image image = Image(m_canvasSize, Color(255, 255, 255, 0));
m_imageDataTable[index] = image;
Layer layer;
layer.index = index;
layer.texture = DynamicTexture(image);
m_layers << layer;
}
void clearAllImage()
{
for (auto& image : m_imageDataTable)
{
image.second.fill(Color(255, 255, 255, 0));
}
if (m_layers.size() == 0)
{
return;
}
m_imageDataTable[m_layers[0].index].fill(Palette::White);
}
void updateTexture()
{
for (auto& layer : m_layers)
{
layer.texture.fill(m_imageDataTable[layer.index]);
}
}
void drawLayers(size_t currentIndex) const
{
const ScopedRenderStates2D sampler{ SamplerState::ClampNearest };
//もっとリストとしてきれいに配置したい。インタラクティブに
for (size_t i = 0; i < m_layers.size(); ++i)
{
const size_t k = m_layers.size() - 1 - i;
m_backgroundTexture.draw(Point(0, k * m_canvasSize.y));
m_layers[i].texture.draw(Point(0, k * m_canvasSize.y));
}
const size_t j = m_layers.size() - 1 - currentIndex;
Rect(m_canvasSize).setPos(0, j * m_canvasSize.y).drawFrame(2.0, Palette::Black);
}
Image generateIntegratedImage()
{
if (m_layers.size() == 0)
{
return Image();
}
Image result = m_imageDataTable[m_layers[0].index];
for (size_t i = 1; i < m_layers.size(); ++i)
{
m_imageDataTable[m_layers[i].index].stamp(result, 0, 0);
}
return result;
}
};
PixelArtData ImportArtDataFromJSON(const JSON& json)
{
//バージョン管理
//基本情報
//Image情報
//Layer情報
}
JSON ExportArtDataToJSON(const PixelArtData& art)
{
JSON json;
json[U"test"] = U"wow";
return json;
}
Transformer2D TranslateDrawingPos(const Vec2& pos)
{
return Transformer2D(Mat3x2::Translate(pos));
}
void Main()
{
// タイトル
Window::SetTitle(U" pixrus ");
int32 scaling = 16;
Scene::SetBackground(Color(125, 162, 163));
// ペンの太さ
constexpr int32 thickness = 1;
// ペンの色
Color penColor = Palette::Orange;
// 書き込み用の画像データを用意
//Image image = DecodeImageFromBase64(U"eNoBrgBR/1JJRkamAAAAV0VCUFZQOEyaAAAALz/ADwAPMP/zP//zH3iAW2uboowOIPQ6CKcwZMogpBDPCLcAZM5GG+GyPg/yDfZLHNH/CeCjqSvFSsqCmjZ3OiP1TC/UsMeEoUZf8MJAo9YEYalr8SHc7DMM+Xbj5RSjfOtjmR0tPGChDMHDDHgQHaoQCVOL/LncvquRSf5ByjaCahlh4jBP9KXBRJq/ogJJ7yS2vOI5+o5/LvgzT50=");
// 表示用のテクスチャ(内容を更新するので DynamicTexture)
//DynamicTexture texture{ image };
PixelArtData data;
data.addLayerImageWhite();
data.addLayerImageTransparent();
data.addLayerImageTransparent();
size_t currentIndex = 0;
Point canvasPos(50, 50);
const Point previewPos(0, 0);
const Point layerPos(600, 0);
Optional<Point> tCanvasMoveCursorPos;
Optional<Point> tCanvasPos;
Optional<String> lastAction;
Array<Blob> histories;
histories << GenerateHistory(data);
const size_t maxHisoryNum = 1000;
Font font(12);
Point colorTextBoxPos(640, 360);
TextEditState r;
TextEditState g;
TextEditState b;
Image clippedImage;
DynamicTexture clippedImageTexture;
Optional<Point> AABBPoint1;
Optional<Point> AABBPoint2;
while (System::Update())
{
auto& image = data.m_imageDataTable[data.m_layers[currentIndex].index];
if (KeyUp.down())
{
currentIndex = (currentIndex + 1 == data.m_layers.size()) ? 0 : currentIndex + 1;
}
if (KeyDown.down())
{
currentIndex = (currentIndex == 0) ? data.m_layers.size() - 1 : currentIndex - 1;
}
if (Mouse::Wheel() > 0.5)
{
scaling = Min(scaling + 2, 32);
}
if (Mouse::Wheel() < -0.5)
{
scaling = Max(scaling - 2, 1);
}
{
//キャンバスの場合
const auto t = Transformer2D(Mat3x2::Scale(scaling) * Mat3x2::Translate(canvasPos), TransformCursor::Yes);
if (Key1.down())
{
AABBPoint1 = Cursor::Pos();
}
if (Key2.down())
{
AABBPoint2 = Cursor::Pos();
}
if (AABBPoint1.has_value() && AABBPoint2.has_value())
{
clippedImage = GetAABBImage(image, AABBPoint1.value(), AABBPoint2.value());
clippedImageTexture = DynamicTexture(clippedImage);
AABBPoint1.reset();
AABBPoint2.reset();
}
if (MouseL.pressed())
{
// 書き込む線の始点は直前のフレームのマウスカーソル座標
// (初回はタッチ操作時の座標のジャンプを防ぐため、現在のマウスカーソル座標にする)
const Point from = (MouseL.down() ? Cursor::Pos()/* / scaling*/ : Cursor::PreviousPos()/* / scaling*/);
// 書き込む線の終点は現在のマウスカーソル座標
const Point to = Cursor::Pos();// / scaling;
// image に線を書き込む
Line{ from, to }.overwrite(image, thickness, penColor, Antialiased::No);
data.updateTexture();
}
if (MouseL.up())
{
lastAction = U"Drawed";
}
if (MouseR.pressed())
{
// 書き込む線の終点は現在のマウスカーソル座標
const Point to = Cursor::Pos();
penColor = image.getPixel(to, ImageAddressMode::Clamp);
}
if (MouseR.up())
{
lastAction = U"Picked";
}
if (KeyF.down())
{
const Point to = Cursor::Pos();
image.floodFill(to, penColor);
data.updateTexture();
lastAction = U"Filled";
}
}
//移動
if (MouseM.down())
{
tCanvasMoveCursorPos = Cursor::Pos();
tCanvasPos = canvasPos;
}
if (MouseM.up())
{
tCanvasMoveCursorPos.reset();
tCanvasPos.reset();
lastAction = U"Moved";
}
if (tCanvasMoveCursorPos.has_value())
{
Cursor::RequestStyle(CursorStyle::Hand);
const Point offset = Cursor::Pos() - tCanvasMoveCursorPos.value();
canvasPos = tCanvasPos.value() + offset;
}
if (KeyR.down())
{
canvasPos = Point(50, 50);
lastAction = U"Reset";
}
// 描いたものを消去するボタンが押されたら
if (SimpleGUI::Button(U"Clear", Vec2{ 640, 500 }, 120))
{
data.clearAllImage();
data.updateTexture();
lastAction = U"Cleared";
}
//if (SimpleGUI::Button(U"Save", Vec2{ 640, 140 }, 120))
//{
// Stopwatch stopwatch;
// stopwatch.start();
// Clipboard::SetText(EncodeImageToBase64(image));
// Print << stopwatch.usF();
//}
if (SimpleGUI::Button(U"Save", Vec2{ 640, 140 }, 120))
{
Image result = data.generateIntegratedImage();
if (const auto filePath = Dialog::SaveImage())
{
result.savePNG(filePath.value());
}
lastAction = U"Saved";
}
HSV color = penColor;
if (SimpleGUI::ColorPicker(color, Vec2{ 640, 240 }, true))
{
penColor = ColorF(color.toColor(), 1.0);
r.text = ToString(penColor.r);
g.text = ToString(penColor.g);
b.text = ToString(penColor.b);
}
if (SimpleGUI::TextBox(r, colorTextBoxPos, 50, 3)
|| SimpleGUI::TextBox(g, colorTextBoxPos + Vec2(55, 0), 50, 3)
|| SimpleGUI::TextBox(b, colorTextBoxPos + Vec2(110, 0), 50, 3))
{
penColor = Color(ParseOr<int32>(r.text, 0), ParseOr<int32>(g.text, 0), ParseOr<int32>(b.text, 0));
}
clippedImageTexture.scaled(4).draw(colorTextBoxPos + Vec2(0, 100));
if (lastAction.has_value())
{
histories << GenerateHistory<PixelArtData>(data);
if (histories.size() > maxHisoryNum)
{
histories.pop_front();
}
//Print << histories.back().size_bytes();
lastAction.reset();
}
if (KeyZ.down())
{
if (histories.size() >= 2)
{
histories.pop_back();
RecoverHistory<PixelArtData>(data, histories.back());
data.updateTexture();
}
}
if (KeyC.down())
{
Clipboard::SetText(GenerateHistory<PixelArtData>(data).base64Str());
}
// テクスチャを表示
{
{
const auto t = Transformer2D(Mat3x2::Scale(scaling) * Mat3x2::Translate(canvasPos));
data.draw();
}
if (scaling >= 16)
{
const auto t = Transformer2D(Mat3x2::Translate(canvasPos));
DrawGrid(data.m_canvasSize, scaling);
}
}
{
const auto t = TranslateDrawingPos(layerPos);
data.drawLayers(currentIndex);
}
{
const auto t = TranslateDrawingPos(previewPos);
data.draw();
}
Circle(Cursor::PosF(), 5.0).draw(penColor);
}
}

# include <Siv3D.hpp>
void Main()
{
//Twitter::OpenTweetWindow(U"yeah");
Scene::SetBackground(ColorF{ 0.8, 0.9, 1.0 });
Vec2 headlinePos{ 100, 60 };
Vec2 inputPos{ 100, 100 };
Vec2 presetPos{ 300, 60 };
Vec2 selectedSizePos{ 100, 200 };
Vec2 buttonPos{ 100, 300 };
TextEditState widthInput;
TextEditState heightInput;
size_t presetIndex = 0;
const Array<String> presets = { U"16x16", U"32x32", U"64x64", U"128x128" };
constexpr std::array<Size, 4> presetSizes = { Size{16, 16}, Size{32, 32}, Size{64, 64}, Size{128, 128} };
Size selectedSize{ 16, 16 };
while (System::Update())
{
SimpleGUI::Headline(U"ドット絵サイズ設定", headlinePos);
SimpleGUI::Headline(U"幅:", inputPos);
SimpleGUI::TextBox(widthInput, inputPos + Vec2{ 40, 0 }, 100, 4);
SimpleGUI::Headline(U"高さ:", inputPos + Vec2{ 0, 40 });
SimpleGUI::TextBox(heightInput, inputPos + Vec2{ 40, 40 }, 100, 4);
SimpleGUI::Headline(U"プリセット", presetPos);
if (SimpleGUI::RadioButtons(presetIndex, presets, presetPos + Vec2{ 0, 40 }))
{
selectedSize = presetSizes[presetIndex];
widthInput.text = Format(selectedSize.x);
heightInput.text = Format(selectedSize.y);
}
SimpleGUI::Headline(U"選択されたサイズ", selectedSizePos);
SimpleGUI::Headline(Format(U"幅: ", selectedSize.x, U", 高さ: ", selectedSize.y), selectedSizePos + Vec2{ 0, 40 });
if (SimpleGUI::Button(U"新規作成", buttonPos, 200, true))
{
// 新規作成ボタンがクリックされたときの処理をここに記述
}
if (SimpleGUI::Button(U"キャンセル", buttonPos + Vec2{ 0, 50 }, 200, true))
{
// キャンセルボタンがクリックされたときの処理をここに記述
}
}
}

タイトル
//達成
//アンドゥ・リドゥ history
//ドット絵としてぼやけないように const ScopedRenderStates2D sampler{ SamplerState::ClampNearest };
//拡大率を変えられるように
//レイヤーの機能は持たせるように 必須 半透明を
//グリッドの表示 8倍以降
//for (auto i : step(61))
//{
// Rect{ 0, i * 10, 600, 1 }.draw(ColorF{ 0.4 });
// Rect{ i * 10, 0, 1, 600 }.draw(ColorF{ 0.4 });
//}
//RGBパレットが必要
//右クリックでスポイト
//TODO
//
//画面構成やレイアウトはJSONで変更できるように
// セーブデータはJSON+
//ドット絵のサイズは最初に選ばせるように 数値入力
//ドット絵の出力方法はいくつか選べるように(PNG、JSON、BMPなど)
//オリジナルのパレットも作れるように いくつかのプリセット スポイト
//範囲選択のコピー・カット・ペースト
//自作のポップアップウィンドウを使う
//import export new セッティング
# include <Siv3D.hpp>
String EncodeImageToBase64(const Image& image)
{
const auto webp = image.encodeWebP(Lossless::Yes);
const auto compressedData = Zlib::Compress(webp, Zlib::MaxCompressionLevel);
return compressedData.base64Str();
}
Image DecodeImageFromBase64(StringView base64TextData)
{
const auto decodedData = Base64::Decode(base64TextData);
const auto decompressedData = Zlib::Decompress(decodedData);
return Image(MemoryReader(decompressedData));
}
template<class Type>
Blob GenerateHistory(const Type& data)
{
return Zlib::Compress(Serializer<MemoryWriter>{}(data)->getBlob());
}
template<class Type>
void RecoverHistory(Type& data, const Blob& blob)
{
Deserializer<MemoryReader>{Zlib::Decompress(blob)}(data);
}
void DrawGrid(const Size& canvasSize, int32 scaling, const ColorF& color = ColorF{ 0.0, 0.1 })
{
for (auto i : step(canvasSize.x))
{
Rect{ i * scaling, 0, 1, canvasSize.x * scaling }.draw(color);
}
for (auto i : step(canvasSize.y))
{
Rect{ 0, i * scaling, canvasSize.y * scaling, 1 }.draw(color);
}
}
//半透明の表現が必要 灰色と白のチェック画像の自動生成
Image GenerateBackgroundImage(const Size& canvasSize)
{
return Image(canvasSize, Arg::generator = [](const Point& p) {return (p.x + p.y) % 2 == 0 ? Color(255) : Color(230); });
}
Rect CalculateAABB(const Point& p1, const Point& p2)
{
const int32 minX = std::min(p1.x, p2.x);
const int32 maxX = std::max(p1.x, p2.x);
const int32 minY = std::min(p1.y, p2.y);
const int32 maxY = std::max(p1.y, p2.y);
return Rect(minX, minY, maxX - minX, maxY - minY);
}
Image GetAABBImage(const Image& originalImage, const Point& p1, const Point& p2)
{
const Rect aabb = CalculateAABB(p1, p2);
return originalImage.clipped(aabb);
}
class PixelArtData
{
public:
struct Layer
{
String index;
DynamicTexture texture;
template <class Archive>
void SIV3D_SERIALIZE(Archive& archive)
{
archive(index);
}
};
PixelArtData(const Size& canvasSize = Size(32, 32))
: m_canvasSize(canvasSize)
{
m_backgroundTexture = Texture(GenerateBackgroundImage(m_canvasSize));
m_palette = Image(32, 32, Palette::White);
}
Texture m_backgroundTexture;
Size m_canvasSize;
HashTable<String, Image> m_imageDataTable;
Array<Layer> m_layers;
size_t m_index = 0;
Image m_palette;
template <class Archive>
void SIV3D_SERIALIZE(Archive& archive)
{
archive(m_canvasSize, m_imageDataTable, m_layers, m_index, m_palette);
}
String generateImageIndex()
{
return ToString(++m_index);
}
void draw() const
{
const ScopedRenderStates2D sampler{ SamplerState::ClampNearest };
// テクスチャを表示
m_backgroundTexture.draw();
for (const auto& layer : m_layers)
{
layer.texture.draw();
}
}
void addLayerImage(const Image& image)
{
if (m_canvasSize != image.size())
{
return;
}
const String index = generateImageIndex();
m_imageDataTable[index] = image;
Layer layer;
layer.index = index;
layer.texture = DynamicTexture(image);
m_layers << layer;
}
void addLayerImageWhite()
{
const String index = generateImageIndex();
const Image image = Image(m_canvasSize, Color(255, 255, 255, 255));
m_imageDataTable[index] = image;
Layer layer;
layer.index = index;
layer.texture = DynamicTexture(image);
m_layers << layer;
}
void addLayerImageTransparent()
{
const String index = generateImageIndex();
const Image image = Image(m_canvasSize, Color(255, 255, 255, 0));
m_imageDataTable[index] = image;
Layer layer;
layer.index = index;
layer.texture = DynamicTexture(image);
m_layers << layer;
}
void clearAllImage()
{
for (auto& image : m_imageDataTable)
{
image.second.fill(Color(255, 255, 255, 0));
}
if (m_layers.size() == 0)
{
return;
}
m_imageDataTable[m_layers[0].index].fill(Palette::White);
}
void updateTexture()
{
for (auto& layer : m_layers)
{
layer.texture.fill(m_imageDataTable[layer.index]);
}
}
void drawLayers(size_t currentIndex) const
{
const ScopedRenderStates2D sampler{ SamplerState::ClampNearest };
//もっとリストとしてきれいに配置したい。インタラクティブに
for (size_t i = 0; i < m_layers.size(); ++i)
{
const size_t k = m_layers.size() - 1 - i;
m_backgroundTexture.draw(Point(0, k * m_canvasSize.y));
m_layers[i].texture.draw(Point(0, k * m_canvasSize.y));
}
const size_t j = m_layers.size() - 1 - currentIndex;
Rect(m_canvasSize).setPos(0, j * m_canvasSize.y).drawFrame(2.0, Palette::Black);
}
Image generateIntegratedImage()
{
if (m_layers.size() == 0)
{
return Image();
}
Image result = m_imageDataTable[m_layers[0].index];
for (size_t i = 1; i < m_layers.size(); ++i)
{
m_imageDataTable[m_layers[i].index].stamp(result, 0, 0);
}
return result;
}
};
class PopUpWindowBase
{
public:
PopUpWindowBase() = default;
virtual ~PopUpWindowBase() = default;
virtual void update()
{
}
virtual void draw() const
{
}
bool m_isDestoyed = false;
private:
};
PixelArtData ImportArtDataFromJSON(const JSON& json)
{
//バージョン管理
//基本情報
//Image情報
//Layer情報
}
JSON ExportArtDataToJSON(const PixelArtData& art)
{
JSON json;
json[U"test"] = U"wow";
return json;
}
Transformer2D TranslateDrawingPos(const Vec2& pos)
{
return Transformer2D(Mat3x2::Translate(pos));
}
class CommonData
{
public:
// シングルトンインスタンスへのアクセスメソッド
static CommonData& getInstance()
{
static CommonData instance;
return instance;
}
// コピー・ムーブ操作の禁止
CommonData(const CommonData&) = delete;
CommonData& operator=(const CommonData&) = delete;
CommonData(CommonData&&) = delete;
CommonData& operator=(CommonData&&) = delete;
// データの設定・取得メソッドなど、必要に応じて追加してください
// ペンの色
PixelArtData data;
Array<Blob> histories;
const size_t maxHisoryNum = 1000;
Font font{ 12 };
std::unique_ptr<PopUpWindowBase> popup;
private:
// コンストラクタとデストラクタをprivateにする
CommonData()
{
}
~CommonData() = default;
};
class CreateNewImageWindow : public PopUpWindowBase
{
public:
CreateNewImageWindow()
: PopUpWindowBase()
{
selectedSize = presetSizes[presetIndex];
widthInput.text = Format(selectedSize.x);
heightInput.text = Format(selectedSize.y);
}
void update() override
{
Scene::Rect().stretched(-20, -20).rounded(30.0).draw(Palette::White);
SimpleGUI::Headline(U"ドット絵サイズ設定 : ドラッグアンドドロップで読み込めるよ", headlinePos);
SimpleGUI::Headline(U"幅:", inputPos);
SimpleGUI::TextBox(widthInput, inputPos + Vec2{ 70, 0 }, 100, 4);
SimpleGUI::Headline(U"高さ:", inputPos + Vec2{ 0, 40 });
SimpleGUI::TextBox(heightInput, inputPos + Vec2{ 70, 40 }, 100, 4);
SimpleGUI::Headline(U"プリセット", presetPos);
if (SimpleGUI::RadioButtons(presetIndex, presets, presetPos + Vec2{ 0, 40 }))
{
selectedSize = presetSizes[presetIndex];
widthInput.text = Format(selectedSize.x);
heightInput.text = Format(selectedSize.y);
}
selectedSize.x = ParseOr<int32>(widthInput.text, 1);
selectedSize.y = ParseOr<int32>(heightInput.text, 1);
SimpleGUI::Headline(U"選択されたサイズ", selectedSizePos);
SimpleGUI::Headline(Format(U"幅: ", selectedSize.x, U", 高さ: ", selectedSize.y), selectedSizePos + Vec2{ 0, 40 });
if (SimpleGUI::Button(U"新規作成", buttonPos, 200, true))
{
// 新規作成ボタンがクリックされたときの処理をここに記述
auto newPixelArtData = PixelArtData(selectedSize);
CommonData::getInstance().data = newPixelArtData;
auto& commonData = CommonData::getInstance();
auto& data = commonData.data;
auto& histories = commonData.histories;
data.addLayerImageWhite();
data.addLayerImageTransparent();
data.addLayerImageTransparent();
histories.clear();
histories << GenerateHistory(data);
m_isDestoyed = true;
}
if (SimpleGUI::Button(U"キャンセル", buttonPos + Vec2{ 0, 50 }, 200, true))
{
// キャンセルボタンがクリックされたときの処理をここに記述
m_isDestoyed = true;
}
// ファイルがドロップされた
if (DragDrop::HasNewFilePaths())
{
// ファイルを画像として読み込めた
if (const Image image{ DragDrop::GetDroppedFilePaths().front().path })
{
selectedSize = image.size();
widthInput.text = Format(selectedSize.x);
heightInput.text = Format(selectedSize.y);
// 画面のサイズに合うように画像を拡大縮小
texture = Texture{ image };
auto newPixelArtData = PixelArtData(selectedSize);
CommonData::getInstance().data = newPixelArtData;
auto& commonData = CommonData::getInstance();
auto& data = commonData.data;
auto& histories = commonData.histories;
data.addLayerImage(image);
data.addLayerImageTransparent();
data.addLayerImageTransparent();
histories.clear();
histories << GenerateHistory(data);
m_isDestoyed = true;
}
}
}
void draw() const override
{
if (texture)
{
texture.draw(Arg::topLeft = Point(400, 100));
}
}
private:
Vec2 headlinePos{ 100, 60 };
Vec2 inputPos{ 100, 100 };
Vec2 presetPos{ 300, 100 };
Vec2 selectedSizePos{ 100, 200 };
Vec2 buttonPos{ 100, 450 };
TextEditState widthInput;
TextEditState heightInput;
size_t presetIndex = 0;
const Array<String> presets = { U"16x16", U"32x32", U"64x64", U"128x128" };
std::array<Size, 4> presetSizes = { Size{16, 16}, Size{32, 32}, Size{64, 64}, Size{128, 128} };
Size selectedSize{ 16, 16 };
Texture texture;
};
class MainScene
{
public:
MainScene()
{
// タイトル
Window::SetTitle(U" pixrus ");
Scene::SetBackground(Color(125, 162, 163));
Scene::SetLetterbox(Scene::GetBackground());
Scene::SetResizeMode(ResizeMode::Keep);
Scene::SetTextureFilter(TextureFilter::Nearest);
Window::SetStyle(WindowStyle::Sizable);
init();
auto& commonData = CommonData::getInstance();
auto& data = commonData.data;
operationFunctionTable[U"pen"] = [&]()
{
auto& image = data.m_imageDataTable[data.m_layers[currentLayerIndex].index];
if (Rect(image.size()).mouseOver() && MouseL.pressed())
{
drawingPos = Cursor::Pos();
}
if (drawingPos.has_value())
{
// 書き込む線の始点は直前のフレームのマウスカーソル座標
// (初回はタッチ操作時の座標のジャンプを防ぐため、現在のマウスカーソル座標にする)
const Point from = (MouseL.down() ? Cursor::Pos() : Cursor::PreviousPos());
// 書き込む線の終点は現在のマウスカーソル座標
const Point to = Cursor::Pos();// / scaling;
// image に線を書き込む
Line{ from, to }.overwrite(image, thickness, penColor, Antialiased::No);
data.updateTexture();
if (MouseL.up())
{
lastAction = U"Drawed";
drawingPos.reset();
}
}
};
operationFunctionTable[U"fill"] = [&]()
{
auto& image = data.m_imageDataTable[data.m_layers[currentLayerIndex].index];
if (Rect(image.size()).mouseOver() && MouseL.down())
{
const Point to = Cursor::Pos();
image.floodFill(to, penColor);
data.updateTexture();
lastAction = U"Filled";
}
};
}
void init()
{
auto& commonData = CommonData::getInstance();
auto& data = commonData.data;
auto& histories = commonData.histories;
data.addLayerImageWhite();
data.addLayerImageTransparent();
data.addLayerImageTransparent();
histories << GenerateHistory(data);
}
void update()
{
auto& commonData = CommonData::getInstance();
auto& data = commonData.data;
auto& image = data.m_imageDataTable[data.m_layers[currentLayerIndex].index];
auto& histories = commonData.histories;
auto& maxHisoryNum = commonData.maxHisoryNum;
if (KeyUp.down())
{
currentLayerIndex = (currentLayerIndex + 1 == data.m_layers.size()) ? 0 : currentLayerIndex + 1;
}
if (KeyDown.down())
{
currentLayerIndex = (currentLayerIndex == 0) ? data.m_layers.size() - 1 : currentLayerIndex - 1;
}
if (Mouse::Wheel() > 0.5)
{
scaling = Min(scaling + 2, 32);
}
if (Mouse::Wheel() < -0.5)
{
scaling = Max(scaling - 2, 1);
}
if (operationMode.has_value())
{
commonData.font(operationMode.value()).draw(Arg::topLeft = Point(80, 10));
//キャンバスの場合
const auto t = Transformer2D(Mat3x2::Scale(scaling) * Mat3x2::Translate(canvasPos), TransformCursor::Yes);
operationFunctionTable[operationMode.value()]();
}
{
//キャンバスの場合
const auto t = Transformer2D(Mat3x2::Scale(scaling) * Mat3x2::Translate(canvasPos), TransformCursor::Yes);
if (Key1.down())
{
AABBPoint1 = Cursor::Pos();
}
if (Key2.down())
{
AABBPoint2 = Cursor::Pos();
}
if (AABBPoint1.has_value() && AABBPoint2.has_value())
{
clippedImage = GetAABBImage(image, AABBPoint1.value(), AABBPoint2.value());
clippedImageTexture = DynamicTexture(clippedImage);
AABBPoint1.reset();
AABBPoint2.reset();
}
}
{
Circle(Vec2(20.0, 50.0), 25.0).draw(Palette::White);
undoTexture.drawAt(20, 50, ColorF{ 0.25 });
if (Circle(Vec2(20.0, 50.0), 25).leftClicked())
{
undoFunction();
}
Circle(Vec2(20.0, 100.0), 25.0).draw(Palette::White);
penTexture.drawAt(20, 100, ColorF{ 0.25 });
if (Circle(Vec2(20.0, 100.0), 25).leftClicked())
{
operationMode = U"pen";
}
Circle(Vec2(20.0, 150.0), 25).draw(Palette::White);
fillTexture.drawAt(20, 150, ColorF{ 0.25 });
if (Circle(Vec2(20.0, 150.0), 25).leftClicked())
{
operationMode = U"fill";
}
}
//移動
if (MouseM.down())
{
tCanvasMoveCursorPos = Cursor::Pos();
tCanvasPos = canvasPos;
}
if (MouseM.up())
{
tCanvasMoveCursorPos.reset();
tCanvasPos.reset();
lastAction = U"Moved";
}
if (tCanvasMoveCursorPos.has_value())
{
Cursor::RequestStyle(CursorStyle::Hand);
const Point offset = Cursor::Pos() - tCanvasMoveCursorPos.value();
canvasPos = tCanvasPos.value() + offset;
}
if (KeyR.down())
{
canvasPos = Point(50, 50);
lastAction = U"Reset";
}
// 描いたものを消去するボタンが押されたら
if (SimpleGUI::Button(U"Clear", Vec2{ 640, 500 }, 120))
{
data.clearAllImage();
data.updateTexture();
lastAction = U"Cleared";
}
if (SimpleGUI::Button(U"Save", Vec2{ 640, 140 }, 120))
{
Image result = data.generateIntegratedImage();
if (const auto filePath = Dialog::SaveImage())
{
result.savePNG(filePath.value());
}
lastAction = U"Saved";
}
HSV color = penColor;
if (SimpleGUI::ColorPicker(color, Vec2{ 640, 240 }, true))
{
penColor = ColorF(color.toColor(), 1.0);
r.text = ToString(penColor.r);
g.text = ToString(penColor.g);
b.text = ToString(penColor.b);
}
if (SimpleGUI::TextBox(r, colorTextBoxPos, 50, 3)
|| SimpleGUI::TextBox(g, colorTextBoxPos + Vec2(55, 0), 50, 3)
|| SimpleGUI::TextBox(b, colorTextBoxPos + Vec2(110, 0), 50, 3))
{
penColor = Color(ParseOr<int32>(r.text, 0), ParseOr<int32>(g.text, 0), ParseOr<int32>(b.text, 0));
}
clippedImageTexture.scaled(4).draw(colorTextBoxPos + Vec2(0, 100));
if (lastAction.has_value())
{
histories << GenerateHistory<PixelArtData>(data);
if (histories.size() > maxHisoryNum)
{
histories.pop_front();
}
//Print << histories.back().size_bytes();
lastAction.reset();
}
if (KeyZ.down())
{
undoFunction();
}
if (KeyC.down())
{
Clipboard::SetText(GenerateHistory<PixelArtData>(data).base64Str());
}
if (KeyN.down())
{
commonData.popup = std::make_unique<CreateNewImageWindow>();
}
// テクスチャを表示
}
void draw() const
{
auto& commonData = CommonData::getInstance();
auto& data = commonData.data;
{
{
const auto t = Transformer2D(Mat3x2::Scale(scaling) * Mat3x2::Translate(canvasPos));
data.draw();
}
if (scaling >= 16)
{
const auto t = Transformer2D(Mat3x2::Translate(canvasPos));
DrawGrid(data.m_canvasSize, scaling);
}
}
{
const auto t = TranslateDrawingPos(layerPos);
data.drawLayers(currentLayerIndex);
}
{
const auto t = TranslateDrawingPos(previewPos);
data.draw();
}
Circle(Cursor::PosF(), 5.0).draw(penColor);
}
void undoFunction()
{
auto& commonData = CommonData::getInstance();
auto& data = commonData.data;
auto& histories = commonData.histories;
if (histories.size() >= 2)
{
histories.pop_back();
RecoverHistory<PixelArtData>(data, histories.back());
data.updateTexture();
}
}
private:
int32 scaling = 16;
size_t currentLayerIndex = 0;
Point canvasPos{ 50, 50 };
const Point previewPos{ 0, 0 };
const Point layerPos{ 600, 0 };
Optional<Point> tCanvasMoveCursorPos;
Optional<Point> tCanvasPos;
Optional<String> lastAction;
Point colorTextBoxPos{ 640, 360 };
TextEditState r;
TextEditState g;
TextEditState b;
Image clippedImage;
DynamicTexture clippedImageTexture;
Optional<Point> AABBPoint1;
Optional<Point> AABBPoint2;
Color penColor = Palette::Orange;
int32 thickness = 1;
bool drawing = false;
// 歯車 (f013) のアイコン
const Texture fillTexture{ 0xF0266_icon, 40 };
const Texture penTexture{ 0xF03EB_icon, 40 };
const Texture undoTexture{ 0xF054C_icon, 40 };
Optional<String> operationMode = U"pen";
HashTable<String, std::function<void()>> operationFunctionTable;
Optional<Point> drawingPos;
};
void Main()
{
MainScene scene;
while (System::Update())
{
auto& popup = CommonData::getInstance().popup;
if (!popup)
scene.update();
scene.draw();
if (popup)
{
Scene::Rect().draw(ColorF(0.0, 0.5));
popup->update();
popup->draw();
if (popup->m_isDestoyed)
{
popup.reset();
}
}
}
}