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