Molten flow
Flowing metallic ripples.
Customize
import {Scene, Video} from "odori";
import {MoltenFlow} from "../components/molten-flow/molten-flow";
<Video>
<Scene id="molten-flow" duration="6s">
<MoltenFlow
colors={[
"#050507",
"#f4f6fa"
]}
speed={0.12}
scale={2.5}
shine={48}
/>
</Scene>
</Video>Installation
pnpm odori add component molten-flownpx odori add component molten-flowbunx odori add component molten-flowEdit the source in videos/components/molten-flow/. Use odori diff to compare upstream changes and odori update to apply updates.
Copy these files into your project
videos/components/molten-flow/molten-flow.tsx
import {Fill, useBrand, useVideo} from "odori";
import {EffectSurface, defineShaderEffect, numberUniform, vec3Uniform} from "odori/effects";
/**
* Liquid metal: warped noise lit as a surface rather than coloured as a field.
*
* The same domain-warped fbm as `noise-field`, but instead of mapping height to
* colour, the height becomes a relief and a light grazes it. The highlight band
* only appears where the slope faces the light, which is what separates metal
* from smoke — a highlight that moves across a fold as the fold moves, rather
* than a bright region that is simply bright.
*
* `shine` is the control worth having: it is the specular exponent, so low is
* brushed pewter and high is a mirror catching one line of light.
*/
export type MoltenFlowProps = {
/** Dark base and highlight. Defaults to the brand's background and accent. */
colors?: [string, string];
/** How fast the surface folds. */
speed?: number;
/** Size of the largest folds, in screens. */
scale?: number;
/** Specular sharpness. Low is brushed, high is chrome. */
shine?: 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: "molten-flow",
uniforms: {
speed: numberUniform(0.12),
scale: numberUniform(2.5),
shine: numberUniform(24),
base: vec3Uniform([0.02, 0.02, 0.03]),
light: vec3Uniform([0.85, 0.88, 0.95]),
},
fragmentShader: `
uniform float speed;
uniform float scale;
uniform float shine;
uniform vec3 base;
uniform vec3 light;
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);
}
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;
}
/* The warped surface, as a height. Factored out because the lighting needs to
sample it three times to know which way it leans. */
float height(vec2 uv, float t) {
vec2 q = vec2(fbm(uv + t), fbm(uv + vec2(4.7, 2.1) - t * 0.8));
return fbm(uv + q * 2.2);
}
void main() {
vec2 uv = gl_FragCoord.xy / resolution;
uv.x *= resolution.x / resolution.y;
uv *= scale;
float t = seconds * speed;
/* A finite-difference normal. The epsilon is in noise space, not pixels, so
the shading does not change character with the output size. */
float e = 0.02;
float h = height(uv, t);
float hx = height(uv + vec2(e, 0.0), t);
float hy = height(uv + vec2(0.0, e), t);
vec3 normal = normalize(vec3((h - hx) / e, (h - hy) / e, 1.2));
vec3 lightDirection = normalize(vec3(-0.45, 0.65, 0.55));
float diffuse = max(dot(normal, lightDirection), 0.0);
/* Blinn half-vector against a viewer straight on, which is enough: the
surface is flat, only the relief pretends otherwise. */
vec3 half_ = normalize(lightDirection + vec3(0.0, 0.0, 1.0));
float specular = pow(max(dot(normal, half_), 0.0), shine);
vec3 colour = base + light * (diffuse * 0.14 + specular * 0.85);
odoriColour = vec4(colour, 1.0);
}`,
});
export const MoltenFlow = ({colors, speed = 0.12, scale = 2.5, shine = 24}: MoltenFlowProps) => {
const brand = useBrand();
const {width, height} = useVideo();
const [base, light] = colors ?? [brand.colors.background, brand.colors.accent];
return (
<EffectSurface effects={[field({speed, scale, shine, base: hexToRgb(base), light: hexToRgb(light)})]}>
<Fill style={{background: brand.colors.background, height, width}} />
</EffectSurface>
);
};
videos/components/molten-flow/molten-flow.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {MoltenFlow} from "./molten-flow";
export default defineComponentPreview({
title: "Molten flow",
category: "Backgrounds",
description: "Flowing metallic ripples.",
component: MoltenFlow,
canvas: {width: 1920, height: 1080, duration: "6s"},
controls: {
speed: {type: "number", defaultValue: 0.12, min: 0, max: 0.6, step: 0.02},
scale: {type: "number", defaultValue: 2.5, min: 1, max: 8, step: 0.5},
shine: {type: "number", defaultValue: 24, min: 4, max: 96, step: 4},
},
examples: [
{name: "Chrome", props: {colors: ["#050507", "#f4f6fa"], shine: 48}},
{name: "Molten", props: {colors: ["#160804", "#ff9a3d"], shine: 12, speed: 0.18}},
{name: "Brand", props: {}},
],
});
Add it to a scene
import {Scene, Video} from "odori";
import {MoltenFlow} from "../components/molten-flow/molten-flow";
<Video>
<Scene id="molten-flow" duration="6s">
<MoltenFlow
colors={[
"#050507",
"#f4f6fa"
]}
speed={0.12}
scale={2.5}
shine={48}
/>
</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 surface holds one lit still
- Requires
- Odori
- Content limits
- none
Props
| Prop | Type | Default | Required |
|---|---|---|---|
colorsDark base and highlight. Defaults to the brand's background and accent. | [string, string] | — | — |
speedHow fast the surface folds. | number | 0.12 | — |
scaleSize of the largest folds, in screens. | number | 2.5 | — |
shineSpecular sharpness. Low is brushed, high is chrome. | number | 24 | — |
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.