├── retro.png ├── favicon.png ├── game_over.ttf ├── game_over.woff ├── README.md ├── index.html ├── main.css └── script.js /retro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bscottnz/esketch/HEAD/retro.png -------------------------------------------------------------------------------- /favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bscottnz/esketch/HEAD/favicon.png -------------------------------------------------------------------------------- /game_over.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bscottnz/esketch/HEAD/game_over.ttf -------------------------------------------------------------------------------- /game_over.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bscottnz/esketch/HEAD/game_over.woff -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Retro-Sketch: Pixel Sketch App 2 | 3 | Create your own pixel art. 4 | 5 | - Select any colour for the pen or background. 6 | - Grab used colours from the canvas. 7 | - Colour fill tool fills in shapes with selected colour. 8 | - Rainbow pen colours each cell a random colour. 9 | - Apply shading / lightening that persists over background colour changes. 10 | - Create a grid size up to 60 x 60. 11 | 12 | Feature ideas to implement 13 | 14 | - Save images. 15 | - Export images. 16 | - Undo tool. 17 | 18 | [Live App](https://bscottnz.github.io/esketch/) 19 | 20 | ![alt text](https://raw.githubusercontent.com/bscottnz/esketch/main/retro.png "App Preview") 21 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Retro-Sketch 7 | 8 | 9 | 10 | 11 |
12 |
13 |

RETRO-SKETCH

14 |
15 |
16 | 17 | 18 |
19 | 20 |
21 |
22 | 23 |
24 | Pen Colour 25 |
26 |
27 |
28 | 29 |
30 | Background Colour 31 |
32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
44 | 46 |
47 |
48 | 49 | 50 |

Grid size: 24 x 24

51 | 52 | 53 | 54 | 55 |
56 |
57 |
58 |
59 |
60 |
61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /main.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap'); 2 | 3 | /* font from https://www.dafont.com/game-over.font */ 4 | @font-face { 5 | font-family: gameover; 6 | src: url('game_over.woff') format('woff'), 7 | url('game_over.ttf') format('ttf'); 8 | } 9 | 10 | * { 11 | box-sizing: border-box; 12 | margin: 0; 13 | padding: 0; 14 | } 15 | 16 | :root { 17 | --bg-color: #ffffff; 18 | } 19 | 20 | /* this is the class that is applied to the grid when it is cleared. 21 | after 1.5 seconds the class is removed */ 22 | 23 | /* multiple classes so that some pixels will fade faster than others */ 24 | .clear-fade { 25 | background-color: var(--bg-color) !important; 26 | transition: background-color 1.5s ease; 27 | } 28 | 29 | .clear-fade-2 { 30 | background-color: var(--bg-color) !important; 31 | transition: background-color 1.4s ease; 32 | } 33 | 34 | .clear-fade-3 { 35 | background-color: var(--bg-color) !important; 36 | transition: background-color 1.3s ease; 37 | } 38 | 39 | .clear-fade-4 { 40 | background-color: var(--bg-color) !important; 41 | transition: background-color 1.2s ease; 42 | } 43 | 44 | .clear-fade-5 { 45 | background-color: var(--bg-color) !important; 46 | transition: background-color 1.1s ease; 47 | } 48 | 49 | .clear-fade-6 { 50 | background-color: var(--bg-color) !important; 51 | transition: background-color 1.0s ease; 52 | } 53 | 54 | 55 | body { 56 | background-color: #181818; 57 | } 58 | 59 | .header { 60 | margin-top: 50px; 61 | color: rgb(62, 166, 255); 62 | font-size: 100px; 63 | text-align: center; 64 | font-family: gameover; 65 | } 66 | 67 | .grid-wrapper { 68 | display: flex; 69 | justify-content: center; 70 | align-items: center; 71 | width: 660px; 72 | height: 660px; 73 | background-color: #202020; 74 | border-radius: 4px; 75 | } 76 | 77 | .grid-container { 78 | /* box-sizing: content-box; */ 79 | display: grid; 80 | height: 600px; 81 | width: 600px; 82 | /* background-color: rgb(156, 156, 156); */ 83 | border: 1px solid rgb(156, 156, 156); 84 | /* border-radius: 4px; */ 85 | /* margin-left: auto; 86 | margin-right: auto; */ 87 | } 88 | 89 | .wrapper { 90 | max-width: 1400px; 91 | margin-left: auto; 92 | margin-right: auto; 93 | } 94 | .main { 95 | display: flex; 96 | justify-content: space-evenly; 97 | } 98 | 99 | input[type="color"] { 100 | display: block; 101 | outline: none; 102 | border: none; 103 | /* border: 1px solid rgb(62, 166, 255); */ 104 | background-color: rgba(0,0,0,0); 105 | border-radius: 2px; 106 | width: 200%; 107 | /* padding: 4px 8px; */ 108 | height: 200%; 109 | transform: translateX(-10px) translateY(-10px); 110 | 111 | } 112 | 113 | .color-box { 114 | height: 36px; 115 | overflow: hidden; 116 | border-radius: 2px; 117 | border: 1px solid rgb(62, 166, 255); 118 | width: 35%; 119 | margin-bottom: 10px; 120 | 121 | /* transform: translateY(-10px); */ 122 | 123 | 124 | } 125 | 126 | .color-card { 127 | font-family: roboto; 128 | font-weight: 500; 129 | font-size: 14px; 130 | color: rgb(62, 166, 255); 131 | display: flex; 132 | position: relative; 133 | 134 | } 135 | 136 | .color-card span { 137 | margin-left: 9px; 138 | position: absolute; 139 | top: 10px; 140 | left: 88px; 141 | 142 | } 143 | 144 | 145 | 146 | button { 147 | display: block; 148 | color: rgb(62, 166, 255); 149 | font-weight: 500; 150 | font-family: roboto; 151 | background-color: rgba(0,0,0,0); 152 | font-size: 14px; 153 | outline: none; 154 | border: 1px solid rgb(62, 166, 255); 155 | border-radius: 2px; 156 | padding: 9px 15px; 157 | width: 100%; 158 | margin-bottom: 10px; 159 | } 160 | 161 | .cc2 { 162 | margin-bottom: 25px; 163 | } 164 | 165 | #color-grabber { 166 | margin-bottom: 34px; 167 | } 168 | 169 | #lighten-btn { 170 | margin-bottom: 31px; 171 | } 172 | 173 | .slider-box { 174 | margin-bottom: 10px; 175 | } 176 | 177 | .grid-size { 178 | font-family: roboto; 179 | font-weight: 500; 180 | font-size: 14px; 181 | color: rgb(62, 166, 255); 182 | text-align: center; 183 | margin-bottom: 10px; 184 | } 185 | 186 | #grid-btn { 187 | margin-bottom: 30px ; 188 | } 189 | 190 | #clear-grid { 191 | margin-bottom: 0px; 192 | } 193 | 194 | .btn-on { 195 | background-color: rgb(62, 166, 255); 196 | color: #202020; 197 | } 198 | 199 | .grid-item { 200 | /* background-color: rgb(255, 255, 255); */ 201 | user-select: none; 202 | } 203 | 204 | .border-top-left { 205 | border-top: 1px solid rgb(156, 156, 156); 206 | border-left: 1px solid rgb(156, 156, 156); 207 | } 208 | 209 | .border-right { 210 | border-right: 1px solid rgb(156, 156, 156); 211 | } 212 | 213 | .border-bottom { 214 | border-bottom: 1px solid rgb(156, 156, 156); 215 | } 216 | 217 | .slider-box { 218 | position: relative; 219 | width: 100%; 220 | } 221 | 222 | #range-value { 223 | /* position: absolute; 224 | top: 2px; 225 | right: 0px; 226 | padding: 2px; 227 | width: 40px; */ 228 | /* background: #fff; */ 229 | text-align: center; 230 | } 231 | 232 | /* #range-value::before { 233 | content: ""; 234 | position: absolute; 235 | top: 50%; 236 | transform: translateY(-50%) rotate(45deg); 237 | left: -5px; 238 | width: 10px; 239 | height: 10px; 240 | background: #fff; 241 | } */ 242 | 243 | .main { 244 | margin-top: 60px; 245 | margin-bottom: 60px; 246 | } 247 | 248 | .controls { 249 | width: 300px; 250 | background-color: #202020; 251 | border-radius: 4px; 252 | padding: 30px; 253 | } 254 | input[type="range"] { 255 | width: 100%; 256 | height: 2px; 257 | background: #404040; 258 | appearance: none; 259 | outline: none; 260 | border-radius: 2px; 261 | } 262 | 263 | input[type="range"]::-webkit-slider-thumb { 264 | appearance: none; 265 | width: 20px; 266 | height: 20px; 267 | border-radius: 50%; 268 | background: rgb(62, 166, 255); 269 | } 270 | 271 | #progress-bar { 272 | width: 40%; 273 | height: 2px; 274 | background-color: rgb(62, 166, 255); 275 | border-radius: 2px; 276 | position: absolute; 277 | top: 12px; 278 | left: 0; 279 | } 280 | 281 | 282 | 283 | -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | //############################################################################## 2 | // After revisting this project, I was thinking whether or not I should clean up 3 | // the code. I decided to leave it as it was so I would have something to look 4 | // back on. So if you're reading this and it seems messy, I'm sorry but my 5 | // later projects get a lot cleaner and organised. 6 | //############################################################################## 7 | 8 | // number of rows / columns in grid 9 | let gridSize = 24; 10 | 11 | const container = document.querySelector('.grid-container'); 12 | 13 | let bgColor = '#ffffff'; 14 | container.style.backgroundColor = bgColor; 15 | 16 | //create new grid items to fill the grid 17 | function createGrid() { 18 | // having the grid with each item at 1fr would leave left over space at the end of the grid 19 | // when there were lots of items, doing it this way seemed to fill in that extra space. 20 | // however the grid broke when there were 3 or less items, so the if statment fixes that 21 | let gridWidth = container.offsetWidth / gridSize; 22 | container.style.gridTemplateColumns = `repeat(${gridSize - 3}, ${gridWidth}px) 1fr 1fr 1fr`; 23 | container.style.gridTemplateRows = `repeat(${gridSize - 3}, ${gridWidth}px) 1fr 1fr 1fr`; 24 | if (gridSize < 4) { 25 | container.style.gridTemplateColumns = `repeat(${gridSize},1fr`; 26 | container.style.gridTemplateRows = `repeat(${gridSize}, 1fr`; 27 | } 28 | 29 | for (let i = 0; i < gridSize ** 2; i++) { 30 | const square = document.createElement('div'); 31 | square.classList.add('grid-item'); 32 | square.setAttribute('draggable', 'false'); 33 | square.style.backgroundColor = bgColor; 34 | container.appendChild(square); 35 | 36 | //to avoid double borders, top and left borders are applied to every cell, 37 | //then the remaining borderless cells are determined and given a border. 38 | // i originally used grid gap and had the container background 39 | //color as the borders. However when i changed the background color by changing each 40 | // cell individually, it got quite slow with large grids. I changed un-colored 41 | // grid items to be transperent, so i could use the container background as the 42 | //background color. now when i change the background color i am only changing the 43 | // one container and it is much faster. 44 | //set border top and left to every grid item 45 | 46 | //! Pav: changed transparent color to bgColor to simplify grid traversal for color-fill 47 | 48 | square.classList.add('border-top-left'); 49 | } 50 | //add a right border the the right most items 51 | const rightItems = document.querySelectorAll(`.grid-item:nth-child(${gridSize}n)`); 52 | for (let i = 0; i < rightItems.length; i++) { 53 | rightItems[i].setAttribute('data-right', 'true'); 54 | rightItems[i].classList.toggle('border-right'); 55 | } 56 | 57 | // add a bottom border to the bottom most items 58 | let gridItems = document.querySelectorAll('.grid-item'); 59 | const lastItems = Array.from(gridItems).slice(-`${gridSize}`); 60 | for (let i = 0; i < lastItems.length; i++) { 61 | lastItems[i].setAttribute('data-bottom', 'true'); 62 | lastItems[i].classList.toggle('border-bottom'); 63 | } 64 | } 65 | 66 | createGrid(); 67 | 68 | gridItems = document.querySelectorAll('.grid-item'); 69 | 70 | // set default colour to black 71 | let ink = '#000000'; 72 | 73 | //pen color picker 74 | const colorPicker = document.querySelector('#color-select'); 75 | colorPicker.addEventListener('input', (e) => { 76 | ink = e.target.value; 77 | if (grab) { 78 | grab = false; 79 | dropper.classList.remove('btn-on'); 80 | } 81 | // fill = false; 82 | // colorFillButton.classList.remove('btn-on'); 83 | }); 84 | 85 | // bg color picker 86 | // will not change the grid items that have the attribute data-inked = true 87 | const bgColorPicker = document.querySelector('#bg-color-select'); 88 | 89 | // toggle button colour when clicked 90 | const buttons = document.getElementsByTagName('button'); 91 | 92 | for (let i = 0; i < buttons.length; i++) { 93 | buttons[i].addEventListener('click', () => { 94 | buttons[i].classList.toggle('btn-on'); 95 | }); 96 | } 97 | 98 | // shading toggle 99 | let shading = false; 100 | const shaderButton = document.querySelector('#shader-btn'); 101 | shaderButton.addEventListener('click', () => { 102 | if (shading) { 103 | shading = false; 104 | } else { 105 | shading = true; 106 | rainbow = false; 107 | rainbowButton.classList.remove('btn-on'); 108 | lighten = false; 109 | lightenButton.classList.remove('btn-on'); 110 | eraser = false; 111 | eraserButton.classList.remove('btn-on'); 112 | } 113 | if (grab) { 114 | grab = false; 115 | dropper.classList.remove('btn-on'); 116 | } 117 | }); 118 | 119 | // lighten toggle 120 | let lighten = false; 121 | const lightenButton = document.querySelector('#lighten-btn'); 122 | lightenButton.addEventListener('click', () => { 123 | if (lighten) { 124 | lighten = false; 125 | } else { 126 | lighten = true; 127 | shading = false; 128 | shaderButton.classList.remove('btn-on'); 129 | rainbow = false; 130 | rainbowButton.classList.remove('btn-on'); 131 | eraser = false; 132 | eraserButton.classList.remove('btn-on'); 133 | } 134 | if (grab) { 135 | grab = false; 136 | dropper.classList.remove('btn-on'); 137 | } 138 | }); 139 | 140 | // shading function 141 | 142 | function RGBToHex(rgb) { 143 | // Choose correct separator 144 | let sep = rgb.indexOf(',') > -1 ? ',' : ' '; 145 | // Turn "rgb(r,g,b)" into [r,g,b] 146 | rgb = rgb.substr(4).split(')')[0].split(sep); 147 | 148 | let r = (+rgb[0]).toString(16), 149 | g = (+rgb[1]).toString(16), 150 | b = (+rgb[2]).toString(16); 151 | 152 | if (r.length == 1) r = '0' + r; 153 | if (g.length == 1) g = '0' + g; 154 | if (b.length == 1) b = '0' + b; 155 | b = (+rgb[2]).toString(16); 156 | 157 | if (r.length == 1) r = '0' + r; 158 | if (g.length == 1) g = '0' + g; 159 | if (b.length == 1) b = '0' + b; 160 | return '#' + r + g + b; 161 | } 162 | 163 | function adjust(RGBToHex, rgb, amount) { 164 | let color = RGBToHex(rgb); 165 | return ( 166 | '#' + 167 | color 168 | .replace(/^#/, '') 169 | .replace(/../g, (color) => 170 | ('0' + Math.min(255, Math.max(0, parseInt(color, 16) + amount)).toString(16)).substr(-2) 171 | ) 172 | ); 173 | } 174 | 175 | // eyedrop color grabbing tool 176 | const dropper = document.querySelector('#color-grabber'); 177 | let grab = false; 178 | dropper.addEventListener('click', () => { 179 | // when grab is true, all drawing is frozen until a color is selected 180 | if (grab) { 181 | grab = false; 182 | dropper.classList.remove('btn-on'); 183 | } else { 184 | grab = true; 185 | } 186 | 187 | if (fill) { 188 | fill = false; 189 | colorFillButton.classList.remove('btn-on'); 190 | } 191 | }); 192 | 193 | // default eraser to false and listen for toggle 194 | let eraser = false; 195 | const eraserButton = document.querySelector('#eraser-btn'); 196 | eraserButton.addEventListener('click', () => { 197 | if (eraser) { 198 | eraser = false; 199 | } else { 200 | eraser = true; 201 | shading = false; 202 | shaderButton.classList.remove('btn-on'); 203 | rainbow = false; 204 | rainbowButton.classList.remove('btn-on'); 205 | lighten = false; 206 | lightenButton.classList.remove('btn-on'); 207 | } 208 | 209 | if (grab) { 210 | grab = false; 211 | dropper.classList.remove('btn-on'); 212 | } 213 | }); 214 | 215 | // default rainbow ink to false and listen for toggle 216 | let rainbow = false; 217 | const rainbowButton = document.querySelector('#rainbow-btn'); 218 | rainbowButton.addEventListener('click', () => { 219 | if (rainbow) { 220 | rainbow = false; 221 | } else { 222 | rainbow = true; 223 | shading = false; 224 | shaderButton.classList.remove('btn-on'); 225 | lighten = false; 226 | lightenButton.classList.remove('btn-on'); 227 | eraser = false; 228 | eraserButton.classList.remove('btn-on'); 229 | } 230 | 231 | if (grab) { 232 | grab = false; 233 | dropper.classList.remove('btn-on'); 234 | } 235 | }); 236 | 237 | //create random colour generator 238 | function randomColor() { 239 | // return "#" + Math.floor(Math.random()*16777215).toString(16); 240 | // this returns fewer colors but they are all nice and bright 241 | return `hsl(${Math.random() * 360}, 100%, 50%)`; 242 | } 243 | 244 | // slider 245 | 246 | let progressBar = document.getElementById('progress-bar'); 247 | 248 | function rangeSlider(value) { 249 | let gridLabels = document.querySelectorAll('#range-value'); 250 | progressBar.style.width = (value / 60) * 100 + '%'; 251 | for (let i = 0; i < gridLabels.length; i++) { 252 | gridLabels[i].textContent = value; 253 | } 254 | // document.querySelectorAll('#range-value').textContent = value; 255 | gridSize = parseInt(value); 256 | deleteGrid(); 257 | createGrid(); 258 | listen(); 259 | reInit(); 260 | // turn the grid button back on if it is off. 261 | const gridButton = document.querySelector('#grid-btn'); 262 | if (gridButton.classList.contains('btn-on')) { 263 | //pass 264 | } else { 265 | gridButton.classList.toggle('btn-on'); 266 | } 267 | } 268 | 269 | function reInit() { 270 | deleteGrid(); 271 | createGrid(); 272 | listen(); 273 | } 274 | 275 | // let progressBar = document.getElementById('progress-bar'); 276 | 277 | function rangeSliderValue(value) { 278 | let gridLabels = document.querySelectorAll('#range-value'); 279 | for (let i = 0; i < gridLabels.length; i++) { 280 | gridLabels[i].textContent = value; 281 | } 282 | progressBar.style.width = (value / 60) * 100 + '%'; 283 | } 284 | 285 | function deleteGrid() { 286 | while (container.firstChild) { 287 | container.removeEventListener('mousedown', drawClick); 288 | container.removeEventListener('mouseenter', drawClickHover); 289 | container.lastChild = null; 290 | container.removeChild(container.lastChild); 291 | } 292 | } 293 | 294 | //fade grid 295 | function fadeGrid(item) { 296 | // if the cell hasnt been coloured, set it to the background color (un marked cells are transperent) 297 | if (item.style.backgroundColor == '' || item.style.backgroundColor == 'transperent') { 298 | item.style.backgroundColor == bgColor; 299 | } 300 | 301 | // attatch class to each item. this fades the color to the background color over 1.5 seconds 302 | 303 | // apply a random fadeout time to each item 304 | 305 | let fadeSpeed = Math.random() * 10; 306 | if (fadeSpeed > 8) { 307 | item.classList.add('clear-fade'); 308 | } else if (fadeSpeed > 6) { 309 | item.classList.add('clear-fade-2'); 310 | } else if (fadeSpeed > 4) { 311 | item.classList.add('clear-fade-3'); 312 | } else if (fadeSpeed > 2) { 313 | item.classList.add('clear-fade-4'); 314 | } else { 315 | item.classList.add('clear-fade-5'); 316 | } 317 | } 318 | 319 | // clear grid with a fade out 320 | let root = document.documentElement; 321 | const clearButton = document.querySelector('#clear-grid'); 322 | function clearGrid() { 323 | // sets the css background color to the js variable bgColor. this is so the fadeout class can be applied, and use its background color 324 | root.style.setProperty('--bg-color', bgColor); 325 | gridItems = document.querySelectorAll('.grid-item'); 326 | for (let i = 0; i < gridItems.length; i++) { 327 | fadeGrid(gridItems[i]); 328 | } 329 | // set a timer so the fade has time to execute, then reset all the grid cells. 330 | setTimeout(function () { 331 | for (let i = 0; i < gridItems.length; i++) { 332 | gridItems[i].style.backgroundColor = ''; 333 | gridItems[i].removeAttribute('data-inked'); 334 | gridItems[i].removeAttribute('data-shade'); 335 | gridItems[i].classList.remove('clear-fade'); 336 | gridItems[i].classList.remove('clear-fade-2'); 337 | gridItems[i].classList.remove('clear-fade-3'); 338 | gridItems[i].classList.remove('clear-fade-4'); 339 | gridItems[i].classList.remove('clear-fade-5'); 340 | } 341 | }, 1500); 342 | container.style.backgroundColor = bgColor; 343 | 344 | // turn off the button after a very short delay 345 | setTimeout(function () { 346 | clearButton.classList.remove('btn-on'); 347 | }, 1400); 348 | } 349 | clearButton.addEventListener('click', clearGrid); 350 | 351 | // set fill to true when the color fill button is pressed 352 | // if fill is true set it to false (clicking the button without filling) 353 | // when fill is true all other events on the grid will stop and listen for a grid area to fill 354 | const colorFillButton = document.querySelector('#color-fill'); 355 | let fill = false; 356 | colorFillButton.addEventListener('click', () => { 357 | if (grab) { 358 | grab = false; 359 | dropper.classList.remove('btn-on'); 360 | } 361 | if (fill) { 362 | fill = false; 363 | } else { 364 | fill = true; 365 | } 366 | }); 367 | // convert array into matrix representing the grid 368 | function toMatrix(arr, width) { 369 | return arr.reduce(function (rows, key, index) { 370 | return (index % width == 0 ? rows.push([key]) : rows[rows.length - 1].push(key)) && rows; 371 | }, []); 372 | } 373 | 374 | //helper funtion to grab adjacent cells of a 2d grid 375 | //function getAdjacent2D(x, y) { 376 | // let xAbove = [x - 1, y]; 377 | 378 | // let xBellow = [x + 1, y]; 379 | 380 | // let xLeft = [x, y - 1]; 381 | 382 | // let xRight = [x, y + 1]; 383 | 384 | // return [xAbove, xBellow, xLeft, xRight] 385 | //} 386 | 387 | //helper function to grab adjacent cells of a 2d grid stored as a 1d array 388 | // only return cells that do not cross over the edge of the grid 389 | 390 | /* 391 | function getAdjacent1D(x, gridX, gridY) { 392 | let xAbove = null; 393 | let xBellow = null; 394 | let xLeft = null; 395 | let xRight = null; 396 | 397 | // make sure x is not in the top row before returning the cell above 398 | if (gridX != 0) { 399 | xAbove = [x - gridSize]; 400 | } 401 | // make sure x is not in the bottom row before returning the cell bellow 402 | if (gridX != gridSize - 1) { 403 | xBellow = [x + gridSize]; 404 | } 405 | // make sure x is not in the left column before returning the cell to its left 406 | if (gridY != 0) { 407 | xLeft = [x - 1]; 408 | } 409 | // make sure x is not in the right column before returning the cell to its right 410 | if (gridY != gridSize - 1) { 411 | xRight = [x + 1]; 412 | } 413 | 414 | // console.log(xAbove, xBellow, xLeft, xRight); 415 | return [xAbove, xBellow, xLeft, xRight]; 416 | } 417 | */ 418 | 419 | function hexToRGB(hex) { 420 | let r = parseInt(hex[1] + hex[2], 16); 421 | let g = parseInt(hex[3] + hex[4], 16); 422 | let b = parseInt(hex[5] + hex[6], 16); 423 | 424 | return `rgb(${r}, ${g}, ${b})`; 425 | } 426 | 427 | function findNeighbors(matrix, x, y, oldColor, newColor) { 428 | const possibleNeighbors = [ 429 | { cell: matrix?.[x]?.[y - 1], x: x, y: y - 1 }, // west 430 | { cell: matrix?.[x]?.[y + 1], x: x, y: y + 1 }, // east 431 | { cell: matrix?.[x - 1]?.[y], x: x - 1, y: y }, // north 432 | { cell: matrix?.[x + 1]?.[y], x: x + 1, y: y }, // south 433 | ]; 434 | 435 | const neighbors = []; 436 | 437 | for (const neighbor of possibleNeighbors) { 438 | if ( 439 | neighbor.cell !== undefined && 440 | neighbor.cell.style.backgroundColor === oldColor && 441 | neighbor.cell.style.backgroundColor !== newColor 442 | ) { 443 | neighbors.push(neighbor); 444 | } 445 | } 446 | 447 | return neighbors; 448 | } 449 | 450 | function floodFill(image, x, y, oldColor, newColor) { 451 | if (oldColor === newColor) return; 452 | 453 | oldColor = hexToRGB(oldColor); 454 | newColor = hexToRGB(newColor); 455 | 456 | const toPaint = [{ x: x, y: y }]; // queue 457 | 458 | while (toPaint.length > 0) { 459 | const { x, y } = toPaint.shift(); 460 | 461 | const neighbors = findNeighbors(image, x, y, oldColor, newColor); 462 | 463 | for (const { cell, x, y } of neighbors) { 464 | toPaint.push({ x, y }); 465 | cell.style.backgroundColor = rainbow ? randomColor() : newColor; 466 | cell.setAttribute('data-inked', 'true'); 467 | } 468 | } 469 | } 470 | 471 | //colorfill 472 | function colorFill(e) { 473 | if (fill) { 474 | //get index of the clicked grid cell 475 | const ogIndex = Array.from(e.target.parentElement.children).indexOf( 476 | e.target 477 | ); 478 | // console.log(ogIndex); 479 | 480 | gridItems = document.querySelectorAll('.grid-item'); 481 | const gridItemsArray = Array.from(gridItems); 482 | // console.log(gridItemsArray.length); 483 | 484 | // create grid-like representation of grid items 485 | const gridItemsArray2D = toMatrix(gridItemsArray, gridSize); 486 | 487 | // get index of clicked item in 2d array 488 | const gridX = Math.floor(ogIndex / gridSize); 489 | const gridY = ogIndex % gridSize; 490 | 491 | const seed = gridItemsArray2D[gridX][gridY]; 492 | const oldInk = seed.style.backgroundColor 493 | ? RGBToHex(seed.style.backgroundColor) 494 | : bgColor; 495 | 496 | floodFill(gridItemsArray2D, gridX, gridY, oldInk, ink); 497 | 498 | colorFillButton.classList.remove('btn-on'); 499 | fill = false; 500 | } 501 | } 502 | 503 | // draw on the grid when clicked 504 | function drawClick(e) { 505 | // when fill or grab is true do not do anything (a seperate listener is waiting for fill / grab input) 506 | if (!grab && !fill) { 507 | if (eraser) { 508 | e.target.style.backgroundColor = ''; 509 | //data-inked = true means the background color change wont affect these elements 510 | e.target.removeAttribute('data-inked'); 511 | e.target.removeAttribute('data-shade'); 512 | } else if (rainbow) { 513 | e.target.style.backgroundColor = randomColor(); 514 | e.target.setAttribute('data-inked', 'true'); 515 | e.target.removeAttribute('data-shade'); 516 | } else if (shading) { 517 | // first check to see if this grid item has been shadded. if it hasnt, set data-shade to 1 518 | // this is nessesarry to transfer shading between bg color changes 519 | if (!e.target.dataset.shade) { 520 | e.target.setAttribute('data-shade', '1'); 521 | } else { 522 | // if the grid item has been shadded, increment the data-shade value 523 | // this keeps track of how many times the grid item has been shaded 524 | let shadeAmount = parseInt(e.target.getAttribute('data-shade')); 525 | shadeAmount++; 526 | e.target.setAttribute('data-shade', `${shadeAmount}`); 527 | } // a transperent item cant be shadded. if item is transperent first set the cell color to bg color 528 | if (e.target.style.backgroundColor == '' || e.target.style.backgroundColor == 'transperent') { 529 | e.target.style.backgroundColor = bgColor; 530 | } 531 | 532 | e.target.style.backgroundColor = adjust(RGBToHex, e.target.style.backgroundColor, -15); 533 | // e.target.setAttribute('data-inked', 'true'); 534 | } else if (lighten) { 535 | if (!e.target.dataset.shade) { 536 | e.target.setAttribute('data-shade', '-1'); 537 | } else { 538 | // if the grid item has been lightened, decrement the data-shade value 539 | // this keeps track of how many times the grid item has been shaded 540 | let shadeAmount = parseInt(e.target.getAttribute('data-shade')); 541 | shadeAmount--; 542 | e.target.setAttribute('data-shade', `${shadeAmount}`); 543 | } 544 | if (e.target.style.backgroundColor == '' || e.target.style.backgroundColor == 'transperent') { 545 | e.target.style.backgroundColor = bgColor; 546 | } 547 | e.target.style.backgroundColor = adjust(RGBToHex, e.target.style.backgroundColor, +15); 548 | // e.target.setAttribute('data-inked', 'true'); 549 | } else { 550 | e.target.style.backgroundColor = ink; 551 | e.target.setAttribute('data-inked', 'true'); 552 | e.target.removeAttribute('data-shade'); 553 | } 554 | } 555 | } 556 | // draw when hovering into a grid with the mouse held down 557 | function drawClickHover(e) { 558 | if (e.buttons > 0) { 559 | if (!grab && !fill) { 560 | if (eraser) { 561 | e.target.style.backgroundColor = ''; 562 | //data-inked = true means the background color change wont affect these elements 563 | e.target.removeAttribute('data-inked'); 564 | e.target.removeAttribute('data-shade'); 565 | } else if (rainbow) { 566 | e.target.style.backgroundColor = randomColor(); 567 | e.target.setAttribute('data-inked', 'true'); 568 | e.target.removeAttribute('data-shade'); 569 | } else if (shading) { 570 | // first check to see if this grid item has been shadded. if it hasnt, set data-shade to 1 571 | // this is nessesarry to transfer shading between bg color changes 572 | if (!e.target.dataset.shade) { 573 | e.target.setAttribute('data-shade', '1'); 574 | } else { 575 | // if the grid item has been shadded, increment the data-shade value 576 | // this keeps track of how many times the grid item has been shaded 577 | let shadeAmount = parseInt(e.target.getAttribute('data-shade')); 578 | shadeAmount++; 579 | e.target.setAttribute('data-shade', `${shadeAmount}`); 580 | } // a transperent item cant be shadded. if item is transperent first set the cell color to bg color 581 | if ( 582 | e.target.style.backgroundColor == '' || 583 | e.target.style.backgroundColor == 'transperent' 584 | ) { 585 | e.target.style.backgroundColor = bgColor; 586 | } 587 | 588 | e.target.style.backgroundColor = adjust(RGBToHex, e.target.style.backgroundColor, -15); 589 | // e.target.setAttribute('data-inked', 'true'); 590 | } else if (lighten) { 591 | if (!e.target.dataset.shade) { 592 | e.target.setAttribute('data-shade', '-1'); 593 | } else { 594 | // if the grid item has been lightened, decrement the data-shade value 595 | // this keeps track of how many times the grid item has been shaded 596 | let shadeAmount = parseInt(e.target.getAttribute('data-shade')); 597 | shadeAmount--; 598 | e.target.setAttribute('data-shade', `${shadeAmount}`); 599 | } 600 | if ( 601 | e.target.style.backgroundColor == '' || 602 | e.target.style.backgroundColor == 'transperent' 603 | ) { 604 | e.target.style.backgroundColor = bgColor; 605 | } 606 | e.target.style.backgroundColor = adjust(RGBToHex, e.target.style.backgroundColor, +15); 607 | // e.target.setAttribute('data-inked', 'true'); 608 | } else { 609 | e.target.style.backgroundColor = ink; 610 | e.target.setAttribute('data-inked', 'true'); 611 | e.target.removeAttribute('data-shade'); 612 | } 613 | } 614 | } 615 | } 616 | 617 | // listen for events 618 | function listen() { 619 | gridItems = document.querySelectorAll('.grid-item'); 620 | for (let i = 0; i < gridItems.length; i++) { 621 | gridItems[i].addEventListener('mousedown', drawClick); 622 | // listen for a mouse over and change colour only if mouse button is pressed 623 | gridItems[i].addEventListener('mouseenter', drawClickHover); 624 | } 625 | 626 | //listen for clicks on all grid items when grab is true (color picker) 627 | for (let i = 0; i < gridItems.length; i++) { 628 | gridItems[i].addEventListener('click', (e) => { 629 | if (grab) { 630 | ink = e.target.style.backgroundColor; 631 | // if trying to grab the color of the background (transperent cell) 632 | if (ink == '') { 633 | colorPicker.value = bgColor; 634 | } else { 635 | colorPicker.value = RGBToHex(ink); 636 | } 637 | dropper.classList.remove('btn-on'); 638 | grab = false; 639 | 640 | // once color has been grabbed, turn off other buttons so you can draw with the new color without 641 | // having to toggle the other button manually 642 | rainbow = false; 643 | rainbowButton.classList.remove('btn-on'); 644 | shading = false; 645 | shaderButton.classList.remove('btn-on'); 646 | lighten = false; 647 | lightenButton.classList.remove('btn-on'); 648 | eraser = false; 649 | eraserButton.classList.remove('btn-on'); 650 | } 651 | }); 652 | } 653 | 654 | // listen for clicks on all grid items when fill is true (colour fill) 655 | for (let i = 0; i < gridItems.length; i++) { 656 | gridItems[i].addEventListener('click', colorFill); 657 | } 658 | 659 | bgColorPicker.addEventListener('input', (e) => { 660 | gridItems = document.querySelectorAll('.grid-item'); 661 | bgColor = e.target.value; 662 | for (let i = 0; i < gridItems.length; i++) { 663 | if (!gridItems[i].dataset.inked) { 664 | container.style.backgroundColor = bgColor; 665 | } 666 | // carry over shading when the bg color changes 667 | //set all shaded items to bg color, so that the shading ran be re-applyed to the new bg color 668 | 669 | // dont change the color of shaded inked cells, only background cells that have been shaded 670 | if (!gridItems[i].dataset.inked) { 671 | if (gridItems[i].dataset.shade) { 672 | gridItems[i].style.backgroundColor = bgColor; 673 | // grab the value of data-shade (the amount of times the cell has been shaded) 674 | let shadeAmount = parseInt(gridItems[i].getAttribute('data-shade')); 675 | // multiply the default shading intensity by shadeAmount, then apply this ammount 676 | //of shading to the cell 677 | let reshadeValue = shadeAmount * -15; 678 | gridItems[i].style.backgroundColor = adjust( 679 | RGBToHex, 680 | gridItems[i].style.backgroundColor, 681 | reshadeValue 682 | ); 683 | } 684 | } 685 | } 686 | }); 687 | 688 | // toggle grid lines 689 | const gridButton = document.querySelector('#grid-btn'); 690 | 691 | gridButton.addEventListener('click', () => { 692 | for (i = 0; i < gridItems.length; i++) { 693 | //toggle top and left cell borders 694 | gridItems[i].classList.toggle('border-top-left'); 695 | //toggle the remaining right borders 696 | if (gridItems[i].dataset.right) { 697 | gridItems[i].classList.toggle('border-right'); 698 | } 699 | // toggle the remaining bottom borders 700 | if (gridItems[i].dataset.bottom) { 701 | gridItems[i].classList.toggle('border-bottom'); 702 | } 703 | } 704 | }); 705 | } 706 | 707 | listen(); 708 | --------------------------------------------------------------------------------