Monocurl Essays
Series Download

The dot product: projection, angle and why the formula works

Multiply matching coordinates and add. The result measures how far one arrow reaches along another, which gives angles, projections and Cauchy-Schwarz.

23 min·Early college·

AssumesWhat is a vector? Arrows, lists of numbers, and adding themLaw of cosines and law of sines: solving any triangle

The arrows $\mathbf{a} = (3, 1)$ (blue) and $\mathbf{b} = (1, 2)$ (orange) are turned together through a quarter turn; the faint arrows mark where they started. All four coordinates change, and so do the two products $a_1b_1$ and $a_2b_2$, but their sum stays at 5 the whole way.
Monocurl source
# the origin sits at the bottom centre of this board, so the quarter turn sweeps all of it
let Hb = Board([-0.81, -1.55, 0], 0.83, -3, 3, 0, 4)
let Turn = |deg| {
    let a = rot2(VA, deg)
    let b = rot2(VB, deg)
    return [
        Vec(Hb, [0, 0], VB, tint(EMBER, 0.3), 3.5),
        Vec(Hb, [0, 0], VA, tint(AZURE, 0.3), 3.5),
        Vec(Hb, [0, 0], b, EMBER),
        Vec(Hb, [0, 0], a, AZURE),
        center{[2.6, 1.5, 0]} color{AZURE} Number(clean(a[0]), 2),
        center{[3.3, 1.5, 0]} color{AZURE} Number(clean(a[1]), 2),
        center{[2.6, 0.9, 0]} color{EMBER} Number(clean(b[0]), 2),
        center{[3.3, 0.9, 0]} color{EMBER} Number(clean(b[1]), 2),
        center{[2.15, -0.4, 0]} color{INK} Number(clean(a[0] * b[0]), 2),
        center{[3.25, -0.4, 0]} color{INK} Number(clean(a[1] * b[1]), 2),
        center{[2.7, -1.25, 0]} scale{1.5} color{LEAF} Number(dp(a, b), 2)
    ]
}
mesh ground = Ground(Hb)
mesh turn = Turn(deg: 0)
mesh label = [
    center{[1.95, 1.5, 0]} Math("\mathbf{a}", 0.95, AZURE),
    center{[1.95, 0.9, 0]} Math("\mathbf{b}", 0.95, EMBER),
    center{[2.7, 0.2, 0]} Math("a_1b_1 + a_2b_2", 0.85, INK),
    center{[2.7, -0.4, 0]} Math("+", 0.9, INK)
]

slide "turn"
    play Wait(0.8)
    turn.deg = 90
    play Lerp(5, [&turn])
    play Wait(1.6)

Uses the shared definitions for this lesson's figures.

Take two arrows from the origin, $\mathbf{a} = (3, 1)$ and $\mathbf{b} = (1, 2)$. Multiply their first coordinates, multiply their second coordinates, and add: $3 \cdot 1 + 1 \cdot 2 = 5$. Now turn the whole picture a quarter turn counterclockwise. Both arrows get new coordinates, $\mathbf{a}$ becomes $(-1, 3)$ and $\mathbf{b}$ becomes $(-2, 1)$, and the recipe gives $(-1)(-2) + 3 \cdot 1 = 5$ again. The animation makes the turn gradually. The two products change the entire time, and their sum does not move.

That needs an explanation. Coordinates depend on where we happened to put the axes, so a recipe built out of coordinates has no obvious right to ignore them. This one behaves as if it responded only to the arrows themselves: how long they are and how they sit relative to each other. Our job is to find out what the 5 is measuring, and then to prove it.

I assume you know that a vector $\mathbf{v} = (v_1, v_2)$ is an arrow from the origin to the point $(v_1, v_2)$, that vectors add coordinate by coordinate and scale by multiplying each coordinate, and that the length of $\mathbf{v}$ is $|\mathbf{v}| = \sqrt{v_1^2 + v_2^2}$ by the Pythagorean theorem. The basis arrows are $\mathbf{i} = (1, 0)$ and $\mathbf{j} = (0, 1)$, and $(v_1, v_2) = v_1\mathbf{i} + v_2\mathbf{j}$.

The definition and its algebra

The inputs are two vectors and the output is a single number. Before asking what the number means, it pays to learn how it behaves.

Pause and try

With $\mathbf{a} = (3, 1)$ and $\mathbf{b} = (1, 2)$: (a) A quarter turn clockwise sends $(x, y)$ to $(y, -x)$. Turn both arrows this way and compute the dot product. (b) Compute $(2\mathbf{a}) \cdot \mathbf{b}$. (c) Compute $\mathbf{a} \cdot \mathbf{a}$ and compare it with the length of $\mathbf{a}$.

Show answerHide answer

(a) The arrows become $(1, -3)$ and $(2, -1)$, and $1 \cdot 2 + (-3)(-1) = 5$, unchanged again. (b) $2\mathbf{a} = (6, 2)$, so $(2\mathbf{a}) \cdot \mathbf{b} = 6 + 4 = 10$: doubling one arrow doubles the result. (c) $\mathbf{a} \cdot \mathbf{a} = 9 + 1 = 10$, and $|\mathbf{a}| = \sqrt{10}$. The dot product of a vector with itself is its length squared.

Parts (b) and (c) are instances of general rules, and all of them come straight from the definition.

Rules 2 and 3 are stated for the first vector, and rule 1 lets us swap the two, so they hold for the second vector as well: $\mathbf{c} \cdot (\mathbf{a} + \mathbf{b}) = \mathbf{c} \cdot \mathbf{a} + \mathbf{c} \cdot \mathbf{b}$ and $\mathbf{a} \cdot (k\mathbf{b}) = k\,(\mathbf{a} \cdot \mathbf{b})$. A product that is linear in each of its two inputs separately is called bilinear. In practice it means that brackets expand the way they do for numbers. One expansion will carry the main proof of this lesson:

$$ (\mathbf{a} - \mathbf{b}) \cdot (\mathbf{a} - \mathbf{b}) = \mathbf{a} \cdot \mathbf{a} - \mathbf{a} \cdot \mathbf{b} - \mathbf{b} \cdot \mathbf{a} + \mathbf{b} \cdot \mathbf{b} = |\mathbf{a}|^2 - 2\,\mathbf{a} \cdot \mathbf{b} + |\mathbf{b}|^2. $$

The easy case: one arrow lies along an axis

The meaning of the number is easiest to see when $\mathbf{b}$ points along the $x$ axis. Let $\mathbf{b} = (4, 0)$. Then $\mathbf{a} \cdot \mathbf{b} = 4a_1 + 0 \cdot a_2 = 4a_1$: the second coordinate of $\mathbf{a}$ is ignored, and the first is multiplied by the length of $\mathbf{b}$.

Physics has a use for exactly this. Suppose a cart rolls 4 meters east along a straight track while you pull it with a force $\mathbf{a}$, measured in newtons, aimed off to one side. Only the eastward part of your pull, $a_1$, moves the cart along the track, and the work you do is that part times the distance: $4a_1$ joules. Pull straight north and you do no work on the cart. Pull partly west and your work is negative, because you are holding it back.

Geometrically, $a_1$ is the shadow of $\mathbf{a}$ on the line of $\mathbf{b}$, if the light shines straight down onto that line. It is a signed length: positive when the shadow falls on the side $\mathbf{b}$ points to, negative on the other side.

Pause and try

Let $\mathbf{b} = (4, 0)$ and let $\mathbf{a}$ be an arrow of length 3 that you may aim anywhere. For which directions of $\mathbf{a}$ is $\mathbf{a} \cdot \mathbf{b}$ largest, zero, and most negative, and what are those three values?

Show answerHide answer

The dot product is 4 times the $x$ coordinate of $\mathbf{a}$, which ranges from $3$ to $-3$. It is largest, $12$, when $\mathbf{a} = (3, 0)$ points along $\mathbf{b}$. It is $0$ when $\mathbf{a} = (0, 3)$ or $(0, -3)$, at a right angle to $\mathbf{b}$. It is most negative, $-12$, when $\mathbf{a} = (-3, 0)$ points against $\mathbf{b}$.

The orange arrow $\mathbf{b} = (4, 0)$ is fixed and the blue arrow $\mathbf{a}$, of length 3, swings from $25^\circ$ to $155^\circ$. The thick band on the axis is the shadow of $\mathbf{a}$ on the line of $\mathbf{b}$. The dot product is the signed length of the shadow times 4: positive, zero at the right angle, then negative.
Monocurl source
# a wide board with the line of b along its bottom edge; the readout runs underneath
let Sb = Board([-0.36, -0.85, 0], 0.93, -3, 4, 0, 3)
let RY = -1.66
let Shadow = |deg| {
    let a = rot2([3, 0], deg)
    var col = LEAF
    if (a[0] < 0) {
        col = ROSE
    }
    var band = []
    if (abs(a[0]) > 0.02) {
        band = Seg(P(Sb, [0, 0]), P(Sb, [a[0], 0]), tint(col, 0.5), 13)
    }
    return [
        band,
        Vec(Sb, [0, 0], [4, 0], EMBER),
        Dash(P(Sb, a), P(Sb, [a[0], 0]), SLATE, 2.5),
        Vec(Sb, [0, 0], a, AZURE),
        Pt(P(Sb, [a[0], 0]), col, 0.09),
        center{[-1.5, RY, 0]} scale{1.15} color{col} Number(clean(a[0]), 2),
        center{[2.75, RY, 0]} scale{1.5} color{col} Number(clean(4 * a[0]), 2)
    ]
}
mesh ground = Ground(Sb)
mesh shadow = Shadow(deg: 25)
mesh label = [
    center{[-2.75, RY, 0]} Note("shadow of a", 0.74),
    center{[-0.42, RY, 0]} Math("\times\ |\mathbf{b}|", 0.9, EMBER),
    center{[0.98, RY, 0]} Math("=\ \mathbf{a} \cdot \mathbf{b}\ =", 0.95, INK),
    center{P(Sb, [3.5, 0.36])} Math("|\mathbf{b}| = 4", 0.8, EMBER)
]

slide "swing"
    play Wait(0.8)
    shadow.deg = 155
    play Lerp(5.5, [&shadow])
    play Wait(1.6)

Uses the shared definitions for this lesson's figures.

If $\theta$ is the angle from $\mathbf{b}$ up to $\mathbf{a}$, the shadow has signed length $|\mathbf{a}|\cos\theta$. That is what the cosine is: an arrow of length $L$ at angle $\theta$ from the positive $x$ axis has $x$ coordinate $L\cos\theta$. So in this special position

$$ \mathbf{a} \cdot \mathbf{b} = |\mathbf{a}|\cos\theta \cdot |\mathbf{b}|. $$

Now recall the opening experiment, where turning both arrows together left the dot product alone. If that is true in general, we could turn any pair of arrows until $\mathbf{b}$ lies along the positive $x$ axis, and the formula above would hold for every pair. So we have a conjecture: $\mathbf{a} \cdot \mathbf{b}$ is always the signed shadow of $\mathbf{a}$ on $\mathbf{b}$, times the length of $\mathbf{b}$. So far it rests on one numerical experiment. I want to do two things with it: see directly why a sum of coordinate products should compute a shadow on a tilted line, and then prove the statement.

Why the formula sees a shadow on a tilted line

Take a line through the origin that is not an axis, the one along $\mathbf{u} = (0.8, 0.6)$. I chose $\mathbf{u}$ to have length 1, since $0.8^2 + 0.6^2 = 1$, so that $\mathbf{u}$ only names a direction. How long is the shadow of $\mathbf{a} = (3, 1)$ on this line? The conjecture says $\mathbf{a} \cdot \mathbf{u} = 3 \cdot 0.8 + 1 \cdot 0.6 = 3$. Step through the figure to see where each term comes from.

  1. Drop a perpendicular from the tip of a (blue) to the line along u (orange). The shadow runs from the origin to its foot.
  2. Walk to the tip of a in two legs: 3 steps of i, then 1 step of j.
  3. One step of i casts a shadow of length 0.8 on the line, so the first leg casts 3 x 0.8 = 2.4.
  4. One step of j casts a shadow of length 0.6, and the second leg’s shadow starts where the first one ended.
  5. The two shadows together are the shadow of a: 2.4 + 0.6 = 3.0, which is 3 x 0.8 + 1 x 0.6.
The shadow of $\mathbf{a} = (3, 1)$ on the line along the unit vector $\mathbf{u} = (0.8, 0.6)$, found by walking to the tip of $\mathbf{a}$ in two legs. The leg $3\mathbf{i}$ casts a shadow of length $3 \times 0.8$, the leg $1\mathbf{j}$ continues it by $1 \times 0.6$, and the total is 3.
Monocurl source
let U = [0.8, 0.6]
let N = [-0.6, 0.8]
let Q = |v| P(Bd, v)
let Line2 = |y, tex, col, s = 0.9| center{[2.6, y, 0]} Math(tex, s, col)
let full = dp(VA, U)
mesh ground = Ground(Bd)
mesh rail = Cut(Bd, times(-2, U), times(7, U), SLATE, 2.5)
mesh shadows = []
mesh drops = [
    Dash(Q(VA), Q(times(full, U)), SLATE, 2.5),
    Corner(Bd, times(full, U), times(-1, U), times(-1, N), 0.22)
]
mesh legs = []
mesh uvec = Vec(Bd, [0, 0], U, EMBER)
mesh arrow = Vec(Bd, [0, 0], VA, AZURE)
mesh head = [center{[2.6, 1.55, 0]} Note("shadow of a", 0.76)]
mesh tally = [Line2(0.95, "?", INK)]

slide "Drop a perpendicular from the tip of a (blue) to the line along u (orange). The shadow runs from the origin to its foot."
    play Fade(0.5)

slide "Walk to the tip of a in two legs: 3 steps of i, then 1 step of j."
    legs = [
        Vec(Bd, [0, 0], [VA[0], 0], PLUM, 4),
        Vec(Bd, [VA[0], 0], VA, GOLD, 4)
    ]
    play Fade(0.6, [&legs])

slide "One step of i casts a shadow of length 0.8 on the line, so the first leg casts 3 x 0.8 = 2.4."
    tally = []
    play Fade(0.3, [&tally])
    shadows = [Seg(Q([0, 0]), Q(times(VA[0] * U[0], U)), tint(PLUM, 0.6), 12)]
    drops .= Dash(Q([VA[0], 0]), Q(times(VA[0] * U[0], U)), PLUM, 2.5)
    tally = [Line2(0.95, str_join([to_string(VA[0]), " \times ", to_string(U[0], 1), " = ", to_string(VA[0] * U[0], 1)]), PLUM)]
    play Fade(0.6, [&shadows, &drops, &tally])

slide "One step of j casts a shadow of length 0.6, and the second leg's shadow starts where the first one ended."
    shadows .= Seg(Q(times(VA[0] * U[0], U)), Q(times(full, U)), tint(GOLD, 0.75), 12)
    tally .= Line2(0.35, str_join(["+\ ", to_string(VA[1]), " \times ", to_string(U[1], 1), " = ", to_string(VA[1] * U[1], 1)]), GOLD)
    play Fade(0.6, [&shadows, &tally])

slide "The two shadows together are the shadow of a: 2.4 + 0.6 = 3.0, which is 3 x 0.8 + 1 x 0.6."
    tally .= Line2(-0.45, str_join(["=\ ", to_string(full, 1)]), LEAF, 1.2)
    play Fade(0.6, [&tally])

Uses the shared definitions for this lesson's figures.

Two facts about shadows are doing the work. The first is that shadows add. Walk from the origin to the tip of $\mathbf{a}$ along $3\mathbf{i}$ and then along $1\mathbf{j}$, and your shadow on the line moves from the origin to the foot of the perpendicular from $\mathbf{a}$, however you got there. So the shadow of $\mathbf{a}$ is the shadow of the first leg plus the shadow of the second, and a leg three times as long casts a shadow three times as long. Casting a shadow on a fixed line is a linear operation.

The second fact is about the basis arrows. How long is the shadow of $\mathbf{i}$ on the line along $\mathbf{u}$? Both are arrows of length 1, and the picture of the two of them is symmetric under the reflection that swaps them. So the shadow of $\mathbf{i}$ on $\mathbf{u}$ is as long as the shadow of $\mathbf{u}$ on $\mathbf{i}$. But the shadow of $\mathbf{u}$ on the $x$ axis is the first coordinate of $\mathbf{u}$, which is $0.8$. In the same way the shadow of $\mathbf{j}$ on $\mathbf{u}$ is $u_2 = 0.6$.

Put the two together: the shadow of $a_1\mathbf{i} + a_2\mathbf{j}$ on the line along a unit vector $\mathbf{u}$ is $a_1u_1 + a_2u_2$. The coordinates of $\mathbf{u}$ appear in the formula because they are the shadows of the basis arrows. For a vector $\mathbf{b}$ of any length in the direction of $\mathbf{u}$, we have $\mathbf{b} = |\mathbf{b}|\,\mathbf{u}$, and rule 3 gives $\mathbf{a} \cdot \mathbf{b} = |\mathbf{b}|\,(\mathbf{a} \cdot \mathbf{u})$: the shadow times the length of $\mathbf{b}$, as conjectured.

This is an explanation, and I would not yet call it a proof, because “shadows add” was read off a picture in which everything had convenient signs. The proof below takes a different road and leans on one theorem from trigonometry.

The theorem: lengths and the angle

First we need to say what the angle between two vectors is. For nonzero $\mathbf{a}$ and $\mathbf{b}$, the angle between them is the angle $\theta$ at the origin in the triangle with corners $\mathbf{0}$, $\mathbf{a}$ and $\mathbf{b}$, so $0^\circ \le \theta \le 180^\circ$. It is $0^\circ$ when $\mathbf{b}$ is a positive multiple of $\mathbf{a}$ and $180^\circ$ when it is a negative multiple; in those two cases the triangle is flat.

The triangle with corners at the origin, \mathbf{a} = (3, 1) and \mathbf{b} = (1, 2). Its third side is the vector \mathbf{a} - \mathbf{b} = (2, -1), drawn from the tip of \mathbf{b} to the tip of \mathbf{a}, and it lies opposite the angle \theta between the two arrows.
The triangle with corners at the origin, $\mathbf{a} = (3, 1)$ and $\mathbf{b} = (1, 2)$. Its third side is the vector $\mathbf{a} - \mathbf{b} = (2, -1)$, drawn from the tip of $\mathbf{b}$ to the tip of $\mathbf{a}$, and it lies opposite the angle $\theta$ between the two arrows.
Monocurl source
let Root = |name, v, col, y| center{[2.6, y, 0]} Math(str_join(["|", name, "| = \sqrt{", to_string(dp(v, v)), "}"]), 0.9, col)
mesh ground = Ground(Bd)
mesh tri = Region([P(Bd, [0, 0]), P(Bd, VA), P(Bd, VB)], tint(GOLD, 0.2))
mesh arc = ArcMark(Bd, 18.435, 63.435, 1.25)
mesh sides = [
    Vec(Bd, VB, VA, PLUM),
    Vec(Bd, [0, 0], VB, EMBER),
    Vec(Bd, [0, 0], VA, AZURE)
]
mesh theta = center{P(Bd, [0.58, 0.5])} Math("\theta", 0.85, INK)
mesh legend = [
    Root("\mathbf{a}", VA, AZURE, 1.2),
    Root("\mathbf{b}", VB, EMBER, 0.45),
    Root("\mathbf{a} - \mathbf{b}", sub2(VA, VB), PLUM, -0.3)
]

Uses the shared definitions for this lesson's figures.

The theorem we borrow is the law of cosines: in a triangle with sides of lengths $p$, $q$, $r$, where the angle between the sides $p$ and $q$ is $\theta$,

$$ r^2 = p^2 + q^2 - 2pq\cos\theta. $$

It is the Pythagorean theorem with a correction term for triangles whose angle is not $90^\circ$, and the linked lesson proves it by dropping an altitude and applying Pythagoras twice.

This is a complete proof, given the law of cosines. Its content is one comparison: the squared distance between the tips can be computed from coordinates, where bilinearity produces the term $-2\,\mathbf{a} \cdot \mathbf{b}$, or from the triangle, where trigonometry produces $-2|\mathbf{a}||\mathbf{b}|\cos\theta$.

The opening puzzle is now settled. Turning both arrows together changes neither length and does not change the angle between them, so the right side stays put, and the left side has to follow it. The shadow conjecture is settled too: $|\mathbf{a}|\cos\theta$ is the signed shadow of $\mathbf{a}$ on the line of $\mathbf{b}$, and it gets multiplied by $|\mathbf{b}|$.

Perpendicular, acute, obtuse

The sign of $\cos\theta$ sorts angles into three kinds, and lengths are positive, so the sign of the dot product does the same sorting without any trigonometry.

The zero vector has no direction and makes no angle with anything, but $\mathbf{0} \cdot \mathbf{b} = 0$ for every $\mathbf{b}$. The usual convention is to call two vectors orthogonal when their dot product is 0, which counts $\mathbf{0}$ as orthogonal to everything and agrees with “perpendicular” otherwise.

Pause and try

Describe all vectors $\mathbf{v} = (x, y)$ that are orthogonal to $\mathbf{a} = (3, 1)$. Where do they lie?

Show answerHide answer

The condition is $3x + y = 0$, so $y = -3x$ and $\mathbf{v} = (x, -3x) = -x\,(-1, 3)$. These are the multiples of $(-1, 3)$: a line through the origin. In general $(-a_2, a_1)$ is orthogonal to $(a_1, a_2)$, since $-a_1a_2 + a_2a_1 = 0$. It is $\mathbf{a}$ turned a quarter turn.

Each whole-number point \mathbf{v} is colored by the sign of \mathbf{a} \cdot \mathbf{v}, where \mathbf{a} = (3, 1) is the blue arrow. The points with dot product 0 lie on the dashed line through the origin perpendicular to \mathbf{a}; the green points are on the side \mathbf{a} points to and the red points are on the other side.
Each whole-number point $\mathbf{v}$ is colored by the sign of $\mathbf{a} \cdot \mathbf{v}$, where $\mathbf{a} = (3, 1)$ is the blue arrow. The points with dot product 0 lie on the dashed line through the origin perpendicular to $\mathbf{a}$; the green points are on the side $\mathbf{a}$ points to and the red points are on the other side.
Monocurl source
mesh ground = Ground(Hd)
mesh zero = dashed{[0.12, 0.09]} Cut(Hd, [2, -6], [-2, 6], INK, 2.5)
mesh dots = block {
    for (x in range(-3, 5)) {
        for (y in range(-1, 5)) {
            let v = dp(VA, [x, y])
            var col = LEAF
            if (v < 0) {
                col = ROSE
            }
            if (v == 0) {
                col = INK
            }
            . Pt(P(Hd, [x, y]), col, 0.09)
        }
    }
}
mesh arrow = Vec(Hd, [0, 0], VA, AZURE)
mesh legend = [
    center{[RX, 0.95, 0]} Math("\mathbf{a} \cdot \mathbf{v} > 0", 0.9, LEAF),
    center{[RX, 0.25, 0]} Math("\mathbf{a} \cdot \mathbf{v} = 0", 0.9, INK),
    center{[RX, -0.45, 0]} Math("\mathbf{a} \cdot \mathbf{v} < 0", 0.9, ROSE)
]

Uses the shared definitions for this lesson's figures.

The dashed line is the set of solutions of $3x + y = 0$. An equation like $3x + y = c$ says “the dot product of $(x, y)$ with $(3, 1)$ equals $c$”, and its solutions form a line perpendicular to $(3, 1)$. The coefficients of a linear equation are a vector, and that vector points straight across the line.

Projection: the shadow as a vector

So far the shadow has been a length. Often we want the shadow itself, as a vector, together with what is left over.

Let us find the shadow of $\mathbf{b} = (1, 2)$ on the line of $\mathbf{a} = (3, 1)$ without any angles. The shadow is some multiple $t\,\mathbf{a}$. What singles out the right $t$ is the perpendicular: the leftover $\mathbf{b} - t\,\mathbf{a}$ must be orthogonal to $\mathbf{a}$. The video slides $t$ and watches the dot product of the leftover with $\mathbf{a}$.

Looking for the multiple $t\,\mathbf{a}$ of $\mathbf{a} = (3, 1)$ closest to the tip of $\mathbf{b} = (1, 2)$. The purple arrow is the leftover $\mathbf{b} - t\,\mathbf{a}$. Its dot product with $\mathbf{a}$ is $5 - 10t$, which reaches 0 at $t = 0.5$, and there the leftover is perpendicular to $\mathbf{a}$.
Monocurl source
let Slide = |t| {
    let p = times(t, VA)
    let val = clean(dp(sub2(VB, p), VA))
    var col = INK
    if (abs(val) < 0.02) {
        col = LEAF
    }
    return [
        Seg(P(Bd, [0, 0]), P(Bd, p), tint(LEAF, 0.55), 12),
        Vec(Bd, [0, 0], VA, AZURE),
        Vec(Bd, p, VB, PLUM),
        Pt(P(Bd, p), INK, 0.09),
        center{[2.6, 0.95, 0]} scale{1.15} color{INK} Number(t, 2),
        center{[2.6, -0.95, 0]} scale{1.5} color{col} Number(val, 2)
    ]
}
mesh ground = Ground(Bd)
mesh rail = Cut(Bd, times(-1, VA), times(3, VA), SLATE, 2)
mesh bvec = Vec(Bd, [0, 0], VB, EMBER)
mesh slide_t = Slide(t: 1)
mesh label = [
    center{[2.6, 1.5, 0]} Math("t", 0.95, INK),
    center{[2.6, -0.2, 0]} Math("(\mathbf{b} - t\,\mathbf{a}) \cdot \mathbf{a}", 1.0, INK)
]
mesh corner = []

slide "slide"
    play Wait(0.8)
    slide_t.t = 0.1
    play Lerp(3, [&slide_t])
    slide_t.t = 0.5
    play Lerp(2, [&slide_t])
    corner = [Corner(Bd, [1.5, 0.5], unit2(VA), unit2([-1, 3]), 0.24)]
    play Fade(0.4, [&corner])
    play Wait(1.8)

Uses the shared definitions for this lesson's figures.

The condition can be solved for $t$ with the rules: $(\mathbf{b} - t\,\mathbf{a}) \cdot \mathbf{a} = \mathbf{b} \cdot \mathbf{a} - t\,(\mathbf{a} \cdot \mathbf{a}) = 5 - 10t$, which is zero for $t = \frac12$. The shadow is $\frac12(3, 1) = (1.5, 0.5)$ and the leftover is $(1, 2) - (1.5, 0.5) = (-0.5, 1.5)$. Nothing in that calculation was special to these numbers.

The proof used only the four rules, so it holds for vectors with any number of coordinates. The length of the projection agrees with the shadow from before: $|\mathbf{p}| = \dfrac{|\mathbf{v} \cdot \mathbf{w}|}{|\mathbf{w}|^2}\,|\mathbf{w}| = \dfrac{|\mathbf{v} \cdot \mathbf{w}|}{|\mathbf{w}|} = |\mathbf{v}|\,|\cos\theta|$.

A crate on a ramp that rises 3 m over 4 m. Gravity (black) pulls straight down with 98 N. Its projection onto the ramp direction (red) has length 58.8 N and points down the slope, and the dashed leftover is perpendicular to the ramp.
A crate on a ramp that rises 3 m over 4 m. Gravity (black) pulls straight down with 98 N. Its projection onto the ramp direction (red) has length 58.8 N and points down the slope, and the dashed leftover is perpendicular to the ramp.
Monocurl source
let L = [-3.4, -1.45]
let U = [0.8, 0.6]
let N = [-0.6, 0.8]
let M = 0.9
let FS = 0.016
let D = [4, 3]
let F = [0, -98]
let S3 = |v| [v[0], v[1], 0]
let T = add(L, times(M, D))
let c = add(L, times(0.5 * M, D))
let g0 = add(c, times(0.35, N))
let p = times(dp(F, D) / dp(D, D), D)
let ptip = add(g0, times(FS, p))
let ftip = add(g0, times(FS, F))
mesh ramp = Shape([S3(L), [T[0], L[1], 0], S3(T)], SLATE, 0.14, 2.5)
mesh disp = Tip(S3(L), S3(T), AZURE, 4.5, 0.2)
mesh crate = Shape([
    S3(add(c, times(-0.35, U))),
    S3(add(c, times(0.35, U))),
    S3(add(add(c, times(0.35, U)), times(0.7, N))),
    S3(add(add(c, times(-0.35, U)), times(0.7, N)))
], GOLD, 0.3, 2.5)
mesh forces = [
    Dash(S3(ptip), S3(ftip), SLATE, 2.5),
    Tip(S3(g0), S3(ftip), INK, 4.5, 0.2),
    Tip(S3(g0), S3(ptip), ROSE, 4.5, 0.2),
    Pt(S3(g0), INK, 0.07)
]
mesh sizes = [
    center{[-0.7, -1.78, 0]} Math(str_join([to_string(D[0]), "\text{ m}"]), 0.8, SLATE),
    center{[0.72, -0.1, 0]} Math(str_join([to_string(D[1]), "\text{ m}"]), 0.8, SLATE)
]
mesh legend = [
    center{[2.45, 1.35, 0]} Math("\mathbf{d} = (4,\ 3)\text{ m}", 0.85, AZURE),
    center{[2.45, 0.65, 0]} Math("\mathbf{F} = (0,\ -98)\text{ N}", 0.85, INK),
    center{[2.45, -0.15, 0]} Note("along the ramp", 0.74),
    center{[2.45, -0.65, 0]} Math(str_join([to_string(mag(p), 1), "\text{ N}"]), 0.95, ROSE)
]

Uses the shared definitions for this lesson's figures.

The Cauchy-Schwarz inequality

A shadow is never longer than the arrow that casts it. Written out with dot products, that plain remark becomes one of the most used inequalities in mathematics.

The blue arrow $\mathbf{a}$ of length 3 makes a full turn while $\mathbf{b} = (2, 1)$ stays fixed. The green band is the projection of $\mathbf{a}$ onto $\mathbf{b}$. The left bar is $|\mathbf{a} \cdot \mathbf{b}|$ and the right bar is $|\mathbf{a}||\mathbf{b}|$: the left never exceeds the right, and they are equal only when the arrows lie on one line.
Monocurl source
let Cd = Board([-1.15, 0, 0], 0.55, -4, 4, -3, 3)
let W = [2, 1]
let K = 0.38
let BASE = -1.2
let Bars = |deg| {
    let a = rot2([3, 0], deg)
    let t = dp(a, W) / dp(W, W)
    let p = times(t, W)
    let h = max(0.02, K * abs(dp(a, W)))
    var band = []
    if (abs(t) > 0.01) {
        band = Seg(P(Cd, [0, 0]), P(Cd, p), tint(LEAF, 0.55), 12)
    }
    return [
        band,
        Dash(P(Cd, a), P(Cd, p), SLATE, 2.5),
        Vec(Cd, [0, 0], a, AZURE),
        Vec(Cd, [0, 0], W, EMBER),
        Box([2.05, BASE + h / 2, 0], [0.55, h], LEAF, 0.45, 2.5),
        center{[2.05, BASE + h + 0.27, 0]} color{LEAF} Number(abs(dp(a, W)), 2)
    ]
}
let top = K * 3 * mag(W)
mesh ground = Ground(Cd)
mesh rail = Cut(Cd, times(-3, W), times(3, W), SLATE, 2)
mesh fixed = [
    Box([3.15, BASE + top / 2, 0], [0.55, top], SLATE, 0.3, 2.5),
    center{[3.15, BASE + top + 0.27, 0]} Math(to_string(3 * mag(W), 2), 0.8, SLATE),
    center{[2.05, BASE - 0.45, 0]} Math("|\mathbf{a} \cdot \mathbf{b}|", 0.92, LEAF),
    center{[3.15, BASE - 0.45, 0]} Math("|\mathbf{a}||\mathbf{b}|", 0.92, SLATE)
]
mesh bars = Bars(deg: 55)

slide "turn"
    play Wait(0.6)
    bars.deg = 415
    play Lerp(7.5, [&bars])
    play Wait(1.8)

Uses the shared definitions for this lesson's figures.

The middle line of the proof is the Pythagorean theorem for the right triangle with legs $\mathbf{p}$ and $\mathbf{r}$ and hypotenuse $\mathbf{a}$, derived here from bilinearity alone. In the plane the inequality also follows from $|\cos\theta| \le 1$, but the proof above never mentions an angle, and that matters. For vectors with three or more coordinates we have not defined an angle at all. Cauchy-Schwarz says that for nonzero vectors the number $\dfrac{\mathbf{a} \cdot \mathbf{b}}{|\mathbf{a}||\mathbf{b}|}$ lies between $-1$ and $1$, so it is the cosine of exactly one angle between $0^\circ$ and $180^\circ$, and that angle is taken as the definition of the angle between $\mathbf{a}$ and $\mathbf{b}$ in any dimension.

Where the analogy with multiplication breaks

The dot product expands like a product of numbers, which tempts people to treat it like one in other ways.

What was hard here was that the definition and the meaning live in different worlds. The definition is arithmetic on coordinates, the meaning is a shadow, and the bridge between them is a single computation of $|\mathbf{a} - \mathbf{b}|^2$ done two ways. The reusable idea is bilinearity: once a product expands like ordinary multiplication, lengths, angles, projections and the Pythagorean theorem can all be handled by algebra, in any number of dimensions. The dot product returns whenever a matrix meets a vector, because each coordinate of $A\mathbf{x}$ is a row of $A$ dotted with $\mathbf{x}$; the lesson on linear systems reads each equation of $A\mathbf{x} = \mathbf{b}$ that way.

Check yourself

Two nonzero vectors satisfy $\mathbf{a} \cdot \mathbf{b} = -6$. What can you conclude?

Problems

The first three are computations, the next three use the rules to explain something, and the last two are proofs.

Problem 1

Compute: (a) $(2, -1) \cdot (3, 4)$; (b) $(1, 2, 3) \cdot (4, -5, 6)$; (c) the length of $(2, -1)$. (d) Which pairs among $(2, -1)$, $(1, 2)$ and $(-4, 2)$ are orthogonal?

Show solutionHide solution

(a) $6 - 4 = 2$. (b) $4 - 10 + 18 = 12$. (c) $\sqrt{4 + 1} = \sqrt5$. (d) $(2, -1) \cdot (1, 2) = 2 - 2 = 0$ and $(1, 2) \cdot (-4, 2) = -4 + 4 = 0$, so those two pairs are orthogonal. $(2, -1) \cdot (-4, 2) = -8 - 2 = -10$, which is not zero. In fact $(-4, 2) = -2\,(2, -1)$, so these two point in opposite directions.

Problem 2

Find the angle between $(3, 4)$ and $(5, 12)$ to the nearest tenth of a degree.

Show solutionHide solution

The dot product is $15 + 48 = 63$. The lengths are $\sqrt{9 + 16} = 5$ and $\sqrt{25 + 144} = 13$. So $\cos\theta = \frac{63}{65} \approx 0.9692$ and $\theta \approx 14.3^\circ$. Check with directions: $(3, 4)$ is at $53.13^\circ$ from the $x$ axis and $(5, 12)$ at $67.38^\circ$, which differ by $14.25^\circ$.

Problem 3

Find the projection of $\mathbf{v} = (4, 7)$ onto $\mathbf{w} = (3, 4)$ and the leftover $\mathbf{r}$. Use them to find the distance from the point $(4, 7)$ to the line through the origin along $(3, 4)$.

Show solutionHide solution

$\mathbf{v} \cdot \mathbf{w} = 12 + 28 = 40$ and $\mathbf{w} \cdot \mathbf{w} = 25$, so $\mathbf{p} = \frac{40}{25}(3, 4) = (4.8, 6.4)$ and $\mathbf{r} = (4, 7) - (4.8, 6.4) = (-0.8, 0.6)$. Check: $\mathbf{r} \cdot \mathbf{w} = -2.4 + 2.4 = 0$. The point $\mathbf{p}$ is the foot of the perpendicular from $(4, 7)$ to the line, so the distance is $|\mathbf{r}| = \sqrt{0.64 + 0.36} = 1$.

Problem 4

For which $k$ are $(k, 2)$ and $(3, k - 5)$ orthogonal?

Show solutionHide solution

The dot product is $3k + 2(k - 5) = 5k - 10$, which is zero exactly for $k = 2$. Check: $(2, 2) \cdot (3, -3) = 6 - 6 = 0$.

Problem 5

A sled is pulled 20 m across level snow by a rope that makes an angle of $30^\circ$ with the ground. The tension in the rope is 50 N. How much work does the rope do on the sled? Answer once with the angle formula and once with coordinates.

Show solutionHide solution

With the angle: $W = |\mathbf{F}||\mathbf{d}|\cos\theta = 50 \times 20 \times \cos 30^\circ = 1000 \times 0.8660 \approx 866$ J. With coordinates: put the $x$ axis along the motion, so $\mathbf{d} = (20, 0)$ and $\mathbf{F} = (50\cos 30^\circ, 50\sin 30^\circ) \approx (43.30, 25)$. Then $\mathbf{F} \cdot \mathbf{d} \approx 43.30 \times 20 + 25 \times 0 = 866$ J. The upward 25 N of the pull does no work, because the sled does not move upward.

Problem 6

Let $\mathbf{a} = (3, 1)$. Describe the set of all $\mathbf{v} = (x, y)$ with $\mathbf{a} \cdot \mathbf{v} = 10$. Which point of that set is closest to the origin, and how far away is it?

Show solutionHide solution

The condition is $3x + y = 10$, a line. One point on it is $\mathbf{a}$ itself, since $\mathbf{a} \cdot \mathbf{a} = 10$. If $\mathbf{v}$ is any point on it, then $\mathbf{a} \cdot (\mathbf{v} - \mathbf{a}) = 10 - 10 = 0$, so $\mathbf{v} = \mathbf{a} + \mathbf{r}$ with $\mathbf{r}$ orthogonal to $\mathbf{a}$: the line passes through the tip of $\mathbf{a}$ and is perpendicular to $\mathbf{a}$. For such a point, $|\mathbf{v}|^2 = |\mathbf{a}|^2 + 2\,\mathbf{a} \cdot \mathbf{r} + |\mathbf{r}|^2 = 10 + |\mathbf{r}|^2$, which is smallest when $\mathbf{r} = \mathbf{0}$. So the closest point is $(3, 1)$, at distance $\sqrt{10} \approx 3.16$.

Problem 7

Prove that the diagonals of a parallelogram are perpendicular if and only if all four sides have the same length (the parallelogram is a rhombus).

Show solutionHide solution

Let the sides from one corner be the vectors $\mathbf{a}$ and $\mathbf{b}$. The diagonals are $\mathbf{a} + \mathbf{b}$ and $\mathbf{a} - \mathbf{b}$. Expanding by bilinearity and using symmetry,

$$ (\mathbf{a} + \mathbf{b}) \cdot (\mathbf{a} - \mathbf{b}) = \mathbf{a} \cdot \mathbf{a} - \mathbf{a} \cdot \mathbf{b} + \mathbf{b} \cdot \mathbf{a} - \mathbf{b} \cdot \mathbf{b} = |\mathbf{a}|^2 - |\mathbf{b}|^2. $$

The diagonals are perpendicular exactly when this is 0, that is, when $|\mathbf{a}| = |\mathbf{b}|$. Opposite sides of a parallelogram are equal, so $|\mathbf{a}| = |\mathbf{b}|$ says all four sides are equal. (A parallelogram with nonzero area has nonzero diagonals, so “dot product zero” does mean perpendicular here.)

Problem 8

Prove the triangle inequality $|\mathbf{a} + \mathbf{b}| \le |\mathbf{a}| + |\mathbf{b}|$ for vectors with any number of coordinates. When does equality hold?

Show solutionHide solution

Both sides are nonnegative, so it is enough to compare their squares. By bilinearity, $|\mathbf{a} + \mathbf{b}|^2 = |\mathbf{a}|^2 + 2\,\mathbf{a} \cdot \mathbf{b} + |\mathbf{b}|^2$. By Cauchy-Schwarz, $\mathbf{a} \cdot \mathbf{b} \le |\mathbf{a} \cdot \mathbf{b}| \le |\mathbf{a}||\mathbf{b}|$, so

$$ |\mathbf{a} + \mathbf{b}|^2 \le |\mathbf{a}|^2 + 2|\mathbf{a}||\mathbf{b}| + |\mathbf{b}|^2 = (|\mathbf{a}| + |\mathbf{b}|)^2. $$

Equality needs $\mathbf{a} \cdot \mathbf{b} = |\mathbf{a}||\mathbf{b}|$. By the equality case of Cauchy-Schwarz one vector is a multiple of the other, say $\mathbf{a} = t\,\mathbf{b}$ (or $\mathbf{b} = \mathbf{0}$), and then $\mathbf{a} \cdot \mathbf{b} = t|\mathbf{b}|^2$ must be nonnegative, so $t \ge 0$. Equality holds exactly when one vector is a nonnegative multiple of the other: the two arrows point the same way and walking along both is a straight path.

Shared definitions used by this lesson's figures
# house kit: prepended to every essay figure after the std imports

# palette. AZURE and EMBER are the two actors, LEAF marks an answer, ROSE a problem
let PAPER = [1, 1, 1, 1]
let INK = [0.08, 0.09, 0.11, 1]
let SLATE = [0.40, 0.44, 0.50, 1]
let MIST = [0.85, 0.86, 0.88, 1]
let AZURE = [0.13, 0.42, 0.68, 1]
let EMBER = [0.89, 0.42, 0.12, 1]
let LEAF = [0.13, 0.55, 0.36, 1]
let ROSE = [0.78, 0.22, 0.24, 1]
let PLUM = [0.47, 0.30, 0.66, 1]
let GOLD = [0.85, 0.62, 0.08, 1]

background = PAPER

# an opaque pastel of col; use for fills so overlaps do not darken
let tint = |col, amount = 0.18| [
    1 - (1 - col[0]) * amount,
    1 - (1 - col[1]) * amount,
    1 - (1 - col[2]) * amount,
    1
]

# text: Cap for labels, Note for quiet labels, Math for typeset maths
let Cap = |txt, size = 0.75, col = INK| color{col} Text(txt, size, "Charter")
let Note = |txt, size = 0.65| color{SLATE} Text(txt, size, "Charter")
let Math = |tex, size = 0.9, col = INK| color{col} Tex(tex, size)

# a paper rectangle to sit behind text that crosses lines
let Plate = |pos, size| center{pos} fill{PAPER} stroke{alpha{0} PAPER, 0} Rect(size)

# shapes with fill and stroke both set, so no stray black appears
let Pt = |pos, col = INK, r = 0.075| center{pos} fill{col} stroke{col, 1} Circle(r, 48)
let Ring = |pos, col = INK, r = 0.1, w = 3| center{pos} fill{PAPER} stroke{col, w} Circle(r, 48)
let Seg = |a, b, col = INK, w = 3| stroke{col, w} Line(a, b)
let Path = |pts, col = INK, w = 3| stroke{col, w} Polyline(pts)
let Region = |pts, col| fill{col} stroke{alpha{0} col, 0} Polygon(pts)
let Outline = |pts, col = INK, w = 3| fill{alpha{0} PAPER} stroke{col, w} Polygon(pts)
let Shape = |pts, col, amount = 0.18, w = 3| fill{tint(col, amount)} stroke{col, w} Polygon(pts)
let Disc = |pos, r, col, amount = 0.18, w = 3| center{pos} fill{tint(col, amount)} stroke{col, w} Circle(r, 96)
let Box = |pos, size, col, amount = 0.18, w = 3| center{pos} fill{tint(col, amount)} stroke{col, w} Rect(size)
# a line from a to b ending in a small solid arrowhead
let Tip = |a, b, col = INK, w = 3, head = 0.17| {
    let d = normalize([b[0] - a[0], b[1] - a[1], 0])
    let n = [-d[1], d[0], 0]
    let base = [b[0] - d[0] * head, b[1] - d[1] * head, 0]
    return [
        stroke{col, w} Line(a, base),
        fill{col} stroke{col, 1} Polygon([
            b,
            [base[0] + n[0] * head * 0.42, base[1] + n[1] * head * 0.42, 0],
            [base[0] - n[0] * head * 0.42, base[1] - n[1] * head * 0.42, 0]
        ])
    ]
}

# plots. a frame is [x0, x1, y0, y1, centre, width, height]: a data window and the scene box it fills
let Frame = |x0, x1, y0, y1, c = [0, 0, 0], w = 6.4, h = 3.6| [x0, x1, y0, y1, c, w, h]
let fx = |fr, x| fr[4][0] + (x - (fr[0] + fr[1]) / 2) * fr[5] / (fr[1] - fr[0])
let fy = |fr, y| fr[4][1] + (y - (fr[2] + fr[3]) / 2) * fr[6] / (fr[3] - fr[2])
let at = |fr, x, y| [fx(fr, x), fy(fr, y), 0]

# axes through the data origin (or the nearest edge of the window), with optional names
let Axes = |fr, x_name = "", y_name = "", col = SLATE| {
    let ox = clamp(fr[0], 0, fr[1])
    let oy = clamp(fr[2], 0, fr[3])
    var parts = [
        Tip(at(fr, fr[0], oy), [fx(fr, fr[1]) + 0.25, fy(fr, oy), 0], col, 2.5, 0.15),
        Tip(at(fr, ox, fr[2]), [fx(fr, ox), fy(fr, fr[3]) + 0.25, 0], col, 2.5, 0.15)
    ]
    if (x_name != "") {
        parts .= center{[fx(fr, fr[1]) + 0.5, fy(fr, oy), 0]} Math(x_name, 0.8, col)
    }
    if (y_name != "") {
        parts .= center{[fx(fr, ox), fy(fr, fr[3]) + 0.5, 0]} Math(y_name, 0.8, col)
    }
    return parts
}

# tick marks with numbers at the listed data values
let XTicks = |fr, xs, col = SLATE, places = 0| block {
    let oy = fy(fr, clamp(fr[2], 0, fr[3]))
    for (x in xs) {
        . Seg([fx(fr, x), oy - 0.07, 0], [fx(fr, x), oy + 0.07, 0], col, 2)
        . center{[fx(fr, x), oy - 0.33, 0]} Math(to_string(x, places), 0.72, col)
    }
}
let YTicks = |fr, ys, col = SLATE, places = 0| block {
    let ox = fx(fr, clamp(fr[0], 0, fr[1]))
    for (y in ys) {
        . Seg([ox - 0.07, fy(fr, y), 0], [ox + 0.07, fy(fr, y), 0], col, 2)
        . center{[ox - 0.36, fy(fr, y), 0]} Math(to_string(y, places), 0.72, col)
    }
}

# faint grid lines at the listed data values
let GridLines = |fr, xs, ys, col = MIST| block {
    for (x in xs) {
        . Seg(at(fr, x, fr[2]), at(fr, x, fr[3]), col, 1.5)
    }
    for (y in ys) {
        . Seg(at(fr, fr[0], y), at(fr, fr[1], y), col, 1.5)
    }
}

# the graph of f over [a, b], drawn in the frame. values are clamped to the window
let Graph = |fr, f, a, b, col = AZURE, w = 4, n = 160|
    stroke{col, w} Polyline(map(sample(a, b, n), |x| at(fr, x, clamp(fr[2], f(x), fr[3]))))

# the region between the graph of f and the x axis over [a, b]
let AreaUnder = |fr, f, a, b, col = AZURE, amount = 0.2, n = 80| {
    var pts = [at(fr, a, 0)]
    for (x in sample(a, b, n)) {
        pts .= at(fr, x, clamp(fr[2], f(x), fr[3]))
    }
    pts .= at(fr, b, 0)
    return fill{tint(col, amount)} stroke{alpha{0} col, 0} Polygon(pts)
}

# vectors are pairs [x, y]. a board is [o, u, x0, x1, y0, y1]: the scene point of the origin,
# the unit length, and the window of whole numbers that is drawn
let Board = |o, u, x0, x1, y0, y1| [o, u, x0, x1, y0, y1]
let P = |pl, v| [pl[0][0] + pl[1] * v[0], pl[0][1] + pl[1] * v[1], 0]
let add = |v, w| [v[0] + w[0], v[1] + w[1]]
let times = |c, v| [c * v[0], c * v[1]]
# matrices are [a, b, c, d] with rows (a b) and (c d), so the columns are (a, c) and (b, d)
let IDENT = [1, 0, 0, 1]
let apply = |m, v| [m[0] * v[0] + m[1] * v[1], m[2] * v[0] + m[3] * v[1]]
let blend = |m, n, t| [
    m[0] + (n[0] - m[0]) * t,
    m[1] + (n[1] - m[1]) * t,
    m[2] + (n[2] - m[2]) * t,
    m[3] + (n[3] - m[3]) * t
]
let GRIDC = tint(AZURE, 0.42)
# faint gridlines, both axes, and numbers along the bottom and left edges every `step`
let Ground = |pl, step = 1, mult = 1| {
    var out = []
    let lo = P(pl, [pl[2], pl[4]])
    let hi = P(pl, [pl[3], pl[5]])
    for (x in range(pl[2], pl[3] + 1)) {
        let sx = P(pl, [x, 0])[0]
        out .= Seg([sx, lo[1], 0], [sx, hi[1], 0], MIST, 1.5)
        if (mod(x, step) == 0) {
            out .= center{[sx, lo[1] - 0.27, 0]} Math(to_string(x * mult), 0.72, SLATE)
        }
    }
    for (y in range(pl[4], pl[5] + 1)) {
        let sy = P(pl, [0, y])[1]
        out .= Seg([lo[0], sy, 0], [hi[0], sy, 0], MIST, 1.5)
        if (mod(y, step) == 0) {
            out .= center{[lo[0] - 0.38, sy, 0]} Math(to_string(y * mult), 0.72, SLATE)
        }
    }
    if (pl[4] <= 0 and pl[5] >= 0) {
        out .= Seg(P(pl, [pl[2], 0]), P(pl, [pl[3], 0]), SLATE, 2.5)
    }
    if (pl[2] <= 0 and pl[3] >= 0) {
        out .= Seg(P(pl, [0, pl[4]]), P(pl, [0, pl[5]]), SLATE, 2.5)
    }
    return out
}
# the part of the segment from scene point p to q that lies in the box lo..hi
let ClipSeg = |p, q, lo, hi, col, w| {
    let d = [q[0] - p[0], q[1] - p[1]]
    var t0 = 0
    var t1 = 1
    for (k in [0, 1]) {
        if (abs(d[k]) < 0.00001) {
            if (p[k] < lo[k] - 0.0001 or p[k] > hi[k] + 0.0001) {
                t1 = -1
            }
        }
        if (abs(d[k]) >= 0.00001) {
            let ta = (lo[k] - p[k]) / d[k]
            let tb = (hi[k] - p[k]) / d[k]
            t0 = max(t0, min(ta, tb))
            t1 = min(t1, max(ta, tb))
        }
    }
    if (t1 - t0 < 0.002) {
        return []
    }
    return stroke{col, w} Line(
        [p[0] + t0 * d[0], p[1] + t0 * d[1], 0],
        [p[0] + t1 * d[0], p[1] + t1 * d[1], 0]
    )
}
# a segment between two points of the board, cut off at the window
let Cut = |pl, a, b, col = SLATE, w = 2|
    ClipSeg(P(pl, a), P(pl, b), P(pl, [pl[2], pl[4]]), P(pl, [pl[3], pl[5]]), col, w)
# the whole-number grid after the matrix m: lines along the first column in col_a, along the second in col_b
let Lines = |pl, m, col_a = GRIDC, col_b = GRIDC, n = 8| block {
    for (k in range(-n, n + 1)) {
        var w = 1.8
        if (k == 0) {
            w = 3
        }
        . Cut(pl, apply(m, [-n, k]), apply(m, [n, k]), col_a, w)
        . Cut(pl, apply(m, [k, -n]), apply(m, [k, n]), col_b, w)
    }
}
# an arrow between two points of the board
let Vec = |pl, a, b, col = INK, w = 4.5, head = 0.2| {
    let p = P(pl, a)
    let q = P(pl, b)
    if (sqrt((q[0] - p[0]) ^ 2 + (q[1] - p[1]) ^ 2) < head * 1.3) {
        return Seg(p, q, col, w)
    }
    return Tip(p, q, col, w, head)
}
# transform the grid: the matrix is t of the way from m0 to m1; the first basis arrow is blue, the second orange
let Warp = |pl, m0, m1, t| {
    let m = blend(m0, m1, t)
    return [
        Lines(pl, m),
        Vec(pl, [0, 0], [m[0], m[2]], AZURE),
        Vec(pl, [0, 0], [m[1], m[3]], EMBER)
    ]
}
let Dash = |a, b, col = SLATE, w = 2.5| stroke{col, w} dashed{[0.1, 0.08]} Line(a, b)
let clean = |x| {
    if (abs(x) < 0.005) {
        return 0
    }
    return x
}
# a 2x2 matrix readout with brackets, first column blue and second orange
let MatBox = |c, m, places = 1, s = 0.9| {
    let dx = 0.5
    let dy = 0.3
    let bx = 1.0
    let by = 0.62
    let lip = 0.13
    let q = |x, y| [c[0] + x, c[1] + y, 0]
    return [
        stroke{INK, 2.5} Polyline([q(-bx + lip, by), q(-bx, by), q(-bx, -by), q(-bx + lip, -by)]),
        stroke{INK, 2.5} Polyline([q(bx - lip, by), q(bx, by), q(bx, -by), q(bx - lip, -by)]),
        center{q(-dx, dy)} scale{s} color{AZURE} Number(clean(m[0]), places),
        center{q(dx, dy)} scale{s} color{EMBER} Number(clean(m[1]), places),
        center{q(-dx, -dy)} scale{s} color{AZURE} Number(clean(m[2]), places),
        center{q(dx, -dy)} scale{s} color{EMBER} Number(clean(m[3]), places)
    ]
}
let matmul = |m, n| [
    m[0] * n[0] + m[1] * n[2],
    m[0] * n[1] + m[1] * n[3],
    m[2] * n[0] + m[3] * n[2],
    m[2] * n[1] + m[3] * n[3]
]
# an F, so that turning and mirroring can be told apart
let FPTS = [[0.4, 0.3], [0.8, 0.3], [0.8, 1.1], [1.3, 1.1], [1.3, 1.5], [0.8, 1.5], [0.8, 1.9], [1.6, 1.9], [1.6, 2.3], [0.4, 2.3]]
let FShape = |pl, m, col = LEAF| Shape(map(FPTS, |p| P(pl, apply(m, p))), col, 0.22, 2.5)
let det = |m| m[0] * m[3] - m[1] * m[2]
# lesson 5: dot products
let dp = |v, w| v[0] * w[0] + v[1] * w[1]
let mag = |v| sqrt(v[0] * v[0] + v[1] * v[1])
let sub2 = |v, w| [v[0] - w[0], v[1] - w[1]]
let rot2 = |v, deg| {
    let t = deg * PI / 180
    return [v[0] * cos(t) - v[1] * sin(t), v[0] * sin(t) + v[1] * cos(t)]
}
let VA = [3, 1]
let VB = [1, 2]
let Bd = Board([-2.58, -1.03, 0], 0.72, -1, 5, -1, 4)
let Hd = Board([-1.26, -0.92, 0], 0.68, -3, 4, -1, 4)
let RX = 2.65
# a right-angle mark at the board point p, between the unit directions e and f
let Corner = |pl, p, e, f, s = 0.28| stroke{INK, 2.5} Polyline([
    P(pl, add(p, times(s, e))),
    P(pl, add(p, add(times(s, e), times(s, f)))),
    P(pl, add(p, times(s, f)))
])
let unit2 = |v| times(1 / mag(v), v)
# an arc about the board origin from angle d0 to d1 (degrees)
let ArcMark = |pl, d0, d1, r, col = INK, w = 2.5| Path(
    map(sample(d0, d1, 24), |d| P(pl, [r * cos(d * PI / 180), r * sin(d * PI / 180)])),
    col, w
)