Как я делал виджет управления мощностью для своего браузерного симулятора космических полетов

в 22:30, , рубрики: canvas, javascript

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

Я довольно долго рылся в интернетах для того чтобы найти хоть что-то похожее на то, что мне нужно, но интернет до сих пор с такой задачей не сталкивался видимо или никто не решился создавать подобный виджет. Наверное никому не надо было.

Для начала озвучу чего мне так хотелось:
Мне нужен слайдер — аналог регулятора громкости, совмещенный с прогресс-баром. Эдакий компонент управления мощностью чего-либо, совмещенный с одновременной индикацией этой мощности. Иногда мощность может превышать установленный предел в 100% — необходимо отображать этот уровень и правильно высчитывать процент. Иногда мощность может заходить ниже нуля ( не знаю может ли — но я на всякий случай предусмотрел такую возможность) и этот уровень тоже надо отображать. Более того, то устройство, которое мы регулируем может быть инертным и разгоняться не с той скоростью, с которой мы выставляем значение. Если вы нажали кнопку форсажа на самолете — то двигатели выйдут на форсажный режим через некоторое время. То есть надо отдельно задавать значение прогрессбара и также отдельно получать-устанавливать текущее значение ползунка слайдера.

Может быть я и фиговый искатель, но в итоге психанул — решил сделать свой:
Здесь ссылка на результат, а под катом описание процесса

Начинаем

Для начала создадим каркас виджета:

var PowerControlWidget = function(settings){
     this.container = settings.container || undefined ;
     this.canvas = document.createElement('CANVAS');
     this.canvas.height = this.height;
     this.canvas.width = this.width;
	
	this.container.appendChild(this.canvas);
	
	this.ctx = this.canvas.getContext('2d');	

        // --- Набор полезных функций

	this.set_value(0);
	this.redraw()
}

Сразу оговорюсь — мне бы хотелось по максимуму быть независимым от jquery.js и jqueryui.js — поэтому я не стал оформлять этот виджет как плагин jQuery.

Обработка событий

Для драг-н-дропов все банально: на mousedown сохраняем состояние, на mouseup — сбрасываем.

var self = this;
this.canvas.addEventListener("mousedown", function(event){
		self.mouse_down = true;
		
		self.value = event.offsetX;
		
		if(event.offsetX < self.padding_left_right){
			self.value = self.padding_left_right;
		}
		if(event.offsetX > self._line_width - self.padding_left_right){
			self.value = self._line_width - self.padding_left_right;
		}
		
		self._percent_value = self._get_percent(self.value);
		self.redraw();
		self.onchange(self._percent_value, self.progress_value);
	})
	this.canvas.addEventListener("mouseup", function(event){
		self.mouse_down = false;
		
		self._percent_value = self._get_percent(self.value);
		self.redraw();
		
		self.onchange(self._percent_value, self.progress_value);
		
		
	})

	
	this.canvas.addEventListener("mousemove", function(event){
		if (self.mouse_down){
			self.value = event.offsetX;

			if(event.offsetX < self.padding_left_right){
				self.value = self.padding_left_right;
			}
			if(event.offsetX > self._line_width - self.padding_left_right){
				self.value = self._line_width - self.padding_left_right;
			}
			
			self._percent_value = self._get_percent(self.value);
			
			self.redraw();
			self.onslide(self._percent_value, self.progress_value);
			
		}
	})

Здесь довольно много вспомогательных переменных, которые задают позицию только в нужном месте этого канваса. В виджете, помимо его основной части надо также еще добавить и вывод точной информации — текущее значение прогрессбара и слайдера. Я и решил это делать внутри канваса, хоть тут мы и натыкаемся на вполне понятные проблемы с позиционированием текста. Вывод текста — чуть позже.

Начинаем рисование

Для самого контрола слайдера мы выделяем некоторую область от этого канваса. Он у нас будет ограничен:

  • шириной this._line_width — Шириной полоски слайдера
  • отступом слева-справа
  • отступом сверху-снизу

примерно вот так:

this._draw_border = function(){
		var b = this.padding_top_bottom; // вспомогательный параметры для кривой безье.
		var a = this.padding_left_right;
		var w = this._line_width - ( 2 * a );
		var h =  this.height - (2*b);
		
		
		this.ctx.beginPath();
		this.ctx.moveTo(a,b);
		
		this.ctx.bezierCurveTo(a+(w/2), b, w-(w/2)+a, b, a+w, b );
		this.ctx.bezierCurveTo(a+w+a, b, a+a+w, b+h, a+w, b+h );
		this.ctx.bezierCurveTo( w/2+a, b+h, w/2+a,b+h, a, b+h);
		this.ctx.bezierCurveTo( 0, b+h, 0,b, a,b);
		
		this.ctx.closePath();
		this.ctx.strokeStyle = this.border_color;
		this.ctx.stroke(); // рисуем границу нужным цветом
		
	};

Напомню, что кривая безье содержит во входных параметрах три точки. Четвертая точка — текущая, мы должны в нее перейти с помощью moveTo.
общий смысл рисования такой кривой:

image

Получаем красивую рамочку с закруглёнными концами.

Теперь начинаем магию:
Для того чтобы нарисовать зону отрицательного процентажа и зону форсажа мы будем пользоваться клипами. Клип — это просто, сначала создаем путь, внутри которого происходит рисование, а потом повторяем рисование границы с одно лишь разницей — мы будем не обводить этот путь, а заливать его изнутри нужным цветом. Звучит банально и просто, выглядит тоже не сложно.

Сначала определям, докуда его рисовать, все что до нуля — рисуем отдельным цветом.

			var zero = this._get_x(0); // Магическое число ноль в исходном коде ставить можно

Создаем область отрисовки

			this.ctx.beginPath();
			this.ctx.rect(0,0, zero, this.height);
			this.ctx.clip();

И заливаем ту же самую безье нужным цветом.

			this.ctx.beginPath();
			this.ctx.bezierCurveTo(a+(w/2), b, w-(w/2)+a, b, a+w, b );
			this.ctx.bezierCurveTo(a+w+a, b, a+a+w, b+h, a+w, b+h );
			this.ctx.bezierCurveTo( w/2+a, b+h, w/2+a,b+h, a, b+h);
			this.ctx.bezierCurveTo( 0, b+h, 0,b, a,b);
			this.ctx.fillStyle = this.below_z_color;
			this.ctx.fill();
			this.ctx.closePath();
			this.ctx.restore();

По аналогии поступаем с областью выше 100%.

Немножко о вычислениях

Нам нужно задать значение на виджете и забирать его, преобразуя координаты курсора мыши в процент и наоборот.
Я для этого написал две очень простых функции:

	this._get_percent = function(x){
		
		var a = this.padding_left_right; // отступы слева и справа
		var w = this._line_width - (2*a); // ширина слайдера
		return ((x - a) * this._range/ w)+this.starting_percent ; 
                // Вычитаем из координат мыши ширину отступа, умножаем на разброс от стартового процента до конечного, делим на ширину слайдера и добавляем стартовый процент. 
	};
	this._get_x = function(p){
		
		var a = this.padding_left_right;
		var w = this._line_width - (2*a);
		return a+ (p - this.starting_percent) * w / this._range;
// Наоборот
		
		
		
	};

Ну и немного о рисовании текста

Хотите одновременно сделать наглядным и точным? Пожалуйста, но тогда без текстовых данных не обойтись.
Будем рисовать текст справа от линейки индикатора. Отдельно будем рисовать состояние слайдера, отдельно прогрессбара. Я думаю, можно поэкспериментировать с расположением, но пока сделаем так.

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

		var val = this._percent_value * 100
		var int = Math.floor(val);
		var frac = Math.floor((val - int)*100);

То есть получили отдельно целую, отдельно дробную части. Рисовать их будем разного размера, но для начала вообще подсчитаем, какой нам лучше использовать шрифт. Я решил сделать простую зависимость, которая не слишком хорошо работает в некоторых условиях:

		var base_font_size = this.height - (this.padding_top_bottom*2) ; // размер шрифта целой части
		var add_font_size = Math.floor(base_font_size / 2); // размер шрифта дробной части
		var base_marg = base_font_size *2; // Отступ слева

Ну и в конце — код для отрисовки текста

		this.ctx.save()
		this.ctx.translate(this._line_width+ this.padding_top_bottom,  this.height-this.padding_top_bottom);
		this.ctx.fillStyle = "#000";
		this.ctx.font = base_font_size + "pt Arial";
		
		this.ctx.textAlign = "end"; // Алигн по концу строки
		this.ctx.fillText("" + (int), base_marg, 0 )
		
		this.ctx.textAlign = "center";
		this.ctx.font = (base_font_size -2) + "pt Arial";
		
		this.ctx.fillText(",", base_marg+1,0 )
		
		this.ctx.font = add_font_size + "pt Arial";
		this.ctx.textAlign = "start"; // Алигн по началу строки
		
		this.ctx.fillText("" + (frac), base_marg+3, 0 )
		
		this.ctx.restore();

Заключение

Готовый индикатор можно использовать. Настройки цветов для него могут задаваться напрямую. К сожалению решение использовать canvas не оставило для нас широких возможностей для расскрасски его с помощью css, Но у канваса другие преимущества — в частности с его помощью можно навешивать на этот индикатор дополнительные штрихи и линейки. Благо, что канвас может очень точно рисовать геометрические фигуры.

Для желающих поковырять его или воспользоваться оставляю адрес репозитория github.com/stavenko/power-control-widget. Сегодня этот виджет работал только с одним браузером — Google Chrome, и я если честно не уверен, что события будут правильно отрабатываться в других браузерах. В частности — в событиях может не быть координат мыши в переменных offsetX. А это было очень удобно — не надо вычислять координаты — они сразу даются относительно верха-лева контейнера.

На этом сегодня все.

Автор: stavenko

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js