В сети есть несколько похожих примеров создания спидометра, но я решил поделиться с вами своим.

image

Для начала нам нужно в DOM'е создать объект canvas:

<canvas id="canvas" width="500" height="500"></canvas>

Прописывать стили инлайн — это грех, но если ширину и высоту канвасу задать с помощью css, мы столкнёмся с массой проблем, а если быть точнее — это просто не будет работать. Теперь переходим непосредственно к скрипту. Сначала нам нужно получить 2d контекст, с которым мы дальше и будем работать:

var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");

Далее следует ряд настроек, которые нам понадобятся в вычислениях. Все значения размеров я высчитываю относительно ширины канваса. Таким образом, при изменении размеров канваса, пропорции спидометра не изменятся

// general settings
var middleX = canvas.width / 2;
var middleY = canvas.height / 2;
var radius = canvas.width / 2 - canvas.width / 10;
// beginning and ending of our arc. Sets by rad * pi
var startAngleIndex = 0.7;
var endAngleIndex = 2.3;

// zones settings
var zoneLineWidth = canvas.width / 30;
var counterClockwise = false;

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

// Center circle settings
var centerCircleRadius = canvas.width / 20;
var centerCircleColor = "#efe5cf";
var centerCircleBorderWidth = canvas.width / 100;

// Arrow settings
var arrowValueIndex = 1.29;
var arrowColor = "#464646";
var arrowWidth = canvas.width / 50;

// Digits settings
var digits = [0, 20, 40, 60, 80, 100, 120, 140, 160, 180, 200, 220, 240];
var digitsColor = "#746845";
var digitsFont = "bold 20px Tahoma";
var digitsOffsetFromArc = canvas.width / 12;

var zonesCount = digits.length - 1;
var step = (endAngleIndex - startAngleIndex) / zonesCount;

Отдельно хотел бы остановиться на переменных startAngleIndex и endAngleIndex . Чтобы лучше понять, откуда взялись такие значения, я покажу картинку, взятую с сайта www.html5canvastutorials.com

image

Далее реализуем несколько методов, которые будут отрисовывать на канвасе элементы спидометра.

var DrawZones = function() {
        var greenZonesCount = Math.ceil(zonesCount / 2);
        var yellowZonesCount = Math.ceil((zonesCount - greenZonesCount) / 2);
        var redZonesCount = zonesCount - greenZonesCount - yellowZonesCount;

        var startAngle = (startAngleIndex - 0.02) * Math.PI;
        var endGreenAngle = (startAngleIndex + greenZonesCount * step) * Math.PI;
        var endYellowAngle = (startAngleIndex + (greenZonesCount + yellowZonesCount) * step) * Math.PI;
        var endRedAngle = (endAngleIndex + 0.02) * Math.PI;

        var sectionOptions = [
            {
                startAngle: startAngle,
                endAngle: endGreenAngle,
                color: "#090"
            },
            {
                startAngle: endGreenAngle,
                endAngle: endYellowAngle,
                color: "#cc0"
            },
            {
                startAngle: endYellowAngle,
                endAngle: endRedAngle,
                color: "#900"
            }
        ];

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

        sectionOptions.forEach(function(options) {
            DrawZone(options);
        });
    };

В этом методе я разбираю массив digits и определяю сколько должно быть зелёных, жёлтых и красных зон.

image
Метод DrawTicks рисует засечки на дуге с шагом, высчитанным ранее

var DrawTicks = function() {

        this.DrawTick = function(angle) {
            var fromX = middleX + (radius - tickOffsetFromArc) * Math.cos(angle);
            var fromY = middleY + (radius - tickOffsetFromArc) * Math.sin(angle);
            var toX = middleX + (radius + tickOffsetFromArc) * Math.cos(angle);
            var toY = middleY + (radius + tickOffsetFromArc) * Math.sin(angle);

            ctx.beginPath();
            ctx.moveTo(fromX, fromY);
            ctx.lineTo(toX, toY);
            ctx.lineWidth = tickWidth;
            ctx.lineCap = "round";
            ctx.strokeStyle = tickColor;
            ctx.stroke();
        };

        for (var i = startAngleIndex; i <= endAngleIndex; i += step) {
            var angle = i * Math.PI;
            this.DrawTick(angle);
        }
    };

image
Следующий метод отрисует нам цифры на будущем спидометре

var DrawDigits = function() {
        var angleIndex = startAngleIndex;

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

            ctx.font = digitsFont;
            ctx.fillStyle = digitsColor;
            ctx.textAlign = "center";
            ctx.textBaseline = "middle";
            ctx.fillText(digit, x, y);
        });
    };

image
Последние два метода предназначены для отрисовки центрального круга и, собственно, стрелки. При желании метод DrawArrow можно научить принимать нормальные значения и реализовать логику, которая будет определять, куда стрелка будет показывать

var DrawArrow = function() {
        var arrowAngle = arrowValueIndex * Math.PI;
        var toX = middleX + (radius) * Math.cos(arrowAngle);
        var toY = middleY + (radius) * Math.sin(arrowAngle);

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

    var DrawCenterCircle = function() {
        ctx.beginPath();
        ctx.arc(middleX, middleY, centerCircleRadius, 0, 2 * Math.PI, false);
        ctx.fillStyle = centerCircleColor;
        ctx.fill();
        ctx.lineWidth = centerCircleBorderWidth;
        ctx.strokeStyle = arrowColor;
        ctx.stroke();
    };

Осталось вызвать наши методы в нужном порядке

DrawTicks();
DrawZones();
DrawDigits();
DrawArrow();
DrawCenterCircle();

Конечно же, порядок вызова имеет значение, так же, как и, например, порядок слоёв в фотошопе (только в обратном порядке)
Результат:
image

Источники:

Поделиться с друзьями
-->

Комментарии (10)


  1. spmbt
    29.07.2016 00:24
    +3

    Оптимизации нет. Если спидометром предполагается пользоваться для многократного изменения значения, 4 функции из 5, очевидно, вызываются избыточно, их стоит применить один первый раз. Далее, чтобы это выглядело не как Boilerplate code, надо обернуть в объект с методами init() и setValue(). А если учесть, что эксперименты (со снежинками на Canvas) показывают, что нарисовать один слой Canvas в браузерах затратнее, чем нарисовать или повернуть один DOM-элемент стрелки спидометра, то решение, очевидно, будет просто во вращении (css transform rotate, а лучше matrix) стрелки по setValue().


    1. lekzd
      29.07.2016 15:09
      +1

      Я бы еще вынес «фон» спидометра на отдельный canvas, который бы расположил под другим, в котором оставил бы только рисование стрелки


    1. Alex_ME
      29.07.2016 16:45

      Разве не наоборот? DOM затратнее, чем Canvas?


      1. lekzd
        29.07.2016 17:30

        CSS трансформации (не путать с изменнением layout) для DOM элементов работают несколько быстрее, потому для плавного перетаскивания и зума яндекс-карт используют CSS transform, а затем перерисовывают canvas


  1. Sirion
    29.07.2016 10:21
    +4

    Рисование графическими примитивами, ня. Прямо школа вспомнилась. Восьмой класс, паскаль, «uses graph», машинки из кружочков и прямоугольничков…


  1. mediakotm
    29.07.2016 10:35

    А анимация?


    1. slavontkn
      31.07.2016 15:44
      -1

      Анимация — это тема для отдельной статьи, имхо


  1. homm
    29.07.2016 13:20
    +10

    Для начала нам нужно в DOM'е создать объект canvas:
    <canvas id="canvas" width="500px" height="500px"></canvas>


    Да нет никаких px в значениях атрибутов в HTML. И для <canvas> нет и для <Img> и для других.


    1. slavontkn
      31.07.2016 15:43

      Спасибо, исправил


  1. Flakky
    07.08.2016 19:19
    -1

    А скорость чего он, собственно, измеряет?)

    Могу предложить несколькими простыми строками переписать его на измерение скорости анимации (через requestAnimationFrame() ) этого добра. Ваш спидометр тогда будет показывать FPS работы самого спидометра и будет пространство для оптимизации с мгновенным визуальным выводом результата :)