design example · embeddable engine

simultané

A generative colour field that lives on one rule: every pixel sits on the arc between two hues locked exactly 180° apart, computed in Oklab so the transition never turns to grey mud. Below: the live engine, four tunings, and everything needed to embed it.

Named after Chevreul's law of simultaneous contrast (1839) and Sonia Delaunay, born in Hradyzk, who called her Paris studio Atelier Simultané.

000° signal 180° click any panel to hold / release

embed

The engine is a single dependency-free script exposing window.Opponent. Point it at a canvas; it handles DPR, resize, tab-hide, context loss, and adaptive quality on its own. This entire page runs on it — five instances.

<canvas id="bg" aria-hidden="true"></canvas>
<script src="opponent-engine.js"></script>
<script>
  var field = Opponent.mount(document.getElementById('bg'), {
    preset: 'ink',   // dark tuning, made to sit behind light text
    speed: 0.8
  });
</script>
#bg{
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: -1;
  pointer-events: none;
}

presets

fogchroma .52 · light
inkchroma .78 · dark floor
vividchroma 1.22 · faster

A preset is five uniforms and a speed — add one by appending to PRESETS at the top of the engine; the shader never changes.

api

mount optiondefaultmeaning
presetsignalsignal · fog · ink · vivid
huerandomstarting hue A in degrees; B is always A+180
speed1multiplier on all motion, including the ~5-minute hue walk; 0 = still
chroma1saturation multiplier (0.1–2) on top of the preset
seedrandomdeterministic phase, for tests and reproducible stills
dprCap1.5maximum devicePixelRatio rendered
onHue(a,b,cssA,cssB)fires when the rounded degree changes; css args are gamut-fitted rgb() strings
instance 
start() stop() hold() destroy()lifecycle; hold() toggles the clock, destroy() removes every listener
setPreset(n) cyclePreset()switch tunings live
setHue(d) nudgeHue(d)place or turn the complement axis
setSpeed(x) setChroma(x)live multipliers
hues()current pair as {a, b} degrees

The full page (opponent.html) also reads URL params — ?preset= &hue= &speed= &chroma= &seed= &ui=off, hash form works too — and binds keys: space hold, p palette, f fullscreen, arrows turn the wheel.

why it looks right