Skip to content

Paint

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta author="MiduDev" />
<meta url="midudev" />
<meta author="ShunTrDev" />
<title>Paint.js</title>
<style>
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
font-family: 'Arial', sans-serif;
background: #222;
padding: 16px;
max-width: 500px;
margin: 0 auto;
}
h1 {
color: #fce300;
font-size: 12px;
font-weight: 600;
display: flex;
justify-content: center;
align-items: center;
gap: 4px;
img {
width: 16px;
height: auto;
}
}
canvas {
background: #fff;
}
header {
grid-area: header;
background: silver;
padding: 2px;
button {
border: 0;
background: transparent;
&:hover {
box-shadow: 1px 1px black, inset -1px -1px gray, inset 1px 1px white;
}
}
}
main {
grid-area: main;
padding: 16px;
}
footer {
grid-area: footer;
background: silver;
padding: 4px;
}
#container {
border: 1px solid #808080;
display: grid;
grid-template-areas:
'header header header'
'aside main main'
'footer footer footer';
}
aside {
background: silver;
grid-area: aside;
width: 52px;
padding-top: 2px;
nav {
display: flex;
flex-wrap: wrap;
gap: 1px;
justify-content: center;
}
button {
width: 24px;
height: 24px;
background: #bdbdbd url('./icons/draw.png') no-repeat center;
border: 1.5px solid #eee;
border-right-color: #000;
border-bottom-color: #000;
image-rendering: pixelated;
&.active {
background-color: #eee;
border-color: #000;
border-right-color: #eee;
border-bottom-color: #eee;
}
&[disabled] {
pointer-events: none;
opacity: 0.5;
}
}
}
#erase-btn {
background-image: url('./icons/erase.png');
}
#rectangle-btn {
background-image: url('./icons/rectangle.png');
}
#ellipse-btn {
background-image: url('./icons/ellipse.png');
}
#picker-btn {
background-image: url('./icons/picker.png');
}
#clear-btn {
background-image: url('./icons/trash.png');
background-size: 18px;
}
</style>
<script type="module">
// CONSTANTS
const MODES = {
DRAW: 'draw',
ERASE: 'erase',
RECTANGLE: 'rectangle',
ELLIPSE: 'ellipse',
PICKER: 'picker',
}
// UTILITIES
const $ = (selector) => document.querySelector(selector)
const $$ = (selector) => document.querySelectorAll(selector)
// ELEMENTS
const $canvas = $('#canvas')
const $colorPicker = $('#color-picker')
const $clearBtn = $('#clear-btn')
const $drawBtn = $('#draw-btn')
const $eraseBtn = $('#erase-btn')
const $rectangleBtn = $('#rectangle-btn')
const $pickerBtn = $('#picker-btn')
const ctx = $canvas.getContext('2d')
// STATE
let isDrawing = false
let isShiftPressed = false
let startX, startY
let lastX = 0
let lastY = 0
let mode = MODES.DRAW
let imageData
// EVENTS
$canvas.addEventListener('mousedown', startDrawing)
$canvas.addEventListener('mousemove', draw)
$canvas.addEventListener('mouseup', stopDrawing)
$canvas.addEventListener('mouseleave', stopDrawing)
$colorPicker.addEventListener('change', handleChangeColor)
$clearBtn.addEventListener('click', clearCanvas)
document.addEventListener('keydown', handleKeyDown)
document.addEventListener('keyup', handleKeyUp)
$pickerBtn.addEventListener('click', () => {
setMode(MODES.PICKER)
})
$eraseBtn.addEventListener('click', () => {
setMode(MODES.ERASE)
})
$rectangleBtn.addEventListener('click', () => {
setMode(MODES.RECTANGLE)
})
$drawBtn.addEventListener('click', () => {
setMode(MODES.DRAW)
})
// METHODS
function startDrawing(event) {
isDrawing = true
const { offsetX, offsetY } = event
// guardar las coordenadas iniciales
;[startX, startY] = [offsetX, offsetY]
;[lastX, lastY] = [offsetX, offsetY]
imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
}
function draw(event) {
if (!isDrawing) return
const { offsetX, offsetY } = event
if (mode === MODES.DRAW || mode === MODES.ERASE) {
// comenzar un trazado
ctx.beginPath()
// mover el trazado a las coordenadas actuales
ctx.moveTo(lastX, lastY)
// dibujar una línea entre coordenadas actuales y las nuevas
ctx.lineTo(offsetX, offsetY)
ctx.stroke()
// actualizar la última coordenada utilizada
;[lastX, lastY] = [offsetX, offsetY]
return
}
if (mode === MODES.RECTANGLE) {
ctx.putImageData(imageData, 0, 0)
// startX -> coordenada inicial del click
let width = offsetX - startX
let height = offsetY - startY
if (isShiftPressed) {
const sideLength = Math.min(Math.abs(width), Math.abs(height))
width = width > 0 ? sideLength : -sideLength
height = height > 0 ? sideLength : -sideLength
}
ctx.beginPath()
ctx.rect(startX, startY, width, height)
ctx.stroke()
return
}
}
function stopDrawing(event) {
isDrawing = false
}
function handleChangeColor() {
const { value } = $colorPicker
ctx.strokeStyle = value
}
function clearCanvas() {
// también os ayudaría a limpiar parte del canvas
// con la herramienta de selección
ctx.clearRect(0, 0, canvas.width, canvas.height)
}
async function setMode(newMode) {
let previousMode = mode
mode = newMode
// para limpiar el botón activo actual
$('button.active')?.classList.remove('active')
if (mode === MODES.DRAW) {
$drawBtn.classList.add('active')
canvas.style.cursor = 'crosshair'
ctx.globalCompositeOperation = 'source-over'
ctx.lineWidth = 2
return
}
if (mode === MODES.RECTANGLE) {
$rectangleBtn.classList.add('active')
canvas.style.cursor = 'nw-resize'
ctx.globalCompositeOperation = 'source-over'
ctx.lineWidth = 2
return
}
if (mode === MODES.ERASE) {
$eraseBtn.classList.add('active')
canvas.style.cursor = 'url("./cursors/erase.png") 0 24, auto'
ctx.globalCompositeOperation = 'destination-out'
ctx.lineWidth = 20
return
}
if (mode === MODES.PICKER) {
$pickerBtn.classList.add('active')
const eyeDropper = new window.EyeDropper()
try {
const result = await eyeDropper.open()
const { sRGBHex } = result
ctx.strokeStyle = sRGBHex
$colorPicker.value = sRGBHex
setMode(previousMode)
} catch (e) {
// si ha habido un error o el usuario no ha recuperado ningún color
}
return
}
}
function handleKeyDown({ key }) {
isShiftPressed = key === 'Shift'
}
function handleKeyUp({ key }) {
if (key === 'Shift') isShiftPressed = false
}
// INIT
setMode(MODES.DRAW)
// Show Picker if browser has support
if (typeof window.EyeDropper !== 'undefined') {
$pickerBtn.removeAttribute('disabled')
}
ctx.lineJoin = 'round'
ctx.lineCap = 'round'
</script>
</head>
<body>
<h1>
<img src="./icon.png" alt="Paint.js" />
Paint.js
</h1>
<div id="container">
<header>
<button>File</button>
<button>Edit</button>
<button>View</button>
<button>Image</button>
<button>Options</button>
<button>Help</button>
</header>
<aside>
<nav>
<button id="draw-btn" title="Pincel"></button>
<button id="erase-btn" title="Borrar"></button>
<button id="rectangle-btn" title="Rectángulo"></button>
<button id="ellipse-btn" title="Elipse"></button>
<button disabled id="picker-btn" title="Capturar Color"></button>
<button id="clear-btn" title="Limpiar dibujo"></button>
</nav>
</aside>
<main>
<canvas id="canvas" width="300" height="200"></canvas>
</main>
<footer>
<input type="color" id="color-picker" value="#000000" />
</footer>
</div>
</body>
</html>