Aurora veil
Flowing curtains of colored light.
Customize
import {Scene, Video} from "odori";
import {AuroraVeil} from "../components/aurora-veil/aurora-veil";
<Video>
<Scene id="aurora-veil" duration="9s">
<AuroraVeil
height={0.65}
speed={1}
bands={3}
/>
</Scene>
</Video>Installation
pnpm odori add component aurora-veilnpx odori add component aurora-veilbunx odori add component aurora-veilEdit the source in videos/components/aurora-veil/. Use odori diff to compare upstream changes and odori update to apply updates.
Copy these files into your project
videos/components/aurora-veil/aurora-veil.tsx
import {Fill, useBrand, useVideo} from "odori";
import {EffectSurface, defineShaderEffect, numberUniform, vec3Uniform} from "odori/effects";
/**
* Curtains of sky light, folding slowly overhead.
*
* An aurora is vertical structure moving horizontally: tall ribbons whose
* brightness comes from how nearly the viewer looks along the curtain. Here
* each curtain is a column of warped noise, lifted where the field runs
* steep, and the colour walks between the two ends as the curtain climbs.
* Everything drifts from the frame clock, so a long shot keeps travelling
* instead of looping.
*
* `height` is the control worth reaching for: low keeps a band of light near
* the horizon for titles to sit above, high fills the frame for a cold open.
*/
export type AuroraVeilProps = {
/** Base of the curtain and its tip. Defaults to brand accent over background. */
colors?: [string, string];
/** How much of the frame the curtains climb. */
height?: number;
/** Drift speed of the folds. */
speed?: number;
/** How many curtains share the frame. */
bands?: 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 aurora = defineShaderEffect({
name: "aurora-veil",
uniforms: {
height: numberUniform(0.65),
speed: numberUniform(1),
bands: numberUniform(3),
base: vec3Uniform([0.1, 0.9, 0.6]),
tip: vec3Uniform([0.4, 0.2, 0.8]),
},
fragmentShader: `
uniform float height;
uniform float speed;
uniform float bands;
uniform vec3 base;
uniform vec3 tip;
float value(vec2 p) {
vec2 cell = floor(p);
vec2 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
return mix(
mix(hash(cell), hash(cell + vec2(1.0, 0.0)), f.x),
mix(hash(cell + vec2(0.0, 1.0)), hash(cell + vec2(1.0, 1.0)), f.x),
f.y);
}
void main() {
vec2 uv = gl_FragCoord.xy / resolution;
float t = seconds * 0.1 * speed;
vec3 colour = vec3(0.0);
for (float band = 0.0; band < 6.0; band += 1.0) {
if (band >= bands) break;
float depth = band / max(bands, 1.0);
/* Where this curtain hangs: a slow ridge along x gives it a wandering
lower edge, and the ridge's strength decides how bright it burns. */
float ridge = value(vec2(uv.x * (1.2 + depth) + band * 13.0, t * (1.0 + depth * 0.5)));
float lower = 0.12 + depth * 0.1 + ridge * 0.15;
float upper = lower + height * (0.35 + ridge * 0.65);
/* Tall vertical falloff: sharp at the base, long fade to the tip. */
float profile = smoothstep(lower, lower + 0.04, uv.y) * (1.0 - smoothstep(lower + 0.05, upper, uv.y));
profile += smoothstep(lower, lower + 0.02, uv.y) * (1.0 - smoothstep(lower, upper, uv.y)) * 0.6;
/* The folds: fine vertical striping that drifts against the ridge. */
float stripe = 0.55 + 0.45 * value(vec2(uv.x * 22.0 - t * 3.0 + band * 31.0, uv.y * 1.5));
float strength = profile * stripe * (0.35 + ridge * 0.8) * (1.0 - depth * 0.4);
float climb = clamp((uv.y - lower) / max(upper - lower, 0.001), 0.0, 1.0);
colour += mix(base, tip, climb) * strength;
}
colour = colour / (1.0 + colour * 0.6);
colour += (hash(gl_FragCoord.xy + frame) - 0.5) * 0.02;
odoriColour = vec4(colour, 1.0);
}`,
});
export const AuroraVeil = ({colors, height = 0.65, speed = 1, bands = 3}: AuroraVeilProps) => {
const brand = useBrand();
const {width, height: frameHeight} = useVideo();
const [base, tip] = colors ?? [brand.colors.accent, brand.colors.muted];
return (
<EffectSurface effects={[aurora({height, speed, bands, base: hexToRgb(base), tip: hexToRgb(tip)})]}>
<Fill style={{background: brand.colors.background, height: frameHeight, width}} />
</EffectSurface>
);
};
videos/components/aurora-veil/aurora-veil.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {AuroraVeil} from "./aurora-veil";
export default defineComponentPreview({
title: "Aurora veil",
category: "Backgrounds",
description: "Flowing curtains of colored light.",
component: AuroraVeil,
canvas: {width: 1920, height: 1080, duration: "9s"},
controls: {
height: {type: "number", defaultValue: 0.65, min: 0.2, max: 1, step: 0.05},
speed: {type: "number", defaultValue: 1, min: 0, max: 3, step: 0.1},
bands: {type: "number", defaultValue: 3, min: 1, max: 6, step: 1},
},
examples: [
{name: "Brand", props: {}},
{name: "Horizon band", props: {height: 0.3, bands: 2}},
{name: "Polar night", props: {colors: ["#2dd4bf", "#818cf8"], height: 0.85, bands: 5}},
],
});
Add it to a scene
import {Scene, Video} from "odori";
import {AuroraVeil} from "../components/aurora-veil/aurora-veil";
<Video>
<Scene id="aurora-veil" duration="9s">
<AuroraVeil
height={0.65}
speed={1}
bands={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
- 270 frames · 9s
- Minimum duration
- 30 frames · 1s
- Entrance
- 0 frames
- Exit
- 0 frames
- Reduced motion
- set speed to 0 and the curtains hold still
- Requires
- Odori
- Content limits
- none
Props
| Prop | Type | Default | Required |
|---|---|---|---|
colorsBase of the curtain and its tip. Defaults to brand accent over background. | [string, string] | — | — |
heightHow much of the frame the curtains climb. | number | 0.65 | — |
speedDrift speed of the folds. | number | 1 | — |
bandsHow many curtains share the frame. | number | 3 | — |
Related components
All componentsBead 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.Glass causticsShifting patterns of light beneath water.