Skip to content

osc

unreviewed draft

Scrolling vertical sine-wave stripes; the offset splits red, green and blue apart into colored bands.

osc is a source: it computes a color for every pixel from that pixel’s coordinate and the time. It uses only the horizontal coordinate xx (0 at the left edge, 1 at the right; see Coordinate space), so it always draws vertical stripes. Each channel is a sine wave squeezed into the 0–1 range:

r=12+12sin⁡ ⁣(φ−offset)g=12+12sin⁡ ⁣(φ)b=12+12sin⁡ ⁣(φ+offset)φ=frequency⋅(x+sync⋅t)\begin{aligned} r &= \tfrac12 + \tfrac12 \sin\!\big(\varphi - \text{offset}\big) \\ g &= \tfrac12 + \tfrac12 \sin\!\big(\varphi\big) \\ b &= \tfrac12 + \tfrac12 \sin\!\big(\varphi + \text{offset}\big) \end{aligned} \qquad \varphi = \text{frequency}\cdot\big(x + \text{sync}\cdot t\big)

where tt is hydra’s time in seconds. Reading the phase φ\varphi term by term:

  • frequency is how many radians the wave advances per screen width. One full stripe (bright → dark → bright) is 2π2\pi radians, so you see frequency/2π\text{frequency}/2\pi stripes: about 9.5 with the default 60.
  • sync is added to xx, scaled by time: the pattern you would have seen at x+sync⋅tx + \text{sync}\cdot t is now drawn at xx, so it slides left by sync screen widths each second.
  • offset is a phase shift applied with opposite signs to red and blue. With offset = 0 all three channels are identical and the stripes are pure gray; increasing it pulls red and blue out of step with green, which is where the colors come from.

The stripes are soft (a sine, not a square wave) and every channel sweeps the full 0–1 range.

Because the formula is defined for every xx, osc keeps going outside the screen: zoom out with .scale(0.5) and you see twice as many stripes, never a repeat or a black border.

osc source in glsl-functions.js (hydra-synth 1.4.0) ↗
Rainbow stripes (channels a third of a cycle apart)
osc(30, 0.05, 2.09).out()
Open in hydra ↗
Exactly 5 stripes, turned horizontal
osc(2 * Math.PI * 5, 0.1, 0.8)
.rotate(Math.PI / 2, 0)
.out()
Open in hydra ↗
Moiré: two oscillators crossed
osc(60, -0.015, 0.3)
.diff(osc(60, 0.08).rotate(Math.PI / 2, 0))
.out()
Open in hydra ↗
Hard-edged stripes
osc(20, 0.1, 0).thresh(0.5, 0).out()
Open in hydra ↗

The third example works because diff takes the absolute difference of the two stripe patterns; where the vertical and horizontal stripes agree you get black, where they disagree, color.

Vertical sine stripes that scroll sideways over time; offset phase-shifts red and blue in opposite directions.

ParameterDefaultUseful rangeUnitsNo-op at
frequency60-100 … 200radians per screen width; frequency/2π\text{frequency} / 2\pi = stripes across the screennone (there is no "off" value; 0 is undefined, see below)
sync0.1-2 … 2screen widths / second (the pattern crosses the screen in 1/∣sync∣1 / |\text{sync}| s)0 (still stripes)
offset0-6.2832 … 6.2832radians of phase (a full stripe = 2π2\pi)0 (or any multiple of 2π2\pi): all three channels equal, so the stripes are gray

frequency= 60

How tightly packed the stripes are, in radians of the sine wave per screen width.

Useful range
-100 … 200
Defined for
frequency ≠ 0 (red and blue compute offset / frequency)
Units
radians per screen width; frequency/2π\text{frequency} / 2\pi = stripes across the screen
No-op at
none (there is no "off" value; 0 is undefined, see below)
Negative values
inverts the colors and swaps red with blue (exactly r′=1−br' = 1 - b, g′=1−gg' = 1 - g, b′=1−rb' = 1 - r); motion direction is unchanged
Accepts
number · array · function

The stripe count is frequency/2π\text{frequency} / 2\pi, so the default 60 gives about 9.5 stripes, and 2 * Math.PI * n gives exactly n. Whole-number counts matter when the pattern is wrapped by scroll, src() feedback or repeat: otherwise there is a visible seam where the last partial stripe meets the first.

Stripes are counted per screen width, whatever the canvas size or shape: a wider canvas gets wider stripes, not more of them.

0 divides by zero in the red and blue channels. In headless Chrome (SwiftShader) it renders flat gray (128, 128, 128), with or without an offset; other GPUs may differ.

frequency = 3.14163.1416
frequency = 6.28326.2832
frequency = 2020
frequency = 6060
frequency = 150150
frequency = -20-20
Sweep of frequency (still at time = 0): osc({frequency}, 0, 1).out() · outlined tile = default

sync= 0.1

Scroll speed of the stripes, in screen widths per second. Positive moves them left.

Useful range
-2 … 2
Units
screen widths / second (the pattern crosses the screen in 1/∣sync∣1 / |\text{sync}| s)
No-op at
0 (still stripes)
Negative values
stripes move right
Accepts
number · array · function

The phase is frequency⋅(x+sync⋅t)\text{frequency}\cdot(x + \text{sync}\cdot t), so the whole pattern slides left by sync screen widths every second. The same stripes also flicker at a fixed pixel, at frequency⋅sync/2π\text{frequency} \cdot \text{sync} / 2\pi times per second: the default osc(60, 0.1) pulses about 0.95 times a second. To make the pattern flash faster without changing how fast it travels, you have to raise frequency (more, narrower stripes).

Because sync multiplies an ever-growing time, changing it (or frequency) while running makes the stripes jump to a new position rather than smoothly change speed.

0
0.05
0.1
0.3333
-0.1
Sweep of sync (3s loops): osc(20, {sync}, 1).out() · outlined tile = default

offset= 0

Splits the color channels apart. Red's wave is shifted by −offset-\text{offset} radians, blue's by +offset+\text{offset}, green never moves.

Useful range
-6.2832 … 6.2832
Units
radians of phase (a full stripe = 2π2\pi)
No-op at
0 (or any multiple of 2π2\pi): all three channels equal, so the stripes are gray
Negative values
swaps the roles of red and blue
Periodic
every 2π2\pi (6.283…); 1 and 1 + 2π render identically
Accepts
number · array · function

It is not a position offset: green stays put, red shifts right and blue shifts left, each by offset/frequency\text{offset} / \text{frequency} screen widths. Where the waves disagree you see color.

  • 0: gray stripes.
  • Math.PI (π\pi): red and blue are the exact opposite of green, so green stripes alternate with magenta ones.
  • 2.094 (2π/32\pi/3): the three channels are evenly spaced a third of a cycle apart, giving the most rainbow-like hues.
  • Small values (0.1–0.5) give gray stripes with red and blue fringes on their edges.

The shift is in phase, so the same offset gives the same colors at any frequency.

offset = 00
offset = 0.50.5
offset = 11
offset = 2.09442.0944
offset = 3.14163.1416
offset = -1-1
offset = 6.28326.2832
Sweep of offset (still at time = 0): osc(20, 0, {offset}).out() · outlined tile = default
  • Stripes are counted per screen width and never aspect-corrected: on a 16 : 9 canvas the same osc has wider stripes (in pixels) than on a square one.
  • Changing frequency or sync while the clock has been running a long time makes the pattern jump: both multiply time inside the phase. Animate offset or use a function argument that keeps its own accumulator if you need smooth changes.
  • Wrapping an osc (with scroll, repeat, src(o0) feedback) shows a seam unless the stripe count is a whole number: use frequency = 2 * Math.PI * n. See the scroll quirk.
  • scroll: move the stripes by a fixed amount, or vertically.
  • rotate: turn the stripes; rotate(Math.PI / 2, 0) makes them horizontal.
  • Coordinate space: what xx is and why it runs 0–1 across any canvas.
  • Arguments & time: what time is, and passing arrays or functions as offset.

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.