Skip to content

Instantly share code, notes, and snippets.

@sthairno
Created January 14, 2025 11:11
Show Gist options
  • Save sthairno/81bf2ccacd6700372867c1c2c4274aa3 to your computer and use it in GitHub Desktop.
Save sthairno/81bf2ccacd6700372867c1c2c4274aa3 to your computer and use it in GitHub Desktop.
# include <Siv3D.hpp> // OpenSiv3D v0.6.15
# include <Eigen/Dense> // Eigenライブラリのインクルード
// 与えられたデータ点をもとに多項式近似を行う関数
Array<double> PolynomialApproximation(const Array<Vec2>& points, int degree)
{
const int n = points.size();
const int m = degree + 1;
// 行列を構築
Eigen::MatrixXd A(n, m);
Eigen::VectorXd b(n);
for (int i = 0; i < n; ++i)
{
double x = points[i].x;
double y = points[i].y;
for (int j = 0; j < m; ++j)
{
A(i, j) = std::pow(x, j);
}
b(i) = y;
}
// 正規方程式を解く
Eigen::VectorXd coefficients = A.householderQr().solve(b);
// 結果をArrayに変換
Array<double> result;
for (int i = 0; i < m; ++i)
{
result << coefficients[i];
}
return result;
}
// N次多項式の値を計算する関数
// f(x) = clamp(c0 + c1 * x + c2 * x^2 + c3 * x^3 + ... + cN * x^N, 0.0, 1.0)
double EvaluatePolynomial(const Array<double>& coefficients, double x)
{
double result = 0.0;
for (size_t i = 0; i < coefficients.size(); ++i)
{
result += coefficients[i] * std::pow(x, i);
}
return Clamp(result, 0.0, 1.0);
}
void Main()
{
// 背景を白に設定
Scene::SetBackground(Palette::White);
// 初期状態のデータ点
Array<Vec2> points{ {0, 0}, {1, 1} };
// 係数
Array<double> coefficients;
bool dirty = true;
// ドラッグ中のデータ点の番号
Optional<size_t> draggingPointIdx;
// 描画済みグラフ
LineString graph01;
double graphMinDistance = 1.0 / 100;
// コントロールポイント
constexpr double ControlPointRadius = 5.0;
Font font{ 16 };
// メインループ
while (System::Update())
{
dirty |= SimpleGUI::Slider(U"グラフ精度", graphMinDistance, 1, 0.01, { 10, 10 }, 120, 200);
String displayText;
{
displayText += U"近似式: f(x) = ";
bool hasPrevTerm = false;
for (size_t i = 0; i < coefficients.size(); ++i)
{
if (coefficients[i] == 0.0)
{
continue;
}
if (hasPrevTerm)
{
displayText += U" + ";
}
switch (i)
{
case 0:
displayText += U"{:.2}"_fmt(coefficients[i]);
break;
case 1:
displayText += U"{:.2}x"_fmt(coefficients[i]);
break;
default:
displayText += U"{:.2}x^{}"_fmt(coefficients[i], i);
break;
}
hasPrevTerm = true;
}
if (not hasPrevTerm)
{
displayText += U"0";
}
}
displayText += U"\nコントロールポイント数: {}\n"_fmt(points.size());
displayText += U"グラフ頂点数: {}\n"_fmt(graph01.size());
font(displayText).draw(16, 46, Palette::Black);
if (dirty)
{
// 係数を再計算
coefficients = PolynomialApproximation(points, points.size());
// グラフを再描画
Vec2 zeroVertex{ 0, EvaluatePolynomial(coefficients, 0) }; // f(0)
Vec2 oneVertex{ 1, EvaluatePolynomial(coefficients, 1) }; // f(1)
Array<Vec2> sortedPoints = points.sorted_by([](const Vec2& a, const Vec2& b) { return a.x < b.x; });
graph01.clear();
for (auto i : Range(0, sortedPoints.size()))
{
auto currentVertex = i != 0 ? sortedPoints[i - 1] : zeroVertex;
auto nextVertex = i < sortedPoints.size() ? sortedPoints[i] : oneVertex;
auto delta = nextVertex - currentVertex;
if (delta.x == 0)
{
continue;
}
graph01 << currentVertex;
// 次の頂点との間隔からおおよその分割数を決定
auto distance = delta.length();
int splits = static_cast<int>(Floor(distance / graphMinDistance));
for (auto j : Range(1, splits))
{
double x = currentVertex.x + (static_cast<double>(j) / (splits + 1)) * delta.x;
double y = EvaluatePolynomial(coefficients, x);
graph01 << Vec2{ x, y };
}
}
// 最後の頂点を追加
if (graph01.back().x < 1.0)
{
graph01 << oneVertex;
}
dirty = false;
}
Rect canvasRect{ Arg::center = Scene::Center(), 400, 400 };
// 更新処理
{
Mat3x2 graphToCanvas = Mat3x2{
static_cast<float>(canvasRect.w), 0,
0, static_cast<float>(-canvasRect.h),
static_cast<float>(canvasRect.x), static_cast<float>(canvasRect.bottomY()),
};
// ボタンを離したらドラッグ終了
if (not MouseL.pressed())
{
draggingPointIdx.reset();
}
if (not draggingPointIdx.has_value())
{
Optional<size_t> hoveredPointIdx;
for (size_t i : Range(points.size() - 1, 0, -1))
{
if (Circle{ graphToCanvas.transformPoint(points[i]), ControlPointRadius }.mouseOver())
{
hoveredPointIdx = i;
break;
}
}
// 左クリックされたらドラッグ開始
if (MouseL.down())
{
if (hoveredPointIdx.has_value())
{
// マウスが重なっているデータ点をドラッグ
draggingPointIdx = *hoveredPointIdx;
}
else if (canvasRect.mouseOver())
{
// 新しいデータ点を追加してドラッグ
Vec2 cursorPos = Cursor::PosF();
draggingPointIdx = points.size();
points << Vec2{
(cursorPos.x - canvasRect.x) / canvasRect.w,
1.0 - (cursorPos.y - canvasRect.y) / canvasRect.h
};
dirty = true;
}
}
// 右クリックされたらデータ点を削除 (初期設定の2点を除く)
if (MouseR.down() && hoveredPointIdx >= 2)
{
points.remove_at(*hoveredPointIdx);
dirty = true;
}
}
// ドラッグ処理
if (draggingPointIdx.has_value())
{
Vec2& point = points[*draggingPointIdx];
Vec2 delta = Cursor::DeltaF() / Vec2{ canvasRect.w, -canvasRect.h };
if (not delta.isZero())
{
point = Vec2{
Clamp(point.x + delta.x, 0.0, 1.0),
Clamp(point.y + delta.y, 0.0, 1.0)
};
dirty = true;
}
}
}
// キャンバスを描画
canvasRect.drawFrame(2, Palette::Black);
{
Transformer2D t{ Mat3x2::Translate(canvasRect.pos) };
Mat3x2 graphToCanvas = Mat3x2{
static_cast<float>(canvasRect.w), 0,
0, static_cast<float>(-canvasRect.h),
0, static_cast<float>(canvasRect.h),
};
// 基準線(?)
Line{ 0, canvasRect.h, canvasRect.w, 0 }
.draw(Palette::Gray);
// グラフ
LineString graph{ graph01.asArray().map([&](const Vec2& p) { return graphToCanvas.transformPoint(p); }) };
graph.draw(2, Palette::Red);
// コントロールポイント
for (size_t i : Iota(0, points.size()))
{
bool isRemovable = i >= 2;
Circle{ graphToCanvas.transformPoint(points[i]), ControlPointRadius }
.draw(draggingPointIdx == i ? Palette::Gray : Palette::White)
.drawFrame(1, isRemovable ? Palette::Gray : Palette::Red);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment