The Simplest Shape

Every vector graphic, from a simple icon to a complex illustration, is built from the same primitive: the line.

But what is a line, really? In vector graphics, it's not pixels. It's math.

Vertices: Points in Space

A vertex is just a position in 2D space. Two numbers: X and Y.

var cfg = {
  "id": "line1",
  "type": "line-editor",
  "bindings": {
    "x1": "vertex.x1",
    "y1": "vertex.y1",
    "x2": "vertex.x2",
    "y2": "vertex.y2"
  },
  "width": 400,
  "height": 200
}

The first vertex is at ({{vertex.x1}}, {{vertex.y1}}). The second is at ({{vertex.x2}}, {{vertex.y2}}).

Try dragging the points. The coordinates update in real-time because they're bound to the visualization.

Segments: Connecting Points

A segment connects two vertices. It says "draw from vertex A to vertex B."

In code, this is remarkably simple:

class Segment {
  constructor(from, to) {
    this.from = from;  // vertex index
    this.to = to;      // vertex index
  }
}

The segment doesn't store coordinates — it stores references to vertices. This is important. When a vertex moves, every segment connected to it updates automatically.

The Vector Network Model

Figma calls this a "vector network" — a graph of vertices connected by segments. Unlike SVG paths (which are linear sequences), vector networks can have:

This is why we can't just use Skia's built-in path API. Paths lose the topology.

Bezier Curves: Bending the Line

Straight lines are boring. Bezier curves add control points that pull the line into a curve.

var cfg = {
  "id": "bezier1",
  "type": "bezier-playground",
  "bindings": {
    "t1x": "bezier.t1x",
    "t1y": "bezier.t1y",
    "t2x": "bezier.t2x",
    "t2y": "bezier.t2y"
  },
  "width": 400,
  "height": 200
}

The control points are at ({{bezier.t1x}}, {{bezier.t1y}}) and ({{bezier.t2x}}, {{bezier.t2y}}).

Drag them. Watch the curve bend.

The Math

A cubic bezier is defined by four points: start (P0), two controls (P1, P2), and end (P3).

The curve is computed by interpolation:

function bezier(t, p0, p1, p2, p3) {
  const mt = 1 - t;
  return mt*mt*mt * p0
       + 3*mt*mt*t * p1
       + 3*mt*t*t * p2
       + t*t*t * p3;
}

As t goes from 0 to 1, the result traces the curve from P0 to P3.

Tangents: The Secret Sauce

In our vector network, we store bezier control points as tangents — offsets from the endpoint, not absolute positions.

class Segment {
  constructor(from, to) {
    this.from = from;
    this.to = to;
    // Tangents are relative to endpoints
    this.t1 = { x: 0, y: 0 };  // offset from 'from' vertex
    this.t2 = { x: 0, y: 0 };  // offset from 'to' vertex
  }
}

When t1 and t2 are both zero, you get a straight line. Any non-zero value bends it.

Why relative? Because when you move a vertex, the tangent moves with it. The curve shape is preserved.

Data Structure

Here's the minimal vector network structure:

const vectorNetwork = {
  vertices: [
    { x: vertex.x1, y: vertex.y1 },
    { x: vertex.x2, y: vertex.y2 }
  ],
  segments: [
    { from: 0, to: 1, t1: {x: 0, y: 0}, t2: {x: 0, y: 0} }
  ]
};

That's it. Two arrays. One for vertices, one for segments.

Summary

Next: Caps, Joins, and Dashes →