データ可視化のサンプル¶
1. 有向グラフの描画¶
コード
# include <Siv3D.hpp>
using VertexID = int32;
struct Vertex
{
String name;
Vec2 pos;
void draw() const
{
pos.asCircle(40).draw(ColorF{ 0.95 }).drawFrame(2, ColorF{ 0.11 });
}
void drawLabel(const Font& font) const
{
font(name).drawAt(40, pos, ColorF{ 0.11 });
}
};
void DrawEdge(const Vertex& from, const Vertex& to)
{
Line{ from.pos, to.pos }.stretched(-40).drawArrow(3, Vec2{ 15, 15 }, ColorF{ 0.11 });
}
void Main()
{
Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });
const Font font{ FontMethod::MSDF, 36, Typeface::Bold };
Array<Array<VertexID>> graph(6);
graph[0] = { 1, 3 };
graph[1] = { 2 };
graph[2] = { 3 };
graph[3] = { 4 };
graph[4] = { 5 };
graph[5] = { 0 };
Array<Vertex> vertices;
for (size_t i = 0; i < graph.size(); ++i)
{
const double rad = (i * (Math::TwoPi / graph.size()));
vertices.push_back(Vertex{ Format(i), OffsetCircular{ Scene::Center(), 200, rad } });
}
while (System::Update())
{
for (const auto& v : vertices)
{
v.draw();
}
for (size_t from = 0; from < graph.size(); ++from)
{
for (const auto& to : graph[from])
{
DrawEdge(vertices[from], vertices[to]);
}
}
for (const auto& v : vertices)
{
v.drawLabel(font);
}
}
}
2. 有向グラフの描画(3D)¶
コード
# include <Siv3D.hpp>
using VertexID = int32;
struct Vertex
{
String name;
Vec3 pos;
void draw() const
{
Sphere{ pos, 1 }.draw();
}
void drawLabel(const Font& font, const BasicCamera3D& camera) const
{
font(name).drawAt(40, camera.worldToScreenPoint(pos).xy(), ColorF{ 0.11 });
}
};
void DrawEdge(const Vertex& from, const Vertex& to)
{
const Vec3 dir = (to.pos - from.pos).normalized();
Cylinder{ from.pos, (to.pos - dir * 2.0), 0.05 }.draw(ColorF{ 0.11 }.removeSRGBCurve());
Cone{ (to.pos - dir * 2.0), (to.pos - dir * 1.0), 0.3 }.draw(ColorF{ 0.11 }.removeSRGBCurve());
}
void Main()
{
const Font font{ FontMethod::MSDF, 36, Typeface::Bold };
const ColorF BackgroundColor = ColorF{ 0.6, 0.8, 0.7 }.removeSRGBCurve();
const MSRenderTexture renderTexture{ Scene::Size(), TextureFormat::R8G8B8A8_Unorm_SRGB, HasDepth::Yes };
constexpr Vec3 focusPosition{ 0, 0, 0 };
Vec3 eyePosition{ 0, 10, 0 };
BasicCamera3D camera{ renderTexture.size(), 45_deg, eyePosition, focusPosition};
Graphics3D::SetSunColor(ColorF{ 0.5 });
Graphics3D::SetGlobalAmbientColor(ColorF{ 0.5 });
Array<Array<VertexID>> graph(5);
graph[0] = { 1, 2, 3, 4 };
graph[1] = { 2, 3, 4 };
graph[2] = { 3, 4 };
const Array<Vertex> vertices =
{
{ U"0", Vec3{ 0, 6, 0 } },
{ U"1", Vec3{ -6, 0.5, 0 } },
{ U"2", Vec3{ 6, 0.5, 0 } },
{ U"3", Vec3{ 0, 0.5, 8 } },
{ U"4", Vec3{ 0, 0.5, -8 } },
};
while (System::Update())
{
// カメラを更新する
{
eyePosition = Cylindrical{ 20, Scene::Time() * 30_deg, 8 + Periodic::Sine0_1(4s) * 8 };
camera.setView(eyePosition, focusPosition);
Graphics3D::SetCameraTransform(camera);
}
{
const ScopedRenderTarget3D target{ renderTexture.clear(BackgroundColor) };
for (auto i : Range(-10, 10))
{
Line3D{ Vec3{ -10, 0, i }, Vec3{ 10, 0, i } }.draw(Linear::Palette::Seagreen);
Line3D{ Vec3{ i, 0, -10 }, Vec3{ i, 0, 10 } }.draw(Linear::Palette::Seagreen);
}
for (size_t from = 0; from < graph.size(); ++from)
{
for (const auto& to : graph[from])
{
DrawEdge(vertices[from], vertices[to]);
}
}
for (const auto& v : vertices)
{
v.draw();
}
}
// 3D シーンを 2D シーンに描画
{
Graphics3D::Flush();
renderTexture.resolve();
Shader::LinearToScreen(renderTexture);
}
{
const Mat4x4 mat = camera.getMat4x4();
for (const auto& v : vertices)
{
v.drawLabel(font, camera);
}
}
}
}
3. 二次元のヒートマップ¶
コード
# include <Siv3D.hpp>
Grid<double> GenerateGrid()
{
Grid<double> grid(20, 20);
PerlinNoise perlin{ RandomUint64() };
for (int32 y = 0; y < grid.height(); ++y)
{
for (int32 x = 0; x < grid.width(); ++x)
{
grid[y][x] = perlin.octave2D0_1((x / 24.0), (y / 24.0), 4);
}
}
return grid;
}
Image ToImage(const Grid<double>& grid, ColormapType colormapType)
{
Image image(grid.size());
for (int32 y = 0; y < grid.height(); ++y)
{
for (int32 x = 0; x < grid.width(); ++x)
{
const double value = grid[y][x];
image[y][x] = Colormap01(value, colormapType);
}
}
return image;
}
Image MakeColorBar(ColormapType colormapType)
{
Image image{ 1, 256 };
for (int32 y = 0; y < image.height(); ++y)
{
const double value = (1.0 - y / 255.0);
image[y][0] = Colormap01(value, colormapType);
}
return image;
}
void Main()
{
Scene::SetBackground(Palette::White);
const Font font{ FontMethod::MSDF, 36 };
constexpr int32 CellSize = 30;;
constexpr ColormapType ColorType = ColormapType::Turbo;
const Texture colorBar{ MakeColorBar(ColorType), TextureDesc::Mipped };
Grid<double> grid = GenerateGrid();
DynamicTexture texture{ ToImage(grid, ColorType) };
while (System::Update())
{
// データを再生成する
if (SimpleGUI::Button(U"Generate", Vec2{ 630, 40 }))
{
grid = GenerateGrid();
texture.fill(ToImage(grid, ColorType));
}
// ヒートマップを表示する
{
const ScopedRenderStates2D sampler{ SamplerState::ClampNearest };
texture.scaled(CellSize).draw();
}
// ヒートマップ上で値を表示する
{
const Point index = (Cursor::Pos() / CellSize);
if (InRange(index.x, 0, (static_cast<int32>(grid.width()) - 1))
&& InRange(index.y, 0, (static_cast<int32>(grid.height()) - 1)))
{
const double value = grid[index.y][index.x];
Rect{ (index * CellSize), CellSize }.drawFrame(2);
PutText(U"{:.3f}"_fmt(value), Arg::leftCenter = Cursor::Pos().movedBy(20, 0));
}
}
// カラーバーを表示する
{
const Rect colorBarRect{ 630, 320, 30, 200 };
const int32 step = ((colorBarRect.h) / 10);
colorBarRect(colorBar).draw().drawFrame(0, 1, Palette::Black);
for (int32 i = 0; i <= 10; i += 2)
{
Rect{ (colorBarRect.rightX() - 4), (colorBarRect.y + (i * step)), 4, 1 }.draw(Palette::Black);
}
for (int32 i = 0; i <= 10; i += 2)
{
font(U"{:.1f}"_fmt(1.0 - i / 10.0)).drawAt(14, Vec2{ (colorBarRect.rightX() + 18), (colorBarRect.y + (i * step)) }, Palette::Black);
}
}
}
}
4. 折れ線グラフ¶
コード
# include <Siv3D.hpp>
void DrawLineGraph(const Rect& graphArea, const Array<double>& values, double maxValue, const ColorF& color, double thickness)
{
const double xStep = (graphArea.w / (values.size() - 1.0));
const double yStep = (graphArea.h / maxValue);
LineString lines;
for (size_t i = 0; i < values.size(); ++i)
{
const double x = (graphArea.x + xStep * i);
const double y = (graphArea.y + graphArea.h - yStep * values[i]);
lines << Vec2{ x, y };
}
lines.draw(LineStyle::RoundCap, thickness, color);
}
void Main()
{
Scene::SetBackground(Palette::White);
const Rect graphArea{ 40, 40, 600, 400 };
Array<double> valuesA = { 10, 40, 20, 50, 30, 60 };
Array<double> valuesB = { 5, 30, 50, 40, 40, 30 };
while (System::Update())
{
if (SimpleGUI::Button(U"Add", Vec2{ 660, 40 }))
{
valuesA << (valuesA.back() * Random(0.8, 1.25));
valuesB << (valuesB.back() * Random(0.8, 1.25));
}
graphArea.left().draw(ColorF{ 0.11 });
graphArea.bottom().draw(ColorF{ 0.11 });
const double maxValue = Max(*std::max_element(valuesA.begin(), valuesA.end())
, *std::max_element(valuesB.begin(), valuesB.end())) * 1.1;
DrawLineGraph(graphArea, valuesA, maxValue, HSV{ 160, 1.0, 0.9 }, 4);
DrawLineGraph(graphArea, valuesB, maxValue, HSV{ 220, 1.0, 0.9 }, 4);
}
}
5. 関数グラフ¶
コード
# include <Siv3D.hpp>
void ToLineString(const Array<double>& values, const Vec2& start, LineString& ls, double yScale)
{
ls.resize(values.size());
for (size_t i = 0; i < values.size(); ++i)
{
ls[i] = (start + Vec2{ i, (values[i] * -yScale) });
}
}
void Main()
{
Scene::SetBackground(Palette::White);
constexpr size_t N = 600;
const Rect graphArea{ 40, 40, N, 400 };
const double xStart = -3.00;
const double xEnd = 3.0;
const double xStep = ((xEnd - xStart) / graphArea.w);
Array<double> valuesA(N + 1);
Array<double> valuesB(N + 1);
for (size_t i = 0; i < (N + 1); ++i)
{
const double x = (xStart + xStep * i);
valuesA[i] = Math::Sin(x);
valuesB[i] = Math::Cos(x);
}
const double yStart = -2.0;
const double yEnd = 2.0;
const double yStep = ((yEnd - yStart) / graphArea.h);
LineString lsA(N + 1), lsB(N + 1);
ToLineString(valuesA, graphArea.leftCenter(), lsA, (1.0 / yStep));
ToLineString(valuesB, graphArea.leftCenter(), lsB, (1.0 / yStep));
const double xAxisStep = 0.5;
const double yAxisStep = 0.5;
while (System::Update())
{
for (int32 x = static_cast<int32>(xStart / xAxisStep); x <= static_cast<int32>(xEnd / xAxisStep); ++x)
{
const double xAxis = (graphArea.x + (x * xAxisStep - xStart) / xStep);
const double thickness = ((x == 0) ? 2.0 : 0.3);
RectF{ Arg::topCenter(xAxis, graphArea.y), thickness, static_cast<double>(graphArea.h) }.draw(ColorF{ 0.11 });
}
for (int32 y = static_cast<int32>(yStart / yAxisStep); y <= static_cast<int32>(yEnd / yAxisStep); ++y)
{
const double yAxis = (graphArea.y + (y * yAxisStep - yStart) / yStep);
const double thickness = ((y == 0) ? 2.0 : 0.3);
RectF{ Arg::leftCenter(graphArea.x, yAxis), static_cast<double>(graphArea.w), thickness }.draw(ColorF{ 0.11 });
}
lsA.draw(3, HSV{ 160 });
lsB.draw(3, HSV{ 220 });
}
}
6. 円グラフ¶
コード
# include <Siv3D.hpp>
Array<double> ToRatios(const Array<double>& values)
{
const double sum = values.sumF();
Array<double> ratios;
for (const auto& value : values)
{
ratios << (value / sum);
}
return ratios;
}
Array<double> CumulativeSum(const Array<double>& values)
{
Array<double> sums = { 0.0 };
for (const auto& value : values)
{
sums << (sums.back() + value);
}
return sums;
}
void Main()
{
Scene::SetBackground(Palette::White);
const Font font{ FontMethod::MSDF, 48, Typeface::Bold };
// ラベル
const Array<String> labels = { U"Apple", U"Bird", U"Cat", U"Dog" };
// 数値
const Array<double> values = { 15.0, 10.0, 5.0, 2.0 };
// 円グラフで占める割合
const Array<double> ratios = ToRatios(values);
// 円グラフの開始位置(割合)
const Array<double> starts = CumulativeSum(ratios);
const Circle circle{ Scene::Center(), 180.0 };
while (System::Update())
{
// 円グラフを描画する
for (size_t i = 0; i < values.size(); ++i)
{
const double startAngle = (starts[i] * 360_deg);
const double angle = (ratios[i] * 360_deg);
circle.drawPie(startAngle, angle, HSV{(120 + 70 * i), 0.5, 0.95});
}
// 境界線を描画する
for (size_t i = 0; i < values.size(); ++i)
{
const double startAngle = (starts[i] * 360_deg);
Line{ circle.center, Arg::angle = startAngle, circle.r }.draw(3);
}
// ラベルを描画する
for (size_t i = 0; i < values.size(); ++i)
{
const double startAngle = (starts[i] * 360_deg);
const double angle = (ratios[i] * 360_deg);
const double midAngle = (startAngle + angle / 2.0);
// 割合に応じてラベルの位置を調整する
const Vec2 pos = OffsetCircular{ circle.center, ((ratios[i] < 0.1) ? 220.0 : (ratios[i] < 0.4) ? 120.0 : 90.0), midAngle };
font(labels[i]).draw(24, Arg::bottomCenter = pos, ColorF{ 0.11 });
font(U"{:.1f}%"_fmt(ratios[i] * 100.0)).draw(18, Arg::topCenter = pos, ColorF{ 0.11 });
}
}
}
7. kd 木¶
コード
# include <Siv3D.hpp>
struct Unit
{
Circle circle;
ColorF color;
void draw() const
{
circle.draw(color);
}
};
// Unit を KDTree で扱えるようにするためのアダプタ
struct UnitAdapter : KDTreeAdapter<Array<Unit>, Vec2>
{
static const element_type* GetPointer(const point_type& point)
{
return point.getPointer();
}
static element_type GetElement(const dataset_type& dataset, size_t index, size_t dim)
{
return dataset[index].circle.center.elem(dim);
}
};
void Main()
{
// 4000 個の Unit を生成する
Array<Unit> units;
for (size_t i = 0; i < 4000; ++i)
{
const Unit unit
{
.circle = Circle{ RandomVec2(Circle{100}), 0.25 },
.color = RandomColorF(),
};
units << unit;
}
// kd-tree を構築する
KDTree<UnitAdapter> kdTree{ units };
// 探索の種類(ラジオボタンのインデックス)
size_t searchTypeIndex = 0;
// radius search する際の探索距離
double searchDistance = 4.0;
// 2D カメラ
Camera2D camera{ Vec2{ 0, 0 }, 24.0 };
while (System::Update())
{
// 2D カメラを更新する
camera.update();
// 画面内のユニットだけ処理するための基準の長方形
const RectF viewRect = camera.getRegion();
const RectF viewRectScaled = viewRect.scaledAt(viewRect.center(), 1.2);
{
const auto transformer = camera.createTransformer();
const Vec2 cursorPos = Cursor::PosF();
if (searchTypeIndex == 0) // radius search
{
Circle{ cursorPos, searchDistance }.draw(ColorF{ 1.0, 0.2 });
// searchDistance 以内の距離にある Unit のインデックスを取得
for (auto index : kdTree.radiusSearch(cursorPos, searchDistance))
{
Line{ cursorPos, units[index].circle.center }.draw(0.1);
}
}
else // k-NN search
{
const size_t k = ((searchTypeIndex == 1) ? 1 : 5);
// 最も近い k 個の Unit のインデックスを取得
for (auto index : kdTree.knnSearch(k, cursorPos))
{
Line{ cursorPos, units[index].circle.center }.draw(0.1);
}
}
// ユニットを描画する
for (const auto& unit : units)
{
// 描画負荷削減のため、画面内 (viewRectScaled) に無ければスキップする
if (not unit.circle.center.intersects(viewRectScaled))
{
continue;
}
unit.draw();
}
}
SimpleGUI::RadioButtons(searchTypeIndex, { U"radius", U"k-NN (k=1)", U"k-NN (k=5)" }, Vec2{ 20, 20 });
SimpleGUI::Slider(U"searchDistance", searchDistance, 0.0, 20.0, Vec2{ 180, 20 }, 160, 120, (searchTypeIndex == 0));
if (SimpleGUI::Button(U"Move units", Vec2{ 180, 60 }))
{
// Unit をランダムに移動する
for (auto& unit : units)
{
unit.circle.moveBy(RandomVec2(0.5));
}
// Unit の座標が更新されたので kd-tree を再構築する
kdTree.rebuildIndex();
}
camera.draw(Palette::Orange);
}
}
8. DisjointSet¶
コード
# include <Siv3D.hpp>
void Main()
{
Window::Resize(1280, 720);
Scene::SetBackground(ColorF{ 0.8, 0.9, 1.0 });
// フォント
const Font font{ FontMethod::MSDF, 48, Typeface::Heavy };
// セルの大きさ
constexpr int32 CellSize = 16;
// マス目の数
constexpr Size GridSize{ 1280 / CellSize, 720 / CellSize };
// 塗りつぶし (白: true, 黒: false)
Grid<bool> grid(GridSize, true);
// Disjoint Set (Union-Find)
DisjointSet<int32> ds{ GridSize.x* GridSize.y };
// 現在存在する領域の root と, 領域の座標の合計値 (中心計算用)
HashTable<int32, Vec2> currentRoots;
// root の番号と色 (hue) の対応表
HashTable<int32, int32> globalColorTable;
int32 colorIndex = 0;
// UnionFind を更新する必要があるか
bool isDirty = true;
while (System::Update())
{
if (isDirty)
{
// Disjoint Set を更新する
{
ds.reset();
for (int32 y = 0; y < GridSize.y; ++y)
{
for (int32 x = 0; x < GridSize.x; ++x)
{
if (grid[y][x])
{
const int32 index = (y * GridSize.x + x);
if (int32 nx = (x + 1); nx < GridSize.x)
{
if (grid[y][nx])
{
ds.merge(index, index + 1);
}
}
if (int32 ny = (y + 1); ny < GridSize.y)
{
if (grid[ny][x])
{
ds.merge(index, (index + GridSize.x));
}
}
}
}
}
}
// 存在する root 一覧を作成する
{
currentRoots.clear();
for (int32 y = 0; y < GridSize.y; ++y)
{
for (int32 x = 0; x < GridSize.x; ++x)
{
if (grid[y][x])
{
const int32 index = (y * GridSize.x + x);
const int32 root = ds.find(index);
const Vec2 pos{ x, y };
if (auto it = currentRoots.find(root); it == currentRoots.end())
{
currentRoots.emplace(root, pos);
}
else
{
it->second += pos;
}
}
}
}
}
// root と色の対応表を更新する
{
for (auto& currentRoot : currentRoots)
{
if (not globalColorTable.contains(currentRoot.first))
{
globalColorTable.emplace(currentRoot.first, (colorIndex++ * 55));
}
}
EraseNodes_if(globalColorTable, [&](const auto& p) { return (not currentRoots.contains(p.first)); });
}
isDirty = false;
}
// すべてのマスを描画する
for (auto p : step(GridSize))
{
const Rect rect = Rect{ (p * CellSize), CellSize }.stretched(-1);
if (grid[p])
{
const int32 index = (p.y * GridSize.x + p.x);
const int32 root = ds.find(index);
rect.draw(HSV{ globalColorTable[root], 0.25, 1.0 });
}
else
{
rect.draw(ColorF{ 0.4 });
}
}
// クリックされたらマスの状態を更新する
if ((MouseL | MouseR).pressed())
{
const Point pos = (Cursor::Pos() / CellSize);
if (InRange(pos.x, 0, (GridSize.x - 1))
&& InRange(pos.y, 0, (GridSize.y - 1)))
{
const bool old = grid[pos];
grid[pos] = MouseL.pressed() ? false : true;
isDirty = (old != grid[pos]);
}
}
// 領域の情報を表示する
for (const auto& currentRoot : currentRoots)
{
const int32 root = currentRoot.first;
const int32 size = static_cast<int32>(ds.size(root));
const Vec2 center = currentRoot.second / size;
const HSV textColor = HSV{ globalColorTable[root], 0.55, 0.9 };
const Vec2 pos = (center * CellSize) + (Vec2::All(CellSize) * 0.5);
const double fontSize = (20 + 2 * Sqrt(size));
const double w = font(size).region(fontSize).w;
Circle{ pos, (w / 1.66 + 10) }.draw(ColorF{ 1.0, 0.88 }).drawFrame(3, textColor);
font(size).drawAt(fontSize, pos, textColor);
}
}
}
9. DisjointSet による画像の塗りつぶし領域の検出¶
コード
# include <Siv3D.hpp>
// グループ情報を構築する関数
void RebuildGroup(DisjointSet<int32>& ds, const Image& image)
{
assert(ds.size() == image.num_pixels());
ds.reset();
for (int32 y = 0; y < image.height(); ++y)
{
for (int32 x = 0; x < image.width(); ++x)
{
const int32 i = (y * image.width() + x);
if ((x + 1) < image.width())
{
if (image[y][x] == image[y][x + 1]) // 右隣のピクセルと同じ色なら
{
ds.merge(i, (i + 1)); // グループ化
}
}
if ((y + 1) < image.height())
{
if (image[y][x] == image[y + 1][x]) // 下のピクセルと同じ色なら
{
ds.merge(i, (i + image.width())); // グループ化
}
}
}
}
}
[[nodiscard]]
Optional<Point> GetPixelIndexFromCursorPos(const Size& canvasSize, const Point& canvasPos, int32 canvasScale)
{
const Vec2 cursorPos = Cursor::PosF();
const int32 x = static_cast<int32>(Math::Floor((cursorPos.x - canvasPos.x) / canvasScale));
const int32 y = static_cast<int32>(Math::Floor((cursorPos.y - canvasPos.y) / canvasScale));
if (InRange(x, 0, (canvasSize.x - 1))
&& InRange(y, 0, (canvasSize.y - 1)))
{
return Point{ x, y };
}
return none;
}
[[nodiscard]]
Rect PixelIndexToRect(const Point& pixelIndex, const Point& canvasPos, int32 canvasScale)
{
return Rect{ (canvasPos.x + pixelIndex.x * canvasScale), (canvasPos.y + pixelIndex.y * canvasScale), canvasScale, canvasScale };
}
[[nodiscard]]
Color GetPixel(Image& image, const Point& pixelIndex)
{
assert(InRange(pixelIndex.x, 0, (image.width() - 1)));
assert(InRange(pixelIndex.y, 0, (image.height() - 1)));
return image[pixelIndex];
}
bool SetPixel(Image& image, const Point& pixelIndex, const Color& color)
{
assert(InRange(pixelIndex.x, 0, (image.width() - 1)));
assert(InRange(pixelIndex.y, 0, (image.height() - 1)));
const Color oldColor = image[pixelIndex];
image[pixelIndex] = color;
return (color != oldColor);
}
bool FillPixel(Image& image, const Point& pixelIndex, DisjointSet<int32>& ds, const Color& color)
{
assert(InRange(pixelIndex.x, 0, (image.width() - 1)));
assert(InRange(pixelIndex.y, 0, (image.height() - 1)));
const int32 index = (pixelIndex.y * image.width() + pixelIndex.x);
const int32 group = ds.find(index);
bool updated = false;
for (int32 y = 0; y < image.height(); ++y)
{
for (int32 x = 0; x < image.width(); ++x)
{
const int32 i = (y * image.width() + x);
if (ds.find(i) == group)
{
updated |= SetPixel(image, Point{ x, y }, color);
}
}
}
return updated;
}
// 画像を描画する関数
void DrawImage(const Texture& texture, const Point& canvasPos, int32 canvasScale)
{
const ScopedRenderStates2D sampler{ SamplerState::ClampNearest };
texture.scaled(canvasScale).draw(canvasPos);
for (int32 y = 0; y <= texture.height(); ++y)
{
Rect{ (canvasPos.x - 1), (canvasPos.y + y * canvasScale - 1), (texture.width() * canvasScale + 2), 2 }.draw();
}
for (int32 x = 0; x <= texture.width(); ++x)
{
Rect{ (canvasPos.x + x * canvasScale - 1), (canvasPos.y - 1), 2, (texture.height() * canvasScale + 2) }.draw();
}
}
// グループ番号を可視化する関数
void DrawGroup(const Font& font, DisjointSet<int32>& ds, const Size& canvasSize, const Point& canvasPos, int32 canvasScale)
{
assert(ds.size() == (canvasSize.x * canvasSize.y));
for (int32 y = 0; y < canvasSize.y; ++y)
{
for (int32 x = 0; x < canvasSize.x; ++x)
{
const int32 i = (y * canvasSize.x + x);
const int32 group = ds.find(i);
const Rect rect = PixelIndexToRect(Point{ x, y }, canvasPos, canvasScale);
font(group).drawAt(12, rect.center());
}
}
}
void Main()
{
Window::Resize(1280, 720);
Scene::SetBackground(ColorF{ 0.8, 0.9, 1.0 });
const Font font{ FontMethod::MSDF, 36, Typeface::Bold };
// 画像のサイズ
constexpr Size CanvasSize{ 16, 16 };
// 総ピクセル数
constexpr int32 NumPixels = (CanvasSize.x * CanvasSize.y);
// デフォルトの色
constexpr Color DefaultColor{ 255, 255, 255, 0 };
// 画像の拡大率
constexpr int32 CanvasScale = 32;
// 画像の描画位置
constexpr Point CanvasPos{ 200, 60 };
// ペンの色
Color penColor{ 0, 0, 0, 255 };
HSV penColorHSV = penColor;
// 画像
Image image{ CanvasSize, DefaultColor };
// 塗りつぶしグループ情報(上下左右で接続されている同じ色 → 同じグループ番号)
DisjointSet<int32> ds(NumPixels);
// グループ情報を更新する
RebuildGroup(ds, image);
// 動的テクスチャ
DynamicTexture dtexture{ image };
while (System::Update())
{
// 選択されているピクセルのインデックス
const Optional<Point> pixelIndex = GetPixelIndexFromCursorPos(CanvasSize, CanvasPos, CanvasScale);
ClearPrint();
Print << pixelIndex;
// 更新
if (pixelIndex)
{
// 左クリックでピクセルの更新
if (MouseL.pressed())
{
if (SetPixel(image, *pixelIndex, penColor))
{
dtexture.fill(image);
RebuildGroup(ds, image);
}
}
// 右クリックで塗りつぶし
if (MouseR.pressed())
{
if (FillPixel(image, *pixelIndex, ds, penColor))
{
dtexture.fill(image);
RebuildGroup(ds, image);
}
}
}
// 描画
{
// 画像の描画
DrawImage(dtexture, CanvasPos, CanvasScale);
// グループ番号の可視化
DrawGroup(font, ds, CanvasSize, CanvasPos, CanvasScale);
// マウスオーバー時のピクセルの枠線
if (pixelIndex)
{
Cursor::RequestStyle(CursorStyle::Hand);
PixelIndexToRect(*pixelIndex, CanvasPos, CanvasScale).drawFrame(4, 0, penColor);
}
// カラーピッカー
if (SimpleGUI::ColorPicker(penColorHSV, Vec2{ 900, 40 }))
{
penColor = penColorHSV;
}
}
}
}
10. 幅優先探索の可視化¶
コード
# include <Siv3D.hpp>
void Main()
{
// 背景色を水色に
Scene::SetBackground(ColorF{ 0.8, 0.9, 1.0 });
// 距離の表示用フォント
const Font font{ FontMethod::MSDF, 48, Typeface::Bold };
// 迷路を可視化するときのマスのサイズ(ピクセル)
constexpr int32 CellSize = 40;
// 二次元配列: 迷路 (0: 通行可能, 1: 壁)
const Grid<int32> maze =
{
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
{ 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1 },
{ 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 },
{ 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1 },
{ 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1 },
{ 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1 },
{ 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1 },
{ 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
};
// 無限大を表現する数
constexpr int32 INF = 10000;
// 二次元配列: maze と同じサイズ、すべての要素を INF にセット
Grid<int32> distances(maze.size(), INF);
// 上、左、右、下のマスへのオフセット
constexpr Point Offsets[4] = { Point{ 0, -1 }, Point{ -1, 0 }, Point{ 1, 0 }, Point{ 0, 1 } };
// 全要素を確認できるように、std::queue の代わりに std::deque を使う
std::deque<Point> q;
// スタート位置
const Point start{ 1, 1 };
q.push_back(start);
distances[start] = 0;
// 更新の間隔(秒)
constexpr double UpdateTime = 0.5;
// 蓄積時間(秒)
double accumulatedTime = 0.0;
while (System::Update())
{
// 状態更新フラグ
bool update = false;
// 前フレームからの経過時間を加算
accumulatedTime += Scene::DeltaTime();
// 更新間隔を超えていたら
if (UpdateTime <= accumulatedTime)
{
accumulatedTime -= UpdateTime;
update = true;
}
// 幅優先探索
if (update && (not q.empty()))
{
const Point currentPos = q.front(); q.pop_front();
const int32 currentDistance = distances[currentPos];
for (const auto& offset : Offsets)
{
const Point nextPos = (currentPos + offset);
if ((maze[nextPos] == 0) && ((currentDistance + 1) < distances[nextPos]))
{
distances[nextPos] = (currentDistance + 1);
q.push_back(nextPos);
}
}
}
// 迷路の状態を可視化
for (int32 y = 0; y < maze.height(); ++y)
{
for (int32 x = 0; x < maze.width(); ++x)
{
// マスの正方形
const Rect rect = Rect{ (x * CellSize), (y * CellSize), CellSize }.stretched(-1);
if (maze[y][x] == 1) // 壁のマス
{
// 黒で表示
rect.draw(ColorF{ 0.25 });
}
else // 通行可能なマス
{
// 距離情報
const int32 distance = distances[y][x];
if (distance == INF)
{
// 灰色で表示
rect.draw(ColorF{ 0.75 });
font(U"∞").drawAt(18, rect.center(), ColorF{ 0.25 });
}
else
{
// 白で表示
rect.draw();
font(distances[y][x]).drawAt(18, rect.center(), ColorF{ 0.25 });
}
}
}
}
// queue に入っているマスの可視化
for (const auto& point : q)
{
// 赤い半透明の正方形を重ねる
Rect{ (point * CellSize), CellSize }.draw(ColorF{ 1.0, 0.0, 0.0, 0.5 });
}
}
}
11. 二次元いもす法の可視化¶
コード
# include <Siv3D.hpp>
void Main()
{
Window::Resize(1280, 720);
Scene::SetBackground(ColorF{ 0.8, 0.9, 1.0 });
// フォント
const Font font{ FontMethod::MSDF, 48, Typeface::Bold };
// セルの大きさ
constexpr int32 CellSize = 40;
// マス目の数
constexpr Size GridSize{ 1080 / CellSize, 720 / CellSize };
Grid<int32> grid(GridSize);
// 選択開始したセル
Optional<Point> grabbed;
// 長方形領域
Array<Rect> rects;
// 累積和計算位置
int32 iX = GridSize.x;
int32 iY = GridSize.y;
// 累積和アニメーションのストップウォッチ
Stopwatch stopwatch;
// アニメーションの速さ
double speed = 0.4;
while (System::Update())
{
// すべてのマスを描画
for (auto p : step(GridSize))
{
const Rect rect{ (p * CellSize), CellSize };
if (auto value = grid[p])
{
const ColorF color = (value < 0)
? ColorF{ 0.0, 0.4, 0.8 } : Colormap01F((value / 6.0), ColormapType::Viridis);
rect.stretched(1).draw(color);
}
else
{
rect.stretched(-1).draw();
}
}
// セルの数値を描画
for (auto p : step(GridSize))
{
const Rect rect = Rect{ (p * CellSize), CellSize }.stretched(-1);
font(grid[p]).drawAt(24, rect.center(), ColorF{ grid[p] ? 1.0 : 0.8 });
}
// 長方形の領域を描画
for (const auto& rect : rects)
{
Rect{ (rect.pos * CellSize), (rect.size * CellSize) }
.drawFrame(3, 1, ColorF{ 0.7 });
}
// 領域の選択を開始
if (MouseL.down())
{
const Point pos = (Cursor::Pos() / CellSize);
if (InRange(pos.x, 0, (GridSize.x - 1))
&& InRange(pos.y, 0, (GridSize.y - 1)))
{
grabbed = pos;
}
}
// 領域選択中
if (grabbed)
{
Point pos = (Cursor::Pos() / CellSize);
pos.x = Clamp(pos.x, 0, (GridSize.x - 1));
pos.y = Clamp(pos.y, 0, (GridSize.y - 1));
const Size size = (pos - *grabbed);
Rect rect{ *grabbed, size };
if (rect.w < 0)
{
rect.x += rect.w;
rect.w *= -1;
}
if (rect.h < 0)
{
rect.y += rect.h;
rect.h *= -1;
}
rect.size += Size::One();
Rect{ rect.pos * CellSize, rect.size * CellSize }
.draw(ColorF{ 0.1, 0.4, 0.7, 0.4 })
.drawFrame(3, 1, ColorF{ 0.7 });
if (MouseL.up())
{
rects << rect;
const Point tl = rect.tl();
const Point br = rect.br();
++grid[tl];
if ((br.x < GridSize.x) && (br.y < GridSize.y))
{
++grid[br];
}
if (br.x < GridSize.x)
{
--grid[{ br.x, tl.y }];
}
if (br.y < GridSize.y)
{
--grid[{ tl.x, br.y }];
}
grabbed.reset();
}
}
if (SimpleGUI::Button(U"X →", Vec2{ 1100, 20 }, 140))
{
iX = 1;
stopwatch.restart();
}
if (SimpleGUI::Button(U"Y ↓", Vec2{ 1100, 80 }, 140))
{
iY = 1;
stopwatch.restart();
}
if (SimpleGUI::Button(U"Reset", Vec2{ 1100, 140 }, 140))
{
rects.clear();
grid.fill(0);
iX = GridSize.x;
iY = GridSize.y;
}
SimpleGUI::Slider(U">>", speed, 0.0, 0.5, Vec2{ 1100, 200 }, 30, 110);
// X 方向累積和(アニメーション)
if (iX < GridSize.x)
{
Line{ (iX * CellSize), 0, (iX * CellSize), 720 }.draw(4, Palette::Red);
if (SecondsF{ 0.5 - speed } <= stopwatch)
{
for (int32 y = 0; y < GridSize.y; ++y)
{
grid[y][iX] += grid[y][iX - 1];
}
++iX;
stopwatch.restart();
}
}
// Y 方向累積和(アニメーション)
if (iY < GridSize.y)
{
Line{ 0, (iY * CellSize), 1080, (iY * CellSize) }.draw(4, Palette::Red);
if (SecondsF{ 0.5 - speed } <= stopwatch)
{
for (int32 x = 0; x < GridSize.x; ++x)
{
grid[iY][x] += grid[iY - 1][x];
}
++iY;
stopwatch.restart();
}
}
}
}