Cairn
algorithms · computational geometry · O(n²)

back to Geometry

Polygon Triangulation (Ear Clipping)

Every other entry in this site's Geometry category answers one question about a fixed shape — does this point sit inside it, do these two segments cross, which polygon owns this later query. Triangulation asks something different: cut a simple polygon (convex or concave, same freedom as Point in Polygon) into n − 2 triangles that exactly tile it, no gaps and no overlaps. Triangles are the shape every downstream consumer actually wants — a GPU rasterizes triangles, not arbitrary polygons; an area or centroid computation over triangles is trivial; a physics engine's collision routines assume convex pieces. Ear clipping is the direct way to get there: find a vertex that can be safely cut off along with its two neighboring edges, cut it, and repeat until three vertices remain — the loop terminates by finding an ear, one triangle at a time.

An ear is a vertex v, with neighbors prev and next, that satisfies two conditions: the interior angle at v is convex (less than 180°), and no other vertex of the polygon lies inside the triangle (prev, v, next). Cut that triangle off — remove v, connect prev directly to next with a new diagonal — and what's left is still a valid simple polygon, one vertex smaller, ready for the same test again.

Try it

The same seven-vertex arrow polygon used by Point in Polygon and Slab Decomposition, vertices numbered 0–6 in their original order. Step through and watch two things that look like they shouldn't happen but do, verified live from the polygon's real coordinates every time: vertex 1 and vertex 5 both start out reflex (concave, can never be an ear) and later become ears once clipping changes who their neighbors are; vertex 3 starts out convex but blocked — vertex 5 sits inside its candidate triangle — and later becomes an ear too, the moment vertex 5 stops being a third-party point and becomes vertex 3's own direct neighbor instead.

triangles: 0/5
Press Step or Run.

Why it works

Termination is guaranteed by a classical fact usually called the two ears theorem (G. H. Meisters, 1975): every simple polygon with four or more vertices has at least two ears. Cutting one off is always safe — by the very definition of an ear, nothing else lies inside triangle (prev, v, next), so the new diagonal (prev, next) can't cross any other edge of the polygon, and what remains is still a simple polygon, still satisfying the same theorem, one vertex lighter. Repeat n − 3 times and three vertices are left — trivially a triangle, no test needed — for n − 2 triangles in total.

A vertex's own convexity depends only on itself and its two current neighbors, so clipping an ear can only possibly change the convexity of that ear's own prev and next — every other live vertex's neighbors are untouched. The "is anything else inside my triangle" half of the test only ever gets easier to satisfy as vertices are removed, since there are strictly fewer candidates left to intrude — a vertex once confirmed an ear stays a valid ear forever. So a correct O(n²) implementation doesn't need to retest every remaining vertex after every clip: cache each live vertex's ear/reflex status, and after clipping v, only recompute its former neighbors prev and next — they're the only two whose own local geometry actually changed. This page's own demo shows both flavors of that recheck landing on the same polygon: vertex 1 flips from reflex to ear once its neighbor set changes; vertex 3 flips from blocked to ear once its blocker, vertex 5, becomes its own neighbor instead of a third party sitting inside its triangle.

Reference implementation

function triangulate(poly) {
  const ccw = signedArea(poly) > 0;
  const nodes = poly.map((p, i) => ({ ...p, idx: i, prev: null, next: null }));
  nodes.forEach((n, i) => {
    n.prev = nodes[(i - 1 + nodes.length) % nodes.length];
    n.next = nodes[(i + 1) % nodes.length];
  });

  function isConvex(n) {
    const c = cross(n.prev, n, n.next);
    return ccw ? c > 0 : c < 0;
  }
  function blockerOf(n) { // any other live vertex inside triangle (prev, n, next)?
    let k = n.next.next;
    while (k !== n.prev) {
      if (pointInTriangle(k, n.prev, n, n.next)) return k;
      k = k.next;
    }
    return null;
  }
  function updateStatus(n) {
    n.isReflex = !isConvex(n);
    n.isEar = !n.isReflex && blockerOf(n) === null;
  }
  nodes.forEach(updateStatus);

  let live = nodes.length, scanStart = nodes[0], triangles = [];
  while (live > 3) {
    let cur = scanStart;
    while (!cur.isEar) cur = cur.next; // guaranteed to find one — two ears theorem
    const { prev, next } = cur;
    triangles.push([prev.idx, cur.idx, next.idx]);
    prev.next = next; next.prev = prev; // clip
    live--; scanStart = next;
    updateStatus(prev); updateStatus(next); // only these two can have changed
  }
  triangles.push([scanStart.idx, scanStart.next.idx, scanStart.next.next.idx]);
  return triangles;
}

Pitfalls

Convexity alone isn't enough to make a vertex an ear — skipping the "does anything else sit inside my triangle" check produces a wrong triangulation with the same triangle count, not an obvious crash. On this page's own arrow polygon, vertex 3 (the tip) is convex from the start, so a version of the algorithm that clips the first convex vertex it finds — no containment check at all — clips it immediately instead of waiting for it to legitimately become an ear once vertex 5 is gone. Ran both versions to completion on this exact polygon: the correct version (shown above) produces 5 triangles summing to exactly 57,000, matching the polygon's own shoelace area; the convexity-only version also produces 5 triangles, but they sum to 67,000 — 10,000 too much, because two of its triangles overlap around vertex 3's premature cut instead of tiling cleanly. The triangle count alone is not a correctness check.

Skipping the neighbor recheck after a clip — leaving prev and next's cached status stale — breaks the algorithm on most inputs, but not reliably enough to catch on a small example. Tested directly: on this page's own seven-vertex arrow, a version with the two updateStatus calls removed after each clip still happens to produce the correct 5 triangles, purely because one particular vertex (0) stays flagged an ear the whole time and keeps supplying a valid clip every round — a coincidence of this specific polygon's shape, not evidence the shortcut is safe. Run the same broken version across 3,000 random simple polygons (5–30 vertices, generated by sorting random points around a center and connecting them in angular order, each one independently confirmed genuinely simple — non-self-intersecting — before being trusted as a test case): it gets stuck with no ear left to find on 334 of them, produces the wrong triangle count or a wrong total area on another 515, and only happens to come out correct on the remaining 2,151. Skipping the recheck isn't a shortcut that trades correctness for speed — it's broken on 28% of random inputs, and the other 72% is luck, not a guarantee.

Complexity

Time: O(n²). There are n − 2 clips; each one scans the live vertex list for a cached ear flag (O(n) worst case) and recomputes exactly two vertices' status, each an O(n) containment check against every other live vertex — so each clip costs O(n), and O(n) clips give O(n²) total. Space: O(n) for the doubly linked vertex list and the output triangle list. Sorting-based algorithms that first decompose the polygon into monotone pieces bring worst-case time down to O(n log n) — themselves typically built with a plane-sweep structure in the same family as the randomized incremental trapezoidal map named on Slab Decomposition and Point in Polygon's own Complexity sections, still not built on this site. A genuinely linear O(n) algorithm exists (Chazelle, 1991), but it's complex enough that it's essentially never used in practice — O(n log n) monotone decomposition is the real-world standard for anything beyond ear clipping's simplicity.

This site's guide, Choosing a Geometry Algorithm, places this entry as the route to take when a simple polygon's boundary already exists and needs cutting into triangles — reach for Delaunay Triangulation instead when there's no boundary to start from, only a bare point set.