├── .gitignore ├── scripts ├── main.js └── modules │ ├── drawing.js │ └── sketch.js ├── assets ├── icons │ ├── Rectangle.svg │ ├── Ellipse.svg │ ├── Triangle.svg │ ├── Info.svg │ ├── undo.svg │ ├── Load.svg │ ├── minus.svg │ ├── Save.svg │ ├── add.svg │ ├── eraser.svg │ ├── trash.svg │ ├── pencil.svg │ └── T.svg └── styles │ ├── animations.css │ ├── variables.css │ ├── modals.css │ └── main.css ├── .eslintrc.json ├── package.json ├── LICENSE ├── README.md ├── index.html └── main.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode/* -------------------------------------------------------------------------------- /scripts/main.js: -------------------------------------------------------------------------------- 1 | import Sketch from './modules/sketch.js'; 2 | 3 | const sketch = new Sketch('sketch'); 4 | 5 | sketch.Setup(); -------------------------------------------------------------------------------- /assets/icons/Rectangle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icons/Ellipse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icons/Triangle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "parserOptions": { 8 | "ecmaVersion": 12, 9 | "sourceType": "module" 10 | }, 11 | "rules": { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /assets/icons/Info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /assets/icons/undo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /assets/icons/Load.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /assets/icons/minus.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/styles/animations.css: -------------------------------------------------------------------------------- 1 | @keyframes appear { 2 | from {transform: scale(0.2)} 3 | to {transform: scale(1)} 4 | } 5 | 6 | @keyframes jump { 7 | 0% {transform: scale(0.2)} 8 | 50% {transform: scale(1.2)} 9 | 100% {transform: scale(1)} 10 | } 11 | 12 | @keyframes rotate { 13 | from {transform: rotate(0deg)} 14 | to {transform: rotate(-360deg)} 15 | } -------------------------------------------------------------------------------- /assets/icons/Save.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /assets/icons/add.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/styles/variables.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --configs-backgroundColor: #2e3131; 3 | --configs-shadow: rgba(46, 49, 49, 0.6); 4 | --selected-icon: #1c1c1c; 5 | --footer-backgroundColor: #2574a9; 6 | --modal-fontColor: rgb(220, 202, 202); 7 | --modal-borderColor: rgb(150, 90, 44); 8 | --modal-backgroundColor: rgb(27, 89, 27); 9 | --modal-shadow: #a3a3a3; 10 | --modalSaves-deleteBtn-backgroundColor: rgb(41, 41, 41); 11 | --textModal-backgroundColor: #444444; 12 | --modalSave-inputSubmitHover-backgroundColor: black; 13 | --modalSave-inputSubmitHover-fontColor: white; 14 | --modalSave-inputTextFocus-shadow: rgb(17, 58, 17); 15 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "draw", 3 | "version": "1.0.0", 4 | "description": "🖌 Web App que fornece uma ferramenta para desenhar qualquer coisa, seja criativo!", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "webpack --mode development --watch --entry ./scripts/main.js -o ./", 8 | "build": "webpack --mode production --entry ./scripts/main.js -o ./" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/phedrakeson/draw.git" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/phedrakeson/draw/issues" 18 | }, 19 | "homepage": "https://github.com/phedrakeson/draw#readme", 20 | "devDependencies": { 21 | "eslint": "^7.18.0", 22 | "webpack": "^5.17.0", 23 | "webpack-cli": "^4.4.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /assets/icons/eraser.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/icons/trash.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Gabriel Duarte 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /assets/icons/pencil.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Draw 2 | 3 | ![GitHub repo size](https://img.shields.io/github/repo-size/phedrakeson/draw?style=for-the-badge) 4 | ![GitHub language count](https://img.shields.io/github/languages/count/phedrakeson/draw?style=for-the-badge) 5 | ![GitHub forks](https://img.shields.io/github/forks/phedrakeson/draw?style=for-the-badge) 6 | ![Bitbucket open issues](https://img.shields.io/bitbucket/issues/phedrakeson/draw?style=for-the-badge) 7 | ![Bitbucket open pull requests](https://img.shields.io/bitbucket/pr-raw/phedrakeson/draw?style=for-the-badge) 8 | 9 | ![](https://i.gyazo.com/43873228b8f6efd0185ea951b416e0a3.png) 10 | 11 | > Projeto feito utilizando o Canvas nativo, tornando possível desenhar na interface tanto pelo Desktop como pelo Mobile (inclusive com dois dedos!) 12 | 13 | ### Ajustes e melhorias 14 | 15 | O projeto ainda está em desenvolvimento e as próximas atualizações serão voltadas nas seguintes tarefas: 16 | 17 | - [x] Tornar possível desenhar no board 18 | - [x] Possibilidade de desenhar no mobile, reconhecendo mais de um toque. 19 | - [x] Ajuste no suporte mobile 20 | - [x] Painel de configurações 21 | - [x] Possibilidade de limpar o board 22 | - [x] Melhorar UI 23 | - [x] Possibilidade de realizar undo no canvas 24 | 25 | ## 📫 Contribuindo para o Draw 26 | 27 | Para contribuir com o Draw, siga estas etapas: 28 | 29 | 1. Bifurque este repositório. 30 | 2. Crie um branch: `git checkout -b `. 31 | 3. Faça suas alterações e confirme-as: `git commit -m ''` 32 | 4. Envie para o branch original: `git push origin / ` 33 | 5. Crie a solicitação de pull. 34 | 35 | Como alternativa, consulte a documentação do GitHub em [como criar uma solicitação pull](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request). 36 | 37 | 38 | ## 📝 Licença 39 | 40 | Esse projeto está sob licença. Veja o arquivo [LICENÇA](LICENSE.md) para mais detalhes. 41 | -------------------------------------------------------------------------------- /assets/icons/T.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/styles/modals.css: -------------------------------------------------------------------------------- 1 | .desappear {display: none} 2 | 3 | .save { 4 | font-family: 'Parisienne', cursive; 5 | display: inline; 6 | font-size: 1pc; 7 | margin: 5px; 8 | transition: 300ms; 9 | cursor: pointer; 10 | } 11 | 12 | .modal { 13 | color: var(--modal-fontColor); 14 | border: 9px solid var(--modal-borderColor); 15 | background-color: var(--modal-backgroundColor); 16 | border-radius: 3rem; 17 | animation: appear 400ms forwards; 18 | border-radius: .3rem; 19 | text-align: center; 20 | position: absolute; 21 | box-shadow: 2px 2px 2px 2px var(--modal-shadow); 22 | } 23 | 24 | .delete { 25 | color: var(--modalSaves-deleteBtn-backgroundColor); 26 | border-radius: 4rem; 27 | padding: 1px 4px; 28 | margin: 5px; 29 | height: 3vh; 30 | } 31 | 32 | .save-box { 33 | transition: 300ms; 34 | align-items: center; 35 | margin: 10px; 36 | } 37 | 38 | .save-box:hover, #modalSave input[type="submit"]:hover, #modalSave input[type="text"]:hover {transform: scale(1.05)} 39 | 40 | .textModal { 41 | animation: appear 300ms; 42 | background-color: var(--textModal-backgroundColor); 43 | padding: 5px; 44 | border-radius: .4rem; 45 | display: flex; 46 | align-items: center; 47 | position: absolute; 48 | z-index: 99; 49 | flex-direction: column; 50 | } 51 | 52 | .textModal input { 53 | max-width: 200px; 54 | max-height: 170px; 55 | margin: 5px; 56 | border-radius: .3rem; 57 | padding: 3px; 58 | } 59 | 60 | .textModal input::placeholder {text-align: center} 61 | 62 | #modalInfo { 63 | z-index: 99; 64 | width: 50vw; 65 | } 66 | 67 | #modalInfo div {margin: 20px 0} 68 | 69 | #modalInfo h1 {font-size: 2pc} 70 | 71 | #modalInfo ul li { 72 | font-size: 1pc; 73 | margin: 10px 0; 74 | } 75 | 76 | #modalSaves { 77 | top: 90px; 78 | right: 10px; 79 | } 80 | 81 | #modalSave {padding: 5px 10px 10px 10px} 82 | 83 | #modalSave h2 {margin: 5px 0 10px 0} 84 | 85 | #modalSave input[type="text"] { 86 | padding: 4px; 87 | margin-right: 10px; 88 | border-radius: .4rem; 89 | } 90 | 91 | #modalSave input[type="submit"] { 92 | padding: 4px; 93 | border-radius: .4rem; 94 | } 95 | 96 | #modalSave input[type="text"]:focus {box-shadow: 0px 2px 5px var(--modalSave-inputTextFocus-shadow)} 97 | 98 | #modalSave input[type="submit"]:hover { 99 | background-color: var(--modalSave-inputSubmitHover-backgroundColor); 100 | color: var(--modalSave-inputSubmitHover-fontColor); 101 | } -------------------------------------------------------------------------------- /assets/styles/main.css: -------------------------------------------------------------------------------- 1 | @import url('./modals.css'); 2 | @import url('./animations.css'); 3 | @import url('./variables.css'); 4 | 5 | * { 6 | padding: 0; 7 | margin: 0; 8 | box-sizing: border-box; 9 | text-decoration: none; 10 | list-style: none; 11 | outline: none; 12 | } 13 | 14 | html { 15 | font-size: 62.5%; 16 | font-family: 'Nunito', sans-serif; 17 | } 18 | 19 | img { 20 | max-width: 100%; 21 | display: block; 22 | } 23 | 24 | body { 25 | position: relative; 26 | min-height: 100vh; 27 | display: flex; 28 | align-items: center; 29 | justify-content: center; 30 | } 31 | 32 | input, button { 33 | border: none; 34 | cursor: pointer; 35 | transition: 300ms; 36 | outline: none; 37 | } 38 | 39 | #sketch {background: #eee} 40 | 41 | #configs { 42 | width: 100%; 43 | height: 80px; 44 | background: var(--configs-backgroundColor); 45 | overflow: hidden; 46 | position: absolute; 47 | top: 0; 48 | left: 0; 49 | box-shadow: 5px 0px 10px 0px var(--configs-shadow); 50 | padding: 15px 60px 15px 60px; 51 | align-items: center; 52 | justify-content: flex-start; 53 | gap: 10px; 54 | } 55 | 56 | #width-panel { 57 | max-width: 120px; 58 | padding: 10px; 59 | border-radius: .4rem; 60 | border: 1.5px solid rgba(238, 238, 238, 0.2); 61 | gap: 15px; 62 | justify-content: center; 63 | } 64 | 65 | .other-tools { 66 | border-radius: .4rem; 67 | margin-left: 20px; 68 | background-color: #272727; 69 | padding: 5px; 70 | } 71 | 72 | .other-tools img {margin: 5px} 73 | 74 | #configs, #width-panel, .other-tools { 75 | display: flex; 76 | align-items: center; 77 | } 78 | 79 | #increase, #decrease { 80 | width: 24px; 81 | height: 24px; 82 | cursor: pointer; 83 | } 84 | 85 | .icon { 86 | padding: 5px; 87 | width: 35px; 88 | height: 35px; 89 | cursor: pointer; 90 | background-size: cover; 91 | } 92 | 93 | .selected { 94 | animation: jump 250ms forwards; 95 | background-color: var(--selected-icon); 96 | border-radius: .4rem; 97 | } 98 | 99 | #widthValue { 100 | color: #eeee; 101 | font-size: 2.4rem; 102 | } 103 | 104 | label { 105 | color: white; 106 | font-size: 1pc; 107 | margin-left: 10px; 108 | } 109 | 110 | #color, #canvas-color { 111 | width: 30px; 112 | height: 30px; 113 | margin: 20px 20px 20px 0; 114 | } 115 | 116 | @media (max-width: 375px) { 117 | #configs { 118 | padding: 15px; 119 | } 120 | } 121 | 122 | #footer { 123 | width: 100%; 124 | padding: 10px; 125 | background-color: var(--footer-backgroundColor); 126 | font-size: 1.6rem; 127 | color: #eee; 128 | bottom: 0; 129 | left: 0; 130 | text-align: center; 131 | position: absolute; 132 | } 133 | 134 | #undo.icon:hover {animation: rotate 500ms;} -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Draw 11 | 12 | 13 | 14 | 40 | 56 | 63 | 64 |
65 |

Desenvolvido com orgulho por Gabriel Duarte 🤘🏽

66 |
67 | 68 | 69 | -------------------------------------------------------------------------------- /scripts/modules/drawing.js: -------------------------------------------------------------------------------- 1 | export default class Drawing { 2 | constructor(canvas) { 3 | this.canvas = document.getElementById(canvas) 4 | this.context = this.canvas.getContext('2d') 5 | } 6 | 7 | UpdateCanvasSize() { 8 | this.canvas.width = window.innerWidth; 9 | this.canvas.height = window.innerHeight; 10 | } 11 | 12 | DrawLine(initialX, initialY, x, y, size, color) { 13 | this.context.lineWidth = size * 2; 14 | this.context.beginPath(); 15 | this.context.moveTo(initialX, initialY); 16 | this.context.lineTo(x, y); 17 | this.context.strokeStyle = color 18 | this.context.stroke() 19 | } 20 | 21 | DrawCircle(x, y, size, color) { 22 | this.context.beginPath() 23 | this.context.arc(x, y, size, 0, Math.PI * 2); 24 | this.context.fillStyle = color; 25 | this.context.fill() 26 | } 27 | 28 | DrawText(text, x, y, color, size, font = "sans-serif") { 29 | this.context.beginPath(); 30 | this.context.font = `${size * 2}px ${font}`; 31 | this.context.fillStyle = color; 32 | this.context.fillText(text, x - size / 2 * text.length, y + size / 2); 33 | } 34 | 35 | DrawTriangle(x, y, color, size) { 36 | this.context.beginPath(); 37 | this.context.moveTo(x, y - size); 38 | this.context.lineTo(x - size, y + size); 39 | this.context.lineTo(x + size, y + size); 40 | this.context.closePath(); 41 | this.context.fillStyle = color; 42 | this.context.fill() 43 | } 44 | 45 | DrawRect(x, y, size, color) { 46 | const _size = size * 2; 47 | this.context.fillStyle = color; 48 | this.context.fillRect(x - _size / 2, y - _size / 2, _size, _size); 49 | } 50 | 51 | DrawPolygon(type, x, y, color, size) { 52 | switch (type) { 53 | case "square": 54 | this.DrawRect(x, y, size, color); 55 | break; 56 | 57 | case "triangle": 58 | this.DrawTriangle(x, y, color, size); 59 | break; 60 | 61 | case "circle": 62 | this.DrawCircle(x, y, size, color) 63 | break; 64 | } 65 | } 66 | 67 | ReDraw({states, type, colorStyle, erasedStates}) { 68 | switch (type) { 69 | case "sketch": 70 | states.forEach(state => { 71 | this.DrawCircle(state.x, state.y, state.size, colorStyle) 72 | this.DrawLine(state.x, state.y, state.prevX, state.prevY, state.size, colorStyle); 73 | }); 74 | break; 75 | 76 | case "polygon": 77 | this.DrawPolygon(states.id, states.x, states.y, colorStyle, states.size) 78 | break; 79 | 80 | case "text": 81 | this.DrawText(states.text, states.x, states.y, colorStyle, states.size) 82 | break; 83 | } 84 | if(erasedStates.length > 0) erasedStates.forEach(erasedState => this.context.clearRect(erasedState.x, erasedState.y, erasedState.size, erasedState.size)); 85 | } 86 | } -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | (()=>{"use strict";new class{constructor(t){this.canvas=document.getElementById(t),this.context=this.canvas.getContext("2d"),this.pressed=!1,this.x=void 0,this.y=void 0,this.ongoingTouches=new Array,this.handleStart=this.handleStart.bind(this),this.handleEnd=this.handleEnd.bind(this),this.handleMove=this.handleMove.bind(this),this.handleCancel=this.handleCancel.bind(this),this.ongoingTouchIndexById=this.ongoingTouchIndexById.bind(this),this.copyTouch=this.copyTouch.bind(this),this.adjustLine=this.adjustLine.bind(this),this.canvas.width=window.innerWidth,this.canvas.height=window.innerHeight}mouseRecognitions(){this.canvas.addEventListener("mousedown",(t=>{this.pressed=!0,this.x=t.offsetX,this.y=t.offsetY})),this.canvas.addEventListener("mouseup",(()=>{this.pressed=!1,this.x=void 0,this.y=void 0})),this.canvas.addEventListener("mousemove",(t=>{if(this.pressed){const e=t.offsetX,i=t.offsetY;this.drawCircle(e,i),this.drawLine(this.x,this.y,e,i),this.x=e,this.y=i}}))}touchRecognitions(){this.canvas.addEventListener("touchstart",this.handleStart,!1),this.canvas.addEventListener("touchend",this.handleEnd,!1),this.canvas.addEventListener("touchmove",this.handleMove,!1),this.canvas.addEventListener("touchcancel",this.handleCancel,!1),this.canvas.addEventListener("touchleave",this.handleEnd,!1)}handleStart(t){const e=t.changedTouches;t.preventDefault();for(let t=0;t=0?(this.drawLine(n,s),this.drawLine(this.x,this.y,n,s),this.ongoingTouches.splice(i,1,this.copyTouch(e[t]))):console.error("Can't figure out which touch to continue.")}}handleEnd(t){t.preventDefault();const e=t.changedTouches;for(let t=0;t=0?(this.context.beginPath(),this.ongoingTouches.splice(i,1)):console.error("Can't figure out which touch to end.")}}handleCancel(t){t.preventDefault();const e=t.changedTouches;for(let t=0;t{this.value-=2,this.value<=0&&(this.value=2),this.lineWidthElement.innerText=this.value,this.updateWidthValueOnScreen(this.value)})),this.increaseBtn.addEventListener("click",(()=>{this.value+=2,this.value>=20&&(this.value=20),this.innerText=this.value,this.updateWidthValueOnScreen(this.value)}))}adjustColor(){this.color=document.getElementById("color"),this.color.addEventListener("change",(t=>{this.color=t.target.value}))}updateWidthValueOnScreen(t){this.lineWidthElement.innerText=t}drawLine(t,e,i,n){this.context.lineWidth=2*this.lineWidthElement.innerText,this.context.beginPath(),this.context.moveTo(t,e),this.context.lineTo(i,n),this.context.strokeStyle=this.color,this.context.stroke()}drawCircle(t,e){this.context.beginPath(),this.context.arc(t,e,this.lineWidthElement.innerText,0,2*Math.PI),this.context.fillStyle=this.color,this.context.fill()}clearBoard(){document.getElementById("trash").addEventListener("click",(()=>{this.context.clearRect(0,0,this.canvas.width,this.canvas.height)}))}}("sketch").setup()})(); -------------------------------------------------------------------------------- /scripts/modules/sketch.js: -------------------------------------------------------------------------------- 1 | import Drawing from "./drawing.js"; 2 | 3 | export default class Sketch extends Drawing { 4 | constructor(canvas) { 5 | super(canvas); 6 | this.pressed = false; 7 | this.x = undefined; 8 | this.y = undefined; 9 | this.lineWidthLimit = 76; 10 | this.ongoingTouches = new Array; 11 | this.paths = new Array; 12 | this.states = new Array; 13 | this.color = "#000000" 14 | this.value = 2 15 | this.user_sketches = JSON.parse(localStorage.getItem("user_sketches")) || [] 16 | this.handleStart = this.handleStart.bind(this); 17 | this.handleEnd = this.handleEnd.bind(this); 18 | this.handleMove = this.handleMove.bind(this); 19 | this.handleCancel = this.handleCancel.bind(this); 20 | this.ongoingTouchIndexById = this.ongoingTouchIndexById.bind(this); 21 | this.copyTouch = this.copyTouch.bind(this); 22 | this.UpdateWidthValueOnScreen(); 23 | this.UpdateSavesModal(); 24 | this.UpdateCanvasSize(); 25 | } 26 | 27 | //#region Updates functions 28 | UpdateSavesModal() { 29 | const modalSaves = document.getElementById('modalSaves'); 30 | modalSaves.innerHTML = ""; 31 | if (this.user_sketches.length > 0) 32 | this.user_sketches.map(sketch => modalSaves.innerHTML += `

${sketch.title.length > 7 ? sketch.title.slice(0, 7).trim() + "..." : sketch.title}

`); 33 | else 34 | modalSaves.innerHTML = `

You have no saves !

`; 35 | document.querySelectorAll('.save').forEach(save => save.addEventListener('click', event => this.Save_Handle(event))); 36 | document.querySelectorAll('.delete').forEach(deleteBtn => deleteBtn.addEventListener('click', event => this.DeleteSave_Handle(event))); 37 | } 38 | 39 | UpdateWidthValueOnScreen() { 40 | document.getElementById('widthValue').innerText = this.value; 41 | } 42 | //#endregion 43 | 44 | //#region Screen recognitions 45 | MouseRecognitions() { 46 | this.canvas.addEventListener('mousedown', event => { 47 | if (document.getElementById('text').classList.contains("selected")) this.CreateTextModal(event.offsetX, event.offsetY); 48 | else if (document.querySelector(".polygon.selected")) { 49 | const id = document.querySelector(".polygon.selected").id; 50 | this.paths.push({ states: {id, x: event.offsetX, y: event.offsetY, size: this.value}, type: "polygon", colorStyle: this.color, erasedStates: new Array()}); 51 | this.DrawPolygon(id, event.offsetX, event.offsetY, this.color, this.value); 52 | } 53 | else { 54 | this.pressed = true; 55 | this.x = event.offsetX; 56 | this.y = event.offsetY; 57 | } 58 | }) 59 | 60 | this.canvas.addEventListener('mouseup', () => { 61 | if (this.states.length > 0) { 62 | this.paths.push({ states: new Array(...this.states), type: "sketch", colorStyle: this.color, erasedStates: new Array()}); 63 | this.states = []; 64 | } 65 | this.pressed = false; 66 | this.x = undefined; 67 | this.y = undefined; 68 | }) 69 | 70 | this.canvas.addEventListener('mousemove', event => { 71 | if(this.pressed) { 72 | const x2 = event.offsetX; 73 | const y2 = event.offsetY; 74 | if (document.getElementById('eraser').classList.contains("selected")) { 75 | const size = this.value * 4; 76 | this.paths[this.paths.length - 1].erasedStates.push({x: x2 - size / 2, y: y2 - size / 2, size}); 77 | this.context.clearRect(x2 - size / 2, y2 - size / 2, size, size); 78 | } else { 79 | this.DrawCircle(x2, y2, this.value, this.color) 80 | this.DrawLine(this.x, this.y, x2, y2, this.value, this.color); 81 | this.states.push({x: x2, y: y2, prevX: this.x, prevY: this.y, size: this.value}); 82 | this.x = x2; 83 | this.y = y2; 84 | } 85 | } 86 | }) 87 | } 88 | 89 | touchRecognitions() { 90 | this.canvas.addEventListener('touchstart', this.handleStart, true); 91 | this.canvas.addEventListener('touchend', this.handleEnd, true); 92 | this.canvas.addEventListener('touchmove', this.handleMove, false); 93 | this.canvas.addEventListener('touchcancel', this.handleCancel, false); 94 | this.canvas.addEventListener('touchleave', this.handleEnd, false) 95 | } 96 | //#endregion 97 | 98 | //#region Click/Key handlers 99 | Undo_Handle() { 100 | this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); 101 | this.paths.forEach((path, index) => { 102 | if (index < this.paths.length - 1) 103 | this.ReDraw(path); 104 | else 105 | this.paths.pop(); 106 | }) 107 | } 108 | 109 | DecreaseBtn_Handle() { 110 | this.value -= 2; 111 | if(this.value <= 0) this.value = 2; 112 | this.UpdateWidthValueOnScreen(); 113 | } 114 | 115 | IncreaseBtn_Handle() { 116 | this.value += 2; 117 | if(this.value >= this.lineWidthLimit) this.value = this.lineWidthLimit; 118 | this.UpdateWidthValueOnScreen(); 119 | } 120 | 121 | DeleteSave_Handle(e) { 122 | this.user_sketches = this.user_sketches.filter(save => save.title != e.target.getAttribute("key")); 123 | localStorage.setItem("user_sketches", JSON.stringify(this.user_sketches)) 124 | this.UpdateSavesModal(); 125 | } 126 | 127 | Save_Handle(e) { 128 | this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); 129 | const selectedSketch = this.user_sketches.find(save => save.title === e.target.getAttribute("key")); 130 | this.paths = selectedSketch.paths; 131 | this.canvas.style.backgroundColor = selectedSketch.backgroundColor; 132 | selectedSketch.paths.map(path => this.ReDraw(path)); 133 | } 134 | 135 | FormSave_Handle(e) { 136 | e.preventDefault(); 137 | const title = e.target[0].value.trim(); 138 | if (this.user_sketches.some(sketch => sketch.title == title)) { 139 | if (confirm(`This title is already been used by another sketch, do you want to update the sketch with the same name ?`)) 140 | this.user_sketches = this.user_sketches.filter(sketch => sketch.title != title); 141 | else return; 142 | } 143 | this.user_sketches.push({paths: this.paths, backgroundColor: this.canvas.style.backgroundColor, title}) 144 | localStorage.setItem("user_sketches", JSON.stringify(this.user_sketches)) 145 | e.target.reset(); 146 | document.getElementById('modalSave').classList.toggle("desappear") 147 | this.UpdateSavesModal(); 148 | } 149 | 150 | CanvasClear_Handle() { 151 | this.states = []; 152 | this.paths = []; 153 | this.canvas.style.backgroundColor = "#EEEE"; 154 | this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); 155 | } 156 | 157 | FormText_Handle(e, x, y) { 158 | e.preventDefault(); 159 | this.DrawText(e.target[0].value, x, y, this.color, this.value); 160 | this.paths.push({states: { text: e.target[0].value, x, y, size: this.value}, type: "text", colorStyle: this.color, erasedStates: new Array()}); 161 | e.target.remove(); 162 | } 163 | //#endregion 164 | 165 | // #region Mobile drawing handlers 166 | handleStart(event) { 167 | const touches = event.changedTouches; 168 | event.preventDefault(); 169 | 170 | for(let i = 0; i < touches.length; i++) { 171 | this.ongoingTouches.push(this.copyTouch(touches[i])); 172 | } 173 | } 174 | 175 | handleMove(event) { 176 | event.preventDefault(); 177 | const touches = event.changedTouches; 178 | 179 | for(let i = 0; i < touches.length; i++) { 180 | const idx = this.ongoingTouchIndexById(touches[i].identifier); 181 | this.x = this.ongoingTouches[idx].clientX; 182 | this.y = this.ongoingTouches[idx].clientY; 183 | const x2 = touches[i].clientX; 184 | const y2 = touches[i].clientY; 185 | 186 | if(idx >= 0) { 187 | this.DrawLine(this.x, this.y, x2, y2, this.value, this.color) 188 | this.DrawCircle(this.x, this.y, this.value, this.color) 189 | this.ongoingTouches.splice(idx, 1, this.copyTouch(touches[i])); 190 | } else console.error("Can't figure out which touch to continue."); 191 | } 192 | } 193 | 194 | handleEnd(event) { 195 | event.preventDefault(); 196 | const touches = event.changedTouches; 197 | 198 | for(let i = 0; i < touches.length; i++) { 199 | const idx = this.ongoingTouchIndexById(touches[i].identifier); 200 | if(idx >= 0) { 201 | this.context.beginPath(); 202 | this.ongoingTouches.splice(idx, 1); 203 | } else { 204 | console.error("Can't figure out which touch to end.") 205 | } 206 | } 207 | } 208 | 209 | handleCancel(event) { 210 | event.preventDefault(); 211 | const touches = event.changedTouches; 212 | 213 | for(let i = 0; i < touches.length; i++) 214 | this.ongoingTouches.splice(i, 1); 215 | } 216 | 217 | copyTouch(touch) { 218 | return { identifier: touch.identifier, clientX: touch.clientX, clientY: touch.clientY }; 219 | } 220 | 221 | ongoingTouchIndexById(idToFind) { 222 | for (var i=0; i < this.ongoingTouches.length; i++) { 223 | var id = this.ongoingTouches[i].identifier; 224 | 225 | if (id == idToFind) return i; 226 | } 227 | return -1; 228 | } 229 | //#endregion 230 | 231 | Setup() { 232 | this.AddEventListeners(); 233 | this.MouseRecognitions(); 234 | this.touchRecognitions(); 235 | } 236 | 237 | AddEventListeners() { 238 | document.getElementById('canvas-color').addEventListener('input', event => this.canvas.style.backgroundColor = event.target.value); 239 | document.getElementById('color').addEventListener('input', event => this.color = event.target.value); 240 | document.getElementById('decrease').addEventListener('click', () => this.DecreaseBtn_Handle()); 241 | document.getElementById('increase').addEventListener('click', () => this.IncreaseBtn_Handle()); 242 | document.getElementById('trash').addEventListener('click', () => this.CanvasClear_Handle()); 243 | document.getElementById('undo').addEventListener('click', () => this.Undo_Handle()); 244 | document.getElementById('formSave').addEventListener('submit', event => this.FormSave_Handle(event)); 245 | document.getElementById("save").addEventListener('click', () => document.getElementById('modalSave').classList.toggle("desappear")); 246 | document.getElementById("load").addEventListener('click', () => document.getElementById('modalSaves').classList.toggle("desappear")); 247 | document.getElementById("info").addEventListener('click', () => document.getElementById('modalInfo').classList.toggle("desappear")); 248 | window.addEventListener('resize', () => this.UpdateCanvasSize()); 249 | document.querySelectorAll('.icon.selectable').forEach(icon => icon.addEventListener('click', event => this.ToggleTools(event))); 250 | window.addEventListener('keydown', e => { 251 | switch (e.code) { 252 | case "Numpad1": 253 | document.getElementById('modalInfo').classList.toggle("desappear"); 254 | break; 255 | 256 | case "Numpad2": 257 | this.CanvasClear_Handle() 258 | break; 259 | 260 | case "Numpad3": 261 | this.DecreaseBtn_Handle(); 262 | break; 263 | 264 | case "Numpad4": 265 | this.IncreaseBtn_Handle(); 266 | break; 267 | 268 | case "Numpad5": 269 | this.Undo_Handle(); 270 | break; 271 | 272 | case "Numpad6": 273 | this.UpdateSavesModal(); 274 | document.getElementById('modalSaves').classList.toggle("desappear"); 275 | break; 276 | 277 | case "Numpad7": 278 | document.getElementById('modalSave').classList.toggle("desappear") 279 | break; 280 | 281 | default: 282 | break; 283 | } 284 | }) 285 | } 286 | 287 | ToggleTools(e) { 288 | document.querySelector(".icon.selected").classList.remove("selected"); 289 | e.target.classList.add("selected"); 290 | } 291 | 292 | CreateTextModal(x, y) { 293 | this.pressed = false; 294 | const txtModal = document.createElement('form') 295 | txtModal.innerHTML = ``; 296 | document.body.appendChild(txtModal); 297 | txtModal.children[1].addEventListener('click', () => txtModal.remove()); 298 | txtModal.children[0].addEventListener('input', e => e.target.setAttribute("style", `background-color: ${this.canvas.style.backgroundColor}; font-size: ${this.value * 2}px; color: ${this.color}`)); 299 | txtModal.addEventListener('submit', e => this.FormText_Handle(e, x, y)); 300 | txtModal.setAttribute('class', "textModal"); 301 | txtModal.setAttribute('style', `top: ${y - txtModal.clientHeight / 2}px; left: ${x - txtModal.clientWidth / 2}px`); 302 | } 303 | } --------------------------------------------------------------------------------