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.

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.

<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>
Props
PropTypeDefaultDescription
tiles
number10Tiles across the shorter side, clamped to 4–24 and rounded; sets the tile size
weight
number0.2Stroke width as a fraction of the tile size, clamped to 0.04–0.34; ignored by the filled variant
passes
number2Full 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
stringcolorColor of alternate tiles in a checkerboard; matches color for a single tone
seed
number1Seeds the opening maze and the random wave order
speed
number1Playback multiplier; 0 freezes the opening maze
loop
booleanfalseAdds a return pass so the cycle ends on its first frame
className
string—Optional class name on the full-frame root