Рубрики
Без рубрики

Вы вращаете меня правильно R⟳und

Как создать сенсорную вращающуюся ручку ручки. Помечено JavaScript, WebComponents, Rotavo.

Это первый в серии сообщений, детализирующихся, как я построил 🥑 Rotavo PWA Отказ Дайте это вихре и посмотри, что вы можете нарисовать!

Ни одна из прохладных детей больше не вступает в скеуморфизм, но это не значит, что нет места для аналога физического контроля в мире программного обеспечения. Давайте посмотрим, как мы можем воссоздать (надеюсь) интуитивно понятный опыт, регулируя громкость на вашем стереосистеме 📻, создавая собственную вращающуюся, сенсорную сенсорную ручку веб-компонента.

Содержание

  • 🎛️. Ползунки, ленты и циферблаты
  • ⚛️. Основной пользовательский элемент
  • 👻 Введите тень DOM
  • 🚧 Поддержка браузера
  • 🤝 ручка трогать
  • 👆 События указателя и поведение по умолчанию
  • 📐 трогательная тригонометрия
  • 🔄 прикосновение демо
  • 🚧 О, подожди … Поддержка браузера
  • 🖌️ Картина и производительность
  • 🏆 Бонусный контент

🎛️. Ползунки, ленты и циферблаты

Мы собираемся использовать вход В качестве основы API мы хотим, чтобы наш компонент представить. Приведенный ниже фрагмент создаст небольшой слайдер, который будет переходить от 0 до 100 с начальным значением 42.


Твердый фундамент, но это лучшее решение? Если я, как я, вам нравится прочтение света, которое является разбирательством Симпозиум на человеческом интерфейсе 2009, проводимый как часть HCI International 2009 Затем вытените в части 2 вокруг Page 773, вы найдете этот ценный самородок:

Пилоты усерднее читать ленты отображения, чем аналоговые циферблаты

Нам понадобится маленький пилотный линго, чтобы понять знания UX, которые выпадают здесь.

Дисплей «ленты» показывает информацию в линейной моде, названной, потому что она была буквально лентой, которая сосудила вверх и вниз позади стекла. Это может быть использовано для показать что-то вроде высоты или скорости воздуха. Альтернативой является индикатор циферблата, который отображает тем же линейной информации в закругленном макете, часто используемый на приборной панели автомобилей для скорости и уровня топлива. Гипотеза вот что человек может быстрее интерпретировать грубое значение с циферблата, в отличие от ленты, потому что весь диапазон виден.

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

⚛️. Основной пользовательский элемент

Я собираюсь в ZIP через основы справедливо быстро, поэтому, если вы ищете правильное введение, вы должны отправиться на « Custom Elements v1: многоразовые веб-компоненты ».

Мы собираемся начать просто: элемент с одним ценность атрибут. Пользовательский элемент нуждается в « - » в названии, чтобы отличить его от встроенных элементов, поэтому давайте пойдем с Обеспечить эту ссылку обратно в :


Для удобства мы собираемся сопоставить атрибут на HTML-элементе в класс JavaScript, чтобы мы могли сделать такие вещи:

const knob = document.querySelector('input-knob');
knob.value = 2;

Это означает, что нам нужно расширить Htmlelement Класс, проводят атрибуты, а затем подключите его к нашей странице:

class InputKnob extends HTMLElement {
  constructor() {
    // Nothing to do in here at the moment, but we do need to call the parent
    // constructor
    super();
  }

  // Watch for changes on the 'value' attribute
  static get observedAttributes() {
    return ['value'];
  }

  // Map the JavaScript property to the HTML attribute
  get value() {
    return this.hasAttribute('value') ? this.getAttribute('value') : 0;
  }

  set value(value) {
    this.setAttribute('value', value);
  }

  // Respond to any changes to the observed attributes
  attributeChangedCallback(attrName, oldVal, newVal) {
    // nothing yet
  }
}

// Map the class to an element name and make it available to the page!
window.customElements.define('input-knob', InputKnob);

Теперь, если вы загружаете это в браузере, вы увидите … ничего. Ничего на странице и ничего в консоли. Ничего не отображается, но и – ничто не сломано. Мы успешно приложили новый элемент на странице, но все, что мы создали, это пустой элемент. Итак, он работает так же, как добавляет пустой на страницу «работает». Удобно, хотя наше новое Элемент может быть создан так же, как и все остальное на странице – давайте сделаем вещи немного более очевидными.

input-knob {
  display: block;
  background: #cadbbc;
  border: 1rem dashed #356211;
  box-shadow: 0.3rem 0.3rem 0.3rem rgba(0, 0, 0, 0.5);
  width: 8rem;
  height: 8rem;
}

Et voilà, видимый квадрат!

👻 Введите тень DOM

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

{
  --angle: 0rad;
  transform: rotate(var(--angle));
}

Однако мы не хотим просто установить этот стиль прямо на Элемент, так как он подвергается деталям реализации пользователю нашего компонента. Если разработчик хочет добавить на их страницу и применить вращение, он собирается столкнуться. Чтобы решить это, мы собираемся использовать Тень Дом Чтобы создать некоторую разметку, которое по существу частное для нашего компонента, где мы можем добавить нашу собственную структуру и стиль, необходимый для обеспечения поведения компонента.

Мы собираемся начать с этого HTML:


Давайте сломаемся здесь, что происходит здесь:

  • - То есть Переменная CSS будет обновляться для управления вращением элемента
  • ID = "Контейнер" в
    определяет элемент обертки, мы собираемся вращать
  • Часть = "Контейнер" в
    Указывает, что мы хотим сделать элемент доступен для стайлинга извне
  • <Слот> Элемент определяет, где Содержание в теге должно быть оказано

Причина, по которой мы хотим разоблачить

Для стилизации через Часть = "Контейнер" Это потому, что наша сейчас наша « » элемент не собирается вращаться. Итак, что все еще можно стиль – но это останется фиксированным, когда значение изменяется. Вместо этого нам нужно использовать :: Часть () селектор В нашем стиле лист. В этом случае это простое дополнение.

input-knob::part(container) {
  display: block;
  background: #cadbbc;
  border: 1rem dashed #356211;
  box-shadow: 0.3rem 0.3rem 0.3rem rgba(0, 0, 0, 0.5);
  width: 8rem;
  height: 8rem;
}

Вернуться к нашему элементу, теперь мы знаем, какую структуру мы хотим пришло время бросить, что в шаблоне наш элемент может использовать. Мы определим это только за пределами Inputknob сорт:

const template = document.createElement('template');
template.innerHTML = `
  
  
`;

Далее мы хотим добавить корень тени на наш элемент и клонировать этот шаблон в него:

class InputKnob extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.appendChild(template.content.cloneNode(true));
    this._container = this.shadowRoot.getElementById('container');
  } // ✂️ rest of the class omitted
}

Мы собираемся ссылку на нашу # Container так что мы можем обновить это - То есть Переменная.

// class InputKnob
_drawState() {
  this._container.style.setProperty('--angle', `${this.value}rad`);
}

Мы хотим обновить это значение, когда компонент сначала добавляется на страницу, и всякий раз, когда ценность изменился.

// class InputKnob
connectedCallback() {
  this._drawState();
}

attributeChangedCallback(attrName, oldVal, newVal) {
  this._drawState();
}

С этим мы теперь получили компонент, который вращается на основе ценность атрибут. Мы можем проверить, что открывая консоль браузера, чтобы попробовать настройки этих значений и просмотр квадрата вращения.

Хорошо, хорошо – вы попадаете, я должен дать вам что-то интерактивное. Давайте принесем его полный круг и добавьте ползунок, чтобы контролировать вращение. Это так просто, как:




Попробуйте это для себя здесь ниже. Слайдер должен взять квадрат от 0 до всего под 2π (6.28) радианов или полное вращение.

🚧 Поддержка браузера

Это было немного слишком чистым, хотя, а? Вряд ли то, что современное разработка веб-разработки. Возможно, я пренебрег, чтобы упомянуть, что в приведенном выше примере появляется немного больше, чтобы получить его отображение в разных браузерах. На самом деле, если вы проверяете Раздел MDN по поддержке браузера для :: Часть () – Хм, ну, я, вероятно, должен отбросить запрос на тягу для этого. В основном это Chrome и другие браузеры сделали положительные шумы Но мы еще не там.

Первый выпуск – это обращение к браузерам, которые не поддерживают пользовательские элементы или Shadow DOM столько, сколько нам нужно. К счастью, мы можем использовать WebComponents.js Polyfill который обнаружит, какая поддержка необходима. Снимите этот скрипт до импорта/определения элемента. Я схватив его из unskg.com , но вы также можете просто скачать пакет или использовать NPM согласно инструкции.


Не усыпляйтесь в ложное чувство безопасности – сейчас становится грязным. Я не мог найти хороший способ обнаружить поддержку для :: Часть () Так что нести со мной. Помните, что мы добавили Часть = "Контейнер" атрибут нашему элементу? Мы собираемся проверить, если этот атрибут доступен через JavaScript. Если это не так, нам нужно создать некоторую альтернативную структуру в стиле. В этом случае A обернуть любой контент в элементе и .fallbback сорт.

// connectedCallback()
if (!this._container.part) {
  // create a 
  const wrapper = document.createElement('span');
  // move this element's child nodes into it
  wrapper.append(...this.childNodes);
  // add the classes
  wrapper.classList.add('fallback');
  this.classList.add('fallback');
  // and add the wrapper into this element
  this.append(wrapper);
}

Сейчас мы дублируем стиль за падшие элементы:

input-knob.fallback {
  display: block;
}

input-knob.fallback>span.fallback {
  display: block;
  background: #cadbbc;
  border: 1rem dashed #356211;
  box-shadow: 0.3rem 0.3rem 0.3rem rgba(0, 0, 0, 0.5);
  width: 8rem;
  height: 8rem;
}

В идеале, вместо того, чтобы дублировать весь блок, мы могли бы добавить селектор к нашему существующему правилу – однако, что заставляет хотя бы один из других браузеров игнорировать все это. Итак, скопируйте/вставить это. Валовой. Во всяком случае, теперь у нас есть элемент, который работает почти везде, и мы никогда не будем говорить об этом снова. Давайте перейдем на что-то веселое.

🤝 ручка трогать

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

  • Использовать Указатели событий
  • Подавить некоторые поведение браузера по умолчанию
  • Отчаянно пытаются помнить некоторую тригонометрию из школы

👆 События указателя и поведение по умолчанию

Начнем с событий указателя (откровенно, это прекрасное API) и бросит в черту справиться с по умолчанию, так как ни один из них не заставит нас большую боль – в отличие от этого последнего элемента в списке.

class InputKnob extends HTMLElement {
  // ✂️ existing code has been removed
  constructor() {
    // Capture the pointer so that the user can still rotate the element even
    // if they leave the bounding area of it.
    this.setPointerCapture(e.pointerId);
    // Re-bind this for listeners so that this == instance of this class
    this._onPointerdown = this._onPointerdown.bind(this);
    this._onPointermove = this._onPointermove.bind(this);
    this._onPointerup = this._onPointerup.bind(this);
  }

  connectedCallback() {
    // Listen for pointerdown events when the element is connected to the page
    this.addEventListener('pointerdown', this._onPointerdown);
  }

  disconnectedCallback() {
    // And stop listening if the element is removed
    this.removeEventListener('pointerdown', this._onPointerdown);
  }

  _onPointerdown(e) {
    e.preventDefault();
    // A long press can bring up the context menu, we want to disable that when
    // the user is controlling the element
    window.oncontextmenu = () => { return false; };
    // Only add listeners for the other events once interaction has started
    this.addEventListener('pointermove', this._onPointermove);
    this.addEventListener('pointerup', this._onPointerup);
    this.addEventListener('pointercancel', this._onPointerup);
  }

  _onPointermove(e) {
    // On all of these we're preventing the default behaviour as we assume our
    // web developer won't do something like put a 

Мы также хотим обновить CSS в нашем Шаблон Для обеспечения некоторых разумных взаимодействий. Мы собираемся использовать : хозяин Псевдо-класс, чтобы выбрать внешнюю ярлык.

host {
  display: inline-block;
  user-select: none;
  -webkit-user-select: none;
  touch-action: none;
}
#container {
  --angle: 0rad;
  transform: rotate(var(--angle));
}

В приведенных выше стилях происходит три вещи:

  • Во-первых, если вы осмотрите Элемент из предыдущего примера в devtools, вы увидите, что у него есть Авто ширина и высота и не занимают место в документе. Нам нужен элемент, чтобы иметь размер, если наш пользователь будет иметь что-нибудь прикоснуться!
  • Во-вторых, User-select Линии предотвращают пользователю выбрать любой текст внутри управления. Многое нравится предотвращение появления контекстного меню, это останавливает нечетные эффекты выбора текста, когда у пользователя есть их палец вниз.
  • В-третьих, Touch-Action Сообщает браузеру не обрабатывать какие-либо обычные жесты на этом элементе – как панорамирование, масштабирование и т. Д., Как мы собираемся реализовать наше собственное поведение здесь.

📐 трогательная тригонометрия

Теперь слушатели проводятся, наш элемент получит события указателя, на которые мы можем ответить. Давайте погрузимся в математику!

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

const TWO_PI = 2 * Math.PI;

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

// _onPointerdown(e)
this._touchX = e.clientX;
this._touchY = e.clientY;
this._centerX =
  this.offsetLeft - this.scrollLeft + this.clientLeft + this.offsetWidth / 2;
this._centerY =
  this.offsetTop - this.scrollTop + this.clientTop + this.offsetHeight / 2;
this._initialAngle = this._angle;

Далее нам нужен начальный угол касания от нулевого вращения. Мы можем визуализировать это как нанесение правого углового треугольника с одним углом в центре объекта и гипотенузы, выходящие на указатель пользователя. Мы можем получить этот угол из 2 аргумента Arctangent функции, ака Math.atan2 () Отказ Принимая координаты мероприятия указателя и вычитая координаты центра для элемента, мы в основном их нормализуем их, как если бы центр элемента был 0, 0.

// _onPointerdown(e)
this._initialTouchAngle = Math.atan2(
  this._touchY - this._centerY,
  this._touchX - this._centerX
);

Теперь, когда пользователь перемещает указатель, мы хотим отслеживать это движение, поэтому мы можем повернуть элемент соответственно.

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

В коде это выглядит так:

// _onPointermove(e)
this._touchX = e.clientX;
this._touchY = e.clientY;
this._angle =
  // initial rotation of the element
  this._initialAngle
  // subtract the starting touch angle
  - this._initialTouchAngle
  // add the current touch angle
  + Math.atan2(this._touchY - this._centerY, this._touchX - this._centerX);
// Normalise value back into a 2π range
this._angle = (this._angle + TWO_PI) % TWO_PI;
// Done, update the value!
this.value = this._angle;

🔄 прикосновение демо

Боже мой, возьми дышилку. Если вы все еще со мной, вы … Вы заработали это. Вы можете протянуть от мыши или пальцем и просто вращать этот квадрат вокруг всего, что вам нравится.

🚧 О, подожди … Поддержка браузера

Извините, я знаю, я знаю. События указателей добираются туда с точки зрения поддержки , но это не везде. Нам нужно надоваться Мышь и Трогать События Когда указатель не поддерживается.

Первый шаг состоит в том, чтобы отделить логику, специфичную указателя от бизнеса фактически отвечая на мероприятие. Если честно, это хорошая профессиональная инженерная практика в любом случае, и, вероятно, как оно должно было быть написано в первую очередь. Я не показываю _ротация * () Методы, поскольку они буквально именно код, который был там раньше.

_onPointerdown(e) {
  e.preventDefault();
  this._touchX = e.clientX;
  this._touchY = e.clientY;
  // ✂️ Chop out existing code, extract into new method
  this._rotationStart();
  this.setPointerCapture(e.pointerId);
  this.addEventListener('pointermove', this._onPointermove);
  this.addEventListener('pointerup', this._onPointerup);
  this.addEventListener('pointercancel', this._onPointerup);
}

_onPointermove(e) {
  e.preventDefault();
  this._touchX = e.clientX;
  this._touchY = e.clientY;
  // ✂️ Cut and paste again
  this._rotationChange();
}

_onPointerup(e) {
  e.preventDefault();
  // ✂️ One last time!
  this._rotationEnd();
  this.releasePointerCapture(e.pointerId);
  this.removeEventListener('pointermove', this._onPointermove);
  this.removeEventListener('pointerup', this._onPointerup);
  this.removeEventListener('pointercancel', this._onPointerup);
}

Нам также необходимо подключить новые слушатели, если мы не можем использовать события указателя:

// connectedCallback()
if ('PointerEvent' in window) {
  this.addEventListener('pointerdown', this._onPointerdown);
} else {
  this.addEventListener('touchstart', this._onTouchstart);
  this.addEventListener('mousedown', this._onMousedown);
}

И убирать, когда мы закончим:

// disconnectedCallback
if ('PointerEvent' in window) {
  this.removeEventListener('pointerdown', this._onPointerdown);
} else {
  this.removeEventListener('touchstart', this._onTouchstart);
  this.removeEventListener('mousedown', this._onMousedown);
}

События мыши легко добавить, поскольку поверхность, которую мы используем, идентична эквиваленту указателя:

_onMousedown(e) {
  this._touchX = e.clientX;
  this._touchY = e.clientY;
  this._rotationStart();
  document.addEventListener('mousemove', this._onMousemove);
  document.addEventListener('mouseup', this._onMouseup);
}

_onMousemove(e) {
  e.preventDefault();
  this._touchX = e.clientX;
  this._touchY = e.clientY;
  this._rotationChange();
}

_onMouseup(e) {
  e.preventDefault();
  document.removeEventListener('mousemove', this._onMousemove);
  document.removeEventListener('mouseup', this._onMouseup);
  this._rotationEnd();
}

Сенсорные события немного больше участвуют, так как есть сфера для нескольких прикосновений. Тем не менее, мы просто предполагаем, что первое прикосновение – это тот, о котором мы заботимся и … ну, это еще не вызвало проблему.

_onTouchstart(e) {
  e.preventDefault();
  this._touchX = e.changedTouches[0].clientX;
  this._touchY = e.changedTouches[0].clientY;
  this._rotationStart();
  this.addEventListener('touchmove', this._onTouchmove);
  this.addEventListener('touchend', this._onTouchend);
  this.addEventListener('touchcancel', this._onTouchend);
}

_onTouchmove(e) {
  e.preventDefault();
  this._touchX = e.targetTouches[0].clientX;
  this._touchY = e.targetTouches[0].clientY;
  this._rotationChange();
}

_onTouchend(e) {
  e.preventDefault();
  this.removeEventListener('touchmove', this._onTouchmove);
  this.removeEventListener('touchend', this._onTouchend);
  this.removeEventListener('touchcancel', this._onTouchend);
  this._rotationEnd();
}

Уио! Это было не так уж плохо – теперь наш код имеет лучшее разделение проблем и поддержки кросс-браузера!

🖌️ Картина и производительность

Мы не совсем сделаны здесь, хотя. Несмотря на то, что элемент функционирует, мы должны заглянуть на то, насколько хорошо элемент выполняет. Для этого вы захотите открыть Предыдущий глюк Demo в новом окне, чтобы вы могли уволить девтулов Chrome. Мы хотим включить «краска», – вы можете найти его в меню 3 точка → на большем количестве инструментов → рендеринг → мигание на краском, или если вам не нравится три уровня меню, а затем принести командную палитру с Ctrl/CMD + Shift + P и тип “краска мигает”. Когда вы пытаетесь вращать элемент, вы увидите постоянный зеленый прямоугольник, набранный на него.

Это плохо. Это означает, что браузер должен нарисовать каждое взаимодействие, и это не всегда дешевая операция. Если вы находитесь на ноутбуке или телефоне с высоким уровнем телефона, вы, вероятно, ничего не заметите, но это ударит вашему кадру на низкоуровневых или загруженных устройствах. К счастью, исправление Super Simple, чтобы добавить. Мы хотим обеспечить указание на браузер, который мы намереваемся менять элемент каким-либо образом. Удобно, будет изменить Собственность позволяет нам сделать именно это. Мы указываем это в стиле для нашего внутреннего # Container

#container {
  --angle: 0rad;
  transform: rotate(var(--angle));
  will-change: transform;
}

Теперь браузер знает, что мы намерены изменить трансформировать Собственность, и он может оптимизировать для этого результата. Сравните это Обновленный глюк Demo С этой одной строкой добавлена. Вы должны увидеть краткую первоначальную вспышку, но после этого – никаких покрасочных событий вообще. Успех!

🏆 Бонусный контент

До сих пор мы не ставили какого-либо контента в нашему элементу – мы только что стилизовали сам элемент. Если вы посмотрите на Rotavo App Вы увидите, что маленькие 🥑 элементы управления являются просто SVGS внутри тега. Оказывается, вы можете поместить любой контент, который вы хотите внутри этого тега и визуализации. Итак, чтобы закончить вещи для этой статьи ( с огромным спасибо когда-либо пациенту Пол Кинлан за то, что я на его лице ) Пожалуйста, не стесняйтесь, чтобы узнать … Пол Спинлан.

В следующий раз мы рассмотрим добавление пользовательских событий, мин/максимальных значений и подсчета вращений к элементу, чтобы мы могли создать приложение вокруг него!

Оригинал: “https://dev.to/rowan_m/you-spin-me-right-r-und-k32”