Lab 1: Pixar Ball

By the end of this lab, you will:

To access the lab template, please click the assignment link in this discussion board message.

This lab is inspired by this exercise from the Pixar-Khan Academy collaboration called Pixar-in-a-Box (similar to the Chapter 2B example). Here, we will control the height of a ball over time using cubic curves which will either interpolate four points, or treat them as Bézier control points.

You will mostly work in the Curve class (in curve.js), but will also (optionally) modify the Animation class (in animation.js) in Part 5. The Animation class maintains two Curve objects: one for the downwards motion of the ball (curve 0), and one for the upwards motion of the ball (curve 1). Starting from the top at time = 0, the ball hits the ground at time = 0.5 and then comes back to the top at time = 1.

Your task in this lab consists of implementing some Curve methods to interpolate, evaluate and render the curves.

Each curve in this lab is parametrized in the interval $t \in [0, 1]$. Evaluating the curve at $t = 0$ gives the curve starting point and evaluating at $t = 1$ gives the curve end point. Note that $t$ is not the same as time in the animation. In each part below, the conversion between time and $t$ has already done for you in the code, which is:

$$ t = \frac{\mathrm{time} - \mathrm{time}_0}{\mathrm{time}_3 - \mathrm{time}_0}, $$

where $\mathrm{time}_0$ is the time (x-coordinate) of the first point on the curve and $\mathrm{time}_3$ is the time (x-coordinate) of the last (fourth) point on the curve. The denominator is actually always 0.5 in this lab since this is the time interval spanned by each curve.

You should initially see a total of 7 points: the three black points are the fixed points of the curves and the four pink points are modifiable. Click and drag any of the pink points to modify these points. There are some callbacks that are already implemented for you (in animation.js), which modify the keyframe points referenced by the two curves.

Each Curve stores the four points that define the curve (whether we interpolate the points or use them as Bézier control points). Within any method of the Curve class, you can access these points using this.points[i] where i is the index of the desired point ($0 \le i \le 3$). Each this.points[i] is a vec2. Therefore this.points[i][0] is the x-coordinate (which is the time) and this.points[i][1] is the y-coordinate (which is the height) of the $i$-th control point.

Part 0: practice with vec2.lerp.

The vec2.lerp function is useful for performing "Linear intERPolation" between two input points. Specifically, the vec2.lerp function (see here) will calculate:

$$ \vec{c} = \vec{a} + t (\vec{b} - \vec{a}), $$

using:

let c = vec2.lerp(vec2.create(), a, b, t);

Try it out (in the Console of this webpage) using a = vec2.fromValues(0.2, 0.5), b = vec2.fromValues(0.75, 0.25) with t = 0.8. The result should be [0.64, 0.3]. The vec2.lerp function will be useful for Parts 3 and 4.

Part 1: complete the Curve interpolate function.

Complete the interpolate method in the Curve class (in curve.js) to interpolate the four points stored in this.points. The method you implement here should be very similar to what we did in class. At the end of this method, this.coefficients should be filled with the four coefficients ($a,\ b,\ c,\ d$) that define the cubic curve:

$$ h(t) = a\ t^3 + b\ t^2 + c\ t + d. $$

The conversion from time to t is already done for you, and the height is extracted from this.points[i][1].

Part 2: complete the Curve evalInterpolant function.

Now complete the evalInterpolant function (also in curve.js) using this.coefficients (computed in Part 1) to evaluate the height curve at a specific parameter value t. Note that the input t is in the range $[0, 1]$ so no conversion is necessary when evaluating the curve.

Also notice that time is already computed, which is the x-coordinate of the output vec2.

How to know when this works? Moving the pink points should produce curves that pass through the four interpolated points of each curve. Also, the red tracer (and the ball height) should follow your interpolating curve when you click the Animate button.

Part 3: implement de Casteljau's algorithm in the Curve drawBezier function.

Change the dropdown to Cubic Bézier and notice the curve rendering looks quite rough. This is because only the base case (depth == 0) is implemented, which simply connects the controls points. If you increase the render depth, the curve will disappear because the recursive case is not implemented yet.

For the recursive case, we can evaluate the curve at $t = 0.5$, divide the original Bézier curve into two new Bézier curves, and continue dividing those until a user-specified recursion depth is reached (there is an HTML input to control the recursion depth of the Bézier curve rendering).

Your task in this part is to complete the code in the Curve drawBezier function to render a Bézier curve recursively using the specified recursion depth.

Remember that de Casteljau's algorithm creates two new Bézier curves, the first one with controls points $[\vec q_0,\ \vec m_0,\ \vec r_0,\ \vec p(0.5)]$ and the second curve with control points $[\vec p(0.5),\ \vec r_1,\ \vec m_2,\ \vec q_3]$. Once you create these two new curves (say curve0 and curve1), you should call their drawBezier functions, decrementing the depth (i.e. curve0.drawBezier(depth - 1) and curve1.drawBezier(depth - 1)).

How to know when this works? The curve should look progressively smoother as you increase the depth in the Bézier render depth input.

Part 4: complete the Curve evaluateBezier function.

Notice that the ball height does not follow the correct Bézier curve - see the red tracer which follows a straight line. This is because we are currently using linear interpolation (vec2.lerp) between the curve endpoints to compute the height of the ball at a particular parameter t within the Curve evalBezier function. This is not what we want.

Please complete the Curve evaluateBezier function. This function takes in a particular parameter value $t$ ($0 \le t \le 1$) and returns a vec2 corresponding to the evaluated point on the curve. Use de Casteljau's algorithm and note that vec2.lerp can be used to do a lot of the calculations here.

How to know when this works? When the dropdown is in Cubic Bézier mode, the red tracer (and ball height) should follow the Bezier curves (as in the animation above).

[Optional] Part 5: create a squash and stretch effect.

Search for the call to context.drawImage in animation.js. Notice that the width (w) and height (h) are both set to 50, which define the size of the pixarball.png image as it is pasted at the $y$-location during the animation. Feel free to experiment with a different way to specify w and h by modifying this block of code in animation.js in order to create a "squash-and-stretch" effect:

if (document.getElementById("input-squash-stretch").checked) {
  // ...
}

Submission

If you want to investigate further with how things are drawn, look up the CanvasRenderingContext2D documentation since everything in this lab is drawn with the "2d" rendering context of the HTMLCanvas. A lot of the other pieces in animation.js (particularly, the transformations) will be covered later in the semester.

The initial submission for the lab is due on Thursday 2/27 at 11:59pm EST. Please see the Setup page for instructions on how to commit and push your work to your GitHub repository and then submit your repository to Gradescope (in the Lab 1 assignment). I will then provide feedback within 1 week so you can edit your submission.

Please note that Codespaces are generally deleted after a certain amount of time (I believe it's 30 days), so it's important to commit and push your work often.


© Philip Caplan, 2025