Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Доброго времени суток, друзья!
Предисловие
Однажды веб серфинг привел меня к этому.
Позже обнаружил статью про то, как это работает.
Казалось бы, ничего особенного — Пикачу, нарисованный средствами CSS. Данная техника называется Pixel Art (пиксельное искусство?). Что меня поразило, так это трудоемкость процесса. Каждая клеточка раскрашивается вручную (ну, почти; благо существуют препроцессоры; Sass в данном случае). Конечно, красота требует жертв. Однако разработчик — существо ленивое. Посему я задумался об автоматизации. Так появилось то, что я назвал Pixel Art Maker.
Условия
Что мы хотим получить?
Нам нужна программа, генерирующая заданное количество клеточек с возможностью их раскрашивания произвольными цветами.
Вот парочка примеров из сети:
- пример 1
- пример 2
Дополнительные функции:
- форма клеточек — квадрат или круг
- ширина клеточек в пикселях
- количество клеточек
- цвет фона
- цвет для раскрашивания
- функция создания холста
- функция отображения номеров клеточек
- функция сохранения/удаления изображения
- функция очистки холста
- функция удаления холста
Более мелкие детали обсудим в процессе кодинга.
Итак, поехали.
Разметка
Для реализации необходимого функционала наш 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
Благодарю за внимание.