# Ripples: Think in Polar

*Task 4 of 4 · [Pixels from Scratch](https://gpu.rocks/learn/pixels-from-scratch-d2869039.md) · GPU.js Learn*

Gradients, cells and curves all thought in x and y. The last move of this module
is to change coordinate systems *inside the kernel*: subtract the canvas center
(63.5, 63.5 — halfway between the two middle rows and columns), and Pythagoras turns the
thread's position into a **radius**. Anything you compute from that radius is
automatically a perfect circle.

Feed the radius into a cosine and you get concentric ripples; multiply by a fade so
they die out toward the edge; tint the channels and the flat canvas turns into water.
One gotcha, handled in the starter: `this.thread.x` is an *integer* on
the GPU, so promote it to a float (`this.thread.x / 1`) before subtracting the
fractional center — otherwise the GPU rounds your 63.5 away.

One more thing comes prewired: a **dial**.
`slider('phase', …)` returns the value this run is using and puts a control
under the console; move it and the whole program re-runs. Phase slides the radius the
cosine sees — one radian of it is `1 / 0.35` px of radius — so at 0 the rings
stand still, and anywhere else the same rings sit further out. Drag it and the crests
travel; at 2π every crest has moved out by exactly one ring, which is the picture you
started from. The fade is computed *before* the slide, on the true radius, so the
vignette never moves — only the water does.

## Figures

- **subtract the center and (x, y) becomes r — anything you do to r is a circle**

## Goal

**Goal:** finish the ripple kernel — a cosine wave over the radius,
faded toward the edge, tinted blue: `this.color(0.4v, 0.75v, v, 1)`.

## Requirements

- Radius from the center: `Math.sqrt(dx*dx + dy*dy)` with dx, dy relative to (63.5, 63.5)
- Ripple: `wave = 0.5 + 0.5 * Math.cos(r * 0.35)`
- Fade: `fade = Math.max(0, 1 - r / 96)`, then `v = wave * fade`
- Blue tint: channels `0.4*v`, `0.75*v`, `v`
- The `phase` dial is prewired — drag it to send the rings travelling, but Run at its default `0`, which is where these tests measure

## Hint 1 — the ripple

`Math.cos(r * 0.35)` swings between −1 and 1 as r grows —
`0.5 + 0.5 * cos` remaps that to 0…1, a crest roughly every 18 pixels.

## Hint 2 — the last three lines

```js
const wave = 0.5 + 0.5 * Math.cos(r * 0.35);
const v = wave * fade;
this.color(0.4 * v, 0.75 * v, v, 1);
```

Use the `fade` the starter already computed rather than writing it again —
it was measured on the true radius, before the phase slide.

## Same idea elsewhere

Radius-and-angle reasoning is everywhere in GPU code: vignette and
lens-distortion passes in Metal and WebGPU post-processing, CUDA and ROCm image warps
that resample in polar space, every "tunnel" demo ever shipped. Center the coordinates,
transform them, color by the result — that opening move never changes. Nor does the next
one: add a phase to the transformed coordinate and the picture starts to move, which is
all the `time` uniform in an animated shader has ever done.

## Starter code

```js
// Change coordinates INSIDE the kernel: position → radius from center.
const gpu = new GPU({ mode });

const ripples = gpu.createKernel(function (phase) {
  // thread ids are integers — "/ 1" promotes them to floats so the
  // half-pixel center stays exact on the GPU
  const dx = this.thread.x / 1 - 63.5;
  const dy = this.thread.y / 1 - 63.5;
  let r = Math.sqrt(dx * dx + dy * dy);
  const fade = Math.max(0, 1 - r / 96);
  // Prewired, and it stays put: the dial slides the radius the cosine will
  // see, so the crests travel while the fade — measured one line up, on the
  // TRUE radius — holds the vignette still.
  r = r - phase / 0.35;
  // TODO: 1) ripple — wave = 0.5 + 0.5 * Math.cos(r * 0.35)
  //       2) combine — v = wave * fade
  //       3) tint    — this.color(0.4 * v, 0.75 * v, v, 1)
  this.color(fade, fade, fade, 1);
}, {
  output: [128, 128],
  graphical: true,
});

// A dial, not a constant: slider() returns the value this run is using and puts
// a control under the console, and moving it re-runs the whole program. The
// tests measure the ripple at rest, so Run with phase back at its default 0.
const phase = slider('phase', { min: 0, max: 6.28, value: 0, step: 0.01 });

await ripples(phase);
render(ripples.canvas);
```

---

Interactive version: https://gpu.rocks/learn/pixels-from-scratch-d2869039/4

[Previous task](https://gpu.rocks/learn/pixels-from-scratch-d2869039/3.md)
