Monocurl Essays
Series Download

Green's theorem: the inside read from the boundary

A walk around the edge of a region, adding up a vector field along the way, equals the integral of the field's curl over the inside. Here is why.

22 min·Early college·

AssumesDivergence and curl: reading a vector fieldDouble integrals: volume under a surfaceThe fundamental theorem of calculus, explained and proved

A disc approximated by finer and finer square tiles (those whose centers lie in the disc), in the wind $\mathbf{W} = (x^2 - 1, xy)$. Orange tiles have counterclockwise circulation and blue tiles clockwise. At every stage the circulations of the tiles add up to exactly the circulation around the outer edge of the tiling, and both approach the circulation around the circle.
Monocurl source
let CY = 0.3
let RR = 1.6
let fh = Frame(-1.7, 1.7, -1.4, 2.0, [-1.3, 0, 0], 3.8, 3.8)
let inside = |i, j, h| {
    let x = (i + 0.5) * h
    let y = (j + 0.5) * h - CY
    return x * x + y * y <= RR * RR
}
let Tiling = |h| block {
    let m = to_int(ceil(2 / h))
    for (i in range(-m, m)) {
        for (j in range(-m, m + to_int(ceil(0.5 / h)))) {
            if (inside(i, j, h)) {
                let yc = (j + 0.5) * h
                var col = tint(EMBER, 0.12 + 0.3 * abs(yc) / 1.9)
                if (yc < 0) { col = tint(AZURE, 0.12 + 0.3 * abs(yc) / 1.9) }
                . fill{col} stroke{[0.55, 0.58, 0.63, 1], 1} Polygon([at(fh, i * h, j * h), at(fh, i * h + h, j * h), at(fh, i * h + h, j * h + h), at(fh, i * h, j * h + h)])
            }
        }
    }
    for (i in range(-m, m)) {
        for (j in range(-m, m + to_int(ceil(0.5 / h)))) {
            if (inside(i, j, h)) {
                if (not inside(i, j - 1, h)) { . Seg(at(fh, i * h, j * h), at(fh, i * h + h, j * h), INK, 3.5) }
                if (not inside(i + 1, j, h)) { . Seg(at(fh, i * h + h, j * h), at(fh, i * h + h, j * h + h), INK, 3.5) }
                if (not inside(i, j + 1, h)) { . Seg(at(fh, i * h, j * h + h), at(fh, i * h + h, j * h + h), INK, 3.5) }
                if (not inside(i - 1, j, h)) { . Seg(at(fh, i * h, j * h), at(fh, i * h, j * h + h), INK, 3.5) }
            }
        }
    }
}
# [number of tiles, sum of the tile circulations, circulation around the outer edge]
let tally = |h| {
    let m = to_int(ceil(2 / h))
    var count = 0
    var tiles = 0
    var edge = 0
    for (i in range(-m, m)) {
        for (j in range(-m, m + to_int(ceil(0.5 / h)))) {
            if (inside(i, j, h)) {
                let c = circs(i * h, j * h, h, 6)
                count = count + 1
                tiles = tiles + c[0] + c[1] + c[2] + c[3]
                if (not inside(i, j - 1, h)) { edge = edge + c[0] }
                if (not inside(i + 1, j, h)) { edge = edge + c[1] }
                if (not inside(i, j + 1, h)) { edge = edge + c[2] }
                if (not inside(i - 1, j, h)) { edge = edge + c[3] }
            }
        }
    }
    return [count, tiles, edge]
}
let Read = |h| {
    let t = tally(h)
    return [
        center{[2.45, 1.05, 0]} Math(to_string(t[0]), 0.95, INK),
        center{[2.45, 0.0, 0]} Math(to_string(t[1], 3), 0.95, EMBER),
        center{[2.45, -1.05, 0]} Math(to_string(t[2], 3), 0.95, INK)
    ]
}
mesh tiles = Tiling(0.8)
mesh circle = [
    center{at(fh, 0, CY)} fill{alpha{0} PAPER} stroke{AZURE, 3} Circle(RR * 3.8 / 3.4, 128)
]
mesh labels = [
    center{[2.45, 1.5, 0]} Note("tiles", 0.72),
    center{[2.45, 0.45, 0]} Note("sum of tile circulations", 0.72),
    center{[2.45, -0.6, 0]} Note("around the outer edge", 0.72),
    center{[2.45, -1.75, 0]} Math(str_join(["\text{around the circle: } ", to_string(CY * PI * RR * RR, 3)]), 0.75, AZURE)
]
mesh read = Read(0.8)

slide "refine"
    play Wait(1.2)
    for (h in [0.4, 0.2, 0.1]) {
        read = []
        play Fade(0.3, [&read])
        tiles = Tiling(h)
        read = Read(h)
        play Fade(0.7, [&tiles, &read])
        play Wait(1.1)
    }
    play Wait(1.2)

Uses the shared definitions for this lesson's figures.

Surveyors once measured the area of a field on a map with a small instrument called a planimeter. You set its pointer on the outline of the field, trace once around, and read the area off a dial. The pointer never visits the interior. How can a trip around the boundary know how much is inside?

The answer is Green’s theorem, named for George Green, whose 1828 essay on electricity and magnetism contains a three-dimensional relative of it, and it is about much more than area. It says that a certain sum taken along the boundary of a region equals an integral over its inside. The reason, we will see, is a cancellation simple enough to check with two squares.

Adding up a field along a path

The previous lesson walked around a small square in a wind and added up the part of the wind that blows along the direction of travel, times the distance traveled. Wind at your back counts as positive, wind in your face as negative, and wind from the side does not count. We need the same sum for a path that curves.

Our wind is again $\mathbf{W}(x, y) = (x^2 - 1,\ xy)$, a vector field: a rule giving a vector $\bigl(P(x, y), Q(x, y)\bigr)$ at each point. Walk through it along the parabola $y = x^2$ from $(0, 0)$ to $(1, 1)$. Describe the walk by your position at time $t$:

$$ \mathbf{r}(t) = (t, t^2), \qquad 0 \le t \le 1. $$

During a short time $\Delta t$ you move by about $\mathbf{r}'(t)\,\Delta t$, where $\mathbf{r}'(t) = (1, 2t)$ is your velocity. The dot product of the wind with this small step, $\mathbf{W} \cdot \mathbf{r}'(t)\,\Delta t$, is the component of the wind along the step times the length of the step, which is exactly the quantity we want. Adding over the whole walk gives an ordinary integral in $t$. On the path the wind is $\mathbf{W}(t, t^2) = (t^2 - 1,\ t^3)$, so

$$ \int_0^1 (t^2 - 1,\ t^3) \cdot (1, 2t)\,dt = \int_0^1 \left(t^2 - 1 + 2t^4\right) dt = \frac13 - 1 + \frac25 = -\frac{4}{15}. $$

The total is negative: for most of this walk the wind is against you.

Walking the parabola $y = x^2$ from $(0, 0)$ to $(1, 1)$ through the wind $\mathbf{W}$. The gray arrow is the wind at the walker and the orange arrow is its component along the path. The plot accumulates that component times distance, and ends at $-\frac{4}{15} \approx -0.267$.
Monocurl source
let fl = Frame(-0.4, 1.4, -0.3, 1.3, [-1.45, 0, 0], 4.05, 3.6)
let fp = Frame(0, 1, -0.55, 0.1, [2.5, -0.5, 0], 1.9, 2.3)
let G = |t| t * t * t / 3 - t + 2 * t ^ 5 / 5
let Walk = |t| {
    let p = [t, t * t]
    let v = W(p)
    let n = sqrt(1 + 4 * t * t)
    let c = (v[0] + v[1] * 2 * t) / n
    return [
        Tip(at(fl, p[0], p[1]), at(fl, p[0] + 0.26 * v[0], p[1] + 0.26 * v[1]), SLATE, 3.5, 0.16),
        Tip(at(fl, p[0], p[1]), at(fl, p[0] + 0.26 * c / n, p[1] + 0.26 * c * 2 * t / n), EMBER, 4.5, 0.18),
        Pt(at(fl, p[0], p[1]), INK, 0.085),
        Path(map(sample(0, max(t, 0.02), 40), |s| at(fp, s, G(s))), EMBER, 3.5),
        Pt(at(fp, t, G(t)), EMBER, 0.08),
        center{[2.5, 1.3, 0]} scale{1.15} color{EMBER} Number(zero(G(t)), 3)
    ]
}
mesh arrows = Arrows(fl, W, sample(-0.3, 1.3, 9), sample(-0.2, 1.2, 8), 0.3)
mesh box = FieldBox(fl, [0, 1], [0, 1])
mesh road = [
    Path(map(sample(0, 1, 50), |t| at(fl, t, t * t)), AZURE, 4),
    Plate(at(fl, 1.08, 0.62), [0.95, 0.4]),
    center{at(fl, 1.08, 0.62)} Math("y = x^2", 0.8, AZURE)
]
mesh plot = block {
    for (y in [-0.5, -0.4, -0.3, -0.2, -0.1]) {
        . Seg(at(fp, 0, y), at(fp, 1, y), MIST, 1.5)
    }
    . Seg(at(fp, 0, 0), at(fp, 1, 0), SLATE, 2.5)
    . Seg(at(fp, 0, -0.55), at(fp, 0, 0.1), SLATE, 2.5)
    for (y in [0, -0.2, -0.4]) {
        . center{[fx(fp, 0) - 0.42, fy(fp, y), 0]} Math(to_string(y, 1), 0.7, SLATE)
    }
    . center{[fx(fp, 0), fy(fp, -0.55) - 0.27, 0]} Math("0", 0.7, SLATE)
    . center{[fx(fp, 1), fy(fp, -0.55) - 0.27, 0]} Math("1", 0.7, SLATE)
    . center{[fx(fp, 0.5), fy(fp, -0.55) - 0.27, 0]} Math("t", 0.8, SLATE)
    . center{[2.5, 1.75, 0]} Note("total so far", 0.72)
}
mesh walk = Walk(t: 0)

slide "walk"
    play Wait(0.8)
    walk.t = 1
    play Lerp(6, [&walk], linear)
    play Wait(1.8)

Uses the shared definitions for this lesson's figures.

The notation $P\,dx + Q\,dy$ records the recipe: replace $dx$ by $x'(t)\,dt$ and $dy$ by $y'(t)\,dt$. Two facts follow from the substitution rule for integrals, and we use them freely. Tracing the same curve in the same direction at a different speed gives the same value, so the integral belongs to the curve and its direction, not to the timetable. Tracing it in the opposite direction changes the sign.

Pause and try

Walk from $(0, 0)$ to $(1, 1)$ along the straight line $y = x$ instead, with $\mathbf{r}(t) = (t, t)$. Compute the line integral of $\mathbf{W}$. Is it the same as along the parabola?

Show answerHide answer

Here $\mathbf{r}'(t) = (1, 1)$ and $\mathbf{W}(t, t) = (t^2 - 1, t^2)$, so the integral is $\int_0^1 (2t^2 - 1)\,dt = \frac23 - 1 = -\frac13$. Along the parabola it was $-\frac{4}{15}$. The two paths join the same points and give different totals: for this wind, the route matters.

That difference is itself a circulation. Go out along the parabola and come back along the line. The return trip is the line walked backwards, worth $+\frac13$, so the round trip totals

$$ -\frac{4}{15} + \frac13 = \frac{1}{15}. $$

Keep this number in mind. We will meet it again, computed without setting foot on the boundary.

Two squares that share a side

Now the cancellation. Take two unit squares side by side, $0 \le x \le 1$ and $1 \le x \le 2$, both with $0 \le y \le 1$, and compute the circulation of $\mathbf{W}$ counterclockwise around each.

  1. Two unit squares in the wind, each to be walked counterclockwise.
  2. Left square: the four sides contribute -0.667, 0.5, 0.667 and 0. Its circulation is 0.5.
  3. Right square: 1.333, 1, -1.333 and -0.5. Its circulation is also 0.5.
  4. Join them. The shared side counted +0.5 for one square and -0.5 for the other. What remains is the walk around the rectangle: 0.5 + 0.5 = 1.
The circulation of $\mathbf{W}$ around two unit squares, each walked counterclockwise, with the contribution of every side. The shared side is walked upward for the left square and downward for the right one, so its two contributions cancel when the squares are joined.
Monocurl source
let OX = -1.2
let CYS = 0.05
let ROWS = [1.0, 0.5, 0.0, -0.5]
let COLS = [2.62, 3.3]
let Sq = |a, cx, inner| block {
    let c = [[cx - 1, CYS - 1], [cx + 1, CYS - 1], [cx + 1, CYS + 1], [cx - 1, CYS + 1]]
    . Outline(map(c, |p| [p[0], p[1], 0]), INK, 3.5)
    . Head([cx, CYS - 1, 0], [1, 0], INK)
    . Head([cx, CYS + 1, 0], [-1, 0], INK)
    if (inner or a == 1) { . Head([cx + 1, CYS, 0], [0, 1], INK) }
    if (inner or a == 0) { . Head([cx - 1, CYS, 0], [0, -1], INK) }
    . center{[cx, CYS + 1.42, 0]} Cap(["left square", "right square"][a], 0.74, SLATE)
    for (k in [0, 1]) {
        let x = cx - 1 + 2 * k
        . Seg([x, CYS - 1, 0], [x, CYS - 1.1, 0], SLATE, 2)
        . center{[x, CYS - 1.34, 0]} Math(to_string(a + k), 0.72, SLATE)
    }
    . center{[cx, CYS - 1.34, 0]} Math(["0 \le x \le 1", "1 \le x \le 2"][a], 0.72, SLATE)
}
let Vals = |a, cx, shared| block {
    let v = circs(a, 0, 1)
    . center{[cx, CYS - 0.7, 0]} Math(to_string(zero(v[0]), 3), 0.74, INK)
    . center{[cx, CYS + 0.7, 0]} Math(to_string(zero(v[2]), 3), 0.74, INK)
    if (shared or a == 1) {
        . center{[cx + 0.52, CYS, 0]} Math(to_string(zero(v[1]), 3), 0.74, INK)
    }
    if (shared or a == 0) {
        . center{[cx - 0.52, CYS, 0]} Math(to_string(zero(v[3]), 3), 0.74, INK)
    }
}
# one column of the ledger: the four sides in the order bottom, right, top, left, then their total
let Column = |a| block {
    let v = circs(a, 0, 1)
    for (i in range(0, 4)) {
        . center{[COLS[a], ROWS[i], 0]} Math(to_string(zero(v[i]), 3), 0.72, INK)
    }
    . center{[COLS[a], -1.05, 0]} Math(to_string(v[0] + v[1] + v[2] + v[3], 3), 0.76, EMBER)
}
let Pair = |g| [Sq(0, OX - 1 - g, g > 0.02), Sq(1, OX + 1 + g, g > 0.02)]
mesh ledger = block {
    . center{[COLS[0], 1.5, 0]} Cap("left", 0.72, SLATE)
    . center{[COLS[1], 1.5, 0]} Cap("right", 0.72, SLATE)
    let names = ["bottom", "right", "top", "left"]
    for (i in range(0, 4)) {
        . center{[1.78, ROWS[i], 0]} Note(names[i], 0.72)
        . Seg([1.35, ROWS[i] - 0.25, 0], [3.65, ROWS[i] - 0.25, 0], MIST, 1.5)
    }
    . Seg([1.35, 1.25, 0], [3.65, 1.25, 0], SLATE, 2)
    . Seg([1.35, -1.33, 0], [3.65, -1.33, 0], SLATE, 2)
    . center{[1.78, -1.05, 0]} Cap("total", 0.72, EMBER)
    . center{[1.78, -1.65, 0]} Cap("joined", 0.72, LEAF)
}
mesh pair = Pair(g: 0.2)
mesh left = []
mesh right = []
mesh join = []

slide "Two unit squares in the wind, each to be walked counterclockwise."
    play Fade(0.6)

slide "Left square: the four sides contribute -0.667, 0.5, 0.667 and 0. Its circulation is 0.5."
    left = [Vals(0, OX - 1.2, 1), Column(0)]
    play Fade(0.6, [&left])

slide "Right square: 1.333, 1, -1.333 and -0.5. Its circulation is also 0.5."
    right = [Vals(1, OX + 1.2, 1), Column(1)]
    play Fade(0.6, [&right])

slide "Join them. The shared side counted +0.5 for one square and -0.5 for the other. What remains is the walk around the rectangle: 0.5 + 0.5 = 1."
    left = []
    right = []
    play Fade(0.4, [&left, &right])
    pair.g = 0
    play Lerp(1.2, [&pair])
    left = [Vals(0, OX - 1, 0), Column(0)]
    right = [Vals(1, OX + 1, 0), Column(1)]
    join = [
        Seg([OX, CYS - 1, 0], [OX, CYS + 1, 0], MIST, 5),
        Seg([COLS[0] - 0.3, ROWS[1], 0], [COLS[0] + 0.3, ROWS[1], 0], ROSE, 2.5),
        Seg([COLS[1] - 0.36, ROWS[3], 0], [COLS[1] + 0.36, ROWS[3], 0], ROSE, 2.5),
        center{[(COLS[0] + COLS[1]) / 2, -1.65, 0]} Math(to_string(sum(circs(0, 0, 1)) + sum(circs(1, 0, 1)), 3), 0.8, LEAF)
    ]
    play Fade(0.6, [&left, &right, &join])

Uses the shared definitions for this lesson's figures.

For the left square: along the bottom, heading east, $\int_0^1 (x^2 - 1)\,dx = -\frac23$. Up the right side, where $x = 1$, the northward wind is $Q = y$, giving $\int_0^1 y\,dy = \frac12$. Back along the top, heading west, $+\frac23$. Down the left side, where $x = 0$ and $Q = 0$, nothing. Total: $\frac12$. For the right square the four sides give $\frac43$, $1$, $-\frac43$ and $-\frac12$, and the total is again $\frac12$.

Look at the side they share, the segment $x = 1$. The left square walks it upward and collects $+\frac12$. The right square walks the same segment downward and collects $-\frac12$. If we add the two circulations, those contributions cancel, and the six sides that remain are the outline of the $2 \times 1$ rectangle, walked counterclockwise. So

$$ \text{circulation around the rectangle} = \tfrac12 + \tfrac12 = 1, $$

and nothing about this depended on the wind. Any side shared by two tiles is walked in opposite directions by its two neighbors.

Pause and try

The previous lesson showed that the circulation around a small square, divided by its area, approaches the curl $Q_x - P_y$, which for $\mathbf{W}$ is $y$. That suggests circulation is curl added up over area. Test the suggestion on the rectangle $0 \le x \le 2$, $0 \le y \le 1$: compute $\iint y\,dA$ over it.

Show answerHide answer

$\int_0^2 \int_0^1 y\,dy\,dx = \int_0^2 \frac12\,dx = 1$. It equals the circulation around the rectangle exactly, and over each unit square alone it gives $\frac12$, matching each square’s circulation.

From tiles to a region

The same cancellation works for any number of tiles. Cut a region into small squares and add up the circulations around all of them. Every interior side is shared by two tiles and drops out, and what survives is the circulation around the outer edge of the whole tiling. The animation at the top shows this for a disc: at each stage the sum over the tiles and the walk around the outer edge agree to every digit.

On the other side of the ledger, the circulation around each small tile is about its curl times its area. Adding these over the tiles is a Riemann sum, and as the tiles shrink it becomes the double integral of the curl. So we expect

$$ \text{circulation around the boundary} = \iint_{\text{inside}} \operatorname{curl} \mathbf{F}\,dA. $$

The round trip of the first section is a test. Its inside is the region $D$ between $y = x^2$ and $y = x$, and

$$ \iint_D y\,dA = \int_0^1 \int_{x^2}^{x} y\,dy\,dx = \int_0^1 \frac{x^2 - x^4}{2}\,dx = \frac12\left(\frac13 - \frac15\right) = \frac{1}{15}, $$

the number we found by walking the boundary.

The round trip from the first section: out along the parabola and back along the line, counterclockwise around the green region D. The two line integrals add to \frac{1}{15}, and so does the double integral of the curl of \mathbf{W} over D.
The round trip from the first section: out along the parabola and back along the line, counterclockwise around the green region $D$. The two line integrals add to $\frac{1}{15}$, and so does the double integral of the curl of $\mathbf{W}$ over $D$.
Monocurl source
let fl = Frame(-0.15, 1.15, -0.15, 1.15, [-1.55, 0, 0], 3.7, 3.7)
let out = line_int(W, |t| [t, t * t], |t| [1, 2 * t], 0, 1)
let back = -line_int(W, |t| [t, t], |t| [1, 1], 0, 1)
let inside = integ(|x| integ(|y| y, x * x, x, 40), 0, 1, 40)
mesh region = block {
    var pts = []
    for (x in sample(0, 1, 40)) { pts .= at(fl, x, x * x) }
    . Region(pts, tint(LEAF, 0.28))
}
mesh arrows = Arrows(fl, W, sample(-0.05, 1.05, 7), sample(-0.05, 1.05, 7), 0.3)
mesh box = FieldBox(fl, [0, 1], [0, 1])
mesh loop = [
    Path(map(sample(0, 1, 50), |t| at(fl, t, t * t)), AZURE, 4),
    Seg(at(fl, 1, 1), at(fl, 0, 0), EMBER, 4),
    Head(at(fl, 0.6, 0.36), normalize([1, 1.2, 0]), AZURE, 0.24),
    Head(at(fl, 0.45, 0.45), normalize([-1, -1, 0]), EMBER, 0.24),
    center{at(fl, 0.62, 0.5)} Math("D", 0.85, LEAF)
]
mesh ledger = [
    center{[2.25, 1.6, 0]} Note("out along the parabola", 0.72),
    center{[2.25, 1.15, 0]} Math(to_string(out, 4), 0.9, AZURE),
    center{[2.25, 0.5, 0]} Note("back along the line", 0.72),
    center{[2.25, 0.05, 0]} Math(to_string(back, 4), 0.9, EMBER),
    Seg([1.35, -0.3, 0], [3.15, -0.3, 0], SLATE, 2),
    center{[2.25, -0.65, 0]} Math(str_join(["\text{round trip } ", to_string(out + back, 4)]), 0.85, INK),
    center{[2.25, -1.4, 0]} Math(str_join(["\iint_D y\,dA \approx ", to_string(inside, 4)]), 0.85, LEAF)
]

Uses the shared definitions for this lesson's figures.

Green’s theorem

To state the result exactly we need names for the kind of boundary allowed. A closed curve is simple if it does not cross itself. It is piecewise smooth if it consists of finitely many pieces, each traced by a function $\mathbf{r}(t)$ with a continuous derivative. A simple closed curve is positively oriented if it is traced counterclockwise, so that the region it encloses is on your left as you walk.

Each hypothesis has a job. “Simple” and “closed” make sure there is one inside (that a simple closed curve has exactly one inside is the Jordan curve theorem, which we take for granted). “Positively oriented” fixes the sign, since walking the other way reverses the left side. “Piecewise smooth” allows corners, as on a rectangle, while keeping the line integral defined. And $\mathbf{F}$ must be continuously differentiable on all of $D$, the inside included, so that the curl exists and is continuous at every point being integrated over. The last section shows what goes wrong without it.

Proof for a rectangle

For a rectangle the theorem can be proved completely, and the proof shows that the cancellation of interior sides is the fundamental theorem of calculus in disguise.

The proof for a rectangle. Along a horizontal strip at height y, the integral of Q_x from x = a to x = b is the change in Q: its value at the right end minus its value at the left end. Adding up the strips leaves Q on the right side minus Q on the left side, which is what the two vertical sides contribute to the circulation.
The proof for a rectangle. Along a horizontal strip at height $y$, the integral of $Q_x$ from $x = a$ to $x = b$ is the change in $Q$: its value at the right end minus its value at the left end. Adding up the strips leaves $Q$ on the right side minus $Q$ on the left side, which is what the two vertical sides contribute to the circulation.
Monocurl source
let L = -1.7
let R = 1.9
let B = -1.35
let T = 1.45
let YS = 0.25
mesh strip = [
    Region([[L, YS - 0.13, 0], [R, YS - 0.13, 0], [R, YS + 0.13, 0], [L, YS + 0.13, 0]], tint(EMBER, 0.3)),
    Tip([L + 0.25, YS, 0], [R - 0.25, YS, 0], EMBER, 3, 0.18)
]
mesh rect = [
    Outline([[L, B, 0], [R, B, 0], [R, T, 0], [L, T, 0]], INK, 3.5),
    Head([(L + R) / 2, B, 0], [1, 0], INK),
    Head([R, -0.6, 0], [0, 1], INK),
    Head([(L + R) / 2, T, 0], [-1, 0], INK),
    Head([L, -0.6, 0], [0, -1], INK),
    Pt([L, YS, 0], EMBER, 0.08),
    Pt([R, YS, 0], EMBER, 0.08)
]
mesh labels = [
    center{[L, B - 0.32, 0]} Math("a", 0.8, SLATE),
    center{[R, B - 0.32, 0]} Math("b", 0.8, SLATE),
    center{[L - 0.3, B, 0]} Math("c", 0.8, SLATE),
    center{[L - 0.3, T, 0]} Math("d", 0.8, SLATE),
    center{[L - 0.3, YS + 0.3, 0]} Math("y", 0.8, EMBER),
    center{[L - 1.2, YS, 0]} Math("Q(a, y)", 0.78, EMBER),
    center{[R + 0.75, YS, 0]} Math("Q(b, y)", 0.78, EMBER),
    center{[(L + R) / 2, YS + 0.55, 0]} Math("\int_a^b Q_x(x, y)\,dx = Q(b, y) - Q(a, y)", 0.78, INK)
]

Uses the shared definitions for this lesson's figures.

Compare with one variable. The fundamental theorem says that the integral of a derivative $F'$ over an interval equals $F(b) - F(a)$: a quantity computed at the boundary of the interval, which is its two endpoints, one counted with a plus sign and one with a minus. Green’s theorem says the same thing one dimension up, with the curl as the derivative and the oriented boundary curve as the endpoints.

General regions: a sketch

For other regions the tiling argument now gives more than we might have hoped. Let $D'$ be any region made of grid squares, such as the staircase shapes in the opening animation. For each tile, Green’s theorem holds exactly, by the proof just given. Add over the tiles. On the left, interior sides cancel and the outer edge remains. On the right, double integrals over the tiles add up to the double integral over $D'$. So Green’s theorem is proved for every region built from squares, with its boundary understood as the sides that did not cancel, each walked with its tile on the left.

What is still missing is the passage from a staircase region to a region with a curved boundary. As the grid is refined, the double integral over the staircase region does approach the double integral over $D$, because the sliver between the staircase edge and the curve has area tending to 0 and the curl is bounded. The line integrals need more care: the staircase edge does not approach the curve in length (a staircase from one corner of a square to the opposite corner has length 2 however fine its steps, while the diagonal has length $\sqrt2$), so it is not obvious that line integrals along it approach the line integral along $C$. They do, but showing it takes an argument we have not given. Textbooks close the gap by a different route: they prove the theorem directly for a region between two graphs, by the same fundamental-theorem computation as for the rectangle with curves as limits, and then cut a general region into pieces of that kind, with the cuts cancelling as shared sides do. We will use the theorem in the generality stated.

Area from a walk around the boundary

Now the planimeter. If a field has curl equal to 1 at every point, the right side of Green’s theorem is $\iint_D 1\,dA$, the area of $D$.

Pause and try

Find a vector field $(P, Q)$ whose curl $Q_x - P_y$ is 1 everywhere. Then find a different one.

Show answerHide answer

$(0, x)$ works: $Q_x = 1$, $P_y = 0$. So does $(-y, 0)$: $Q_x = 0$, $P_y = -1$. So does their average $\left(-\frac{y}{2}, \frac{x}{2}\right)$, half of the rigid rotation field, and there are many others.

The third form has a picture. For a short step from $\mathbf{r} = (x, y)$ to $\mathbf{r} + \Delta\mathbf{r}$, the quantity $\frac12(x\,\Delta y - y\,\Delta x)$ is half of a determinant, which is the signed area of the thin triangle with corners at the origin, $\mathbf{r}$ and $\mathbf{r} + \Delta\mathbf{r}$: positive if the step goes counterclockwise as seen from the origin, negative if clockwise.

The formula $\frac12 \oint x\,dy - y\,dx$ as a sweep. A line from the origin $O$ to a point moving around the curve sweeps out thin triangles. On the far side of the curve they count as positive and are painted in. On the near side they count as negative and remove the part painted outside the curve. What is left is the inside.
Monocurl source
let O = [-3.1, -1.65, 0]
let C0 = [0.35, 0.4, 0]
let A = 1.75
let B = 1.1
let N = 150
let pt = |s| [C0[0] + A * cos(s), C0[1] + B * sin(s), 0]
let bearing = |s| arctan2(pt(s)[1] - O[1], pt(s)[0] - O[0])
# start where the far side begins: the point of the curve with the smallest bearing from O
var best = 0
for (k in range(0, 720)) {
    if (bearing(TAU * k / 720) < bearing(best)) { best = TAU * k / 720 }
}
let S0 = best
let tri = |k| {
    let p = pt(S0 + TAU * k / N)
    let q = pt(S0 + TAU * (k + 1) / N)
    return 0.5 * ((p[0] - O[0]) * (q[1] - O[1]) - (p[1] - O[1]) * (q[0] - O[0]))
}
let Sweep = |u| block {
    var total = 0
    for (k in range(0, N)) {
        if (k < u * N - 0.001) {
            let a = tri(k)
            total = total + a
            var col = tint(EMBER, 0.45)
            if (a < 0) { col = PAPER }
            . fill{col} stroke{col, 1} Polygon([O, pt(S0 + TAU * k / N), pt(S0 + TAU * (k + 1) / N)])
        }
    }
    let tip = pt(S0 + TAU * u)
    . Seg(O, tip, SLATE, 2.5)
    . Pt(tip, INK, 0.085)
    . center{[2.9, 1.2, 0]} scale{1.15} color{EMBER} Number(zero(total), 2)
}
mesh sweep = Sweep(u: 0)
mesh curve = [
    Path(map(sample(0, TAU, 120), |s| pt(s)), AZURE, 4),
    Head(pt(1.3), normalize([-A * sin(1.3), B * cos(1.3), 0]), AZURE, 0.24),
    Pt(O, INK, 0.07),
    center{[O[0] - 0.05, O[1] - 0.3, 0]} Math("O", 0.85, INK),
    center{[2.9, 1.65, 0]} Note("signed area swept", 0.72),
    center{[2.9, -1.2, 0]} Note("area inside", 0.72),
    center{[2.9, -1.65, 0]} Math(to_string(PI * A * B, 2), 0.9, AZURE)
]

slide "sweep"
    play Wait(0.8)
    sweep.u = 1
    play Lerp(7, [&sweep], linear)
    play Wait(2)

Uses the shared definitions for this lesson's figures.

A planimeter is a mechanical version of this. Its measuring wheel can roll only in one direction, so as the pointer moves, the wheel records the component of the pointer’s motion along a direction that depends on where the pointer is. That is a line integral of a vector field, and the linkage is built so that the field has constant curl. Green’s theorem then says the dial reads a fixed multiple of the area.

Where the theorem stops

The hypothesis on $\mathbf{F}$ matters too. The whirlpool field of the previous lesson,

$$ \mathbf{V}(x, y) = \left(\frac{-y}{x^2 + y^2},\ \frac{x}{x^2 + y^2}\right), $$

has curl 0 at every point where it is defined. If Green’s theorem applied to the unit circle, the circulation around it would be $\iint 0\,dA = 0$. But on the unit circle $\mathbf{V}$ is the unit vector pointing along the direction of travel, so the circulation is the length of the circle, $2\pi$.

The whirlpool \mathbf{V}, which has zero curl wherever it is defined and is undefined at the origin. Around the orange circle, which encloses the origin, its circulation is 2\pi. Around the green circle, which does not, its circulation is 0, as Green's theorem requires.
The whirlpool $\mathbf{V}$, which has zero curl wherever it is defined and is undefined at the origin. Around the orange circle, which encloses the origin, its circulation is $2\pi$. Around the green circle, which does not, its circulation is 0, as Green’s theorem requires.
Monocurl source
let fv = Frame(-2.2, 2.2, -1.2, 1.2, [0.1, 0.15, 0], 6.6, 3.6)
let V = |p| [-p[1] / (p[0] * p[0] + p[1] * p[1]), p[0] / (p[0] * p[0] + p[1] * p[1])]
let around = |c, rho| line_int(V, |t| [c[0] + rho * cos(t), c[1] + rho * sin(t)], |t| [-rho * sin(t), rho * cos(t)], 0, TAU)
mesh arrows = Arrows(fv, |p| {
    if (p[0] * p[0] + p[1] * p[1] < 0.03) { return [0, 0] }
    if (p[0] < -0.8 and p[0] > -1.95 and p[1] > 0.65) { return [0, 0] }
    if (p[0] > 0.9 and p[0] < 2.05 and p[1] < -0.3 and p[1] > -0.8) { return [0, 0] }
    return V(p)
}, sample(-2, 2, 17), sample(-1, 1, 9), 0.3)
mesh box = FieldBox(fv, [-2, -1, 0, 1, 2], [-1, 0, 1])
mesh loops = [
    center{at(fv, 0, 0)} fill{alpha{0} PAPER} stroke{EMBER, 4} Circle(0.8 * 1.5, 96),
    Head(at(fv, 0, 0.8), [-1, 0], EMBER, 0.24),
    center{at(fv, 1.45, 0.3)} fill{alpha{0} PAPER} stroke{LEAF, 4} Circle(0.5 * 1.5, 96),
    Head(at(fv, 1.45, 0.8), [-1, 0], LEAF, 0.24),
    Ring(at(fv, 0, 0), INK, 0.08, 2.5),
    center{at(fv, -1.35, 0.9)} Math(str_join(["\oint \approx ", to_string(around([0, 0], 0.8), 3)]), 0.8, EMBER),
    center{at(fv, 1.45, -0.55)} Math(str_join(["\oint = ", to_string(zero(around([1.45, 0.3], 0.5)), 0)]), 0.8, LEAF)
]

Uses the shared definitions for this lesson's figures.

There is no contradiction, because the unit disc contains the origin, where $\mathbf{V}$ is not defined, let alone continuously differentiable. The theorem’s hypothesis asks for a field that is smooth on the whole region, and this one has a hole in its domain at exactly the place where all of the circulation is generated. Around any loop that does not enclose the origin the theorem applies, and the circulation is 0.

Check yourself

Let $C$ be the boundary of the square $0 \le x \le 2$, $0 \le y \le 2$, traced counterclockwise. What is $\oint_C (-y)\,dx + 3x\,dy$?

Step back. The theorem turns a sum along a boundary into a sum over an inside, and the whole reason is that shared sides are walked twice in opposite directions. Two squares made the cancellation plain. The hard part was passing from tiles to curved regions, and we said where our argument stopped. The reusable idea is the pattern itself: integrating a derivative over a region leaves only boundary terms. It gave the fundamental theorem on an interval and Green’s theorem in the plane, and the same pattern in space is the subject of the theorems of Gauss and Stokes.

Problems

Problems 1 and 2 are warm-ups, 3 to 6 combine ideas, and 7 and 8 go further.

Problem 1

Compute the line integral of $\mathbf{F} = (y, x)$ along the straight segment from $(0, 0)$ to $(2, 1)$.

Show solutionHide solution

Trace the segment by $\mathbf{r}(t) = (2t, t)$, $0 \le t \le 1$, with $\mathbf{r}'(t) = (2, 1)$. On it $\mathbf{F} = (t, 2t)$, so $\mathbf{F} \cdot \mathbf{r}' = 2t + 2t = 4t$, and $\int_0^1 4t\,dt = 2$.

Problem 2

Use Green’s theorem to find the circulation of $\mathbf{F} = (-y^2,\ xy)$ counterclockwise around the rectangle $0 \le x \le 3$, $0 \le y \le 2$.

Show solutionHide solution

$Q_x = y$ and $P_y = -2y$, so the curl is $y + 2y = 3y$. The field is a polynomial field and the rectangle’s boundary is a positively oriented piecewise smooth simple closed curve, so the circulation is $\int_0^3 \int_0^2 3y\,dy\,dx = \int_0^3 6\,dx = 18$.

Problem 3

Verify Green’s theorem for the wind $\mathbf{W} = (x^2 - 1,\ xy)$ on the triangle with corners $(0, 0)$, $(1, 0)$ and $(1, 1)$ by computing both sides.

Show solutionHide solution

The double integral. The curl is $y$, and the triangle is $0 \le x \le 1$, $0 \le y \le x$: $\int_0^1 \int_0^x y\,dy\,dx = \int_0^1 \frac{x^2}{2}\,dx = \frac16$.

The line integral, counterclockwise. Bottom, $\mathbf{r} = (t, 0)$, velocity $(1, 0)$: $\int_0^1 (t^2 - 1)\,dt = -\frac23$. Right side, $\mathbf{r} = (1, t)$, velocity $(0, 1)$: $\int_0^1 Q(1, t)\,dt = \int_0^1 t\,dt = \frac12$. Hypotenuse from $(1, 1)$ back to $(0, 0)$: this is the reverse of $\mathbf{r} = (t, t)$, along which the integral is $\int_0^1 \bigl((t^2 - 1) + t^2\bigr)\,dt = -\frac13$, so it contributes $+\frac13$. Total: $-\frac23 + \frac12 + \frac13 = \frac16$. The two sides agree.

Problem 4

(a) Show that for the straight segment from $(x_1, y_1)$ to $(x_2, y_2)$, $\frac12 \int x\,dy - y\,dx = \frac12 (x_1 y_2 - x_2 y_1)$. (b) Use this to find the area of the quadrilateral with corners $(0, 0)$, $(4, 0)$, $(5, 3)$, $(1, 2)$, listed counterclockwise.

Show solutionHide solution

(a) Trace the segment by $x = x_1 + t\,\Delta x$, $y = y_1 + t\,\Delta y$ for $0 \le t \le 1$, where $\Delta x = x_2 - x_1$ and $\Delta y = y_2 - y_1$. Then $x\,y' - y\,x' = (x_1 + t\,\Delta x)\Delta y - (y_1 + t\,\Delta y)\Delta x = x_1 \Delta y - y_1 \Delta x$, a constant, and it equals $x_1(y_2 - y_1) - y_1(x_2 - x_1) = x_1 y_2 - x_2 y_1$. Integrating from 0 to 1 and halving gives the claim.

(b) By the area theorem, the area is the sum of $\frac12(x_1 y_2 - x_2 y_1)$ over the four sides in order: $\frac12\bigl[(0 \cdot 0 - 4 \cdot 0) + (4 \cdot 3 - 5 \cdot 0) + (5 \cdot 2 - 1 \cdot 3) + (1 \cdot 0 - 0 \cdot 2)\bigr] = \frac12 (0 + 12 + 7 + 0) = 9.5$. This is the surveyor’s “shoelace formula”, and it is Green’s theorem for polygons.

Problem 5

Let $\mathbf{V}$ be the whirlpool field, with curl 0 away from the origin. (a) Compute its circulation counterclockwise around the circle of radius $R$ centered at the origin, directly from the definition. (b) Use the cancellation of shared sides to explain why the answer in (a) cannot depend on $R$.

Show solutionHide solution

(a) With $\mathbf{r}(t) = (R\cos t, R\sin t)$ we have $x^2 + y^2 = R^2$, so $\mathbf{V} = \frac{1}{R^2}(-R\sin t, R\cos t)$ and $\mathbf{r}' = (-R\sin t, R\cos t)$. Their dot product is $\frac{1}{R^2} \cdot R^2 = 1$, and $\int_0^{2\pi} 1\,dt = 2\pi$.

(b) Take two such circles, of radii $R_1 < R_2$, and the ring between them. Cut the ring into two halves along the $x$-axis. Each half is bounded by a simple closed curve and does not contain the origin, so $\mathbf{V}$ is continuously differentiable on it and Green’s theorem gives circulation $\iint 0\,dA = 0$ around each half. Add the two. The cuts along the $x$-axis are walked in opposite directions by the two halves and cancel. What remains is the outer circle counterclockwise and the inner circle clockwise. So (outer circulation) $-$ (inner circulation) $= 0$: the two are equal.

Problem 6

Let $f$ have continuous second partial derivatives on the whole plane, and let $\mathbf{F} = \nabla f = (f_x, f_y)$. (a) Show that the circulation of $\mathbf{F}$ around every curve satisfying the hypotheses of Green’s theorem is 0. (b) Two piecewise smooth paths from $A$ to $B$, neither crossing itself, meet only at their ends. Show that $\mathbf{F}$ has the same line integral along both. (c) Why does the wind $\mathbf{W}$ not behave this way?

Show solutionHide solution

(a) The curl of $\mathbf{F}$ is $\partial_x f_y - \partial_y f_x = f_{yx} - f_{xy}$, which is 0 because mixed partial derivatives of such a function are equal (Clairaut’s theorem). The field is continuously differentiable everywhere, so Green’s theorem gives circulation $\iint_D 0\,dA = 0$. (b) The first path followed by the second one reversed is a simple closed curve. If it is positively oriented, (a) says its circulation is 0; if not, reverse it, which only changes the sign of 0. That circulation is (integral along the first path) $-$ (integral along the second), so the two are equal. (c) The curl of $\mathbf{W}$ is $y$, which is not 0, so $\mathbf{W}$ is not a gradient. Along the parabola and the line from $(0, 0)$ to $(1, 1)$ its integrals differed by $\frac{1}{15}$, which is exactly the integral of its curl over the region between them.

Problem 7

(The flux form.) Let $C$, $D$ and $\mathbf{F} = (P, Q)$ be as in Green’s theorem. Apply the theorem to the field $(-Q, P)$ to show that $\oint_C P\,dy - Q\,dx = \iint_D (P_x + Q_y)\,dA$. Then explain why the left side is the flux of $\mathbf{F}$ outward across $C$.

Show solutionHide solution

The field $(-Q, P)$ is continuously differentiable where $\mathbf{F}$ is. Green’s theorem for it reads $\oint_C (-Q)\,dx + P\,dy = \iint_D \bigl(\partial_x P - \partial_y(-Q)\bigr)\,dA = \iint_D (P_x + Q_y)\,dA$, which is the claim. For the interpretation: a step along the curve is $(\Delta x, \Delta y)$. Turning it a quarter turn clockwise gives $(\Delta y, -\Delta x)$, a vector of the same length pointing to the right of the direction of travel, which for a counterclockwise curve is outward. The dot product of $\mathbf{F}$ with it is $P\,\Delta y - Q\,\Delta x$: the outward component of $\mathbf{F}$ times the length of the step, which is the flux across that step. So the total outflow across the boundary equals the integral of the divergence $P_x + Q_y$ over the inside. This is the divergence theorem in the plane.

Problem 8

The astroid is the curve $x = \cos^3 t$, $y = \sin^3 t$, $0 \le t \le 2\pi$, a four-pointed star traced counterclockwise. Find the area it encloses. (You may use $\int_0^{2\pi} \sin^2 t \cos^2 t\,dt = \frac{\pi}{4}$.)

Show solutionHide solution

$x' = -3\cos^2 t \sin t$ and $y' = 3\sin^2 t \cos t$. Then

$$ x\,y' - y\,x' = 3\cos^4 t \sin^2 t + 3\sin^4 t \cos^2 t = 3\sin^2 t \cos^2 t\,(\cos^2 t + \sin^2 t) = 3\sin^2 t \cos^2 t. $$

Both coordinates have continuous derivatives, so the curve is piecewise smooth in our sense (the velocity vanishes at the four cusps, which our definition permits), and it is simple and positively oriented, so the area is $\frac12 \int_0^{2\pi} 3\sin^2 t\cos^2 t\,dt = \frac32 \cdot \frac{\pi}{4} = \frac{3\pi}{8} \approx 1.18$. As a check, the astroid lies inside the unit circle, of area $\pi \approx 3.14$, and inside the square with corners $(\pm 1, 0)$, $(0, \pm 1)$, of area 2, since its sides bow inward.

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)
}

# the running wind field, with curl y
let W = |p| [p[0] * p[0] - 1, p[0] * p[1]]
# arrows of the field F at the grid points; length grows with speed and levels off
let Arrows = |fr, F, xs, ys, len = 0.34, col = [0.62, 0.65, 0.70, 1], w = 2| block {
    for (x in xs) {
        for (y in ys) {
            let v = F([x, y])
            let m = sqrt(v[0] * v[0] + v[1] * v[1])
            if (m > 0.04) {
                let l = len * m / (m + 0.7)
                let a = at(fr, x, y)
                let d = [l * v[0] / m / 2, l * v[1] / m / 2]
                . Tip([a[0] - d[0], a[1] - d[1], 0], [a[0] + d[0], a[1] + d[1], 0], col, w, min(0.1, 0.55 * l))
            }
        }
    }
}
# border and tick numbers along the bottom and left of a field frame
let FieldBox = |fr, xs, ys| block {
    . Outline([at(fr, fr[0], fr[2]), at(fr, fr[1], fr[2]), at(fr, fr[1], fr[3]), at(fr, fr[0], fr[3])], SLATE, 2.5)
    for (x in xs) {
        . Seg(at(fr, x, fr[2]), [fx(fr, x), fy(fr, fr[2]) - 0.08, 0], SLATE, 2)
        . center{[fx(fr, x), fy(fr, fr[2]) - 0.29, 0]} Math(to_string(x), 0.7, SLATE)
    }
    for (y in ys) {
        . Seg(at(fr, fr[0], y), [fx(fr, fr[0]) - 0.08, fy(fr, y), 0], SLATE, 2)
        . center{[fx(fr, fr[0]) - 0.33, fy(fr, y), 0]} Math(to_string(y), 0.7, SLATE)
    }
}
# the integral of g over [a, b] by the midpoint rule
let integ = |g, a, b, n = 60| {
    var total = 0
    for (k in range(0, n)) {
        total = total + g(a + (k + 0.5) * (b - a) / n) * (b - a) / n
    }
    return total
}
# the line integral of F along the curve r(t), a <= t <= b, with velocity dr(t)
let line_int = |F, r, dr, a, b| integ(|t| {
    let v = F(r(t))
    let d = dr(t)
    return v[0] * d[0] + v[1] * d[1]
}, a, b, 200)
# the four edge circulations [bottom, right, top, left] of W for the square with corner (a, b) and side h
let circs = |a, b, h, n = 60| [
    integ(|x| W([x, b])[0], a, a + h, n),
    integ(|y| W([a + h, y])[1], b, b + h, n),
    -integ(|x| W([x, b + h])[0], a, a + h, n),
    -integ(|y| W([a, y])[1], b, b + h, n)
]
# a small arrowhead on a path at the scene point p, pointing along d
let Head = |p, d, col = INK, size = 0.2| Tip([p[0] - 0.04 * d[0], p[1] - 0.04 * d[1], 0], [p[0] + size * 0.5 * d[0], p[1] + size * 0.5 * d[1], 0], col, 2, size)
let zero = |v| {
    if (abs(v) < 0.0005) { return 0 }
    return v
}