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.

tsx
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.

tsx
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.

Props15

PropTypeDefault
shape
"wave" | "zigzag" | "loops" | "coil""wave"

Smooth sine, sharp triangle, cursive loop chain, or a spring seen from the side

x
number0.375

Start 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.5

Start of the course as a 0..1 fraction of the composition height

length
number320

Course length in reference px at a 720px-tall composition

angle
number0

Course direction in degrees; 0 runs right and positive values turn clockwise

amplitude
number14

Half-height of the wave when the draw starts, in reference px

wavelength
number48

Course distance of one cycle in reference px, at least 8

weight
number3

Stroke width in reference px, with round caps

color
string"#ffffff"

Stroke color

settle
number0.5

Share of the amplitude that calms away, from 0 to 1; 1 settles into a straight line

hold
number12

Frames at 30 fps that the settled line rests before the erase

erase
booleantrue

Erases the line toward its end point after the hold; false keeps it on screen

delay
number0

Composition frames before the head leaves the start point

speed
number1

Playback multiplier; 0 freezes before anything is drawn

className
string—

Optional class name on the full-frame root

  • shape"wave" | "zigzag" | "loops" | "coil"

    Smooth sine, sharp triangle, cursive loop chain, or a spring seen from the side

    Default"wave"

  • xnumber

    Start of the course as a 0..1 fraction of the composition width; the default centers the default line on a 16:9 frame

    Default0.375

  • ynumber

    Start of the course as a 0..1 fraction of the composition height

    Default0.5

  • lengthnumber

    Course length in reference px at a 720px-tall composition

    Default320

  • anglenumber

    Course direction in degrees; 0 runs right and positive values turn clockwise

    Default0

  • amplitudenumber

    Half-height of the wave when the draw starts, in reference px

    Default14

  • wavelengthnumber

    Course distance of one cycle in reference px, at least 8

    Default48

  • weightnumber

    Stroke width in reference px, with round caps

    Default3

  • colorstring

    Stroke color

    Default"#ffffff"

  • settlenumber

    Share of the amplitude that calms away, from 0 to 1; 1 settles into a straight line

    Default0.5

  • holdnumber

    Frames at 30 fps that the settled line rests before the erase

    Default12

  • eraseboolean

    Erases the line toward its end point after the hold; false keeps it on screen

    Defaulttrue

  • delaynumber

    Composition frames before the head leaves the start point

    Default0

  • speednumber

    Playback multiplier; 0 freezes before anything is drawn

    Default1

  • classNamestring

    Optional class name on the full-frame root