50
$\begingroup$

I am trying to fill a shape with diagonal lines. I am aware of Texture, but it rasterizes the fill pattern, which is not desirable. Here was my crack at it:

Graphics[{FaceForm[White], EdgeForm[Black], 
  Polygon[{{0, 0}, {1, 0}, {1, 1}, {0.5, 1.5}, {0, 1}}], 
  Sequence@Table[
    Inset[Graphics[Line[{{0, 0}, {1, 1}}]], {0, 0.1 l}, {0, 0}, 
     Scaled[{1, 1}]], {l, -10, 10}]}]

Which produces the following:

enter image description here

Ideally, I would want to just clip the insets such that they do not appear outside of the shape, rather than having to calculate the intersection between the lines and the polygon (for which I have shown a relatively simple example, in principle I'd like this to work for more complicated shapes such as FilledCurve). Note that there is a very large whitespace around the figure as well, which only appears when I include the insets.

$\endgroup$
8
  • $\begingroup$ You can use AbsoluteOptions to obtain the PlotRange of interest of the polygon (without the insets). Then, when you plot the polygon plus insets, you can use the same PlotRange. $\endgroup$
    – DavidC
    Commented Mar 30, 2012 at 20:05
  • $\begingroup$ Just to augment the answer by @Yu-Sung (which seems to do what you want): you can use PlotRange with Graphics, and that allows you to get rid of the white space. Maybe that's what @David already hinted at above... so you could add e.g. PlotRange -> {{-.1, 1.5}, {-.1, 1.5}}. $\endgroup$
    – Jens
    Commented Mar 30, 2012 at 20:56
  • $\begingroup$ @Jens, Yes, that's what I was referring to. Using AbsoluteOptions[g, PlotRange] you can obtain the needed PlotRange. (My answer illustrates how this is used.) $\endgroup$
    – DavidC
    Commented Mar 30, 2012 at 21:48
  • $\begingroup$ @David yes, I shouldn't have doubted you. Just wanted t make sure. Another observation about @Yu-Sung's answer: it could be simplified so that it doesn't require Inset. Then the white-space problem would likely be fixed too. $\endgroup$
    – Jens
    Commented Mar 30, 2012 at 22:15
  • $\begingroup$ @Jens I don't yet fully understand Yu-Sung's answers. Not sure why Inset was used. $\endgroup$
    – DavidC
    Commented Mar 30, 2012 at 22:45

6 Answers 6

5
$\begingroup$

Finally, with version 12.1 come the directives HatchFilling and PatternFilling which make the task much easier:

Graphics[{HatchFilling["Diagonal"], EdgeForm[Black], 
    Polygon[{{0, 0}, {1, 0}, {1, 1}, {0.5, 1.5}, {0, 1}}]}]

enter image description here

img = ExampleData[{"TestImage", "Mandrill"}];

Graphics[{PatternFilling[img, ImageScaled[1.5]], EdgeForm[Black], 
    Polygon[{{0, 0}, {1, 0}, {1, 1}, {0.5, 1.5}, {0, 1}}] }]

enter image description here

Graphics[{PatternFilling[#], EdgeForm[Black], 
   Polygon[{{0, 0}, {1, 0}, {1, 1}, {0.5, 1.5}, {0, 1}}] }, ImageSize -> 100] & /@ 
  {"Checkerboard", "Chevron", "ChevronLine", "Circle", "Diamond", "DiamondBox", 
   "DiamondPlate", "DiamondPoint", "Grain", "Grid", "GridPoint", 
   "Halftone", "HalftoneGrid", "Herringbone", "Hexagon", 
   "Octagon", "Plaid", "Weave", "XGrid", "XGridPoint"}  // Multicolumn[#, 5] &

enter image description here

$\endgroup$
43
$\begingroup$

The following solution is hacky work-around to overcome the lack of proper pattern directives. I don't like it 100%, but still it is usable.

Solution

The idea is simple. If you see FilledCurve documentation, it supports an object with holes:

hole

So what if you put a huge enough rectangle as an outer boundary? Then, your polygon becomes a hole and you can see through stuff behind it.

Now, few issues:

  1. How big the boundary should be? Ideally, it should be always outside of the screen. Thankfully, it is possible by using ImageScaled coordinates, which is supported by all 2D primitives.

  2. Now your background color of the screen should be the face color of the outer polygon.

  3. The hatch (pattern) should come first so that the outer polygon would be drawn on top of it.

This is the result.

Graphics[{
  (* The hatch comes first *)
  Sequence@
   Table[Inset[Graphics[Line[{{0, 0}, {1, 1}}]], {0, 0.1 l}, {0, 0}, 
     Scaled[{1, 1}]], {l, -10, 10}],

  (* FaceForm color should be your "background" color: usually white *)
  FaceForm[White], EdgeForm[Black],

  (* FilledCurve syntax is essentially the same as Polygon if used with Line *)
  FilledCurve[{
    (* Large outer boundary. Its edges are always outside of your screen *)
    {
       Line[{ImageScaled[{-2, -2}],ImageScaled[{2, -2}],
         ImageScaled[{2, 2}],ImageScaled[{-2, 2}]}]
    },
    (* Your original polygon *)
    {
       Line[{{0, 0}, {1, 0}, {1, 1}, {0.5, 1.5}, {0, 1}}]
    }
  }]
}]

Here is the result:

result

Generalization

The following example uses two different types of patterns (an image and a graphic), and locate them in two different places.

Graphics[{
  (* Pattern for the first object *)
  Inset[ExampleData[{"ColorTexture", "MultiSpiralsPattern"}],
   {-.25, -.25}, {Left, Bottom}, {1.5, 1.5}],
  (* Pattern for the second object *)
  Inset[Graphics[Table[Circle[{i, j}, .25], {i, 10}, {j, 15}]],
   {1.5, 0}, {Left, Bottom}, {1, 1.5}],

  FaceForm[White], EdgeForm[Black],
  FilledCurve[{
    (* Outer polygon *)
    {Line[{ImageScaled[{-2, 2}], ImageScaled[{2, -2}], 
       ImageScaled[{2, 2}], ImageScaled[{-2, 2}]}]},
    (* The first object *)
    {Line[Table[(.5 + .25 (-1)^t) {Cos[Pi t/5], 
       Sin[Pi t/5]} + {.5, .5}, {t, 0, 9}]]},
    (* The second object *)
    {Line[{{1.5, 0}, {2.5, 0}, {2.5, 1}, {2, 1.5}, {1.5, 1}}]}
    }]
  }]

Here is the result:

result2

Pros & Cons

The benefits of this approach is:

  1. You need to convert your polygon to inequalities to use RegionPlot or any other solutions using Plot with RegionFunction which is not always possible (such as country polygons).

  2. Anything can be a pattern as long as it is behind the polygon.

  3. The syntax is straightforward, as long as you stays in Polygon or even better FilledCurve. You just have to add one extra component.

The problems:

  1. It is tricky to handle many objects with different patterns, especially if your object contains a concave part.

  2. Circle support is hard. In fact, circles can be described as B-splines (See the first example of Applications section), but combining it with the rest of FilledCurve can be messy.


In fact, if you are familiar with Mac OS's graphics API (Quartz or Cocoa), this is exactly how they deal with filled polygons with patterns (link). It would have been a nice addition to Mathematica's graphics if it were built-in.

As a side note, FilledCurve syntax is quite complex, but if you want nice graphics, it is well-worth learning. It follows the concept of path in many other graphics languages (Postscript, SVG, or system APIs) and conversion to it is usually straightforward.

$\endgroup$
4
  • $\begingroup$ This is the answer I was looking for. The only minor complaint is that there still appears to be a large whitespace around the shape, I'm guessing its because there's something screwy going on with the Inset coordinates... $\endgroup$
    – Guillochon
    Commented Mar 30, 2012 at 19:43
  • 1
    $\begingroup$ Great idea! I do see one drawback: all objects that you want to hatch must have the same hatching pattern. $\endgroup$ Commented Mar 30, 2012 at 19:44
  • 2
    $\begingroup$ @Guillochon, yes it is due to your hatching patterns coordinate interfering with plot range computation. Could be a bug, although I think not. Usually when you have Scaled coordinates with PlotRange->All, FE is falling into chicken-egg trouble: To compute Scaled you need to know plot range. But to compute plot range, you need to know the boundaries of objects in the scene... So sometimes it does a guesstimation. $\endgroup$ Commented Mar 30, 2012 at 20:10
  • $\begingroup$ @SjoerdC.deVries, Yes, that's why I said that I don't like it 100%. You can do different patterns by carefully laying down patterns around, but it gets quite nasty / impossible, if your object has concave parts. System API doesn't have that trouble because it draws each filled path on different buffer, then combine later. $\endgroup$ Commented Mar 30, 2012 at 20:12
25
$\begingroup$

ragfield's solution is a good one that can easily be extended to arbitrarily complicated polygons if we create an inPolyQ function using winding numbers.

Here is a complicated polygon (the points only) from Mathematica's example data.

poly = Rescale[ExampleData[{"Statistics", "WesternUgandaBorder"}]];

This function determines whether a point is in the polygon.

inPolyQ = 
  Compile[{{polygon, _Real, 2}, {x, _Real}, {y, _Real}}, 
   Block[{polySides = Length[polygon], X = polygon[[All, 1]], 
     Y = polygon[[All, 2]], Xi, Yi, Yip1, wn = 0, i = 1}, 
    While[i < polySides, Yi = Y[[i]]; Yip1 = Y[[i + 1]]; 
     If[Yi <= y, 
      If[Yip1 > y, Xi = X[[i]]; 
        If[(X[[i + 1]] - Xi) (y - Yi) - (x - Xi) (Yip1 - Yi) > 0, 
         wn++;];];, 
      If[Yip1 <= y, Xi = X[[i]]; 
        If[(X[[i + 1]] - Xi) (y - Yi) - (x - Xi) (Yip1 - Yi) < 0, 
         wn--;];];]; i++]; ! wn == 0]];

We can use inPolyQ to generate our region for RegionPlot.

RegionPlot[inPolyQ[poly, x, y], {x, 0, .7}, {y, 0, 1}, Mesh -> 20,  
   MeshFunctions -> {#1 - #2 &}, MeshShading -> {None}, PlotPoints -> 25]

enter image description here

Edit:

As a proof of concept that this can be used for other sorts of textures..

text = ExampleData[{"ColorTexture", "MultiSpiralsPattern"}];

RegionPlot[inPolyQ[poly, x, y], {x, 0, .7}, {y, 0, 1.1}, 
 PlotStyle -> Texture[text], PlotPoints -> 50, Frame -> None]

enter image description here

$\endgroup$
18
$\begingroup$

Mathematica's graphics language lacks a clipping primitive. However, you can sometimes simulate the same appearance using the sophisticated visualization functions.

RegionPlot[((0 <= x <= .5) && (0 <= y <= 1 + x)) || ((.5 < x <= 1) && 
    0 <= y <= 2 - x), {x, -1, 2}, {y, -1, 2}, Mesh -> 20, 
 MeshFunctions -> {#1 - #2 &}, MeshShading -> {None}]

enter image description here

$\endgroup$
2
  • $\begingroup$ Can you supply complicated shapes such as with FilledCurve to RegionPlot? Seemed like that was what the OP had in mind... $\endgroup$
    – rm -rf
    Commented Mar 30, 2012 at 19:31
  • 2
    $\begingroup$ @R.M You can do that with my PointinPoly function used in this answer. $\endgroup$ Commented Mar 30, 2012 at 19:56
14
$\begingroup$

The FilledCurve approach in the accepted answer by @Yu-Sung Chang is very interesting, so I tried to make my own version of it. I defined the whole thing as a function that accepts graphics directives for the objects to be masked (backgroundDirectives - e.g., the fill pattern or image), and an arbitrary polygon as the mask. This includes Polygon with several lists as arguments, specifying a group of separate polygons which then appear as separate holes. The color of the masking foreground is specified in maskDirectives.

mask[backgroundDirectives_, p_Polygon, maskDirectives_] := 
 Module[{pts = p[[1]]},
  {backgroundDirectives,
   FaceForm[maskDirectives], EdgeForm[],
   FilledCurve[
    Prepend[Map[List, Flatten[Map[Line, {pts}, {-3}]]], {
      Line[
       {ImageScaled[{0, 0}], ImageScaled[{1, 0}], ImageScaled[{1, 1}],
         ImageScaled[{0, 1}]}
       ]
      }
     ]
    ]
   }
  ]
mask1 = With[{polygon = 
     Polygon[{{{0, 0}, {1, 0}, {1, 1}, {0.5, 1.5}, {0, 1}}, 
       Most@Table[.3 {Cos[a], Sin[a]} + {-.3, .4}, {a, 0, 
          2 Pi, Pi/18}]}],
    plot = 
     DensityPlot[Cos[(x - y)/2], {x, -10, 10}, {y, -20, 20}, 
      PlotPoints -> 60,
      Frame -> None,
      ImageSize -> 360]
    },
   {
    mask[Inset[Show[plot, AspectRatio -> Automatic]], polygon, Cyan],
    EdgeForm[Directive[Thick, Dashed, Blue]], FaceForm[],
    polygon
    }
   ];
Graphics[mask1]

Density plot with mask applied

The image above shows how a background DensityPlot is masked by an area with two holes. Since I have separated the masking process as a function, it can be applied repeatedly, as I'm doing here with the previous output. Note that the output mask1 didn't have head Graphics so it doesn't need to be inserted as an Inset:

With[{polygon = Polygon[
    Most@Table[2.2 {Cos[a], Sin[a]}, {a, 0, 2 Pi, Pi/2}]]
  },
 Graphics[{
   mask[mask1, polygon, Orange],
   EdgeForm[Directive[Thin, Red]], FaceForm[],
   polygon
   }
  ]
 ]

Another mask applied to masked image

By having the mask function operate on graphics objects, we only have to use Inset when calling mask with an image or Graphics in the background (as I did in the first graphic). We could also still use Inset to control the size of the background, as e.g. with Inset[Graphics[mask1]] in the last plot.

The second plot shows that the original background (the DensityPlot) is scaled to the final image dimensions by default, so the "stripes" get thicker relative to the masking polygons.

What you also see in the examples is that the mask doesn't draw the masking polygon(s) themselves; it only uses their shapes. That way you can decide on your own whether and how to draw the outline of the mask, e.g. as dashes as I did above.

Edit

The relative size of the mask and the other graphics it reveals depends on the settings for ImageSize. For example, the plots I posted will change if the ImageSize option in plot (contained in mask1) is changed, or if the output is manually resized.

$\endgroup$
11
$\begingroup$

Paint Poly with background colored stripes

Just for fun, I painted the lines in the background color. The unnecessary white space is avoided by using AbsoluteOptions[g,PlotRange] from the polygon when graphed alone.

poly[n, color] makes a (solid colored) polygon with n vertices.

s[p, bc] puts stripes of a background color bc on polygon p. pr is the PlotRange for the solid polygon (left pane, below). This same PlotRange is used in the center and right panes. It crops the picture to the polygon.

poly[n_,color_:Blue]:=Graphics[{FaceForm[color],Polygon[RandomReal[1,{n,2}]]}]

s[p_,bc_:White]:= Module[{pr=AbsoluteOptions[p,PlotRange],pts,xMin,xMax,yMin,yMax},
  xMin=pr[[1,2,1,1]]; xMax=pr[[1,2,1,2]];
  yMin=pr[[1,2,2,1]]; yMax=pr[[1,2,2,2]];
  pts=Append[pList=InputForm[p][[1,1,2,1]],pList[[1]]];
  Show[p,
       Plot[Table[x+k,{k,yMin-xMax,xMin+yMax,.1}],{x,xMin,xMax},
           Axes-> None,PlotStyle->Directive[bc,Thickness[.06]]],
       Graphics[Line[pts]],pr[[1]],PlotRangePadding->Scaled[ .01]]]

Now lets look at three outputs. The middle pane may appear to have blue stripes. But the stripes are actually white. The right pane shows the full extent of the drawn stripes (now in pink).

GraphicsGrid[{{p1 = poly[7], s[p1], s[p1, Pink]}}, Frame -> All]

three polys

Left: polygon Center: blue polygon painted with white stripes Right: blue polygon painted with pink stripes

Background is white in all 3 cases.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.