반응형
그림판 애플리케이션
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://kit.fontawesome.com/833be080c6.js" crossorigin="anonymous"></script>
<link rel="stylesheet" href="./main.css">
<script defer src="./index.js"></script>
</head>
<body>
<canvas></canvas>
<div class="controls">
<select class="size control" title="line size">
<option value="5">5</option>
<option value="10">10</option>
<option value="15">15</option>
<option value="20">20</option>
<option value="25">25</option>
<option value="30">30</option>
<option value="35">35</option>
<option value="40">40</option>
<option value="45">45</option>
<option value="50">50</option>
</select>
<input type="color" class="line-color control" title="font color">
<input type="color" class="background-color control" title="background color">
<button class="control" title="save image">
<i class="fas fa-save"></i>
</button>
<button class="control">
<i class="fas fa-redo" title="clear"></i>
</button>
<button class="control">
<i class="fas fa-paste" title="copy"></i>
</button>
<button class="control">
<i class="fas fa-upload" title="load"></i>
</button>
<button class="control">
<i class="fas fa-trash-alt" title="delete"></i>
</button>
<button class="control">
<i class="fas fa-brush" title="draw"></i>
</button>
<button class="control">
<i class="fas fa-eraser" title="eraser"></i>
</button>
<button class="control">
<i class="fas fa-arrow-left" title="rollback"></i>
</button>
</div>
<div class="modal hide">
<div class="shadow"></div>
<div class="card">
<input type="text" class="download-title" title="title" placeholder="Title">
<select name="mime" id="" class="mime">
<option value="png">png</option>
<option value="jpg">jpg</option>
<option value="jpeg">jpeg</option>
</select>
<a id="download">
<button class="btn-download-confirm">Save</button>
</a>
</div>
</div>
</body>
</html>
main.css
@import url('https:fonts.googleapis.com/css?family=Poppins&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
}
canvas {
background-color: aliceblue;
}
.controls {
height: 50px;
background-color: rebeccapurple;
display: flex;
align-items: center;
justify-content: space-around;
}
.control {
background-color: #f0f8ff;
border-radius: 5px;
height: 40px;
width: 100px;
font-size: 1.9rem;
padding: 0 10px;
cursor: pointer;
transition: .4 ease;
border: none;
box-shadow: 1px 1px 2px 0 rgba(0, 0, 0, .3);
margin: 5px;
}
button.control:hover {
filter: brightness(110%);
}
button.control:active {
transform: scale(.98);
}
.modal {
height: 100vh;
width: 100%;
position: fixed;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
}
.modal.hide {
display: none;
}
.shadow {
height: 100vh;
width: 100%;
position: absolute;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, .3);
}
.modal .card {
z-index: 1;
border: 3px solid #efefef;
}
.modal .card input, select, button {
height: 30px;
border: none;
padding: 0 10px;
transition: .4 ease;
}
.modal .card input:focus {
outline: none;
}
.modal .card button:active {
transform: scale(.98);
}
index.js
// elements
const canvasEl = document.querySelector('canvas')
const statusEl = document.querySelector('.status')
const sizeEl = document.querySelector('.size')
const lineColorEl = document.querySelector('.line-color')
const backgroundColorEl = document.querySelector('.background-color')
const btnControlsEl = document.querySelectorAll('button')
const modalEl = document.querySelector('.modal')
const btnSave = document.getElementById('download')
// variables
const cv = canvasEl.getContext('2d')
let x
let x2
let y
let y2
let isMouseDown = false
let lineColor
let backgroundColor =
'#' +
getComputedStyle(canvasEl)
.backgroundColor.replace(/[^\d,]/g, '')
.split(',')
.map((n) => parseInt(n).toString(16))
.join('')
let lineSize
let eraserOn = false
let routes = []
let drawings = []
// event handlers
window.addEventListener('load', (e) => {
canvasEl.height = window.innerHeight - 50
canvasEl.width = window.innerWidth
console.log(canvasEl.height, canvasEl.width)
backgroundColorEl.value = backgroundColor
})
window.addEventListener('resize', (e) => {
canvasEl.height = window.innerHeight - 50
canvasEl.width = window.innerWidth
console.log('resize: ', canvasEl.height, canvasEl.width)
})
canvasEl.addEventListener('mousedown', (e) => {
x = e.offsetX
y = e.offsetY
isMouseDown = true
})
canvasEl.addEventListener('mousemove', (e) => {
if (isMouseDown) {
x2 = e.offsetX
y2 = e.offsetY
cv.beginPath()
drawLine()
x = x2
y = y2
}
})
canvasEl.addEventListener('mouseup', (e) => {
isMouseDown = false
drawings.push(routes)
routes = []
})
lineColorEl.addEventListener('change', (e) => {
lineColor = e.target.value
})
backgroundColorEl.addEventListener('change', (e) => {
canvasEl.style.backgroundColor = e.target.value
})
sizeEl.addEventListener('change', (e) => {
lineSize = e.target.value
})
btnControlsEl.forEach((btn, idx) => {
btn.addEventListener('click', () => {
switch (idx) {
case 0:
downloadImage()
break
case 1:
cv.clearRect(0, 0, canvasEl.width, canvasEl.height)
routes = []
drawings = []
break
case 2:
saveDrawing()
break
case 3:
loadDrawing()
break
case 4:
deleteDrawing()
break
case 5:
eraserOn = false
break
case 6:
eraserOn = true
break
case 7:
removeLastDrawing()
break
}
console.log(idx)
})
})
btnSave.addEventListener('click', () => {
const fileName = document.querySelector('.download-title').value
const ext = document.querySelector('.mime').value
console.log(fileName + '.' + ext)
btnSave.href = canvasEl.toDataURL(`image/${ext}`, 1.0)
btnSave.download = fileName + '.' + ext
modalEl.classList.add('hide')
})
// functions
function drawLine() {
cv.moveTo(x, y)
cv.lineTo(x2, y2)
cv.strokeStyle = lineColor
cv.lineWidth = lineSize
cv.arc(x, y, lineSize, 0, Math.PI * 2)
cv.fillStyle = lineColor
routes.push({
x: x,
y: y,
color: lineColor,
width: lineSize,
})
if (eraserOn) {
cv.globalCompositeOperation = 'destination-out'
} else {
cv.globalCompositeOperation = 'source-over'
}
cv.stroke()
cv.fill()
}
function downloadImage() {
modalEl.classList.remove('hide')
}
function saveDrawing() {
if (drawings.length > 0) {
localStorage.setItem('drawing', JSON.stringify(drawings))
}
}
function loadDrawing() {
if (localStorage.getItem('drawing')) {
drawings = JSON.parse(localStorage.getItem('drawing'))
redrawLoadedDrawing()
}
}
function deleteDrawing() {
if (localStorage.getItem('drawing')) {
localStorage.removeItem('drawing')
}
}
function redrawLoadedDrawing() {
drawings.forEach((drawing) => {
for (let i = 0; i < drawing.length - 1; i++) {
cv.moveTo(drawing[i].x, drawing[i].y)
cv.lineTo(drawing[i + 1].x, drawing[i + 1].y)
cv.strokeStyle = drawing[i].color
cv.lineWidth = drawing[i].width
cv.arc(drawing[i].x, drawing[i].y, drawing[i].width, 0, Math.PI * 2)
cv.fillStyle = drawing[i].color
cv.stroke()
cv.fill()
}
})
}
function removeLastDrawing() {
cv.globalCompositeOperation = 'destination-out'
if (drawings.length > 0) {
drawings[drawings.length - 1].forEach((route) => {
cv.lineTo(route.x, route.y)
cv.strokeStyle = route.color
cv.lineWidth = route.width
cv.arc(route.x, route.y, route.width, 0, Math.PI * 2)
cv.fillStyle = route.color
cv.stroke()
cv.fill()
})
drawings.pop()
}
cv.globalCompositeOperation = 'source-over'
}
데모
Document
jin-co-jcg.vercel.app
728x90
반응형
'프론트엔드 > 자바스크립트' 카테고리의 다른 글
데이터 타입 - Blob (0) | 2023.01.08 |
---|---|
JavaScript? (0) | 2023.01.08 |
프로젝트 - 계산기 (0) | 2023.01.01 |
클라이언트 측 데이터 저장소 - 인덱스드 디비 (0) | 2022.12.29 |
클라이언트 측 데이터 저장소 - 웹 스토리지 (Web Storage) (0) | 2022.12.29 |