Fall 2024
In this lab, you’ll implement algorithms to draw Bézier spline curves.
Environment: The code is implemented in Javascript using an HTML Canvas, so all you need is a text editor and a browser.
Collaboration: You should write your own code, but please collaborate freely with your classmates on this lab. If you’ve finished tasks 1-3, help out those who haven’t; conversely, if you’re stuck, check in with your neighbors to see if they have any helpful suggestions.
Extensions: There are plenty of ways to extend this lab if you have extra time. These are optional, and listed at the end of this handout. If you can’t find anyone who needs help, work on some of the Optional Extra Fun tasks at the bottom of this writeup.
Submission: I anticipate that you’ll be able to finish this during class time, but I’m giving you until 10pm Friday 11/22 to submit your code on Github. If you finish during class, let me know so I can assign your grade right away.
This lab will be done in a new repository, created via Github
Classroom as usual. Accept the invite (found on Canvas) and clone your
repo. You’ll be working in splines.js
, which loosely
mirrors one of the task?.js
files from A3. The
splines.html
file just contains the canvas we’ll draw on,
and matrix.js
contains modified version of the
matrix.js
library from A3. I made three significant changes
to the matrix library:
SimplerMatrix
(Simpler, with an r)multiplyVector
to
not assume the fourth component of the input vector is 1.eval_direct
to directly compute the position
of the curve at time t
eval_dcj
to evaluate the position using de
Casteljau’s algorithm.draw_dcj
to draw the curve using piecewise
linear subdivision.Start by looking over the code from the beginning until the comment marker that says “end of lab code”. Everything after that can be safely ignored - it’s just setup code and event handling code to make the control points click-and-draggable.
Part A: In the Spline object’s “constructor”, fill in the (hard-coded) Bézier control matrix that we derived in lecture. This matrix converts the control point coordinates into polynomial coefficients.
Part B: For a given call to the render function, the
control points are fixed, and we’ll want to evaluate potentially many
points along the curve. For efficiency, we’ll start by pre-computing the
polynomial coefficients by multiplying the Bézier matrix by the control
points and saving them for repeated use when evaluating points on the
curve. At the top of the render
function, complete the two
lines that set this.Ax
and this.Ay
. Recall
that the vector \(\vec{a}\) of
coefficients is given by multiplying the Bézier matrix \(B\) by the positions of the control points
\(\vec{p}\). Part C:
Finally, implement eval_direct
, which takes a
t
value and computes the position on the curve by directly
evaluating the polynomial; recall that since Bézier curves are cubic,
the position at \(t\) is \(a_0 + a_1 t + a_2 t^2 + a_3 t^3\).
If you’ve correctly implemented Task 1, you should be able to
uncomment the first commented-out block of the render
function. This function draws a series of dots at evenly spaced values
of \(t\) along the curve. With this
task complete, I get a result that looks like this:
You should be able to click and drag the blue dots to change the control points and watch the red dots adjust. You can also click anywhere in the canvas and the nearest control point to your cursor will jump to that location.
Instead of direct evaluation, we will now evaluate points using de Casteljau’s algorithm. Recall that this algorithm computes a point on the curve by linearly interpolating between control points, then linearly interpolating between those interpolated points, and so on until you arrive at a single point. I find the following figure is good to keep in mind for visual intuition: The left figure is easier to parse, but just keep in mind that the algorithm works for any value of \(t\) (the book is using \(u\) instead of \(t\)).
Implement eval_dcj
. Make use of the lerp
helper function just above; if you give it \(x\), \(y\), and \(t\), it gives you the value that is \(100t\%\) of the way from \(x\) to \(y\). I don’t recommend trying to put this
in a loop - it’s likely to be more complicated than just writing out the
(somewhat verbose) sequence of 12 (6, but one each for \(x\) and \(y\)) lerp
operations.
Uncomment the second block in the render
function to
test this. That code is similar to the Task 1 code, except it draws
green dots at offset positions. My result looks like this:
Finally, let’s actually draw the curve as a line. We could do this by drawing many points with sufficiently tight spacing so that there are no gaps, but this is inefficient and it’s also not always clear how tight the spacing needs to be, since equally spaced \(t\) values do not result in equally spaced points. The approach we’ll use leverages a nifty property that’s unique to Bézier curves: when you compute the interpolated points in de Casteljau’s algorithm, you’re actually generating the control points for two sub-curves that comprise the original curve. Here’s the figure to keep in mind:
In this picture, the original curve has been subdivided into two new curves. The first has control points A, AB, AC, and AD, while the second has control points AD, BD, CD, and D.
Drawing a curve can be accomplished by performing this subdivision recursively; at some point, a sub-curve becomes sufficiently flat that we won’t know the difference between a line segment and the sub-curve; at this point, we can draw line segments connecting the control points and terminate the recursion.
Take a look at the draw_dcj
method. The base case, which
draws line segments and returns whether the given curve is sufficiently
line-segment-like, is implemented for you. Your job is to implement the
recursive case: subidvide the curve into two pieces by splitting it at
\(t = 0.5\), then make two recursive
calls to draw_dcj
. When this is working, you should see a
result like this:
When you’re convinced it works, you can comment out the dot-drawing
pieces in the render
function so you can just see the
un-decorated curve. Drag the control points around and see if there’s
any configuration where you can tell that it’s a piecewise linear
approximation. Try changing the maxDepth
argument too
draw_dcj
to a smaller number to limit the recursion levels
and then see if you can spot inaccuracies.
Much of the math in this codebase operates on 2D points; lacking nice
features like the ability to easily define a Vec2
type and
have elementwise operations Just Work, the code often repeats the same
line once for the \(x\) coordinate and
once for the \(y\) coordinate. It’s a
little verbose, but at least we’re only in 2D.
In Javascript, a variable is global unless you specify otherwise. Who
knew? Not me, until I started impelmenting Task 4 and my
turns-out-not-local variables were getting modified by the first
recursive call before the second one was made. Pro tip: declare local
variables using var
, as in var x = 4
, to give
them local scope.
Debugging this code can be a hassle, because the render function is called over and over again. I don’t normally advocate for the use of debuggers, but I found it helpful to be able to set breakpoints. I was using Firefox, and the debugger is pretty intuitive to use - going to the Debugger tab of the developer tools and clicking a code line number sets a breakpoint there; once stopped, you can inspect values.
In addition to the nearly-collinear base case in
draw_dcj
(Task 3), I also included an escape hatch: you can
specify a maxDepth
argument to limit how deep the recursion
goes. Setting this to something small in the render method’s call too
draw_dcj
can help when debugging.
This lab is worth 10 points in the Surveys, Labs, and Participation category. Grading is pretty chill: if all the tasks work, you’ll get full credit. The three tasks will be worth 7, 2, and 1, respectively. If you finish them during class, please let me know so I can assign your grade right away.
Add multiple Spline objects. Add support for Spline objects that share one control point (so that their ends are joined). Add support for Splines that also have their second control points linked such that continuity is maintained at the point where they join (in other words, the point’s tangent is the same on both curves). Add UI elements to let a user draw new curves. Add a button that lets a user split a curve using de Casteljau.
Implement other kinds of splines:
Generalize your code to draw order-\(k\) Bézier curves using de Casteljau.
Try out a degree-8 polynomial with control points specified at even intervals throughout. Is this easy to use?
Implement cardinal splines, defined by the following constraints, with some chosen value between 0 and 1 of the “tension” parameter \(t\):
\[ \begin{align*} \mathbf{f}(0) &= \mathbf{p}_1\\ \mathbf{f}(1) &= \mathbf{p}_2\\ \mathbf{f}'(0) &= \frac{1}{2}(1-t)(\mathbf{p}_2 - \mathbf{p}_0)\\ \mathbf{f}'(1) &= \frac{1}{2}(1-t)(\mathbf{p}_3 - \mathbf{p}_1)\\ \end{align*} \]