String Art

Threads are strung around a ring of pins as a times table, then the multiplier morphs the figure from a cardioid through a nephroid and beyond

Installation
$ pnpm dlx shadcn@latest add @remocn/string-art

Usage

StringArt draws a modular times table. pins points sit on a ring, and a thread runs from every pin i to pin k × i, wrapping around the ring. For k = 2 the threads envelope a cardioid, for k = 3 a nephroid, and for k = 4 a three-cusp epicycloid. Pin 0 sits on the left, so the figures are symmetric about the horizontal axis.

The pins fade in first. Then the threads are strung one pin after another, each growing from its pin to its target. After a short pause the multiplier walks from from to to one whole step at a time: each step eases through the in-between tables, which morph smoothly, and then holds on the clean figure. A short band of threads in accent rides along the stringing head and keeps circling the ring after the figure lands. A monospaced label in the corner shows the running rule, i → k · i mod pins.

import { StringArt } from "@/components/remocn/string-art";
 
export const TimesTable = () => <StringArt />;

The component fills its parent and paints its own background. The ring radius is 42% of the short side, centered, and strokes scale with the composition. All geometry comes from the Remotion frame, so renders are deterministic.

Timing

At 30 fps, speed={1} and the defaults (from={2}, to={4}):

FramesMotion
0–12The pins fade in
0–60Threads are strung one pin after another, easing in and out, into a cardioid
50–66The label fades in
60–84The cardioid holds
84–168The multiplier eases from 2 to 3; the figure morphs into a nephroid
168–204The nephroid holds
204–288The multiplier eases from 3 to 4
288The three-cusp figure lands and holds
60 onwardThe accent band circles the ring at 0.9 pins per frame, a lap every 222 frames

stringFrames sets the stringing time and morphFrames the whole morph. The morph starts 24 frames after the stringing ends and is divided into one equal share per whole step between from and to. Each share spends 70% easing and 30% holding. The figure stops changing at stringFrames + 24 + morphFrames − 0.3 × share frames, 288 at the defaults, and the docs preview adds a short hold at 340. Divide by speed for faster playback. Only the accent band keeps moving after that; set sweep={0} for a fully still end frame.

If to − from is not a whole number, the holds land on equal fractions of the span rather than on whole multipliers.

Variations

from={1} starts from the empty table and grows the cardioid out of nothing. Higher multipliers give more cusps: to={6} ends on a five-cusp figure. Fewer pins show individual threads; more give a solid envelope. showLabel={false} drops the corner label.

<StringArt
  pins={300}
  from={3}
  to={6}
  morphFrames={360}
  threadOpacity={0.3}
  showLabel={false}
  background="#0b1020"
  color="#c9d4ff"
  accent="#ffd166"
/>
Props
PropTypeDefaultDescription
pins
number200Pins around the ring, rounded, minimum 8
from
number2Multiplier at the start; 2 draws a cardioid
to
number4Multiplier at the end; 3 is a nephroid, 4 a three-cusp epicycloid
stringFrames
number60Frames for the threads to be strung, one pin after another
morphFrames
number240Frames for the multiplier to walk from from to to, holds included
sweep
number0.08Length of the accent band as a share of all threads; 0 disables it
sweepSpeed
number0.9Pins per frame the accent band travels once the threads are strung
threadOpacity
number0.42Thread opacity, 0 to 1
showLabel
booleantrueShows the running multiplier in the bottom-left corner
background
string"#111111"Background fill
color
string"#FFFFFF"Threads, pins and label text
accent
string"#E5322D"Accent band and the arrow in the label
speed
number1Playback multiplier