0

I have a white inner circle and a blue outer circle.

I am trying to clip out the white inner circle to leave a blue ring.

(I know I can achieve the same thing using a single circle and a stroke, but I will be using more complex shapes in the future and want to achieve this simple task first.)

enter image description here

I have tried variations of the following:

const container = SVG().addTo('body').size(500, 500)

const bg = container.rect(500,500).fill('green')

// outer circle
const outerCircle = container.circle(100).fill('blue');

// inner circle
const innerCircle = container.circle(80).fill('white').center(outerCircle.cx(), outerCircle.cy());

// Clip the outer circle with the inner circle
outerCircle.clipWith(innerCircle);

However, this removes the outer circle, like so:

enter image description here

Can anyone tell me what I am doing wrong?

Here's a public JSFiddle: https://jsfiddle.net/j1pq7drb/

J-C
  • 33
  • 4
  • Clip paths works the other way round: they will only show the area within its the clip path shape. You could use a [mask instead](https://codepen.io/herrstrietzel/pen/ExOWEXr) – herrstrietzel Jun 28 '23 at 18:13
  • Should the area/white circle be transparent so that you can see the white color behind the green? OR is it fine that the green color shows? – chrwahl Jun 29 '23 at 19:51
  • I want the green be visible. herrstrietzel's fiddle is exactly what I want to acheive, but I want to use SVG.JS if possible rather than raw SVG. Thanks – J-C Jun 30 '23 at 14:42

2 Answers2

0

Based on the comments, you know you can use a mask instead of clip to achieve the desired affect.

In SVG.JS, you can use maskWith, which will look something like this:

const container = SVG().addTo('body').size(500, 500);

const bg = container.rect(500,500).fill('green');

// outer circle
const outerCircle = container.circle(100).fill('blue');
const maskGroup = container.mask();

// This is to allow the original shape to be visible. Could also be done with a clone of the original shape
maskGroup.rect(500,500).fill('white');

// Add the inner circle to the group
maskGroup.circle(80).fill('black').center(outerCircle.cx(), outerCircle.cy());

// Mask the outer circle with the the group
outerCircle.maskWith(maskGroup);

Since Mask inherits from SVG.Container in SVG.JS, you can be very creative with its usage. For more complicated shapes (multiple polkadots, as an example), you can create groups inside your mask to keep everything tidy.

const container = SVG().addTo('body').size(500, 500);
const bg = container.rect(500,500).fill('green');

// Create some shape
const shape = container.circle(250).center(200, 200).fill('pink');

// Create the mask def
const mask = container.mask();

// Add a white rectangle the size of the svg to allow the shapes to be visible
mask.rect(500, 500).fill('white');

// Create a group within the mask
const polkaDots = mask.group();

// Add circles to the mask which will appear "cut out" of the main shape
polkaDots.circle(20).center(250, 210).fill('black');
polkaDots.circle(50).center(150, 200).fill('black');
polkaDots.circle(30).center(200, 120).fill('black');

// Apply the mask to the shape
shape.maskWith(mask);

This has an added benefit of allowing you to use group styles or group positioning to make sweeping changes.

Dallas
  • 68
  • 1
  • 6
0

Although I've recommended <mask> as a workaround ...

<mask> vs. <clipPath> – don't forget compound paths

Sure compound paths require a better understanding of SVG path commands, but it's worthwhile!

Actually, each "e" and "o" you see in this post – rendered from text, based on a font etc. – relies on compound paths – a path containing an outer and inner shapes that appear "cut out" (like the letter O).

For simple tasks like rendering a "donut" shape, you should prefer compound paths over masks or clip paths.

const container = SVG().addTo('body').size(500, 500)
const bg = container.rect(500,500).fill('green')

let donutPathData = createDonutPathData(20, 30, 100, 80);
const donut = container.path(donutPathData).fill('blue');

// create donut paths
function createDonutPathData(x, y, d1, d2){
  let strokeWidth = (d1-d2)
  let pathData = 
      // outer circle
      createCirclePathData(x, y, d1, 1) + 
      // inner circle (cut out)
      ' '+ createCirclePathData(0, strokeWidth/2, d2, 0, true)
  return pathData;
}

// create circle paths
function createCirclePathData(x=0, y=0, d=100, sweep=1, relative=false ){
  let M = relative ? `m ${x} ${y}` : `M ${d/2+x} ${y}`;
  let pathData = `${M} a ${d/2} ${d/2} 0 0 ${sweep} 0 ${d} a ${d/2} ${d/2} 0 0 ${sweep} 0 -${d}`;
  return pathData;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/svg.js/3.2.0/svg.min.js"></script>

How it works

We're using a custom helper function createCirclePathData() generating arcs according to radius and x/y values returning a d pathData attribute string which can be passed to SVG.js path() method.

A donut shape could easily be created with one path including a subpath like so – outer diameter=100; inner diameter=80:

M 50 0
A 50 50 0 0 1 50 100
A 50 50 0 0 1 50 0
M 50 10
A 40 40 0 0 0 50 90
A 40 40 0 0 0 50 10

Both outer and inner circle are based on the A (Arc) command

The above code translates to

/**
* starting point: x = r or diameter/2; 
*/
M 50 0 
/**
* outer circle: 
* radius, radius, angle, long arc flag, sweep flag (clockwise or counterclockwise), final x, final y 
*/
A 50 50 0 0 1 50 100
A 50 50 0 0 1 50 0

/** inner circle: 
* new starting point according to inner diameter
* radius and final points change accordingly – 
* we change the path direction via sweep flag. 
* This way the inner circle will define the cut-out shape
*/ 
M 50 10
A 40 40 0 0 0 50 90
A 40 40 0 0 0 50 10

See also "How to calculate the SVG Path for an arc (of a circle)" (and a lot more posts tackling pie charts in svg)

The additional custom function createDonutPathData() is just a wrapper to generate a path d value with outer and inner circle pathData.

The benefits of this approach:

  • no need for additional fill-rule attributes (that might not be supported by all applications
  • more compact output – no need for <clipPath> or <mask> definitions
  • no need for legacy syntax references like xlink:href – still required by a lot of graphic applications or image viewers.

Benefits of <mask>

  • masks are great for complex shapes like dashed strokes
  • support semi-transparency

Caveats of <mask>

  • more expensive because they introduce an alpha transparency rendering context
  • The pointer area is not "clipped" to the visible shape: A masked element responds to pointer events in the same way as the original/unmasked element, even in transparent/masked areas.
herrstrietzel
  • 11,541
  • 2
  • 12
  • 34