Часы чертежника на Canvas

Рассмотрим пример реализации часов чертежника с применением Canvas.

Canvas — элемент HTML5, на котором можно создавать растровые изображения с помощью чистого JavaScript.
Часы чертежника — оригинальная идея аналоговых часов, стрелки которых являются ребрами прямоугольного параллелепипеда.

Приводится демонстрация результата и аннотированный исходный код.

Демо

Исходный код

> > Gist «squareclock.html»

Подробности ниже.

Создание

В тело страницы необходимо добавить элемент <canvas> с указанием размера:

<canvas width="900" height="800" id='canv'></canvas>

В качестве основы JavaScript-приложения воспользуемся паттерном:

// Объект приложения
var app = {
	conf: {
    	// Конфиги
    },
    
    // Исходная точка
    start: function() {
    	this.someFunc();
    },
    
    // Некая функции, запускаемая при старте приложения
    someFunc: function() {
    	// Your awesome code here (:
    }
};

// Старт приложения при загрузке страницы
window.onload = function () {
	app.start();
};

Опишем сущности объекта.

Константы

conf: {
	idCanv: 'canv', // ID Canvas

    R: 240,         // Радиус циферблата
    hourL: 70,      // Длина часовой
    minL: 100,      // Длина минутной
    secL: 110,      // Длина секундной
    hourW: 4,       // Толщина часовой
    minW: 3,        // Толщина минутной
    secW: 2,        // Толщина секундной

    armClr: '#000',     // Цвет стрелок
    vectorClr: '#999',  // Цвет векторов
    clockClr: '#5E7490',// Цвет циферблата
    numberClr: '#777',  // Цвет чисел

    intervalMs: 30  // Интервал отрисовки в миллисекундах
},
centerX: 0,         // Центр по X
centerY: 0,         // Центр по Y
canv: {},           // DOM элемент канвы
ctx: {},            // Контекст канвы
tau: 2 * Math.PI,   // Константа для упрощения расчетов

Исходная точка приложения

start: function () {
    this.setCanvas();
	this.startClock()
},

Необходимо создать два метода:
setCanvas — для подготовки канвы и startClock — для запуска таймера отрисовки часов.

Подготовка канвы

setCanvas: function () {
    this.canv = document.getElementById("canv");
    this.ctx = this.canv.getContext('2d');
    this.centerX = this.canv.width / 2;
    this.centerY = this.canv.height / 2;
},

Основной цикл отрисовки

Составим алгоритм:

  1. Очистка;
  2. Получение текущего времени;
  3. Отрисовка циферблата и цифр;
  4. Отрисовка механизма;
    4.1. Отрисовка стрелок;

    4.2. Отрисовка соединяющих векторов — ребер (шутка в тему).

Код цикла:

startClock: function () {
    var self = this;

    setInterval(function () {
        // Очистка
        self.ctx.clearRect(0, 0, self.canv.width, self.canv.height);
		
        // Получение времени
        var date = new Date(),
                h = date.getHours(),
                m = date.getMinutes(),
                s = date.getSeconds(),
                ms = date.getMilliseconds();

        // Корректировка движения стрелок
        s += ms / 1000;
        m += s / 60;
        h += m / 60;

        self.drawClockface();
        self.drawArms(h,m,s);

    }, this.intervalMs);
},

Отрисовка циферблата, метод drawClockface:

drawClockface: function () {
    // Окружность
    this.ctx.beginPath();
    this.ctx.arc(this.centerX, this.centerY, this.conf.R, 0, Math.PI * 2, true);
    this.ctx.lineWidth = 4;
    this.ctx.strokeStyle = this.conf.clockClr;
    this.ctx.stroke();
    this.ctx.closePath();

    // Цифры
    this.ctx.beginPath();
    this.ctx.fillStyle = this.conf.numberClr;
    this.ctx.font = "30pt Arial";
    this.ctx.textAlign = "center";
    this.ctx.textBaseline = "middle";
    this.ctx.fillText("12", this.centerX, this.centerY - this.conf.R * 0.9);
    this.ctx.fillText("3", this.centerX + this.conf.R * 0.9, this.centerY);
    this.ctx.fillText("6", this.centerX, this.centerY + this.conf.R * 0.9);
    this.ctx.fillText("9", this.centerX - this.conf.R * 0.9, this.centerY);
    this.ctx.closePath();
},

Далее представлен наиболее ёмкий метод по отрисовке стрелок и ребер часов: drawArms.

Метод принимает 3 численных параметра: часы, минуты, секунды.
Для понимания нижепредставленного кода необходимо помнить основы геометрии на плоскости.

Код метода:

drawArms: function (h, m, s) {
    /* Отрисовка стрелок */

    // Часы
    var hArmRadians = (this.tau * h / 12) - (this.tau / 4),
            hTargetX = this.centerX + Math.cos(hArmRadians) * this.conf.hourL,
            hTargetY = this.centerY + Math.sin(hArmRadians) * this.conf.hourL;
    drawArm.apply(this,[hTargetX, hTargetY, this.conf.hourW]);

    // Минуты
    var mArmRadians = (this.tau * m / 60) - (this.tau / 4),
            mTargetX = this.centerX + Math.cos(mArmRadians) * this.conf.minL,
            mTargetY = this.centerY + Math.sin(mArmRadians) * this.conf.minL;
    drawArm.apply(this,[mTargetX, mTargetY, this.conf.minW]);

    // Секунды
    var sArmRadians = (this.tau * s / 60) - (this.tau / 4),
            sTargetX = this.centerX + Math.cos(sArmRadians) * this.conf.secL,
            sTargetY = this.centerY + Math.sin(sArmRadians) * this.conf.secL;
    drawArm.apply(this,[sTargetX, sTargetY, this.conf.secW]);

    /* Отрисовка векторов */

    // Вектор между часами и минутами
    var hmVectorX = (hTargetX + mTargetX) - this.centerX,
            hmVectorY = (hTargetY + mTargetY) - this.centerY;
    drawVector.apply(this,[hTargetX, hTargetY, hmVectorX, hmVectorY, mTargetX, mTargetY]);

    // Вектор между минутыми и секундами
    var msVectorX = (mTargetX + sTargetX) - this.centerX,
            msVectorY = (mTargetY + sTargetY) - this.centerY;
    drawVector.apply(this,[mTargetX, mTargetY, msVectorX, msVectorY, sTargetX, sTargetY]);

    // Вектор между часами и секундами
    var hsVectorX = (hTargetX + sTargetX) - this.centerX,
            hsVectorY = (hTargetY + sTargetY) - this.centerY;
    drawVector.apply(this,[hTargetX, hTargetY, hsVectorX, hsVectorY, sTargetX, sTargetY]);

    // Результирующий вектор
    this.ctx.beginPath();
    var hmsVectorX = (hTargetX + mTargetX + sTargetX) - this.centerX * 2,
            hmsVectorY = (hTargetY + mTargetY + sTargetY) - this.centerY * 2;
    this.ctx.moveTo(hsVectorX, hsVectorY);
    this.ctx.lineTo(hmsVectorX, hmsVectorY);
    this.ctx.moveTo(hmVectorX, hmVectorY);
    this.ctx.lineTo(hmsVectorX, hmsVectorY);
    this.ctx.moveTo(msVectorX, msVectorY);
    this.ctx.lineTo(hmsVectorX, hmsVectorY);
    this.ctx.lineWidth = 1;
    this.ctx.strokeStyle = this.conf.vectorClr;
    this.ctx.stroke();
    this.ctx.closePath();

    /**
     * Отрисовка самой стрелки-палки.
     * Использовать в контексте.
     * @param x Координата X
     * @param y Координата Y
     * @param w Толщина линии
     */
    function drawArm(x, y, w) {
        this.ctx.beginPath();
        this.ctx.moveTo(this.centerX, this.centerY);
        this.ctx.lineTo(x, y);
        this.ctx.lineWidth = w;
        this.ctx.strokeStyle = this.conf.armClr;
        this.ctx.stroke();
        this.ctx.closePath();
    }

    /**
     * Отрисовка векторов.
     * Рисует ломаную, состоящую из 2-х векторов.
     * Использовать в контексте.
     * @param startX X-координата начала
     * @param startY Y-координата начала
     * @param line1X X-координата конца 1-го вектора, начало 2-го
     * @param line1Y Y-координата конца 1-го вектора, начало 2-го
     * @param line2X X-координата конца 2-го вектора
     * @param line2Y Y-координата конца 2-го вектора
     */
    function drawVector(startX, startY, line1X, line1Y, line2X, line2Y) {
        this.ctx.beginPath();
        this.ctx.moveTo(startX, startY);
        this.ctx.lineTo(line1X, line1Y);
        this.ctx.lineTo(line2X, line2Y);
        this.ctx.lineWidth = 1;
        this.ctx.strokeStyle = this.conf.vectorClr;
        this.ctx.stroke();
        this.ctx.closePath();
    }
}

Готово. Результат представлен в начале данной статьи.

Рекомендую также к ознакомлению интересный плагин для jQuery, позволяющий обрабатывать событие тройного клика мыши.