Skip to content

FreeIn reviewv1.0.0

Studio

A seamless photo-studio sweep with a soft key light and an optional contact shadow: quiet by design.
npx shadcn@latest add @vibebox/studio

Controls

1
1
<Studio />

About

A wall and a floor joined by a smooth step, each lit analytically: the wall by a Gaussian falloff from the key light's x and a vertical fade to the shade colour; the floor by an elliptical pool, darker toward the viewer. A faint line of light follows the curve where it bends, an optional dark ellipse sits where a product would stand, and a vignette and a fine grain finish it. There is no noise to compute, so it is the cheapest scene in the library.

Use it when

  • A product, jewellery or fashion page wants a quiet, studio-lit backdrop behind a hero object.
  • A portfolio wants the work to lead with a calm surface behind it.
  • A launch page wants a background that will not compete with a 3D object placed over it.

Skip it when

  • The page needs a landscape or a mood; use Ocean, Aurora or Rays.
  • The design is graphic and flat; use Dot Waves or Contour Lines.

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/studio
  3. 3Use it:
    import { Studio } from "@/components/vibebox/studio";
    
    <Studio preset="ivory" className="min-h-[560px] rounded-xl">
      <div className="p-12 text-white">
        <h1 className="text-4xl font-semibold">Lit once, shot twice.</h1>
      </div>
    </Studio>

Code

3 files
components/vibebox/studio/index.tsx
/**
 * VibeBox Motion · Studio
 * 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 { Studio, type StudioProps, type StudioColor, type StudioValue } from "./studio.client";
export { PRESET_NAMES as STUDIO_PRESETS, presets as studioPresets, type StudioPreset } from "./studio.shader";
components/vibebox/studio/studio.client.tsx
/**
 * VibeBox Motion · Studio
 * 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 { studioPiece, presets, type StudioPreset } from "./studio.shader";

export type StudioColor = "wall" | "floor" | "light" | "shade";
export type StudioValue = "horizon" | "sweep" | "keyX" | "keyPower" | "beam" | "grain" | "shadow";

export type StudioProps = {
  /** What sits on the scene: a heading, a call to action, a whole section. */
  children?: ReactNode;
  /** A named look: `ivory`, `slate`, `rose`, `sage`, `ink`, `sand`. */
  preset?: StudioPreset;
  /** Overrides single palette colours with any CSS colour. */
  colors?: Partial<Record<StudioColor, 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<StudioValue, number>>;

const POSTER: PosterSpec = { type: "horizon", top: "wall", horizon: "wall", groundTop: "floor", groundBottom: "shade", at: 60, glow: "light", glowAt: [42, 78] };

export function Studio({ children, preset, colors, speed, intensity, interactive = true, label, still, className, horizon, sweep, keyX, keyPower, beam, grain, shadow }: StudioProps) {
  return (
    <BackdropCanvas
      slug="studio"
      piece={studioPiece}
      presets={presets}
      fallback="ivory"
      preset={preset}
      colors={colors}
      values={{ horizon, sweep, keyX, keyPower, beam, grain, shadow }}
      speed={speed}
      intensity={intensity}
      poster={POSTER}
      scale={0.4}
      stillTime={2}
      still={still}
      interactive={interactive}
      label={label}
      className={className}
    >
      {children}
    </BackdropCanvas>
  );
}
components/vibebox/studio/studio.shader.ts
/**
 * VibeBox Motion · Studio
 * 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 photographer's seamless sweep: a wall that curves into a floor with no corner, lit by a soft
 * key from one side. The light is the picture: a broad pool on the floor where a product would
 * stand, a falloff up the wall, a faint shaft from the top and a vignette to hold the edges. It
 * is quiet by design, so type and objects sit on it like they do in a studio. The pointer moves
 * the key light.
 */

const SCENE = /* glsl */ `
uniform vec3 uWall;
uniform vec3 uFloor;
uniform vec3 uLight;
uniform vec3 uShade;
uniform float uHorizon;
uniform float uSweep;
uniform float uKeyX;
uniform float uKeyPower;
uniform float uBeam;
uniform float uGrain;
uniform float uShadow;
uniform float uSpeed;
uniform float uIntensity;

float T;

void main() {
  vec2 uv = centred(gl_FragCoord.xy);
  T = uTime * uSpeed;
  float aspect = uResolution.x / uResolution.y;
  float keyX = uKeyX * 0.5 * aspect + (uPointer.x - 0.5) * 0.25 * (0.3 + uHover);
  vec3 light = lin(uLight);
  vec3 shade = lin(uShade);

  // The sweep: wall above, floor below, joined by a curve of width uSweep.
  float t = smoothstep(uHorizon + uSweep, uHorizon - uSweep, uv.y);

  // The wall is lit from one side by a big soft box and falls away to the shade at the top.
  float across = (uv.x - keyX) * 0.75;
  float wallLight = exp(-across * across * 1.5 / uKeyPower);
  float up = smoothstep(uHorizon, 0.55, uv.y);
  vec3 wall = lin(uWall);
  vec3 wallCol = mix(mix(wall, shade, 0.78), wall * 1.1, wallLight);
  wallCol = mix(wallCol, mix(wall, shade, 0.85), up * 0.6);
  wallCol += light * wallLight * (1.0 - up) * 0.14;

  // The floor reflects the wall near the horizon and falls to the shade toward the viewer, with
  // the key light's pool lying on it.
  float toward = smoothstep(uHorizon, -0.62, uv.y);
  vec3 floorC = lin(uFloor);
  vec3 floorCol = mix(floorC * 1.08, mix(floorC, shade, 0.9), toward);
  vec2 pool = vec2((uv.x - keyX * 0.6) * 1.0, (uv.y - (uHorizon - 0.24)) * 3.0);
  float poolAmt = exp(-dot(pool, pool) * 3.2 / uKeyPower);
  floorCol += light * poolAmt * 0.62;

  // The sweep catches a line of light where it curves, and an unseen product can cast a shadow.
  vec3 col = mix(wallCol, floorCol, t);
  float bend = exp(-pow((uv.y - uHorizon) / max(uSweep * 0.7, 0.02), 2.0));
  col += light * bend * 0.07 * (0.4 + wallLight);
  vec2 sp = vec2((uv.x - keyX * 0.6) * 1.1, (uv.y - (uHorizon - 0.3)) * 4.2);
  col *= 1.0 - uShadow * 0.5 * exp(-dot(sp, sp) * 3.0);

  // A faint shaft from the top, drifting very slowly.
  float shaft = exp(-pow((uv.x - keyX - 0.1 * sin(T * 0.05)) * 3.2 + uv.y * 0.8, 2.0)) * smoothstep(0.6, -0.1, uv.y);
  col += light * shaft * 0.1 * uBeam;

  col *= uIntensity;
  col *= 1.0 - 0.32 * smoothstep(0.4, 1.2, length(uv * vec2(0.8, 1.0)));
  float g = (h2(ivec2(gl_FragCoord.xy)) - 0.5) * uGrain * 0.045;
  col += col * g;
  fragColor = vec4(finish(col, gl_FragCoord.xy), 1.0);
}
`;

export const fragment = compose(SCENE);

export const PRESET_NAMES = ["ivory", "slate", "rose", "sage", "ink", "sand"] as const;
export type StudioPreset = (typeof PRESET_NAMES)[number];

export const presets: Record<StudioPreset, ScenePreset> = {
  ivory: {
    colors: { wall: "#e7e0d4", floor: "#cfc5b4", light: "#fff6e6", shade: "#6d6455" },
    values: { horizon: -0.08, sweep: 0.2, keyX: -0.35, keyPower: 1, beam: 1, grain: 1, shadow: 0.4, speed: 0.5, intensity: 1 },
  },
  slate: {
    colors: { wall: "#3b4350", floor: "#262c36", light: "#9fb4cf", shade: "#0b0e14" },
    values: { horizon: -0.1, sweep: 0.22, keyX: 0.3, keyPower: 1, beam: 1.1, grain: 1, shadow: 0.4, speed: 0.5, intensity: 1.05 },
  },
  rose: {
    colors: { wall: "#e9c9c9", floor: "#d9a8ab", light: "#fff0ee", shade: "#6a3a42" },
    values: { horizon: -0.06, sweep: 0.2, keyX: 0.25, keyPower: 1, beam: 1, grain: 1, shadow: 0.4, speed: 0.5, intensity: 1 },
  },
  sage: {
    colors: { wall: "#bccbb6", floor: "#98ab93", light: "#f6fbe8", shade: "#33422f" },
    values: { horizon: -0.07, sweep: 0.22, keyX: -0.25, keyPower: 1, beam: 0.9, grain: 1, shadow: 0.4, speed: 0.5, intensity: 1 },
  },
  ink: {
    colors: { wall: "#16181f", floor: "#0b0c10", light: "#7f8cff", shade: "#020204" },
    values: { horizon: -0.1, sweep: 0.18, keyX: 0.0, keyPower: 1.2, beam: 1.3, grain: 1.2, shadow: 0.4, speed: 0.5, intensity: 1.1 },
  },
  sand: {
    colors: { wall: "#d9c3a1", floor: "#c0a47d", light: "#fff1d0", shade: "#5a4426" },
    values: { horizon: -0.08, sweep: 0.2, keyX: 0.3, keyPower: 0.9, beam: 1, grain: 1, shadow: 0.4, speed: 0.5, intensity: 1 },
  },
};

export const studioPiece = createScenePiece("studio", fragment);

Props

PropTypeDefaultDescription
childrenReactNode—What sits on the scene: a heading, a call to action, a whole section.
preset"ivory" | "slate" | "rose" | "sage" | "ink" | "sand""ivory"The look: ivory, slate, rose, sage, ink, sand.
colors{ wall?: string; floor?: string; light?: string; shade?: string }—Overrides single palette colours with any CSS colour: wall (the wall's colour where the light is strongest), floor (the floor's colour at the horizon), light (the key light and the pool it throws), shade (the colour the edges and the floor's near edge fall to).
speednumber1Multiplies the preset's speed; 0 holds the scene still.
intensitynumber1Multiplies the preset's brightness, 0.5 to 1.4.
horizonnumber-0.08Where the sweep curves, from low (-0.5) to high (0.4) (-0.5 to 0.4; each look sets its own).
sweepnumber0.2How wide the curve between wall and floor is (0.05 to 0.5; each look sets its own).
keyXnumber-0.35Where the soft box sits across the frame (-1 to 1; each look sets its own).
keyPowernumber1How tight the key light is (higher is tighter) (0.5 to 2; each look sets its own).
beamnumber1Strength of the faint shaft from above (0 to 2; each look sets its own).
grainnumber1A fine film grain that keeps the gradient from banding (0 to 2; each look sets its own).
shadownumber0.4A soft shadow where a product would stand (0 to 1; each look sets its own).
interactivebooleantrueThe key light slides toward the pointer.
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 key light like the cursor does; scrolling is never blocked.
Reduced motion
One still frame; the faint shaft does not drift.

Edge cases and limits

  • It is quiet by design: on a busy page it can read as blank, so pair it with a strong object.
  • The shadow assumes the object stands near the pool; move the key with keyX if it does not.
  • Light looks (ivory, rose, sage, sand) need dark text; slate and ink need white.
  • 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

  • Deep violet pleated silk running out of a dark centre with lilac highlights and a fine grain

    Burst

    Free

    Pleated silk seen from its centre: folds run out of a dark middle like a drawn curtain, with a fine cloth grain.

    Backgrounds · WebGL2

↑ ↓ to move↵ to openesc to close