Introduction

Picture standing at a fixed point while a friend walks the entire boundary of a polygon once, all the way around, back to where they started. As they walk, look at the direction from you to them. That direction rotates. When they finish the lap, how many full 360°360° turns did that direction make?

That count is the winding number. If your friend's loop never enclosed you, the direction wobbles back and forth but ends up net zero turns — you are outside. If the loop wraps around you once, the direction completes exactly one full turn — you are inside.

It sounds like a roundabout way to ask a simple question, but this "count the turns" idea turns out to be sturdier than the classic ray-casting trick the moment a polygon gets messy: self-crossing edges, overlapping loops, or shapes traced in different directions. The winding number does not just say inside or outside — it says how many times wrapped, and that extra information is exactly what rescues the ambiguous cases.

Drag the Point

Below is a figure-eight-style polygon — one loop wound one way, the other wound the opposite way, crossing itself in the middle. A movable dot sits on the canvas. Drag it around and watch the accumulated turning angle update live as it sums the signed angle to every edge.

<p class="hint">{{hint_para}}</p>
<div class="stage" id="stage">
  <svg id="scene" viewBox="0 0 400 260" xmlns="http://www.w3.org/2000/svg"></svg>
</div>
<div class="readout">
  <div class="pill"><span class="label">{{label_angle}}</span><span id="angleVal">0°</span></div>
  <div class="pill"><span class="label">{{label_winding}}</span><span id="windingVal">0</span></div>
  <div class="pill" id="verdictPill"><span id="verdictVal">{{verdict_outside}}</span></div>
</div>
<div class="btns">
  <button id="animate" type="button">{{btn_animate}}</button>
  <button id="reset" type="button" class="ghost">{{btn_reset}}</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.stage { border: 1px solid #cdd9e3; border-radius: 10px; background: #f7f9fb; touch-action: none; }
svg { display: block; width: 100%; height: auto; cursor: grab; }
.loop { fill: rgba(29,53,87,0.06); stroke: #1d3557; stroke-width: 2; }
.loop.right { stroke: #1d3557; }
.loop.left { stroke: #7b2cbf; }
.arrowhead { fill: #1d3557; }
.arrowhead.left { fill: #7b2cbf; }
.radial { stroke: #e63946; stroke-width: 1.2; stroke-dasharray: 3 3; opacity: .85; }
.point { fill: #e63946; stroke: #fff; stroke-width: 1.5; cursor: grab; }
.readout { display: flex; gap: .5rem; flex-wrap: wrap; margin: .6rem 0; }
.pill { background: #e8eef3; border: 1px solid #cdd9e3; border-radius: 999px; padding: .3rem .8rem;
        font: 600 13px ui-monospace, monospace; color: #1d3557; display: flex; gap: .4rem; align-items: center; }
.pill .label { font: 600 11px system-ui, sans-serif; color: #5a6b7a; text-transform: uppercase; letter-spacing: .03em; }
#verdictPill { font: 700 13px system-ui, sans-serif; text-transform: none; }
#verdictPill.inside { background: #d9f2e3; border-color: #9fd8b4; color: #0a7d33; }
#verdictPill.outside { background: #f3e8ea; border-color: #e3c3ca; color: #6b6b6b; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
         background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
// Code not found

Divide the total angle by 360°360° and round: that integer is the winding number, shown next to the verdict. In the right loop it settles at +1, in the left loop at -1 (wound the opposite way), and outside both it lands on 0. Try the even-odd rule mentally on this shape — it gets confused about which loop counts as "inside" once loops overlap or reverse direction. The winding number just keeps adding up signed angle and always reports a clean integer.

The Real Complexity

The winding number is not a harder problem than plain point-in-polygon — it is the same cost, computed more carefully.

  • Still O(n)O(n) per query. Walk the nn edges once, and for each add the signed angle it subtends at the query point (via atan2 of the two endpoint vectors, or an equivalent branch-free crossing-number update). Sum, divide by 2π2\pi, round to the nearest integer. No extra asymptotic cost over ray casting.
  • What it fixes. Even-odd ray casting only ever answers "odd or even" — it cannot tell a doubly-wound region from open space, and it can disagree with intuition when a polygon's edges are ordered clockwise in one part and counterclockwise in another (self-intersecting outlines, font glyphs, offset curves). The winding number's raw integer — not just its parity — is exactly what disambiguates those cases; this is the standard nonzero-fill rule used by vector graphics and font renderers, contrasted with the even-odd rule.
  • No hidden hardness. There is no search, no combinatorial blowup, nothing NP about it — it is arithmetic on nn vectors. The one thing to watch is numerical care near the boundary (very small angles, points exactly on an edge), which is a precision issue, not a complexity one.
  • Same preprocessing tricks apply. As with plain point-in-polygon, if you must test millions of points against one fixed polygon, you can preprocess into a structure that answers each query in O(logn)O(\log n) instead of O(n)O(n).

So the winding number buys robustness for free: identical running time, but a definition that survives the polygons where simple ray counting quietly gives the wrong answer.

Where It Matters

Counting turns around a point shows up far beyond simple polygons:

  • Vector graphics and fonts: SVG's fill-rule="nonzero" and PostScript/TrueType glyph rendering both fill a region wherever the winding number is nonzero — it is how overlapping strokes and holes in letters like "O" or "A" render correctly.
  • Complex analysis: the same integer appears as the winding number of a curve around a point in the complex plane, central to Cauchy's integral formula and the argument principle for counting zeros of a function.
  • Topology and knot theory: winding and linking numbers classify how loops wrap around points or around each other — a basic invariant for curves on a plane or in space.
  • Robotics and motion planning: reasoning about how many times a cable, path, or robot arm has wrapped around an obstacle uses the exact same accumulated-angle idea.

Whenever a shape can double back on itself, "count the crossings" quietly becomes ambiguous, but "sum the turning angle" keeps working — the reason the winding number, not the even-odd rule, is the default in most professional rendering pipelines.

Conclusion

The winding number reframes "is this point inside?" as "how many times does the boundary wrap around it?" — a small shift in perspective that costs nothing extra to compute and pays off exactly when shapes get weird: overlapping loops, mixed orientations, self-intersections.

It is a good reminder that a solved problem can still have more than one right answer to generalize into. Simple ray casting is fast and fine for clean, simple polygons; the winding number keeps the same O(n)O(n) price tag while staying correct on the messy shapes real fonts, real SVG paths, and real robot cables actually produce.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/winding-number-point-in-polygon/Content licensed under CC BY-NC 4.0.