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

Сьогодні я зробив невеликий сниппет коду для себе і вирішив поділитися з співтовариством його вмістом та історією його створення.
 
Я досить довго рився в інтернетах для того щоб знайти хоч щось схоже на те, що мені потрібно, але інтернет досі з таким завданням не стикався мабуть або ніхто не зважився створювати подібний віджет. Напевно нікому не треба було.
 
Для початку озвучу чого мені так хотілося:
Мені потрібен слайдер — аналог регулятора гучності, поєднаний з прогрес-баром. Такий собі компонент управління потужністю чого-небудь, поєднаний з одночасною індикацією цієї потужності. Іноді потужність може перевищувати встановлену межу в 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. А це було дуже зручно — не треба обчислювати координати — вони відразу даються щодо верху-лева контейнера.
 
На цьому сьогодні все.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.