Vivid.Polygon.Fill (vivid v1.0.0)

Copy Markdown View Source

Computes the filled area of a polygon by scanline conversion.

For each scanline the polygon's edges are intersected with it, the intersections sorted by x, and the spans between them filled according to the non-zero winding rule - the same rule used by PostScript, PDF, SVG and Canvas. A span is inside the polygon whenever the number of upward edge crossings to its left differs from the number of downward ones, which means vertex order determines what counts as inside for self-intersecting polygons.

Edges are tested against a scanline over the half-open interval y_min <= y < y_max. This counts a vertex the edges merely pass through once and a vertex at a local minimum or maximum not at all, so vertices lying exactly on a scanline need no special handling. Horizontal edges fall out of the same test and are discarded.

The returned area includes the pixels the edges themselves pass through, so it is the whole filled shape rather than just its interior.

A list of polygons may be filled as a single area, in which case every contour's edges cross the same scanline and contribute to the same winding count. A contour wound against the one enclosing it therefore cuts a hole, which is what Vivid.Region is built on.

Example

Filling a notched polygon.

iex> use Vivid
...> frame = Frame.init(16, 16, RGBA.black())
...> polygon = Polygon.init([Point.init(1, 1), Point.init(4, 1), Point.init(4, 7), Point.init(11, 7), Point.init(11, 1), Point.init(14, 1), Point.init(14, 14), Point.init(1, 14)])
...> Frame.push(frame, Group.init(Vivid.Polygon.Fill.fill(polygon)), RGBA.white())
...> |> to_string()
"                \n" <>
"                \n" <>
" @@@@@@@@@@@@@@ \n" <>
" @@@@@@@@@@@@@@ \n" <>
" @@@@@@@@@@@@@@ \n" <>
" @@@@@@@@@@@@@@ \n" <>
" @@@@@@@@@@@@@@ \n" <>
" @@@@@@@@@@@@@@ \n" <>
" @@@@@@@@@@@@@@ \n" <>
" @@@@      @@@@ \n" <>
" @@@@      @@@@ \n" <>
" @@@@      @@@@ \n" <>
" @@@@      @@@@ \n" <>
" @@@@      @@@@ \n" <>
" @@@@      @@@@ \n" <>
"                \n"

Summary

Functions

Returns the points making up the filled area of polygon as a MapSet.

Returns the points making up the filled area of polygon which lie within bounds, as a MapSet.

Functions

fill(contours)

@spec fill(Vivid.Polygon.t() | [Vivid.Polygon.t()]) :: MapSet.t()

Returns the points making up the filled area of polygon as a MapSet.

Examples

A triangle, whose sloped edges are interpolated per scanline.

iex> use Vivid
...> frame = Frame.init(16, 16, RGBA.black())
...> triangle = Polygon.init([Point.init(2, 2), Point.init(13, 2), Point.init(7, 13)])
...> Frame.push(frame, Group.init(Vivid.Polygon.Fill.fill(triangle)), RGBA.white())
...> |> to_string()
"                \n" <>
"                \n" <>
"                \n" <>
"       @        \n" <>
"       @@       \n" <>
"      @@@       \n" <>
"      @@@@      \n" <>
"     @@@@@      \n" <>
"     @@@@@@     \n" <>
"    @@@@@@@     \n" <>
"    @@@@@@@@    \n" <>
"   @@@@@@@@@    \n" <>
"   @@@@@@@@@@   \n" <>
"  @@@@@@@@@@@@  \n" <>
"                \n" <>
"                \n"

A diamond, drawn over its own outline. The fill meets the border on every edge regardless of the direction of its slope.

iex> use Vivid
...> frame = Frame.init(16, 16, RGBA.black())
...> diamond = Polygon.init([Point.init(8, 2), Point.init(14, 8), Point.init(8, 14), Point.init(2, 8)])
...> frame
...> |> Frame.push(diamond, RGBA.white())
...> |> Frame.push(Group.init(Vivid.Polygon.Fill.fill(diamond)), RGBA.white())
...> |> to_string()
"                \n" <>
"        @       \n" <>
"       @@@      \n" <>
"      @@@@@     \n" <>
"     @@@@@@@    \n" <>
"    @@@@@@@@@   \n" <>
"   @@@@@@@@@@@  \n" <>
"  @@@@@@@@@@@@@ \n" <>
"   @@@@@@@@@@@  \n" <>
"    @@@@@@@@@   \n" <>
"     @@@@@@@    \n" <>
"      @@@@@     \n" <>
"       @@@      \n" <>
"        @       \n" <>
"                \n" <>
"                \n"

A self-intersecting pentagram with fractional vertices. Under the non-zero winding rule the centre is inside the polygon, so it fills solid; the even-odd rule would leave it hollow.

iex> use Vivid
...> frame = Frame.init(21, 21, RGBA.black())
...> pentagram = Polygon.init([Point.init(10, 18.5), Point.init(5.004, 3.123), Point.init(18.084, 12.627), Point.init(1.916, 12.627), Point.init(14.996, 3.123)])
...> frame
...> |> Frame.push(pentagram, RGBA.white())
...> |> Frame.push(Group.init(Vivid.Polygon.Fill.fill(pentagram)), RGBA.white())
...> |> to_string()
"                     \n" <>
"          @          \n" <>
"          @          \n" <>
"         @@@         \n" <>
"         @@@         \n" <>
"         @@@         \n" <>
"        @@@@@        \n" <>
"  @@@@@@@@@@@@@@@@@  \n" <>
"   @@@@@@@@@@@@@@@   \n" <>
"    @@@@@@@@@@@@@    \n" <>
"      @@@@@@@@@      \n" <>
"       @@@@@@@       \n" <>
"       @@@@@@@       \n" <>
"      @@@@@@@@@      \n" <>
"      @@@@ @@@@      \n" <>
"      @@@   @@@      \n" <>
"     @@       @@     \n" <>
"     @         @     \n" <>
"                     \n" <>
"                     \n" <>
"                     \n"

fill(polygon, bounds)

Returns the points making up the filled area of polygon which lie within bounds, as a MapSet.

Spans are clipped to bounds before their pixels are generated, so filling a polygon much larger than the area you care about costs no more than the area itself.

Example

A polygon overhanging the frame on every side, clipped to the middle of it.

iex> use Vivid
...> frame = Frame.init(8, 8, RGBA.black())
...> polygon = Polygon.init([Point.init(-5, -5), Point.init(12, -5), Point.init(12, 12), Point.init(-5, 12)])
...> Frame.push(frame, Group.init(Vivid.Polygon.Fill.fill(polygon, Bounds.init(2, 2, 5, 5))), RGBA.white())
...> |> to_string()
"        \n" <>
"        \n" <>
"  @@@@  \n" <>
"  @@@@  \n" <>
"  @@@@  \n" <>
"  @@@@  \n" <>
"        \n" <>
"        \n"