Skip to content

UI のサンプル

1. 選択領域を点線で描画する

コード
# include <Siv3D.hpp>

/// @brief 長方形の領域を選択する点線を描画します。
/// @param start 選択の開始位置
/// @param cursorPos マウスカーソルの位置
/// @param thickness 線の太さ
/// @param lineColor 線の色
void DrawSelectRect(const Vec2& start, const Vec2& cursorPos, double thickness, const ColorF& lineColor = Palette::White)
{
	const RectF rect = RectF::FromPoints(start, cursorPos);

	Line top = rect.top(), right = rect.right(), bottom = rect.bottom(), left = rect.left();

	// 始点からの点線の伸縮が自然になるよう、線の方向を調整する
	{
		if (cursorPos.x < start.x)
		{
			top.reverse();
		}

		if (start.x < cursorPos.x)
		{
			bottom.reverse();
		}

		if (cursorPos.y < start.y)
		{
			right.reverse();
		}

		if (start.y < cursorPos.y)
		{
			left.reverse();
		}
	}

	top.draw(LineStyle::SquareDot, thickness, lineColor);
	right.draw(LineStyle::SquareDot, thickness, lineColor);
	bottom.draw(LineStyle::SquareDot, thickness, lineColor);
	left.draw(LineStyle::SquareDot, thickness, lineColor);
}

void Main()
{
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

	constexpr double Thickness = 4.0;

	Optional<Point> start;

	while (System::Update())
	{
		if (MouseL.down())
		{
			start = Cursor::Pos();
		}
		else if (MouseL.up())
		{
			start.reset();
		}

		if (start && MouseL.pressed())
		{
			DrawSelectRect(*start, Cursor::Pos(), Thickness);
		}
	}
}

2. プルダウン

コード
# include <Siv3D.hpp>

class Pulldown
{
public:

	Pulldown() = default;

	Pulldown(const Array<String>& items, const Font& font, const double fontSize, const Vec2& pos)
		: m_font{ font }
		, m_fontSize{ fontSize }
		, m_items{ items }
		, m_maxitemWidth{ getMaxItemWidth() }
		, m_rect{ getRect(pos) } {}

	bool isEmpty() const noexcept
	{
		return m_items.empty();
	}

	void setPos(const Vec2& pos) noexcept
	{
		m_rect.setPos(pos);
	}

	[[nodiscard]]
	const RectF& getRect() const noexcept
	{
		return m_rect;
	}

	[[nodiscard]]
	size_t getIndex() const noexcept
	{
		return m_index;
	}

	[[nodiscard]]
	const Array<String>& getItems() const noexcept
	{
		return m_items;
	}

	void update()
	{
		if (isEmpty())
		{
			return;
		}

		if (m_rect.leftClicked())
		{
			m_isOpen = (not m_isOpen);
			MouseL.clearInput();
		}

		if (not m_isOpen)
		{
			return;
		}

		Vec2 itemPos = m_rect.pos.movedBy(0, m_rect.h);

		for (size_t i = 0; i < m_items.size(); ++i)
		{
			const RectF itemRect{ itemPos, m_rect.w, m_rect.h };

			if (itemRect.leftClicked())
			{
				m_index = i;
				m_isOpen = false;
				MouseL.clearInput();
				break;
			}

			itemPos.y += m_rect.h;
		}
	}

	void draw() const
	{
		m_rect.draw();

		if (isEmpty())
		{
			return;
		}

		m_rect.drawFrame(1, 0, m_isOpen ? Palette::Orange : Palette::Gray);

		m_font(m_items[m_index]).draw(m_fontSize, (m_rect.pos + Padding), TextColor);

		Triangle{ (m_rect.rightX() - DownButtonSize / 2.0 - Padding.x), (m_rect.y + m_rect.h / 2.0),
			(DownButtonSize * 0.5), 180_deg }.draw(TextColor);

		if (not m_isOpen)
		{
			return;
		}

		Vec2 itemPos = m_rect.bl();

		const RectF backRect{ itemPos, m_rect.w, (m_rect.h * m_items.size()) };

		backRect.drawShadow({ 1, 1 }, 5, 0).draw();

		for (const auto& item : m_items)
		{
			const RectF rect{ itemPos, m_rect.size };

			if (rect.mouseOver())
			{
				rect.draw(Palette::Skyblue);
			}

			m_font(item).draw(m_fontSize, (itemPos + Padding), TextColor);

			itemPos.y += m_rect.h;
		}

		backRect.drawFrame(1, 0, Palette::Gray);
	}

private:

	static constexpr Size Padding{ 8, 2 };

	static constexpr int32 DownButtonSize = 16;

	static constexpr ColorF TextColor{ 0.11 };

	Font m_font;

	double m_fontSize = 12;

	Array<String> m_items;

	size_t m_index = 0;

	double m_maxitemWidth = 0;

	RectF m_rect{ 0 };

	bool m_isOpen = false;

	[[nodiscard]]
	double getMaxItemWidth() const
	{
		double result = 0.0;

		for (const auto& item : m_items)
		{
			result = Max(result, (m_font(item).region(m_fontSize).w));
		}

		return result;
	}

	[[nodiscard]]
	RectF getRect(const Vec2& pos) const noexcept
	{
		const double fontHeight = (m_font.height() * (m_fontSize / m_font.fontSize()));

		return{
			pos,
			(m_maxitemWidth + (Padding.x * 3 + DownButtonSize)),
			(fontHeight + Padding.y * 2)
		};
	}
};

void Main()
{
	Scene::SetBackground(ColorF{ 0.8, 0.9, 1.0 });

	const Font font{ FontMethod::MSDF, 48 };

	const Array<String> items = { U"日本語", U"English", U"中文", U"Español", U"Français" };

	Pulldown pulldown1{ items, font, 24, Vec2{ 160, 40 } };

	Pulldown pulldown2{ items, font, 16, Vec2{ 320, 40 } };

	while (System::Update())
	{
		ClearPrint();
		Print << pulldown1.getItems()[pulldown1.getIndex()];
		Print << pulldown2.getItems()[pulldown2.getIndex()];

		pulldown1.update();
		pulldown2.update();

		pulldown1.draw();
		pulldown2.draw();
	}
}

3. トースト通知(Windows 版)

コード
# include <Siv3D.hpp>

void Main()
{
	Scene::SetBackground(ColorF{ 0.9, 0.6, 0.3 });

	// 通知ごとに割り振られる ID
	ToastNotificationID latest = -1;

	// 画像を作成・保存
	Emoji::CreateImage(U"🍕").save(U"pizza.png");

	while (System::Update())
	{
		ClearPrint();

		// 通知の状態
		Print << (int32)Platform::Windows::ToastNotification::GetState(latest);

		// アクションボタンの結果
		Print << U"Action: " << Platform::Windows::ToastNotification::GetAction(latest);

		if (SimpleGUI::Button(U"Send a notification", Vec2{ 10, 70 }))
		{
			const ToastNotificationItem toast{
				.title = U"Title", // 通知のタイトル
				.message = U"Message", // 通知の本文
				.imagePath = U"pizza.png", // 大きい画像だと使われないことがある
				.actions = { U"Yes", U"No" } // アクションボタン(不要な場合は設定しない)
			};

			// 通知ごとに割り振られる ID を取得
			latest = Platform::Windows::ToastNotification::Show(toast);
		}
	}
}

4. 手書き風 UI

コード
# include <Siv3D.hpp>

struct Button
{
	Rect rect;
	String label;
};

class PenEffect
{
public:

	PenEffect() = default;

	explicit PenEffect(const Size& size)
		: m_texture{ size, ColorF{ 1.0, 0.0 } }
	{
		initLines();
	}

	void reset()
	{
		initLines();
		m_texture.clear(ColorF{ 1.0, 0.0 });
		m_texture.resolve();
	}

	void update(double delta)
	{
		m_accumulatedLength = Min(m_accumulatedLength + (m_length * delta), m_length);

		if ((4.0 <= (m_accumulatedLength - m_paintedLength)))
		{
			BlendState bs = BlendState::Default2D;
			bs.srcAlpha = Blend::SrcAlpha;
			bs.dstAlpha = Blend::DestAlpha;
			bs.opAlpha = BlendOp::Max;
			const ScopedRenderStates2D blend(bs);
			const ScopedRenderTarget2D target{ m_texture };

			while (4.0 <= (m_accumulatedLength - m_paintedLength))
			{
				m_lines.calculatePointFromOrigin(m_paintedLength)
					.asCircle(6).draw(ColorF{ 1.0 });

				m_paintedLength += 4.0;
			}

			Graphics2D::Flush();
			m_texture.resolve();
		}
	}

	const Texture& getTexture() const
	{
		return m_texture;
	}

private:

	void initLines()
	{
		m_lines.clear();

		const Size size = m_texture.size();

		Point penPos{ 8, (size.y - Random(8, 24)) };

		for (;;)
		{
			m_lines << penPos;
			penPos.x += Random(18, 28);
			penPos.y = Random(6, 20);

			if ((size.x - 8) < penPos.x)
			{
				break;
			}

			m_lines << penPos;
			penPos.x -= Random(8, 16);
			penPos.y = size.y - Random(6, 20);
		}

		m_length = m_lines.calculateLength();
		m_accumulatedLength = 0.0;
		m_paintedLength = 0.0;
	}

	MSRenderTexture m_texture;

	LineString m_lines;

	double m_length = 0.0;

	double m_accumulatedLength = 0.0;

	double m_paintedLength = 0.0;
};

void Main()
{
	const ColorF backgroundColor{ 1.0, 0.98, 0.96 };
	Scene::SetBackground(backgroundColor);

	const Array<Button> buttons =
	{
		Button{ Rect{ Arg::center(400, 300), 300, 80 }, U"あそぶ" },
		Button{ Rect{ Arg::center(400, 400), 300, 80 }, U"スコア" },
		Button{ Rect{ Arg::center(400, 500), 300, 80 }, U"おわる" },
	};

	const Font font{ FontMethod::MSDF, 48, Typeface::Bold };

	Array<PenEffect> penEffects =
	{
		PenEffect{ Size{300, 90} },
		PenEffect{ Size{300, 90} },
		PenEffect{ Size{300, 90} }
	};

	Optional<size_t> selectedItem;
	Stopwatch stopwatch{ StartImmediately::Yes };

	while (System::Update())
	{
		if (SimpleGUI::Button(U"Restart", Vec2{ 20,20 }))
		{
			for (auto& penEffect : penEffects)
			{
				penEffect.reset();
			}

			stopwatch.restart();
		}

		for (size_t i = 0; i < penEffects.size(); ++i)
		{
			if ((i * 250) < stopwatch.ms())
			{
				penEffects[i].update(Scene::DeltaTime() * 0.5);
			}
		}

		selectedItem.reset();

		for (size_t i = 0; i < buttons.size(); ++i)
		{
			const auto& button = buttons[i];

			if (button.rect.mouseOver())
			{
				selectedItem = i;
				Cursor::RequestStyle(CursorStyle::Hand);
			}

			const bool selected = (selectedItem == i);

			penEffects[i].getTexture().drawAt(button.rect.center(), HSV{ 30 + i * 60 });

			font(button.label)
				.drawAt(TextStyle::OutlineShadow(0.3, HSV{ backgroundColor } - HSV{0.0, 0.0, 0.5}, Vec2{ 0, 0 }, backgroundColor),
					(selected ? 48 : 40), button.rect.center(), ColorF{ 1.0, 0.0 });
		}
	}
}

5. メニュー画面

コード
# include <Siv3D.hpp>

void Main()
{
	Scene::SetBackground(ColorF{ 0.9, 0.95, 1.0 });

	while (System::Update())
	{
		Rect{ 450, 0, 100, 600 }
			.shearedX(150).draw(HSV{ 40, 0.5, 1.0 });

		Rect{ 550, 0, 400, 600 }
			.shearedX(150).draw(HSV{ 40, 0.8, 1.0 });

		for (int32 i = 0; i < 5; ++i)
		{
			const RoundRect rr{ 50, (60 + i * 100), 350, 80, 40 };

			rr.drawShadow(Vec2{ 4, 4 }, 18, 0)
				.draw();

			Circle{ rr.rect.pos.movedBy(rr.r, rr.r), rr.r }
				.stretched(-5)
				.draw(HSV{ 40, 0.5, 1.0 });
		}
	}
}

6. スプレッドシート

コード
# include <Siv3D.hpp>

void Main()
{
	Window::Resize(1280, 720);
	Scene::SetBackground(ColorF{ 0.8, 0.9, 1.0 });

	constexpr int32 CellCountX = 8;
	constexpr int32 CellCountY = 15;

	Array<double> minColumnWidths((CellCountX + 1), 120);
	minColumnWidths[0] = 20;

	Array<String> columnNames = { U"" };
	for (int32 i = 1; i < (CellCountX + 1); ++i)
	{
		columnNames.push_back(String{ (U'A' + i - 1) });
	}

	SimpleTable table{ minColumnWidths, {
		.cellHeight = 36,
		.variableWidth = true,
	} };
	table.push_back_row(columnNames, Array<int32>((CellCountX + 1), 0));
	table.setRowBackgroundColor(0, ColorF{ 0.9 });

	for (int32 i = 1; i < (CellCountY + 1); ++i)
	{
		Array<String> row(CellCountX + 1);
		row[0] = U"{}"_fmt(i);

		Array<int32> rowAlignments((CellCountX + 1), 1);
		rowAlignments[0] = 0;

		table.push_back_row(row, rowAlignments);
		table.setBackgroundColor(i, 0, ColorF{ 0.9 });
	}

	Optional<Point> activeIndex;
	Optional<Point> nextActiveIndex;
	TextEditState textEditState;

	while (System::Update())
	{
		if (nextActiveIndex)
		{
			activeIndex = *nextActiveIndex;
			textEditState = TextEditState{ table.getItem(*activeIndex).text };
			textEditState.cursorPos = textEditState.text.length();
			textEditState.active = true;
			nextActiveIndex.reset();
		}

		{
			constexpr Vec2 TablePos{ 40, 40 };

			if (MouseL.down())
			{
				const auto newActiveIndex = table.cellIndex(TablePos, Cursor::Pos());

				if (newActiveIndex != activeIndex)
				{
					activeIndex = table.cellIndex(TablePos, Cursor::Pos());

					if (activeIndex)
					{
						textEditState = TextEditState{ table.getItem(*activeIndex).text };
						textEditState.cursorPos = textEditState.text.length();
						textEditState.active = true;
						MouseL.clearInput();
					}
				}
			}

			table.draw(TablePos);

			if (activeIndex && ((activeIndex->x != 0) && (activeIndex->y != 0)))
			{
				const RectF cellRegion = table.cellRegion(TablePos, *activeIndex);

				if (SimpleGUI::TextBox(textEditState, cellRegion.pos, cellRegion.w))
				{
					table.setText(*activeIndex, textEditState.text);
				}

				if (textEditState.enterKey)
				{
					nextActiveIndex = Point{ activeIndex->x, (activeIndex->y + 1) };
				}

				if ((1 < activeIndex->y) && KeyUp.down())
				{
					nextActiveIndex = Point{ activeIndex->x, (activeIndex->y - 1) };
				}

				if ((activeIndex->y < CellCountY) && KeyDown.down())
				{
					nextActiveIndex = Point{ activeIndex->x, (activeIndex->y + 1) };
				}

				if ((1 < activeIndex->x) && KeyLeft.down())
				{
					nextActiveIndex = Point{ (activeIndex->x - 1), activeIndex->y };
				}

				if ((activeIndex->x < CellCountX) && KeyRight.down())
				{
					nextActiveIndex = Point{ (activeIndex->x + 1), activeIndex->y };
				}

				if (KeyDelete.down())
				{
					textEditState.clear();
					table.setText(*activeIndex, U"");
				}
			}
		}
	}
}

7. 絵文字付きのボタン

コード
# include <Siv3D.hpp>

// ボタンの背景テクスチャを作成する
Texture CreateButtonTexture()
{
	MSRenderTexture renderTexture{ Size{ 160, 60 }, ColorF{ 0.96 } };
	{
		const ScopedRenderTarget2D renderTarget{ renderTexture };

		const ColorF PatternColor{ 0.85 };

		for (int32 x = 0; x <= 8; ++x)
		{
			RectF{ Arg::center((x * 20), 25), 2 }.rotated(45_deg).draw(PatternColor);
			RectF{ Arg::center((10 + x * 20), 30), 3 }.rotated(45_deg).draw(PatternColor);
			RectF{ Arg::center((x * 20), 35), 4 }.rotated(45_deg).draw(PatternColor);
			RectF{ Arg::center((10 + x * 20), 40), 5 }.rotated(45_deg).draw(PatternColor);
			RectF{ Arg::center((x * 20), 45), 6 }.rotated(45_deg).draw(PatternColor);
			RectF{ Arg::center((10 + x * 20), 50), 7 }.rotated(45_deg).draw(PatternColor);
			RectF{ Arg::center((x * 20), 55), 8 }.rotated(45_deg).draw(PatternColor);
			RectF{ Arg::center((10 + x * 20), 60), 9 }.rotated(45_deg).draw(PatternColor);
		}
	}

	// MSRenderTexture の完成には
	// 2D 描画命令の発行 (Flush) + MSAA の解決 (Resolve) が必要
	Graphics2D::Flush();
	renderTexture.resolve();

	// 完成したテクスチャを返す
	return renderTexture;
}

class RichButton
{
public:

	RichButton() = default;

	explicit RichButton(const Emoji& emoji)
		: m_emoji{ emoji }
		, m_bufferedEmoji{ MakeRoundBuffer(CreateEmojiPolygons(emoji), 5).scaled(EmojiScale) } {}

	void draw(const Rect& rect, const Texture& buttonTexture, const Font& font, const String& text)
	{
		const ColorF PrimaryColor{ 0.3, 0.5, 1.0 };

		const RoundRect roundRect{ rect, 10 };

		const bool mouseOver = roundRect.mouseOver();

		m_transition.update((not roundRect.intersects(Cursor::PreviousPos())) && mouseOver);

		if (mouseOver)
		{
			Cursor::RequestStyle(CursorStyle::Hand);

			roundRect(buttonTexture).draw(MouseL.pressed() ? ColorF{ 0.95 } : ColorF{ 1.05 }).drawFrame(0, 3, PrimaryColor);
		}
		else
		{
			roundRect(buttonTexture).draw().drawFrame(2);
		}

		{
			double angle = Math::Sin(m_transition.value() * 8_pi) * 5_deg * m_transition.value();
			const Vec2 emojiCenter = rect.getRelativePoint(0.5, 0.05);

			{
				const Transformer2D transformer{ Mat3x2::Rotate(angle, emojiCenter) };
				m_bufferedEmoji.draw(emojiCenter, mouseOver ? PrimaryColor : ColorF{ 0.3, 0.25, 0.2 });
				m_emoji.scaled(EmojiScale).rotated(angle).drawAt(emojiCenter);
			}
		}

		font(text).drawAt(TextStyle::Outline(0.0, 0.2, ColorF{ 1.0 }), 26, rect.getRelativePoint(0.5, 0.7), PrimaryColor);
	}

private:

	Texture m_emoji;

	MultiPolygon m_bufferedEmoji;

	Transition m_transition{ 0.0s, 0.8s };

	static constexpr double EmojiScale = 0.4;

	static MultiPolygon CreateEmojiPolygons(const Emoji& emoji)
	{
		return Image{ emoji }.alphaToPolygonsCentered(160, AllowHoles::No);
	}

	static MultiPolygon MakeRoundBuffer(const MultiPolygon& polygons, int32 distance)
	{
		MultiPolygon result;

		for (const auto& polygon : polygons)
		{
			result = Geometry2D::Or(result, polygon.calculateRoundBuffer(distance));
		}

		return result;
	}
};

void Main()
{
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

	const Texture buttonTexture = CreateButtonTexture();
	const Font font{ FontMethod::MSDF, 48, Typeface::Heavy };

	RichButton button1{ U"🗺"_emoji };
	RichButton button2{ U"🛠"_emoji };
	RichButton button3{ U"✉"_emoji };
	RichButton button4{ U"⚙"_emoji };

	while (System::Update())
	{
		button1.draw(Rect{ 40, 500, 160, 60 }, buttonTexture, font, U"マップ");
		button2.draw(Rect{ 220, 500, 160, 60 }, buttonTexture, font, U"開発");
		button3.draw(Rect{ 400, 500, 160, 60 }, buttonTexture, font, U"お知らせ");
		button4.draw(Rect{ 580, 500, 160, 60 }, buttonTexture, font, U"設定");
	}
}

8. 奥行き型 UI

コード
# include <Siv3D.hpp>

// ホモグラフィ変換シェーダのパラメータ
struct Homography
{
	Float4 m1;
	Float4 m2;
	Float4 m3;
};

void Main()
{
	Window::Resize(1000, 600);
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

	// ホモグラフィ変換用のシェーダ
	const VertexShader vs = HLSL{ U"example/shader/hlsl/homography.hlsl", U"VS" }
		| GLSL{ U"example/shader/glsl/homography.vert", {{ U"VSConstants2D", 0 }, { U"VSHomography", 1} } };
	const PixelShader ps = HLSL{ U"example/shader/hlsl/homography.hlsl", U"PS" }
		| GLSL{ U"example/shader/glsl/homography.frag", {{ U"PSConstants2D", 0 }, { U"PSHomography", 1} } };

	if ((not vs) || (not ps))
	{
		throw Error{ U"Failed to load shader files" };
	}

	// ホモグラフィ変換シェーダの定数バッファ(パラメータ)
	ConstantBuffer<Homography> vsHomography;
	ConstantBuffer<Homography> psHomography;

	const Font font{ FontMethod::MSDF, 48, Typeface::Bold };
	const Texture compassIcon{ 0xF018B_icon, 90 };
	const Texture swordIcon{ 0xF18BE_icon, 90 };
	const Texture plusIcon{ 0xF0417_icon, 42 };
	const Texture moneyEmoji{ U"💰"_emoji };
	const Texture gemEmoji{ U"💎"_emoji };
	const ColorF PrimaryColor{ 0.98, 0.96, 0.94 };
	const ColorF HoverColor{ 1.0, 0.96, 0.8 };

	// 変換前の四角形
	const Rect BaseRect{ 0, 0, 600, 600 };
	// 変換後の四角形
	const Quad TargetQuad{ 500, 60, 1000, 0, 1000, 600, 480, 520 };
	// ホモグラフィ変換の射影行列を得る
	const Mat3x3 projection = Mat3x3::Homography(Rect{ 600 }.asQuad(), TargetQuad);

	const Rect Button1{ 40, 40, 560, 200 };
	const Rect Button2{ 100, 260, 240, 100 };
	const Rect Button3{ 360, 260, 240, 100 };
	const Rect Button4{ 160, 380, 440, 140 };
	const Rect Button5{ Arg::center(230, 570), 40 };

	// 各ボタンの射影後の四角形
	const Quad Button1Quad = projection.transformRect(Button1);
	const Quad Button2Quad = projection.transformRect(Button2);
	const Quad Button3Quad = projection.transformRect(Button3);
	const Quad Button4Quad = projection.transformRect(Button4);
	const Quad Button5Quad = projection.transformRect(Button5);

	// UI の描画先のレンダーテクスチャ
	MSRenderTexture renderTexture{ BaseRect.size };

	while (System::Update())
	{
		// レンダーテクスチャに UI を描く
		{
			// renderTexture を ColorF{ 1.0, 0.0 } でクリアし,
			// renderTexture をレンダーターゲットにする
			const ScopedRenderTarget2D renderTarget{ renderTexture.clear(ColorF{ 1.0, 0.0 }) };

			// renderTexture のアルファ値がすべて 0 なので、最大のアルファ値を書き込むようなブレンドステートを適用する
			BlendState blend = BlendState::Default2D;
			blend.opAlpha = BlendOp::Max;
			blend.dstAlpha = Blend::DestAlpha;
			blend.srcAlpha = Blend::SrcAlpha;
			const ScopedRenderStates2D renderState{ blend };

			// UI を描画する
			{
				// 探索
				{
					Button1.movedBy(12, 10).draw(ColorF{ 0.5, 0.4, 0.3 });
					Button1.draw(Button1Quad.mouseOver() ? HoverColor : PrimaryColor);
					font(U"探索").draw(88, Arg::leftCenter(80, 140), ColorF{ 0.4, 0.3, 0.2 });
					if (Button1Quad.mouseOver())
					{
						Cursor::RequestStyle(CursorStyle::Hand);
					}
				}

				// 任務
				{
					Button2.movedBy(12, 10).draw(ColorF{ 0.5, 0.4, 0.3 });
					Button2.draw(Button2Quad.mouseOver() ? HoverColor : PrimaryColor);
					font(U"任務").draw(44, Arg::leftCenter(120, 310), ColorF{ 0.4, 0.3, 0.2 });
					compassIcon.drawAt(280, 310, ColorF{ 0.8 });
					if (Button2Quad.mouseOver())
					{
						Cursor::RequestStyle(CursorStyle::Hand);
					}
				}

				// 編成
				{
					Button3.movedBy(12, 10).draw(ColorF{ 0.5, 0.4, 0.3 });
					Button3.draw(Button3Quad.mouseOver() ? HoverColor : PrimaryColor);
					font(U"編成").draw(44, Arg::leftCenter(380, 310), ColorF{ 0.4, 0.3, 0.2 });
					swordIcon.drawAt(540, 310, ColorF{ 0.8 });
					if (Button3Quad.mouseOver())
					{
						Cursor::RequestStyle(CursorStyle::Hand);
					}
				}

				// イベント
				{
					Button4.draw(ColorF{ 0.2, 0.4, 0.6 });
					font(U"イベント").draw(33, Arg::leftCenter(180, 415));
					if (Button4Quad.mouseOver())
					{
						Cursor::RequestStyle(CursorStyle::Hand);
					}
				}

				// ジェムとお金
				{
					Rect{ 60, 540, 540, 60 }.draw(ColorF{ 0.0, 0.6 });
					gemEmoji.scaled(0.36).drawAt(120, 570);
					font(U"67").draw(TextStyle::Outline(0.0, 0.2, ColorF{ 0.1 }), 36, Arg::leftCenter(150, 570));

					Circle{ Button5.center(), 20 }.draw(ColorF{ 0.2, 0.8 });
					plusIcon.drawAt(Button5.center(), Button5Quad.mouseOver() ? HoverColor : PrimaryColor);
					if (Button5Quad.mouseOver())
					{
						Cursor::RequestStyle(CursorStyle::Hand);
					}

					moneyEmoji.scaled(0.36).drawAt(300, 570);
					font(ThousandsSeparate(12345)).draw(TextStyle::Outline(0.0, 0.2, ColorF{ 0.1 }), 36, Arg::leftCenter(330, 570));
				}
			}

			// MSRenderTexture の完成には
			// 2D 描画命令の発行 (Flush) + MSAA の解決 (Resolve) が必要
			Graphics2D::Flush();
			renderTexture.resolve();
		}

		// 奥行き型の UI を描く
		{
			// 右端に向かって影の効果
			Rect{ 460, 0, 540, 600 }.draw(Arg::left = ColorF{ 0.0, 0.0 }, Arg::right = ColorF{ 0.0, 0.2 });

			// レンダーテクスチャをホモグラフィ変換で射影する
			{
				const ScopedCustomShader2D shader{ vs, ps };
				const ScopedRenderStates2D sampler{ SamplerState::ClampAniso };

				const Mat3x3 mat = Mat3x3::Homography(TargetQuad);
				vsHomography = { Float4{ mat._11_12_13, 0 }, Float4{ mat._21_22_23, 0 }, Float4{ mat._31_32_33, 0 } };
				Graphics2D::SetVSConstantBuffer(1, vsHomography);

				const Mat3x3 inv = mat.inverse();
				psHomography = { Float4{ inv._11_12_13, 0 }, Float4{ inv._21_22_23, 0 }, Float4{ inv._31_32_33, 0 } };
				Graphics2D::SetPSConstantBuffer(1, psHomography);

				// 1x1 の Rect に貼り付けて描くと、適切にホモグラフィ変換される
				Rect{ 1 }(renderTexture).draw();
			}
		}
	}
}

9. 文章中に画像を挿入する

コード
# include <Siv3D.hpp>

struct Item
{
	Texture icon;

	String name;

	String desc;
};

void Main()
{
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

	const Font font1{ 30, Typeface::Heavy }, font2{ 15, Typeface::Bold };

	const Array<Texture> emojis = {
		Texture{ U"⚙️"_emoji }, Texture{ U"⚡"_emoji }, Texture{ U"♥"_emoji } };

	const Array<Item> items =
	{
		{ Texture{ U"🏭"_emoji }, U"工場", U"毎ターン 6 $0 を生産する\n電力 3 $1 が必要" },
		{ Texture{ U"🏟"_emoji }, U"スタジアム", U"毎ターン 4 $2 を供給する\n電力 2 $1 が必要" },
		{ Texture{ U"🏖"_emoji }, U"ビーチ", U"毎ターン 2 $2 を供給する\n砂浜にしか建設できない" }
	};

	const RoundRect r0{ 0, 0, 360, 100, 6 };
	const RoundRect r1{ 5, 5, 90, 90, 5 };
	constexpr double EmojiSize = 22;

	while (System::Update())
	{
		for (size_t i = 0; i < items.size(); ++i)
		{
			const auto& item = items[i];

			const Transformer2D t{ Mat3x2::Translate(40, (40 + i * 110.0)) };

			r0.drawShadow(Vec2{ 4, 4 }, 8, 1)
				.draw(ColorF{ 0.2, 0.25, 0.3 })
				.drawFrame(1, 1, ColorF{ 0.4, 0.5, 0.6 });

			r1.draw(ColorF{ 0.85, 0.9, 0.95 });

			item.icon.resized(80).drawAt(r1.center());

			font1(item.name).draw(r1.rect.pos.movedBy(102, 0));

			const Vec2 penPos0 = r1.rect.pos.movedBy(102, 42);

			Vec2 penPos = penPos0;

			bool onTag = false;

			for (const auto& glyph : font2.getGlyphs(item.desc))
			{
				if (onTag)
				{
					emojis[(glyph.codePoint - U'0')].resized(EmojiSize).draw(Arg::leftCenter(penPos.x, penPos.y + font2.height() * 0.5));
					penPos.x += EmojiSize;
					onTag = false;
					continue;
				}

				if (glyph.codePoint == U'$')
				{
					onTag = true;
					continue;
				}

				onTag = false;

				if (glyph.codePoint == U'\n')
				{
					penPos.x = penPos0.x;
					penPos.y += font2.height();
				}
				else
				{
					glyph.texture.draw(Math::Round(penPos + glyph.getOffset()));
					penPos.x += glyph.xAdvance;
				}
			}
		}
	}
}

10. タイル型のボタン

コード
# include <Siv3D.hpp>

class TileButton
{
public:

	struct Palette
	{
		ColorF tileColor1;

		ColorF tileColor2;

		ColorF borderColor1;

		ColorF borderColor2;
	};

	TileButton() = default;

	TileButton(const Icon& icon, int32 iconSize, const Font& font, double fontSize, const String& text, const RectF& rect, const Palette& palette)
		: TileButton{ Texture{ icon, iconSize }, iconSize, font, fontSize, text, rect, palette } {}

	// Texture からアイコンを作成
	TileButton(const TextureRegion& textureRegion, int32 iconSize, const Font& font, double fontSize, const String& text, const RectF& rect, const Palette& palette)
		: m_icon{ textureRegion }
		, m_iconSize{ iconSize }
		, m_font{ font }
		, m_fontSize{ fontSize }
		, m_text{ text }
		, m_rect{ rect }
		, m_palette{ palette } {}

	bool update()
	{
		const bool mouseOver = m_rect.mouseOver();

		bool pushed = false;

		if (mouseOver)
		{
			Cursor::RequestStyle(CursorStyle::Hand);
		}

		if (not m_pressed)
		{
			if (m_rect.leftClicked())
			{
				m_pressed = true;
			}
		}
		else
		{
			if (m_rect.leftReleased())
			{
				m_pressed = false;
				pushed = true;
			}
			else if (not m_rect.mouseOver())
			{
				m_pressed = false;
			}
		}

		m_transitionPressed.update(m_pressed);

		return pushed;
	}

	void draw() const
	{
		const double t = m_transitionPressed.value();

		const Transformer2D transform{ Mat3x2::Scale((1 + t * 0.06), m_rect.center()) };

		// タイル
		{
			m_rect.draw(m_palette.tileColor1.lerp(m_palette.tileColor2, t));

			m_rect.stretched(Math::Lerp(-InnerBorderMargin, 0, t))
				.drawFrame(0.1, (1.0 + t * 2.0), m_palette.borderColor1.lerp(m_palette.borderColor2, t));
		}

		// アイコン
		{
			m_icon
				.drawAt(m_rect.getRelativePoint(0.5, 0.4), m_palette.tileColor2.lerp(m_palette.tileColor1, t));
		}

		// ラベル
		{
			m_font(m_text)
				.drawAt(m_fontSize, m_rect.getRelativePoint(0.5, 0.8), m_palette.tileColor2.lerp(m_palette.tileColor1, t));
		}
	}

private:

	static constexpr double InnerBorderMargin = 3.0;

	TextureRegion m_icon;

	int32 m_iconSize = 0;

	Font m_font;

	double m_fontSize = 0;

	String m_text;

	RectF m_rect;

	Transition m_transitionPressed{ 0.09s, 0.12s };

	Palette m_palette;

	bool m_pressed = false;
};

void Main()
{
	Window::Resize(1280, 720);
	Scene::SetBackground(ColorF{ 0.3 });

	const Font font1{ FontMethod::MSDF, 48, U"example/font/RocknRoll/RocknRollOne-Regular.ttf" };
	const Font font2{ FontMethod::MSDF, 48, Typeface::Heavy };
	constexpr int32 IconSize1 = 65;
	constexpr int32 IconSize2 = 40;
	constexpr int32 IconSize3 = 60;
	constexpr double FontSize1 = 22;
	constexpr double FontSize2 = 15.5;
	constexpr double FontSize3 = 24;
	constexpr TileButton::Palette Palette1{
		.tileColor1 = ColorF{ 0.3, 0.2, 0.0 },
		.tileColor2 = ColorF{ 1.0, 0.95, 0.75 },
		.borderColor1 = ColorF{ 1.0, 0.4 },
		.borderColor2 = ColorF{ 1.0, 0.8, 0.4 }
	};

	Array<TileButton> buttons = {
		{ 0xF034D_icon, IconSize1, font1, FontSize1, U"マップ", Rect{40, 40, 130}, Palette1 },
		{ 0xF018B_icon, IconSize1, font1, FontSize1, U"イベント", Rect{180, 40, 130}, Palette1 },
		{ 0xF0E10_icon, IconSize1, font1, FontSize1, U"バッグ", Rect{ 320, 40, 130 }, Palette1 },
		{ 0xF05DA_icon, IconSize1, font1, FontSize1, U"冒険の記録", Rect{ 460, 40, 130 }, Palette1 },
		{ 0xF0538_icon, IconSize1, font1, FontSize1, U"実績", Rect{ 600, 40, 130 }, Palette1 },
		{ 0xF0493_icon, IconSize1, font1, FontSize1, U"設定", Rect{ 740, 40, 130 }, Palette1 },

		{ 0xF034D_icon, IconSize2, font1, FontSize2, U"マップ", Rect{ 40, 200, 90 }, Palette1 },
		{ 0xF018B_icon, IconSize2, font1, FontSize2, U"イベント", Rect{ 140, 200, 90 }, Palette1 },
		{ 0xF0E10_icon, IconSize2, font1, FontSize2, U"バッグ", Rect{ 240, 200, 90 }, Palette1 },
		{ 0xF05DA_icon, IconSize2, font1, FontSize2, U"冒険の記録", Rect{ 340, 200, 90 }, Palette1 },
		{ 0xF0538_icon, IconSize2, font1, FontSize2, U"実績", Rect{ 440, 200, 90 }, Palette1 },
		{ 0xF0493_icon, IconSize2, font1, FontSize2, U"設定", Rect{ 540, 200, 90 }, Palette1 },

		{ 0xF0A70_icon, IconSize3, font2, FontSize3, U"メニュー", Rect{ 40, 360, 150, 120 }, { HSV{ 25, 1, 0.8 }, Palette::White, ColorF{ 1.0, 0.4 }, HSV{ 25, 0.5, 1 } } },
		{ 0xF0AAF_icon, IconSize3, font2, FontSize3, U"具材", Rect{ 200, 360, 150, 120 }, { HSV{ 75, 1, 0.8 }, Palette::White, ColorF{ 1.0, 0.4 }, HSV{ 75, 0.5, 1 } } },
		{ 0xF110E_icon, IconSize3, font2, FontSize3, U"調味料", Rect{ 360, 360, 150, 120 }, { HSV{ 125, 1, 0.8 }, Palette::White, ColorF{ 1.0, 0.4 }, HSV{ 125, 0.5, 1 } } },
		{ 0xF0110_icon, IconSize3, font2, FontSize3, U"仕入れ", Rect{ 520, 360, 150, 120 }, { HSV{ 175, 1, 0.8 }, Palette::White, ColorF{ 1.0, 0.4 }, HSV{ 175, 0.5, 1 } } },
		{ 0xF04DE_icon, IconSize3, font2, FontSize3, U"設備", Rect{ 680, 360, 150, 120 }, { HSV{ 225, 1, 0.8 }, Palette::White, ColorF{ 1.0, 0.4 }, HSV{ 225, 0.5, 1 } } },
		{ 0xF00E6_icon, IconSize3, font2, FontSize3, U"宣伝", Rect{ 840, 360, 150, 120 }, { HSV{ 275, 1, 0.8 }, Palette::White, ColorF{ 1.0, 0.4 }, HSV{ 275, 0.5, 1 } } },
		{ 0xF012A_icon, IconSize3, font2, FontSize3, U"売り上げ", Rect{ 1000, 360, 150, 120 }, { HSV{ 325, 1, 0.8 }, Palette::White, ColorF{ 1.0, 0.4 }, HSV{ 325, 0.5, 1 } } },
	};

	while (System::Update())
	{
		Rect{ 0, 0, Scene::Width(), 320 }.draw(ColorF{ 0.8, 0.7, 0.6 });

		for (auto& button : buttons)
		{
			button.update();
		}

		for (const auto& button : buttons)
		{
			button.draw();
		}
	}
}

11. ゲームに便利なアイコン集

コード
# include <Siv3D.hpp>

void Main()
{
	Scene::SetBackground(ColorF{ 0.8, 0.9, 1.0 });

	const Font iconFont{ FontMethod::MSDF, 40, Typeface::Icon_MaterialDesign };
	const Texture faceEmoji{ U"🐱"_emoji };

	double volume = 1.0;
	int32 up = 0, down = 0;

	while (System::Update())
	{
		SimpleGUI::Button(U"\U000F0009 アカウント", Vec2{ 20, 20 }, 220);
		SimpleGUI::Button(U"\U000F01F0 お知らせ", Vec2{ 20, 60 }, 220);
		SimpleGUI::Button(U"\U000F01DA ダウンロード", Vec2{ 20, 100 }, 220);
		SimpleGUI::Button(U"\U000F01A5 最高記録", Vec2{ 20, 140 }, 220);
		SimpleGUI::Button(U"\U000F0193 保存", Vec2{ 20, 180 }, 220);
		SimpleGUI::Button(U"\U000F1268 Copy to clipboard", Vec2{ 20, 220 }, 220);
		SimpleGUI::Button(U"\U000F0189 メッセージ", Vec2{ 20, 260 }, 220);
		SimpleGUI::Button(U"\U000F0493 設定", Vec2{ 20, 300 }, 220);
		SimpleGUI::Button(U"\U000F1398 中断する", Vec2{ 20, 340 }, 220);
		SimpleGUI::Button(U"\U000F0E1E OK", Vec2{ 20, 380 }, 220);
		SimpleGUI::Button(U"\U000F0639 カードを配る", Vec2{ 20, 420 }, 220);
		SimpleGUI::Button(U"\U000F0240 領土を広げる", Vec2{ 20, 460 }, 220);
		SimpleGUI::Button(U"\U000F02A1 プレゼントする", Vec2{ 20, 500 }, 220);
		SimpleGUI::Button(U"\U000F02DA 履歴", Vec2{ 20, 540 }, 220);

		// 音量調整
		SimpleGUI::Slider((0.5 < volume) ? U"\U000F057E"
			: (0.0 < volume) ? U"\U000F0580" : U"\U000F0581", volume, Vec2{ 260, 20 }, 30, 170);

		// Undo / Redo
		SimpleGUI::Button(U"\U000F054C", Vec2{ 260, 60 }, 40);
		SimpleGUI::Button(U"\U000F044E", Vec2{ 310, 60 }, 40);

		// upvote
		if (SimpleGUI::Button(U"\U000F0513  {}"_fmt(up), Vec2{ 260, 100 }, 90))
		{
			++up;
		}

		// downvote
		if (SimpleGUI::Button(U"\U000F0511  {}"_fmt(down), Vec2{ 370, 100 }, 90))
		{
			++down;
		}

		SimpleGUI::Button(U"公式サイト \U000F0327", Vec2{ 260, 140 }, 200);
		SimpleGUI::Button(U"\U000F0544 公式 Twitter", Vec2{ 260, 180 }, 200);
		SimpleGUI::Button(U"\U000F018C 任務一覧", Vec2{ 260, 220 }, 200);
		SimpleGUI::Button(U"\U000F0982 マップ", Vec2{ 260, 260 }, 200);
		SimpleGUI::Button(U"\U000F034E 現在地", Vec2{ 260, 300 }, 200);
		SimpleGUI::Button(U"\U000F0A7A 削除", Vec2{ 260, 340 }, 200);
		SimpleGUI::Button(U"\U000F05B7 修繕", Vec2{ 260, 380 }, 200);
		SimpleGUI::Button(U"\U000F0349 検索", Vec2{ 260, 420 }, 200);
		SimpleGUI::Button(U"\U000F0432 QR 作成", Vec2{ 260, 460 }, 200);
		SimpleGUI::Button(U"\U000F0433 QR 読み込み", Vec2{ 260, 500 }, 200);
		SimpleGUI::Button(U"\U000F04E6 同期", Vec2{ 260, 540 }, 200);

		// ハート
		iconFont(U"\U000F02D1\U000F02D1\U000F02D1\U000F06DF").draw(500, 20, ColorF{ 0.8, 0.2, 0.2 });

		// サイコロ
		iconFont(U"\U000F037D\U000F030C\U000F0297").draw(500, 80, ColorF{ 0.25 });

		// 操作方法
		iconFont(U"\U000F114A\U000F114B\U000F114C\U000F114D\U000F114E\U000F114F").draw(500, 140, ColorF{ 0.25 });

		// 動画の再生
		Rect{ 500, 200, 240, 160 }.draw(ColorF{ 0.6 });
		iconFont(U"\U000F040C").drawAt(80, 620, 280, ColorF{ 1.0 });

		// セリフアイコン
		faceEmoji.scaled(0.75).drawAt(560, 440);
		iconFont(U"\U000F1170").drawAt(50, 630, 400, ColorF{ 0.1 });

		// 拡大縮小
		Circle{ 540, 530, 30 }.draw();
		iconFont(U"\U000F06EC").drawAt(50, 540, 530, ColorF{ 0.1 });
		Circle{ 620, 530, 30 }.draw();
		iconFont(U"\U000F06ED").drawAt(50, 620, 530, ColorF{ 0.1 });
	}
}

12. タブ

Siv3D-Sample | タブ

13. HP バー

Siv3D-Sample | HP バー

14. パイメニュー

コード
# include <Siv3D.hpp>

/// @brief パイメニュー用のアイコン
class PieMenuIcon
{
public:

	PieMenuIcon() = default;

	/// @brief パイメニュー用のアイコンを作成します。
	/// @param texture アイコンのテクスチャ
	/// @param color アイコンの描画色
	PieMenuIcon(const Texture& texture, const ColorF& color)
		: m_texture{ texture }
		, m_blurTexture{ m_texture.size(), ColorF{ 0.0, 1.0 } }
		, m_color{ color }
	{
		RenderTexture m_internalTexture{ m_texture.size() };

		// アイコンをやや小さめに描画する
		{
			const ScopedRenderTarget2D target{ m_blurTexture };
			m_texture.scaled(0.8).drawAt(m_blurTexture.size() * 0.5);
		}

		// ガウスぼかしを 2 回かける
		Shader::GaussianBlur(m_blurTexture, m_internalTexture, m_blurTexture);
		Shader::GaussianBlur(m_blurTexture, m_internalTexture, m_blurTexture);
	}

	void draw() const
	{
		// 背景のぼかした影を減算ブレンディングで描画する
		{
			const ScopedRenderStates2D blend{ BlendState::Subtractive };
			m_blurTexture.scaled(1.35).drawAt(Vec2{ 0, 0 }, ColorF{ 0.25 });
		}
		m_texture.drawAt(Vec2{ 0, 0 }, m_color);
	}

private:

	Texture m_texture;

	RenderTexture m_blurTexture;

	ColorF m_color;
};

/// @brief パイメニュークラス
class PieMenu
{
public:

	/// @brief パイメニューのスタイル
	struct Style
	{
		/// @brief パイメニューの外側の半径
		double outerRadius = 180.0;

		/// @brief パイメニューの内側の半径
		double innerRadius = 90.0;

		/// @brief アクティブなアイテムが外側に移動する距離
		double pieOuterOffset = 10.0;

		/// @brief アクティブなアイテムの枠の太さ
		double outlineThickness = 8.0;

		/// @brief パイメニューの色
		ColorF pieColor{ 0.0, 0.75 };

		/// @brief パイメニューの内側の枠の色
		ColorF pieInnerFrameColor{ 0.6 };

		/// @brief 無効なアイテムの色
		ColorF disabledPieColor{ 0.36, 0.4 };

		/// @brief アクティブなアイテムの色
		ColorF activePieColor{ 0.36, 0.84, 1.0 };

		/// @brief パイメニューの外側の枠の色
		ColorF activePieOutlineColor{ 1.0, 0.9, 0.2 };

		/// @brief アイテムを指す矢印の色
		ColorF arrowColor{ 1.0, 0.9, 0.2 };

		[[nodiscard]]
		static constexpr Style Default() noexcept
		{
			return{};
		}
	};

	SIV3D_NODISCARD_CXX20
	PieMenu() = default;

	/// @brief パイメニューを作成します。
	/// @param icons パイメニューのアイコン
	/// @param center パイメニューの中心座標
	/// @param style パイメニューのスタイル
	SIV3D_NODISCARD_CXX20
	PieMenu(const Array<PieMenuIcon>& icons, const Vec2& center, const Style& style = Style::Default())
		: m_itemCount{ static_cast<int32>(icons.size()) }
		, m_pieAngle{ Math::TwoPi / m_itemCount }
		, m_style{ style }
		, m_center{ center }
		, m_transitions{ icons.size(), Transition{ 0.1s, 0.2s } }
		, m_icons{ icons }
		, m_enabled(m_itemCount, true)
	{
		const Circle circle{ m_style.outerRadius - m_style.pieOuterOffset };
		const double activePieOffset = 0;
		const double outlineThicknessHalf = (m_style.outlineThickness * 0.5);

		{
			m_defaultPolygon = Polygon::CorrectOne(circle.arcAsPolygon((-m_pieAngle / 2), m_pieAngle, (m_style.outerRadius - m_style.innerRadius), 0.0).outer());
		}

		{
			m_innerOutline = m_defaultPolygon.calculateBuffer(-3).outer();
		}

		{
			const Array<Vec2> outline = circle.stretched(activePieOffset + outlineThicknessHalf)
				.arcAsPolygon((-m_pieAngle / 2), m_pieAngle, (m_style.outerRadius + activePieOffset - m_style.innerRadius + m_style.outlineThickness), 0.0).outer();

			// m_outlinePolygon の生成に失敗する確率を下げるためのおまじない
			{
				m_outlinePolygon = LineString{ outline.rotated(2) }.densified(4.0).calculateRoundBufferClosed(outlineThicknessHalf);

				if ((not m_outlinePolygon) || (not m_outlinePolygon.hasHoles()))
				{
					m_outlinePolygon = LineString{ outline.rotated(1) }.calculateRoundBufferClosed(outlineThicknessHalf);
				}
			}
		}
	}

	/// @brief パイメニューのアイテム数を返します。
	/// @return パイメニューのアイテム数
	[[nodiscard]]
	size_t size() const noexcept
	{
		return m_itemCount;
	}

	/// @brief パイメニューの指定したアイテムの有効・無効を設定します。
	/// @param index アイテムのインデックス
	/// @param enabled 有効にする場合 true, 無効にする場合は false
	/// @return *this
	PieMenu& setEnabled(size_t index, bool enabled) noexcept
	{
		m_enabled[index] = enabled;
		return *this;
	}

	/// @brief パイメニューの指定したアイテムが有効かを返します。
	/// @param index アイテムのインデックス
	/// @return アイテムが有効な場合 true, 無効な場合は false
	[[nodiscard]]
	bool getEnabled(size_t index) const noexcept
	{
		return m_enabled[index];
	}

	/// @brief パイメニューのアニメーションをリセットします。
	void reset() noexcept
	{
		m_selectedPie.reset();
		m_transitions.fill(Transition{ 0.1s, 0.2s });
	}

	/// @brief パイメニューを更新します。
	/// @return 選択されているアイテムのインデックス。選択されていない場合は none
	Optional<int32> update()
	{
		m_selectedPie.reset();

		const Vec2 cursorVector = (Cursor::PosF() - m_center).limitLengthSelf(Math::Lerp(m_style.innerRadius, m_style.outerRadius, 0.55));

		if (m_style.innerRadius <= cursorVector.length())
		{
			m_selectedPie = (static_cast<int32>((cursorVector.getAngle() + Math::TwoPi + (m_pieAngle / 2)) / m_pieAngle) % m_itemCount);
		}

		if (m_selectedPie && (not m_enabled[*m_selectedPie]))
		{
			m_selectedPie.reset();
		}

		for (int32 i = 0; i < m_itemCount; ++i)
		{
			const bool hovered = (i == m_selectedPie);
			m_transitions[i].update(hovered);
		}

		return m_selectedPie;
	}

	/// @brief パイメニューを描画します。
	void draw() const
	{
		for (int32 i = 0; i < m_itemCount; ++i)
		{
			const double centerAngle = (i * m_pieAngle);
			const bool hovered = (i == m_selectedPie);
			const Vec2 offset = Circular{ (m_style.pieOuterOffset + m_style.outlineThickness * m_transitions[i].value()), centerAngle };
			const Vec2 scalingCenter = Circular{ ((m_style.innerRadius + m_style.outerRadius) / 2), centerAngle };

			if (m_enabled[i])
			{
				{
					const Transformer2D transformer{ Mat3x2::Rotate(centerAngle).translated(m_center + offset) };
					m_defaultPolygon.draw(hovered ? m_style.activePieColor : m_style.pieColor);

					if (hovered)
					{
						m_outlinePolygon.draw(m_style.activePieOutlineColor);
					}
					else
					{
						m_innerOutline.drawClosed(m_style.pieInnerFrameColor);
					}
				}

				{
					const Vec2 iconOffset = Circular{ Math::Lerp(m_style.innerRadius, m_style.outerRadius, 0.53) + (m_style.outlineThickness * m_transitions[i].value()), centerAngle };
					const Transformer2D transformer{ Mat3x2::Translate(m_center + iconOffset) };
					m_icons[i].draw();
				}
			}
			else
			{
				const Transformer2D transformer{ Mat3x2::Rotate(centerAngle).translated(m_center + offset) };
				m_defaultPolygon.draw(m_style.disabledPieColor);

				if (hovered)
				{
					m_outlinePolygon.draw(m_style.activePieOutlineColor);
				}
			}
		}

		{
			const Vec2 cursorVector = (Cursor::PosF() - m_center).limitLengthSelf(Math::Lerp(m_style.innerRadius, m_style.outerRadius, 0.55));
			const double lineLength = Max(0.0, cursorVector.length() - (m_selectedPie ? 14 : 18));
			Line{ m_center, (m_center + cursorVector.withLength(lineLength)) }.draw(4, m_style.arrowColor);
			const Vec2 triangleCenter = (m_center + cursorVector);

			if (m_selectedPie)
			{
				Triangle{ triangleCenter, 12, cursorVector.getAngle() }.draw(m_style.arrowColor);
				Triangle{ triangleCenter, 26, cursorVector.getAngle() }.drawFrame(4, m_style.arrowColor);
			}
			else
			{
				Circle{ triangleCenter, 7 }.draw(m_style.arrowColor);
				Circle{ triangleCenter, 12 }.drawFrame(4, m_style.arrowColor);
			}
		}
	}

private:

	int32 m_itemCount = 0;

	double m_pieAngle = 0.0;

	Style m_style;

	Vec2 m_center{ 0, 0 };

	Polygon m_defaultPolygon;

	LineString m_innerOutline;

	Polygon m_outlinePolygon;

	Array<Transition> m_transitions;

	Array<PieMenuIcon> m_icons;

	Array<bool> m_enabled;

	Optional<int32> m_selectedPie;
};

void Main()
{
	Scene::SetBackground(ColorF{ 0.8 });

	const Array<PieMenuIcon> icons =
	{
		PieMenuIcon{ Texture{ 0xF064C_icon, 60 }, ColorF{ 0.86, 0.98, 0.80 }},
		PieMenuIcon{ Texture{ 0xF0100_icon, 60 }, ColorF{ 0.60, 0.98, 0.60 }},
		PieMenuIcon{ Texture{ 0xF0E46_icon, 60 }, ColorF{ 1.00, 0.50, 0.31 }},
		PieMenuIcon{ Texture{ 0xF194B_icon, 60 }, ColorF{ 0.73, 0.33, 0.83 }},
		PieMenuIcon{ Texture{ 0xF18BF_icon, 60 }, ColorF{ 0.50, 1.00, 0.83 }},
		PieMenuIcon{ Texture{ 0xF0BEB_icon, 60 }, ColorF{ 1.0 }},
		PieMenuIcon{ Texture{ 0xF11DE_icon, 60 }, ColorF{ 1.00, 0.65, 0.00 }},
		PieMenuIcon{ Texture{ 0xF018C_icon, 60 }, ColorF{ 0.68, 1.00, 0.18 }},
	};

	std::unique_ptr<PieMenu> pieMenu;

	constexpr int32 CellSize = 20;

	while (System::Update())
	{
		// 右クリックされたらパイメニューを登場させる
		if (MouseR.down())
		{
			pieMenu = std::make_unique<PieMenu>(icons, Cursor::PosF());

			// 1 番目と 3 番目のアイテムを無効化する
			pieMenu->setEnabled(1, false).setEnabled(3, false);
		}

		// 背景の市松模様を描画する
		for (int32 y = 0; y < (Scene::Height() / CellSize); ++y)
		{
			for (int32 x = 0; x < (Scene::Width() / CellSize); ++x)
			{
				if (IsEven(y + x))
				{
					Rect{ (x * CellSize), (y * CellSize), CellSize }.draw(ColorF{ 0.75 });
				}
			}
		}

		// パイメニューがあれば
		if (pieMenu)
		{
			const Optional<int32> selected = pieMenu->update();

			pieMenu->draw();

			// 右クリックが離されたら、選択されたアイテムを表示する
			if (MouseR.up())
			{
				ClearPrint();
				Print << selected;
				pieMenu.reset();
			}
		}
	}
}

15. 数値パッド

Siv3D-Sample | 数値パッド