Drawing shapes
When conducting an interview that involves running code on an actual computer, I like to offer the candidate a problem that produces some sort of “fun output.” Seeing their program output the correct result provides a nice sense of completion and leaves the candidate feeling good about their work. Problems that require producing a plaintext from a ciphertext obviously have this property, but for a more general phone-screen-level question, one of my favorite exercises is to have the candidate draw shapes.
This exercise is extremely adaptable. It can be made short or long, math-light or math-heavy, and works in any programming language (I’ll give examples here in JavaScript and Python). The point of the problem is to have the candidate draw shapes, pixel by pixel, so if they want concepts like “line” or “arc,” they will have to build those themselves.
The first step is to define some way to draw pixels on a canvas. You can provide this as starter code or have the candidate write it. In JavaScript I built on top of the canvas API while in Python I defined a pixel array and printed it to stdout.
Here’s my Python code:
class Canvas:
def __init__(self, width, height):
self._width = width
self._height = height
self.clear()
def clear(self):
self._canvas = [[" "] * self._width for _ in range(self._height)]
def drawPixel(self, x, y):
self._canvas[y][x] = "O"
def __repr__(self):
return "\n".join("".join(row) for row in self._canvas)
And my JavaScript code:
/** Draw on an HTML5 canvas, pixel by pixel
el: the canvas element to draw on
width, height: the dimensions of the canvas, in physical pixels
pxSize: the size of each pixel on our canvas (defaults to 3px)
Note: a 300x300px canvas with pxSize=3 will have 100x100 "virtual pixels"
*/
class CanvasView {
constructor (el, width, height, pxSize=3) {
this._canvas = el;
this._canvas.width = width;
this._canvas.height = height;
this._ctx = this._canvas.getContext('2d');
this._ctx.fillStyle = 'black';
this._pxSize = pxSize;
}
pxSize() {
return this._pxSize;
}
drawPixel(x, y) {
this._ctx.fillRect(x * this._pxSize, y * this._pxSize, this._pxSize, this._pxSize);
}
clear() {
this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
}
}
I’ll give the rest of these examples in JavaScript, but the same ideas apply in any language.
My initial assumption was that the easiest starting point for candidates would be drawing a line, but after going through the exercises I think starting off with a circle is the best way to go.
/** Draw a circle
cv: the CanvasView to draw on
x, y: the center of the circle
r: the radius of the circle (in virtual pixels)
*/
function drawCircle(cv, x, y, r) {
let numPoints = 4 * Math.PI * r;
for (let i = 0; i < numPoints; i++) {
let angle = (i/numPoints) * (2*Math.PI);
let xp = x + r * Math.cos(angle);
let yp = y + r * Math.sin(angle);
cv.drawPixel(xp, yp);
}
}
There are a lot of interesting pieces already in this first function. The
candidate has to figure out how many pixels to draw (hint: what is the
circumference of the circle measured in pixels?) and they have to remember some
basic trigonometry (or else the x^2 + y^2 == r^2
formula for a circle). I’ve
made a few modifications to my function, like using twice as many points in
order to get a more even line. It’s always nice to see the candidate take
initiative to make their drawing “prettier,” and visual problems encourage that
initiative
Once the candidate has a nice circle, a natural extension is to draw a spiral.
/** Draw a spiral
cv: the CanvasView to draw on
x, y: the center of the spiral
r: the radius of the outermost curl of the spiral (in virtual pixels)
curls: the number of turns the spiral should make
*/
function drawSpiral(cv, x, y, r, curls) {
let numPoints = 4 * Math.PI * r * curls;
for (let i = 0; i < numPoints; i++) {
let angle = (i/numPoints) * (2*Math.PI) * curls;
let xp = x + r * i/numPoints * Math.cos(angle);
let yp = y + r * i/numPoints * Math.sin(angle);
cv.drawPixel(xp, yp);
}
}
Only a few changes are required here, but you can first ask the candidate to
draw one loop of a spiral, avoiding the need for the curls
variable, and then
add support for multiple turns.
One relatively easy extension is to draw arbitrary functions. It’s important to
emphasize that the functions should be defined in code like y = (x) => x * x
,
not as strings like "y=x^2"
, although that’s an interesting problem too! Even
though this is the easiest part of the problem, I don’t ask it first because if
I did, candidates might implement drawCircle()
using two functions (+/-
sqrt(r^2 - x^2)
) and then get stuck writing drawSpiral()
.
/** Draw a function
cv: the CanvasView to draw on
f: the function to draw
x0, xf: the domain of the function to draw
*/
function drawFunction(cv, f, x0, xf) {
for (let i = x0; i <= xf; i++) {
cv.drawPixel(i, f(i));
}
}
Finally, I’ll return to drawing straight lines. This can also be specified using
drawFunction()
, but I’ll write it out explicitly here. This function uses
knowledge of the distance function and how to find the direction of a vector,
but otherwise, if the candidate has made it this far they should be able to add
more shapes without much trouble.
/** Draw a line
cv: the CanvasView to draw on
x0, y0: the starting point of the line
x1, y1: the ending point of the line
*/
function drawLine(cv, x0, y0, x1, y1) {
let dist = Math.hypot(x1 - x0, y1 - y0);
let resolution = cv.pxSize();
let theta = Math.atan((y1 - y0)/(x1 - x0));
for (let i = 0; i < resolution * dist; i++) {
let xp = i/resolution * Math.cos(theta);
let yp = i/resolution * Math.sin(theta);
cv.drawPixel(xp, yp);
}
}
There are lots of other fun possibilities at this point, like animating drawings over time. By drawing larger and larger concentric circles, for example, the candidate can implement the “dynamite eraser” effect from Kid Pix. For completeness, I’ll include the code that was used to generate the picture at the top of this page.
/* Remember, this canvas is 101x101 virtual pixels */
let canvas = new CanvasView(document.querySelector('canvas'), 303, 303);
drawCircle(canvas, 50, 50, 50);
drawSpiral(canvas, 50, 50, 50, 5);
let sq = (x) => 100-x*x/100;
drawFunction(canvas, sq, 0, 100);
let sine = (x) => 50 + 50*Math.sin(x/25*Math.PI);
drawFunction(canvas, sine, 0, 100);
drawLine(canvas, 0, 0, 101, 101);
You can play around with this yourself by opening the JavaScript console on this
page and creating a new CanvasView
. To draw a circle in the canvas at the top
of this page and then clear it, you can run the following.
let cv = new CanvasView(document.querySelector('canvas'), 303, 303);
drawCircle(cv, 50, 50, 50);
cv.clear();