nandi/bresenham-circlepublic Fork 0
3d0b0a38a545e8531a7d6cb7182993e543ca51ed
Commits
Clone
git clone https://git.rickub.com/nandi/bresenham-circle.git
git clone ssh://git@rickub.com/nandi/bresenham-circle.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

bresenham.nim · 342 lines · 11.8 KBNim Blame HistoryRaw
Bresenham midpoint circle visualizer, with a zero-symmetry counterpart 3d0b0a3 nandi 21h ago1## Two ways to put a circle on a grid, side by side.
2##
3## left -- Bresenham: integer decisions, one octant, mirrored 8 ways.
4## right -- Poisson sprinkle: uniform directions snapped to cells.
5##
6## Both land on the same lattice. The difference is the symmetry of the
7## *selection*: Bresenham's is exactly 8-fold, the sprinkle's is trivial.
8##
9## Controls: SPACE step A auto R reset UP/DOWN radius [ ] oversample ESC
10
11import raylib, std/[strformat, os, math, strutils, random, sets, hashes]
12
13const
14 InitW = 1400
15 InitH = 760
16 PanelWant = 380
17 MinCell = 2
18 MinRadius = 2
19
20 Bg = Color(r: 18, g: 18, b: 24, a: 255)
21 GridLine = Color(r: 36, g: 36, b: 46, a: 255)
22 Axis = Color(r: 70, g: 70, b: 88, a: 255)
23 TrueArc = Color(r: 100, g: 100, b: 128, a: 255)
24 Octant = Color(r: 88, g: 166, b: 255, a: 255)
25 Mirror = Color(r: 88, g: 166, b: 255, a: 105)
26 Sprink = Color(r: 118, g: 222, b: 160, a: 210)
27 Cursor = Color(r: 255, g: 176, b: 64, a: 255)
28 Candidate = Color(r: 255, g: 176, b: 64, a: 60)
29 Mid = Color(r: 255, g: 92, b: 92, a: 255)
30 Ink = Color(r: 226, g: 226, b: 236, a: 255)
31 Dim = Color(r: 130, g: 130, b: 148, a: 255)
32 HiLine = Color(r: 40, g: 62, b: 92, a: 255)
33
34type
35 Branch = enum brNone, brEast, brSouthEast
36
37 Cells = HashSet[(int, int)]
38
39 State = object
40 r: int
41 # -- Bresenham
42 x, y, d: int ## the entire algorithm state: three integers
43 plotted: seq[(int, int)] ## octant only; the rest is reflection
44 bcells: Cells ## all 8 octants, for counting
45 branch: Branch
46 lastUpdate: string
47 done: bool
48 # -- Poisson sprinkle
49 over: int ## samples per Bresenham pixel
50 samples: seq[(int, int)] ## pre-rolled, revealed progressively
51 revealed: int
52 scells: Cells
53 perStep: int
54
55# ---------------------------------------------------------------- algorithms --
56
57proc mirrors(x, y: int): array[8, (int, int)] =
58 ## The complete symmetry group of the square lattice: D4, order 8.
59 [(x, y), (y, x), (y, -x), (x, -y), (-x, -y), (-y, -x), (-y, x), (-x, y)]
60
61proc reset(s: var State, r: int, over = -1) =
62 if over >= 0: s.over = over
63 if s.over == 0: s.over = 4
64 s.r = r
65 s.x = 0
66 s.y = r
67 s.d = 1 - r ## F(1, r - 1/2), the 1/4 dropped so d stays integral
68 s.plotted = @[]
69 s.bcells = initHashSet[(int, int)]()
70 s.branch = brNone
71 s.lastUpdate = ""
72 s.done = false
73
74 # Sprinkle: uniform in angle, so isotropic in distribution but with no
75 # exact symmetry at all. Fixed seed per radius so runs are reproducible.
76 var rng = initRand(0xC0FFEE + r * 7919)
77 let n = int(s.over.float * 4.0 * sqrt(2.0) * r.float)
78 s.samples = newSeqOfCap[(int, int)](n)
79 for _ in 1 .. n:
80 let t = rng.rand(2.0 * PI)
81 s.samples.add (int(round(r.float * cos(t))), int(round(r.float * sin(t))))
82 s.revealed = 0
83 s.scells = initHashSet[(int, int)]()
84 s.perStep = max(1, n div max(1, int(r.float / sqrt(2.0))))
85
86proc reveal(s: var State, k: int) =
87 let stop = min(s.revealed + k, s.samples.len)
88 while s.revealed < stop:
89 s.scells.incl s.samples[s.revealed]
90 inc s.revealed
91
92proc step(s: var State) =
93 ## Exactly one iteration of the Bresenham loop body, plus the sprinkle's
94 ## proportional share of samples so the two fill at a comparable rate.
95 if s.done: return
96 if s.x > s.y:
97 s.done = true
98 s.branch = brNone
99 s.reveal(s.samples.len)
100 return
101
102 s.plotted.add (s.x, s.y)
103 for m in mirrors(s.x, s.y): s.bcells.incl m
104
105 if s.d < 0:
106 # midpoint (x+1, y-1/2) is INSIDE the circle -> East, keep y
107 s.branch = brEast
108 s.lastUpdate = fmt"d += 2*{s.x} + 3 = {2 * s.x + 3}"
109 s.d += 2 * s.x + 3
110 else:
111 # midpoint is OUTSIDE -> South-East, step y in
112 s.branch = brSouthEast
113 s.lastUpdate = fmt"d += 2*({s.x} - {s.y}) + 5 = {2 * (s.x - s.y) + 5}"
114 s.d += 2 * (s.x - s.y) + 5
115 dec s.y
116 inc s.x
117
118 s.reveal(s.perStep)
119 if s.x > s.y:
120 s.done = true
121 s.reveal(s.samples.len)
122
123proc symmetries(c: Cells): int =
124 ## How many of the lattice's 8 symmetries actually map this pixel set to
125 ## itself. Bresenham: 8. A sprinkle: 1 (the identity), essentially always.
126 if c.len == 0: return 0
127 for k in 0 ..< 8:
128 var ok = true
129 for (x, y) in c:
130 if mirrors(x, y)[k] notin c:
131 ok = false
132 break
133 if ok: inc result
134
135# ------------------------------------------------------------------- layout --
136
137proc layout(sw, sh: int): tuple[panelW, gridW, halfW, maxR, cellCap: int] =
138 let panelW = clamp(PanelWant, 280, sw div 3)
139 let gridW = sw - panelW
140 let halfW = gridW div 2
141 let avail = max(min(halfW - 30, sh - 150), MinCell)
142 let maxR = max(MinRadius, (avail div MinCell - 3) div 2)
143 (panelW, gridW, halfW, maxR, avail)
144
145# ------------------------------------------------------------------ drawing --
146
147type View = object
148 ox, oy, cell, half: int
149
150proc cx(v: View, gx: int): int = v.ox + gx * v.cell - v.half
151proc cy(v: View, gy: int): int = v.oy - gy * v.cell - v.half
152
153proc drawRect(x, y, w, h: int, c: Color) =
154 drawRectangle(x.int32, y.int32, w.int32, h.int32, c)
155
156proc drawRectLines(x, y, w, h: int, c: Color) =
157 drawRectangleLines(x.int32, y.int32, w.int32, h.int32, c)
158
159proc drawLn(x1, y1, x2, y2: int, c: Color) =
160 drawLine(x1.int32, y1.int32, x2.int32, y2.int32, c)
161
162proc text(s: string, x, y, size: int, c: Color) =
163 drawText(s, x.int32, y.int32, size.int32, c)
164
165proc backdrop(v: View, r: int) =
166 ## Grid, axes and the ideal circle both methods are chasing.
167 let ext = r + 1
168 if v.cell >= 9:
169 for i in -ext .. ext:
170 drawLn(v.cx(-ext), v.cy(i) + v.half, v.cx(ext) + v.cell, v.cy(i) + v.half, GridLine)
171 drawLn(v.cx(i) + v.half, v.cy(-ext) + v.cell, v.cx(i) + v.half, v.cy(ext), GridLine)
172 drawLn(v.cx(-ext), v.oy, v.cx(ext) + v.cell, v.oy, Axis)
173 drawLn(v.ox, v.cy(-ext) + v.cell, v.ox, v.cy(ext), Axis)
174 drawCircleLines(v.ox.int32, v.oy.int32, float32(r * v.cell), TrueArc)
175
176proc main =
177 setConfigFlags(flags(WindowResizable))
178 initWindow(InitW, InitH, "Bresenham vs Poisson sprinkle")
179 defer: closeWindow()
180 setWindowMinSize(900, 640)
181 setTargetFPS(60)
182
183 var
184 s = State()
185 auto = false
186 held = 0
187 tick = 0
188 frames = 0
189 shot = "--shot" in commandLineParams()
190 shotFrame = 400
191 startR = 12
192 for a in commandLineParams():
193 if a.startsWith("--r="): startR = max(parseInt(a[4 .. ^1]), MinRadius)
194 elif a.startsWith("--frame="): shotFrame = parseInt(a[8 .. ^1])
195 s.over = 4
196 s.reset(min(startR, layout(getScreenWidth().int, getScreenHeight().int).maxR))
197 if shot: auto = true
198
199 while not windowShouldClose():
200 inc frames
201
202 let
203 sw = getScreenWidth().int
204 sh = getScreenHeight().int
205 (panelW, gridW, halfW, maxR, _) = layout(sw, sh)
206 if s.r > maxR: s.reset(maxR)
207
208 # ---- input
209 if isKeyPressed(Space): s.step()
210 if isKeyPressed(A): auto = not auto
211 if isKeyPressed(R): s.reset(s.r)
212 if isKeyPressed(RightBracket) and s.over < 12: s.reset(s.r, s.over + 1)
213 if isKeyPressed(LeftBracket) and s.over > 1: s.reset(s.r, s.over - 1)
214 var delta = 0
215 if isKeyPressed(Up): delta = 1
216 elif isKeyPressed(Down): delta = -1
217 elif isKeyDown(Up) or isKeyDown(Down):
218 inc held
219 if held > 18:
220 let fast = if held > 70: 1 else: 3
221 if held mod fast == 0: delta = if isKeyDown(Up): 1 else: -1
222 else:
223 held = 0
224 if delta != 0:
225 let want = clamp(s.r + delta, MinRadius, maxR)
226 if want != s.r: s.reset(want)
227 if auto and not s.done:
228 inc tick
229 if tick mod max(1, 10 - s.r div 8) == 0: s.step()
230
231 # ---- both panes share a cell size so they are directly comparable
232 let
233 span = 2 * s.r + 3
234 cell = max(min(halfW - 30, sh - 150) div span, MinCell)
235 oy = sh div 2 + 10
236 left = View(ox: halfW div 2, oy: oy, cell: cell, half: cell div 2)
237 right = View(ox: halfW + halfW div 2, oy: oy, cell: cell, half: cell div 2)
238
239 drawing:
240 clearBackground(Bg)
241
242 # ================= left: Bresenham =================
243 backdrop(left, s.r)
244 let ext = s.r + 1
245 drawLn(left.ox, left.oy, left.ox, left.cy(ext) + left.half, Axis)
246 drawLn(left.ox, left.oy, left.cx(ext) + left.half, left.cy(ext) + left.half, Axis)
247
248 for (px, py) in s.plotted:
249 for k, (mx, my) in mirrors(px, py):
250 drawRect(left.cx(mx), left.cy(my), cell, cell,
251 if k == 0: Octant else: Mirror)
252
253 if not s.done:
254 drawRectLines(left.cx(s.x), left.cy(s.y), cell, cell, Cursor)
255 if s.x + 1 <= s.y:
256 drawRect(left.cx(s.x + 1), left.cy(s.y), cell, cell, Candidate)
257 drawRect(left.cx(s.x + 1), left.cy(s.y - 1), cell, cell, Candidate)
258 drawCircle(int32(left.cx(s.x + 1) + left.half), int32(left.cy(s.y) + cell),
259 float32(max(3, cell div 5)), Mid)
260
261 # ================= right: Poisson sprinkle =================
262 backdrop(right, s.r)
263 for (mx, my) in s.scells:
264 drawRect(right.cx(mx), right.cy(my), cell, cell, Sprink)
265
266 # ---- titles and per-pane stats
267 let
268 bSym = symmetries(s.bcells)
269 sSym = symmetries(s.scells)
270 lx = 24
271 rx = halfW + 24
272 text("BRESENHAM", lx, 16, 20, Octant)
273 text("one octant, mirrored 8 ways", lx, 40, 14, Dim)
274 text("POISSON SPRINKLE", rx, 16, 20, Sprink)
275 text(fmt"uniform directions, snapped ({s.over}x oversample)", rx, 40, 14, Dim)
276
277 let by = sh - 66
278 text(fmt"{s.bcells.len} cells {s.plotted.len} computed, rest free", lx, by, 16, Ink)
279 text(fmt"exact symmetries: {bSym} / 8", lx, by + 22, 16, Octant)
280 text(fmt"{s.scells.len} cells {s.revealed} samples rolled", rx, by, 16, Ink)
281 text(fmt"exact symmetries: {sSym} / 8", rx, by + 22, 16, Sprink)
282
283 drawLn(halfW, 0, halfW, sh, GridLine)
284
285 # ================= panel =================
286 let px = gridW + 24
287 drawLn(gridW, 0, gridW, sh, GridLine)
288
289 text("MIDPOINT CIRCLE", px, 20, 22, Ink)
290 text(fmt"radius {s.r} (max {maxR} at {sw}x{sh})", px, 48, 15, Dim)
291
292 let code = [
293 "x = 0; y = r; d = 1 - r",
294 "while x <= y:",
295 " plot8(x, y)",
296 " if d < 0: # mid inside",
297 " d += 2*x + 3",
298 " else: # mid outside",
299 " d += 2*(x - y) + 5",
300 " y -= 1",
301 " x += 1",
302 ]
303 let lit =
304 case s.branch
305 of brEast: @[2, 3, 4, 8]
306 of brSouthEast: @[2, 5, 6, 7, 8]
307 of brNone: @[]
308 for i, line in code:
309 let ly = 84 + i * 22
310 if i in lit: drawRect(px - 8, ly - 3, panelW - 32, 22, HiLine)
311 text(line, px, ly, 16, if i in lit: Ink else: Dim)
312
313 var sy = 300
314 text("state", px, sy, 16, Dim); sy += 26
315 text(fmt"x = {s.x}", px, sy, 20, Ink); sy += 26
316 text(fmt"y = {s.y}", px, sy, 20, Ink); sy += 26
317 text(fmt"d = {s.d}", px, sy, 20, (if s.d < 0: Octant else: Cursor)); sy += 32
318 text(s.lastUpdate, px, sy, 15, Cursor); sy += 30
319 if s.done:
320 text("done -- x > y, octant closed", px, sy, 18, Octant)
321 elif s.x + 1 <= s.y:
322 text(fmt"testing midpoint ({s.x + 1}, {s.y}-1/2)", px, sy, 15, Mid)
323
324 var ly = sh - 170
325 text("cost of zero symmetry", px, ly, 15, Dim); ly += 24
326 if s.done and s.scells.len > 0:
327 let waste = s.revealed.float / s.scells.len.float
328 text(fmt"{waste:.1f} samples per cell drawn", px, ly, 15, Sprink); ly += 22
329 let gap = 100.0 * (s.scells.len.float / max(1, s.bcells.len).float - 1.0)
330 text(fmt"{gap:+.0f}% cells vs bresenham", px, ly, 15, Sprink); ly += 30
331 else:
332 ly += 52
333
334 text("SPACE step A auto R reset", px, ly, 15, (if auto: Cursor else: Dim)); ly += 20
335 text("UP/DOWN radius (hold to scrub)", px, ly, 15, Dim); ly += 20
336 text("[ ] sprinkle oversample", px, ly, 15, Dim)
337
338 if shot and frames == shotFrame:
339 takeScreenshot("shot.png")
340 break
341
342main()