# Oddity
> Oddity is a browser studio for designing animated WebGL backgrounds —
> gradients, photos and print screens (halftone, ASCII, dither) stacked as
> one composition — and shipping them into a real site. Everything renders
> client-side; there is no account and no server-side rendering step.
Studio: https://oddity.design/studio
Human docs: https://oddity.design/docs
## Two ways to put a look in a page
1. **Angular component** (typed, editable in your codebase)
npm install oddity-angular # requires Angular 22 or newer
import { IOddityConfig, OddityCanvas } from "oddity-angular";
@Component({
selector: "hero-section",
imports: [OddityCanvas],
template: `
`,
})
export class HeroSection {
readonly look: IOddityConfig = { /* fields below */ };
}
2. **Embed** (any framework, or none)
The `look` attribute is the hash from a studio URL — everything after
the `#` in https://oddity.design/studio#... — so a link IS a look, and
the same string drives both paths.
Two optional attributes carry what a hash cannot. `image` takes a data
URI (the export inlines an uploaded photo there). `video` takes a URL
the HOST page serves — relative to that page, not to Oddity — and its
presence is what declares the source to be footage:
## Rules both paths follow
- The canvas fills its nearest positioned ancestor. Give that ancestor
`position: relative`; the canvas takes no space of its own.
- It sits behind sibling content and never receives pointer events, so it
cannot eat a click.
- It pauses when scrolled offscreen and when the tab is hidden — footage
included: a clip nobody is looking at stops decoding, it does not merely
stop being drawn.
- `prefers-reduced-motion: reduce` stops the animation; the look still
renders, as its resting frame. Footage holds on its first frame.
- Photos you uploaded in the studio travel inline as data URIs. Footage
does not travel at all: the studio's code export hands over the FILE
and points at it — `video="./clip.mp4"` in an embed, `imageSrc` plus
`imageKind: "vid"` in a config. Serve it yourself; cross-origin needs
`Access-Control-Allow-Origin`, since the frames become a WebGL texture.
## Writing a config by hand
`IOddityConfig` is a partial: **every field is optional and anything you
omit takes the default below.** A config therefore reads as the diff from
a plain gradient, which is also exactly what the studio's "Copy Angular
component" emits.
Two fields decide what is in the composition at all, and both are strings
of single-character codes:
fxOrder: which stages are stacked, bottom to top. Layer codes are
"G" Color, "I" Image, "P" Pattern; effect codes are listed per stage below.
hidden: codes present in the stack but not drawn.
A stage's own fields do nothing unless its code is in `fxOrder`.
## Effect stages and their fields
### Waves — code "w" (Distort)
waveMode: 0
waveSide: 0
waveAmount: 0.3
waveFrequency: 8
waveFalloff: 0.5
wavePhase: 0
waveX: 0.5
waveY: 0.5
### Glass — code "g" (Distort)
glassMode: 0
glassAmount: 0.5
glassCount: 24
glassRatio: 1
glassAngle: 0
glassProfile: 3
glassBevel: 0.04
glassShadow: 0.3
glassHighlight: 0.3
### Kaleidoscope — code "k" (Distort)
kaleidoSides: 6
kaleidoAngle: 0
kaleidoX: 0.5
kaleidoY: 0.5
kaleidoZoom: 1
kaleidoMirror: true
kaleidoSpin: 0
### Flow — code "f" (Distort)
flowAmount: 0
flowScale: 3
flowDetail: 3
flowAngle: 0
flowBias: 0
### Shear — code "z" (Distort)
shearBands: 12
shearAmount: 0
shearAngle: 0
shearSeed: 0
shearSpread: 0.5
### Mirror — code "y" (Distort)
mirrorCols: 1
mirrorRows: 1
mirrorX: 0
mirrorY: 0
### Light — code "l" (Light & lens)
lightMode: 0
lightAmount: 0.5
lightSize: 0.5
lightX: 0.5
lightY: 0.5
lightCount: 1
lightFollow: 0
light2X: 0.25
light2Y: 0.7
light3X: 0.75
light3Y: 0.7
light4X: 0.5
light4Y: 0.85
lightColor: "ffffff"
lightColor2: "ffffff"
lightColor3: "ffffff"
lightColor4: "ffffff"
lightGain: 1
lightGain2: 1
lightGain3: 1
lightGain4: 1
lightSpread: 1
lightSpread2: 1
lightSpread3: 1
lightSpread4: 1
### Lens — code "o" (Light & lens)
aberration: 0
softness: 0
lensDistort: 0
### Spectra — code "b" (Light & lens)
spectraAmount: 0
spectraScale: 2.5
spectraSpread: 0.5
spectraWidth: 0.35
### Iris — code "n" (Light & lens)
irisAmount: 0
irisCells: 5
irisBend: 0.6
irisRings: 5
irisWidth: 0.3
irisShape: 0
irisPalette: 0
### Threshold — code "x" (Color & tone)
thresholdLevel: 0.5
thresholdSoft: 0.02
thresholdInvert: false
### Duotone — code "u" (Color & tone)
duotoneShadow: "1b2a4a"
duotoneLight: "f0d9b5"
duotoneSteps: 1
duotoneBalance: 0
duotoneAmount: 0
### Grade — code "e" (Color & tone)
gradeExposure: 0
gradeContrast: 1
gradeSaturation: 1
gradeTemp: 0
gradeTint: 0
### Pixelate — code "p" (Pixels & print)
pixelate: 0
pixelateRatio: 1
pixelateGap: 0
pixelateGroutColor: ""
### Dither — code "d" (Pixels & print)
ditherAlgo: 0
ditherMatrix: 4
ditherScale: 4
ditherLevels: 4
ditherStrength: 1
ditherColorMode: 0
ditherThreshold: 0.5
ditherGamma: 1
ditherInvert: false
ditherRatio: 1
### CRT — code "v" (Pixels & print)
crtCurve: 0
crtScanSize: 3
crtScanDepth: 0
crtMask: 0
crtBoost: 0
### Halftone — code "h" (Pixels & print)
halftoneSize: 8
halftoneStyle: 0
halftoneStagger: false
halftoneDotScale: 0.85
halftoneColor: 1
halftonePaper: 0.2
halftonePaperColor: "ffffff"
halftoneEmboss: 0
halftoneWind: 0
halftoneWindAngle: 0
halftoneWobble: 0
halftoneGlint: 0
halftoneJitter: 0
halftoneJitterSeed: 0
halftoneAngle: 0
halftoneGamma: 1
halftoneInvert: false
halftoneRatio: 1
halftoneInkColor: ""
### ASCII — code "a" (Pixels & print)
asciiSize: 16
asciiRatio: 1
asciiStyle: 0
asciiColor: 1
asciiBackground: 0.2
asciiExposure: 1
asciiFill: 1
asciiDensity: 64
asciiScatter: 0
asciiGap: 0
asciiAngle: 0
asciiThreshold: 0.5
asciiGamma: 1
asciiInvert: false
asciiInkColor: ""
asciiPaperColor: "000000"
asciiOpacity: 1
asciiBlend: 0
### Contours — code "c" (Pixels & print)
contourLevels: 8
contourWeight: 2
contourDash: 0
contourGlow: 0
contourColor: 1
contourPaper: 0.2
contourInkColor: ""
contourPaperColor: ""
### Hatch — code "t" (Pixels & print)
toneGeoSize: 36
toneGeoLow: 0.4
toneGeoHigh: 0.6
toneGeoInvert: false
toneGeoRules: 3
toneGeoCross: 0
toneGeoAngle: 0
toneGeoGrid: 0
toneGeoJitter: 0
toneGeoJitterSeed: 0
toneGeoWeight: 0.5
toneGeoColor: 1
toneGeoPaper: 0.2
toneGeoInkColor: ""
toneGeoPaperColor: "ffffff"
### Grain — code "r" (Texture)
grain: 0
grainSize: 1
### Paper — code "s" (Texture)
paperAmount: 0
paperColor: ""
paperFibre: 1
paperVignette: 1
### Pointer — code "m" (Interact)
pointerMode: 0
pointerAmount: 0.5
pointerSize: 0.4
pointerFade: 0.5
pointerDamping: -1
pointerFlip: false
## Frame, canvas and layers
Everything no stage's key list claims: the frame, the color/image/pattern
layers and animation. **A field named after a stage belongs to that
stage** — `contour*` is Contours, `toneGeo*` is Hatch (those keys
predate the rename) — and only takes effect when that stage's code is in
`fxOrder`.
fxOrder: "G"
hidden: ""
shape: 4
stops: [{"color":"1e1b4b","pos":0},{"color":"4338ca","pos":25},{"color":"7c6ff0","pos":50},{"color":"cbb0f7","pos":75},{"color":"faf9f6","pos":100}]
seed: 128
warp: 0.8
noiseScale: 1.2
detail: 3
contrast: 1
angle: 45
centerX: 0.5
centerY: 0.5
gradientFollow: 0
gradientOpacity: 1
gradientBlend: 0
oklab: false
imageSrc: ""
imageKind: "img"
imageOpacity: 1
imageBlend: 0
imageBlur: 0
imageFit: 0
imageFocalX: 0.5
imageFocalY: 0.5
imageZoom: 1
patternSource: "l"
patternLayout: "b"
patternCols: 6
patternRows: 6
patternGap: 8
patternRadius: 16
patternGapColor: ""
patternSeed: 1
animate: false
animSpeed: 1
aspect: "1x1"
sizeAnchor: 1
## Getting a config you did not write
Design the look at https://oddity.design/studio, then Export →
Copy Angular component — the component above, config filled in
Copy embed code — the two-line embed
Copy look — a link that reopens the exact composition
An agent asked to "use this background" is usually being handed one of
those three. All of them describe the same composition.