# Paint the Flow Field

*Task 5 of 5 · [Optical Flow](https://gpu.rocks/learn/optical-flow-e85c6dfa.md) · GPU.js Learn*

A flow field is two numbers per pixel, and nobody can read that as a table. The
standard picture encodes it as colour: **direction becomes hue**, walking once
around the colour wheel as the vector turns once around the circle, and
**magnitude becomes saturation**, so a still pixel is white and a fast one is
vivid. Hue and saturation as an angle and a radius is the polar half of HSV — the same wheel
Colour Spaces works in — and here it costs one `Math.atan2`.

`flow` is the field task 3 produced for this scene, handed to you finished:
`flow[0][y][x]` is `u` and `flow[1][y][x]` is
`v`. The hue→RGB conversion is written for you in the starter — it is six
straight lines around a hexagon, not the lesson. Your part is the encoding.

Watch what the picture tells you: the whole scene translated by `(1, 1)`, and
half of it comes out **white**. That is not a bug. Those are the pixels
task 4 scored zero, painted honestly as "no idea".

## Goal

**Goal:** paint the flow field — hue from the vector's direction,
saturation from its length.

## Requirements

- A graphical kernel: `graphical: true`, `output: [64, 64]`
- Saturation: `s = Math.min(1, magnitude / this.constants.maxFlow)`, where magnitude is `Math.sqrt(u * u + v * v)`
- Hue: `hh = (Math.atan2(v, u) / (2 * Math.PI) + 0.5) * 6` — the sextant the direction lands in, from 0 to 6
- Paint the hue blended toward white by `s`: `this.color(1 - s + s * r, 1 - s + s * g, 1 - s + s * b, 1)`, so a zero-length vector comes out pure white

## Hint 1 — why the + 0.5

`Math.atan2` returns an angle in `−π…π`. Dividing by
`2π` maps that to `−0.5…0.5`, and the `+ 0.5` slides it
to `0…1` without a branch or a modulo. Which direction gets which hue is
arbitrary; that the mapping is *one-to-one* is the part that matters.

## Hint 2 — the two lines above the hexagon

```js
const mag = Math.sqrt(u * u + v * v);
const s = Math.min(1, mag / this.constants.maxFlow);
const hh = (Math.atan2(v, u) / (2 * Math.PI) + 0.5) * 6;
```

and the last line under it is the blend: `1 - s + s * r` is
`r` when `s` is 1 and white when `s` is 0.

## Same idea elsewhere

The Middlebury flow-colour wheel is this exact encoding, and it is what every
optical-flow paper prints. The pattern generalises: whenever a pass produces a vector per
pixel — normals, velocity, curvature — direction-to-hue keeps it readable, and it costs one
extra fragment/compute pass in WebGPU, CUDA or Metal alike. It is also the cheapest
debugging tool in graphics: a wrong sign in a flow field is invisible in a table and
blindingly obvious as a picture in the complementary colour.

## Starter code

```js
// Direction becomes hue, magnitude becomes saturation, stillness stays white.
const gpu = new GPU({ mode });

const paintFlow = gpu.createKernel(function (flow) {
  const u = flow[0][this.thread.y][this.thread.x];
  const v = flow[1][this.thread.y][this.thread.x];

  // TODO: s — the flow's length over this.constants.maxFlow, capped at 1
  const s = 0;
  // TODO: hh — (Math.atan2(v, u) / (2 * Math.PI) + 0.5) * 6
  const hh = 0;

  // Hue → RGB, written for you: six straight lines around the colour hexagon.
  let r = 0;
  let g = 0;
  let b = 0;
  if (hh < 1) {
    r = 1;
    g = hh;
  } else if (hh < 2) {
    r = 2 - hh;
    g = 1;
  } else if (hh < 3) {
    g = 1;
    b = hh - 2;
  } else if (hh < 4) {
    g = 4 - hh;
    b = 1;
  } else if (hh < 5) {
    r = hh - 4;
    b = 1;
  } else {
    r = 1;
    b = 6 - hh;
  }

  // TODO: blend each channel toward white by s and paint it.
  this.color(1, 0, 1, 1);
}, {
  output: [64, 64],
  graphical: true,
  constants: { maxFlow: 1.5 },
});

await paintFlow(flow);
render(paintFlow.canvas);
```

---

Interactive version: https://gpu.rocks/learn/optical-flow-e85c6dfa/5

[Previous task](https://gpu.rocks/learn/optical-flow-e85c6dfa/4.md)
