Cairn
algorithms · computational geometry · O(n)

back to Geometry

Point in Polygon

Given a simple polygon and a query point, is the point inside? Unlike every entry in this site's Convex Hull category, the polygon here doesn't have to be convex — it can bend inward as sharply as it likes. That rules out the shortcut a convex shape would allow (checking which side of every edge's line the point falls on, and requiring it to be consistently "inside" for all of them at once): a concave polygon can have a point that's on the inner side of every nearby edge yet still sits outside, in a notch the boundary cuts back through. The test that works regardless of convexity is ray casting: draw a ray from the query point off to infinity in any fixed direction, and count how many times it crosses the polygon's boundary. An odd number of crossings means the point is inside; an even number, including zero, means it's outside. Like Line Segment Intersection, this site's other Geometry entry, the whole test reduces to per-edge arithmetic — no sorting, no recursion — but here every one of the polygon's n edges has to be checked once per query, so the cost is O(n), not O(1).

Try it

A seven-vertex arrow: a rectangular body with a triangular head, the head's two "wings" cut back in to meet the tip — two genuine concave notches. Six preset query points; the ray is drawn rightward from the point to the edge of the canvas, every polygon edge the ray actually crosses highlights, and the crossing count decides the verdict live from the point's real coordinates. upper notch is the one to check first: it sits inside the bounding box of the arrow's head, on the inner side of the wing's slanted edge if you only glance at that one edge, but the ray from it crosses the boundary zero times — outside.

crossings:
Pick a point above.

Why it works

Think of the ray as a path from the query point out to somewhere unambiguously outside the polygon. Each time that path crosses the boundary, it switches which region it's in — inside to outside, or outside to inside — because a simple polygon's boundary is exactly the set of places where "inside" and "outside" meet. Start at the query point in whatever region it's actually in, end far away in the outside region for certain; the number of switches along the way tells you whether the start and end regions are the same (even number of switches) or different (odd). Since the end is always outside, an odd crossing count means the start — the query point — was inside, and an even count means it was already outside.

The one piece of care needed is what happens when the ray doesn't cleanly cross an edge in its interior but grazes a vertex instead — two edges meeting exactly at the ray's height. Testing each edge with (yi > y) !== (yj > y), using the endpoint's y-coordinate strictly above the query's y on both sides, means that of the two edges sharing a vertex sitting exactly on the ray, at most one of them ever registers — never both, never neither, for a vertex where the boundary genuinely passes through top-to-bottom. (A vertex that's a genuine peak or valley — both adjoining edges heading the same direction, up or down, in y — can register on both sides at once, but two crossings cancel out anyway: a ray tangent to a peak shouldn't count as entering the polygon at all, and it doesn't.) A horizontal edge, where yi equals yj, fails the strict inequality on both sides identically and never registers regardless of the query point's x — which also sidesteps a division by that edge's zero y-span before it would ever happen.

Reference implementation

function pointInPolygon(point, poly) {
  let inside = false;
  for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
    const xi = poly[i].x, yi = poly[i].y;
    const xj = poly[j].x, yj = poly[j].y;
    // does edge (j, i) cross the rightward ray from `point`?
    const crosses = ((yi > point.y) !== (yj > point.y)) &&
      (point.x < (xj - xi) * (point.y - yi) / (yj - yi) + xi);
    if (crosses) inside = !inside; // each real crossing flips inside/outside
  }
  return inside;
}

Pitfalls

A concave notch defeats any per-edge "which side" shortcut, even though ray casting handles it without special-casing. This page's own upper notch preset, (340, 80), sits inside the bounding box of the arrow's triangular head and on the polygon-interior side of the wing's single slanted edge — a check against that one edge alone would call it inside. Ray casting disagrees, correctly: cast rightward from (340, 80), and the ray crosses the polygon's actual boundary zero times before leaving the canvas, because the notch cut back out of the head means the boundary never separates that point from the outside at all. Verified independently before writing a line of this page's HTML: a from-scratch winding-number implementation — a genuinely different algorithm, accumulating signed turn angles around the point rather than counting crossings — agreed with ray casting on every one of 50,000 random query points against this exact polygon, including this preset. That winding-number test is now its own page — see Winding Number Algorithm for where the two methods stop agreeing (a polygon that winds around itself more than once).

Changing the strict > to >= on both sides of the crossing test looks like a harmless simplification and isn't one. It still agreed with the canonical version across the same 200,000 random trials used above — because the divergence only shows up for a query point sitting at exactly the same height as a vertex, a probability-zero event for random floating-point coordinates. Targeting that case directly finds real, verified divergences: at query point (200, 100) — sitting exactly on this polygon's top edge — the canonical (strict-both-sides) version reports inside; the >=-both-sides version reports outside. Same divergence, mirrored, at (200, 280) on the bottom edge. A stress test that only samples randomly, however many trials it runs, will never surface a bug that only manifests at an exact coincidence — this one needed a query built specifically to land on the boundary, not just a lot of them.

Complexity

Time: O(n) per query, where n is the polygon's vertex count — every edge gets one constant-time crossing test, no early exit possible in general since a point past the last crossing could always still flip the answer. Space: O(1) beyond the polygon itself, just a running boolean and loop counters. If the same polygon is queried repeatedly, a preprocessing structure can answer each later query in O(log n) instead of rescanning every edge — see Slab Decomposition, this site's own such structure, straightforward to build but O(n²) in the worst case; a fancier randomized incremental trapezoidal map gets preprocessing down to O(n log n) expected while keeping the same O(log n) query.

This site's guide, Choosing a Geometry Algorithm, places this entry first among the site's eleven Geometry pages: the default answer when a polygon is asked to contain a point once, or a handful of times, and might change between queries — reach for Winding Number Algorithm instead the moment the polygon might self-intersect, or for Slab Decomposition/Trapezoidal Map once the same polygon needs to answer many queries.