23
$\begingroup$

I want to make some button shaped graphics that would essentially be a rectangular shape with curved edges. In the example below I have used Polygon rather than Rectangle so as to take advantage of VertexColors and have a gradient fill. The code below illustrates the sort of thing I want in so far as the Frame with RoundingRadius shows where I want the boundaries of the Graphic to be cut off (for example).

Framed[Graphics[{
   Polygon[{{0, 0}, {1, 0}, {1, 1}, {0, 1}}, 
    VertexColors -> {Red, Red, Blue, Blue}]
   },
  AspectRatio -> 0.2,
  ImagePadding -> 0,
  ImageMargins -> 0,
  ImageSize -> 200,
  PlotRangePadding -> 0],
 ContentPadding -> True,
 FrameMargins -> 0,
 ImageMargins -> 0,
 RoundingRadius -> 20]

I'm thinking there is probably a very straight forward way of accomplishing this. Is there some way to exclude parts of the Graphic that fall outside the Frame from displaying? Any alternative methods would be welcome.

Edit

I had been expecting that this was going to be possible with existing options rather than having to write functions. @Mr.Wizard provided a concise solution from existing built in functionality but I ultimately didn't want a raster solution. @Heike used RegionPlot like the others, but in a way in which the user, i.e. me, could implement it by simply changing a rounding radius parameter, so that makes it a more straight forward solution IMO.

$\endgroup$
5
  • $\begingroup$ You are aware that Rectangle accepts the RoundingRadius option, right? Also, is rasterization acceptable? $\endgroup$
    – Mr.Wizard
    Commented Jun 8, 2012 at 7:37
  • $\begingroup$ @Mr.Wizard yes but in this instance I went with Polygon so as to make the gradient easy. If you can show me how to do the gradient fill in the rounded rectangle that will do fine. Would prefer no rasterization at this stage. $\endgroup$ Commented Jun 8, 2012 at 7:44
  • $\begingroup$ Okay, I figured as much. No, I don't know how to get the fill and keep it vector. That +1 is mine. All of this would be easy if Mathematica could do proper vector graphics intersections. $\endgroup$
    – Mr.Wizard
    Commented Jun 8, 2012 at 7:47
  • $\begingroup$ @Mr.Wizard it looks like a non raster solution is either going to be very lengthy or very unsatisfactory. What was your rasterized solution? $\endgroup$ Commented Jun 8, 2012 at 8:31
  • $\begingroup$ Related: mathematica.stackexchange.com/q/1882/121 $\endgroup$
    – Mr.Wizard
    Commented Jun 10, 2012 at 0:06

9 Answers 9

14
$\begingroup$

This answer uses RegionPlot to plot the rounded rectangle. In roundedRect, {{xmin, xmax}, {ymin, ymax}} is the range of the rectangle and rad the rounding radius. roundedRect accepts any option of RegionPlot, in particular ColorFunction which you can use to shade the rectangle.

Options[roundedRect] = Options[RegionPlot];
SetOptions[roundedRect, {Frame -> False, Axes -> False, BoundaryStyle -> None}];

roundedRect[range : {{xmin_, xmax_}, {ymin_, ymax_}}, rad_, 
  opt : OptionsPattern[roundedRect]] := Module[{p, norm},
  p = 1/Log2[Sqrt[2] + 2];
  norm[pt_, pt0_] := Total[Abs[pt - pt0]^p]^(1/p) > rad;
  RegionPlot[And @@ (norm[{x, y}, #] & /@ Tuples[range]),
   {x, xmin, xmax}, {y, ymin, ymax}, opt,
   AspectRatio -> Abs[ymax - ymin]/Abs[xmax - xmin],
   Evaluate[Options[roundedRect]]]]

Example

roundedRect[{{0, 5}, {0, 1}}, .4, ColorFunction -> (Blend[{Red, Blue}, #2] &)]

Mathematica graphics

Edit

@Heike I hope you do not mind me making a change to your answer. I think this is more Mathematica like by having the rounding radius as an option.

ClearAll[roundedRect];

Options[roundedRect] = Flatten[{RoundingRadius -> 0.5, Options[RegionPlot]}];
SetOptions[roundedRect, {Frame -> False, Axes -> False, BoundaryStyle -> None}];

roundedRect[range : {{xmin_, xmax_}, {ymin_, ymax_}}, 
  opt : OptionsPattern[roundedRect]] := Module[{p, norm, opts, rad},

  rad = OptionValue[RoundingRadius];
  opts = FilterRules[{opt}, Options[RegionPlot]];

  p = 1/Log2[Sqrt[2] + 2];
  norm[pt_, pt0_] := Total[Abs[pt - pt0]^p]^(1/p) > rad;

  RegionPlot[
   And @@ (norm[{x, y}, #] & /@ Tuples[range]), {x, xmin, xmax}, {y, 
    ymin, ymax}, Evaluate@opts,
   AspectRatio -> Abs[ymax - ymin]/Abs[xmax - xmin]]]

example:

roundedRect[{{0, 5}, {0, 1}}, Frame -> False, RoundingRadius -> 0.4, 
 ColorFunction -> (Blend[{Red, Blue}, #2] &)]
$\endgroup$
0
24
$\begingroup$

The raster method I alluded to in a comment was requested.

g1 = Graphics[{
        Polygon[{{0, 0}, {3, 0}, {3, 1}, {0, 1}}, VertexColors -> {Red, Red, Blue, Blue}]
     }]

g2 = Graphics[{Rectangle[{0, 0}, {3, 1}, RoundingRadius -> 0.5]}]

ImageAdd[g1, g2]

Mathematica graphics

$\endgroup$
5
  • $\begingroup$ Ok I think I am going to have to accept that, unless e.g. @Heike comes on here with a solution. While I was wanting a non-rasterized solution this is too easy and concise to ignore. I'll leave it open for until tomorrow. $\endgroup$ Commented Jun 8, 2012 at 8:41
  • 2
    $\begingroup$ To whoever downvoted I specifically asked @Mr.Wizard to post a raster answer. $\endgroup$ Commented Jun 9, 2012 at 6:34
  • $\begingroup$ You got a +1 from me, but I think I improved on your answer (see bottom of my answer). $\endgroup$
    – Jens
    Commented Jun 9, 2012 at 17:50
  • $\begingroup$ @Jens thanks for the vote. I don't have SetAlphaChannel but I suppose it works well. $\endgroup$
    – Mr.Wizard
    Commented Jun 10, 2012 at 0:07
  • $\begingroup$ Oh - you're on version 7, I recall now. $\endgroup$
    – Jens
    Commented Jun 10, 2012 at 1:20
14
$\begingroup$

Edit

One can use either an image-based (hence rasterized) or a vector-based (resolution-independent) approach to get the rounded corners. I'll first discuss the vector based solution, and then add a raster-based solution. Although Mr. Wizard already posted a raster-based approach, I think it can be improved.

Update

The function roundedGraphics is rewritten so that it contains both a vector and a bitmap option in a single command. The bitmap option isn't used until later.

Vector-based approach

This solves the question but can also be used more generally to put rounded corners on arbitrary objects:

Options[roundedGraphics] = {Background -> White, 
   ImageResolution -> Infinity};
roundedGraphics[g_, w_, h_, r_, opts : OptionsPattern[]] := Module[
  {bgColor = OptionValue[Background],
   resolution = OptionValue[ImageResolution],
   commonOptions = Sequence[
     PlotRange -> {{0, 1}, {0, 1}},
     ImageSize -> {w, h},
     AspectRatio -> Full],
   passepartout},
  passepartout = FilledCurve[
    {{BezierCurve[
         {{0, #1/h}, {0, 1 - #2/h}, {0, 1 - #2/h}, {0, 1}, {#2/w, 
           1}, {1 - #3/w, 1}, {1 - #3/w, 1}, {1, 1}, {1, 
           1 - #3/h}, {1, #4/h}, {1, #4/h}, {1, 0}, {1 - #4/w, 
           0}, {#1/w, 0}, {#1/w, 0}, {0, 0}, {0, #1/h}},
         SplineDegree -> 2
         ] & @@ Apply[PadRight[#, 4, Last[#]] &, {Flatten[{r}]}]},
     {Line[{{0, 0}, {0, 1}, {1, 1}, {1, 0}}]}}
    ];
  If[
     resolution < Infinity,
     SetAlphaChannel @@ Map[
       Rasterize[#, "Image",
         ImageResolution -> resolution] &,
       {#,
        Graphics[{FaceForm[Black], EdgeForm[Black], passepartout},
         Background -> White, commonOptions]}
       ],
     #
     ] &[
   Graphics[{Inset[g, {0, 0}, {Left, Bottom}, {1, 1}], 
     FaceForm[bgColor], EdgeForm[bgColor], passepartout}, 
    Background -> bgColor, commonOptions]]
  ]

For even more generality, I'm allowing each corner to have an individually different radius. But if you only specify a single radius, that number will be used for all corners.

The arguments w, h, and r are the image width, height and rounding radius in pixels.

To get the button with a gradient, I just have to take the "object" g that is passed to cropGraphics as a rectangle with the desired gradient. So let's just copy Mr. Wizard's choice of gradient here:

g1 = Graphics[{Polygon[{{0, 0}, {3, 0}, {3, 1}, {0, 1}}, 
    VertexColors -> {Red, Red, Blue, Blue}]}, ImagePadding -> 0, 
  PlotRangePadding -> 0]

Iv'e made sure the gradient rectangle doesn't have any whitespace around it. Now I'll apply the rounding to it:

roundedGraphics[Show[g1, AspectRatio -> Full], 400, 50, 20]

button

The point of my more complicated looking function is that you can use it with other objects:

im = Import["ExampleData/lena.tif"];
roundedGraphics[im, #1, #2, 10] & @@ ImageDimensions[im]

lena

g3 = Show[ExampleData[{"Geometry3D", "StanfordBunny"}], 
   ImageSize -> 360];

roundedGraphics[
 Show[g3, AspectRatio -> Full, Background -> Black], 400, 400, 20]

bunny

You may wonder what the purpose of the AspectRatio->Full statement in the last example is. To see what it does, change the width w of the roundedGraphics from 400 to 100. With AspectRatio->Full the inset object becomes stretchable. That's especially nice if you want to make a button from an image but the image dimensions don't match the button dimensions:

splash = ImageCrop[ExampleData[{"TestImage", "Splash"}], {400, 400}];    
roundedGraphics[Show[splash, AspectRatio -> Full], 400, 200, 20]

splashhigh

roundedGraphics[Show[splash, AspectRatio -> Full], 400, 100, 20]

splashLow

Here is an example that uses individual rounding radii (when given as a list, they start at the bottom left):

roundedGraphics[
 Show[splash, AspectRatio -> Full], 400, 100, {0, 20, 20, 0}]

RoundNotRound

This kind of arrangement can be useful when making tabs instead of buttons. Since the rounded boundary is defined by a Bezier curve, you can also invoke the interactive graphics editor to adjust the control points and re-shape the output (double-click on the masking border and highlight a point on the inner curve - the mouse pointer turns into a white dot when it's ready to select a curve point).

The masking shape that defines the rounded rectangle is white by default, but you can give it a different color by using the Background option.

The main ideas in this approach come from Yu-Sung Chang for the FilledCurve trick, and this answer regarding cropping of graphics for the Inset approach.

Raster-based approach

If you're going to choose the route via a bitmap representation of the button, then you may as well make better use of the features that a bitmap approach offers and that are hard to duplicate in the vector-based approach.

The obvious additional feature that one can add here is transparency, applied to the corners of the rounded button, so that the rounding also works when the image is superimposed on an arbitrary background.

I suggested that approach in a comment to this answer (which in its last part is identical to what Mr. Wizard used in his answer here):

g1 = 
 Graphics[{Polygon[{{0, 0}, {3, 0}, {3, 1}, {0, 1}}, 
    VertexColors -> {Red, Red, Blue, Blue}]}, PlotRangePadding -> None]

g2 = Graphics[{White, 
   Rectangle[{0, 0}, {3, 1}, RoundingRadius -> 0.5]}, 
  Background -> Black, PlotRangePadding -> None]

Mask

Now I define the button with rounded corners:

button = SetAlphaChannel[g1, g2];

To show the difference to ImageAdd, display the button in front of a background:

Show[button, Background -> Yellow]

button with background

This same method is also built into the function roundedGraphics. You invoke it simply by specifying the option ImageResolution - this tells it that you want a bitmap, and the alpha channel transparency is then automatically set. Here is another example with a gradient that explicitly uses bitmaps with the standard screen resolution:

bitmapButton = roundedGraphics[
  Graphics[
   Raster[Transpose@{(Range[256] - 1)/256},
    ColorFunction -> "NeonColors"],
   ImagePadding -> 0,
   PlotRangePadding -> 0,
   AspectRatio -> Full
   ], 300, 50, {10, 40, 10, 40}, ImageResolution -> 72]

button2

This button now has transparent corners, and by choosing a higher resolution you can get arbitrarily close to the quality of the vectorized version discussed earlier.

The output of roundedGraphics with the ImageResolution option isn't a Graphics object but an Image, so you have to use bitmap commands on it, as in this example:

ImageCompose[ExampleData[{"TestImage", "Tree"}], 
 ImageResize[bitmapButton, 200], {130, 130}]

images

$\endgroup$
2
  • $\begingroup$ Nice general answer @Jens. $\endgroup$ Commented Jun 9, 2012 at 1:02
  • $\begingroup$ @MikeHoneychurch I've added an improved image-based approach since you appear to prefer that. It adds transparent corners so the button is independent of the background color. $\endgroup$
    – Jens
    Commented Jun 9, 2012 at 17:19
11
$\begingroup$

Vitaliy had a great answer. I guess another way to do this is to simply make the curves using many lines:

enter image description here

In the following code, resolution is the number of lines used to make the curve and m is how big the corners are.

    resolution = 30;
    w = 2;
    h = 1;
    m = 0.1;

circlePoint[center_, radius_, radian_] := radius {Cos[radian], Sin[radian]} + center;

    max = Max[w, h]*m;
    pts1 = Sequence @@ 
       Table[circlePoint[{max, max}, max, r], {r, \[Pi], 
         3 \[Pi]/2, \[Pi]/2/resolution}];
    pts2 = Sequence @@ 
       Table[circlePoint[{w - max, max}, max, r], {r, 3 \[Pi]/2, 
         2 \[Pi], \[Pi]/2/resolution}];
    pts3 = Sequence @@ 
       Table[circlePoint[{w - max, h - max}, max, r], {r, 
         0, \[Pi]/2, \[Pi]/2/resolution}];
    pts4 = Sequence @@ 
       Table[circlePoint[{max, h - max}, max, 
         r], {r, \[Pi]/2, \[Pi], \[Pi]/2/resolution}];

    Graphics[{Polygon[{{0, max}, pts1, { max, 0}, {w - max, 0}, 
        pts2, {w, max}, {w, h - max}, pts3, {w - max, h}, {w - max, h }, 
        pts4, {0, h - max}}, 
       VertexColors -> 
        Join[Table[Red, {2 (resolution + 1) + 4}], 
         Table[Blue, {2 (resolution + 1) + 4}]]]}]
$\endgroup$
3
  • $\begingroup$ the resulting graphic looks very nice but you haven't defined circlePoint $\endgroup$ Commented Jun 8, 2012 at 8:29
  • $\begingroup$ sorry, just added it $\endgroup$
    – M.R.
    Commented Jun 8, 2012 at 8:34
  • $\begingroup$ I assumed you wanted a non-rasterized method, but yes the image one is simpler... $\endgroup$
    – M.R.
    Commented Jun 8, 2012 at 9:05
8
$\begingroup$

Use ColorFunction along a single dimension for gradient and a smart analytic curve for boundary. You can easily control type of color gradient via ColorFunction.

RegionPlot[.7 x^8 + 80 y^8 < .3, {x, -2, 2}, {y, -2, 2}, 
 Frame -> False, Axes -> False, 
 ColorFunction -> Function[{x, y}, Hue[.3 y]]]

enter image description here

$\endgroup$
4
  • $\begingroup$ The Chang post does not deal with curved boundaries. I can fill a rectangle, star, you name it, with a gradient using the method in the question. My problem is how to get curved/rounded edges. $\endgroup$ Commented Jun 8, 2012 at 8:20
  • $\begingroup$ Reducing the Thickness shows that this method produces apparent curved edges by "obliterating" the underlying graphic. As such the user is stuck with a very thick "frame" around the graphic. $\endgroup$ Commented Jun 8, 2012 at 8:27
  • $\begingroup$ @MikeHoneychurch Sorry Mike, I just changed the whole thing before I saw your comment. $\endgroup$ Commented Jun 8, 2012 at 8:31
  • $\begingroup$ while that is more concise than the original, the problem I have is that each time I want to change the shape I am going to get a headache trying to figure out the region equation. Surely there must be a way in Mma to start with a rounded rectangle and fill it with a gradient? $\endgroup$ Commented Jun 8, 2012 at 8:35
6
$\begingroup$

The equation for Vitaliy Kaurov's solution isn't too confusing. It's just a generalization of the equation of an ellipse.

The equation for an ellipse

$\frac{x^2}{a^2}+\frac{y^2}{b^2}=1$

generalizes to

$\frac{\mid{x}\mid^n}{a^n}+\frac{\mid y \mid ^n}{b^n}=1$

Just like an ellipse, a tells you how far to the left and right x goes

and b does the same for y.

As n gets larger, the sides get flatter and the corners get sharper.

The Mathematica code I came up with looks like this.

Unit[θ_] := {Cos[θ], Sin[θ]}

BlockShape[a_, b_, n_] := 
 Table[(1/(Abs[Cos[θ]]^n/a^n + Abs[Sin[θ]]^n/b^n))^(1/
      n) Unit[θ], {θ, 0, 2 π, 2 π/200}]

RoundedBlock[a_, b_, n_] := 
 Polygon[BlockShape[a, b, n], 
  VertexColors -> 
   Table[Blend[{Darker[Blue, 0.3], Lighter[Blue, 0.4]}, 
     1/2 + Sin[θ]], {θ, 0, 2 π, 2 π/200}]]


Graphics[RoundedBlock[2, 1, 8]]

enter image description here

Graphics[RoundedBlock[2, 1, 20]]

enter image description here

For values of n between 0 and 1 we get some interesting shapes.

Graphics[RoundedBlock[1, 1, 0.5]]

enter image description here

Graphics[RoundedBlock[1, 1, 1]]

enter image description here

For further reading, see http://mathworld.wolfram.com/Rectellipse.html

$\endgroup$
6
$\begingroup$

I think you can achieve what you want using Texture. Basically, create a FilledCurve version of a rounded rectangle, and then use VertexTextureCoordinates to map a texture onto the FilledCurve. First, here is a function to generate corners:

corner[{x_,y_}, r_, {a1_,a2_}] := With[{phi = a2-a1},
    BezierCurve @ AffineTransform[{{{Cos[a1], -Sin[a1]}, {Sin[a1], Cos[a1]}}, {x,y}}][
        r * {
        {1, 4/3 Tan[phi/4]},
        {Cos[phi]+4/3 Tan[phi/4] Sin[phi], Sin[phi]-4/3 Tan[phi/4] Cos[phi]},
        {Cos[phi], Sin[phi]}
        }
    ]
]

Note that the first coordinate ({1, 0}) of the BezierCurve has been dropped because of the way FilledCurve works. With this corner function, we can create a roundedRectangle function that creates the desired FilledCurve:

Options[roundedRectangle] = {RoundingRadius -> 0};

roundedRectangle[{x1_,y1_}, {x2_,y2_}, OptionsPattern[]] := Module[
    {r = OptionValue[RoundingRadius], curve, vertices},

    curve={
        Line[{{x1+r,y1}, {x2-r,y1}}],
        corner[{x2-r,y1+r}, r, {3Pi/2,2Pi}],
        Line[{{x2,y1+r}, {x2,y2-r}}],
        corner[{x2-r,y2-r}, r, {0,Pi/2}],
        Line[{{x2-r,y2}, {x1+r,y2}}],
        corner[{x1+r,y2-r}, r, {Pi/2,Pi}],
        Line[{{x1,y2-r}, {x1,y1+r}}],
        corner[{x1+r,y1+r}, r, {Pi,3Pi/2}]
    };
    vertices = curve /. {Line[{a__}] :> a, BezierCurve[{a__}] :> a};
    FilledCurve[
        curve,
        VertexTextureCoordinates -> RescalingTransform[{{x1,x2},{y1,y2}}][vertices]
    ]
]

Here is an example of a rounded rectangle using the OP's desired gradient fill:

Graphics[{
    Texture @ Graphics[
        Polygon[{{0,0}, {1,0}, {1,1}, {0,1}}, VertexColors->{Red,Red,Blue,Blue}],
        PlotRangePadding->0
    ],
    roundedRectangle[{0,0}, {1,.2}, RoundingRadius->.05]
}]

enter image description here

And here is a more interesting Texture:

Graphics[{
    Texture[ExampleData[{"TestImage", "Sailboat"}]],
    roundedRectangle[{0,0}, {1,.2}, RoundingRadius->0.05]
}]

enter image description here

$\endgroup$
5
$\begingroup$

Since V 10.2 there is StadiumShape and since V 12.2 we have LinearGradientFilling

Graphics[{LinearGradientFilling[{Red, Blue}, Top], StadiumShape[0.5]}, Frame -> True]

enter image description here

The 3D-Version is called CapsuleShape

Graphics3D[{
  Lighting -> "ThreePoint",
  MaterialShading["Bronze"],
  CapsuleShape[0.5]}]

enter image description here

$\endgroup$
4
$\begingroup$

I've evolved the vector-based approach. Rather than converting the object being rounded to an image, I needed to interact with maps and 3D graphics so have used an Overlay with a transparent interior. This means the object in question can exist as its original head and behaves the same, it just has rounded edges overlaid.

Options[roundedOverlay] = {Background -> White};
roundedOverlay[g_, r_, opts : OptionsPattern[]] :=
 Module[
  {w, h, bgColor = OptionValue[Background], 
   commonOptions = 
    Sequence[PlotRange -> {{0, 1}, {0, 1}}, AspectRatio -> Full], 
   passepartout},
  {w, h} = ImageDimensions[g];
  passepartout = 
   FilledCurve[{{BezierCurve[{{0, #1/h}, {0, 1 - #2/h}, {0, 
           1 - #2/h}, {0, 1}, {#2/w, 1}, {1 - #3/w, 1}, {1 - #3/w, 
           1}, {1, 1}, {1, 1 - #3/h}, {1, #4/h}, {1, #4/h}, {1, 
           0}, {1 - #4/w, 0}, {#1/w, 0}, {#1/w, 0}, {0, 
           0}, {0, #1/h}}, SplineDegree -> 2] & @@ 
       Apply[PadRight[#, 4, Last[#]] &, {Flatten[{r}]}]}, {Line[{{0, 
         0}, {0, 1}, {1, 1}, {1, 0}}]}}];
  Overlay[{g, 
    Graphics[{FaceForm[bgColor], EdgeForm[bgColor], passepartout }, 
     Background -> Transparent, ImageSize -> ImageDimensions[g], 
     commonOptions]}, All, 1]
  ]

Here's some examples:

im = GeoGraphics[
   Tooltip[Polygon[Entity["Country", "UnitedStates"]], "USA"]];
roundedOverlay[im, 20]

Tooltipped map

test = Graphics3D[{Yellow, Cuboid[{0, 0, 0}, {1, 3, 1}], Blue, 
    Cuboid[{2, 1, 1}, {4, 2, 3}]}, SphericalRegion -> True, 
   Background -> LightBlue];
roundedOverlay[test, 50]

(Note the SphericalRegion->True is needed as the overlay does not resize) enter image description here

$\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.