├── LICENSE.md
├── README.md
├── gif
├── selectionCopy.gif
└── speedDrawing.gif
├── img
├── clipboard.png
├── colorpicker.png
├── eraser.png
├── eraserBig.png
├── favicon.png
├── fill.png
├── floppy.png
├── grid.png
├── pencil.png
├── redo.png
├── selection.png
├── selectionBright.png
├── selectionOnToolbar.png
└── undo.png
├── index.html
├── js
├── algo.js
├── eventHandlers.js
├── historyStates.js
├── palette.js
├── script.js
├── tools.js
└── utils.js
└── main.css
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Adam Kulidjian
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Pixel Paint
2 |
3 | Make intuitve pixel art in this simple drawing tool written in _vanilla javascript_ :cake:
4 |
5 | 
6 |
7 | ## Features
8 | - [x] 100% vanilla javascript!
9 | - [x] pencil, fill, eraser, selection and colorpicker tools
10 | - [x] implements undo and redo with a stack of canvas states
11 | - [x] color palette from the NES and Gameboy
12 | - [x] save your pixel art to PNG
13 | - [x] original 32x32 cursors/button icons
14 |
15 | ## Hotkeys
16 |
17 | | Command | Hotkey |
18 | | :--------------- | :------------------ |
19 | | pencil mode | P |
20 | | bucket mode | B |
21 | | mode eraser mode | E |
22 | | colorpicker mode | V |
23 | | selection mode | S |
24 | | copy selection | ALT+click and drag |
25 | | delete selected area | Del or Backspace|
26 | | remove selection | Esc |
27 | | toggle grid | G |
28 | | undo | Z |
29 | | redo | X |
30 | | color swap | C |
31 |
32 |
33 | ## Contributing
34 |
35 | One of the joys of posting this project on Github is to get people excited and wanting to contribute to them. Feel free to create Pull Requests for any changes that you think the app could benifit from, or something that you would like to use.
36 |
37 | Have a look at some of the [Issues](https://github.com/Kully/pixel-paint/issues) in the repo. If you see something you want to add or comment on, by all means do so!
38 |
39 | Have fun and be creative! :art:
40 |
--------------------------------------------------------------------------------
/gif/selectionCopy.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kully/pixel-paint/6b7daec4d14b9bb3a6a3e03f3a8a63fb4363b7e5/gif/selectionCopy.gif
--------------------------------------------------------------------------------
/gif/speedDrawing.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kully/pixel-paint/6b7daec4d14b9bb3a6a3e03f3a8a63fb4363b7e5/gif/speedDrawing.gif
--------------------------------------------------------------------------------
/img/clipboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kully/pixel-paint/6b7daec4d14b9bb3a6a3e03f3a8a63fb4363b7e5/img/clipboard.png
--------------------------------------------------------------------------------
/img/colorpicker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kully/pixel-paint/6b7daec4d14b9bb3a6a3e03f3a8a63fb4363b7e5/img/colorpicker.png
--------------------------------------------------------------------------------
/img/eraser.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kully/pixel-paint/6b7daec4d14b9bb3a6a3e03f3a8a63fb4363b7e5/img/eraser.png
--------------------------------------------------------------------------------
/img/eraserBig.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kully/pixel-paint/6b7daec4d14b9bb3a6a3e03f3a8a63fb4363b7e5/img/eraserBig.png
--------------------------------------------------------------------------------
/img/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kully/pixel-paint/6b7daec4d14b9bb3a6a3e03f3a8a63fb4363b7e5/img/favicon.png
--------------------------------------------------------------------------------
/img/fill.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kully/pixel-paint/6b7daec4d14b9bb3a6a3e03f3a8a63fb4363b7e5/img/fill.png
--------------------------------------------------------------------------------
/img/floppy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kully/pixel-paint/6b7daec4d14b9bb3a6a3e03f3a8a63fb4363b7e5/img/floppy.png
--------------------------------------------------------------------------------
/img/grid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kully/pixel-paint/6b7daec4d14b9bb3a6a3e03f3a8a63fb4363b7e5/img/grid.png
--------------------------------------------------------------------------------
/img/pencil.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kully/pixel-paint/6b7daec4d14b9bb3a6a3e03f3a8a63fb4363b7e5/img/pencil.png
--------------------------------------------------------------------------------
/img/redo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kully/pixel-paint/6b7daec4d14b9bb3a6a3e03f3a8a63fb4363b7e5/img/redo.png
--------------------------------------------------------------------------------
/img/selection.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kully/pixel-paint/6b7daec4d14b9bb3a6a3e03f3a8a63fb4363b7e5/img/selection.png
--------------------------------------------------------------------------------
/img/selectionBright.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kully/pixel-paint/6b7daec4d14b9bb3a6a3e03f3a8a63fb4363b7e5/img/selectionBright.png
--------------------------------------------------------------------------------
/img/selectionOnToolbar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kully/pixel-paint/6b7daec4d14b9bb3a6a3e03f3a8a63fb4363b7e5/img/selectionOnToolbar.png
--------------------------------------------------------------------------------
/img/undo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kully/pixel-paint/6b7daec4d14b9bb3a6a3e03f3a8a63fb4363b7e5/img/undo.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Pixel Paint
5 |
6 |
7 |
8 |
9 |
10 |
11 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | NES
63 |
64 |
65 |
66 |
67 | Gameboy
68 |
69 |
70 |
71 |
72 |
73 | How to Copy Selection: hold down alt and drag the selected area.
74 |
75 |
76 |
Shortcuts
77 |
78 |
79 |
80 | Copy Selection Alt + Click Drag
81 | Delete Selection Del or Backspace
82 | Remove Selection Esc
83 |
84 |
85 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
--------------------------------------------------------------------------------
/js/algo.js:
--------------------------------------------------------------------------------
1 | function Flood_Fill_Algorithm(cell_id, target_color, replacement_color) {
2 | function Cell_Coordinates_Out_Of_Bounds(x, y) {
3 | if ((0 <= x) && (x <= CELLS_PER_ROW - 1) && (0 <= y) && (y <= CELLS_PER_ROW - 1))
4 | return false;
5 | return true;
6 | }
7 |
8 | function Cell_Coordinates_In_Bounds(x, y) {
9 | if ((0 <= x) && (x <= CELLS_PER_ROW - 1) && (0 <= y) && (y <= CELLS_PER_ROW - 1))
10 | return true;
11 | return false;
12 | }
13 |
14 | let cell_int = Number(cell_id);
15 | let cell_x = Get_X_From_CellInt(cell_int);
16 | let cell_y = Get_Y_From_CellInt(cell_int);
17 |
18 | Cell_Coordinates_In_Bounds(cell_x, cell_y + 1);
19 |
20 | let cell_element = document.getElementById(cell_id);
21 |
22 | if (Rgb_To_Hex(target_color) === replacement_color)
23 | return;
24 | else if (cell_element.style.backgroundColor !== target_color)
25 | return;
26 | else {
27 | cell_element.style.backgroundColor = replacement_color;
28 |
29 | if (Cell_Coordinates_In_Bounds(cell_x, cell_y + 1)) {
30 | let next_cell_int = Get_CellInt_From_CellXY(cell_x, cell_y + 1);
31 | let next_cell_id = Pad_Start_Int(next_cell_int);
32 |
33 | Flood_Fill_Algorithm(next_cell_id, target_color, replacement_color);
34 | }
35 | if (Cell_Coordinates_In_Bounds(cell_x, cell_y - 1)) {
36 | let next_cell_int = Get_CellInt_From_CellXY(cell_x, cell_y - 1);
37 | let next_cell_id = Pad_Start_Int(next_cell_int);
38 |
39 | Flood_Fill_Algorithm(next_cell_id, target_color, replacement_color);
40 | }
41 | if (Cell_Coordinates_In_Bounds(cell_x - 1, cell_y)) {
42 | let next_cell_int = Get_CellInt_From_CellXY(cell_x - 1, cell_y);
43 | let next_cell_id = Pad_Start_Int(next_cell_int);
44 |
45 | Flood_Fill_Algorithm(next_cell_id, target_color, replacement_color);
46 | }
47 | if (Cell_Coordinates_In_Bounds(cell_x + 1, cell_y)) {
48 | let next_cell_int = Get_CellInt_From_CellXY(cell_x + 1, cell_y);
49 | let next_cell_id = Pad_Start_Int(next_cell_int);
50 |
51 | Flood_Fill_Algorithm(next_cell_id, target_color, replacement_color);
52 | }
53 | }
54 | };
55 |
56 | /**
57 | * Draws a line using the Bresenham's line algorithm. The line is drawn from (startX, startY) to (endX, endY)
58 | *
59 | * @param {number} startX - The x-coordinate of the starting point.
60 | * @param {number} startY - The y-coordinate of the starting point.
61 | * @param {number} endX - The x-coordinate of the ending point.
62 | * @param {number} endY - The y-coordinate of the ending point.
63 | * @param {function} callback - The callback function to be called for each cell on the line.
64 | * @return {void} This function does not return a value.
65 | */
66 | function Bresenham_Line_Algorithm(startX, startY, endX, endY, callback)
67 | {
68 | if (typeof callback !== 'function') { console.error("Invalid callback function"); return; }
69 | let deltaX = Math.abs(endX - startX);
70 | let deltaY = Math.abs(endY - startY);
71 | let stepX = (startX < endX) ? 1 : -1;
72 | let stepY = (startY < endY) ? 1 : -1;
73 | let error = deltaX - deltaY;
74 |
75 | while (true)
76 | {
77 | let cellId = Pad_Start_Int(Get_CellInt_From_CellXY(Math.round(startX), Math.round(startY)));
78 | let currentCell = document.getElementById(cellId);
79 |
80 | if (currentCell) {
81 | callback(currentCell);
82 | }
83 | if (Math.round(startX) === Math.round(endX) &&
84 | Math.round(startY) === Math.round(endY)) break;
85 |
86 | let error2 = 2 * error;
87 | if (error2 > -deltaY) {
88 | error -= deltaY;
89 | startX += stepX;
90 | }
91 | if (error2 < deltaX) {
92 | error += deltaX;
93 | startY += stepY;
94 | }
95 | }
96 | };
97 |
--------------------------------------------------------------------------------
/js/eventHandlers.js:
--------------------------------------------------------------------------------
1 | let previousCursorX = null;
2 | let previousCursorY = null;
3 | function Add_EventHandlers_To_Canvas_Div()
4 | {
5 | let isDrawingOutside = false;
6 | let lastOutsideX = null;
7 | let lastOutsideY = null;
8 |
9 | function Update_Cursor_Coordinates_On_Screen(e)
10 | {
11 | const cursorXY = Canvas_Cursor_XY(e);
12 | let cellX = Math.floor(cursorXY[0] / CELL_WIDTH_PX);
13 | let cellY = Math.floor(cursorXY[1] / CELL_WIDTH_PX);
14 |
15 | cellX = Pad_Start_Int(cellX, 2);
16 | cellY = Pad_Start_Int(cellY, 2);
17 |
18 | document.getElementById("cursor-coords-display").innerHTML = "(" + cellX + ", " + cellY + ")";
19 | }
20 |
21 | const canvasDiv = document.getElementById("canvas-div");
22 | canvasDiv.addEventListener("mousedown", function () {
23 | STATE["brushDown"] = true;
24 | isDrawingOutside = false;
25 | });
26 | canvasDiv.addEventListener("mousemove", Update_Cursor_Coordinates_On_Screen);
27 | canvasDiv.addEventListener("mouseup", function () {
28 | STATE["brushDown"] = false;
29 | previousCursorX = previousCursorY = null;
30 | Save_Canvas_State();
31 | });
32 | canvasDiv.addEventListener("mouseleave", function (e) {
33 | if (STATE["brushDown"]) {
34 | const canvasRect = canvasDiv.getBoundingClientRect();
35 | const x = Math.max(0, Math.min(e.clientX - canvasRect.left, canvasRect.width - 1));
36 | const y = Math.max(0, Math.min(e.clientY - canvasRect.top, canvasRect.height - 1));
37 | const targetCell = document.elementFromPoint(x + canvasRect.left, y + canvasRect.top);
38 |
39 | if (targetCell && targetCell.classList.contains('canvasCell')) {
40 | const targetX = targetCell.offsetLeft / CELL_WIDTH_PX;
41 | const targetY = targetCell.offsetTop / CELL_WIDTH_PX;
42 | // console.log(`Previous Cursor: (${previousCursorX}, ${previousCursorY}) Exit Target: (${targetX}, ${targetY})`);
43 | if (previousCursorX !== null && previousCursorY !== null) {
44 | Bresenham_Line_Algorithm(previousCursorX, previousCursorY, targetX, targetY, Get_Tool_Action_Callback());
45 | }
46 | previousCursorX = previousCursorY = null;
47 | }
48 | document.addEventListener("mousemove", Track_Mouse_Outside);
49 | }
50 | isDrawingOutside = true;
51 | });
52 | canvasDiv.addEventListener("mouseenter", function (e) {
53 | if (STATE["brushDown"] && isDrawingOutside) {
54 | isDrawingOutside = false;
55 | const targetCell = document.elementFromPoint(e.clientX, e.clientY);
56 |
57 | if (targetCell && targetCell.classList.contains('canvasCell')) {
58 | const targetX = targetCell.offsetLeft / CELL_WIDTH_PX;
59 | const targetY = targetCell.offsetTop / CELL_WIDTH_PX;
60 |
61 | let entryPointX, entryPointY;
62 | const deltaX = targetX - lastOutsideX;
63 | const deltaY = targetY - lastOutsideY;
64 |
65 | if (Math.abs(deltaX) > Math.abs(deltaY)) {
66 | if (deltaX > 0) { entryPointX = 0; }
67 | else { entryPointX = CELLS_PER_ROW - 1; }
68 | entryPointY = Math.floor(lastOutsideY);
69 | } else {
70 | if (deltaY > 0) { entryPointY = 0; }
71 | else { entryPointY = CELLS_PER_ROW - 1; }
72 | entryPointX = Math.floor(lastOutsideX);
73 | }
74 | entryPointX = Math.max(0, Math.min(entryPointX, CELLS_PER_ROW - 1));
75 | entryPointY = Math.max(0, Math.min(entryPointY, CELLS_PER_ROW - 1));
76 |
77 | if (entryPointX === 0 || entryPointX === CELLS_PER_ROW - 1) {
78 | entryPointY = Math.floor(lastOutsideY);
79 | } else if (entryPointY === 0 || entryPointY === CELLS_PER_ROW - 1) {
80 | entryPointX = Math.floor(lastOutsideX);
81 | }
82 | // console.log(`Entry Point: (${entryPointX}, ${entryPointY}) Target: (${targetX}, ${targetY})`);
83 | Bresenham_Line_Algorithm(entryPointX, entryPointY, targetX, targetY, Get_Tool_Action_Callback());
84 |
85 | previousCursorX = targetX;
86 | previousCursorY = targetY;
87 | document.removeEventListener("mousemove", Track_Mouse_Outside);
88 | }
89 | }
90 | });
91 |
92 | function Track_Mouse_Outside(e)
93 | {
94 | const canvasRect = canvasDiv.getBoundingClientRect();
95 | lastOutsideX = (e.clientX - canvasRect.left) / CELL_WIDTH_PX;
96 | lastOutsideY = (e.clientY - canvasRect.top) / CELL_WIDTH_PX;
97 | // console.log(`Mouse Outside Position: (${lastOutsideX}, ${lastOutsideY})`);
98 | }
99 | }
100 |
101 | function Add_EventHandlers_To_Palette_Cells()
102 | {
103 | const allPaletteCells = document.querySelectorAll(".paletteCell");
104 | allPaletteCells.forEach(function (cell) {
105 | cell.addEventListener("click", function (e) {
106 | STATE[ACTIVE_COLOR_SELECT] = e.target.style.backgroundColor;
107 | Update_Active_Color_Preview();
108 | Update_Active_Color_Label();
109 | })
110 | })
111 | }
112 |
113 | function Add_EventHandlers_To_Color_Preview()
114 | {
115 | const allColorPreviews = document.querySelectorAll(".active-color-preview");
116 | allColorPreviews.forEach(function (preview) {
117 | preview.addEventListener("click", function (e) {
118 | Swap_Active_Color()
119 | })
120 | })
121 | }
122 |
123 | function Add_EventHandlers_To_Canvas_Cells()
124 | {
125 | function Create_Selection_Div(e)
126 | {
127 | const canvasDiv = document.getElementById("canvas-div");
128 |
129 | let selection = document.createElement("div");
130 | selection.id = "selection";
131 |
132 | const cursorXY = Canvas_Cursor_XY_Rounded_To_Neareset_Cell_Corner(e);
133 | selection.style.left = cursorXY[0] + "px";
134 | selection.style.top = cursorXY[1] + "px";
135 |
136 | STATE["selection"]["startX"] = cursorXY[0];
137 | STATE["selection"]["startY"] = cursorXY[1];
138 |
139 | canvasDiv.appendChild(selection);
140 | }
141 |
142 | function Selection_Mousedown(e)
143 | {
144 | if (STATE["activeTool"] === "selection") {
145 | let selection = document.getElementById("selection");
146 | let cursorXY = Canvas_Cursor_XY(e);
147 |
148 | if ((STATE["selection"]["isLocked"] === true) &&
149 | (selection) &&
150 | CursorXY_In_Selection(cursorXY, selection)) {
151 | if (STATE["altKeyDown"] === true) {
152 | STATE["selection"]["floatingCopy"] = true;
153 |
154 | let colorArray = Canvas_Pixels_From_Selection();
155 | STATE["selectionCopy"]["colorArray"] = colorArray;
156 |
157 | STATE["selectionCopy"]["initCursorX"] = cursorXY[0] / CELL_WIDTH_PX;
158 | STATE["selectionCopy"]["initCursorY"] = cursorXY[1] / CELL_WIDTH_PX;
159 | } else {
160 |
161 | let colorArray = Canvas_Pixels_From_Selection();
162 | STATE["selectionCopy"]["colorArray"] = colorArray;
163 |
164 | let origLeft = Px_To_Int(selection.style.left) / CELL_WIDTH_PX;
165 | let origTop = Px_To_Int(selection.style.top) / CELL_WIDTH_PX;
166 |
167 | STATE["selection"]["isMoving"] = true;
168 | STATE["selectionMove"] = {
169 | initCursorX: cursorXY[0] / CELL_WIDTH_PX,
170 | initCursorY: cursorXY[1] / CELL_WIDTH_PX,
171 | initLeft: origLeft,
172 | initTop: origTop
173 | };
174 | }
175 | } else {
176 | Remove_Selection();
177 | Unlock_Selection();
178 | Create_Selection_Div(e);
179 | }
180 | }
181 | }
182 |
183 | function Selection_Mousemove(e)
184 | {
185 | const selection = document.getElementById("selection");
186 |
187 | if ((STATE["activeTool"] === "selection") &&
188 | (STATE["selection"]["isLocked"] === false)) {
189 | const canvasDiv = document.getElementById("canvas-div");
190 |
191 | if (!selection)
192 | return;
193 |
194 | const cursorXY = Canvas_Cursor_XY_Rounded_To_Neareset_Cell_Corner(e);
195 | if (cursorXY[0] < STATE["selection"]["startX"]) {
196 | selection.style.left = cursorXY[0] + "px";
197 | selection.style.width = Math.abs(cursorXY[0] - STATE["selection"]["startX"]) + "px";
198 | } else {
199 | let newWidth = cursorXY[0] - Px_To_Int(selection.style.left);
200 | newWidth = Math.ceil(newWidth);
201 | newWidth = newWidth - (newWidth % CELL_WIDTH_PX);
202 |
203 | selection.style.left = STATE["selection"]["startX"] + "px";
204 | selection.style.width = newWidth + "px";
205 | }
206 |
207 | if (cursorXY[1] < STATE["selection"]["startY"]) {
208 | selection.style.top = cursorXY[1] + "px";
209 | selection.style.height = Math.abs(cursorXY[1] - STATE["selection"]["startY"]) + "px";
210 | } else {
211 | let newHeight = cursorXY[1] - Px_To_Int(selection.style.top);
212 | newHeight = Math.floor(newHeight);
213 | newHeight = newHeight - (newHeight % CELL_WIDTH_PX);
214 |
215 | selection.style.top = STATE["selection"]["startY"] + "px";
216 | selection.style.height = newHeight + "px";
217 | }
218 |
219 | let width = Px_To_Int(selection.style.width);
220 | selection.style.width = (width - 1) + "px";
221 | let height = Px_To_Int(selection.style.height);
222 | selection.style.height = (height - 1) + "px";
223 |
224 | return;
225 | } else
226 | if ((STATE["activeTool"] === "selection") &&
227 | (STATE["selection"]["isLocked"] === true))
228 | {
229 | let cursorXY = Canvas_Cursor_XY(e);
230 | if (STATE["selection"]["floatingCopy"] === true) {
231 | Set_Cursor("move"); // drag copied selection
232 | let dx = (cursorXY[0] / CELL_WIDTH_PX) - STATE["selectionCopy"]["initCursorX"];
233 | let dy = (cursorXY[1] / CELL_WIDTH_PX) - STATE["selectionCopy"]["initCursorY"];
234 | let selectionLeft = Px_To_Int(selection.style.left) / CELL_WIDTH_PX;
235 | let selectionTop = Px_To_Int(selection.style.top) / CELL_WIDTH_PX;
236 |
237 | let selectionWidth = (Px_To_Int(selection.style.width) + 1) / CELL_WIDTH_PX;
238 | let selectionHeight = (Px_To_Int(selection.style.height) + 1) / CELL_WIDTH_PX;
239 |
240 | let SelectionOffScreen = (
241 | (selectionTop + dy < 0) ||
242 | (selectionTop + dy + selectionHeight > CELLS_PER_ROW) ||
243 | (selectionLeft + dx + selectionWidth > CELLS_PER_ROW) ||
244 | (selectionLeft + dx < 0)
245 | );
246 |
247 | if (SelectionOffScreen) { return; }
248 |
249 | for (let i = 0; i < CELLS_PER_ROW * CELLS_PER_ROW; i += 1) {
250 | let cell = document.getElementById(Pad_Start_Int(i, 4));
251 | cell.style.backgroundColor = HISTORY_STATES.getCurrentState()[i];
252 | }
253 |
254 | let cell0 = Get_CellInt_From_CellXY(selectionLeft + dx, selectionTop + dy);
255 | for (let y = 0; y < selectionHeight; y += 1)
256 | for (let x = 0; x < selectionWidth; x += 1) {
257 | let id = Pad_Start_Int(cell0 + y * CELLS_PER_ROW + x);
258 | let cell = document.getElementById(id);
259 | let idx = x + y * selectionWidth;
260 | let color = STATE["selectionCopy"]["colorArray"][idx];
261 | if (color && color !== "transparent") {
262 | cell.style.backgroundColor = color;
263 | }
264 | }
265 |
266 | STATE["selectionCopy"]["left"] = selectionLeft + dx;
267 | STATE["selectionCopy"]["top"] = selectionTop + dy;
268 | } else if (STATE["selection"]["isMoving"] === true) {
269 | Set_Cursor("move");
270 | let dx = (cursorXY[0] / CELL_WIDTH_PX) - STATE["selectionMove"]["initCursorX"];
271 | let dy = (cursorXY[1] / CELL_WIDTH_PX) - STATE["selectionMove"]["initCursorY"];
272 |
273 | let newLeft = STATE["selectionMove"]["initLeft"] + dx;
274 | let newTop = STATE["selectionMove"]["initTop"] + dy;
275 |
276 | let selectionWidth = (Px_To_Int(selection.style.width) + 1) / CELL_WIDTH_PX;
277 | let selectionHeight = (Px_To_Int(selection.style.height) + 1) / CELL_WIDTH_PX;
278 |
279 | if (
280 | newLeft < 0 || newTop < 0 ||
281 | (newLeft + selectionWidth) > CELLS_PER_ROW ||
282 | (newTop + selectionHeight) > CELLS_PER_ROW
283 | ) {
284 | return;
285 | }
286 |
287 | if (!STATE["selection"]["originalCleared"]) {
288 | for (let y = 0; y < selectionHeight; y++) {
289 | for (let x = 0; x < selectionWidth; x++) {
290 | let index = (STATE["selectionMove"]["initTop"] + y) * CELLS_PER_ROW +
291 | (STATE["selectionMove"]["initLeft"] + x);
292 | HISTORY_STATES.getCurrentState()[index] = "transparent";
293 | }
294 | }
295 | STATE["selection"]["originalCleared"] = true;
296 | }
297 |
298 | for (let i = 0; i < CELLS_PER_ROW * CELLS_PER_ROW; i++) {
299 | let cell = document.getElementById(Pad_Start_Int(i, 4));
300 | cell.style.backgroundColor = HISTORY_STATES.getCurrentState()[i];
301 | }
302 |
303 | let cell0 = Get_CellInt_From_CellXY(newLeft, newTop);
304 | for (let y = 0; y < selectionHeight; y++) {
305 | for (let x = 0; x < selectionWidth; x++) {
306 | let id = Pad_Start_Int(cell0 + y * CELLS_PER_ROW + x);
307 | let cell = document.getElementById(id);
308 | let idx = x + y * selectionWidth;
309 | let color = STATE["selectionCopy"]["colorArray"][idx];
310 | if (color && color !== "transparent") {
311 | cell.style.backgroundColor = color;
312 | }
313 | }
314 | }
315 |
316 | selection.style.left = newLeft * CELL_WIDTH_PX + "px";
317 | selection.style.top = newTop * CELL_WIDTH_PX + "px";
318 | } else
319 | if (CursorXY_In_Selection(cursorXY, selection) && STATE["altKeyDown"] === true) {
320 | Set_Cursor("move");
321 | }
322 | }
323 | }
324 |
325 | function Selection_Mouseup(e)
326 | {
327 | if (STATE["activeTool"] === "selection" &&
328 | STATE["selection"]["isLocked"] === false) {
329 | let selection = document.getElementById("selection");
330 | let selectionWidth = selection.style.width;
331 | let selectionHeight = selection.style.height;
332 |
333 | if ((selectionWidth === "0px") || (selectionWidth === "") ||
334 | (selectionHeight === "0px") || (selectionHeight === "")) {
335 | Remove_Selection();
336 | Unlock_Selection();
337 | } else {
338 | Selection_Locked_To_Grid();
339 |
340 | if (STATE["selection"]["totalCount"] < 3)
341 | Alert_User("Alt to copy");
342 | STATE["selection"]["totalCount"] += 1;
343 | }
344 | } else
345 | if (STATE["activeTool"] === "selection" &&
346 | STATE["selection"]["isLocked"] === true) {
347 | let selection = document.getElementById("selection");
348 |
349 | if (STATE["selection"]["floatingCopy"] === true) {
350 | selection.style.left = STATE["selectionCopy"]["left"] * CELL_WIDTH_PX + "px";
351 | selection.style.top = STATE["selectionCopy"]["top"] * CELL_WIDTH_PX + "px";
352 | STATE["selection"]["floatingCopy"] = false;
353 | }
354 | if (STATE["selection"]["isMoving"] === true) {
355 | STATE["selection"]["isMoving"] = false;
356 | STATE["selection"]["originalCleared"] = false;
357 | }
358 | if (STATE["altKeyDown"] === false) {
359 | Set_Cursor(Tools["selection"]["cursor"]);
360 | }
361 | }
362 | }
363 |
364 | function Tool_Action_On_Canvas_Cell(e)
365 | {
366 | const cell = e.target;
367 | const x = Math.floor(cell.offsetLeft / CELL_WIDTH_PX);
368 | const y = Math.floor(cell.offsetTop / CELL_WIDTH_PX);
369 | const callback = Get_Tool_Action_Callback();
370 |
371 | if (typeof callback === 'function') {
372 | if (previousCursorX !== null && previousCursorY !== null) {
373 | Bresenham_Line_Algorithm(previousCursorX, previousCursorY, x, y, callback, true);
374 | } else {
375 | Save_Canvas_State();
376 | }
377 | previousCursorX = x;
378 | previousCursorY = y;
379 | callback(cell);
380 | }
381 | }
382 |
383 | const canvasCells = document.querySelectorAll(".canvasCell");
384 | for (let i = 0; i < CELLS_PER_ROW * CELLS_PER_ROW; i += 1) {
385 | canvasCells[i].addEventListener("mousedown", function (e) {
386 | Reset_Previous_Cursor_Position();
387 | Selection_Mousedown(e);
388 | Tool_Action_On_Canvas_Cell(e);
389 | });
390 | canvasCells[i].addEventListener("mousemove", Selection_Mousemove);
391 | canvasCells[i].addEventListener("mouseup", Selection_Mouseup);
392 | canvasCells[i].addEventListener("mousedown", Tool_Action_On_Canvas_Cell);
393 |
394 | canvasCells[i].addEventListener("mousemove", function (e) {
395 | if (STATE["brushDown"]) {
396 | Tool_Action_On_Canvas_Cell(e);
397 | }
398 | });
399 |
400 | canvasCells[i].addEventListener("mouseup", function (e) {
401 | let cursor = Get_Cursor();
402 | if (cursor.includes("fill.png")) {
403 | let cell_id = e.target.id;
404 | let target_color = e.target.style.backgroundColor;
405 | let replacement_color = STATE[ACTIVE_COLOR_SELECT];
406 |
407 | Flood_Fill_Algorithm(cell_id, target_color, replacement_color);
408 | }
409 | Reset_Previous_Cursor_Position();
410 | });
411 |
412 | canvasCells[i].addEventListener("click", function (e) {
413 | let cursor = Get_Cursor();
414 | if (cursor.includes("colorpicker.png")) {
415 | const cell = e.target;
416 | const pickedColor = cell.style.backgroundColor;
417 | STATE[ACTIVE_COLOR_SELECT] = pickedColor;
418 | Update_Active_Color_Preview();
419 | Update_Active_Color_Label();
420 | }
421 | });
422 | }
423 |
424 | document.addEventListener("mouseup", function (e) {
425 | Exit_Drawing_Mode();
426 | Reset_Previous_Cursor_Position();
427 | if (e.target.id !== "undo-button" && e.target.id !== "redo-button") {
428 | Save_Canvas_State();
429 | }
430 | });
431 | }
432 |
433 | function Add_EventHandlers_To_Toolbar_Buttons()
434 | {
435 | const toolbarButtons = document.querySelectorAll(".toolbarButton");
436 | toolbarButtons.forEach(button => {
437 | switch (button.id) {
438 | case "undo-button":
439 | button.addEventListener("click", Undo);
440 | break;
441 | case "redo-button":
442 | button.addEventListener("click", Redo);
443 | break;
444 | case "pencil-button":
445 | button.addEventListener("click", () => Activate_Tool("pencil"));
446 | break;
447 | case "fill-button":
448 | button.addEventListener("click", () => Activate_Tool("fill"));
449 | break;
450 | case "eraser-button":
451 | button.addEventListener("click", () => Activate_Tool("eraser"));
452 | break;
453 | case "selection-button":
454 | button.addEventListener("click", () => Activate_Tool("selection"));
455 | break;
456 | case "colorpicker-button":
457 | button.addEventListener("click", () => Activate_Tool("colorpicker"));
458 | break;
459 | case "grid-button":
460 | button.addEventListener("click", Toggle_Grid);
461 | break;
462 | }
463 | });
464 | }
465 |
466 | function Add_EventHandlers_To_Save_Button()
467 | {
468 | function Save_To_PNG() {
469 | let temporaryCanvas = document.createElement("canvas");
470 | let width = CELLS_PER_ROW;
471 | let height = CELLS_PER_ROW;
472 |
473 | temporaryCanvas.width = width;
474 | temporaryCanvas.height = height;
475 |
476 | let context = temporaryCanvas.getContext("2d");
477 | let imageData = context.createImageData(width, height);
478 |
479 | let pixelIndex = 0;
480 | Get_Canvas_Pixels().forEach(function (pixel) {
481 | let rgbArray = Get_Array_From_Rgb(pixel);
482 | imageData.data[pixelIndex] = rgbArray[0];
483 | imageData.data[pixelIndex + 1] = rgbArray[1];
484 | imageData.data[pixelIndex + 2] = rgbArray[2];
485 | imageData.data[pixelIndex + 3] = rgbArray[3] !== undefined ? rgbArray[3] : (pixel === "transparent" ? 0 : 255);
486 | pixelIndex += 4;
487 | });
488 | context.putImageData(imageData, 0, 0);
489 |
490 | let download = document.createElement('a');
491 | download.href = temporaryCanvas.toDataURL("image/png");
492 | download.download = 'pixelpaint.png';
493 | download.click();
494 |
495 | Alert_User("Saved!");
496 | }
497 |
498 | let saveButton = document.getElementById("save-button");
499 | saveButton.addEventListener("click", Save_To_PNG);
500 | }
501 |
502 | function Exit_Drawing_Mode()
503 | {
504 | STATE["brushDown"] = false;
505 | }
506 |
507 | function Reset_Previous_Cursor_Position()
508 | {
509 | previousCursorX = null;
510 | previousCursorY = null;
511 | }
512 |
513 | function Add_EventHandlers_To_Document()
514 | {
515 | const keyDownHandler = function (e) {
516 | if (e.code === "AltLeft" || e.code === "AltRight") {
517 | STATE["altKeyDown"] = true;
518 |
519 | if (STATE["activeTool"] === "selection" &&
520 | STATE["selection"]["isLocked"] === true &&
521 | STATE["selection"]["isLocked"] === true) {
522 | Set_Cursor("move");
523 | }
524 | }
525 | if (e.code === "Delete" || e.code === "Backspace") {
526 | Delete_Selected();
527 | Save_Canvas_State();
528 | }
529 | if (e.code === "Escape") {
530 | Remove_Selection();
531 | Unlock_Selection();
532 | Set_Cursor(Tools[STATE["activeTool"]]["cursor"]);
533 | STATE["selection"]["floatingCopy"] = false;
534 | }
535 | if (e.code === "KeyZ") {
536 | Undo();
537 | }
538 | if (e.code === "KeyX") {
539 | Redo();
540 | }
541 | if (e.code === "KeyC") {
542 | Swap_Active_Color();
543 | }
544 |
545 | if (e.code === STATE["grid"]["hotkey"]) {
546 | STATE["grid"]["KeyG_Counter"] += 1;
547 | }
548 |
549 | for (const [toolLabel, toolConfig] of Object.entries(Tools)) {
550 | if (e.code === toolConfig["hotkey"]) {
551 | Activate_Tool(toolLabel);
552 | Reset_Previous_Cursor_Position();
553 | break;
554 | }
555 | }
556 | };
557 | const keyUpHandler = function (e) {
558 | if (e.code === "AltLeft" || e.code === "AltRight") {
559 | STATE["altKeyDown"] = false;
560 |
561 | if (STATE["activeTool"] === "selection" &&
562 | STATE["selection"]["floatingCopy"] === false) {
563 | Set_Cursor(Tools["selection"]["cursor"]);
564 | }
565 | }
566 | if (e.code === STATE["grid"]["hotkey"]) {
567 | STATE["grid"]["KeyG_Counter"] = 0;
568 | Toggle_Grid();
569 | }
570 | };
571 | document.addEventListener("keydown", keyDownHandler);
572 | document.addEventListener("keyup", keyUpHandler);
573 | }
574 |
575 | function Add_EventHandlers()
576 | {
577 | Add_EventHandlers_To_Canvas_Cells();
578 | Add_EventHandlers_To_Canvas_Div();
579 | Add_EventHandlers_To_Document();
580 | Add_EventHandlers_To_Palette_Cells();
581 | Add_EventHandlers_To_Color_Preview();
582 | Add_EventHandlers_To_Save_Button();
583 | Add_EventHandlers_To_Toolbar_Buttons();
584 | }
--------------------------------------------------------------------------------
/js/historyStates.js:
--------------------------------------------------------------------------------
1 | class History_States {
2 | constructor(maxSize) {
3 | let arr = new Array(CELLS_PER_ROW * CELLS_PER_ROW).fill(CANVAS_INIT_COLOR);
4 | this.array = [arr]; // empty array
5 | this.ptr = 0; // pointer
6 | this.maxSize = maxSize; // maximum size
7 | }
8 | getCurrentState() {
9 | return this.array[this.ptr];
10 | }
11 | decPtr() {
12 | if (this.ptr > 0) {
13 | this.ptr--;
14 | }
15 | }
16 | incPtr() {
17 | if (this.ptr < this.array.length - 1) {
18 | this.ptr++;
19 | }
20 | }
21 | pushToPtr(item) {
22 | if (this._Can_Push(item)) {
23 | this.ptr++;
24 | this.array.splice(this.ptr, 0, item);
25 | }
26 |
27 | // slice off array after ptr
28 | this.array = this.array.slice(0, this.ptr + 1);
29 |
30 | this._manageSize();
31 | }
32 | _Can_Push(item) {
33 | if (this.ptr === 0) {
34 | return true;
35 | }
36 | return !_Arrays_Are_Equal(this.array[this.ptr], item);
37 | }
38 |
39 | _manageSize() {
40 | if (this.array.length > this.maxSize) {
41 | this.array.shift();
42 | this.ptr--;
43 | }
44 | }
45 | ptrToEndOfStateArray(item) {
46 | this.ptr = this.array.length - 1;
47 | }
48 | print() {
49 | console.log(this);
50 | }
51 | }
52 |
53 | function _Arrays_Are_Equal(a, b)
54 | {
55 | if (a === b) {
56 | return true;
57 | }
58 | if (a.length !== b.length) {
59 | return false;
60 | }
61 | for (let i = 0; i < a.length; i++) {
62 | if (a[i] !== b[i]) {
63 | return false;
64 | }
65 | }
66 | return true;
67 | }
68 |
69 | function Save_Canvas_State()
70 | {
71 | let canvasPixels = Get_Canvas_Pixels();
72 | HISTORY_STATES.pushToPtr(canvasPixels);
73 | }
74 |
--------------------------------------------------------------------------------
/js/palette.js:
--------------------------------------------------------------------------------
1 | //NES color palette
2 | let palette_color_array = [
3 | "#fcfcfc",
4 | "#f8f8f8",
5 | "#bcbcbc",
6 | "#7c7c7c",
7 |
8 | "#a4e4fc",
9 | "#3cbcfc",
10 | "#0078f8",
11 | "#0007fc",
12 |
13 | "#b8b8f8",
14 | "#6888fc",
15 | "#0059f8",
16 | "#0004bc",
17 |
18 | "#d8b8f8",
19 | "#9878f8",
20 | "#6846fc",
21 | "#432abc",
22 |
23 | "#f8b8f8",
24 | "#f878f8",
25 | "#d801cc",
26 | "#940084",
27 |
28 | "#f8a4c0",
29 | "#f878f8",
30 | "#d801cc",
31 | "#a80020",
32 |
33 | "#f0cfb0",
34 | "#f87758",
35 | "#f83701",
36 | "#a80e00",
37 |
38 | "#fce0a8",
39 | "#fc9f44",
40 | "#e45b11",
41 | "#881400",
42 |
43 | "#f8d878",
44 | "#f8b801",
45 | "#ac7b01",
46 | "#503000",
47 |
48 | "#d8f878",
49 | "#b9f819",
50 | "#02b801",
51 | "#017800",
52 |
53 | "#b8f8b8",
54 | "#58d854",
55 | "#01a801",
56 | "#016800",
57 |
58 | "#b8f8d8",
59 | "#58f898",
60 | "#01a844",
61 | "#015800",
62 |
63 | "#00fcfc",
64 | "#00e8d8",
65 | "#008888",
66 | "#004058",
67 |
68 | "#c4c4c4",
69 | "#787878",
70 | "#000001",
71 | "#000000",
72 | ];
73 |
74 | //Classic Gameboy and Gameboy pocket color palettes
75 | let gb_palette_color_array = [
76 | "#9bbc0f",
77 | "#8bac0f",
78 | "#306230",
79 | "#0f380f",
80 |
81 | "#ffffff",
82 | "#a9a9a9",
83 | "#545454",
84 | "#000000",
85 | ];
86 |
--------------------------------------------------------------------------------
/js/script.js:
--------------------------------------------------------------------------------
1 | const CELLS_PER_ROW = 32;
2 | const CELL_WIDTH_PX = 16;
3 | const MAX_UNDOS = 35;
4 | const GRID_OUTLINE_CSS = "1px dashed #aaa";
5 | const SELECTION_LOCKED_OUTLINE = "1px dashed #ff0000";
6 | const BUTTON_UP_COLOR = "#a0a0a0";
7 | const BUTTON_UP_OUTLINE = "";
8 | const BUTTON_DOWN_COLOR = "#f0f0f0";
9 | const BUTTON_DOWN_OUTLINE = "1px solid blue";
10 | const CANVAS_INIT_COLOR = "transparent";
11 | let ACTIVE_COLOR_SELECT = "firstColor";
12 | const STATE = {
13 | "firstColor": palette_color_array[palette_color_array.length - 1],
14 | "secondColor": palette_color_array[0],
15 | "activeTool": "pencil-button",
16 | "brushDown": false,
17 | "grid": {
18 | "KeyG_Counter": 0,
19 | "hotkey": "KeyG",
20 | "isToggled": false,
21 | },
22 | "altKeyDown": false,
23 | "selection": {
24 | "totalCount": 0,
25 | "startX": 0,
26 | "startY": 0,
27 | "isLocked": false,
28 | "floatingCopy": false,
29 | },
30 | "selectionCopy": {
31 | "initCursorX": 0,
32 | "initCursorY": 0,
33 | "left": 0,
34 | "top": 0,
35 | "colorArray": [],
36 | },
37 | }
38 | const PALETTE_DISPLAY = {
39 | nes: document.getElementById("palette-div"),
40 | gameboy: document.getElementById("gameboy-palette-div"),
41 | radioNES: document.getElementById("radioNES"),
42 | radioGameboy: document.getElementById("radioGB")
43 | }
44 |
45 | const HISTORY_STATES = new History_States(MAX_UNDOS);
46 | Save_Canvas_State();
47 |
48 | const dropShadowPreview = document.getElementById("drop-shadow-preview");
49 | const canvasDiv = document.getElementById("canvas-div");
50 |
51 | function Canvas_Cursor_XY(e)
52 | {
53 | let parentCell = e.target.closest("div.canvasCell");
54 | let x = parentCell.offsetLeft;
55 | let y = parentCell.offsetTop;
56 | return [x, y];
57 | }
58 |
59 | function Canvas_Cursor_XY_Rounded_To_Neareset_Cell_Corner(e)
60 | {
61 | let parentCell = e.target.closest("div.canvasCell");
62 | let parentCellId = parseInt(parentCell.id);
63 | const maxId = Math.pow(CELLS_PER_ROW, 2) - 1;
64 |
65 | let x = 0;
66 | if( e.offsetX <= Math.floor( CELL_WIDTH_PX / 2 ) )
67 | {
68 | x = parentCell.offsetLeft;
69 | }
70 | else if( Get_X_From_CellInt(Number(parentCellId)) === CELLS_PER_ROW - 1 )
71 | {
72 | x = parentCell.offsetLeft + CELL_WIDTH_PX; // right side of canvas
73 | }
74 | else
75 | {
76 | let rightCell = document.getElementById(Pad_Start_Int(parentCellId+1));
77 | x = rightCell.offsetLeft;
78 | }
79 |
80 | let y = 0;
81 | if( e.offsetY <= Math.floor( CELL_WIDTH_PX / 2 ) )
82 | {
83 | y = parentCell.offsetTop;
84 | }
85 | else
86 | if( parseInt(parentCell.id)+CELLS_PER_ROW > maxId ) // bottom of canvas
87 | {
88 | y = parentCell.offsetTop + CELL_WIDTH_PX;
89 | }
90 | else
91 | {
92 | let cellIdBelow = Pad_Start_Int(parentCellId + CELLS_PER_ROW);
93 | let belowCell = document.getElementById(cellIdBelow);
94 | y = document.getElementById(cellIdBelow).offsetTop;
95 | }
96 | return [x, y];
97 | }
98 |
99 | function Add_Ids_To_Palette_Cells()
100 | {
101 | const allPaletteCells = document.querySelectorAll(".paletteCell");
102 | let j = 0;
103 | allPaletteCells.forEach(function(item){
104 | item.id = "palette-cell-"+j;
105 | j += 1;
106 | })
107 | }
108 |
109 | function Update_Active_Color_Preview()
110 | {
111 | let activeColorDiv_1 = document.getElementById("active-color-preview-1");
112 | let activeColorDiv_2 = document.getElementById("active-color-preview-2");
113 | activeColorDiv_1.style.backgroundColor = STATE["firstColor"];
114 | activeColorDiv_2.style.backgroundColor = STATE["secondColor"];
115 | Update_Active_Color_Label();
116 | }
117 |
118 | function Alert_User(text)
119 | {
120 | let popupMessage = document.getElementById("popup-message");
121 | popupMessage.classList.remove("fadeOutAnimation");
122 | void popupMessage.offsetWidth;
123 | popupMessage.innerHTML = text;
124 | popupMessage.style.animationPlayState = "running";
125 | popupMessage.classList.add("fadeOutAnimation");
126 | }
127 |
128 | function Swap_Active_Color()
129 | {
130 | let activeColorDiv_1 = document.getElementById("active-color-preview-1");
131 | let activeColorDiv_2 = document.getElementById("active-color-preview-2");
132 | if(ACTIVE_COLOR_SELECT === "firstColor")
133 | {
134 | ACTIVE_COLOR_SELECT = "secondColor";
135 | activeColorDiv_2.classList.add("active_indicator");
136 | activeColorDiv_1.classList.remove("active_indicator");
137 | }
138 | else
139 | {
140 | ACTIVE_COLOR_SELECT = "firstColor";
141 | activeColorDiv_1.classList.add("active_indicator");
142 | activeColorDiv_2.classList.remove("active_indicator");
143 | }
144 | Update_Active_Color_Label();
145 | }
146 |
147 | function Update_Active_Color_Label()
148 | {
149 | activeColorLabel = document.getElementById("active-color-label");
150 |
151 | STATE[ACTIVE_COLOR_SELECT] = Rgb_To_Hex(STATE[ACTIVE_COLOR_SELECT]);
152 | activeColorLabel.innerHTML = STATE[ACTIVE_COLOR_SELECT]; // label
153 | }
154 |
155 | function Add_EventHandlers_To_Palette_Cells()
156 | {
157 | const allPaletteCells = document.querySelectorAll(".paletteCell");
158 | allPaletteCells.forEach(function(cell){
159 | // click palette to change color
160 | cell.addEventListener("click", function(e){
161 | STATE[ACTIVE_COLOR_SELECT] = e.target.style.backgroundColor;
162 | Update_Active_Color_Preview();
163 | Update_Active_Color_Label();
164 | })
165 | })
166 | }
167 |
168 | function Canvas_Pixels_From_Selection(selection)
169 | {
170 | let selectionLeft = Px_To_Int(selection.style.left);
171 | let selectionTop = Px_To_Int(selection.style.top);
172 | let selectionWidth = Px_To_Int(selection.style.width);
173 | let selectionHeight = Px_To_Int(selection.style.height);
174 | }
175 |
176 | function Reset_Color_Of_Canvas_Cells()
177 | {
178 | let canvasCells = document.querySelectorAll(".canvasCell");
179 | for(let i=0; i= selectionLeft) &&
245 | (cursorXY[0] <= selectionLeft + selectionWidth) &&
246 | (cursorXY[1] >= selectionTop) &&
247 | (cursorXY[1] <= selectionTop + selectionHeight) )
248 | return 1;
249 | return 0;
250 | }
251 |
252 | function Selection_Locked_To_Grid()
253 | {
254 | STATE["selection"]["isLocked"] = true;
255 |
256 | let selection = document.getElementById("selection");
257 | selection.style.outline = SELECTION_LOCKED_OUTLINE;
258 | }
259 |
260 | function Unlock_Selection()
261 | {
262 | STATE["selection"]["isLocked"] = false;
263 | }
264 |
265 | function Toggle_Grid(e)
266 | {
267 | const gridButton = document.getElementById("grid-button");
268 | const canvasCells = document.querySelectorAll(".canvasCell");
269 |
270 | if(STATE["grid"]["isToggled"] === false)
271 | {
272 | Color_Toolbar_Button_As_Down(gridButton);
273 | canvasCells.forEach(function(cell) {
274 | cell.style.outline = GRID_OUTLINE_CSS;
275 | })
276 | STATE["grid"]["isToggled"] = true;
277 | }
278 | else
279 | {
280 | Color_Toolbar_Button_When_Up(gridButton);
281 | canvasCells.forEach(function(cell) {
282 | cell.style.outline = "";
283 | })
284 | STATE["grid"]["isToggled"] = false;
285 | }
286 | }
287 |
288 | function Transfer_Canvas_State_To_Screen(ptr)
289 | {
290 | let savedCanvas = HISTORY_STATES.array[ptr];
291 | let canvasCells = document.querySelectorAll(".canvasCell");
292 |
293 | for(let i=0; i" + " (" + hotkey[3] + ")" + "";
135 | }
136 | }
137 |
138 | function Set_Cursor(newCursorString)
139 | {
140 | document.getElementById("canvas-div").style.cursor = newCursorString;
141 | }
142 |
143 | function Get_Cursor()
144 | {
145 | return document.getElementById("canvas-div").style.cursor;
146 | }
147 |
148 | function Color_Toolbar_Button_As_Down(elem)
149 | {
150 | elem.style.backgroundColor = BUTTON_DOWN_COLOR;
151 | elem.style.outline = BUTTON_DOWN_OUTLINE;
152 | }
153 |
154 | function Color_Toolbar_Button_When_Up(elem)
155 | {
156 | elem.style.backgroundColor = BUTTON_UP_COLOR;
157 | elem.style.outline = BUTTON_UP_OUTLINE;
158 | }
159 |
160 | function Get_Canvas_Pixels()
161 | {
162 | let canvasCells = document.querySelectorAll(".canvasCell");
163 | let canvasPixels = [];
164 | canvasCells.forEach(function(cell){
165 | canvasPixels.push(cell.style.backgroundColor);
166 | })
167 | return canvasPixels;
168 | }
169 |
170 | function Display_Keyboard_Shortcuts()
171 | {
172 | document.getElementById("info-section").style.opacity = "1";
173 | document.getElementById("info-section").style.backgroundColor = "#2a2a2a";
174 | }
175 |
176 | function Hide_Keyboard_Shortcuts()
177 | {
178 | document.getElementById("info-section").style.opacity = "0";
179 | document.getElementById("info-section").style.backgroundColor = "none";
180 | }
181 |
--------------------------------------------------------------------------------
/main.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --body-height: 556px;
3 | --body-margin: 8px;
4 | --border-radius: 4px;
5 | --button-border-radius: 1px;
6 |
7 | --toolbar-width: 512px;
8 | --toolbar-height: 32px;
9 | --line-height-toolbar-text: 30px;
10 |
11 | --palette-width: 80px;
12 | --color-preview-width: 35px;
13 | --palette-cell-width: 20px;
14 | --palette-cell-height: 20px;
15 | --palette-margin-right: 10px;
16 |
17 | --tooltiptext-background-color: #ffffc9;
18 | --canvas-width: 512px; /* 16*32 */
19 | --canvas-cell-width: 16px;
20 | --body-background-color: #161619;
21 | --blanket-background-color: #2a2a2a;
22 | --toolbar-background-color: #777;
23 |
24 | --shadow: 2px 2px 8px #224;
25 |
26 | --checkerboard_color_light: #FFFFFF;
27 | --checkerboard_color_dark: #CBCBCB;
28 | --checkerboard_square_width: 8px;
29 | }
30 |
31 | .no-select {
32 | -webkit-touch-callout: none; /* iOS Safari */
33 | -webkit-user-select: none; /* Safari */
34 | -khtml-user-select: none; /* Konqueror HTML */
35 | -moz-user-select: none; /* Old versions of Firefox */
36 | -ms-user-select: none; /* Internet Explorer/Edge */
37 | user-select: none; /* Chrome, Opera and Firefox */
38 | }
39 |
40 | body {
41 | position: relative;
42 | transform: translate(-50%, 0);
43 | left: 50%;
44 | color: #efefef;
45 | background-color: var(--body-background-color);
46 | width: fit-content;
47 | height: var(--body-height);
48 | margin: var(--body-margin);
49 | font-family: 'Open Sans', sans-serif;
50 | font-weight: 100;
51 | pointer-events: fill;
52 | }
53 |
54 | .hotkeyText {
55 | color: blue;
56 | }
57 |
58 | #blanket-around-body {
59 | position: relative;
60 | background-color: var(--blanket-background-color);
61 | width: 606px;
62 | height: 554px;
63 | padding-right: 20px;
64 | padding-bottom: 20px;
65 | }
66 |
67 | #selection-explanation-text {
68 | position: fixed;
69 | padding-left: 4px;
70 | left: 0%;
71 | top: 99%;
72 | color: #7c7c7c;
73 | z-index: 100;
74 | }
75 |
76 | #selection-explanation-text b {
77 | color: var(--tooltiptext-background-color);
78 | }
79 |
80 | #selection-explanation-text span {
81 | color: black;
82 | background-color: grey;
83 | padding-left: 2px;
84 | padding-right: 2px;
85 | font-family: Menlo;
86 | }
87 |
88 | #selection-explanation-text img {
89 | display: inline;
90 | width: 16px;
91 | height: 16px;
92 | transform: translate(0, 2px);
93 | margin: 0;
94 | padding: 0;
95 | }
96 |
97 | .imgInSelectionExplanation {
98 | display: inline;
99 | width: 16px;
100 | height: 16px;
101 | transform: translate(0, 2px);
102 | margin: 0;
103 | padding: 0;
104 | }
105 |
106 | .highlightedText {
107 | color: blue;
108 | }
109 |
110 | #popup-message {
111 | position: relative;
112 | float: left;
113 | margin-right: 10px;
114 | color: #000010;
115 | top: 1px;
116 | font-size: 14px;
117 | border: 1px solid black;
118 | border-radius: var(--border-radius);
119 | background-color: var(--tooltiptext-background-color);
120 | padding-left: 4px;
121 | padding-right: 4px;
122 | padding-top: 4px;
123 | padding-bottom: 4px;
124 | }
125 |
126 | .fadeOutAnimation {
127 | animation: fadeOut 3s;
128 | animation-fill-mode: forwards;
129 | animation-play-state: paused;
130 | }
131 |
132 | @keyframes fadeOut
133 | {
134 | 0% {opacity: 0; transform: translate(0, -10px);}
135 | 2% {opacity: 1; transform: none;}
136 | 80% {opacity: 1;}
137 | 100% {opacity: 0;}
138 | }
139 |
140 | #hidden-text-div {
141 | position: absolute;
142 | left: 0;
143 | top: 0;
144 | width: 0;
145 | height: 0;
146 | background-color: red;
147 | }
148 |
149 | .tooltipText {
150 | visibility: hidden;
151 | position: absolute;
152 | z-index: 50;
153 | font-size: 14px;
154 | margin: auto;
155 | color: black;
156 | display: inline-block;
157 | background-color: var(--tooltiptext-background-color);
158 | border-radius: var(--button-border-radius);
159 | padding: 2px;
160 | top: 48px;
161 | }
162 |
163 | .tooltipText::after {
164 | content: "";
165 | position: absolute;
166 | bottom: 100%;
167 | left: 16px;
168 | margin-left: -5px;
169 | border-width: 5px;
170 | border-style: solid;
171 | border-color: transparent transparent var(--tooltiptext-background-color) transparent;
172 | }
173 |
174 | .toolbarButton {
175 | height: 32px;
176 | width: 32px;
177 | padding: 0;
178 | margin: 0;
179 | outline: 0;
180 | margin-right: 2px;
181 | border-radius: 2px;
182 | font-family: 'Open Sans', sans-serif;
183 | float: left;
184 | cursor: pointer;
185 | }
186 | .toolbarButton:hover {
187 | outline: 1px solid var(--tooltiptext-background-color);
188 | }
189 |
190 | .toolbarButton:hover .tooltipText {
191 | visibility: visible;
192 | }
193 |
194 | #toolbar {
195 | position: relative;
196 | width: var(--toolbar-width);
197 | height: var(--toolbar-height);
198 | margin-bottom: 4px;
199 | cursor: default;
200 | border: 1px solid white;
201 | border-radius: var(--border-radius);
202 | background-color: var(--toolbar-background-color);
203 | }
204 |
205 | /* for toolbar icons */
206 | .undoImg {
207 | background-image: url(img/undo.png);
208 | background-position: center;
209 | background-repeat: no-repeat;
210 | }
211 | .redoImg {
212 | background-image: url(img/redo.png);
213 | background-position: center;
214 | background-repeat: no-repeat;
215 | }
216 | .pencilImg {
217 | background-image: url(img/pencil.png);
218 | background-position: center;
219 | background-repeat: no-repeat;
220 | }
221 | .selectionImg {
222 | background-image: url(img/selection.png);
223 | background-position: center;
224 | background-repeat: no-repeat;
225 | }
226 | .fillImg {
227 | background-image: url(img/fill.png);
228 | background-position: center;
229 | background-repeat: no-repeat;
230 | }
231 | .eraserImg {
232 | background-image: url(img/eraserBig.png);
233 | background-position: center;
234 | background-repeat: no-repeat;
235 | }
236 | .colorpickerImg {
237 | background-image: url(img/colorpicker.png);
238 | background-position: center;
239 | background-repeat: no-repeat;
240 | }
241 |
242 | /* other toolbar buttons */
243 | .gridImg {
244 | background-image: url(img/grid.png);
245 | background-position: center;
246 | background-repeat: no-repeat;
247 | }
248 | .saveImg {
249 | background-image: url(img/floppy.png);
250 | background-position: center;
251 | background-repeat: no-repeat;
252 | }
253 |
254 | #add-color-button {
255 | position: relative;
256 | outline: 1px solid black;
257 | cursor: pointer;
258 | margin-bottom: 4px;
259 | background-color: #d1d1d2;
260 | height: var(--palette-cell-height);
261 | width: var(--palette-cell-height);
262 | }
263 |
264 | #add-color-button:hover
265 | {
266 | box-shadow: 0px 0px 10px aqua;
267 | }
268 |
269 | #add-color-button:active
270 | {
271 | box-shadow: none;
272 | }
273 |
274 | #active-color-label {
275 | position: relative;
276 | float: right;
277 | padding-top: auto;
278 | line-height: var(--line-height-toolbar-text);
279 | margin-right: var(--palette-margin-right);
280 | font-family: Menlo;
281 | color: var(--blanket-background-color);
282 | }
283 |
284 | #active-color-preview-1, #active-color-preview-2{
285 | position: absolute;
286 | width: var(--color-preview-width);
287 | height: var(--toolbar-height);
288 | border: 1px solid #eef;
289 | border-radius: var(--border-radius);
290 | margin-left: var(--palette-margin-right);
291 | transform: translate(-2px, -1px);
292 | }
293 | #active-color-preview-1 {
294 | left: var(--toolbar-width);
295 | }
296 | #active-color-preview-2 {
297 | left: calc(var(--toolbar-width) + var(--palette-width) - var(--color-preview-width));
298 | }
299 | .active_indicator {
300 | border: 2px solid #eef !important;
301 | }
302 |
303 | #canvas-div {
304 | position: relative;
305 | width: var(--canvas-width);
306 | height: var(--canvas-width);
307 | margin-right: var(--palette-margin-right);
308 | float: left;
309 | background:
310 | linear-gradient(45deg, var(--checkerboard_color_light) 25%, transparent 25%, transparent 75%, var(--checkerboard_color_light) 75%, var(--checkerboard_color_light)),
311 | linear-gradient(45deg, var(--checkerboard_color_light) 25%, transparent 25%, transparent 75%, var(--checkerboard_color_light) 75%, var(--checkerboard_color_light));
312 | background-position: 0 0, var(--checkerboard_square_width) var(--checkerboard_square_width);
313 | background-size: calc(2 * var(--checkerboard_square_width)) calc(2 * var(--checkerboard_square_width));
314 | background-color: var(--checkerboard_color_dark);
315 | }
316 |
317 | #palette-div {
318 | position: relative;
319 | width: var(--palette-width);
320 | float: left;
321 | cursor: default;
322 | }
323 |
324 | #secret-info {
325 | position: fixed;
326 | bottom: 0;
327 | display: none;
328 | }
329 |
330 | .paletteCell {
331 | position: relative;
332 | float: left;
333 | height: var(--palette-cell-height);
334 | width: var(--palette-cell-width);
335 | cursor: pointer;
336 | }
337 |
338 | .textInPaletteCell {
339 | position: relative;
340 | left: var(--palette-width);
341 | height: inherit;
342 | z-index: 1;
343 | outline: 1px dotted grey;
344 | visibility: hidden;
345 | }
346 |
347 | .canvasCell {
348 | width: var(--canvas-cell-width);
349 | height: var(--canvas-cell-width);
350 | pointer-events: auto;
351 | float: left;
352 | }
353 |
354 | #selection {
355 | position: absolute;
356 | z-index: 2;
357 | outline: 1px dashed #ff0000;
358 | pointer-events: none;
359 | }
360 |
361 | #output-div {
362 | position: relative;
363 | left: 40px;
364 | width: 200px;
365 | height: 640px;
366 | float: left;
367 | opacity: 1;
368 | overflow-y: scroll;
369 | background-color: #eee;
370 | }
371 |
372 | .palette {
373 | display: none;
374 | padding-top: 5px;
375 | }
376 |
377 | .container {
378 | font-size: 14.5px;
379 | padding-bottom: 5px;
380 | }
381 |
382 | input[type="radio"] {
383 | margin: 0;
384 | }
385 |
386 | #info-section {
387 | position: fixed;
388 | top: 80%;
389 | right: 1%;
390 | z-index: 1000;
391 | opacity: 0;
392 | background-color: none;
393 | transition: background-color .2s ease;
394 | pointer-events: none;
395 | }
396 |
397 | #keyboard-shortcut-hover {
398 | position: fixed;
399 | right: 0%;
400 | top: 99%;
401 |
402 | background-color: #e9e9a2;
403 | padding: 0 4px;
404 | border-radius: 2px 0 0 0;
405 | color: #0d0c09;
406 | user-select: none;
407 | }
408 |
409 | #keyboard-shortcuts {
410 | border: 1px solid;
411 | width: 350px;
412 | transition: opacity .2s ease;
413 | }
414 |
415 | #drop-shadow-preview {
416 | position: absolute;
417 | pointer-events: none;
418 | display: none;
419 | width: var(--canvas-cell-width);
420 | height: var(--canvas-cell-width);
421 | opacity: 0.5;
422 | }
423 |
424 | /* buy me a coffee support section */
425 | #coffee-support {
426 | background-color: var(--body-background-color);
427 | border: 1px solid #555;
428 | border-radius: var(--border-radius);
429 | padding: 8px 12px;
430 | font-size: 12px;
431 | font-weight: 100;
432 | color: #efefef;
433 | text-align: left;
434 | margin-top: 10px;
435 | }
436 |
437 | #coffee-support a {
438 | color: var(--tooltiptext-background-color);
439 | text-decoration: none;
440 | font-weight: 200;
441 | transition: all 0.2s ease;
442 | }
443 |
444 | #coffee-support a:hover {
445 | color: #ffffff;
446 | text-shadow: 0 0 8px var(--tooltiptext-background-color);
447 | }
448 |
449 | #coffee-support .coffee-emoji {
450 | font-size: 14px;
451 | margin-left: 2px;
452 | }
453 |
454 | #coffee-support .no-pressure {
455 | color: #999;
456 | font-size: 10px;
457 | }
458 |
--------------------------------------------------------------------------------