Skip to content

Coordinate space

unreviewed draft

How hydra addresses pixels (a 0–1 square stretched over the canvas), what sources draw outside it, and why geometry functions seem to run backwards.

Almost every hydra parameter that moves, zooms or repeats something is measured in one coordinate system. Once you know it, the units on every page (screen widths, “positive moves left”, “wraps every 1”) follow from a few lines of math.

Each pixel gets a coordinate called stst (hydra’s name for it; think xyxy):

x →y ↓(0, 0)(1, 0)(0, 1)(1, 1)(0.5, 0.5)
  • xx runs from 0 at the left edge to 1 at the right.
  • yy runs from 0 at the top to 1 at the bottom: it points down, as in most image formats.
  • Both axes go 0 → 1 whatever the canvas size or shape. A 1280 × 720 canvas is still a “1 × 1” square; a step of 0.1 in xx is 128 pixels, a step of 0.1 in yy only 72.

gradient() draws exactly this: red = xx, green = yy. Probe on a 16 : 9 canvas: top-left (5, 4, 0), top-right (250, 4, 0), bottom-left (5, 251, 0), bottom-right (250, 251, 0).

That last bullet has visible consequences. shape(64, 0.5) (a near-circle) covers 0.25–0.75 on both axes, which on a 256 × 144 canvas is 128 pixels wide and 72 tall: an ellipse. Every distance-based function (shape, rotate, kaleid, scale with equal multipliers) treats one unit of xx and one unit of yy as equal, even though they aren’t on screen.

Geometry functions can ask a source for coordinates outside the square (zoom out with .scale(0.5) and the screen covers −0.5 to 1.5). What you see there depends on the source, because each one is just a formula of stst:

SourceOutside 0–1Evidence (probe with .scale(0.5))
src(…) (images, video, other outputs)repeats (tiles): it samples at fract⁡(st)\operatorname{fract}(st)quadrant texture appears 2 × 2
osccontinues the stripes4 stripes across instead of 2
noisecontinuesdifferent, non-repeating values at the edges
gradientclamps: stst below 0 shows black, above 1 full color(0, 0, 0) near top-left, (255, 255, 0) near bottom-right
shapeblack: the shape sits at the center; everything else is emptyone small square, black around it
solidthe same color everywhere

gradient clamps because its output is the coordinate, and colors are stored in 0–1 between passes.

Textures loaded into s0…s3 (images, video, camera) are stretched to fill the 0–1 square, so a square image on a wide canvas is displayed wide. They appear upright: the image’s top-left corner is at st=(0,0)st = (0, 0).

Geometry functions (scroll, scale, rotate, repeat, kaleid, pixelate, and the modulate… family) never move pixels. For each screen pixel they compute which coordinate to ask the source for. That’s why their parameters often seem inverted:

  • scroll(0.25, 0) asks for x+0.25x + 0.25: each pixel shows what was a quarter screen to its right, so the image moves left.
  • scale(2) asks for (st−0.5)/2+0.5(st - 0.5) / 2 + 0.5: it divides the distance from the center, so a pixel near the edge shows something closer to the middle, and the image looks twice as big. Probe: gradient().scale(2) at x=0.1x = 0.1 shows the color of x=0.3x = 0.3. scale(0.5) shrinks; a negative amount divides by a negative number, which flips both axes (a 180° turn, not a mirror).
  • rotate(θ) asks for the point rotated by −θ-\theta around the center, so the image turns by +θ+\theta, clockwise on screen because yy points down.
color(st)=source(T(st))scroll: T(st)=st+dscale: T(st)=st−ca+crotate: T(st)=R(−θ)(st−c)+c\text{color}(st) = \text{source}\big(T(st)\big) \qquad\text{scroll: } T(st) = st + d \quad \text{scale: } T(st) = \frac{st - c}{a} + c \quad \text{rotate: } T(st) = R(-\theta)(st - c) + c

In src(s0).rotate(…).scroll(…), hydra builds the shader so that the last geometry function in the chain transforms the screen coordinate first:

color(st)=src(Trotate(Tscroll(st)))\text{color}(st) = \text{src}\Big(T_{\text{rotate}}\big(T_{\text{scroll}}(st)\big)\Big)

Color functions (color, brightness, thresh, …) are the other way round: they run after sampling, in chain order.

The good news: the two reversals cancel. If you read the chain left to right as operations on the image, you get the right picture. Probe with a red | green / blue | white quadrant texture: src(s0).rotate(Math.PI / 2, 0).scroll(0.25, 0) has a top row reading B B R R R R B B: the quadrants turned 90° clockwise (blue top-left, red top-right), then the result slid a quarter screen left.

The backwards order matters when a function’s behavior depends on the coordinate values it receives. scroll wraps its output into 0–1 with fract. Put .scale(0.5) after it in the chain and the scale runs first, handing scroll coordinates from −0.5 to 1.5, which it wraps into tiles:

Rotation is done on the unit square, and the unit square is stretched to the canvas. On a 16 : 9 canvas a rotation in stst space is a rotation in a squashed space, which on screen looks like a shear: right angles stop being right angles partway through a turn. The cure is to make the units square first, rotate, and undo it; rotate shows the chain. The same idea fixes ellipses and stretched images (examples below).

See the coordinates (red = x, green = y)
gradient().out()
Open in hydra ↗
Order matters: wrap, then zoom out
shape(4, 0.3, 0.01).scroll(0, 0).scale(0.5).out()
Open in hydra ↗
A round circle on any canvas
shape(64, 0.4, 0.01).scale(1, height / width, 1).out()
Open in hydra ↗
An image at its own aspect ratio
s0.initImage("https://hydra-ref.audiodude.xyz/cards/photo.jpg")
src(s0)
.scale(1, () => s0.src ? (s0.src.width / s0.src.height) / (width / height) : 1, 1)
.out()
Open in hydra ↗

The circle example shrinks xx by the canvas’s height/width ratio, so one unit of xx and one of yy cover the same number of pixels (probe on 256 × 144: about 64 × 54 px, against 96 × 54 px without the fix, within the probe’s 8 px / 4.5 px sampling steps). The image example does the same with the image’s own ratio, computed every frame by a function argument (so it’s correct once the image has loaded; until then s0.src is null and the factor is 1). Because src() repeats, an image narrower than the canvas tiles at the sides instead of leaving black bars: probe with a square texture on a 256 × 144 canvas gives a square image 144 px wide in the middle with partial copies left and right. The sample photo is 16 : 9 itself, so on a 16 : 9 canvas the factor is 1 and nothing changes; swap in a portrait image to see the difference.

  • scroll: the simplest geometry function, and the wrap.
  • rotate: angles, the center pivot, aspect-correct rotation.
  • osc: a source that only looks at xx.
  • Arguments & time: numbers, arrays and functions as parameters.

Documents hydra-synth 1.4.0 (AGPL-3.0; this site runs it unmodified). Annotations drafted by Claude (Anthropic) from the hydra-synth source and checked against rendered output — see about. Site code AGPL-3.0, prose CC BY 4.0,source.