How to fill outside instead of inside graphical shapes?

6

1

I want to omit parts of Graphics from scenes by defining a geometric shape where details are visible, a "keyhole" of sorts, and fill all the rest as one would do to insides of the regular graphics primitives. How to accomplish this?

My best effort example this far is below, and there are several issues with it.

ClearAll[showWithHole];

showWithHole[g_Graphics, face_, edge_, shape_Graphics] := 
 With[{invreg = 
    RegionDifference[FullRegion[2], DiscretizeGraphics[shape]],
   opts = AbsoluteOptions[g]},
  Show[g, Graphics[
    {FaceForm[face], EdgeForm[], 
     MeshPrimitives[
      DiscretizeRegion[invreg, 1.1 PlotRange /. opts, 
       MeshQualityGoal -> "Minimal"], 2],
     edge, 
     MeshPrimitives[
      BoundaryDiscretizeRegion[invreg, 1.1 PlotRange /. opts], 1]}], 
   Sequence @@ opts]]

EDIT:

Some improvements (particularly, hairlines are now gone!):

ClearAll[showWithHole];

showWithHole[g_Graphics, face_, edge_, shape_Graphics] := 
 With[{invreg = 
    RegionDifference[FullRegion[2], DiscretizeGraphics[shape]],
   opts = AbsoluteOptions[g]},
  Show[g, Graphics[
    {FaceForm[face], EdgeForm[edge], 
     FilledCurve[
      BoundaryDiscretizeRegion[invreg, 
         CoordinateBounds[Transpose[PlotRange /. opts], 
          Scaled[0.05]]]["BoundaryPolygons"] /. 
       Polygon[pts_] :> {Line[pts]}]}], Sequence @@ opts]]

showWithHole[
 Graphics[{Blue, Disk[{1, 0}, 3/2]}],
 Directive[Opacity[1/2], White], Red,
 Graphics[{Disk[{0, 0}, 1], 
   FilledCurve[{{Line@CirclePoints[{2, 1}, 1/2, 6]},
                {Line@CirclePoints[{2, 1}, 1/4, 6]}}]}]]

enter image description here

EDIT 2:

[This is now split into an answer below.]

Answers with less kludgy approach (especially avoiding geometric region discretization step!) are still welcome.

kirma

Posted 2016-07-17T10:28:20.507

Reputation: 13 550

Care to share the issues? Doesn't look too bad here... – Yves Klett – 2016-07-17T11:15:30.743

@YvesKlett Unedited version suffers from sides of polygons on partially transparent area being visible (but edited version fixes that). Issues with discretization of "complex" shapes is another problem, and in this case shows up on the circle being clearly edgy, and sometimes even discretized polygons lose some sides. – kirma – 2016-07-17T11:23:16.857

I think this would be better presented as a question and answer, as with edit 2 you appear to have a fully working solution. – Simon Woods – 2016-07-17T13:55:57.873

@SimonWoods I don't particularly like that solution, but yes, I should split it to a candidate answer. – kirma – 2016-07-17T13:58:34.887

1Will HighlightImage help? – Wjx – 2016-07-18T00:21:04.547

@Wjx Please write an answer based on it! It seems that HighlightImage and FilledCurve based methods provide somewhat different subsets of styling, though. Also, a big drawback for HighlightImage is that it requires Images, when working with vector Graphics would be more quality-friendly. – kirma – 2016-07-18T04:16:18.387

@kirma yes, Image stuff lose quality. I will write an answer later as now I'm at sxhool – Wjx – 2016-07-18T04:32:09.073

@kirma I've posted an answer but It seems that HighlightImage simply refuse to work with FilledCurve I strongly suspect that is a bug. Also, the quality of the image is quite good. – Wjx – 2016-07-18T13:33:55.413

@Wjx Interesting, this complete omission of FilledCurve in the implementation! I'd assume it to work. Interestingly JoinedCurve does sort of work. I might assume this is caused by wrong item somewhere in internal pattern matching for graphics primitives. Have you reported a bug on this? – kirma – 2016-07-18T14:19:53.273

nope, I'm wondering whether should I post a new question confirming whether this do is a bug. – Wjx – 2016-07-18T14:20:57.100

@Wjx Go ahead, somebody used to spelunking with Mma internals might provide insight on this! :) – kirma – 2016-07-18T14:21:42.493

1These two approaches both have their own unique advantages and drawbacks, Interesting!!! – Wjx – 2016-07-18T14:21:53.990

1

Closely related: Filling a polygon with a pattern of insets

– Jens – 2016-07-18T23:05:30.447

Answers

4

This answer is split from evolution of the question. In its core, it relies on BoundaryDiscretizeGraphics to create polygons defining holes (note, these may be inside each other), and properties of FilledCurve with polygons inside each other to perform the intended "filling of the outside." Parts of data given to FilledCurve intentionally extend outside PlotRange, in order not to frame the plot with EdgeForm.

ClearAll[showWithHole];

showWithHole[g_Graphics, face_, edge_, shape_Graphics, 
  discoptions : OptionsPattern[BoundaryDiscretizeGraphics]] :=
 Module[{opts, scaledbounds},
  opts = AbsoluteOptions[g];
  scaledbounds = 
   CoordinateBounds[Transpose[PlotRange /. opts], Scaled[0.1]];
  Show[g, Graphics[
    {FaceForm[face], EdgeForm[edge], 
     FilledCurve[
      Prepend[BoundaryDiscretizeGraphics[shape, 
          PlotRange -> scaledbounds, discoptions][
         "BoundaryPolygons"] /. 
        Polygon[pts_] :> {Line[pts]}, {Line[
         Tuples[scaledbounds][[{1, 2, 4, 3}]]]}]]}], 
   Sequence @@ opts]]

showWithHole[
 Graphics[{Red, Disk[{1, 0}, 3/2]}],
 Directive[Opacity[9/10], White], Black, 
 Graphics[{Disk[{0, 0}, 1], 
   FilledCurve[{{Line@CirclePoints[{2, 1}, 1/2, 6]},
                {Line@CirclePoints[{2, 1}, 1/4, 6]}}]}],
 MaxCellMeasure -> 0.001]

enter image description here

kirma

Posted 2016-07-17T10:28:20.507

Reputation: 13 550

2

I finally got home and finished my answer, but there's still one drawback which I strongly suspect as a bug of HighlightImage. So, I'll present you a partial solution now:

Clear["`*"];
img = Graphics[{Blue, Disk[{1, 0}, 3/2]}];
mask = {Disk[{0, 0}, 1], Polygon[CirclePoints[{2, 1}, 1/2, 6]]};

range = AbsoluteOptions[img, PlotRange][[1, 2]];
trans[{x_, y_}] := 
 Scaled@{(x - range[[1, 1]])/(range[[1, 2]] - range[[1, 1]]), (y - 
      range[[2, 1]])/(range[[2, 2]] - range[[2, 1]])}
tsize[{x_, y_}] := 
 Scaled@{x/(range[[1, 2]] - range[[1, 1]]), 
   y/(range[[2, 2]] - range[[2, 1]])}

(*Attention: while using Arrow, only the first form is allowed. Do \
not support ***Triangle series*)
rep = {(f : (Point | Line | HalfLine | InfiniteLine | BezierCurve | 
         BSplineCurve | Arrow | Triangle | Polygon | RegularPolygon | 
         Rectangle | Parallelogram | Simplex))[x_] :> 
    f[x /. e : {ax_?NumericQ, ay_?NumericQ} :> 
       trans@e], (f : (Disk | Circle))[cen_: {0, 0}, r_: {1, 1}, 
     angle_: {0, 2 Pi}] :> 
    f[trans[cen], If[Head[r] === List, tsize[r], tsize[{r, r}]], 
     angle]};

HighlightImage[Rasterize@img, mask /. rep, {"Lighten", .5}]

img1

This result differs from what you need a bit, I will make clear of that later.


Usage

  1. img is the original Graphics or Plot (It also okay when using Image content, but you may need to specify the range by yourself). Feel free to put anything here.

  2. mask is mask. There's some restrictions here: You cannot use ***Triangle as It's hard to define it after some scaling or so, also, maybe There'll be some strange function I didn't add in. Most Graphics primitives are all okay, so feel free to add something here as well.

  3. style can be adjusted in HighlightImage, check the documentation and find out everthing about customizing this part.


How it works

The key part is to scale the mask, so I used most of the code to convert everything into Scaled form:

  1. Extract the PlotRange option using:

    range = AbsoluteOptions[img, PlotRange][[1, 2]];
    
  2. Create simple scaling functions:

    trans function aims to transform point coordinates and tsize function aims to transform the relative positions.

    trans[{x_, y_}] := 
     Scaled@{(x - range[[1, 1]])/(range[[1, 2]] - range[[1, 1]]), (y - 
          range[[2, 1]])/(range[[2, 2]] - range[[2, 1]])}
    
    tsize[{x_, y_}] := 
     Scaled@{x/(range[[1, 2]] - range[[1, 1]]), 
       y/(range[[2, 2]] - range[[2, 1]])}
    
  3. Implement the scaling transform to multiple functions:

    Lots of funtions like Point or Line always use absolute coordinates, so simple transformation will be ediquate. but Disk and Circle shall need a bit more special attention.

    rep = {(f : (Point | Line | HalfLine | InfiniteLine | BezierCurve | 
             BSplineCurve | Arrow | Triangle | Polygon | RegularPolygon | 
             Rectangle | Parallelogram | Simplex))[x_] :> 
        f[x /. e : {ax_?NumericQ, ay_?NumericQ} :> 
           trans@e], (f : (Disk | Circle))[cen_: {0, 0}, r_: {1, 1}, 
         angle_: {0, 2 Pi}] :> 
        f[trans[cen], If[Head[r] === List, tsize[r], tsize[{r, r}]], 
         angle]};
    
  4. Create the combined image:

     HighlightImage[Rasterize@img, mask /. rep, {"Lighten", .5}]
    

Some other issue

This approach offers a different set of customization. For example, you can blur, desaturate, Lighten, Darken, remove inner and outer region.

Clear["`*"];
img = Graphics[
   Table[{Hue[t/15, 1, .9, .3], 
     Disk[4 {Cos[2 Pi t/15], Sin[2 Pi t/15]}, 4]}, {t, 15}]];
mask = Graphics[
   Table[{Disk[(8 - r) {Cos[2 Pi q/12], Sin[2 Pi q/12]}, 
      Sqrt[(8 - r)]*(Sqrt[q] + 1)/15]}, {r, 6}, {q, 12}]];

range = AbsoluteOptions[img, PlotRange][[1, 2]];
trans[{x_, y_}] := 
 Scaled@{(x - range[[1, 1]])/(range[[1, 2]] - range[[1, 1]]), (y - 
      range[[2, 1]])/(range[[2, 2]] - range[[2, 1]])}
tsize[{x_, y_}] := 
 Scaled@{x/(range[[1, 2]] - range[[1, 1]]), 
   y/(range[[2, 2]] - range[[2, 1]])}

(*Attention: while using Arrow, only the first form is allowed. Do \
not support ***Triangle series*)
rep = {(f : (Point | Line | HalfLine | InfiniteLine | BezierCurve | 
         BSplineCurve | Arrow | Triangle | Polygon | RegularPolygon | 
         Rectangle | Parallelogram | Simplex))[x_] :> 
    f[x /. e : {ax_?NumericQ, ay_?NumericQ} :> 
       trans@e], (f : (Disk | Circle))[cen_: {0, 0}, r_: {1, 1}, 
     angle_: {0, 2 Pi}] :> 
    f[trans[cen], If[Head[r] === List, tsize[r], tsize[{r, r}]], 
     angle]};

HighlightImage[img, mask /. rep, {"Desaturate", .7}] // AbsoluteTiming

A result with desaturate effect, this effect is hard to achieve by Graphics I suppose?

decolorize

It's said in the documentation that the second part of HighlightImage can be any form of Graphics primitives, but whenever a FilledCurve Primitive is presented, HighlightImage will refuse to create filling. Also, Blur option will even ignore Polygons or so. Quite confusing, isn't it?

Is it a bug?

Wjx

Posted 2016-07-17T10:28:20.507

Reputation: 8 808

emmm, the poor quality of the figure is due to the poor screenshot, not the real quality of the image. In fact, the final image is of quite high resolution and accuracy. – Wjx – 2016-07-18T13:32:08.623