Как стать автором
Обновить

Делаем вращательный регулятор.

Время на прочтение11 мин
Количество просмотров12K
Этим топиком я продолжаю цикл статей о написании всяких вкусностей для MooTools. Сегодня мы на чистом JavaScript сделаем вращательный регулятор — контрол, который часто используют в работающих со звуком программах для регулировки громкости или баланса. Вот примерно такой:

Sample

Так как с помощью JavaScript нельзя вращать изображения, то мы поступим не так как поступили бы во флеше. Для реализации задачи нам понадобятся два изображения:

Elements

Идея в том, чтобы поместить индикатор на подложку и вращать его в соответствии с положением курсора мыши. Для вычисления положения индикатора нам пригодится математика. Не бойтесь, волновое уравнение решать нам не прийдется, вспомим немного школьной тригонометрии и полярную систему координат.

Итак, полярная система координат определяет положение точки по двум величинам: угол поворота радиус-вектора относительно полярной оси φ и длина радиус-вектора r. Для лучшего понимания взгляните на схему ниже.

Schema 1

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

x = r⋅cos(φ)
y = r⋅sin(φ)

Кооринаты x и y мы подставим в CSS-свойства left и top индикатора. Стоит отметить, что на схеме ось y направлена снизу вверх. В браузере же у нее противоположное направление. Это мы учтем, когда будем менять координаты индикатора.

Ок, с этим разобрались. Но если r известна изначально (мы знаем размеры картинки), то как же определить угол φ из положения курсора мыши? Опять нам на помощь приходит школьная тригонометрия. Если взглянуть на следующую схему, то можно заметить, что вектор от полюса до точки положения курсора со своими проекциями образует прямоугольный треугольник.

Schema 2

Катетами в этом треугольнике будут координаты курсора. Отсюда мы можем найти тангенс угла φ как отношение противолежащего катета к прилежащему:

tg(φ) = y / x

Угол φ отсюда находим как арктангенс:

φ = arctg(tg(φ)) = arctg(y / x)

Из математики это, пожалуй, все. Теперь займемся кодингом. В HTML все предельно просто:
  <div id="Container">
    <div id="Indicator"></div>
  </div>

CSS:
#Container
{
  position: relative;
  background-image: url('./images/rheostat.png');
  width: 64px;
  height: 64px;
}

#Indicator
{
  position: absolute;
  background-image: url('./images/indicator.png');
  width: 4px;
  height: 4px;
  visibility: hidden;
}


JavaScript код написан с использованием фреймворка MooTools:
var Rheostat = new Class({
  Implements: [Events, Options],
  
  // Задаем опци по умолчанию.
  options: {
    radius: 27,
    minValue: 0,
    maxValue: 100
  },
  
  // Константы для конвертирования градусов в радианы и наоборот.
  deg2rad: Math.PI / 180,
  rad2deg: 180 / Math.PI,
  
  // Флаг, указывающий на то, что мы зажали левую кнопку мыши.
  captured: false,
  
  // Коструктор
  initialize: function(container, indicator, options){
    this.setOptions(options);
    this.indicator = $(indicator);
    this.container = $(container);
    
    // Показываем спрятанный по умолчанию индикатор.
    this.indicator.fade('show');
    
    // Добавляем обработку событий мыши.
    this.container.addEvent('mousedown', this.captureMouse.bind(this));
    document.addEvents({
      'mousemove': this.updateAngle.bind(this),
      'mouseup': this.releaseMouse.bind(this)
    });

    // Размер контейнера нам нужен для того, чтобы сместить систему координат индикатора
    // из левого верхнего угла контейнера в его центр.
    var containerSize = this.container.getSize();
    var indicatorSize = this.indicator.getSize();
    this.offset = {
      x: Math.floor(containerSize.x / 2) - Math.floor(indicatorSize.x / 2),
      y: Math.floor(containerSize.y / 2) - Math.floor(indicatorSize.y / 2)
    };
    
    this.angle = 0;
    this.updateIndicatorPosition();
  },
  
  // Запоминаем, что контрол захвачен мышью.
  captureMouse: function(){
    this.captured = true;
  },
  
  // Стираем флаг захвата.
  releaseMouse: function(){
    this.captured = false;
  },
  
  // В этом методе считается угол по положению курсора мыши.
  updateAngle: function(e){
    if (this.captured)
    {
      var containerPosition = this.container.getPosition();
      
      // Катеты нашего треугольника.
      // К mouseLeft я прибавил 0.1 для того, чтобы избежать возможного деления на ноль впоследствии.
      var mouseLeft = e.client.x - this.offset.x - containerPosition.x + 0.1;
      var mouseTop = this.offset.y - e.client.y + containerPosition.y;
      
      // Вычисление угла (т.к. Math.atan() возвращает значение в радианах,
      // для более простого оперирования с ним переведем его в градусы).
      this.angle = Math.atan(mouseTop / mouseLeft) * this.rad2deg;
      
      // Т.к. функция арктангенса может вернуть нам значения только от -90 до +90, то
      // если курсор находится в левой половине к углу прибавим 180. Иначе в левой половине
      // мы индикатор никогда не увидим.
      if (mouseLeft < 0)
        this.angle += 180;
      
      // Еще одна проверка, чтобы сплошную последовательность значений от 0 до 360 градусов.
      if (this.angle < 0)
        this.angle += 360;
      
      // Считаем значение в соответствии с заданными минимальным и максимальным значениями регулируемой величины.
      var value = Math.floor((this.options.maxValue - this.options.minValue) * this.angle / 360 + this.options.minValue);
      this.fireEvent('valueChanged', value)
      this.updateIndicatorPosition();
    }
  },
  
  updateIndicatorPosition: function(){
    // Переводим угол в радианы для передачи его в Math.cos() и Math.sin().
    var radAngle = this.angle * this.deg2rad
    var left = this.options.radius * Math.cos(radAngle) + this.offset.x;
    
    // Обратите внимание на знак "-". Этим мы разворачиваем ось y наоборот.
    var top = -this.options.radius * Math.sin(radAngle) + this.offset.y;
    
    // Позиционирование индикатора.
    this.indicator.setStyle('left', left);
    this.indicator.setStyle('top', top);
  }
});

Конструируется экземпляр этого регулятора так:
var rheostat = new Rheostat('Container', 'Indicator');

Используем событие изменения значения:
rheostat.addEvent('valueChanged', function(value){
  // В value попадет текущее значение регулируемого параметра.
});

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

UPD. Улучшенный скрипт, устраняющий многие недостатки:
var Rheostat = new Class({
  Implements: [Events, Options],
  
  // Задаем опци по умолчанию.
  options: {
    radius: 27,
    
    // Диапазон величин регулируемого параметра.
    minValue: 0,
    maxValue: 100,
    
    // Ограничение углов вращения индикатора.
    minAngle: 50,
    maxAngle: 310,
    
    // Сдвиг нуля шкалы в градусах.
    angleOffset: -90,
    
    // Развернуть ли шкалу?
    reversed: true
  },
  
  // Константы для конвертирования градусов в радианы и наоборот.
  deg2rad: Math.PI / 180,
  rad2deg: 180 / Math.PI,
  
  // Флаг, указывающий на то, что мы зажали левую кнопку мыши на контроле.
  captured: false,
  
  angle: 0,
  mouseAngle: 0,
  oldMouseAngle: 0,
  
  // Конструктор.
  initialize: function(container, indicator, options){
    this.setOptions(options);
    this.indicator = $(indicator);
    this.container = $(container);
    
    // Добавляем обработку событий мыши.
    this.container.addEvents({
      'mousedown': this.captureMouse.bind(this),
      'mousewheel': this.handleWheel.bind(this),
    });
    
    document.addEvents({
      'mousemove': this.updateAngle.bind(this),
      'mouseup': this.releaseMouse.bind(this)
    });

    // Размер контейнера нам нужен для того, чтобы сместить систему координат индикатора
    // из левого верхнего угла контейнера в его центр.
    var containerSize = this.container.getSize();
    var indicatorSize = this.indicator.getSize();
    this.offset = {
      x: Math.floor(containerSize.x / 2) - Math.floor(indicatorSize.x / 2),
      y: Math.floor(containerSize.y / 2) - Math.floor(indicatorSize.y / 2)
    };
    
    // Угол по умолчанию в начало шкалы.
    this.angle = this.options.minAngle + this.options.angleOffset;
    this.updateIndicatorPosition();
    
    // Показываем спрятанный по умолчанию индикатор.
    this.indicator.fade('hide').fade('in');
  },
  
  // Обработка колесика мыши.
  handleWheel: function(e){
    // Вычисление угла.
    var wheelAngle = this.angle + e.wheel;
    if ((wheelAngle >= this.options.minAngle) && (wheelAngle <= this.options.maxAngle)){
      this.oldMouseAngle = this.mouseAngle = this.angle = wheelAngle;
      this.updateIndicatorPosition();
    }
  },
  
  // Запоминаем, что контрол захвачен мышью.
  captureMouse: function(e){
    this.captured = true;
    
    // Выставляем индикатор в место клика.
    var mouseAngle = this.getMouseAngle(e);
    if ((mouseAngle >= this.options.minAngle) && (mouseAngle <= this.options.maxAngle)){
      this.oldMouseAngle = this.mouseAngle = this.angle = mouseAngle;
      this.updateIndicatorPosition();
    }
  },
  
  // Стираем флаг захвата.
  releaseMouse: function(){
    this.captured = false;
  },
  
  // В этом методе считается угол по положению курсора мыши.
  getMouseAngle: function(e){
    var containerPosition = this.container.getPosition();
  
    // Катеты нашего треугольника.
    // К mouseLeft я прибавил 0.1 для того, чтобы избежать возможного деления на ноль впоследствии.
    var mouseLeft = e.client.x - this.offset.x - containerPosition.x + 0.1;
    var mouseTop = this.offset.y - e.client.y + containerPosition.y;
  
    // Вычисление угла наклона курсора (т.к. Math.atan() возвращает значение в радианах,
    // для более простого оперирования с ним переведем его в градусы).
    var angle = Math.atan(mouseTop / mouseLeft) * this.rad2deg;
  
    // Т.к. функция арктангенса может вернуть нам значения только от -90 до +90, то
    // если курсор находится в левой половине к углу прибавим 180. Иначе в левой половине
    // мы индикатор никогда не увидим.
    if (mouseLeft < 0)
      angle += 180;
  
    // Еще одна проверка, чтобы иметь сплошную последовательность значений от 0 до 360 градусов.
    if (angle < 0)
      angle += 360;
  
    return angle - this.options.angleOffset;
  },
  
  // Вычисляем угол поворота индикатора на основе направления движения курсора мыши.
  updateAngle: function(e){
    // Захвачен ли контрол мышью?
    if (this.captured){
      var mouseAngle = this.getMouseAngle(e);
  
      // Приращение угла индикатора.
      var diffAngle = mouseAngle - this.oldMouseAngle;
      
      // Проверка пересачения границ.
      if ((this.angle + diffAngle >= this.options.minAngle) && (this.angle + diffAngle <= this.options.maxAngle))
        this.angle += diffAngle;

      this.oldMouseAngle = this.mouseAngle = mouseAngle;
      this.updateIndicatorPosition();
    }
  },
  
  // Считаем значение в соответствии с заданными минимальным и максимальным значениями регулируемой величины.
  updateValue: function(){
    var value = Math.floor(
      (this.options.maxValue - this.options.minValue + 1) *
      (this.angle - this.options.minAngle) /
      (this.options.maxAngle - this.options.minAngle)
    );
  
    // Генерируем событие об изменившемся значении.
    this.fireEvent('valueChanged', (this.options.reversed) ? this.options.maxValue - value : value);
  },
  
  // Обновление положения индикатора.
  updateIndicatorPosition: function(){
    // Переводим угол в радианы для передачи его в Math.cos() и Math.sin().
    var radAngle = (this.angle + this.options.angleOffset) * this.deg2rad;
    var left = this.options.radius * Math.cos(radAngle) + this.offset.x;
    
    // Обратите внимание на знак "-". Этим мы разворачиваем ось y наоборот.
    var top = -this.options.radius * Math.sin(radAngle) + this.offset.y;
    
    // Позиционирование индикатора.
    this.indicator.setStyles({left: left, top: top});
    
    // Обновляем значение.
    this.updateValue();
  }
});

Пример с регулировкой размера шрифта.
Теги:
Хабы:
Всего голосов 104: ↑99 и ↓5+94
Комментарии72

Публикации

Истории

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань