Task 5 of 6
One seam is a curiosity. Thirty-two is a resize. And the loop has a catch that matters: once a seam is gone the picture is a different picture, so the energy map and the cost map both have to be built again from the carved luminance — not from the original. Energy → cost → seam → carve, and round again.
Everything you wrote is here already. What is left is the orchestration, and the order is the whole thing: each stage awaits the one before it, and inside the cost map the rows must go one at a time, because row y reads row y - 1's answer. Three colour planes and the luminance plane all reflow with the same seam — the carve kernel has no idea what it is carving, which is why one kernel does all four.
Two console tricks pay for themselves here. render() once per removal
collapses into a frame scrubber you can drag back and forth — that is
where you actually see the picture reflow. And plotting each seam's energy against its
removal number gives you the curve that tells you when to stop: the cheap seams go first,
and when the curve knees upwards the picture has run out of things it can afford to lose.
The slider under the console re-runs the whole program, so you can carve less and compare.
Image data comes in row-major: image[y][x] is the pixel in row y,
column x, and each pixel is an [r, g, b, a] array with channels from
0 to 1. Mind the inversion that catches everyone — sizes are given width-first
(output: [width, height]), but indexing runs row-first, so this thread's own
pixel is image[this.thread.y][this.thread.x]. Swap those two and you read the
transpose of your image. Three-dimensional data follows the same rule:
output: [w, h, d] is indexed [z][y][x].
carveOnce — energy, cumulative cost,
backtrack, then carve every plane with that one seam — and return the new planes together
with the seam's price.planes[3]) at the current width — energy.setOutput([w, 72])step launch per row, in orderbacktrack the rows to a seam, and take its price from the bottom row[w - 1, 72], and return the new planes plus the costNothing needs to be tracked by hand: the planes know how wide they are.
const w = planes[0][0].length;
energy.setOutput([w, 72]);
step.setOutput([w]);
carve.setOutput([w - 1, 72]);Exactly the driver from the cumulative-cost task, over the energy map you just built:
const rows = [Float32Array.from(e[0])];
for (let y = 1; y < 72; y++) rows.push(await step(e[y], rows[y - 1]));A for loop, not .map() — a callback cannot hold an
await, and every one of these is a kernel call:
const next = [];
for (let i = 0; i < planes.length; i++) {
next.push(await carve(planes[i], seam));
}
return { planes: next, cost: rows[71][seam[71]] };This page is an interactive exercise — the editor, the GPU runner and your saved progress need JavaScript. The text above is the full brief.