Introducing v3 — a new editor, faster engine, and more.

See what's new

Transforms & Bounding Boxes

Every layer in a shader can be positioned, sized, and rotated inside the canvas with a bounding box — a rectangular frame, just like a layer in a design tool. You move it, resize it, and rotate it, and the layer is drawn to fit.

This is different from CSS transforms. A CSS transform moves the canvas element in the DOM layout. A bounding box positions the content within the canvas — the layer shifts, scales, or rotates without touching the DOM.

<!-- CSS transform: moves the whole canvas element in the page -->
<Shader class="-translate-x-4">...</Shader>

<!-- Bounding box: frames one layer inside the canvas -->
<Shader>
  <LinearGradient :bounding-box="{
    x: { unit: 'uv', value: 0.25 },
    y: { unit: 'uv', value: 0.25 },
    width: { unit: 'uv', value: 0.5 },
    height: { unit: 'uv', value: 0.5 },
    origin: 'top-left',
    rotation: 0
  }" />
</Shader>

The boundingBox prop

Every renderable component accepts a boundingBox object. It has four required dimensions — x, y, width, height — plus an optional origin, a rotation, and a couple of refinements.

FieldTypeDefaultDescription
x{ value, unit }Horizontal position of the box's anchor corner, measured in the origin frame
y{ value, unit }Vertical position of the box's anchor corner, measured in the origin frame
width{ value, unit }Box width
height{ value, unit }Box height
originstring'top-left'Which reference point x / y are measured from — see Origin
rotationnumber0Rotation in degrees, around the box's geometric center
cornerRadius{ value, unit }Rounds the corners of the clipped region (filter layers only)
lockAspectbooleanfalseEditor-only hint that locks the resize handles to the box's aspect ratio. Ignored at render time

Each of x, y, width, height (and cornerRadius) is a dimensional value — a { value, unit } pair, so you can mix relative and absolute units on the same layer.

When no boundingBox is set, a layer fills the whole canvas. Setting a box is purely opt-in.

Units: uv (%) vs px

Each dimension carries its own unit:

UnitMeaning
uvA fraction of the canvas, 01. x/width are fractions of the canvas width; y/height of the canvas height. 0.5 = halfway / half the canvas. The Design Editor shows this as a percentage (50%).
pxDevice pixels. A fixed size that stays constant as the canvas resizes — the renderer re-resolves px → uv on every resize. Good for fixed-size logos, badges, and UI chrome.
<!-- Relative box: always a centered quarter of the canvas, at any size -->
<Glass :bounding-box="{
  x: { unit: 'uv', value: 0.25 },
  y: { unit: 'uv', value: 0.25 },
  width: { unit: 'uv', value: 0.5 },
  height: { unit: 'uv', value: 0.5 },
  origin: 'top-left',
  rotation: 0
}" />

<!-- Fixed box: a 200×200px square, 24px from the top-left, regardless of canvas size -->
<Glass :bounding-box="{
  x: { unit: 'px', value: 24 },
  y: { unit: 'px', value: 24 },
  width: { unit: 'px', value: 200 },
  height: { unit: 'px', value: 200 },
  origin: 'top-left',
  rotation: 0
}" />

Because each property has its own unit, you can mix them — for example a fixed-height bar that always spans the full width:

<LinearGradient :bounding-box="{
  x: { unit: 'uv', value: 0 },
  y: { unit: 'px', value: 0 },
  width: { unit: 'uv', value: 1 },
  height: { unit: 'px', value: 80 },
  origin: 'top-left',
  rotation: 0
}" />

Origin

The origin is the reference point that x and y are measured from — the box's anchor corner and the frame those coordinates live in. It's a 3×3 grid of nine positions:

top-left      top-center      top-right
center-left   center          center-right
bottom-left   bottom-center   bottom-right

The two axes are independent. The horizontal part picks the X reference (left → from the left edge, right → from the right edge, center → a signed offset from the horizontal center). The vertical part does the same for Y (top, bottom, or center). The bare value center centers both axes.

  • 'top-left' (default) — x/y are measured rightward/downward from the top-left corner.
  • 'top-right'x is measured leftward from the right edge; y downward from the top.
  • 'bottom-right'x leftward from the right, y upward from the bottom.
  • 'center'x/y are signed offsets from the canvas center (0, 0 centers the box).

This makes "pin to a corner" trivial and resize-stable. A badge 24px in from the top-right corner stays glued there at any canvas size:

<ImageTexture
  url="/badge.svg"
  :bounding-box="{
    x: { unit: 'px', value: 24 },
    y: { unit: 'px', value: 24 },
    width: { unit: 'px', value: 96 },
    height: { unit: 'px', value: 96 },
    origin: 'top-right',
    rotation: 0
  }"
/>

Origin vs. rotation pivot: origin only controls where x/y are measured from. Rotation always pivots around the box's own geometric center, independent of the origin you choose.

Rotation

rotation spins the box (and its content) around its center, in degrees. Positive values rotate clockwise.

<Stripes :bounding-box="{
  x: { unit: 'uv', value: 0.2 },
  y: { unit: 'uv', value: 0.2 },
  width: { unit: 'uv', value: 0.6 },
  height: { unit: 'uv', value: 0.6 },
  origin: 'top-left',
  rotation: 30
}" />

How different layers fill the box

The box means "draw this layer here," but exactly how a layer fills its box depends on the kind of layer:

  • Generators (gradients, noise, patterns, textures, etc.) re-fit into the box — the effect lays itself out as if the canvas had been resized to the box, undistorted, then confined to the box rectangle. (A handful of screen-anchored patterns simply confine to the box instead of re-fitting.)
  • Shapes (Circle, Star, Blob, and the SDF shape effects like Glass and Crystal) drive their native position/size/rotation from the box, so the shape itself moves and scales with the frame.
  • Filters & distortions (Blur, Glitch, distortions, …) clip to the box — the effect is confined to the rectangle, and the untouched layer beneath shows through outside it. This is where cornerRadius applies, rounding the clipped region.

In all cases the box is a rotated rectangle, so rotation and corner rounding behave consistently.

Basic usage across frameworks

<Shader className="w-full h-64">
  <LinearGradient
    boundingBox={{
      x: { unit: 'uv', value: 0.1 },
      y: { unit: 'uv', value: 0.1 },
      width: { unit: 'uv', value: 0.8 },
      height: { unit: 'uv', value: 0.8 },
      origin: 'top-left',
      rotation: 15
    }}
  />
</Shader>

Animating a bounding box

boundingBox is a reactive prop — bind any field to state and it updates live, no recompile:

import { useState, useEffect, useRef } from 'react'

function SpinningBox() {
  const [rotation, setRotation] = useState(0)
  const frameRef = useRef(null)

  useEffect(() => {
    const animate = () => {
      setRotation(r => (r + 0.5) % 360)
      frameRef.current = requestAnimationFrame(animate)
    }
    frameRef.current = requestAnimationFrame(animate)
    return () => cancelAnimationFrame(frameRef.current)
  }, [])

  return (
    <Shader className="w-full h-64">
      <Stripes
        boundingBox={{
          x: { unit: 'uv', value: 0.2 },
          y: { unit: 'uv', value: 0.2 },
          width: { unit: 'uv', value: 0.6 },
          height: { unit: 'uv', value: 0.6 },
          origin: 'top-left',
          rotation
        }}
      />
    </Shader>
  )
}

For continuous animation, you can also reach for a Dynamic Prop on the underlying layer instead of driving rotation from component code.