Drawing the speedometer arrow in canvas

I want to make an animation of the speedometer using canvas. But I need the speedometer needle to be triangular in shape and when changing the value, it points to the desired value, but its base always remains in the center. Tell me what formula or algorithm you need to apply for this. The code is shown below.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<canvas id="canvas" width="500" height="500"></canvas>
<script>
    const canvas = document.getElementById("canvas"),
        ctx = canvas.getContext("2d"),

        // general settings
        middleX = canvas.width / 2,
        middleY = canvas.height / 2,
        radius = 240,

        counterClockwise = false,

        // ticks settings
        tickWidth = canvas.width / 100,
        // tickColor = "#746845";
        tickOffsetFromArc = canvas.width / 40,

        // Center circle settings
        centerCircleRadius = canvas.width / 20,
        centerCircleColor = "#ccc",
        centerCircleBorderWidth = canvas.width / 100,

        // Arrow settings
        arrowValueIndex = .73,
        arrowColor = "#464646",
        arrowWidth = canvas.width / 50,

        // numbers
        digits = [0, 20, 40, 50, 60, 70, 80, 90, 100],
        digitsColor = "#0a0a0a",
        digitsFont = "bold 20px Tahoma",
        digitsOffsetFromArc = canvas.width / 15,

        //zones
        zonesCount = digits.length - 1;
    // beginning and ending of our arc. Sets by radius*pi
    let startAngleIndex = .75,
        endAngleIndex = 2.25,
        step = (endAngleIndex - startAngleIndex) / zonesCount;

    /*draw zones*/
    let DrawZones = function () {
        const greyZonesCount = zonesCount / 1.6;
              greenZonesCount = zonesCount - greyZonesCount,
              startAngle = (startAngleIndex - 0.02) * Math.PI,
              endGreyAngle = (startAngleIndex + greyZonesCount * step) * Math.PI,
              endGreenAngle = (endAngleIndex + 0.02) * Math.PI,

              //zones' options
              sectionOptions = [
                  {
                      startAngle: startAngle,
                      endAngle: endGreyAngle,
                      color: "#e7e7e7",
                      zoneLineWidth: 2
                  },
                  {
                      startAngle: endGreyAngle,
                      endAngle: endGreenAngle,
                      color: "#13b74b",
                      zoneLineWidth: 5
                  },
              ];

        this.DrawZone = function (options) {
            ctx.beginPath();
            ctx.arc(middleX, middleY, radius, options.startAngle, options.endAngle, counterClockwise);
            ctx.lineWidth = options.zoneLineWidth;
            ctx.strokeStyle = options.color;
            ctx.lineCap = "round";
            ctx.stroke();
        };

        sectionOptions.forEach(options => this.DrawZone(options));
    };

    /*draw dots*/
    let DrawTicks = function () {
        startAngleIndex = .73,
        endAngleIndex = 2.27,
        step = (endAngleIndex - startAngleIndex) / zonesCount;
        this.DrawTick = function (angle,count) {

            let fromX = middleX + (radius - tickOffsetFromArc) * Math.cos(angle),
                fromY = middleY + (radius - tickOffsetFromArc) * Math.sin(angle),
                toX = middleX + (radius + tickOffsetFromArc) * Math.cos(angle),
                toY = middleY + (radius + tickOffsetFromArc) * Math.sin(angle),

                centerOfDotX=(fromX+toX)/2,
                centerOfDotY=(fromY+toY)/2;
            ctx.beginPath();
            ctx.arc(centerOfDotX,centerOfDotY,6,0,Math.PI*2,true);
            if (count<6){
                switch (count) {
                    case 1:
                    case 2:
                    case 3:
                        ctx.fillStyle="#FF0000";
                        break;
                    default:
                        ctx.fillStyle="#F9AF00";
                        break;
                }
            }else{
                ctx.fillStyle="#FFF";
                ctx.strokeStyle="#13B74B";
                ctx.shadowColor = "#a8bbaa";
                ctx.shadowBlur = 15;
                ctx.shadowOffsetX = 0;
                ctx.shadowOffsetY = 0;
                ctx.stroke();
            }
            ctx.fill();
            ctx.closePath();
            ctx.shadowBlur =0;
        };
        let count=0;
        for (let i = startAngleIndex; i <= endAngleIndex; i += step) {
            let angle = i * Math.PI;
            count++;
            this.DrawTick(angle,count);
        }
    };

    //draw numbers
    let DrawDigits = function () {
        let angleIndex = startAngleIndex;

        digits.forEach(function (digit) {
            let angle = angleIndex * Math.PI,
                    x = middleX + (radius - digitsOffsetFromArc) * Math.cos(angle),
                    y = middleY + (radius - digitsOffsetFromArc) * Math.sin(angle);

            angleIndex += step;

            ctx.font = digitsFont;
            ctx.fillStyle = digitsColor;
            ctx.textAlign = "center";
            ctx.textBaseline = "middle";
            ctx.fillText(digit, x, y);
        });
    };
    /*draw arrow РИСОВАНИЕ СТРЕЛКИ*/
    let DrawArrow = function () {
        let arrowAngle = arrowValueIndex * Math.PI;
        let toX = middleX + (radius) * Math.cos(arrowAngle)+50;
        let toY = middleY + (radius) * Math.sin(arrowAngle)-50;

        ctx.beginPath();
        ctx.moveTo(middleX, middleY);
        ctx.lineTo(toX, toY);
        ctx.strokeStyle = arrowColor;
        ctx.lineWidth = arrowWidth;
        ctx.stroke();
        ctx.closePath();
    };


window.onload=()=>{
    DrawZones();
    DrawTicks();
    DrawDigits();
    DrawArrow();
};

</script>
</body>
</html>

Author: corocoto, 2019-04-10

3 answers

Here is another option, I did not destroy the old one, let there be a second answer:

let canvas = document.getElementById("canvas"),
    ctx = canvas.getContext("2d"),

    // general settings
    middleX = canvas.width / 2,
    middleY = canvas.height / 2,
    radius = 240,

    counterClockwise = false,

    // ticks settings
    tickWidth = canvas.width / 100,
    // tickColor = "#746845";
    tickOffsetFromArc = canvas.width / 40,

    // Center circle settings
    centerCircleRadius = canvas.width / 20,
    centerCircleColor = "#ccc",
    centerCircleBorderWidth = canvas.width / 100,

    // Arrow settings
    arrowValueIndex = 0,
    arrowColor = "#464646",
    arrowWidth = canvas.width / 150,

    // numbers
    digits = [0, 20, 40, 50, 60, 70, 80, 90, 100],
    digitsColor = "#0a0a0a",
    digitsFont = "bold 20px Tahoma",
    digitsOffsetFromArc = canvas.width / 15,

    //zones
    zonesCount = digits.length - 1;
// beginning and ending of our arc. Sets by radius*pi
let startAngleIndex = .75,
    endAngleIndex = 2.25,
    step = (endAngleIndex - startAngleIndex) / zonesCount;

/*draw zones*/
let DrawZones = function () {
  const greyZonesCount = zonesCount / 1.6;
    greenZonesCount = zonesCount - greyZonesCount,
    startAngle = (startAngleIndex - 0.02) * Math.PI,
    endGreyAngle = (startAngleIndex + greyZonesCount * step) * Math.PI,
    endGreenAngle = (endAngleIndex + 0.02) * Math.PI,

    //zones' options
    sectionOptions = [{
      startAngle: startAngle,
      endAngle: endGreyAngle,
      color: "#e7e7e7",
      zoneLineWidth: 2
    },{
      startAngle: endGreyAngle,
      endAngle: endGreenAngle,
      color: "#13b74b",
      zoneLineWidth: 5
    }];

  this.DrawZone = function (options) {
    ctx.beginPath();
    ctx.arc(middleX, middleY, radius, options.startAngle, options.endAngle, counterClockwise);
    ctx.lineWidth = options.zoneLineWidth;
    ctx.strokeStyle = options.color;
    ctx.lineCap = "round";
    ctx.stroke();
  };

  sectionOptions.forEach(options => this.DrawZone(options));
};

/*draw dots*/
let DrawTicks = function () {
  startAngleIndex = .73,
  endAngleIndex = 2.27,
  step = (endAngleIndex - startAngleIndex) / zonesCount;
  this.DrawTick = function (angle,count) {
    let fromX = middleX + (radius - tickOffsetFromArc) * Math.cos(angle),
        fromY = middleY + (radius - tickOffsetFromArc) * Math.sin(angle),
        toX = middleX + (radius + tickOffsetFromArc) * Math.cos(angle),
        toY = middleY + (radius + tickOffsetFromArc) * Math.sin(angle),
        centerOfDotX=(fromX+toX)/2,
        centerOfDotY=(fromY+toY)/2;
    ctx.beginPath();
    ctx.arc(centerOfDotX,centerOfDotY,6,0,Math.PI*2,true);
    if (count<6){
      switch (count) {
        case 1:
        case 2:
        case 3:
          ctx.fillStyle="#FF0000";
          break;
        default:
          ctx.fillStyle="#F9AF00";
          break;
      }
    } else {
      ctx.fillStyle="#FFF";
      ctx.strokeStyle="#13B74B";
      ctx.shadowColor = "#a8bbaa";
      ctx.shadowBlur = 15;
      ctx.shadowOffsetX = 0;
      ctx.shadowOffsetY = 0;
      ctx.stroke();
    }
    ctx.fill();
    ctx.closePath();
    ctx.shadowBlur =0;
  };
  let count=0;
  for (let i = startAngleIndex; i <= endAngleIndex; i += step) {
    let angle = i * Math.PI;
    count++;
    this.DrawTick(angle,count);
  }
};

//draw numbers
let DrawDigits = function () {
  let angleIndex = startAngleIndex;
  digits.forEach(function (digit) {
    let angle = angleIndex * Math.PI,
            x = middleX + (radius - digitsOffsetFromArc) * Math.cos(angle),
            y = middleY + (radius - digitsOffsetFromArc) * Math.sin(angle);
    angleIndex += step;
    ctx.font = digitsFont;
    ctx.fillStyle = digitsColor;
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    ctx.fillText(digit, x, y);
  });
};

/*draw arrow РИСОВАНИЕ СТРЕЛКИ*/
let DrawArrow = function () {
    ctx.beginPath();
    ctx.moveTo(middleX-17, middleY-47);
    ctx.lineTo(middleX, middleY-180);
    ctx.lineTo(middleX+17, middleY-47);
    ctx.strokeStyle = arrowColor;
    ctx.lineWidth = arrowWidth;
    ctx.stroke();
    ctx.closePath();
    ctx.beginPath();
    ctx.arc(middleX, middleY, 50, Math.PI/8- Math.PI/2, 2 * Math.PI-Math.PI/8- Math.PI/2);
    ctx.stroke();
};

function draw() {

   ctx.clearRect(0,0,canvas.width,canvas.height);
   
   DrawZones();
   DrawTicks();
   DrawDigits();
   
   ctx.translate(middleX,middleY);
   ctx.rotate(arrowValueIndex);
   ctx.translate(-middleX,-middleY);
   
   DrawArrow();
 
   ctx.translate(middleX,middleY);
   ctx.rotate(-arrowValueIndex);
   ctx.translate(-middleX,-middleY);
}

window.onload = draw

function val(value) {
  let sector = Math.PI*0.385
  if (value < 40)
    arrowValueIndex = value/40*sector - sector*2;
  else
    arrowValueIndex = (value-40)/60*sector*3 - sector;
  document.querySelector('span').textContent = value;
  draw();
}
<input type="range" value="60" onmousemove="val(this.value)"><span></span><br>
<canvas id="canvas" width="500" height="500"></canvas>
 3
Author: Stranger in the Q, 2019-04-10 20:06:05

It is sad that the distribution of the scale is uneven, if it were uniform, you could do something like this

function draw() {

   ctx.clearRect(0,0,canvas.width,canvas.height) //очистка канвы

   ctx.translate(middleX,middleY);   // сдвиг в центр
   ctx.rotate(arrowValueIndex);      // поворот табло
   ctx.translate(-middleX,-middleY); // сдвиг обратно

   DrawZones();
   DrawTicks();                      // рисует табло
   DrawDigits();

   ctx.translate(middleX,middleY);   // сдвиг в центр
   ctx.rotate(-arrowValueIndex);     // обратный поворот табло
   ctx.translate(-middleX,-middleY); // сдвиг обратно

   DrawArrow();                      // стрелка

}

let canvas = document.getElementById("canvas"),
        ctx = canvas.getContext("2d"),

        // general settings
        middleX = canvas.width / 2,
        middleY = canvas.height / 2,
        radius = 240,

        counterClockwise = false,

        // ticks settings
        tickWidth = canvas.width / 100,
        // tickColor = "#746845";
        tickOffsetFromArc = canvas.width / 40,

        // Center circle settings
        centerCircleRadius = canvas.width / 20,
        centerCircleColor = "#ccc",
        centerCircleBorderWidth = canvas.width / 100,

        // Arrow settings
        arrowValueIndex = 0,
        arrowColor = "#464646",
        arrowWidth = canvas.width / 150,

        // numbers
        digits = [0, 20, 40, 50, 60, 70, 80, 90, 100],
        digitsColor = "#0a0a0a",
        digitsFont = "bold 20px Tahoma",
        digitsOffsetFromArc = canvas.width / 15,

        //zones
        zonesCount = digits.length - 1;
    // beginning and ending of our arc. Sets by radius*pi
    let startAngleIndex = .75,
        endAngleIndex = 2.25,
        step = (endAngleIndex - startAngleIndex) / zonesCount;

    /*draw zones*/
    let DrawZones = function () {
        const greyZonesCount = zonesCount / 1.6;
              greenZonesCount = zonesCount - greyZonesCount,
              startAngle = (startAngleIndex - 0.02) * Math.PI,
              endGreyAngle = (startAngleIndex + greyZonesCount * step) * Math.PI,
              endGreenAngle = (endAngleIndex + 0.02) * Math.PI,

              //zones' options
              sectionOptions = [
                  {
                      startAngle: startAngle,
                      endAngle: endGreyAngle,
                      color: "#e7e7e7",
                      zoneLineWidth: 2
                  },
                  {
                      startAngle: endGreyAngle,
                      endAngle: endGreenAngle,
                      color: "#13b74b",
                      zoneLineWidth: 5
                  },
              ];

        this.DrawZone = function (options) {
            ctx.beginPath();
            ctx.arc(middleX, middleY, radius, options.startAngle, options.endAngle, counterClockwise);
            ctx.lineWidth = options.zoneLineWidth;
            ctx.strokeStyle = options.color;
            ctx.lineCap = "round";
            ctx.stroke();
        };

        sectionOptions.forEach(options => this.DrawZone(options));
    };

    /*draw dots*/
    let DrawTicks = function () {
        startAngleIndex = .73,
        endAngleIndex = 2.27,
        step = (endAngleIndex - startAngleIndex) / zonesCount;
        this.DrawTick = function (angle,count) {

            let fromX = middleX + (radius - tickOffsetFromArc) * Math.cos(angle),
                fromY = middleY + (radius - tickOffsetFromArc) * Math.sin(angle),
                toX = middleX + (radius + tickOffsetFromArc) * Math.cos(angle),
                toY = middleY + (radius + tickOffsetFromArc) * Math.sin(angle),

                centerOfDotX=(fromX+toX)/2,
                centerOfDotY=(fromY+toY)/2;
            ctx.beginPath();
            ctx.arc(centerOfDotX,centerOfDotY,6,0,Math.PI*2,true);
            if (count<6){
                switch (count) {
                    case 1:
                    case 2:
                    case 3:
                        ctx.fillStyle="#FF0000";
                        break;
                    default:
                        ctx.fillStyle="#F9AF00";
                        break;
                }
            }else{
                ctx.fillStyle="#FFF";
                ctx.strokeStyle="#13B74B";
                ctx.shadowColor = "#a8bbaa";
                ctx.shadowBlur = 15;
                ctx.shadowOffsetX = 0;
                ctx.shadowOffsetY = 0;
                ctx.stroke();
            }
            ctx.fill();
            ctx.closePath();
            ctx.shadowBlur =0;
        };
        let count=0;
        for (let i = startAngleIndex; i <= endAngleIndex; i += step) {
            let angle = i * Math.PI;
            count++;
            this.DrawTick(angle,count);
        }
    };

    //draw numbers
    let DrawDigits = function () {
        let angleIndex = startAngleIndex;

        digits.forEach(function (digit) {
            let angle = angleIndex * Math.PI,
                    x = middleX + (radius - digitsOffsetFromArc) * Math.cos(angle),
                    y = middleY + (radius - digitsOffsetFromArc) * Math.sin(angle);

            angleIndex += step;

            ctx.font = digitsFont;
            ctx.fillStyle = digitsColor;
            ctx.textAlign = "center";
            ctx.textBaseline = "middle";
            ctx.fillText(digit, x, y);
        });
    };
    /*draw arrow РИСОВАНИЕ СТРЕЛКИ*/
    let DrawArrow = function () {
        ctx.beginPath();
        ctx.moveTo(middleX-17, middleY-47);
        ctx.lineTo(middleX, middleY-180);
        ctx.lineTo(middleX+17, middleY-47);
        ctx.strokeStyle = arrowColor;
        ctx.lineWidth = arrowWidth;
        ctx.stroke();
        ctx.closePath();
        ctx.beginPath();
        ctx.arc(middleX, middleY, 50, Math.PI/8- Math.PI/2, 2 * Math.PI-Math.PI/8- Math.PI/2);
        ctx.stroke();
    };

function draw() {

   ctx.clearRect(0,0,canvas.width,canvas.height)
   
   ctx.translate(middleX,middleY);
   ctx.rotate(arrowValueIndex);
   ctx.translate(-middleX,-middleY);
   
   DrawZones();
   DrawTicks();
   DrawDigits();
   
   ctx.translate(middleX,middleY);
   ctx.rotate(-arrowValueIndex);
   ctx.translate(-middleX,-middleY);
   
   DrawArrow();
   
}

window.onload = draw;

function val(value) {
  let sector = Math.PI*0.385
  if (value < 40)
    arrowValueIndex = value/40*sector - sector*2;
  else
    arrowValueIndex = (value-40)/60*sector*3 - sector;
  document.querySelector('span').textContent = value;
  draw()
}
<input type="range" value="60" onmousemove="val(this.value)"><span></span><br>
<canvas id="canvas" width="500" height="500"></canvas>
 1
Author: Stranger in the Q, 2019-04-10 20:08:29

Didn't work with canvas, but look at the implementation data, maybe it will help: http://www.knowstack.com/html5-canvas-speedometer/

Https://github.com/vjt/canvas-speedometer

 0
Author: Андрей, 2019-04-10 16:33:37