Ben Guest Ben Guest - 1 month ago 5
Javascript Question

Partially fill a shape's border with colour

I am trying to create a progress effect whereby colour fills a DOM object's border (or possibly background). The image attached should give you a better idea of what I'm going for. I have achieved the current result by adding an object with a solid background colour over the grey lines and setting its height. This object has

mix-blend-mode: color-burn;
applied to it which is why it only colours the grey lines underneath it.

This works okay, but ruins the anti aliasing around the circle, and also the produced colour is unpredictable (changes depending on the colour of the lines).

I feel there must be a better way of achieving this, perhaps with the canvas element. Could someone point me in the right direction please?

Thanks in advance!

Example

Answer

This should be possible to do with Canvas and may even be possible with CSS itself by playing with multiple elements etc but I would definitely recommend you to use SVG. SVG offers a lot of benefits in terms of how easy it is to code, maintain and also produce responsive outputs (unlike Canvas which tends to become pixelated when scaled).

The following are the components:

  • A rect element which is the same size as the parent svg and has a linear-gradient fill. The gradient has two colors - one is the base (light gray) and the other is the progress (cyan-ish).
  • A mask which is applied on the rect element. The mask has a path which is nothing but the line and the circle. When the mask is applied to the rect, only this path would show through the actual background (or fill) of the rect, the rest of the area would be masked out by the other rect which is added inside the mask.
  • The mask also has a text element to show the progress value.
  • The linear-gradient has the stop offset set in such a way that it is equal to the progress. By changing the offset, we can always make sure that the path shows the progress fill only for the required length and the base (light gray) for the rest.

window.onload = function() {
  var progress = document.querySelector('#progress'),
    base = document.querySelector('#base'),
    prgText = document.querySelector('#prg-text'),
    prgInput = document.querySelector('#prg-input');
  prgInput.addEventListener('change', function() {
    prgText.textContent = this.value + '%';
    progress.setAttribute('offset', this.value + '%');
    base.setAttribute('offset', this.value + '%');
  });
}
svg {
  width: 200px;
  height: 300px;
}
path {
  stroke-width: 4;
}
#rect {
  fill: url(#grad);
  mask: url(#path);
}

/* just for demo */
.controls {
  position: absolute;
  top: 0;
  right: 0;
  height: 100px;
  line-height: 100px;
  border: 1px solid;
}
.controls * {
  vertical-align: middle;
}

body {
  background-image: radial-gradient(circle, #3F9CBA 0%, #153346 100%);
}
<svg viewBox='0 0 200 300' id='shape-container'>
  <linearGradient id='grad' gradientTransform='rotate(90 0 0)'>
    <stop offset='50%' stop-color='rgb(0,218,235)' id='progress' />
    <stop offset='50%' stop-color='rgb(238,238,238)' id='base' />
  </linearGradient>
  <mask id='path' maskUnits='userSpaceOnUse' x='0' y='0' width='200' height='300'>
    <rect x='0' y='0' width='200' height='300' fill='black' />
    <path d='M100,0 100,100 A50,50 0 0,0 100,200 L100,300 M100,200 A50,50 0 1,0 100,100' stroke='white' />
    <text id='prg-text' x='100' y='155' font-size='20' text-anchor='middle' fill='white'>50%</text>
  </mask>
  <rect id='rect' x='0' y='0' width='200' height='300' />
</svg>

<!-- just for demo -->
<div class='controls'>
  <label>Set Progress:</label>
  <input type='range' id='prg-input' min='0' max='100' value='50' />
</div>


If you are new to SVG you can refer to the MDN or the SO Docs (links provided below) for more information about the elements, their attributes and values.

Comments