Skip to content

FreeIn reviewv1.0.0

Contour Lines

A survey map: shifting contours with heavier index lines over soft hill shading.
npx shadcn@latest add @vibebox/contour-lines

Controls

1
1
<ContourLines />

About

The height is noise; contours are the places where fract(height * levels) is half, found with the screen-space derivative of the same value, so the line is a constant number of pixels wide at any resolution. Every fifth level is an index line, thicker and in its own colour. The slope of the field, from the derivatives, shades the hills from one side. The pointer adds a Gaussian hill.

Use it when

  • A property, surveying, civil, mapping, outdoor or trail business wants its trade in the hero.
  • A software or data product wants a technical, quiet texture.
  • An adviser wants a map-like calm.

Skip it when

  • The page already has lines, grids or tables behind the text.
  • The brand is soft and decorative.

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/contour-lines
  3. 3Use it:
    import { ContourLines } from "@/components/vibebox/contour-lines";
    
    <ContourLines preset="charcoal" className="min-h-[560px] rounded-xl">
      <div className="p-12 text-white">
        <h1 className="text-4xl font-semibold">Know the land before you build.</h1>
      </div>
    </ContourLines>

Code

3 files
components/vibebox/contour-lines/contour-lines.client.tsx
/**
 * VibeBox Motion · Contour Lines
 * 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 { contourLinesPiece, presets, type ContourLinesPreset } from "./contour-lines.shader";

export type ContourLinesColor = "paper" | "line" | "index" | "tint";
export type ContourLinesValue = "levels" | "scaleC" | "width" | "shade";

export type ContourLinesProps = {
  /** What sits on the scene: a heading, a call to action, a whole section. */
  children?: ReactNode;
  /** A named look: `charcoal`, `blueprint`, `sand`, `steel`, `forest`. */
  preset?: ContourLinesPreset;
  /** Overrides single palette colours with any CSS colour. */
  colors?: Partial<Record<ContourLinesColor, 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<ContourLinesValue, number>>;

const POSTER: PosterSpec = { type: "linear", from: "paper", to: "tint", angle: 135 };

export function ContourLines({ children, preset, colors, speed, intensity, interactive = true, label, still, className, levels, scaleC, width, shade }: ContourLinesProps) {
  return (
    <BackdropCanvas
      slug="contour-lines"
      piece={contourLinesPiece}
      presets={presets}
      fallback="charcoal"
      preset={preset}
      colors={colors}
      values={{ levels, scaleC, width, shade }}
      speed={speed}
      intensity={intensity}
      poster={POSTER}
      scale={0.5}
      stillTime={3}
      still={still}
      interactive={interactive}
      label={label}
      className={className}
    >
      {children}
    </BackdropCanvas>
  );
}
components/vibebox/contour-lines/contour-lines.shader.ts
/**
 * VibeBox Motion · Contour Lines
 * 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 survey map: a slowly shifting height field drawn as contour lines, every fifth one heavier,
 * over soft hill shading. The line width is measured in pixels with screen-space derivatives, so
 * it stays crisp at any size. The pointer raises a small hill under it.
 */

const SCENE = /* glsl */ `
uniform vec3 uPaper;
uniform vec3 uLine;
uniform vec3 uIndex;
uniform vec3 uTint;
uniform float uLevels;
uniform float uScaleC;
uniform float uWidth;
uniform float uShade;
uniform float uSpeed;
uniform float uIntensity;

float T;
vec2 pointerP;

float heightAt(vec2 p) {
  float h = fbm(p * uScaleC + vec2(T * 0.02, -T * 0.015));
  h = h * 0.85 + 0.15 * vnoise(p * uScaleC * 0.4 + 7.0);
  float d = length(p - pointerP);
  h += uHover * 0.22 * exp(-d * d * 5.0);
  return h;
}

void main() {
  vec2 uv = centred(gl_FragCoord.xy);
  T = uTime * uSpeed;
  float aspect = uResolution.x / uResolution.y;
  pointerP = vec2((uPointer.x - 0.5) * aspect, uPointer.y - 0.5);
  float h = heightAt(uv);
  float f = h * uLevels;

  // Distance to the nearest contour, in pixels.
  float w = max(fwidth(f), 1e-4);
  float dist = abs(fract(f - 0.5) - 0.5) / w;
  float line = 1.0 - smoothstep(uWidth * 0.5, uWidth * 0.5 + 1.0, dist);
  // Every fifth contour is an index line.
  float idx = mod(floor(f + 0.5), 5.0) < 0.5 ? 1.0 : 0.0;
  float indexLine = (1.0 - smoothstep(uWidth * 0.9, uWidth * 0.9 + 1.0, dist)) * idx;

  // Hill shading from the slope.
  vec2 g = vec2(dFdx(h), dFdy(h)) * uResolution.y * 0.35;
  float shade = clamp(0.5 + dot(g, normalize(vec2(-0.6, 0.8))) * 2.0, 0.0, 1.0);

  vec3 col = lin(uPaper);
  col = mix(col, lin(uTint), smoothstep(0.35, 0.8, h) * 0.55);
  col *= mix(1.0, 0.72 + 0.5 * shade, uShade);
  col = mix(col, lin(uLine), clamp(line, 0.0, 1.0) * 0.75);
  col = mix(col, lin(uIndex), clamp(indexLine, 0.0, 1.0));

  col *= uIntensity;
  col *= 1.0 - 0.18 * smoothstep(0.6, 1.2, 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 = ["charcoal", "blueprint", "sand", "steel", "forest"] as const;
export type ContourLinesPreset = (typeof PRESET_NAMES)[number];

export const presets: Record<ContourLinesPreset, ScenePreset> = {
  charcoal: {
    colors: { paper: "#0b0d12", line: "#5d6578", index: "#cfd6e6", tint: "#1b2236" },
    values: { levels: 22, scaleC: 1.6, width: 1.1, shade: 0.5, speed: 0.6, intensity: 1 },
  },
  blueprint: {
    colors: { paper: "#0f3a8a", line: "#6fa3ff", index: "#ffffff", tint: "#1450c0" },
    values: { levels: 26, scaleC: 1.5, width: 1.1, shade: 0.35, speed: 0.5, intensity: 1 },
  },
  sand: {
    colors: { paper: "#f1e6cf", line: "#b89868", index: "#7a5a2c", tint: "#e0cca4" },
    values: { levels: 20, scaleC: 1.5, width: 1.2, shade: 0.7, speed: 0.4, intensity: 1 },
  },
  steel: {
    colors: { paper: "#c9d1da", line: "#7c8794", index: "#2a323d", tint: "#aeb9c5" },
    values: { levels: 24, scaleC: 1.7, width: 1.1, shade: 0.6, speed: 0.5, intensity: 1 },
  },
  forest: {
    colors: { paper: "#0c1f17", line: "#2f6b4d", index: "#9fe0b8", tint: "#123826" },
    values: { levels: 22, scaleC: 1.6, width: 1.1, shade: 0.5, speed: 0.55, intensity: 1 },
  },
};

export const contourLinesPiece = createScenePiece("contour-lines", fragment);
components/vibebox/contour-lines/index.tsx
/**
 * VibeBox Motion · Contour Lines
 * 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 { ContourLines, type ContourLinesProps, type ContourLinesColor, type ContourLinesValue } from "./contour-lines.client";
export { PRESET_NAMES as CONTOUR_LINES_PRESETS, presets as contourLinesPresets, type ContourLinesPreset } from "./contour-lines.shader";

Props

PropTypeDefaultDescription
childrenReactNode—What sits on the scene: a heading, a call to action, a whole section.
preset"charcoal" | "blueprint" | "sand" | "steel" | "forest""charcoal"The look: charcoal, blueprint, sand, steel, forest.
colors{ paper?: string; line?: string; index?: string; tint?: string }—Overrides single palette colours with any CSS colour: paper (the map's paper), line (the contour lines), index (the heavier index lines), tint (the high ground).
speednumber1Multiplies the preset's speed; 0 holds the scene still.
intensitynumber1Multiplies the preset's brightness, 0.5 to 1.4.
levelsnumber22How many contour intervals (8 to 40; each look sets its own).
scaleCnumber1.6How large the landforms are (larger is finer) (0.8 to 3; each look sets its own).
widthnumber1.1The line width in pixels (0.6 to 2.4; each look sets its own).
shadenumber0.5How strong the hill shading is (0 to 1; each look sets its own).
interactivebooleantrueThe pointer raises a small hill under it.
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 moves the scene like the cursor does; scrolling is never blocked.
Reduced motion
One still frame of the map; the terrain does not shift.

Edge cases and limits

  • Index lines are the strongest detail: keep text off the thick ones when it can move.
  • Blueprint is saturated: use white text.
  • Very high levels on a phone becomes a texture; keep it under 28.
  • A mountain valley with green slopes, snowy peaks and a lake under a blue sky

    A flight over land: mountains, dunes, islands, ice or lava, with fog, a sun and a camera that leans to the pointer.

    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