Search code examples
javascripthtmlcsshtml5-canvas

progress ring with color change based on angle


I need to get 100 ring images.

Every image, receives a percentage, and starting from the top, and going clockwise, the ring will be coloured.

But, and this is the tricky part for me, the ring color has to change entirely based on that percentage. I mean, if is 10%, the colored part of the ring, should be green, and at the same time it growns (11,12,13...) that green should be "yellowing" and then, "redding" until 100% that should be pure red.

I have tried using a canvas, and drawing an arc, with given color, but Im unable to change the arc stroke with linear progress from green to red.

Behaviour of that ring should be like the tyre wear indicator from f1 game, like this:

enter image description here enter image description here enter image description here enter image description here enter image description here enter image description here

As you see, the ring (and also its bg) changes from green to red, through limegreen, yellow, orange...

Which is the best approach? Is there any "easy" way? some tool, or editor, or even photoshop? or its only achievable with js? If the answer is JS, how can I do it?

Thanks you in advance!

I have achieved some changes, but, its far from perfect, I cant get the given linearity, from 0 to (more or less) 20 should be mostly green (adding a bit of yellow) then change to yellow and orange until 80% that gets red to 100%. Exact percentages are not relevant, just something similar.

EDIT: this is my current code:

const c = document.getElementById("myCanvas");
const d = document.getElementById("input-box");
const f = document.getElementById("crear");
const ctx = c.getContext("2d");
let originalInput;
var offset = 1.5 * Math.PI;
var start = offset;


function iniciar() {
  for (i = 0; i < 368; i = i + 3.6 * 5) {
    crear(i);
    DownloadCanvasAsImage(Math.trunc(i / 3.6));
    setTimeout(() => {
      console.log("1 Segundo esperado")
    }, 3000);

  }
}

function getcolor(valor, trans = 55) {

  hue = Math.max(120 - (valor * 1.6), 0);
  color = 'hsl(' + hue + ',50%,' + trans + '%)';
  return color;
}

function sliderChange(value) {
  valor = Number(value) * 3.6;
  originalInput = Number(valor) * 0.5 / 90;
  ctx.beginPath();
  ctx.arc(30, 30, 26, 0, 2 * Math.PI);
  ctx.lineWidth = 4;
  ctx.strokeStyle = getcolor(value, 80);
  ctx.stroke();
  ctx.beginPath();
  ctx.arc(30, 30, 26, start, originalInput * Math.PI + offset);
  ctx.lineWidth = 7;
  ctx.strokeStyle = getcolor(value);
  ctx.stroke();

}

function iniciar() {
  for (i = 0; i < 101; i = i + 2) {
    crear(i);
    DownloadCanvasAsImage(i);
  }
}

function crear(i) {
  valor = Number(i) * 3.6;
  originalInput = Number(valor) * 0.5 / 90;
  ctx.beginPath();
  ctx.arc(30, 30, 26, 0, 2 * Math.PI);
  ctx.lineWidth = 4;
  ctx.strokeStyle = getcolor(i, 80);
  ctx.stroke();
  ctx.beginPath();
  ctx.arc(30, 30, 26, start, originalInput * Math.PI + offset);
  ctx.lineWidth = 7;
  ctx.strokeStyle = getcolor(i);
  ctx.stroke();
}

function DownloadCanvasAsImage(i) {
  let downloadLink = document.createElement('a');
  downloadLink.setAttribute('download', 'n_' + i + '.png');
  let canvas = document.getElementById('myCanvas');
  let dataURL = canvas.toDataURL('image/png');
  let url = dataURL.replace(/^data:image\/png/, 'data:application/octet-stream');
  downloadLink.setAttribute('href', url);
  downloadLink.click();
}
<h1>HTML5 Canvas</h1>
<h2>The arc() Method</h2>

<canvas id="myCanvas" width="60" height="60" style="border:1px solid grey"></canvas>
<input type="text" id="input-box" maxlength="3" value="0" class="input-box">
<input type="button" id="crear" value="crear" onclick="iniciar()" />
<input type="button" id="descargar" value="descargar" onclick="DownloadCanvasAsImage()" />
<input type="range" id="volume" name="volume" min="0" max="100" value="0" oninput="sliderChange(this.value)" />

Edit2: I have ended using this: https://www.joshwcomeau.com/gradient-generator/

It generates a list of HSL codes, that could be tweaked.

The code now is this:

const c = document.getElementById("myCanvas");
        const d = document.getElementById("input-box");
        const f = document.getElementById("crear");
        const ctx = c.getContext("2d");
        let originalInput;
        var offset = 1.5 * Math.PI;
        var start = offset;


        function iniciar() {
            for (i = 0; i < 101; i++) {
                sliderChange(i);
                DownloadCanvasAsImage(i);
                setTimeout(() => {
                    console.log("1 Segundo esperado")
                }, 3000);

            }
        }

        function getColor(percentage, trans = 0) {
            // Create a list of HSL colors from the given list.
            const colors = [
            [92, 100, 52, 0],
                  [85, 100, 49, 0],
                  [81, 100, 48, 0],
                  [76, 100, 47, 0],
                  [72, 100, 47, 0],
                  [68, 100, 46, 0],
                  [64, 100, 45, 0],
                  [60, 100, 44, 1],
                  [57, 100, 45, 1],
                  [53, 100, 47, 3],
                  [51, 100, 49, 5],
                  [48, 100, 50, 9],
                  [45, 100, 50, 45],
                  [42, 100, 50, 60],
                  [39, 100, 50, 68],
                  [36, 100, 50, 74],
                  [33, 100, 50, 79],
                  [30, 100, 50, 83],
                  [26, 100, 50, 87],
                  [22, 100, 50, 91],
                  [18, 100, 50, 94],
                  [12, 100, 50, 97],
                  [0, 100, 50, 100],
];

            // Calculate the index of the HSL color that corresponds to the given percentage.
            const index = Math.min(Math.floor(percentage * colors.length / 100),22);
            const color = colors[index];
            // Return the HSL code for the calculated index.
            if (trans != 0) {
                return "hsl(" + color[0] + "deg,10%,90%)";
            } else {
                return "hsl(" + color[0] + "deg," + color[1] + "%," + color[2] + "%)";
            }
        }

        function sliderChange(value) {
            ctx.clearRect(0, 0, c.width, c.height); // <<--- Add this

            valor = Number(value) * 3.6;
            originalInput = Number(valor) * 0.5 / 90;
            ctx.beginPath();
            ctx.arc(c.width/2, c.height/2, 45, 0, 2 * Math.PI);
            ctx.lineWidth = 2;
            fondo = getColor(value);
            ctx.strokeStyle = getColor(value, 90);
            ctx.stroke();
            ctx.beginPath();
            ctx.arc(c.width/2, c.height/2, 45, start, originalInput * Math.PI + offset);
            ctx.lineWidth = 4;
            ctx.font = "22px Formula1";
            ctx.fillStyle = "white";
            ctx.textAlign = "center";
            ctx.textBaseline = 'middle';
            ctx.fillText(value+"%", c.width/2, c.height/2);
            ctx.strokeStyle = getColor(value);
            ctx.stroke();

        }

        function crear(i) {
            ctx.clearRect(0, 0, c.width, c.height); // <<--- Add this
            valor = Number(i) * 3.6;
            originalInput = Number(valor) * 0.5 / 90;
            ctx.beginPath();
            ctx.arc(90, 90, 50, 0, 2 * Math.PI);
            ctx.lineWidth = 2;
            fondo = getColor(i);
            ctx.strokeStyle = getColor(i, 90);
            ctx.stroke();
            ctx.beginPath();
            ctx.arc(90, 90, 50, start, originalInput * Math.PI + offset);
            ctx.lineWidth = 4;
            ctx.font = "20px Formula1";
            ctx.fillStyle = "white";
            ctx.textAlign = "center";
            ctx.textBaseline = 'middle';
            ctx.fillText(Math.trunc(i / 3.6)+"%", c.width/2, c.height/2);
            ctx.strokeStyle = getColor(Math.trunc(i / 3.6));
            ctx.stroke();
        }

        function DownloadCanvasAsImage(i) {
            let downloadLink = document.createElement('a');
            downloadLink.setAttribute('download', 'n_' + i + '.png');
            let canvas = document.getElementById('myCanvas');
            let dataURL = canvas.toDataURL('image/png');
            let url = dataURL.replace(/^data:image\/png/, 'data:application/octet-stream');
            downloadLink.setAttribute('href', url);
            downloadLink.click();
        }
        #myCanvas {
            background-color: black;
            stroke-linecap: round;
        }
        @font-face {
    font-family: 'Formula1';
    src: url('/home/pablo/Descargas/Formula1-Bold-4.ttf');
}
        
    <canvas id="myCanvas" width="100" height="100" style="border:1px solid grey"></canvas>
    <input type="text" id="input-box" maxlength="3" value="0" class="input-box">
    <input type="button" id="crear" value="crear" onclick="iniciar()" />
    <input type="button" id="descargar" value="descargar" onclick="DownloadCanvasAsImage()" />
    <input type="range" id="volume" name="volume" min="0" max="100" value="0" oninput="sliderChange(this.value)" />


Solution

  • So far, the only thing missing is clearing out the old drawing, so it doesn't bleed through:

    ctx.clearRect(0, 0, c.width, c.height);
    

    Other ideas for getting the colors:

    A. creating an interpolating algorithm, example

    B. Or, drawing a gradient on a hidden element and sampling the color at a specific coordinate as follows:

    class Gradient {
      constructor(stops) {
        const canvas = document.createElement('canvas')
        canvas.width = 101
        canvas.height = 1
        const ctx = canvas.getContext('2d')
        const gradient = ctx.createLinearGradient(0, 0, 101, 0)
        for (const [stop, color] of stops)
          gradient.addColorStop(stop, color)
        ctx.fillStyle = gradient
        ctx.fillRect(0, 0, 101, 1)
        this.pixels = ctx.getImageData(0, 0, 101, 1).data
      }
    
      colorAt(percent) {
        const i = percent * 4 | 0
        const r = this.pixels[i + 0]
        const g = this.pixels[i + 1]
        const b = this.pixels[i + 2]
        const a = this.pixels[i + 3]
        return `rgba(${r},${g},${b},${a})`
      }
    }
    
    const gradient = new Gradient([
      [0.05, 'rgb(0,255,0)'],
      [0.25, 'rgb(255,255,0)'],
      [0.75, 'rgb(255,0,0)']
    ])
    
    
    const c = document.getElementById("myCanvas");
    const ctx = c.getContext("2d");
    let originalInput;
    var offset = 1.5 * Math.PI;
    var start = offset;
    
    sliderChange(0);
    function sliderChange(value) {
      ctx.clearRect(0, 0, c.width, c.height); // <<--- Add this
      valor = Number(value) * 3.6;
      originalInput = Number(valor) * 0.5 / 90;
    
      ctx.filter = 'none'; // No Glow
      ctx.globalAlpha = 0.5;
      ctx.beginPath();
      ctx.arc(30, 30, 26, 0, 2 * Math.PI);
      ctx.lineWidth = 4;
      ctx.strokeStyle = gradient.colorAt(value);
      ctx.stroke();
    
      ctx.filter = 'blur(3px)'; // Glow
      ctx.globalAlpha = 1;
      ctx.beginPath();
      ctx.arc(30, 30, 26, start, originalInput * Math.PI + offset);
      ctx.lineWidth = 7;
      ctx.stroke();
    }
    <canvas id="myCanvas" width="60" height="60" style="background: black"></canvas>
    
    
    <input type="range" min="0" max="100" value="0" oninput="sliderChange(this.value)" />