Skip to content

FreeIn reviewv1.0.0

Terrain

A flight over land: mountains, dunes, islands, ice or lava, with fog, a sun and a camera that leans to the pointer.
npx shadcn@latest add @vibebox/terrain

Controls

1
1
<Terrain />

About

A heightfield ray-marched per pixel. One octave loop reads a noise value two ways, as a smooth sum and as a creased sum, and ridge blends them; a power curve flattens the valleys; the water is the same field clamped at a level. Far ground takes fewer octaves, normalised so the level holds, which is most of the speed. Colour is altitude and slope: low ground in grass, steep faces in rock with faint strata, snow above a line that wavers with the surface, a lighter shore where land meets water. A short march toward the sun gives a soft shadow; fog gathers distance into haze. With glow above zero the water becomes a dark crust with hot veins that pulse.

Use it when

  • A resort, trail, climbing, outdoor or property brand wants the place in the hero.
  • A travel or adventure page wants a flight over somewhere without a photo.
  • A logistics or energy page wants the volcanic or arctic look for contrast.

Skip it when

  • The page is dense and technical; use Contour Lines or Dot Waves.
  • The brand has nothing to do with places or the outdoors.

Install

  1. 1Add the VibeBox registry to components.json:
    {
      "registries": {
        "@vibebox": "https://motion.vibeboxph.com/r/{name}.json"
      }
    }
  2. 2Add the component with the shadcn CLI:
    npx shadcn@latest add @vibebox/terrain
  3. 3Use it:
    import { Terrain } from "@/components/vibebox/terrain";
    
    <Terrain preset="alpine" className="min-h-[560px] rounded-xl">
      <div className="p-12 text-white">
        <h1 className="text-4xl font-semibold">Above the valley, before the day starts.</h1>
      </div>
    </Terrain>

Code

3 files
components/vibebox/terrain/index.tsx
/**
 * VibeBox Motion · Terrain
 * Copyright (c) 2026 VibeBox. Licensed under the VibeBox Motion Free License:
 * https://motion.vibeboxph.com/license
 * Use it in unlimited personal, commercial and client projects.
 * Do not sell or redistribute it as a component, kit, template or builder.
 */
export { Terrain, type TerrainProps, type TerrainColor, type TerrainValue } from "./terrain.client";
export { PRESET_NAMES as TERRAIN_PRESETS, presets as terrainPresets, type TerrainPreset } from "./terrain.shader";
components/vibebox/terrain/terrain.client.tsx
/**
 * VibeBox Motion · Terrain
 * Copyright (c) 2026 VibeBox. Licensed under the VibeBox Motion Free License:
 * https://motion.vibeboxph.com/license
 * Use it in unlimited personal, commercial and client projects.
 * Do not sell or redistribute it as a component, kit, template or builder.
 */
"use client";

import type { ReactNode } from "react";
import { BackdropCanvas, type PosterSpec } from "@/lib/vibebox/backdrop-canvas";
import { terrainPiece, presets, type TerrainPreset } from "./terrain.shader";

export type TerrainColor = "haze" | "zenith" | "horizon" | "sun" | "grass" | "rock" | "snow" | "deep" | "shallow";
export type TerrainValue = "amp" | "ridge" | "power" | "water" | "snowLine" | "fog" | "sunAz" | "sunEl" | "glow" | "height";

export type TerrainProps = {
  /** What sits on the scene: a heading, a call to action, a whole section. */
  children?: ReactNode;
  /** A named look: `alpine`, `tropical`, `desert`, `arctic`, `volcanic`, `dusk`. */
  preset?: TerrainPreset;
  /** Overrides single palette colours with any CSS colour. */
  colors?: Partial<Record<TerrainColor, string>>;
  /** Multiplies the preset's speed (1 keeps it, 0 holds the scene still). */
  speed?: number;
  /** Multiplies the preset's brightness. */
  intensity?: number;
  /** The pointer and touch move the scene. */
  interactive?: boolean;
  /** Accessible name; leave it out when the scene is only a background. */
  label?: string;
  /** Hold one still frame until this is false (for a gallery card). */
  still?: boolean;
  className?: string;
} & Partial<Record<TerrainValue, number>>;

const POSTER: PosterSpec = { type: "horizon", top: "zenith", horizon: "horizon", groundTop: "rock", groundBottom: "grass", at: 48, glow: "sun", glowAt: [72, 40] };

export function Terrain({ children, preset, colors, speed, intensity, interactive = true, label, still, className, amp, ridge, power, water, snowLine, fog, sunAz, sunEl, glow, height }: TerrainProps) {
  return (
    <BackdropCanvas
      slug="terrain"
      piece={terrainPiece}
      presets={presets}
      fallback="alpine"
      preset={preset}
      colors={colors}
      values={{ amp, ridge, power, water, snowLine, fog, sunAz, sunEl, glow, height }}
      speed={speed}
      intensity={intensity}
      poster={POSTER}
      scale={0.6}
      stillTime={5}
      still={still}
      interactive={interactive}
      label={label}
      className={className}
    >
      {children}
    </BackdropCanvas>
  );
}
components/vibebox/terrain/terrain.shader.ts
/**
 * VibeBox Motion · Terrain
 * Copyright (c) 2026 VibeBox. Licensed under the VibeBox Motion Free License:
 * https://motion.vibeboxph.com/license
 * Use it in unlimited personal, commercial and client projects.
 * Do not sell or redistribute it as a component, kit, template or builder.
 */
import { compose, createScenePiece, type ScenePreset } from "@/lib/vibebox/engine/backdrops";

/*
 * A flight over land. The ground is a fractal heightfield ray-marched in the fragment shader:
 * valleys flattened by a power curve, ridges where the "ridge" value blends in creased noise, a
 * water plane at a level you choose. Colour comes from altitude and slope (grass low, rock on
 * steep faces, snow above a line), the sun lights it with a short soft shadow, and fog gathers the
 * distance into the horizon. The camera glides forward on its own and leans toward the pointer.
 */

const SCENE = /* glsl */ `
uniform vec3 uZenith;
uniform vec3 uHorizon;
uniform vec3 uHaze;
uniform vec3 uSun;
uniform vec3 uGrass;
uniform vec3 uRock;
uniform vec3 uSnow;
uniform vec3 uDeep;
uniform vec3 uShallow;
uniform float uAmp;
uniform float uRidge;
uniform float uPower;
uniform float uWater;
uniform float uSnowLine;
uniform float uFog;
uniform float uSunAz;
uniform float uSunEl;
uniform float uGlow;
uniform float uHeight;
uniform float uSpeed;
uniform float uIntensity;

float T;

/* The heightfield: one octave loop feeds a smooth sum and a creased sum (the same noise read two
   ways), blended by "ridge". Far ground takes fewer octaves, normalised so the level holds. */
float raw(vec2 p, float lod) {
  vec2 q = p * 0.36 + 4.0;
  float soft = 0.0, creased = 0.0, a = 0.5, wsum = 0.0;
  for (int i = 0; i < 5; i++) {
    float w = clamp(lod - float(i), 0.0, 1.0);
    if (w <= 0.0) break;
    float v = vnoise(q);
    soft += a * w * v;
    creased += a * w * (1.0 - abs(2.0 * v - 1.0));
    wsum += a * w;
    q = ROT * q * 2.03 + 11.7;
    a *= 0.5;
  }
  float h = mix(soft, creased * 0.92, uRidge) / max(wsum, 0.001);
  return pow(clamp(h, 0.0, 1.0), uPower) * uAmp;
}
float ground(vec2 p, float lod) { return max(raw(p, lod), uWater); }

vec3 skyColor(vec3 rd, vec3 L) {
  float y = clamp(rd.y, 0.0, 1.0);
  vec3 col = mix(lin(uHorizon), lin(uZenith), pow(y, 0.55));
  float s = max(dot(rd, L), 0.0);
  col += lin(uSun) * (pow(s, 5.0) * 0.14 + pow(s, 90.0) * 0.55 + pow(s, 1500.0) * 5.0);
  return col;
}

void main() {
  vec2 uv = centred(gl_FragCoord.xy);
  T = uTime * uSpeed;
  float lean = 0.3 + uHover;
  vec3 L = normalize(vec3(sin(uSunAz) * cos(uSunEl), sin(uSunEl), cos(uSunAz) * cos(uSunEl)));

  // The camera: forward over the land, wandering a little, never below the ground.
  vec3 ro = vec3((uPointer.x - 0.5) * 0.6 * lean + sin(T * 0.06) * 0.7, 0.0, T * 0.32);
  ro.y = max(ground(ro.xz, 5.0) + 0.1, uWater + 0.1) + uHeight;
  float yaw = sin(T * 0.045) * 0.22 + (uPointer.x - 0.5) * 0.3 * lean;
  float pitch = -0.07 - (uPointer.y - 0.5) * 0.14 * lean;
  vec3 f = normalize(vec3(sin(yaw), sin(pitch), cos(yaw)));
  vec3 r = normalize(cross(vec3(0.0, 1.0, 0.0), f));
  vec3 u = cross(f, r);
  vec3 rd = normalize(f + (uv.x * r + uv.y * u) * 1.2);

  vec3 fogCol = mix(lin(uHaze), lin(uSun) * 0.7, pow(max(dot(rd, L), 0.0), 7.0) * 0.45);
  vec3 col;
  float t = 0.03;
  float hit = -1.0;
  bool lookingAway = rd.y > 0.03 && ro.y > uAmp;
  if (!lookingAway) {
    for (int i = 0; i < 76; i++) {
      vec3 p = ro + rd * t;
      float lod = clamp(5.2 - 0.8 * log2(max(t, 0.9)), 2.0, 5.0);
      float d = p.y - ground(p.xz, lod);
      if (d < 0.0012 * t) { hit = t; break; }
      t += clamp(d * 0.45, 0.003 * t, 0.7);
      if (t > 20.0) break;
    }
    // A ray that skims the far ground runs out of steps before it lands. It was still close to
    // the surface, so it counts as a hit there; otherwise the horizon fills with sky-coloured slits.
    if (hit < 0.0 && t <= 20.0) hit = t;
  }

  if (hit < 0.0) {
    col = mix(skyColor(rd, L), fogCol, pow(1.0 - clamp(rd.y * 2.2, 0.0, 1.0), 3.0) * 0.8);
  } else {
    vec3 p = ro + rd * hit;
    float rh = raw(p.xz, 5.0);
    float e = 0.004 + 0.003 * hit;
    float h0 = ground(p.xz, 5.0);
    vec3 n = normalize(vec3(h0 - ground(p.xz + vec2(e, 0.0), 5.0), e * 2.2, h0 - ground(p.xz + vec2(0.0, e), 5.0)));
    bool water = rh <= uWater + 0.0015;
    if (water && uGlow > 0.01) {
      // Lava: a dark crust broken by hot veins that pulse, drawn on a warped field so they curve.
      vec2 lp = p.xz * 2.6 + vec2(T * 0.03, -T * 0.02);
      lp += 0.55 * vec2(gnoise(lp * 0.8), gnoise(lp * 0.8 + 9.0));
      float vein = 1.0 - abs(gnoise(lp * 1.3));
      float vein2 = 1.0 - abs(gnoise(lp * 3.1 + 7.0));
      float cracks = pow(clamp(vein, 0.0, 1.0), 14.0) + 0.5 * pow(clamp(vein2, 0.0, 1.0), 16.0);
      float pulse = 0.8 + 0.2 * sin(T * 1.3 + gnoise(lp) * 6.0);
      float crustT = 0.5 + 0.5 * gnoise(lp * 0.6 + 3.0);
      vec3 crust = mix(lin(uRock) * 0.35, lin(uDeep) * 0.55, crustT * crustT);
      vec3 hotCol = mix(lin(uDeep) * 2.0, lin(uShallow) * 2.2, clamp(cracks, 0.0, 1.0));
      col = mix(crust, hotCol * pulse, clamp(cracks * 1.4, 0.0, 1.0));
      col *= 0.7 + 0.5 * uGlow;
    } else if (water) {
      float depth = clamp((uWater - rh) / 0.35, 0.0, 1.0);
      vec3 wn = normalize(vec3(gnoise(p.xz * 5.0 + vec2(T * 0.3, 0.0)) * 0.07, 1.0, gnoise(p.xz * 5.0 + vec2(9.0, -T * 0.25)) * 0.07));
      vec3 refl = reflect(rd, wn);
      refl.y = abs(refl.y);
      float fres = 0.03 + 0.97 * pow(1.0 - max(dot(-rd, wn), 0.0), 5.0);
      vec3 wcol = mix(lin(uShallow), lin(uDeep), smoothstep(0.0, 0.9, depth));
      col = mix(wcol, skyColor(refl, L), fres);
      float foam = smoothstep(0.1, 0.0, depth) * (0.4 + 0.6 * vnoise(p.xz * 14.0 - T * 0.3));
      col = mix(col, vec3(0.9), foam * 0.35);
    } else {
      float alt = (rh - uWater) / max(uAmp - uWater, 0.001);
      // A little grain on the surface so near ground is not smooth clay.
      float near = 1.0 - smoothstep(3.0, 10.0, hit);
      vec2 bp = p.xz * 38.0;
      n = normalize(n + vec3(gnoise(bp), 0.0, gnoise(bp + 17.0)) * 0.07 * near);
      float slope = 1.0 - n.y;
      float detail = vnoise(p.xz * 9.0) * 0.5 + vnoise(p.xz * 27.0) * 0.25;
      float patches = vnoise(p.xz * 3.2) * 0.6 + vnoise(p.xz * 11.0) * 0.4;
      vec3 grass = mix(lin(uGrass) * 0.5, lin(uGrass) * 1.2, smoothstep(0.35, 0.65, patches)) * (0.8 + 0.4 * detail);
      float strata = 0.82 + 0.36 * vnoise(vec2(p.y * 26.0, (p.x + p.z) * 2.5));
      vec3 rock = lin(uRock) * (0.7 + 0.6 * detail) * strata;
      vec3 snow = lin(uSnow);
      float rockiness = smoothstep(0.1, 0.38, slope + alt * 0.25);
      vec3 alb = mix(grass, rock, rockiness);
      float snowMask = smoothstep(uSnowLine, uSnowLine + 0.12, alt + (n.y - 0.6) * 0.25 + detail * 0.1);
      alb = mix(alb, snow, snowMask);
      // A shore of lighter ground where the land meets the water.
      alb = mix(alb, mix(grass, snow, 0.35), smoothstep(0.035, 0.0, alt) * 0.55);
      float diff = max(dot(n, L), 0.0);
      // A short, soft shadow marched toward the sun.
      float shade = 1.0;
      for (int k = 1; k <= 4; k++) {
        vec3 s = p + n * 0.01 + L * (float(k) * 0.15);
        float d = s.y - ground(s.xz, 2.0);
        shade = min(shade, clamp(7.0 * d / (float(k) * 0.15), 0.0, 1.0));
      }
      vec3 amb = mix(lin(uHorizon), lin(uZenith), 0.4) * (0.62 + 0.5 * n.y);
      col = alb * (lin(uSun) * diff * shade * 1.35 + amb);
      // The lava's glow on low ground.
      col += lin(uShallow) * uGlow * 0.1 * smoothstep(0.3, 0.0, alt) * (0.4 + 0.6 * n.y);
    }
    float fogAmt = 1.0 - exp(-(hit * hit * 0.007 + hit * 0.03) * uFog);
    col = mix(col, fogCol, clamp(fogAmt, 0.0, 1.0));
  }

  col *= uIntensity;
  col *= 1.0 - 0.28 * smoothstep(0.5, 1.15, length(uv * vec2(0.85, 1.1)));
  fragColor = vec4(finish(col, gl_FragCoord.xy), 1.0);
}
`;

export const fragment = compose(SCENE);

export const PRESET_NAMES = ["alpine", "tropical", "desert", "arctic", "volcanic", "dusk"] as const;
export type TerrainPreset = (typeof PRESET_NAMES)[number];

export const presets: Record<TerrainPreset, ScenePreset> = {
  alpine: {
    colors: { haze: "#cfe0ee", zenith: "#356fc2", horizon: "#cfe2f2", sun: "#fff3d8", grass: "#4c7a3e", rock: "#7b7366", snow: "#f6f8fc", deep: "#0f3a58", shallow: "#3a9aa8" },
    values: { amp: 2.4, ridge: 0.72, power: 1.45, water: 0.34, snowLine: 0.46, fog: 0.5, sunAz: 0.7, sunEl: 0.5, glow: 0, height: 0.35, speed: 0.6, intensity: 1 },
  },
  tropical: {
    colors: { haze: "#cfeaf0", zenith: "#2a86d6", horizon: "#c4e8f0", sun: "#fffbe8", grass: "#2f8f4e", rock: "#8a7a5c", snow: "#f1e7c8", deep: "#06476e", shallow: "#2ed0c0" },
    values: { amp: 1.7, ridge: 0.18, power: 1.35, water: 0.52, snowLine: 3, fog: 0.4, sunAz: -0.5, sunEl: 0.8, glow: 0, height: 0.5, speed: 0.55, intensity: 1.05 },
  },
  desert: {
    colors: { haze: "#f4d9b0", zenith: "#4f86c8", horizon: "#fbe0b6", sun: "#fff4d6", grass: "#d6a066", rock: "#9c4f2c", snow: "#f6e0b6", deep: "#1f5f78", shallow: "#6cc1b0" },
    values: { amp: 1.4, ridge: 0.8, power: 1.2, water: -1, snowLine: 3, fog: 0.6, sunAz: 0.9, sunEl: 0.45, glow: 0, height: 0.14, speed: 0.6, intensity: 1 },
  },
  arctic: {
    colors: { haze: "#e0edf8", zenith: "#4b7fb8", horizon: "#e2eff9", sun: "#ffffff", grass: "#a9c6dc", rock: "#6f879d", snow: "#f7fafe", deep: "#0a3b5c", shallow: "#6fb6d8" },
    values: { amp: 1.8, ridge: 0.6, power: 1.4, water: 0.24, snowLine: 0.3, fog: 0.7, sunAz: 0.5, sunEl: 0.3, glow: 0, height: 0.15, speed: 0.5, intensity: 1 },
  },
  volcanic: {
    colors: { haze: "#4a2418", zenith: "#3a1420", horizon: "#ff8a45", sun: "#ffd29a", grass: "#4a3a36", rock: "#33262a", snow: "#6a5048", deep: "#c01c00", shallow: "#ffb52a" },
    values: { amp: 2, ridge: 0.85, power: 1.3, water: 0.85, snowLine: 3, fog: 0.7, sunAz: 0.9, sunEl: 0.12, glow: 1, height: 0.3, speed: 0.5, intensity: 1.1 },
  },
  dusk: {
    colors: { haze: "#e8a58f", zenith: "#26357a", horizon: "#ff9e7a", sun: "#ffd7a0", grass: "#3a5578", rock: "#5b4868", snow: "#ffd9d0", deep: "#16284f", shallow: "#c46a8a" },
    values: { amp: 2.2, ridge: 0.65, power: 1.45, water: 0.26, snowLine: 0.58, fog: 0.5, sunAz: 1.15, sunEl: 0.18, glow: 0, height: 0.25, speed: 0.55, intensity: 1.15 },
  },
};

export const terrainPiece = createScenePiece("terrain", fragment);

Props

PropTypeDefaultDescription
childrenReactNode—What sits on the scene: a heading, a call to action, a whole section.
preset"alpine" | "tropical" | "desert" | "arctic" | "volcanic" | "dusk""alpine"The look: alpine, tropical, desert, arctic, volcanic, dusk.
colors{ haze?: string; zenith?: string; horizon?: string; sun?: string; grass?: string; rock?: string; snow?: string; deep?: string; shallow?: string }—Overrides single palette colours with any CSS colour: haze (the distance fog's colour), zenith (the sky overhead), horizon (the sky at the horizon), sun (the sun and its light on the land), grass (the low ground (grass, sand, ice or ash)), rock (steep faces and cliffs), snow (high ground above the snow line), deep (deep water (or the lava's dark red)), shallow (shallow water (or the lava's bright yellow)).
speednumber1Multiplies the preset's speed; 0 holds the scene still.
intensitynumber1Multiplies the preset's brightness, 0.5 to 1.4.
ampnumber2.4How tall the land stands (0.8 to 3; each look sets its own).
ridgenumber0.720 for rolling land, 1 for creased peaks (0 to 1; each look sets its own).
powernumber1.45How flat the low ground is (higher is flatter) (0.9 to 2.4; each look sets its own).
waternumber0.34Where the water plane sits; below the land to have none (-1 to 1.4; each look sets its own).
snowLinenumber0.46How high the snow starts (3 for no snow) (0.1 to 3; each look sets its own).
fognumber0.5How fast the distance goes to haze (0.1 to 1.6; each look sets its own).
sunAznumber0.7Where the sun sits around the compass (-3.1 to 3.1; each look sets its own).
sunElnumber0.5How high the sun stands (0.04 to 1.2; each look sets its own).
glownumber00 for water, above 0 the water is molten (0 to 1.5; each look sets its own).
heightnumber0.35How high the camera flies above the land (0.05 to 1; each look sets its own).
interactivebooleantrueThe camera leans toward the pointer and swings a little further when it is over the scene.
labelstring—Accessible name; leave it out when the scene is only a background.
stillbooleanfalseHold one composed frame until false (a gallery card that plays on hover).
classNamestring—Merged with cn(); give it a size and a radius.

Accessibility and motion

Keyboard
Not focusable; content on it keeps its own focus order.
Screen readers
Decorative by default; with label it is an image with that name.
Touch
A finger leans the camera like the cursor does; scrolling is never blocked.
Reduced motion
One still frame of the land; the camera does not fly and the lava does not pulse.

Edge cases and limits

  • It is the heaviest background in the library: the stage settles to a still frame on slower phones.
  • The sun and snow are the brightest parts: keep text away from them or on a card.
  • A very low sun (dusk, volcanic) leaves the near ground in shadow by design.
  • Layers of snow-capped mountains fading into a warm haze

    Layers of mountains, dunes or forest receding into haze under a low sun.

    Backgrounds · WebGL2

  • Green and violet aurora curtains over a mountain ridge and stars

    Aurora

    Free

    Curtains of northern light over a night sky and a mountain ridge, drawn toward the pointer.

    Backgrounds · WebGL2

  • Warm amber bokeh lights of different sizes on a dark background

    Bokeh

    Free

    Out-of-focus lights in three depths, brighter at the rim and drifting with the pointer.

    Backgrounds · WebGL2

↑ ↓ to move↵ to openesc to close