Flatiron Field Day - это проект, вдохновленный экспериментом Reddit r / place. Если вы не знакомы, r / place был холстом, который позволял пользователям рисовать все, что они хотят. Предостережение заключалось в том, что они могли рисовать только один пиксель за раз и только раз в десять минут или около того. Конечным результатом этого была серия альянсов, выстраивающих цвета, узоры или изображения, которые были важны для них, и пытались защитить их от людей, которые попытаются перезаписать.

В нашей версии участники могли рисовать пиксели до четырех раз в секунду, но у них было еще одно преимущество, которого не было у участников r / place - они могли использовать код для рисования. Учащиеся в нашей школе находятся на разных уровнях, и в зависимости от этого уровня они могут практиковать Ruby, JavaScript или Python, поэтому мы создали клиентов для каждого из этих языков. Мы также создали средство отслеживания статистики, которое обновляется в реальном времени, сервер для обработки всех этих запросов и программу, которую мы назвали The Nexus, чтобы отфильтровывать неверные запросы и ограничивать скорость, с которой студенты могли рисовать.

Последней частью головоломки был клиент Canvas, который был моей областью. Когда я начал, меня ждала альфа-версия клиента, но в ней не было тонны функциональности. Он успешно отображал плитки, которые уже были установлены, и пользователи могли прыгать в консоль и программно устанавливать плитки, но это было некрасиво и неудобно для пользователя. Что еще более важно, пользоваться им было неинтересно!

У меня было несколько целей, связанных с этим обновлением клиента Canvas, но общая директива заключалась в том, чтобы «сделать это круто, сделать это весело, сделать это сексуально».

- A user should be able to drag the canvas around the screen
- A user should be able to move the canvas with arrow keys
- A user should be able to zoom the canvas
- A user should be able to select a color
- A user should be able to click a pixel to paint it

Масштаб холста

cycleZoom(e) {
    e.preventDefault()
    switch (this.zoom) {
      case 1:
        this.zoom = 2
        this.zoomStatus.innerText = `x2`
        break;
      case 2:
        this.zoom = 4
        this.zoomStatus.innerText = `x4`
        break;
      case 4:
        this.zoom = 8
        this.zoomStatus.innerText = `x8`
        break;
      case 8:
        this.zoom = 16
        this.zoomStatus.innerText = `x16`
        break;
      case 16:
        this.zoom = 1
        this.zoomStatus.innerText = `x1`
        break;
      default:
        this.zoom = 1
        this.zoomStatus.innerText = `x1`
    }
    this.zoomer.style.transform = `scale(${this.zoomMult(this.zoom)}, ${this.zoomMult(this.zoom)})`
  }

Я черпал вдохновение для нашей функции масштабирования холста из блога разработчиков r / place. Это был потрясающий ресурс и в целом отличное чтение. Я взял много информации из блога о преобразованиях CSS, которые были моим основным способом манипуляции во всем.

Мы использовали холст 500 x 500 и решили реализовать различные уровни масштабирования, чтобы наши пользователи могли быть настолько близки, насколько они считали полезными для них. Для этого мы обернули холст в div, который мы назвали zoomer, и все это контролируется нашим классом CanvasManager.

Я прикрепил прослушиватель событий keyDown к основной части сайта. Таким образом, каждый раз, когда кто-то нажимает пробел, этот слушатель запускает cycleZoom в CanvasManager. В свою очередь, cycleZoom проверит текущий уровень масштабирования, а затем переключит нашего пользователя на следующий доступный уровень. С этим было довольно легко справиться, и все работало прекрасно, пока я не подключил свой компьютер к монитору 4K, на котором мы будем отображать доску во время Дня поля. На огромном экране холст выглядел крошечным.

Чтобы позаботиться об этом и учесть любое другое разрешение экрана, которое люди могут использовать, я создал формулу, которая будет регулироваться в зависимости от размера экрана. В этом случае переменная sizeAdjust будет в 0,00165 раза больше высоты экрана. Умножение этого на текущий уровень масштабирования даст фактическое число, необходимое мне для правильной настройки масштаба.

Холст Движение

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

Само сопротивление довольно стандартное, хотя математика движений может сбивать с толку. Я дал конструктору свойств Dragger startingX и startingY, а также lastTimeX и lastTimeY. Используя clientX и clientY, которые дает нам событие перемещения мыши, я вычислил, насколько пользователь переместил свою мышь, их начальную позицию и позицию остановки.

Я также добавил логическое значение, чтобы отслеживать, происходит ли событие перетаскивания. При нажатии мыши он переключается с false на true. Когда это правда, я бы преобразовал div в зависимости от движения пользователя.

mouseMove(e){
    if (this.dragEvent === true){
      this.xDiff = this.startingX - e.clientX
      this.yDiff = this.startingY - e.clientY
      this.newX = this.lastTimeX - this.xDiff
      this.newY = this.lastTimeY - this.yDiff
      const newPos = `translate(${this.newX}px, ${this.newY}px)`
      this.dragger.style.transform = newPos
      this.handleTilt(e)
    }
  }

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

Выбор цвета / Покраска

Это было потрясающе. Я начал с создания тупой версии селектора цвета на странице. Я сделал несколько забавных новых значков (пипетку и кисть) для курсора, которые, как я думал, были просто для стиля, но в итоге оказались полезными и для логики.

По сути, на холсте был прослушиватель событий щелчка. Если бы курсор был пипеткой, я бы переключил курсор обратно в нормальное состояние и изменил цвет фона блока выбора цвета на цвет плитки, на которой щелкнул курсор. Достаточно легко получить значения RGB из определенной координаты с помощью getImageData, если у вас есть значения X и Y. И, используя clientX и clientY вместе с величиной масштабирования и множителем масштабирования, я смог определить точное положение мыши, даже когда пользователь был увеличен.

Если бы курсор был кистью, я бы снова взял эти X и Y, взял цвет фона поля выбора цвета, а затем сделал бы запрос на публикацию на наш сервер, используя всю эту информацию.

Делаем это сексуально

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

Поскольку я думал, что функция наклона может быть немного выше моих возможностей, я начал с поиска библиотеки, которая справилась бы с этим за меня. В итоге я остановился на vanilla-tilt, интересной библиотеке, которая создает эффект трехмерного наклона при наведении курсора на элемент. Его легко реализовать, и казалось, что адаптировать его к моим потребностям будет несложно.

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

Для этого вернемся к преобразованиям CSS. Поскольку преобразования представляют собой строку, ими сложно манипулировать, но их легко заменить. Таким образом, я создал div для наклона по оси Y и отдельный для наклона по оси X. Приведенный ниже фрагмент является самой базовой версией, которую я построил, и только для движения по оси X.

handleTilt(e){
    if (e.movementX < -5) {
      this.Ytilter.style.transform = "perspective(800px) rotateY(-3deg)"
    } else if (e.movementX > 5) {
      this.Ytilter.style.transform = "perspective(800px) rotateY(3deg)"
    } else {
      this.Ytilter.style.transform = "perspective(800px) rotateY(0deg)"
    }
}

e.movementX и представляют расстояние, на которое указатель мыши переместился между его последним событием и текущим моментом. Если для базового порога не установлено значение 5, доска будет наклоняться, когда пользователи не собираются этого делать, так как слегка неаккуратный щелчок по-прежнему может указывать на небольшое движение.

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

if (e.movementX < -150) {
      this.Ytilter.style.transform = "perspective(800px) rotateY(-360deg)"
    }else if(e.movementX < -20){
     this.Ytilter.style.transform = "perspective(800px) rotateY(-7deg)"
    }else if (e.movementX < -5) {
      this.Ytilter.style.transform = "perspective(800px) rotateY(-3deg)"
    }

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

#Ytilter {
  transition: all 0.15s;
}
#Xtilter{
  transition: all 0.15s;
}

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

Подведение итогов

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

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

использованная литература

Хотите узнать больше о Дне поля? Посмотрите эту замечательную статью о том, как мы создали Nexus!

Https://medium.com/@sbal13/building-the-nexus-for-flatiron-field-day-72131c0942c1