Quick fact about the way the interactivity is done, all of the code for it is in this blogpost.js file: https://aaaa.sh/creatures/blogpost.js, which is only about 100 lines long. Each block has a list of scripts that it pulls from like so:
<div class="code-example" scripts="grid-sm 2d-vector-gfx-lib draw-grid full-algo-intro feather-canvas-edges"></div>
and then there's a set of script tags with those ids. I figured it was a nice solution!
This algorithm is one I use to demo some features in a language I'm making called calculang [0][1] I like the way you step through the logic.
My only suggestion would be to include a raycasted scene because you can really grab a wide(r) audience (and I'd love to see how you get to voxel scenes!).
Either way - thanks for adding a neat new resource for this algorithm and I'm definitely taking notes Re your clean presentation of the details!
In other words, decompose the problem into one of finding where the ray crosses the X grid lines and the Y grid lines, and the mental image is:
+-------+-------+-------+ | | | | -------------------------
| | | ^ | | | | ^ | ^
| | | / | | | | / | /
| | | / | | | | / | /
+-------+-------+-*-----+ | | | / | ------------------*------
| | |/ | | | |/ | /
| | * | | | * | /
| | /| | | | /| | /
+-------+-----*-+-------+ = | | / | | U --------------*----------
| | / | | | | / | | /
| | / | | | | / | | /
| | / | | | | / | | /
+-------+-*-----+-------+ | | / | | ----------*--------------
| |/ | | | |/ | | /
| * | | | * | | /
| o| | | | o| | | o
+-------+-------+-------+ | | | | -------------------------
Now, if our ray has origin o, and direction d, the position of any point along the ray for a t-value is: x' = o.x + t * d.x
y' = o.y + t * d.y.
Where's the first grid intersection in X? Assuming d.x is positive, it'll be at ceil(o.x). So solve for t: ceil(o.x) = x' = o.x + t * d.x
t = (ceil(o.x) - o.x) / d.x.
Then, since the grid lines are spaced 1 unit apart, every subsequent intersection will be 1/d.x further along in t. In other words, the ray t-values of all the grid crossing in X are stream generated by a simple linear function: ((ceil(o.x) - o.x)/d.x) + step * 1/d.x.
And exactly the same thing applies in Y for the Y grid crossings.So let's say that we have a ray with origin (0.8, 0.2) and a direction of (0.5, 0.25). Then we get:
X-crossings = [0.4, 2.4, 4.4, 6.4, 8.4, 10.4, ...]
Y-crossings = [3.2, 7.2, 11.2, 15.2, ...]
Then we merge those two infinite ordered streams: [0.4, 2.4, 3.2, 4.4, 6.4, 7.2, 8.4, 10.4, 11.2, ...]
X X Y X X Y X X Y ...
and those are the t-values where we walk from one cell to another. Keeping track of which value came from which stream tells us whether the step is in X or in Y (it's always one or the other - a DDA never takes a diagonal step). Continue lazily generating and merging the stream this way until you run into something, run out of bounds, or just decide you've gone far enough to stop.Obviously, there's some fiddliness when the direction has negative components or worse, zero, but it's not to hard to work out the adjustments for those cases. Likewise, you also want to be very careful about handling the case where the ray origin is exactly on a cell boundary. But again, focusing on just one dimension at a time helps when reasoning all that out.
Generalizing the concept to a DDA in 3D on a voxel grid isn't much harder, either: now you find the stream of Z-crossings as well and do a 3-way merge.
(If you ever want to vectorize grid traversal across multiple rays, there are some tricks to that in a 2006 SIGGRAPH paper that I worked on: https://www.sci.utah.edu/~wald/Publications/2006/CGT/grid.pd...)
An interesting point about Bresenham's algorithm is made by David Schmenk (dschmenk) on his "Bresen-Span" page:
"Take note that the algorithm can be viewed as the long division of delta-major/delta-minor. The error term is really the running remainder, and every step results in a pixel along the major axis until the division completes with a remainder. The division restarts by moving along the minor axis and adding the dividend back in to the running remainder (error term). This is a bit of a simplification, but the concept is that the long division will only result in two integral spans of pixels, depending on the value of the running remainder (error term). We will take this in to account to write a routine that outputs spans based on the two span lengths: a short-span and a long-span."
In other code, dschmenk does use DDA for anti-aliased lines.
Can't wait to see how you explain it.
But nice otherwise, interactivity is great.