Truchet Flip

Quarter-circle tiles snap a quarter turn as a wave crosses the grid, reconnecting the lines until they settle into rings, a calm field, or a circle

Installation
$ pnpm dlx shadcn@latest add @remocn/truchet-flip

Usage

TruchetFlip fills the frame with Truchet tiles. Each tile holds two quarter-circle arcs on opposite corners, and every arc ends on an edge midpoint, so neighboring tiles join into continuous lines. A wave crosses the grid, and each tile it reaches snaps a quarter turn with a small overshoot. The lines reconnect behind the wave, and mazes form and dissolve. Two passes run through the seeded maze. A final pass then turns only the tiles that differ from the target, and the pattern settles into a circle at the center, ringed by concentric wavy loops.

tsx
import { Backdrop } from "@/components/remocn/backdrop";
import { TruchetFlip } from "@/components/remocn/truchet-flip";
 
export const PatternBed = () => (
  <Backdrop
    fill={{ type: "color", value: "#002fa7" }}
    padding={0}
    radius={0}
    shadow=""
  >
    <TruchetFlip />
  </Backdrop>
);

The component paints no background, so it composites over any scene; the backdrop supplies the color. Tile size fits the shorter side of the composition, and a tile corner sits exactly on the frame center, so the rings stay centered at 16:9, 1:1 and 9:16. Everything is flat SVG geometry derived from the Remotion frame and the seed, so seeking and rendering are deterministic.

Timing

At 30 fps, speed={1} and the default two passes:

FramesMotion
0–6The seeded maze holds still
6–48First pass turns every tile a quarter, clockwise, outward from the center
30–72Second pass follows and turns every tile back
60–102Resolve pass turns only the tiles that differ from the target
102–120The resolved pattern holds

Each turn takes 12 frames. The tile reaches its new angle on frame 4, overshoots by about 6°, and lands exactly on the quarter by frame 12. The wave needs 30 frames to cross the grid, so neighboring tiles start a few frames apart and several are always mid-turn. A title placed over the animation can enter around frame 96, as the last tiles settle.

truchetFlipLength is 102, the frame where the last tile settles with the default two passes. getTruchetFlipTimeline({ passes }).settled gives that frame for any pass count. getTruchetFlipDuration({ speed, passes, loop }) returns the preview duration at 30 fps, hold included. Each pass beyond the first adds 24 frames. With passes={0} the maze resolves directly: the last tile settles on frame 48 and the preview runs 66 frames. For another frame rate, multiply by fps / 30 and round up.

Set loop for a background. The resolved pattern holds for 24 frames, then a return pass turns the changed tiles back to the opening maze. The 168-frame cycle ends on the opening maze, so it repeats without a seam.

Variations

waveMode="diagonal" sweeps from the top-left corner to the bottom-right, and waveMode="random" turns tiles in a seeded shuffle. resolve="field" aligns every tile into parallel wavy diagonals. resolve="silhouette" closes the tiles inside a centered circle into small rings and wraps the field around them. variant="filled" draws the arcs as quarter discs, and secondaryColor colors alternate tiles for a two-tone checker.

tsx
<Backdrop
  fill={{ type: "color", value: "#f1eee7" }}
  padding={0}
  radius={0}
  shadow=""
>
  <TruchetFlip
    tiles={14}
    weight={0.14}
    waveMode="diagonal"
    resolve="silhouette"
    color="#171717"
    secondaryColor="#d97757"
    loop
  />
</Backdrop>

Props12

PropTypeDefault
tiles
number10

Tiles across the shorter side, clamped to 4–24 and rounded; sets the tile size

weight
number0.2

Stroke width as a fraction of the tile size, clamped to 0.04–0.34; ignored by the filled variant

passes
number2

Full passes that turn every tile before the resolve pass, clamped to 0–4 and rounded

waveMode
"radial" | "diagonal" | "random""radial"

Turn order: outward from the center, a top-left to bottom-right sweep, or a seeded shuffle

resolve
"rings" | "field" | "silhouette""rings"

Structure the last pass settles into: concentric loops, parallel diagonals, or small rings inside a centered circle

variant
"stroke" | "filled""stroke"

Arcs as butt-capped strokes, or as filled quarter discs

color
string"#ffffff"

Line or disc color

secondaryColor
stringcolor

Color of alternate tiles in a checkerboard; matches color for a single tone

seed
number1

Seeds the opening maze and the random wave order

speed
number1

Playback multiplier; 0 freezes the opening maze

loop
booleanfalse

Adds a return pass so the cycle ends on its first frame

className
string—

Optional class name on the full-frame root

  • tilesnumber

    Tiles across the shorter side, clamped to 4–24 and rounded; sets the tile size

    Default10

  • weightnumber

    Stroke width as a fraction of the tile size, clamped to 0.04–0.34; ignored by the filled variant

    Default0.2

  • passesnumber

    Full passes that turn every tile before the resolve pass, clamped to 0–4 and rounded

    Default2

  • waveMode"radial" | "diagonal" | "random"

    Turn order: outward from the center, a top-left to bottom-right sweep, or a seeded shuffle

    Default"radial"

  • resolve"rings" | "field" | "silhouette"

    Structure the last pass settles into: concentric loops, parallel diagonals, or small rings inside a centered circle

    Default"rings"

  • variant"stroke" | "filled"

    Arcs as butt-capped strokes, or as filled quarter discs

    Default"stroke"

  • colorstring

    Line or disc color

    Default"#ffffff"

  • secondaryColorstring

    Color of alternate tiles in a checkerboard; matches color for a single tone

    Defaultcolor

  • seednumber

    Seeds the opening maze and the random wave order

    Default1

  • speednumber

    Playback multiplier; 0 freezes the opening maze

    Default1

  • loopboolean

    Adds a return pass so the cycle ends on its first frame

    Defaultfalse

  • classNamestring

    Optional class name on the full-frame root