Writing Pixel Art Maker in JavaScript



Good day, friends!

Foreword


Once web surfing led me to this .

Later I discovered an article about how this works.

It would seem nothing special - Pikachu, drawn with CSS. This technique is called Pixel Art (pixel art?). What struck me was the complexity of the process. Each cell is painted manually (well, almost; since there are preprocessors; Sass in this case). Of course, beauty requires sacrifice. However, the developer is a lazy creature. Therefore, I thought about automation. So what I called Pixel Art Maker appeared.

Conditions


What do we want to get?

We need a program that generates a given number of cells with the possibility of coloring them with arbitrary colors.

Here are a couple of examples from the web:


Additional functions:

  • cell shape - square or circle
  • cell width in pixels
  • number of cells
  • background color
  • color for coloring
  • canvas creation function
  • cell number display function
  • image save / delete function
  • canvas cleaning function
  • canvas removal function

We will discuss smaller details in the coding process.

So let's go.

Markup


To implement the necessary functionality, our HTML should look something like this:

<!--     -->
<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>

The range (limit) of values ​​for the width and number of cells was determined empirically. The experiments showed that smaller / larger values ​​are impractical for reasons of excessive detail (for values ​​<10 for width), decreased performance (for values> 50 for quantity), etc.

Styles


We have nothing special in styles.

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


Define the canvas and its context (2D drawing context):

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

We find the button for creating the canvas and hang the click event handler on it:

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

All further code will be in the generateCanvas function:

function generateCanvas(){
    ...
}

We determine the shape, width, horizontal amount and total amount (the canvas represents the same number of cells horizontally and vertically), as well as the background color:

// 
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

We determine the size of the canvas and set the appropriate attributes for it (remember that the correct canvas size is set through the attributes):

//  =  =   *    
let W = H = shapeWidth * shapeNumber
c.setAttribute('width', W)
c.setAttribute('height', H)

Some additional settings:

//  
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()
}

Here's what the squares function looks like:

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()
    }
}

The circles function is very similar to the squares function.

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()
    }
}

We find the button for saving / deleting the result (image) and hang the click event handler on it:

document.querySelector('.save').onclick = () => {
    //  
    let img = document.querySelector('img')

    //   , 
    //  , 
    img == null ? document.body.appendChild(document.createElement('img')).src = c.toDataURL() : document.body.removeChild(img)
}

We find the button for cleaning the canvas and ...:

document.querySelector('.clear').onclick = () => {
    //    
    $.clearRect(0, 0, W, H)
    generateCanvas()
}

Find the button to delete the canvas and ...:

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

The result looks like this:



Codepen (added a couple of use cases)

Github

Thank you for your attention.

All Articles