Home > Mobile >  How to achieve an XOR operation with two SVG shapes on top of each other?
How to achieve an XOR operation with two SVG shapes on top of each other?

Time:09-08

Here's the simple web app I'm trying to build: a randomly chosen shape (from a set of say 100 possible shapes) will be placed on top of a randomly chosen larger "container" (from a set of say 100 possible containers).

Some of the containers are opaque shapes, fully filled in, but some of them are outlines (imagine a 1px stroke circle).

So when combining the shapes, I need to do an XOR operation, essentially:

  • if the container is an outline: render both shape and container
  • if the container is filled in: cut the shape out of the container

Here's an image that hopefully communicates the desired result:

my code desired output
enter image description here enter image description here

I have a code reference where it's working as desired, but it's in React Native (unfortunately can't share it here), using Shopify's React Native Skia library, because that library has a blendMode="xor" option that SVG doesn't appear to have natively.

I found <feComposite operation="xor">, but that appears to work with pixels instead of in vectorspace. Honestly, that should be fine, but I can't quite get that working either.

Here's a sample shape and a sample of each of the two kinds of container:

  • shape:
<path d="M8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16ZM8 13C10.7614 13 13 10.7614 13 8C13 5.23858 10.7614 3 8 3C5.23858 3 3 5.23858 3 8C3 10.7614 5.23858 13 8 13Z" fillType="evenOdd"></path>
  • container 1 (filled in):
<path d="M0 8C0 3.58172 3.58172 0 8 0H28C32.4183 0 36 3.58172 36 8V28C36 32.4183 32.4183 36 28 36H8C3.58172 36 0 32.4183 0 28V8Z"></path>
  • container2 (outline):
<path d="M18 2.76923C14.1765 2.76923 11.0769 5.8688 11.0769 9.69231V11.0769H9.69231C5.8688 11.0769 2.76923 14.1765 2.76923 18C2.76923 21.8235 5.8688 24.9231 9.69231 24.9231H11.0769V26.3077C11.0769 30.1312 14.1765 33.2308 18 33.2308C21.8235 33.2308 24.9231 30.1312 24.9231 26.3077V24.9231H26.3077C30.1312 24.9231 33.2308 21.8235 33.2308 18C33.2308 14.1765 30.1312 11.0769 26.3077 11.0769H24.9231V9.69231C24.9231 5.8688 21.8235 2.76923 18 2.76923ZM8.39391 8.39391C9.02837 3.65493 13.0874 0 18 0C22.9126 0 26.9716 3.65493 27.6061 8.39391C32.3451 9.02837 36 13.0874 36 18C36 22.9126 32.3451 26.9716 27.6061 27.6061C26.9716 32.3451 22.9126 36 18 36C13.0874 36 9.02837 32.3451 8.39391 27.6061C3.65493 26.9716 0 22.9126 0 18C0 13.0874 3.65493 9.02837 8.39391 8.39391Z" fillType="evenOdd"></path>

And here is a CodePen of the below code:

const shape = (
  <path
    d="M8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16ZM8 13C10.7614 13 13 10.7614 13 8C13 5.23858 10.7614 3 8 3C5.23858 3 3 5.23858 3 8C3 10.7614 5.23858 13 8 13Z"
    fill-type="evenOdd"
  ></path>
);

const containerOutline = (
  <path
    d="M18 2.76923C14.1765 2.76923 11.0769 5.8688 11.0769 9.69231V11.0769H9.69231C5.8688 11.0769 2.76923 14.1765 2.76923 18C2.76923 21.8235 5.8688 24.9231 9.69231 24.9231H11.0769V26.3077C11.0769 30.1312 14.1765 33.2308 18 33.2308C21.8235 33.2308 24.9231 30.1312 24.9231 26.3077V24.9231H26.3077C30.1312 24.9231 33.2308 21.8235 33.2308 18C33.2308 14.1765 30.1312 11.0769 26.3077 11.0769H24.9231V9.69231C24.9231 5.8688 21.8235 2.76923 18 2.76923ZM8.39391 8.39391C9.02837 3.65493 13.0874 0 18 0C22.9126 0 26.9716 3.65493 27.6061 8.39391C32.3451 9.02837 36 13.0874 36 18C36 22.9126 32.3451 26.9716 27.6061 27.6061C26.9716 32.3451 22.9126 36 18 36C13.0874 36 9.02837 32.3451 8.39391 27.6061C3.65493 26.9716 0 22.9126 0 18C0 13.0874 3.65493 9.02837 8.39391 8.39391Z"
    fill-type="evenOdd"
  ></path>
);

const containerFilled = (
  <path d="M0 8C0 3.58172 3.58172 0 8 0H28C32.4183 0 36 3.58172 36 8V28C36 32.4183 32.4183 36 28 36H8C3.58172 36 0 32.4183 0 28V8Z"></path>
);

const OutputSVG = ({ shape, container }) => {
  return (
    <svg>
      <defs>
        <filter id="output">
          <g id="shape" transform="translate(10,10)">
            {shape}
          </g>
          <feComposite operator="xor" in="shape" />
        </filter>
      </defs>

      <g
        id="container"
        filter="url(#output)"
      >
        {container}
      </g>
    </svg>
  );
};

const App = () => {
  return (
    <div>
      <p>Outline</p>
      <OutputSVG shape={shape} container={containerFilled} />
      <p>Filled</p>
      <OutputSVG shape={shape} container={containerOutline} />
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById("root"));

I'm trying to get the OutputSVG component to do an XOR operation on the shape and container. (And actually, this whole outputted shape will likely be a mask once I figure out this step, since I want the "fill" of the resulting shape to be a complex gradient, rather than just a black fill.)

I'm not very good at SVG stuff, so I'm probably doing something completely wrong, but I'm finding it hard to find good example code that does what I want.

If an SVG god out there can help me get this working I'll be eternally grateful. Thanks!

CodePudding user response:

Here's how you'd use an SVG filter to do an xor composite. You have to put the shape into the defs section and "import" it into your filter using feImage before you can use it. (This doesn't work on Firefox which doesn't support using fragment identifiers within feImage - so you'll have to use an feImage with a data:uri for cross browser.)

Here's the plain SVG Chrome/Safari version anyway. feImage rendering/positioning is buggy, and it doesn't adjust properly for non-default filter regions, so you have to move your shape away from the edge of the filter region using a transform/translate to get it to render properly. Be prepared to adjust the dimensioning of viewBox, filter region and feImage position to get the shapes positioned properly.

svg {
background: rgb(0,50, 50);
}
<svg width="800px" height="600px" viewBox="0 0 100 100">

<defs>

  <path transform="translate (2 5)" id="shapeOne"fill="blue" stroke="red" stroke-width="2" d="M8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16ZM8 13C10.7614 13 13 10.7614 13 8C13 5.23858 10.7614 3 8 3C5.23858 3 3 5.23858 3 8C3 10.7614 5.23858 13 8 13Z" fill-type="evenOdd" />
    
<filter id="XORme">
  <feImage xlink:href="#shapeOne"/>
  <feComposite operator="xor" in="SourceGraphic"/>
</filter>

</defs>

<g filter="url(#XORme)">
  <path fill="green" stroke="gray" d="M18 2.76923C14.1765 2.76923 11.0769 5.8688 11.0769 9.69231V11.0769H9.69231C5.8688 11.0769 2.76923 14.1765 2.76923 18C2.76923 21.8235 5.8688 24.9231 9.69231 24.9231H11.0769V26.3077C11.0769 30.1312 14.1765 33.2308 18 33.2308C21.8235 33.2308 24.9231 30.1312 24.9231 26.3077V24.9231H26.3077C30.1312 24.9231 33.2308 21.8235 33.2308 18C33.2308 14.1765 30.1312 11.0769 26.3077 11.0769H24.9231V9.69231C24.9231 5.8688 21.8235 2.76923 18 2.76923ZM8.39391 8.39391C9.02837 3.65493 13.0874 0 18 0C22.9126 0 26.9716 3.65493 27.6061 8.39391C32.3451 9.02837 36 13.0874 36 18C36 22.9126 32.3451 26.9716 27.6061 27.6061C26.9716 32.3451 22.9126 36 18 36C13.0874 36 9.02837 32.3451 8.39391 27.6061C3.65493 26.9716 0 22.9126 0 18C0 13.0874 3.65493 9.02837 8.39391 8.39391Z" fill-type="evenOdd" />
  </g>
  
  
</svg>

  • Related