A DOM-native Web Component for the Hydra visual synthesizer.
Create generative visuals with Hydra in any HTML page.
<script type="module" src="https://cdn.jsdelivr.net/npm/hydra-element"></script>
<hydra-element>osc(10, 0.2, 0.5).out()</hydra-element>That's it β write Hydra code between the tags and you're live. Each element runs its own engine, so several on one page don't interfere.
Want to poke around without setting up a project? Open the CodePen example for a ready-to-edit playground and quick tests.
<script type="module" src="https://cdn.jsdelivr.net/npm/hydra-element"></script>or
npm install hydra-element # or pnpm / yarnimport 'hydra-element'The class is also exported if you need it directly:
import { HydraElement } from 'hydra-element'The full Hydra DSL works between the tags (osc(), noise(), solid(),
setFunction(), sources s0βs3, outputs o0βo3, time, bpm, speed,
mouse) β no synth. prefix needed. await works too, so async sources and
loadScript(...) are supported.
<hydra-element>
osc(30, 0.01, 1)
.mult(osc(() => 100 * Math.sin(time * 0.1), -0.1, 1).modulate(noise(3, 1)).rotate(0.7))
.blend(src(s0))
.posterize([3, 10, 2].fast(0.5).smooth(1))
.out()
</hydra-element>Change the scene from JS:
document.querySelector('hydra-element').code = 'osc(20).out()'Or drive the synth directly once it's ready:
const el = document.querySelector('hydra-element')
const { synth } = await el.ready
synth.s0.initImage('...')
synth.bpm = 120This is not a sandbox β code runs in your page, so only evaluate code you trust.
Feed values from your page into the sketch without touching globalThis.
Bound names can also shadow the engine's own built-ins (time, width, speed, mouse, a β¦),
so you can override those or add entirely new ones:
const el = document.querySelector('hydra-element')
const slider = document.querySelector('#freq')
// static β a pinned value; shadows the engine's own `speed` read
el.bind('speed', 1.5)
// live β a getter re-read on every access: moving the slider updates the scene
el.bindLive('freq', () => Number(slider.value))
// remove it; the engine's own `speed` read applies from then on
el.unbind('speed')<hydra-element>osc(() => freq, 0.1, speed).out()</hydra-element>
<input id="freq" type="range" min="1" max="120" value="30">Parameters re-evaluate per frame only when passed as functions: osc(freq, β¦)
pins the value at eval time, osc(() => freq, β¦) stays live. A bound getter is
read-only inside the sketch β assigning it throws.
| Attribute | Default | What it does |
|---|---|---|
width / height |
CSS | Canvas backing size in pixels (overrides the CSS size). |
dpr |
2 |
Cap for the device-pixel-ratio used by auto-sized canvases. |
precision |
default | Shader precision: highp, mediump, lowp. |
sources / outputs |
4 |
Number of source/output buffers (0β16). Extra buffers are s4, s5, β¦. |
audio |
false |
Enable audio analysis (a.fft, β¦) β requests microphone access. |
global |
false |
Keep Hydra globals on window. Use at most one per document. |
loop |
true |
Whether the element drives its own render loop. |
Auto-sized canvases follow the layout via ResizeObserver and scale by
min(devicePixelRatio, dpr), so they stay sharp on retina. Changing
width/height/dpr resizes in place β no engine recreation.
Turn loop off and drive frames yourself with tick:
<hydra-element loop="false"></hydra-element>const el = document.querySelector('hydra-element')
function frame(now) {
el.tick(now - last)
last = now
requestAnimationFrame(frame)
}You can toggle loop at runtime too β it starts/stops the loop without
recreating the engine.
| Member | Type | Description |
|---|---|---|
code |
get/set | The scene source. Setting it (re)evaluates the sketch. |
ready |
get (read-only) | Promise<{ synth }> that resolves once the engine is initialized. |
tick(dt) |
method | Manual frame tick (ms) β used when loop="false". |
canvas |
get/set | The backing <canvas>. Assign your own to take over rendering. |
synth |
get (read-only) | The hydra-synth engine (el.synth.osc, el.synth.s0, β¦). |
transforms |
get/set | Array of custom GLSL functions (setFunction under the hood). |
pb |
get/set | An rtc-patch-bay instance for streaming (recreates the engine). |
scope |
get | The persistent eval scope β bare assignments, bound values, and _hydra/hydraSynth live here. |
bind(name, value) |
method | Binds a static value into the eval scope; wins over live engine-owned reads (time, width, β¦). |
bindLive(name, fn) |
method | Binds a getter re-read on every access (read-only inside the sketch). |
unbind(name) |
method | Removes a previously bound value or live getter. |
loadScript(url) |
method | Loads an extension script, scoped to this element. |
destroy() |
method | Tears the element down (engine, loop, canvas) without removing it from the DOM. |
Bubbling CustomEvents dispatched on the element:
| Event | Detail |
|---|---|
hydra-eval |
{ success, error?, line? } β after each code assignment. |
hydra-ready |
{ synth } β after every engine (re)initialization. |
hydra-element-resize |
{ width, height } β when the canvas backing store resizes. |
el.addEventListener('hydra-eval', e => {
if (!e.detail.success) console.error(e.detail.error)
})The internal canvas and the audio analyzer are exposed as CSS parts:
hydra-element::part(canvas) {
border-radius: 0.5rem;
}
/* hide the audio analyzer overlay */
hydra-element::part(analyzer) {
display: none;
}Load any Hydra extension with loadScript β no global attribute needed. The
script is fetched and evaluated inside the element's scope:
<hydra-element>
await loadScript("https://cdn.jsdelivr.net/gh/geikha/hyper-hydra@latest/hydra-arithmetics.js")
osc(10,.1,2)
.mod(gradient().asin().cos())
.step(noise(2).unipolar().div(o0))
.blend(o0,.2)
.out()
</hydra-element>Note β extensions built for the classic single-global editor read
window._hydra,window.hydraSynth,window.update, etc. Alone they work fine, but across several isolated elements the bridge may resolve to the wrong engine. And anything global by nature β APIs exposed onwindowor UI appended todocument.body(MIDI monitor, audio analyzer, β¦) β can collide between elements.
Don't need the <hydra-element> tag in the page? The evaluation core is
available headless from hydra-element/context:
import Hydra from 'hydra-synth'
import { createContext, loadScript } from 'hydra-element/context'
const hydra = new Hydra({ canvas, makeGlobal: false })
const context = createContext(hydra)
context.bind('speed', 1.5)
context.bindLive('freq', () => 20 + 10 * Math.sin(Date.now() / 1000))
await context.eval('osc(() => freq, 0.1, speed).out()')
await loadScript('https://β¦/lib-noise.js', { hydra, scope: context.scope })By default the context also binds _hydra/hydraSynth into its scope; pass
{ editorGlobals: false } to createContext to skip that.
Exports: createContext, loadScript, hydraEval, userCodeLine (V8-only β parses error.stack frame format).
- ~16 WebGL contexts per browser β ~12+ elements on one page may hit it.
hydra-synthitself is only tested with 4 outputs; raiseoutputswith caution.
- Olivia Jack for creating Hydra π
- The Hydra community for the extensions and ecosystem that surround it π§©
See CONTRIBUTING.md and ARCHITECTURE.md.
