I went with Jason R's idea of adjusting the linear filtering, except I didn't like the idea of doing a division on every output pixel, using more than just a simple kernel or how normalising that way would ruin the smooth borders. So instead I modified the kernel itself, which it turns out is done by flattening the peak of the triangle.

It seems like a pretty good solution, and you can see how the sum of the triangles progresses into something resembling a Gaussian function.
Here's how it's done, first of all there's a 'knee' joining the flat top with the rest of the triangle. It ping-pongs on the horizontal axis between 0 and 0.5 like so. Note: $n$ is the scale multiplier for the width of the kernel and the size of the downscaling, it's always 1 or lower, it's the reciprocal of the number displayed at the top of the animation. The smaller $n$, the smaller the output image, the wider the kernel over the original image.
$knee = 0.5 - |(n^{-1} \mod 1) - 0.5|$
Next we need to calculate the height of the top of the flattened triangle, which is done like this:
$m = \lfloor n^{-1} + 0.5\rfloor$
$top = \frac{1 - knee \cdot n}{2m - nm^2}$
And finally we turn it into an affine function which we can use directly when calculating weights:
$c_1 = \frac{-top}{n^{-1}-knee}$
$c_0 = \frac{-c_1}{n}$
The above is calculated only once and $knee$, $top$, $c_1$ and $c_0$ are stored and used when computing weights like this:
$$f(x) =
\begin{cases}
\hfill top \hfill & \text{ if $|x| \leq knee$ } \\
\hfill c_1 x + c_0 \hfill & \text{ if $knee < |x| \leq n^{-1}$ } \\
\hfill 0 \hfill & \text{ otherwise } \\
\end{cases}$$