Jose
Jose

Reputation: 45

How can I mask an SVG's background using polygons?

I'm trying yo make an SVG with a gradient backgound color from red to yellow to green, inside the SVG I've got 2 polygons (for now because there'll be more) that I wish would mask the SVG's background. I tried the following code:

<svg height="500" width="500">
    <defs>
    <linearGradient id="grad1" x1="0%" y1="0%" x2="0%" y2="100%">
    <stop offset="0%" style="stop-color:rgb(34,177,76);stop-opacity:1" />
    <stop offset="50%" style="stop-color:rgb(255,242,0);stop-opacity:1" />
    <stop offset="100%" style="stop-color:rgb(237,28,36);stop-opacity:1" />
    </linearGradient>
    </defs>

    <polygon points="0, 0, 100, 0, 100, 100, 0, 100" fill="url(#grad1)">
    <polygon points="100, 100, 150, 250, 300, 350, 150, 400, 100, 300" fill="url(#grad1)" >
    </svg>

How can I apply a background to the SVG and have the polygons show the part of the background based on their position?

enter image description here

UPDATE

I updated my code (YES it needs some work) to achive a path with a unified background and also have transparent polygons in case you wish to add a onclick event.

<?php
//Array with the different sections and their coordinates
        $body['item1'] = array(array(0, 0), array(100, 0), array(100, 100), array(0, 100));
        $body['item2'] = array(array(100, 100), array(150, 250), array(300, 350), array(150, 300), array(100, 300));
        $body['item3'] = array(array(300, 50), array(300, 350), array(200, 350), array(200, 50));
        ?>
        <svg x="0px" y="0px" height="500px" width="500px">
        <defs>
        <linearGradient id="gradient" x1="0%" y1="0%" x2="0%" y2="100%">
        <stop offset="0" style="stop-color:rgb(34,177,76);" />
        <stop offset="50%" style="stop-color:rgb(255,242,0);" />
        <stop offset="100%" style="stop-color:rgb(237,28,36);" />
        </linearGradient>
        </defs>

        <g id="singlePath_correctGradient" fill="url(#gradient)">
        <?php
        $polygon = '';
        echo '<path d="';
        foreach ($body as $key => $values) {
            $polygon .= '<polygon points="';
            $a = 1;

            foreach ($values as $coord) {
                echo $a == 1 ? ' M ' . $coord[0] . ', ' . $coord[1] : '';
                echo $a == 2 ? ' L ' . $coord[0] . ', ' . $coord[1] : '';
                echo $a > 2 ? ', ' . $coord[0] . ', ' . $coord[1] : '';

                $polygon .= ($a == 1 ? '' : ', ') . $coord[0] . ', ' . $coord[1];
                $a++;
            }
            $polygon .= '" onclick="alert(\'' . $key . '\')" fill-opacity="0"/>';
        }
        echo '"></path>';

        echo $polygon;
        ?>
        </g>
        </svg>

Upvotes: 0

Views: 253

Answers (2)

Michael Mullany
Michael Mullany

Reputation: 31750

Change your gradientUnits to "userSpaceOnUse" - that way the gradient is defined in the SVG box space, not the filled unit. Easy. (and please, close your elements!! - SVG is XML)

<svg x="0px" y="0px" height="500px" width="500px" viewBox="0 0 500 500">
    <defs>
    <linearGradient id="grad1" x1="0" y1="0" x2="300" y2="350" gradientUnits="userSpaceOnUse">
    <stop offset="0" style="stop-color:rgb(34,177,76);" />
    <stop offset="50%" style="stop-color:rgb(255,242,0);" />
    <stop offset="100%" style="stop-color:rgb(237,28,36);" />
    </linearGradient>
    </defs>

    <polygon points="0, 0, 100, 0, 100, 100, 0, 100" fill="url(#grad1)"/>
    <polygon points="100, 100, 150, 250, 300, 350, 150, 400, 100, 300" fill="url(#grad1)" />
    </svg>

Upvotes: 1

Andrew Willems
Andrew Willems

Reputation: 12458

UPDATE: I now show two snippets below. The first (original) snippet shows a manual solution. The second (newer) snippet shows a programmatic solution using JavaScript.

Are you OK with converting your polygons into paths? If so, you can combine multiple polygons into a single path and then apply the gradient to the whole thing. In the first code snippet below, I show three groups:

  1. your original two polygons...incorrect gradients
  2. the two polygons converted to two paths...still incorrect gradients
  3. the two paths merged into a single path...correct gradient

Basically, to convert a polygon to a path, change the points attribute to a d attribute. Then, within the d value string, place an M (for "MoveTo") at the beginning and an L (for "LineTo") after the first two numbers. Putting an M halfway through the path is your way of telling the program "lift the pen and, without drawing anything, move it to this new location and continue drawing there", effectively allowing you to draw multiple "shapes" within a single path.

<svg height="500" width="500">
  <defs>
    <linearGradient id="grad1" x1="0%" y1="0%" x2="0%" y2="100%">
      <stop offset="0%" style="stop-color:rgb(34,177,76);stop-opacity:1" />
      <stop offset="50%" style="stop-color:rgb(255,242,0);stop-opacity:1" />
      <stop offset="100%" style="stop-color:rgb(237,28,36);stop-opacity:1" />
    </linearGradient>
  </defs>

  <g id="twoPolygons_incorrectGradient" fill="url(#grad1)" transform="scale(0.5) translate(0,0)">
    <polygon points="0, 0 100, 0, 100, 100, 0, 100"></polygon>
    <polygon points="100, 100 150, 250, 300, 350, 150, 400, 100, 300"></polygon>
  </g>
  <g id="twoPaths_incorrectGradient" fill="url(#grad1)" transform="scale(0.5) translate(300,0)">
    <path d="M 0, 0 L 100, 0, 100, 100, 0, 100"></path>
    <path d="M 100, 100 L 150, 250, 300, 350, 150, 400, 100, 300"></path>
  </g>
  <g id="singlePath_correctGradient" fill="url(#grad1)" transform="scale(0.5) translate(600,0)">
    <path d="M 0, 0 L 100, 0, 100, 100, 0, 100 
             M 100, 100 L 150, 250, 300, 350, 150, 400, 100, 300"></path>
  </g>
  <g>
    <text   x="0" y="70">wrong</text>
    <text x="150" y="70">wrong</text>
    <text x="300" y="70">right</text>
  </g>
</svg>

I've done the conversions above manually. However, this type of conversion could also be done programmatically, e.g. with JavaScript, which I have done in the second snippet below. Essentially this does the following:

  • get all desired polygons
  • for each polygon, read the points attribute value string
  • duplicate the final point (to numerically close each shape...important for strokes/borders)
  • insert the "M" (MoveTo) and "L" (LineTo) as described above
  • concatenate all theses strings
  • add a final "Z" (ClosePath) to close the path
  • create a new single path element
  • place the concatenated string from above into the d attribute of the path
  • apply the gradient to the new path

The example below initially shows 7 polygons with the same gradient applied to each individual shape. Note that the last polygon is simply one point and is thus invisible, indicated by the dotted circle.

After clicking the "Combine..." button, polygons 2 and 3 are combined into a single path, as are polygons 4 and 5 as well as polygons 6 and 7. The same gradient as before is now applied to each combined path. Note how the path combinations affect the placement of the colors along the gradient.

Note that the utility function I've written makes it as simple as adding more polygon id's to an array in order to combine more than two polygons together.

var xmlns = "http://www.w3.org/2000/svg";

var combine = function(polygonIds, newPathId) {
  var $newPath = $(document.createElementNS(xmlns, "path"));
  // jQuery does not work here, e.g. var $newPath = $("<path>", {id: newPathId});
  $newPath.attr("id", newPathId);
  $("svg").append($newPath);
  var dStr = "";
  polygonIds.forEach(function(polygonId, idx, arr) {
    var $polygon = $("#" + polygonId);
    var ptsStr = $polygon.attr("points");
    dStr += "M " + ptsStr.replace(/, *| +/g, " ").trim().replace(/^([^ ]+ [^ ]+ )(.*)/, "$1L $2 $1");
  });
  dStr += "Z";
  $newPath.attr("d", dStr);
};

$("button#combine").click(function() {
  combine(["shape2", "shape3"], "path2and3");
  combine(["shape4", "shape5"], "path4and5");
  combine(["shape6", "shape7"], "path6and7");
  $("#path2and3").attr("fill", "url(#grad1)");
  $("#path4and5").attr("fill", "url(#grad1)");
  $("#path6and7").attr("fill", "url(#grad1)");
  $("#labels").remove().appendTo($("svg"));
});
$("button#delete").click(function() {
  $("path").remove();
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<p>Best viewed on "Full Page"</p>
<p>The "Combine..." button will combine polygons 2 and 3, polygons 4 and 5, and polygons 6 and 7. Polygon 7 is a single dot and is thus essentially invisible. The same gradient is applied to each individual polygon as well as to each of the new paths.</p>
<div>
  <button id="combine">Combine polygons into paths</button>
  <button id="delete" >Delete new paths           </button>
</div>
<svg height="500" width="500">
  <defs>
    <linearGradient id="grad1" x1="0%" y1="0%" x2="0%" y2="100%">
      <stop offset="0%"   style="stop-color:rgb(34,177,76);stop-opacity:1" />
      <stop offset="50%"  style="stop-color:rgb(255,242,0);stop-opacity:1" />
      <stop offset="100%" style="stop-color:rgb(237,28,36);stop-opacity:1" />
    </linearGradient>
  </defs>

  <g fill="url(#grad1)" transform="scale(1) translate(0,0)">
    <polygon id="shape1" points=" 30, 100   60, 160   30, 220    0, 160"></polygon>
    <polygon id="shape2" points="100,   0  120,   0  110, 120"          ></polygon>
    <polygon id="shape3" points="130, 100  160, 160  130, 220  100, 160"></polygon>
    <polygon id="shape4" points="230, 100  260, 160  230, 220  200, 160"></polygon>
    <polygon id="shape5" points="210, 200  220, 320  200, 320"          ></polygon>
    <polygon id="shape6" points="330, 100  360, 160  330, 220  300, 160"></polygon>
    <polygon id="shape7" points="330, 320"                              ></polygon>
  </g>
  <circle cx="330" cy="320" r="10" fill="none" stroke="black" stroke-dasharray="3 3"></circle>
  <g id="labels" font-family="Verdana" font-size="18">
    <text x=" 24" y="165">1</text>
    <text x="104" y=" 20">2</text>
    <text x="124" y="165">3</text>
    <text x="224" y="165">4</text>
    <text x="204" y="310">5</text>
    <text x="324" y="165">6</text>
    <text x="324" y="305">7</text>
  </g>
  
</svg>

Upvotes: 0

Related Questions