3

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)

floatingpurr
  • 7,749
  • 9
  • 46
  • 106
  • 1
    @RobertLongson thanx. Do I need to use offsets like I did in the example that I posted above (ie. `stop offset="60%" `)? Still not sure if this selects the 60% of the area (ie. number of filling pixels) or if it just puts a stop at the 60% of the Y-coordinate. – floatingpurr Jun 07 '21 at 15:19
  • 1
    Also see: [StackOverflow: Show percentage in SVG graphic](https://stackoverflow.com/questions/66671913/show-percentage-in-svg-graphic) – Danny '365CSI' Engelman Jun 07 '21 at 15:33
  • 1
    Not sure if I am mistaken, but the linear gradient doesn't seem to work (see the snippet in the post) – floatingpurr Jun 07 '21 at 16:00
  • 1
    Just to be clear: none of the reported duplicated posts answers this question – floatingpurr Jun 07 '21 at 16:20
  • 1
    If you really need to fill exactly, say, 60% of the *area* (and not simply 60% of the *height*) then linear gradient approaches will not work, since the algorithm is heavily dependent on the shape of the path. Your question is very interesting, but the difference between area and height got mistakenly overlooked by those who closed it. Would you consider creating a new one with more emphasis on the area vs height difference? – Rodrigo Divino Jun 07 '21 at 20:07
  • 1
    @Robert, I was working on an answer.. and you closed it! Non of the answers you link to do the **exact** percentage.. It can be done, I am almost there... https://jsfiddle.net/dannye/h69dfu0j/ (But its bedtime here in Amsterdam) – Danny '365CSI' Engelman Jun 07 '21 at 20:40
  • Does this solution have to work for any shape you throw at it, or do you know what the shape will be beforehand? – Paul LeBeau Jun 09 '21 at 08:47
  • It'd be great if a generic approach exists. However, I'd like to use it for circular sectors with corner radii https://bl.ocks.org/mbostock/b7671cb38efdfa5da3af – floatingpurr Jun 09 '21 at 08:50
  • So you want to be able to fill 60% of the pixels is one of those rounded circular sectors? – Paul LeBeau Jun 09 '21 at 09:06
  • I started from a generic question because I thought there was a generic approach. However, yes, my first aim is filling x% of the pixels of a rounded circular sector – floatingpurr Jun 09 '21 at 09:22
  • I got close, but no cigar.. maybe this SO answer helps: https://stackoverflow.com/questions/10039679/how-can-i-calculate-the-area-of-a-bezier-curve Do read all comments – Danny '365CSI' Engelman Jun 09 '21 at 12:00
  • See below, it does the job, but with a brute-force method. – Danny '365CSI' Engelman Jun 22 '21 at 11:20

2 Answers2

3

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.

Matt Sergej Rinc
  • 565
  • 3
  • 11
3

How to fill a <path> with a percentage volume

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

Using a native JavaScript Web Component (JSWC)

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>
Danny '365CSI' Engelman
  • 16,526
  • 2
  • 32
  • 49
  • Oh, ok! What about using the pixel approach to get the approximated offset of a gradient? This would color better each shape. E.g, a function taking as input the path and the portion of the area you want to color and returning the estimated offset that can be later used in a common gradient. – floatingpurr Jun 22 '21 at 11:25
  • 1
    That can be done with the ``P.y`` value of the green circle. The circles are drawn from bottom to top.... [I've spent enough time on this] – Danny '365CSI' Engelman Jun 22 '21 at 11:36
  • Yes, I don't want to take advantage of your time. It was just a comment for the readers. Thanx! – floatingpurr Jun 22 '21 at 11:41
  • This is very useful thanks. I'm trying to see if I can manipulate it to fill as far as a defined *Y* height value in the shape, rather than a percentage of the shape. I'm having no luck but trying to check if the current circle in the array crosses that Y threshold. – biscuitstack Jul 05 '21 at 15:58
  • Actually, I've sorted this: it's just a case of comparing the y value you're already using to see if it's hit a certain threshold. I'm having some issues with this that I can't get my head around but I'm pretty confident the logic is right and I've a bug somewhere else. – biscuitstack Jul 06 '21 at 11:40
  • Post your code in a new question so all SVG gurus can chime in. I am now the only person receiving your comments – Danny '365CSI' Engelman Jul 06 '21 at 14:02