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

от автора

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

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

Для начала озвучу чего мне так хотелось:
Мне нужен слайдер — аналог регулятора громкости, совмещенный с прогресс-баром. Эдакий компонент управления мощностью чего-либо, совмещенный с одновременной индикацией этой мощности. Иногда мощность может превышать установленный предел в 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. А это было очень удобно — не надо вычислять координаты — они сразу даются относительно верха-лева контейнера.

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

ссылка на оригинал статьи http://habrahabr.ru/post/224173/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *