
Доброго времени суток, друзья!
Предисловие
Однажды веб серфинг привел меня к этому.
Позже обнаружил статью про то, как это работает.
Казалось бы, ничего особенного — Пикачу, нарисованный средствами 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
Благодарю за внимание.
