Squiggle

A flat line draws itself along a straight course while its wave swims forward and calms, then erases toward the end

Installation
$ pnpm dlx shadcn@latest add @remocn/squiggle

Usage

Squiggle draws one flat line along a straight course. The course starts at x, y, given as fractions of the composition, and runs length reference px at angle. The line is a traveling wave, and its shape is recomputed on every frame. While the head runs out, the crests swim forward along the line and the amplitude relaxes from lively to calm. After a short hold, the tail erases toward the end point. Both ends stay pinned to the course, so the line leaves its start and meets its end along the course direction.

import { Backdrop } from "@/components/remocn/backdrop";
import { Squiggle } from "@/components/remocn/squiggle";
 
export const Accent = () => (
  <Backdrop
    fill={{ type: "color", value: "#002fa7" }}
    padding={0}
    radius={0}
    shadow=""
  >
    <Squiggle />
  </Backdrop>
);

The component is an overlay. Its root fills the parent box, paints no background, and ignores the pointer, so it composites over any scene. Sizes are reference px at a 720px-tall composition and scale with height / 720, like confetti, so the same props look the same at 720p, 1080p and 4K. Every value derives from the Remotion frame, so seeking and rendering are deterministic.

On a moment

Put the squiggle in a Sequence that starts on the event frame, next to the element it accents. Here the beat to stress is frame 18. The line fires on that frame, draws under the word, and stays. Positions are fractions of the frame and the length is in reference px, so convert the word's box with the composition size.

import { AbsoluteFill, Sequence, useVideoConfig } from "remotion";
import { Backdrop } from "@/components/remocn/backdrop";
import { Squiggle } from "@/components/remocn/squiggle";
 
const WORD = { left: 530, top: 290, width: 220, height: 88 };
 
export const Headline = () => {
  const { width, height } = useVideoConfig();
 
  return (
    <Backdrop
      fill={{ type: "color", value: "#002fa7" }}
      padding={0}
      radius={0}
      shadow=""
    >
      <AbsoluteFill>
        <div
          style={{
            position: "absolute",
            left: WORD.left,
            top: WORD.top,
            width: WORD.width,
            lineHeight: `${WORD.height}px`,
            textAlign: "center",
            color: "#fafafa",
            fontSize: 72,
            fontWeight: 600,
          }}
        >
          faster
        </div>
      </AbsoluteFill>
      <Sequence from={18}>
        <Squiggle
          x={WORD.left / width}
          y={(WORD.top + WORD.height + 10) / height}
          length={(WORD.width * 720) / height}
          erase={false}
        />
      </Sequence>
    </Backdrop>
  );
};

To link two elements, start at the first one. Set angle to Math.atan2(dy, dx) * 180 / Math.PI, and pass the distance between them, converted to reference px, as length. To stagger several accents inside one Sequence, give them different delay values.

Timing

At 30 fps with speed={1} and the default props:

FramesMotion
0Nothing is drawn
0–24The head runs out along the line on an ease-out, with 16% drawn on frame 1 and 80% by frame 8
0–36The wave swims forward 1.5 cycles and the amplitude eases from amplitude to amplitude × (1 − settle)
36–48The settled line holds still for hold frames
48–66The tail erases toward the end point on an ease-in-out, and the pattern drifts another half cycle
66–80The preview stays empty, then loops

The head moves along the arc length, so a loop or a coil takes as long to draw as a plain wave. The crests start at about 6 px per frame and slow to rest by frame 36, after the head has landed. The amplitude only ever decreases, so the line never overshoots its calm shape.

squiggleLength is 66, the frame where the tail reaches the end point. With erase={false} the motion ends at frame 36 and the line stays. getSquiggleTimeline({ hold, erase }) returns every beat. getSquiggleDuration({ speed, delay, hold, erase }) returns the preview length, the motion plus a short rest before the loop: 80 frames by default and 60 without the erase. delay counts composition frames before the draw starts and does not scale with speed. hold is part of the motion and does. For another frame rate, multiply the beat frames by fps / 30.

Variations

shape="zigzag" draws sharp corners with miter joins. shape="loops" turns the wave into a cursive chain of loops, and shape="coil" into a spring seen from the side. Both sit on the left of the drawing direction, so they stand above a line drawn left to right. To hang them below, draw from the other end with angle={180}. As the phase swims, the loops roll forward like a wheel and the coil crawls like a turning screw. settle={0} keeps the lively height to the end, and settle={1} calms any shape into a straight line.

Props
PropTypeDefaultDescription
shape
"wave" | "zigzag" | "loops" | "coil""wave"Smooth sine, sharp triangle, cursive loop chain, or a spring seen from the side
x
number0.375Start of the course as a 0..1 fraction of the composition width; the default centers the default line on a 16:9 frame
y
number0.5Start of the course as a 0..1 fraction of the composition height
length
number320Course length in reference px at a 720px-tall composition
angle
number0Course direction in degrees; 0 runs right and positive values turn clockwise
amplitude
number14Half-height of the wave when the draw starts, in reference px
wavelength
number48Course distance of one cycle in reference px, at least 8
weight
number3Stroke width in reference px, with round caps
color
string"#ffffff"Stroke color
settle
number0.5Share of the amplitude that calms away, from 0 to 1; 1 settles into a straight line
hold
number12Frames at 30 fps that the settled line rests before the erase
erase
booleantrueErases the line toward its end point after the hold; false keeps it on screen
delay
number0Composition frames before the head leaves the start point
speed
number1Playback multiplier; 0 freezes before anything is drawn
className
string—Optional class name on the full-frame root