Skip to content

scroll

unreviewed draft

Slides the image sideways and/or up and down, wrapping it around the edges, by a fixed amount or continuously over time.

scroll, scrollX and scrollY are geometry functions: they change which coordinate the earlier part of the chain is asked to draw at each pixel (see Coordinate space). For screen coordinate stst, scroll hands the source

st′=fract⁡ ⁣(st+(scrollX+speedX⋅t,  scrollY+speedY⋅t)),fract⁡(v)=v−⌊v⌋st' = \operatorname{fract}\!\Big(st + \big(\text{scrollX} + \text{speedX}\cdot t,\ \ \text{scrollY} + \text{speedY}\cdot t\big)\Big), \qquad \operatorname{fract}(v) = v - \lfloor v \rfloor

where tt is hydra’s time in seconds. Two things follow:

  • Direction. The pixel at xx shows what used to be at x+0.25x + 0.25, i.e. the content that was a quarter screen to its right. So a positive shift moves the image left, and (because yy points down the screen) a positive vertical shift moves it up.
  • Wrapping. fract throws away the whole-number part, so coordinates always land back in [0,1)[0, 1): what scrolls off one edge comes back on the opposite one, and a shift of 1 is the same as 0.

Units are fractions of the screen: 0.25 is a quarter of the width (for xx) or height (for yy), whatever the canvas size, and speeds are screens per second.

scrollX(scrollX, speed) and scrollY(scrollY, speed) are the same thing on one axis, with the speed parameter renamed to plain speed.

scroll source in glsl-functions.js (hydra-synth 1.4.0) ↗ scrollX source in glsl-functions.js (hydra-synth 1.4.0) ↗ scrollY source in glsl-functions.js (hydra-synth 1.4.0) ↗
Endless horizontal scroll
s0.initImage("https://hydra-ref.audiodude.xyz/cards/uv-grid.png")
src(s0).scroll(0, 0, 0.1, 0).out()
Open in hydra ↗
Swaying side to side with a function argument
shape(3, 0.3, 0.01)
.scrollX(() => Math.sin(time) * 0.25, 0)
.out()
Open in hydra ↗
Rotating around a point other than the center
s0.initImage("https://hydra-ref.audiodude.xyz/cards/uv-grid.png")
src(s0)
.scroll(-0.25, -0.25)
.rotate(0, 0.5)
.scroll(0.25, 0.25)
.out()
Open in hydra ↗
Deliberate tiling: wrap first, then zoom out
shape(4, 0.3, 0.01)
.scroll(0, 0, 0.05, 0.05)
.scale(0.5)
.out()
Open in hydra ↗

The third example reads left to right as image operations: move the point (0.25,0.25)(0.25, 0.25) to the center (a negative scroll moves the image right and down), spin around the center, move it back. The pivot stays fixed: probing it with gradient() and a 90° rotate returns the same color at (0.25,0.25)(0.25, 0.25) before and after. The last example relies on the wrap described below.

Shifts the coordinates fed to everything earlier in the chain by a fixed and/or time-growing amount, then wraps them into 0–1.

ParameterDefaultUseful rangeUnitsNo-op at
scrollX0.5-1 … 1screen widths (fraction of the canvas width)0 (or any whole number), with speedX = 0; the wrap still applies, see Gotchas
scrollY0.5-1 … 1screen heights (fraction of the canvas height)0 (or any whole number), with speedY = 0; the wrap still applies, see Gotchas
speedX0-1 … 1screen widths / second (one full lap every 1/∣speedX∣1 / |\text{speedX}| s)0
speedY0-1 … 1screen heights / second (one full lap every 1/∣speedY∣1 / |\text{speedY}| s)0

scrollX= 0.5

Fixed horizontal shift, in screen widths. Positive moves the image left.

Useful range
-1 … 1
Units
screen widths (fraction of the canvas width)
No-op at
0 (or any whole number), with speedX = 0; the wrap still applies, see Gotchas
Negative values
moves the image right
Periodic
every 1; 1.25 looks the same as 0.25
Accepts
number · array · function

The default is 0.5, not 0: .scroll() with no arguments moves the image half a screen left and half a screen up, so whatever was in the center ends up split across the four corners.

The image wraps around: what leaves on the left comes back on the right.

scrollX = 00
scrollX = 0.10.1
scrollX = 0.250.25
scrollX = 0.50.5
scrollX = -0.25-0.25
scrollX = 11
Sweep of scrollX (still at time = 0): s0.initImage("https://hydra-ref.audiodude.xyz/cards/uv-grid.png") src(s0).scroll({scrollX}, 0, 0, 0).out() · outlined tile = default

scrollY= 0.5

Fixed vertical shift, in screen heights. Positive moves the image up.

Useful range
-1 … 1
Units
screen heights (fraction of the canvas height)
No-op at
0 (or any whole number), with speedY = 0; the wrap still applies, see Gotchas
Negative values
moves the image down
Periodic
every 1
Accepts
number · array · function

Hydra's yy axis points down the screen, and scroll adds to the coordinate, so positive values pull content from below: the image moves up.

scrollY = 00
scrollY = 0.10.1
scrollY = 0.250.25
scrollY = 0.50.5
scrollY = -0.25-0.25
scrollY = 11
Sweep of scrollY (still at time = 0): s0.initImage("https://hydra-ref.audiodude.xyz/cards/uv-grid.png") src(s0).scroll(0, {scrollY}, 0, 0).out() · outlined tile = default

speedX= 0

Continuous horizontal scrolling, in screen widths per second. Positive moves the image left.

Useful range
-1 … 1
Units
screen widths / second (one full lap every 1/∣speedX∣1 / |\text{speedX}| s)
No-op at
0
Negative values
scrolls right
Accepts
number · array · function

The total shift is scrollX+speedX⋅t\text{scrollX} + \text{speedX}\cdot t, where tt is hydra's time in seconds. speedX multiplies an ever-growing time, so changing it while running makes the image jump to a new position rather than smoothly change pace.

The strip clips are 3 s long, so 0.3333 makes exactly one lap per loop.

0
0.1
0.3333
-0.1
-0.3333
Sweep of speedX (3s loops): s0.initImage("https://hydra-ref.audiodude.xyz/cards/uv-grid.png") src(s0).scroll(0, 0, {speedX}, 0).out() · outlined tile = default

speedY= 0

Continuous vertical scrolling, in screen heights per second. Positive moves the image up.

Useful range
-1 … 1
Units
screen heights / second (one full lap every 1/∣speedY∣1 / |\text{speedY}| s)
No-op at
0
Negative values
scrolls down
Accepts
number · array · function

The total shift is scrollY+speedY⋅t\text{scrollY} + \text{speedY}\cdot t. Same jump-on-change caveat as speedX.

0
0.1
0.3333
-0.1
-0.3333
Sweep of speedY (3s loops): s0.initImage("https://hydra-ref.audiodude.xyz/cards/uv-grid.png") src(s0).scroll(0, 0, 0, {speedY}).out() · outlined tile = default

Horizontal-only scroll. Still wraps both axes into 0–1.

ParameterDefaultUseful rangeUnitsNo-op at
scrollX0.5-1 … 1screen widths0 (or any whole number), with speed = 0; the wrap still applies
speed0-1 … 1screen widths / second0

scrollX= 0.5

Fixed horizontal shift, in screen widths. Positive moves the image left.

Useful range
-1 … 1
Units
screen widths
No-op at
0 (or any whole number), with speed = 0; the wrap still applies
Negative values
moves the image right
Periodic
every 1
Accepts
number · array · function

Same as scroll's scrollX, including the 0.5 default (half a screen, not a no-op).

scrollX = 00
scrollX = 0.250.25
scrollX = 0.50.5
scrollX = -0.25-0.25
Sweep of scrollX (still at time = 0): s0.initImage("https://hydra-ref.audiodude.xyz/cards/uv-grid.png") src(s0).scrollX({scrollX}, 0).out() · outlined tile = default

speed= 0

Continuous horizontal scrolling, in screen widths per second. Positive moves the image left.

Useful range
-1 … 1
Units
screen widths / second
No-op at
0
Negative values
scrolls right
Accepts
number · array · function

Same as scroll's speedX; renamed because this function has only one axis.

0
0.3333
-0.3333
Sweep of speed (3s loops): s0.initImage("https://hydra-ref.audiodude.xyz/cards/uv-grid.png") src(s0).scrollX(0, {speed}).out() · outlined tile = default

Vertical-only scroll. Still wraps both axes into 0–1.

ParameterDefaultUseful rangeUnitsNo-op at
scrollY0.5-1 … 1screen heights0 (or any whole number), with speed = 0; the wrap still applies
speed0-1 … 1screen heights / second0

scrollY= 0.5

Fixed vertical shift, in screen heights. Positive moves the image up.

Useful range
-1 … 1
Units
screen heights
No-op at
0 (or any whole number), with speed = 0; the wrap still applies
Negative values
moves the image down
Periodic
every 1
Accepts
number · array · function

Same as scroll's scrollY, including the 0.5 default (half a screen, not a no-op).

scrollY = 00
scrollY = 0.250.25
scrollY = 0.50.5
scrollY = -0.25-0.25
Sweep of scrollY (still at time = 0): s0.initImage("https://hydra-ref.audiodude.xyz/cards/uv-grid.png") src(s0).scrollY({scrollY}, 0).out() · outlined tile = default

speed= 0

Continuous vertical scrolling, in screen heights per second. Positive moves the image up.

Useful range
-1 … 1
Units
screen heights / second
No-op at
0
Negative values
scrolls down
Accepts
number · array · function

Same as scroll's speedY; renamed because this function has only one axis.

0
0.3333
-0.3333
Sweep of speed (3s loops): s0.initImage("https://hydra-ref.audiodude.xyz/cards/uv-grid.png") src(s0).scrollY(0, {speed}).out() · outlined tile = default
  • The order of scroll relative to other geometry matters. Read the chain left to right as operations on the image: .rotate(…).scroll(0.25, 0) turns the image and then slides the result left; .scroll(0.25, 0).rotate(…) slides first, so the slide happens along the rotated direction.
  • speedX/speedY multiply time, which keeps growing; changing them live makes the image jump. Pass a function to scrollX/scrollY that accumulates its own offset if you need smooth changes of pace.
  • Use modulateScrollX/modulateScrollY to scroll by an amount that varies per pixel.
  • rotate: combine with scroll to rotate around any point (third example).
  • osc: choose frequency = 2 * Math.PI * n so scrolled stripes wrap without a seam.
  • Coordinate space: why geometry runs “backwards” through the chain.
  • Arguments & time: arrays and functions as shift and speed arguments.

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.