Пишем Pixel Art Maker на чистом JavaScript

от автора

Доброго времени суток, друзья!

Предисловие

Однажды веб серфинг привел меня к этому.

Позже обнаружил статью про то, как это работает.

Казалось бы, ничего особенного — Пикачу, нарисованный средствами CSS. Данная техника называется Pixel Art (пиксельное искусство?). Что меня поразило, так это трудоемкость процесса. Каждая клеточка раскрашивается вручную (ну, почти; благо существуют препроцессоры; Sass в данном случае). Конечно, красота требует жертв. Однако разработчик — существо ленивое. Посему я задумался об автоматизации. Так появилось то, что я назвал Pixel Art Maker.

Условия

Что мы хотим получить?

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

Вот парочка примеров из сети:

Дополнительные функции:

  • форма клеточек — квадрат или круг
  • ширина клеточек в пикселях
  • количество клеточек
  • цвет фона
  • цвет для раскрашивания
  • функция создания холста
  • функция отображения номеров клеточек
  • функция сохранения/удаления изображения
  • функция очистки холста
  • функция удаления холста

Более мелкие детали обсудим в процессе кодинга.

Итак, поехали.

Разметка

Для реализации необходимого функционала наш HTML должен выглядеть примерно так:

<!-- создаем контейнер для инструментов --> <div class="tools">      <!-- создаем контейнер для формы фигур (клеточек) -->     <div>         <p>Shape Form</p>         <select>             <!-- квадрат -->             <option value="squares">Square</option>              <!-- круг -->             <option value="circles">Circle</option>         </select>     </div>      <!-- создаем контейнер для ширины и количества клеточек -->     <div class="numbers">         <!-- ширина -->         <div>             <!-- устанавливаем диапазон от 10 до 50 (объяснение ниже) -->             <p>Shape Width <br> <span>(from 10 to 50)</span></p>             <input type="number" value="20" class="shapeWidth">         </div>          <!-- количество -->         <div>             <!-- устанавливаем аналогичный диапазон -->             <p>Shape Number <br> <span>(from 10 to 50)</span></p>             <input type="number" value="30" class="shapeNumber">         </div>     </div>      <!-- создаем контейнер для цветов -->     <div class="colors">         <!-- цвет фона -->         <div>             <p>Background Color</p>             <input type="color" value="#ffff00" required class="backColor">         </div>          <!-- цвет фигуры (для раскрашивания) -->         <div>             <p>Shape Color</p>             <input type="color" value="#0000ff" class="shapeColor">         </div>     </div>      <!-- создаем контейнер для кнопок -->     <div class="buttons">         <!-- кнопка для создания холста -->         <input type="button" value="Generate Canvas" class="generate">          <!-- кнопка для показа/скрытия номеров клеточек (фигур) -->         <input type="button" value="Show/Hide Numbers" class="show">          <!-- кнопка сохранения/удаления изображения (результата) -->         <input type="button" value="Save/Delete Image" class="save">          <!-- кнопка для очистки холста с сохранением ширины и количества фигур -->         <input type="button" value="Clear Canvas" class="clear">          <!-- кнопка для полного удаления холста -->         <input type="button" value="Delete Canvas" class="delete">     </div> </div>  <!-- холст --> <canvas></canvas> 

Диапазон (лимит) значений для ширины и количества клеточек определялся опытным путем. Эксперименты показали, что меньшие/большие значения нецелесообразны по причинам чрезмерной детализации (для значений < 10 для ширины), снижения производительности (для значений > 50 для количества) и т.д.

Стили

В стилях у нас ничего особенного.

CSS:

* {     margin: 0;     padding: 0;     box-sizing: border-box; }  body {     margin: 0;     min-height: 100vh;     display: flex;     flex-wrap: wrap;     justify-content: center;     align-items: center;     align-content: flex-start; }  h1 {     width: 100%;     text-align: center;     font-size: 2.4em;     color: #222; }  .tools {     height: 100%;     display: inherit;     flex-direction: column;     margin: 0;     font-size: 1.1em; }  .buttons {     display: inherit;     flex-direction: column;     align-items: center; }  div {     margin: .25em;     text-align: center; }  p {     margin: .25em 0;     user-select: none; }  select {     padding: .25em .5em;     font-size: .8em; }  input, select {     outline: none;     cursor: pointer; }  input[type="number"] {     width: 30%;     padding: .25em 0;     text-align: center;     font-size: .8em; }  input[type="color"] {     width: 30px;     height: 30px; }  .buttons input {     width: 80%;     padding: .5em;     margin-bottom: .5em;     font-size: .8em; }  .examples {     position: absolute;     top: 0;     right: 0; }  a {     display: block; }  span {     font-size: .8em; }  canvas {     display: none;     margin: 1em;     cursor: pointer;     box-shadow: 0 0 1px #222; } 

JavaScript

Определяем холст и его контекст (2D контекст рисования):

let c = document.querySelector('canvas'),     $ = c.getContext('2d') 

Находим кнопку для создания холста и «вешаем» на нее обработчик события «клик»:

document.querySelector('.generate').onclick = generateCanvas 

Весь дальнейший код будет находиться в функции «generateCanvas»:

function generateCanvas(){     ... } 

Определяем форму, ширину, количество по горизонтали и общее количество (холст представляет собой одинаковое количество клеточек по горизонтали и вертикали), а также цвет фона:

// форма let shapeForm = document.querySelector('select').value // ширина (только целые числа) let shapeWidth = parseInt(document.querySelector('.shapeWidth').value) // количество по горизонтали (только целые числа) let shapeNumber = parseInt(document.querySelector('.shapeNumber').value) // общее количество (удваиваем количество по горизонтали) let shapeAmount = Math.pow(shapeNumber, 2) // цвет фона let backColor = document.querySelector('.backColor').value 

Определяем размер холста и устанавливаем ему соответствующие атрибуты (помним, что правильный размер холста устанавливается через атрибуты):

// ширина = высота = ширина клеточки * количество клеточек по горизонтали let W = H = shapeWidth * shapeNumber c.setAttribute('width', W) c.setAttribute('height', H) 

Некоторые дополнительные настройки:

// ширина границ let border = 1 // цвет границ let borderColor = 'rgba(0,0,0,.4)' // по умолчанию номера фигур не отображаются let isShown = false  // проверяем соблюдение диапазона значений // и числового формата данных // отображаем холст // и в зависимости от формы фигуры запускаем соответствующую функцию if (shapeWidth < 10 || shapeWidth > 50 || shapeNumber < 10 || shapeNumber > 50 || isNaN(shapeWidth) || isNaN(shapeNumber)) {     throw new Error(alert('wrong number')) } else if (shapeForm == 'squares') {     c.style.display = 'block'     squares() } else {     c.style.display = 'block'     circles() } 

Вот как выглядит функция «squares»:

function squares() {     // определяем начальные координаты     let x = y = 0      // массив фигур     let squares = []      // ширина и высота фигуры (квадрата)     let w = h = shapeWidth      // формируем необходимое количество фигур     addSquares()      // функция-конструктор     function Square(x, y) {         // координата х          this.x = x         // координата y          this.y = y         // цвет фигуры = цвет фона         this.color = backColor         // по умолчанию фигура не выбрана         this.isSelected = false     }      // функция добавления фигур     function addSquares() {         // цикл по общему количеству фигур         for (let i = 0; i < shapeAmount; i++) {             // используем конструктор             let square = new Square(x, y)              // определяем координаты каждой фигуры             // для этого к значению х прибавляем ширину фигуры             x += w              // когда значение х становится равным ширине холста             // увеличиваем значение y на высоту фигуры             // так осуществляется переход к следующей строке             // сбрасываем значение х             if (x == W) {                 y += h                 x = 0             }              // добавляем фигуру в массив             squares.push(square)         }         // рисуем фигуры на холсте         drawSquares()     }      // функция рисования фигур     function drawSquares() {         // очищаем холст         $.clearRect(0, 0, W, H)          // цикл по количеству фигур         for (let i = 0; i < squares.length; i++) {             // берем фигуру из массива             let square = squares[i]             // начинаем рисовать             $.beginPath()             // рисуем квадрат, используя координаты фигуры             $.rect(square.x, square.y, w, h)             // цвет фигуры             $.fillStyle = square.color             // ширина границ             $.lineWidth = border             // цвет границ             $.strokeStyle = borderColor             // заливаем фигуру             $.fill()             // обводим фигуру             $.stroke()              // если нажата кнопка для отображения номеров фигур             if (isShown) {                 $.beginPath()                 // параметры шрифта                 $.font = '8pt Calibri'                 // цвет текста                 $.fillStyle = 'rgba(0,0,0,.6)'                 // рисуем номер, опираясь на его координаты                 $.fillText(i + 1, square.x, (square.y + 8))             }         }     }      // вешаем на холст обработчик события "клик"     c.onclick = select     // функция обработки клика     function select(e) {         // определяем координаты курсора         let clickX = e.pageX - c.offsetLeft,             clickY = e.pageY - c.offsetTop          // цикл по количеству фигур         for (let i = 0; i < squares.length; i++) {             let square = squares[i]              // определяем фигуру, по которой кликнули             // пришлось повозиться             // возможно, существует более изящное решение             if (clickX > square.x && clickX < (square.x + w) && clickY > square.y && clickY < (square.y + h)) {                 // раскрашиваем фигуру, по которой кликнули, заданным цветом                 // при повторном клике возвращаем фигуре первоначальный цвет (цвет фона)                 if (square.isSelected == false) {                     square.isSelected = true                     square.color = document.querySelector('.shapeColor').value                 } else {                     square.isSelected = false                     square.color = backColor                 }                 // перерисовываем фигуры                 // в принципе, можно реализовать перерисовку только фигуры, по которой кликнули                 // но решение, по крайней мере у меня, получилось громоздким                 // решил, что игра не стоит свеч                  drawSquares()             }         }     }      // находим кнопку для отображения номеров фигур и вешаем на нее обработчик события "клик"     document.querySelector('.show').onclick = showNumbers     // функция отображения номеров фигур     function showNumbers() {         if (!isShown) {             isShown = true             // цикл по количеству фигур              for (let i = 0; i < squares.length; i++) {                 let square = squares[i]                 $.beginPath()                 // параметры шрифта                  $.font = '8pt Calibri'                 // цвет шрифта                  $.fillStyle = 'rgba(0,0,0,.6)'                 // рисуем номер, опираясь на его координаты                 $.fillText(i + 1, square.x, (square.y + 8))             }         } else {             isShown = false         }         // перерисовываем фигуры         drawSquares()     } } 

Функция «circles» очень похожа на функцию «squares».

JavaScript:

function circles() {     // радиус круга     let r = shapeWidth / 2      let x = y = r      let circles = []      addCircles()      function Circle(x, y) {         this.x = x         this.y = y         this.color = backColor         this.isSelected = false     }      function addCircles() {         for (let i = 0; i < shapeAmount; i++) {             let circle = new Circle(x, y)             // к значению х прибавляется ширина фигуры             x += shapeWidth             // когда значение х становится равным сумме ширины холста и радиуса фигуры             // увеличиваем значение у на ширину фигуры             // сбрасываем значение х до значения радиуса             if (x == W + r) {                 y += shapeWidth                 x = r             }             circles.push(circle)         }         drawCircles()     }      function drawCircles() {         $.clearRect(0, 0, W, H)          for (let i = 0; i < circles.length; i++) {             let circle = circles[i]             $.beginPath()             // рисуем круг             $.arc(circle.x, circle.y, r, 0, Math.PI * 2)             $.fillStyle = circle.color             $.strokeStyle = borderColor             $.lineWidth = border             $.fill()             $.stroke()             if (isShown) {                 $.beginPath()                 $.font = '8pt Calibri'                 $.fillStyle = 'rgba(0,0,0,.6)'                 $.fillText(i + 1, (circle.x - 8), circle.y)             }         }     }      c.onclick = select     function select(e) {         let clickX = e.pageX - c.offsetLeft,             clickY = e.pageY - c.offsetTop          for (let i = 0; i < circles.length; i++) {             let circle = circles[i]              // определяем круг, по которому кликнули             let distanceFromCenter = Math.sqrt(Math.pow(circle.x - clickX, 2) + Math.pow(circle.y - clickY, 2))              if (distanceFromCenter <= r) {                 if (circle.isSelected == false) {                     circle.isSelected = true                     circle.color = document.querySelector('.shapeColor').value                 } else {                     circle.isSelected = false                     circle.color = backColor                 }                 drawCircles()             }         }     }      document.querySelector('.show').onclick = showNumbers     function showNumbers() {         if (!isShown) {             isShown = true             for (let i = 0; i < circles.length; i++) {                 let circle = circles[i]                 $.beginPath()                 $.font = '8pt Calibri'                 $.fillStyle = 'rgba(0,0,0,.6)'                 $.fillText(i + 1, (circle.x - 8), circle.y)             }         } else {             isShown = false         }         drawCircles()     } } 

Находим кнопку для сохранения/удаления результата (изображения) и вешаем на нее обработчик события «клик»:

document.querySelector('.save').onclick = () => {     // ищем изображение     let img = document.querySelector('img')      // если не находим, создаем     // если находим, удаляем     img == null ? document.body.appendChild(document.createElement('img')).src = c.toDataURL() : document.body.removeChild(img) } 

Находим кнопку для очистки холста и…:

document.querySelector('.clear').onclick = () => {     // очищаем и перерисовываем холст     $.clearRect(0, 0, W, H)     generateCanvas() } 

Находим кнопку для удаления холста и…:

document.querySelector('.delete').onclick = () => {     $.clearRect(0, 0, W, H)     c.style.display = 'none' } 

Результат выглядит так:

Codepen (добавил парочку примеров использования)

Github

Благодарю за внимание.

ссылка на оригинал статьи https://habr.com/ru/post/495772/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *