Scaling the filling portion of an SVG path

Viewed 504

Let's say we have the following SVG path:

<path id="heart" d="M 10,30 A 20,20 0,0,1 50,30 A 20,20 0,0,1 90,30 Q 90,60 50,90 Q 10,60 10,30 z" />

I'm wondering if there is a way to fill only a portion of the area of this shape proportional to a given ratio. I do need to color exactly the amount of pixels equals to the ratio.

E.g., ratio = 0,6, then just fill the 60% of the pixels of the shape (starting from the bottom).

Edit:

I tried to implement the @RobertLongson's hint in the comment. There you go:

<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">

<linearGradient id="myGradient" gradientTransform="rotate(90)">
  <stop offset="50%"  stop-color="black" stop-opacity='0' />
  <stop offset="50%" stop-color="black" />
</linearGradient>


<path id="heart" d="M 10,30 A 20,20 0,0,1 50,30 A 20,20 0,0,1 90,30 Q 90,60 50,90 Q 10,60 10,30 z" fill="url('#myGradient')" stroke="black" stroke-width="1" />
</svg>

However, if the ratio is 50%, this doesn't work. (i.e. we are not coloring the 50% of the pixels)

2 Answers

While "Danny '365CSI' Engelman" is on the right track if he solves concave regions I will twist this around (because I always search for existing solutions to use if possible).

So (explaining the algorithm, somebody can implement it) why don't you use turf.js library? It has implemented area calculation that could be used. It would be somehow tricky, reqPct stands for requested percentage of area to fill:

get points of SVG path to an arrPoint
svgPolygon <- turf.polygon(arrPoint)
fullArea <- turf.area(svgPolygon)
tolerance <- 0.5
btmPct <- 0
topPct <- 100
divPct <- 0
curAreaPct <- 0
while (abs(reqPct-curAreaPct) > tolerance) do
  divPct <- (topPct-btmPct)/2
  rect <- turf.polygon with full width and divPct height of polygon
  intersectPolygon <- turf.intersect(svgPolygon, rect);
  interArea <- turf.area(intersectPolygon);
  curAreaPct <- 100 * interArea/fullArea;
  if (curAreaPct > reqPct)
    topPct <- divPct
  else
    btmPct <- divPct
end while
fill the SVG with gradient color from bottom to divPct

The idea is to turn points of the path (determining how many points has been solved by algorithms respecting the curviness) to the polygon and use turf library to calculate areas of matching intersected polygon (found by placing a rectangle with full SVG width and divPct percentage times SVG height - both can be obtained with getBBox() function) in a loop.

Finding that polygon is a simple binary search to find the one that has an area close to requested percentage. When that is true you just have to fill the SVG with a gradient color from the bottom to the found percentage (height of the SVG, not fill area percentage value).

I know it appears complex and some canvas based solution might also work yet this would still be fast to calculate and finally fill the SVG to the desired percentage of its area.

In the comments I tried to be too smart with https://jsfiddle.net/dannye/h69dfu0j/

I went for the brute force method eventually;

Check every SVG pixel being in the shape with isPointInFill

All data is available in the filled Array and P.y values

filled values are from bottom to top of the path shape

Needs some more work with those values to draw a solid fill or gradient

50% , 20% and 80% <svg-area> shapes:

<style>
  svg { --size: 180px; height: var(--size); background: pink }
  path { fill: lightgreen; stroke: grey }
</style>
<svg-area percent=".5">
  <svg viewBox="0 0 100 100">
    <path d="M10,30A20,20,0,0,1,50,30A20,20,0,0,1,90,30Q90,60,50,90Q10,60,10,30z"></path>
  </svg>
</svg-area>
<svg-area percent=".2">
  <svg viewBox="0 0 100 100">
    <path d="M60,10L90,90L10,60z"></path>
  </svg>
</svg-area>
<svg-area percent=".8">
  <svg viewBox="0 0 50 50">
    <path d="m18 15q41 18 28-8l-14 30-29-15a1 1 0 0029 15z"></path>
  </svg>
</svg-area>
<script>
  customElements.define("svg-area", class extends HTMLElement {
    connectedCallback() {
      setTimeout(() => {// make sure Web Component can read parsed innerHTML
        let svg = this.querySelector("svg");
        let path = svg.querySelector("path");
        let filled = [], percent = this.getAttribute("percent");
        Array(svg.height.baseVal.value).fill().map((_, y) => {
          Array(svg.width.baseVal.value).fill().map((_, x) => {
            let P = svg.createSVGPoint(); P.x = x; P.y = y;
            if (path.isPointInFill(P)) filled.push( P );
          });
        });
        svg.append(
              ...filled.reverse()
                       .filter( (P,idx) => idx < filled.length * percent)
                       .map( P => this.circle(P.x, P.y, "green") ));
      });
    }
    circle(cx, cy, fill) {
      let circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
      circle.setAttribute("cx", cx);
      circle.setAttribute("cy", cy);
      circle.setAttribute("fill", fill);
      circle.setAttribute("r", .3);
      return circle;
    }
  });
</script>

Related