Noise field
Animated noise in your brand colors.
Customize
import {Scene, Video} from "odori";
import {NoiseField} from "../components/noise-field/noise-field";
<Video>
<Scene id="noise-field" duration="6s">
<NoiseField
speed={0.25}
scale={3}
contrast={0.3}
/>
</Scene>
</Video>Installation
pnpm odori add component noise-fieldnpx odori add component noise-fieldbunx odori add component noise-fieldEdit the source in videos/components/noise-field/. Use odori diff to compare upstream changes and odori update to apply updates.
Copy these files into your project
videos/components/noise-field/noise-field.tsx
import {Fill, useBrand, useVideo} from "odori";
import {EffectSurface, defineShaderEffect, numberUniform, vec3Uniform} from "odori/effects";
/**
* Flowing fractal noise, for a background with structure rather than a blur.
*
* Where a gradient field is four soft centres, this is layered value noise:
* wisps and filaments that fold through each other. It reads as smoke, tide,
* or contour depending on how hard the contrast is driven, which is the one
* control worth having.
*
* The flow is a function of `seconds`, so a longer cut travels further rather
* than looping, and the noise itself comes from the prelude's integer hash so
* two machines agree on it.
*/
export type NoiseFieldProps = {
/** Two colours the noise sits between. Defaults to the brand. */
colors?: [string, string];
/** How fast the field folds through itself. */
speed?: number;
/** Size of the largest features, in screens. */
scale?: number;
/** 0 is a soft cloud, 1 is hard contour lines. */
contrast?: number;
};
const hexToRgb = (hex: string): [number, number, number] => {
const clean = hex.replace("#", "");
const full = clean.length === 3 ? [...clean].map((c) => c + c).join("") : clean;
const value = Number.parseInt(full.slice(0, 6), 16);
return [((value >> 16) & 255) / 255, ((value >> 8) & 255) / 255, (value & 255) / 255];
};
const field = defineShaderEffect({
name: "noise-field",
uniforms: {
speed: numberUniform(0.25),
scale: numberUniform(3),
contrast: numberUniform(0.3),
low: vec3Uniform([0.04, 0.05, 0.09]),
high: vec3Uniform([0.45, 0.55, 0.85]),
},
fragmentShader: `
uniform float speed;
uniform float scale;
uniform float contrast;
uniform vec3 low;
uniform vec3 high;
/* Value noise: the integer hash at four lattice corners, smoothed between.
Not gradient noise, which needs more taps for a look this will blur anyway. */
float value(vec2 p) {
vec2 cell = floor(p);
vec2 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float a = hash(cell);
float b = hash(cell + vec2(1.0, 0.0));
float c = hash(cell + vec2(0.0, 1.0));
float d = hash(cell + vec2(1.0, 1.0));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
/* Five octaves, each half the size and half the weight of the last. */
float fbm(vec2 p) {
float sum = 0.0;
float weight = 0.5;
for (int i = 0; i < 5; i++) {
sum += value(p) * weight;
p = p * 2.03 + 17.3;
weight *= 0.5;
}
return sum;
}
void main() {
vec2 uv = gl_FragCoord.xy / resolution;
uv.x *= resolution.x / resolution.y;
float t = seconds * speed;
/* Domain warping: the field is sampled at a position the field itself
decides, which is what turns smooth noise into something that curls. */
vec2 q = vec2(fbm(uv * scale + t), fbm(uv * scale + vec2(5.2, 1.3) - t));
float n = fbm(uv * scale + q * 1.8);
/* Contrast pushes the midpoint into a hard edge, so the same field reads as
cloud at 0 and as a contour map at 1. */
n = mix(n, smoothstep(0.45, 0.55, n), contrast);
odoriColour = vec4(mix(low, high, clamp(n, 0.0, 1.0)), 1.0);
}`,
});
export const NoiseField = ({colors, speed = 0.25, scale = 3, contrast = 0.3}: NoiseFieldProps) => {
const brand = useBrand();
const {width, height} = useVideo();
const [low, high] = colors ?? [brand.colors.background, brand.colors.accent];
return (
<EffectSurface effects={[field({speed, scale, contrast, low: hexToRgb(low), high: hexToRgb(high)})]}>
<Fill style={{background: brand.colors.background, height, width}} />
</EffectSurface>
);
};
videos/components/noise-field/noise-field.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {NoiseField} from "./noise-field";
export default defineComponentPreview({
title: "Noise field",
category: "Backgrounds",
description: "Animated noise in your brand colors.",
component: NoiseField,
canvas: {width: 1920, height: 1080, duration: "6s"},
controls: {
speed: {type: "number", defaultValue: 0.25, min: 0, max: 1.5, step: 0.05},
scale: {type: "number", defaultValue: 3, min: 1, max: 10, step: 0.5},
contrast: {type: "number", defaultValue: 0.3, min: 0, max: 1, step: 0.05},
},
examples: [
{name: "Brand", props: {}},
{name: "Contour", props: {contrast: 1, scale: 4}},
{name: "Tide", props: {colors: ["#04121a", "#4fd1c5"], scale: 2, speed: 0.15}},
],
});
Add it to a scene
import {Scene, Video} from "odori";
import {NoiseField} from "../components/noise-field/noise-field";
<Video>
<Scene id="noise-field" duration="6s">
<NoiseField
speed={0.25}
scale={3}
contrast={0.3}
/>
</Scene>
</Video>Timing contract
Use these durations and content limits when composing a scene. Run odori test to check your video.
- Family
- Backgrounds
- Recommended duration
- 180 frames · 6s
- Minimum duration
- 30 frames · 1s
- Entrance
- 0 frames
- Exit
- 0 frames
- Reduced motion
- set speed to 0 and the field holds still
- Requires
- Odori
- Content limits
- none
Props
| Prop | Type | Default | Required |
|---|---|---|---|
colorsTwo colours the noise sits between. Defaults to the brand. | [string, string] | — | — |
speedHow fast the field folds through itself. | number | 0.25 | — |
scaleSize of the largest features, in screens. | number | 3 | — |
contrast0 is a soft cloud, 1 is hard contour lines. | number | 0.3 | — |
Related components
All componentsAurora veilFlowing curtains of colored light.Bead fieldA fixed grid of dots that swell as two drifting ripples cross them.Deep swellOcean waves beneath a sunlit horizon.Dither fieldA moving gradient with pixelated shading.Dust motesSoft particles drifting across the frame.Gel washBlurred bands of color over a dark background.