Skip to content20cuts
Menu

Connector

Draw a curved connection and move a pulse along its exact path.

MarkdownView source

Installation

Run once from your product repo. The installer includes Connector, its source, and the agent skill.

Terminal
npx -y https://20cuts.com/releases/v0.3.0/20cuts-create-0.3.0.tgz video/20cuts

Already installed? Import it in your video project. Full setup guide

Import
import { Connector } from "./system/motion";

Usage

The starter already includes ConnectorExample in Remotion Studio. To edit a copy, save the Code tab as src/ConnectorExample.tsx and register it below.

Register in Remotion

Use the complete example from the Code tab. For a separate entry point, replace src/index.tsx with this registration, then run npm run studio.

src/index.tsx
import { Composition, registerRoot } from "remotion";
import { ConnectorExample } from "./ConnectorExample";

registerRoot(() => (
  <Composition
    id="ConnectorExample"
    component={ConnectorExample}
    width={1280}
    height={720}
    fps={30}
    durationInFrames={180}
  />
));

Props

PropTypeDefault
from

Start point in SVG coordinates.

PointRequired
to

End point in SVG coordinates.

PointRequired
width

SVG width and viewBox width in pixels.

number1280
height

SVG height and viewBox height in pixels.

number720
bend

Vertical offset of the quadratic control point; positive bends upward.

number0
color

CSS color.

string"#777"
thickness

Stroke thickness in pixels.

number3
pulse

Show a repeating pulse after the line is drawn.

booleantrue
pulseDuration

Seconds for one complete pulse traversal.

number1.2
at

Start time in seconds within the current Remotion Sequence.

number0
duration

Animation duration in seconds.

number0.7
style

Styles applied to the component wrapper.

CSSProperties
  • Coordinates use the SVG viewBox. Match width and height to the containing world; they do not inherit the composition dimensions.
MotionTiming
MotionTiming type
Source
export type MotionTiming = {at?: number; duration?: number};
PropTypeDefault
at

Start time in seconds within the current Remotion Sequence.

number
duration

Animation duration in seconds.

number
Point
Point type
Source
export type Point = {x: number; y: number};
PropTypeDefault
x

Horizontal coordinate in pixels.

numberRequired
y

Vertical coordinate in pixels.

numberRequired

Source

Read the component files
motion/surfaces.tsx
Source
import React, {type CSSProperties, type ReactNode} from "react";
import {useCurrentFrame, useVideoConfig} from "remotion";
import {connectorGeometry, phase, positive, reveal, type MotionTiming, type Point} from "./core.js";

export type AppFrameProps = {children: ReactNode; title?: string; width?: number; height?: number; background?: string; style?: CSSProperties};
export function AppFrame({children, title = "Your product", width = 960, height = 540, background = "#fff", style}: AppFrameProps) {
  positive(width, "width"); positive(height, "height");
  return <div style={{width, height, display: "flex", flexDirection: "column", overflow: "hidden", border: "1px solid #d8d8d8", borderRadius: 18, background, boxShadow: "0 24px 72px #00000012", ...style}}>
    <div style={{height: 52, flex: "0 0 52px", display: "flex", alignItems: "center", gap: 8, padding: "0 20px", borderBottom: "1px solid #e9e9e9", background: "#fafafa"}}>
      <div aria-hidden style={{display: "flex", gap: 6}}>{[0, 1, 2].map(i => <span key={i} style={{width: 9, height: 9, borderRadius: "50%", background: "#d0d0d0"}} />)}</div>
      <span style={{fontSize: 16, color: "#666", marginLeft: 12}}>{title}</span>
    </div>
    <div style={{position: "relative", flex: 1, minHeight: 0, overflow: "hidden"}}>{children}</div>
  </div>;
}

export type ProcessNodeProps = {title: string; rows?: readonly string[]; state?: "idle" | "running" | "success" | "error"; accent?: string; width?: number; style?: CSSProperties};
export function ProcessNode({title, rows = [], state = "idle", accent = "#62972d", width = 300, style}: ProcessNodeProps) {
  positive(width, "width");
  if (!["idle", "running", "success", "error"].includes(state)) throw new TypeError("state must be idle, running, success, or error.");
  const active = state !== "idle", color = state === "error" ? "#b84636" : state === "idle" ? "#8b8b8b" : accent;
  return <div style={{width, boxSizing: "border-box", border: `1.5px solid ${active ? color : "#ddd"}`, borderRadius: 16, padding: 24, background: "#fff", color: "#171717", boxShadow: active ? `0 0 0 4px color-mix(in srgb, ${color} 10%, transparent)` : "none", ...style}}>
    <div style={{display: "flex", alignItems: "center", gap: 12, fontWeight: 650, fontSize: 24}}><span aria-label={state} style={{width: 10, height: 10, flexShrink: 0, borderRadius: "50%", background: color}} />{title}</div>
    {rows.length > 0 && <div style={{display: "grid", gap: 10, marginTop: 18, color: "#666", fontSize: 18}}>{rows.map((row, i) => <div key={i}>{row}</div>)}</div>}
  </div>;
}

export type ConnectorProps = MotionTiming & {from: Point; to: Point; width?: number; height?: number; bend?: number; color?: string; thickness?: number; pulse?: boolean; pulseDuration?: number; style?: CSSProperties};
/** Coordinates use the explicit SVG viewBox; pass the same size as the containing world. */
export function Connector({from, to, width = 1280, height = 720, bend = 0, color = "#777", thickness = 3, pulse = true, pulseDuration = 1.2, at = 0, duration = 0.7, style}: ConnectorProps) {
  const frame = useCurrentFrame(), {fps} = useVideoConfig();
  positive(width, "width"); positive(height, "height"); positive(thickness, "thickness"); positive(pulseDuration, "pulseDuration");
  const geometry = connectorGeometry(from, to, bend), p = reveal(frame, fps, at, duration);
  const elapsed = Math.max(0, frame / fps - at - duration);
  const pulseAt = phase(elapsed % pulseDuration, 1, 0, pulseDuration), dot = geometry.point(pulseAt);
  return <svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} style={{overflow: "visible", ...style}}>
    {p > 0 && <path d={geometry.path} fill="none" stroke={color} strokeWidth={thickness} strokeLinecap="round" pathLength={1} strokeDasharray={1} strokeDashoffset={1 - p} />}
    {pulse && p === 1 && <circle cx={dot.x} cy={dot.y} r={thickness * 1.8} fill={color} />}
  </svg>;
}