Skip to content
This repository was archived by the owner on May 12, 2026. It is now read-only.

Commit 455c1e9

Browse files
Change calculations to use doubles to massively simplify and get smoother edges, remove Manhattan distance for now
1 parent ffe8f6f commit 455c1e9

6 files changed

Lines changed: 161 additions & 195 deletions

File tree

Home/Pages/Games/Voronoi/Index.razor

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@
88
<svg @onclick="OnCanvasClick" class="voronoi-svg">
99
@foreach(VoronoiRegion region in Regions)
1010
{
11-
string points = string.Join(" ", region.PolygonPoints.Select(p => $"{p.x},{p.y}"));
11+
string points = string.Join(" ", region.PolygonPoints.Select(p => $"{p.X},{p.Y}"));
1212
@if (!string.IsNullOrEmpty(points))
1313
{
14-
<polygon points="@points" fill="@region.Point.Color" stroke="black" stroke-width="2" />
14+
<polygon points="@points" fill="@region.VoronoiPoint.Color" stroke="black" stroke-width="2" />
1515
}
1616
}
1717

18-
@foreach(VoronoiPoint point in State.GeneratingPoints)
18+
@foreach(Point point in State.VoronoiPoints)
1919
{
2020
<circle cx="@point.X" cy="@point.Y" r="7" fill="black" />
2121
<circle cx="@point.X" cy="@point.Y" r="5" fill="white" />
@@ -31,10 +31,10 @@
3131
</p>
3232

3333
<div>
34-
<label>
34+
<!--label>
3535
<input type="checkbox" @onchange="OnManhattanToggle" checked="@State.ManhattanDistance" />
3636
Use Manhattan Distance
37-
</label>
37+
</label-->
3838
<button @onclick="OnClear">Clear Points</button>
3939
</div>
4040
</Box>

Home/Pages/Games/Voronoi/Index.razor.cs

Lines changed: 9 additions & 179 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@ public partial class Index(IJSRuntime JS, ILogger<Index> logger) : ComponentBase
99
{
1010
private VoronoiGameState State { get; set; } = new();
1111
private List<VoronoiRegion> Regions { get; set; } = [];
12-
private int CanvasWidth = 600;
13-
private int CanvasHeight = 600; // Keep square
14-
private const int GridResolution = 4; // Sample every 4 pixels for boundary detection
12+
private int CanvasSize = 600;
1513
private bool isUpdating = false;
1614

1715
private class DOMRectData
@@ -28,9 +26,8 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
2826
{
2927
// Measure the actual SVG size and use it for calculations
3028
DOMRectData rect = await JS.InvokeAsync<DOMRectData>("eval", "document.querySelector('svg').getBoundingClientRect()");
31-
CanvasWidth = (int)rect.Width;
32-
CanvasHeight = (int)rect.Height;
33-
logger.LogInformation("SVG dimensions: {Width}x{Height}", CanvasWidth, CanvasHeight);
29+
CanvasSize = (int)rect.Width;
30+
logger.LogInformation("SVG dimensions: {Width}x{Height}", CanvasSize, CanvasSize);
3431
StateHasChanged();
3532
}
3633
}
@@ -39,11 +36,11 @@ private async Task OnCanvasClick(MouseEventArgs e)
3936
{
4037
// Get click position relative to SVG
4138
DOMRectData rect = await JS.InvokeAsync<DOMRectData>("eval", "document.querySelector('svg').getBoundingClientRect()");
42-
int x = (int)((e.ClientX - rect.Left) * CanvasWidth / rect.Width);
43-
int y = (int)((e.ClientY - rect.Top) * CanvasHeight / rect.Height);
39+
int x = (int)((e.ClientX - rect.Left) * CanvasSize / rect.Width);
40+
int y = (int)((e.ClientY - rect.Top) * CanvasSize / rect.Height);
4441

45-
x = Math.Max(0, Math.Min(CanvasWidth - 1, x));
46-
y = Math.Max(0, Math.Min(CanvasHeight - 1, y));
42+
x = Math.Max(0, Math.Min(CanvasSize - 1, x));
43+
y = Math.Max(0, Math.Min(CanvasSize - 1, y));
4744

4845
logger.LogInformation("SVG clicked at ({X}, {Y})", x, y);
4946
State.AddPoint(x, y);
@@ -74,14 +71,14 @@ private async Task UpdateVoronoiDiagram()
7471
isUpdating = true;
7572
try
7673
{
77-
if (State.GeneratingPoints.Count == 0)
74+
if (State.VoronoiPoints.Count == 0)
7875
{
7976
Regions.Clear();
8077
return;
8178
}
8279

8380
// Compute regions on background thread
84-
Regions = await Task.Run(() => ComputeVoronoiRegions());
81+
Regions = await Task.Run(() => State.BuildVoronoiDiagram(CanvasSize));
8582
logger.LogInformation("Voronoi diagram computed with {RegionCount} regions.", Regions.Count);
8683
}
8784
finally
@@ -90,171 +87,4 @@ private async Task UpdateVoronoiDiagram()
9087
StateHasChanged();
9188
}
9289
}
93-
94-
private List<VoronoiRegion> ComputeVoronoiRegions()
95-
{
96-
// Create a grid mapping each cell to its closest point
97-
int gridWidth = (CanvasWidth / GridResolution) + 1;
98-
int gridHeight = (CanvasHeight / GridResolution) + 1;
99-
VoronoiPoint[,] grid = new VoronoiPoint[gridWidth, gridHeight];
100-
101-
// Sample the grid
102-
for (int gy = 0; gy < gridHeight; gy++)
103-
{
104-
for (int gx = 0; gx < gridWidth; gx++)
105-
{
106-
int sampleX = gx * GridResolution;
107-
int sampleY = gy * GridResolution;
108-
109-
VoronoiPoint closestPoint = State.GeneratingPoints[0];
110-
int closestDist = closestPoint.Distance(sampleX, sampleY, State.ManhattanDistance);
111-
112-
for (int i = 1; i < State.GeneratingPoints.Count; i++)
113-
{
114-
int dist = State.GeneratingPoints[i].Distance(sampleX, sampleY, State.ManhattanDistance);
115-
if (dist < closestDist)
116-
{
117-
closestDist = dist;
118-
closestPoint = State.GeneratingPoints[i];
119-
}
120-
}
121-
122-
grid[gx, gy] = closestPoint;
123-
}
124-
}
125-
126-
// Find boundaries and create regions
127-
Dictionary<VoronoiPoint, List<(int x, int y)>> regions = [];
128-
HashSet<(int, int)> visited = [];
129-
130-
for (int gy = 0; gy < gridHeight; gy++)
131-
{
132-
for (int gx = 0; gx < gridWidth; gx++)
133-
{
134-
VoronoiPoint point = grid[gx, gy];
135-
136-
if (!regions.ContainsKey(point))
137-
{
138-
regions[point] = [];
139-
}
140-
141-
// Add the pixel coordinates that are on the boundary of this point
142-
int pixelX = gx * GridResolution;
143-
int pixelY = gy * GridResolution;
144-
145-
// Check if this is a boundary cell (different from a neighbor)
146-
bool isBoundary = false;
147-
if (gx == 0 || gx == gridWidth - 1 || gy == 0 || gy == gridHeight - 1)
148-
{
149-
isBoundary = true;
150-
}
151-
else if (gx > 0 && grid[gx - 1, gy] != point)
152-
{
153-
isBoundary = true;
154-
}
155-
else if (gx < gridWidth - 1 && grid[gx + 1, gy] != point)
156-
{
157-
isBoundary = true;
158-
}
159-
else if (gy > 0 && grid[gx, gy - 1] != point)
160-
{
161-
isBoundary = true;
162-
}
163-
else if (gy < gridHeight - 1 && grid[gx, gy + 1] != point)
164-
{
165-
isBoundary = true;
166-
}
167-
168-
if (isBoundary && !visited.Contains((gx, gy)))
169-
{
170-
regions[point].Add((pixelX, pixelY));
171-
visited.Add((gx, gy));
172-
}
173-
}
174-
}
175-
176-
// Convert to VoronoiRegion objects and compute final polygon boundaries
177-
List<VoronoiRegion> result = [];
178-
foreach ((VoronoiPoint? point, List<(int x, int y)>? pixels) in regions)
179-
{
180-
VoronoiRegion region = new()
181-
{
182-
Point = point
183-
};
184-
185-
if (pixels.Count > 0)
186-
{
187-
// Sort them radially around the point's center
188-
List<(int x, int y)> sortedPixels = pixels
189-
.OrderBy(p => Math.Atan2(p.y - point.Y, p.x - point.X))
190-
.ToList();
191-
192-
// Simplify the polygon to smooth jagged edges
193-
List<(int x, int y)> simplifiedPixels = SimplifyPolygon(sortedPixels, tolerance: 10);
194-
region.PolygonPoints = simplifiedPixels;
195-
}
196-
197-
if (region.PolygonPoints.Count > 2)
198-
{
199-
result.Add(region);
200-
}
201-
}
202-
203-
return result;
204-
}
205-
206-
private List<(int x, int y)> SimplifyPolygon(List<(int x, int y)> points, double tolerance)
207-
{
208-
if (points.Count <= 2)
209-
{
210-
return points;
211-
}
212-
213-
// Douglas-Peucker algorithm for polygon simplification
214-
double dmax = 0.0;
215-
int index = 0;
216-
217-
for (int i = 1; i < points.Count - 1; i++)
218-
{
219-
double d = PointToLineDistance(points[i], points[0], points[^1]);
220-
if (d > dmax)
221-
{
222-
index = i;
223-
dmax = d;
224-
}
225-
}
226-
227-
if (dmax > tolerance)
228-
{
229-
List<(int x, int y)> rec1 = SimplifyPolygon(points.GetRange(0, index + 1), tolerance);
230-
List<(int x, int y)> rec2 = SimplifyPolygon(points.GetRange(index, points.Count - index), tolerance);
231-
232-
List<(int x, int y)> result = rec1.GetRange(0, rec1.Count - 1);
233-
result.AddRange(rec2);
234-
return result;
235-
}
236-
else
237-
{
238-
return [points[0], points[^1]];
239-
}
240-
}
241-
242-
private double PointToLineDistance((int x, int y) point, (int x, int y) lineStart, (int x, int y) lineEnd)
243-
{
244-
int dx = lineEnd.x - lineStart.x;
245-
int dy = lineEnd.y - lineStart.y;
246-
247-
if (dx == 0 && dy == 0)
248-
{
249-
return Math.Sqrt(Math.Pow(point.x - lineStart.x, 2) + Math.Pow(point.y - lineStart.y, 2));
250-
}
251-
252-
int t = ((point.x - lineStart.x) * dx + (point.y - lineStart.y) * dy) / (dx * dx + dy * dy);
253-
t = Math.Max(0, Math.Min(1, t));
254-
255-
int closestX = lineStart.x + t * dx;
256-
int closestY = lineStart.y + t * dy;
257-
258-
return Math.Sqrt(Math.Pow(point.x - closestX, 2) + Math.Pow(point.y - closestY, 2));
259-
}
26090
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
internal readonly struct DPoint(double x, double y)
2+
{
3+
public readonly double X = x;
4+
public readonly double Y = y;
5+
6+
public static DPoint operator +(DPoint a, DPoint b)
7+
=> new(a.X + b.X, a.Y + b.Y);
8+
9+
public static DPoint operator -(DPoint a, DPoint b)
10+
=> new(a.X - b.X, a.Y - b.Y);
11+
12+
public static DPoint operator *(DPoint a, double s)
13+
=> new(a.X * s, a.Y * s);
14+
}

Home/Pages/Games/Voronoi/Models/VoronoiPoint.cs renamed to Home/Pages/Games/Voronoi/Models/Point.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
namespace Home.Pages.Games.Voronoi.Models;
22

3-
public class VoronoiPoint
3+
public class Point
44
{
5-
public int X { get; set; }
6-
public int Y { get; set; }
7-
public string Color { get; set; } = string.Empty;
5+
public required int X { get; set; }
6+
public required int Y { get; set; }
7+
public string? Color { get; set; }
88

99
public int Distance(int x, int y, bool manhattanDistance)
1010
{

0 commit comments

Comments
 (0)