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:
- Branches: One vertex connected to many segments
- Junctions: Multiple paths crossing at the same point
- Holes: Closed regions inside other regions
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
- A vertex is a point (x, y)
- A segment connects two vertices by index
- Tangents turn segments into bezier curves
- Store tangents as relative offsets, not absolute positions
- This is topology, not pixels — the renderer decides how to draw it