Task 5 of 5
The payoff: run two convolutions at once. Sobel's
Gx filter responds to horizontal change, Gy to vertical change,
and the length of that gradient vector — √(gx² + gy²) — is how
edge-like the pixel is, whatever the edge's direction.
This is a two-kernel pipeline like the finale of Data In, Data Out: a
numeric pass turns the image
into a luminance map (written for you), then the Sobel pass reads each map cell's eight
neighbors, applies both weight grids, and paints the magnitude. Border pixels have no
full neighborhood, so the starter already paints them black — your work lives in the
else branch.
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].
gray, compute gx and gy with the weights shown in
the starter, and paint Math.sqrt(gx * gx + gy * gy) as a gray value.gray[y][x] (no clamping needed — the border branch already ran)gx from the right column minus the left, gy from the bottom row minus the topMath.sqrt(gx * gx + gy * gy) as gray via this.color(m, m, m, 1)Pull the nine cells into locals first —
const tl = gray[y - 1][x - 1]; through
const br = gray[y + 1][x + 1]; — then the two weighted sums are easy to
read off the grids.
const gx = (tr + 2 * mr + br) - (tl + 2 * ml + bl);
— right column minus left column, middle counted double. gy is the same
with rows: (bl + 2 * bm + br) - (tl + 2 * tm + tr).
const m = Math.sqrt(gx * gx + gy * gy);
this.color(m, m, m, 1);
— flat areas give 0 (black), sharp edges overshoot 1 and clamp to white.
This page is an interactive exercise — the editor, the GPU runner and your saved progress need JavaScript. The text above is the full brief.