Merge pull request #13 from unsettledgames/master

Implemented layers
This commit is contained in:
Lospec 2020-09-08 21:40:42 -04:00 committed by GitHub
commit dd7d7decdb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1739 additions and 278 deletions

View File

@ -12,6 +12,15 @@ The next version is mostly focused on adding missing essential features and port
Suggestions / Planned features: Suggestions / Planned features:
- Line tool
- Resize canvas
- Snap brush preview to pixel grid
- Move selection with arrows
- Load palette from LPE file
- Move colours in palette editor
- Duplicate layer
- Hide non-hovered layers
- Custom color picker - Custom color picker
- custom code without dependencies - custom code without dependencies
- more features such as sliders / color modes - more features such as sliders / color modes
@ -23,16 +32,14 @@ Suggestions / Planned features:
- Stack colors when too many - Stack colors when too many
- Fix popups - Fix popups
- Copy/paste - Show colors which would need to be added to palette
- Add as selection
- Show colors which would need to be added to palette
- Palette option remove unused colors - Palette option remove unused colors
- Pixel Grid - Pixel Grid
- Another currentLayer.canvas - Another currentLayer.canvas
- Must be rescaled each zoom - Must be rescaled each zoom
- Possibly add collaborate function using together.js - Possibly add collaborate function
- Bug fix - Bug fix
- Alt + scroll broken - Alt + scroll broken

20
_ext/svg/invisible.svg Normal file
View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 469.44 469.44" style="enable-background:new 0 0 469.44 469.44;" xml:space="preserve">
<g>
<g>
<g>
<path d="M231.147,160.373l67.2,67.2l0.32-3.52c0-35.307-28.693-64-64-64L231.147,160.373z"/>
<path d="M234.667,117.387c58.88,0,106.667,47.787,106.667,106.667c0,13.76-2.773,26.88-7.573,38.933l62.4,62.4
c32.213-26.88,57.6-61.653,73.28-101.333c-37.013-93.653-128-160-234.773-160c-29.867,0-58.453,5.333-85.013,14.933l46.08,45.973
C207.787,120.267,220.907,117.387,234.667,117.387z"/>
<path d="M21.333,59.253l48.64,48.64l9.707,9.707C44.48,145.12,16.64,181.707,0,224.053c36.907,93.653,128,160,234.667,160
c33.067,0,64.64-6.4,93.547-18.027l9.067,9.067l62.187,62.293l27.2-27.093L48.533,32.053L21.333,59.253z M139.307,177.12
l32.96,32.96c-0.96,4.587-1.6,9.173-1.6,13.973c0,35.307,28.693,64,64,64c4.8,0,9.387-0.64,13.867-1.6l32.96,32.96
c-14.187,7.04-29.973,11.307-46.827,11.307C175.787,330.72,128,282.933,128,224.053C128,207.2,132.267,191.413,139.307,177.12z"
/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="486.733px" height="486.733px" viewBox="0 0 486.733 486.733" style="enable-background:new 0 0 486.733 486.733;"
xml:space="preserve">
<g>
<path d="M403.88,196.563h-9.484v-44.388c0-82.099-65.151-150.681-146.582-152.145c-2.225-0.04-6.671-0.04-8.895,0
C157.486,1.494,92.336,70.076,92.336,152.175v44.388h-9.485c-14.616,0-26.538,15.082-26.538,33.709v222.632
c0,18.606,11.922,33.829,26.539,33.829h321.028c14.616,0,26.539-15.223,26.539-33.829V230.272
C430.419,211.646,418.497,196.563,403.88,196.563z M273.442,341.362v67.271c0,7.703-6.449,14.222-14.158,14.222H227.45
c-7.71,0-14.159-6.519-14.159-14.222v-67.271c-7.477-7.36-11.83-17.537-11.83-28.795c0-21.334,16.491-39.666,37.459-40.513
c2.222-0.09,6.673-0.09,8.895,0c20.968,0.847,37.459,19.179,37.459,40.513C285.272,323.825,280.919,334.002,273.442,341.362z
M331.886,196.563h-84.072h-8.895h-84.072v-44.388c0-48.905,39.744-89.342,88.519-89.342c48.775,0,88.521,40.437,88.521,89.342
V196.563z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="486.866px" height="486.866px" viewBox="0 0 486.866 486.866" style="enable-background:new 0 0 486.866 486.866;"
xml:space="preserve">
<g>
<path d="M393.904,214.852h-8.891v-72.198c0-76.962-61.075-141.253-137.411-142.625c-2.084-0.038-6.254-0.038-8.338,0
C162.927,1.4,101.853,65.691,101.853,142.653v1.603c0,16.182,13.118,29.3,29.3,29.3c16.182,0,29.299-13.118,29.299-29.3v-1.603
c0-45.845,37.257-83.752,82.98-83.752s82.981,37.907,82.981,83.752v72.198H92.963c-13.702,0-24.878,14.139-24.878,31.602v208.701
c0,17.44,11.176,31.712,24.878,31.712h300.941c13.703,0,24.878-14.271,24.878-31.712V246.452
C418.783,228.989,407.607,214.852,393.904,214.852z M271.627,350.591v63.062c0,7.222-6.046,13.332-13.273,13.332h-29.841
c-7.228,0-13.273-6.11-13.273-13.332v-63.062c-7.009-6.9-11.09-16.44-11.09-26.993c0-19.999,15.459-37.185,35.115-37.977
c2.083-0.085,6.255-0.085,8.337,0c19.656,0.792,35.115,17.978,35.115,37.977C282.717,334.149,278.637,343.69,271.627,350.591z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

16
_ext/svg/visible.svg Normal file
View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 469.333 469.333" style="enable-background:new 0 0 469.333 469.333;" xml:space="preserve">
<g>
<g>
<g>
<path d="M234.667,170.667c-35.307,0-64,28.693-64,64s28.693,64,64,64s64-28.693,64-64S269.973,170.667,234.667,170.667z"/>
<path d="M234.667,74.667C128,74.667,36.907,141.013,0,234.667c36.907,93.653,128,160,234.667,160
c106.773,0,197.76-66.347,234.667-160C432.427,141.013,341.44,74.667,234.667,74.667z M234.667,341.333
c-58.88,0-106.667-47.787-106.667-106.667S175.787,128,234.667,128s106.667,47.787,106.667,106.667
S293.547,341.333,234.667,341.333z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 875 B

View File

@ -22,6 +22,214 @@ body {
/* Disable Android and iOS callouts*/ /* Disable Android and iOS callouts*/
} }
#layer-properties-menu {
visibility:hidden;
margin: 0;
padding: 0;
top: 0;
right: 0;
width:120px;
text-align:center;
margin-right:200px;
/*border:1px solid color(menu, foreground);*/
list-style:none;
position:relative;
z-index:1200;
list-style-type: none;
background-color: color(base);
position: fixed;
overflow: visible;
li button {
color: color(menu, foreground);
height: 100%;
padding: 10px;
background: none;
border: none;
cursor: pointer;
width:100%;
}
li button:hover {
background-color:color(menu, background);
}
}
.preview-canvas {
image-rendering:optimizeSpeed; /* Legal fallback */
image-rendering:-moz-crisp-edges; /* Firefox */
image-rendering:-o-crisp-edges; /* Opera */
image-rendering:-webkit-optimize-contrast; /* Safari */
image-rendering:optimize-contrast; /* CSS3 Proposed */
image-rendering:crisp-edges; /* CSS4 Proposed */
image-rendering:pixelated; /* CSS4 Proposed */
-ms-interpolation-mode:nearest-neighbor; /* IE8+ */
}
#layers-menu {
&::-webkit-scrollbar {
background: #232125;
width: 1em;
}
&::-webkit-scrollbar-track {
margin-top: -0.125em;
width: 1em;
}
&::-webkit-scrollbar-thumb {
background: #332f35;
border-radius: 0.25em;
border: solid 0.125em #232125; //same color as scrollbar back to fake padding
}
&::-webkit-scrollbar-corner {
background: #232125;
}
scrollbar-color: #332f35 #232125;
scroll-behavior: smooth;
width:200px;
top: 48px;
bottom: 0;
right:0;
padding: 0;
margin: 0;
background-color: color(base);
box-sizing: border-box;
position: fixed;
z-index: 1120;
list-style-type: none;
overflow-y:scroll;
overflow-x:hidden;
// TODO: make the scroll bar a bit fancier
#add-layer-button {
path {
fill:color(menu, foreground);
}
svg {
position:relative;
margin-right:10px;
}
position:relative;
justify-content: center;
display:flex;
align-items:center;
margin-top:2px;
font-size: 1.2em;
color: color(menu, foreground);
height: 100%;
width: 100%;
padding: 17px;
background: none;
border: none;
cursor: pointer;
background-color: color(menu);
transition: color 0.2s, background-color 0.2s;
}
#add-layer-button:hover {
color: color(base, foreground, bold);
background-color: color(base, foreground, default);
}
}
.selected-layer {
ul.layer-buttons li{
visibility:visible;
}
color: color(base, foreground, bold);
background-color: color(base, foreground, default);
}
.layerdragover {
margin-top:5px;
border-top: 3px solid color(base, foreground, bold);
}
.layers-menu-entry {
cursor:pointer;
margin-top:2px;
font-size: 1em;
color: color(base, foreground, text);
background-color: color(menu, background);
display:inline-block;
height:50px;
width:100%;
display:flex;
align-items:center;
ul.layer-buttons {
top:0;
left:0;
margin:0;
padding:0;
box-sizing:border-box;
position:relative;
height:100%;
list-style:none;
path {
fill: color(base, foreground);
}
li:hover {
background: color(base, background, hover);
path {
fill: color(base, foreground, hover);
}
}
.layer-button {
visibility:hidden;
height:50%;
}
}
.lock-layer-button, .hide-layer-button {
color: color(menu, foreground);
background: none;
border: none;
cursor: pointer;
background-color: color(menu);
transition: color 0.2s, background-color 0.2s;
height:100%;
position:relative;
}
canvas {
display:inline-block;
height:50px;
width:50px;
background-color:lightgrey;
left:4px;
}
p {
right:0;
display:inline-block;
padding-left:10px;
height:18px;
overflow:hidden;
position:relative;
}
transition: color 0.1s, background-color 0.1s;
-moz-transition: color 0.1s, background-color 0.1s;
-webkit-transition: color 0.1s, background-color 0.1s;
}
.layers-menu-entry:hover, .selected-layer {
ul.layer-buttons li{
visibility:visible !important;
}
color: color(base, foreground, bold);
background-color: color(base, foreground, default);
}
//don't let svg handle click events, just send to parents //don't let svg handle click events, just send to parents
svg { svg {
pointer-events: none; pointer-events: none;
@ -36,8 +244,8 @@ svg {
} }
.weak { .weak {
font-size: 0.8em; font-size: 0.8em;
color: color(base,foreground,weak); color: color(base,foreground,weak);
} }
.drawingCanvas { .drawingCanvas {
@ -183,9 +391,9 @@ svg {
} }
} }
a { a {
display: block; display: block;
text-decoration: none; text-decoration: none;
box-sizing: border-box; box-sizing: border-box;
} }
} }
} }
@ -229,7 +437,7 @@ svg {
} }
#colors-menu { #colors-menu {
right: 0; right: 200px;
width: 48px; width: 48px;
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
@ -703,6 +911,14 @@ svg {
font-style: italic; font-style: italic;
} }
#editor-mode-info {
font-style:italic;
}
#editor-mode {
display:none;
}
#compatibility-warning { #compatibility-warning {
display: flex; display: flex;
justify-content: center; justify-content: center;

View File

@ -1,3 +1,5 @@
let currentPalette = [];
//adds the given color to the palette //adds the given color to the palette
//input hex color string //input hex color string
//returns list item element //returns list item element
@ -6,7 +8,7 @@ function addColor (newColor) {
//add # at beginning if not present //add # at beginning if not present
if (newColor.charAt(0) != '#') if (newColor.charAt(0) != '#')
newColor = '#' + newColor; newColor = '#' + newColor;
currentPalette.push(newColor);
//create list item //create list item
var listItem = document.createElement('li'); var listItem = document.createElement('li');
@ -45,5 +47,7 @@ function addColor (newColor) {
button.parentElement.firstChild.jscolor.show(); button.parentElement.firstChild.jscolor.show();
}); });
console.log(currentPalette);
return listItem; return listItem;
} }

View File

@ -8,6 +8,7 @@ var nSquaresFilled = 0;
function fillCheckerboard() { function fillCheckerboard() {
// Getting checkerboard context // Getting checkerboard context
var context = checkerBoard.context; var context = checkerBoard.context;
context.clearRect(0, 0, canvasSize[0], canvasSize[1]);
// Cycling through the canvas (using it as a matrix) // Cycling through the canvas (using it as a matrix)
for (var i=0; i<canvasSize[0] / checkerBoardSquareSize; i++) { for (var i=0; i<canvasSize[0] / checkerBoardSquareSize; i++) {

View File

@ -28,10 +28,11 @@ function colorChanged(e) {
var newColor = hexToRgb(e.target.value); var newColor = hexToRgb(e.target.value);
var oldColor = e.target.oldColor; var oldColor = e.target.oldColor;
currentPalette.splice(currentPalette.indexOf("#" + newColor), 1);
newColor.a = 255; newColor.a = 255;
//save undo state //save undo state
//saveHistoryState({type: 'colorchange', newColor: e.target.value, oldColor: rgbToHex(oldColor), canvas: context.getImageData(0, 0, canvasSize[0], canvasSize[1])});
new HistoryStateEditColor(e.target.value.toLowerCase(), rgbToHex(oldColor)); new HistoryStateEditColor(e.target.value.toLowerCase(), rgbToHex(oldColor));
//get the currently selected color //get the currently selected color
@ -82,7 +83,7 @@ function colorChanged(e) {
//set new old color to changed color //set new old color to changed color
e.target.oldColor = newColor; e.target.oldColor = newColor;
currentPalette.push('#' + newColorHex);
//if this is the current color, update the drawing color //if this is the current color, update the drawing color
if (e.target.colorElement.parentElement.classList.contains('selected')) { if (e.target.colorElement.parentElement.classList.contains('selected')) {

View File

@ -20,6 +20,10 @@ function copySelection() {
} }
function pasteSelection() { function pasteSelection() {
// Can't paste if the layer is locked
if (currentLayer.isLocked) {
return;
}
endSelection(); endSelection();
isPasting = true; isPasting = true;

View File

@ -1,7 +1,8 @@
on('click', 'create-button', function (){ on('click', 'create-button', function (){
var width = getValue('size-width'); var width = getValue('size-width');
var height = getValue('size-height'); var height = getValue('size-height');
newPixel(width,height,'asdfg'); var mode = getValue("editor-mode");
newPixel(width, height, mode);
document.getElementById('new-pixel-warning').style.display = 'block'; document.getElementById('new-pixel-warning').style.display = 'block';
//get selected palette name //get selected palette name
@ -16,6 +17,9 @@ on('click', 'create-button', function (){
//reset new form //reset new form
setValue('size-width', 64); setValue('size-width', 64);
setValue('size-height', 64); setValue('size-height', 64);
setValue("editor-mode", 'Advanced')
setText('editor-mode-button', 'Choose a mode...');
setText('palette-button', 'Choose a palette...'); setText('palette-button', 'Choose a palette...');
setText('preset-button', 'Choose a preset...'); setText('preset-button', 'Choose a preset...');
}); });

View File

@ -1,9 +1,11 @@
function createColorPalette(selectedPalette, fillBackground) { function createColorPalette(selectedPalette, fillBackground, deletePreviousPalette = true) {
//remove current palette //remove current palette
colors = document.getElementsByClassName('color-button'); if (deletePreviousPalette) {
while (colors.length > 0) { colors = document.getElementsByClassName('color-button');
colors[0].parentElement.remove(); while (colors.length > 0) {
colors[0].parentElement.remove();
}
} }
var lightestColor = '#000000'; var lightestColor = '#000000';
@ -31,15 +33,49 @@ function createColorPalette(selectedPalette, fillBackground) {
darkestColor = newColor; darkestColor = newColor;
} }
}
//fill bg with lightest color
if (fillBackground) {
currentLayer.context.fillStyle = lightestColor;
currentLayer.context.fillRect(0, 0, canvasSize[0], canvasSize[1]);
} }
//set as current color //set as current color
currentLayer.context.fillStyle = darkestColor; currentLayer.context.fillStyle = darkestColor;
} }
function createPaletteFromLayers() {
let colors = {};
for (let i=0; i<layers.length; i++) {
if (layers[i].menuEntry != null) {
let imageData = layers[i].context.getImageData(0, 0, layers[i].canvasSize[0], layers[i].canvasSize[1]).data;
let dataLength = imageData.length;
for (let j=0; j<dataLength; j += 4) {
if (!isPixelEmpty(imageData[j])) {
let color = imageData[j]+','+imageData[j + 1]+','+imageData[j + 2];
if (!colors[color]) {
colors[color] = {r:imageData[j],g:imageData[j + 1],b:imageData[j + 2]};
//don't allow more than 256 colors to be added
if (Object.keys(colors).length >= settings.maxColorsOnImportedImage) {
alert('The image loaded seems to have more than '+settings.maxColorsOnImportedImage+' colors.');
break;
}
}
}
}
}
}
console.log(colors);
//create array out of colors object
let colorPaletteArray = [];
for (let color in colors) {
if (colors.hasOwnProperty(color)) {
colorPaletteArray.push('#'+rgbToHex(colors[color]));
}
}
console.log('COLOR PALETTE ARRAY', colorPaletteArray);
//create palette form colors array
createColorPalette(colorPaletteArray, false);
}

View File

@ -1,6 +1,5 @@
//draw a line between two points on canvas //draw a line between two points on canvas
function line(x0,y0,x1,y1, brushSize) { function line(x0,y0,x1,y1, brushSize) {
var dx = Math.abs(x1-x0); var dx = Math.abs(x1-x0);
var dy = Math.abs(y1-y0); var dy = Math.abs(y1-y0);
var sx = (x0 < x1 ? 1 : -1); var sx = (x0 < x1 ? 1 : -1);
@ -21,35 +20,17 @@ function line(x0,y0,x1,y1, brushSize) {
//if we've reached the end goal, exit the loop //if we've reached the end goal, exit the loop
if ((x0==x1) && (y0==y1)) break; if ((x0==x1) && (y0==y1)) break;
var e2 = 2*err; var e2 = 2*err;
if (e2 >-dy) {err -=dy; x0+=sx;}
if (e2 < dx) {err +=dx; y0+=sy;}
}
}
//draw a line between two points on canvas if (e2 >-dy) {
function lineOnLayer(x0,y0,x1,y1, brushSize, context) { err -=dy;
x0+=sx;
var dx = Math.abs(x1-x0);
var dy = Math.abs(y1-y0);
var sx = (x0 < x1 ? 1 : -1);
var sy = (y0 < y1 ? 1 : -1);
var err = dx-dy;
while (true) {
//set pixel
// If the current tool is the brush
if (currentTool.name == 'pencil' || currentTool.name == 'rectangle') {
// I fill the rect
context.fillRect(x0-Math.floor(brushSize/2), y0-Math.floor(brushSize/2), brushSize, brushSize);
} else if (currentTool.name == 'eraser') {
// In case I'm using the eraser I must clear the rect
context.clearRect(x0-Math.floor(tool.eraser.brushSize/2), y0-Math.floor(tool.eraser.brushSize/2), tool.eraser.brushSize, tool.eraser.brushSize);
} }
//if we've reached the end goal, exit the loop if (e2 < dx) {
if ((x0==x1) && (y0==y1)) break; err +=dx;
var e2 = 2*err; y0+=sy;
if (e2 >-dy) {err -=dy; x0+=sx;} }
if (e2 < dx) {err +=dx; y0+=sy;}
console.log(x0 + ", " + x1);
} }
} }

96
js/_editorMode.js Normal file
View File

@ -0,0 +1,96 @@
let modes = {
'Basic' : {
description: 'Basic mode is perfect if you want to create simple sprites or try out palettes.'
},
'Advanced' : {
description: 'Choose advanced mode to gain access to features such as layers.'
}
}
let infoBox = document.getElementById('editor-mode-info');
function switchMode(currentMode, mustConfirm = true) {
if (currentMode == 'Basic') {
// Switch to advanced ez pez lemon squez
document.getElementById('switch-mode-button').innerHTML = 'Switch to basic mode';
// Show the layer menus
layerList.style.display = "inline-block";
document.getElementById('layer-button').style.display = 'inline-block';
// Move the palette menu
document.getElementById('colors-menu').style.right = '200px';
pixelEditorMode = 'Advanced';
}
else {
// Switch to basic
if (mustConfirm) {
if (!confirm('Switching to basic mode will flatten all the visible layers. Are you sure you want to continue?')) {
return;
}
}
document.getElementById('switch-mode-button').innerHTML = 'Switch to advanced mode';
// Selecting the current layer
currentLayer.selectLayer();
// Flatten the layers
flatten(true);
// Hide the layer menus
layerList.style.display = 'none';
document.getElementById('layer-button').style.display = 'none';
// Move the palette menu
document.getElementById('colors-menu').style.right = '0px';
pixelEditorMode = 'Basic';
}
}
on('click', 'switch-mode-button', function (e) {
switchMode(pixelEditorMode);
});
// Makes the menu open
on('click', 'editor-mode-button', function (e){
//open or close the preset menu
toggle('editor-mode-button');
toggle('editor-mode-menu');
//close the palette menu
deselect('palette-button');
deselect('palette-menu');
//close the preset menu
deselect('preset-button');
deselect('preset-menu');
//stop the click from propogating to the parent element
e.stopPropagation();
});
//populate preset list in new pixel menu
Object.keys(modes).forEach(function(modeName,index) {
var editorModeMenu = document.getElementById('editor-mode-menu');
//create button
var button = document.createElement('button');
button.appendChild(document.createTextNode(modeName));
//insert new element
editorModeMenu.appendChild(button);
//add click event listener
on('click', button, function() {
//change mode on new pixel
setValue('editor-mode', modeName);
// Change description
infoBox.innerHTML = modes[modeName].description;
//hide the dropdown menu
deselect('editor-mode-menu');
deselect('editor-mode-button');
//set the text of the dropdown to the newly selected mode
setText('editor-mode-button', modeName);
});
});

View File

@ -33,10 +33,31 @@ for (var i = 1; i < mainMenuItems.length; i++) {
//File Menu //File Menu
case 'New': case 'New':
showDialogue('new-pixel'); showDialogue('new-pixel');
break;
case 'Save project':
//create name
var selectedPalette = getText('palette-button');
if (selectedPalette != 'Choose a palette...'){
var paletteAbbreviation = palettes[selectedPalette].abbreviation;
var fileName = 'pixel-'+paletteAbbreviation+'-'+canvasSize[0]+'x'+canvasSize[1]+'.lpe';
} else {
var fileName = 'pixel-'+canvasSize[0]+'x'+canvasSize[1]+'.lpe';
selectedPalette = 'none';
}
//set download link
var linkHolder = document.getElementById('save-project-link-holder');
// create file content
var content = getProjectData();
linkHolder.href = 'data:text/plain;charset=utf-8,' + encodeURIComponent(content);
linkHolder.download = fileName;
linkHolder.click();
ga('send', 'event', 'Pixel Editor Save', selectedPalette, canvasSize[0]+'/'+canvasSize[1]); /*global ga*/
break; break;
case 'Open': case 'Open':
//if a document exists //if a document exists
if (documentCreated) { if (documentCreated) {
//check if the user wants to overwrite //check if the user wants to overwrite
@ -50,9 +71,8 @@ for (var i = 1; i < mainMenuItems.length; i++) {
break; break;
case 'Save as...': case 'Export':
if (documentCreated) { if (documentCreated) {
//create name //create name
var selectedPalette = getText('palette-button'); var selectedPalette = getText('palette-button');
if (selectedPalette != 'Choose a palette...'){ if (selectedPalette != 'Choose a palette...'){
@ -65,13 +85,44 @@ for (var i = 1; i < mainMenuItems.length; i++) {
//set download link //set download link
var linkHolder = document.getElementById('save-image-link-holder'); var linkHolder = document.getElementById('save-image-link-holder');
linkHolder.href = canvas.toDataURL(); // Creating a tmp canvas to flatten everything
var exportCanvas = document.createElement("canvas");
var emptyCanvas = document.createElement("canvas");
var layersCopy = layers.slice();
exportCanvas.width = canvasSize[0];
exportCanvas.height = canvasSize[1];
emptyCanvas.width = canvasSize[0];
emptyCanvas.height = canvasSize[1];
// Sorting the layers by z index
layersCopy.sort((a, b) => (a.canvas.style.zIndex > b.canvas.style.zIndex) ? 1 : -1);
// Merging every layer on the export canvas
for (let i=0; i<layersCopy.length; i++) {
if (layersCopy[i].menuEntry != null && layersCopy[i].isVisible) {
mergeLayers(exportCanvas.getContext('2d'), layersCopy[i].context);
}
// I'm not going to find out why the layer ordering screws up if you don't copy
// a blank canvas when layers[i] is not set as visible, but if you have time to
// spend, feel free to investigate (comment the else, create 3 layers: hide the
// middle one and export, the other 2 will be swapped in their order)
else {
mergeLayers(exportCanvas.getContext('2d'), emptyCanvas.getContext('2d'));
}
}
linkHolder.href = exportCanvas.toDataURL();
linkHolder.download = fileName; linkHolder.download = fileName;
linkHolder.click(); linkHolder.click();
emptyCanvas.remove();
exportCanvas.remove();
//track google event //track google event
ga('send', 'event', 'Pixel Editor Save', selectedPalette, canvasSize[0]+'/'+canvasSize[1]); /*global ga*/ ga('send', 'event', 'Pixel Editor Export', selectedPalette, canvasSize[0]+'/'+canvasSize[1]); /*global ga*/
} }
break; break;
@ -120,7 +171,6 @@ for (var i = 1; i < mainMenuItems.length; i++) {
break; break;
//Help Menu //Help Menu
case 'Settings': case 'Settings':
//fill form with current settings values //fill form with current settings values
setValue('setting-numberOfHistoryStates', settings.numberOfHistoryStates); setValue('setting-numberOfHistoryStates', settings.numberOfHistoryStates);
@ -149,3 +199,34 @@ function closeMenu () {
deselect(mainMenuItems[i]); deselect(mainMenuItems[i]);
} }
} }
function getProjectData() {
// use a dictionary
let dictionary = {};
// sorting layers by increasing z-index
let layersCopy = layers.slice();
layersCopy.sort((a, b) => (a.canvas.style.zIndex > b.canvas.style.zIndex) ? 1 : -1);
// save canvas size
dictionary['canvasWidth'] = currentLayer.canvasSize[0];
dictionary['canvasHeight'] = currentLayer.canvasSize[1];
// save editor mode
dictionary['editorMode'] = pixelEditorMode;
// save palette
for (let i=0; i<currentPalette.length; i++) {
dictionary["color" + i] = currentPalette[i];
}
// save number of layers
dictionary["nLayers"] = layersCopy.length;
// save layers
for (let i=0; i<layersCopy.length; i++) {
// Only saving the layers the user has access to (no vfx, tmp or checkerboard layers)
if (layersCopy[i].menuEntry != null) {
dictionary["layer" + i] = layersCopy[i];
dictionary["layer" + i + "ImageData"] = layersCopy[i].canvas.toDataURL();
}
}
return JSON.stringify(dictionary);
}

View File

@ -18,10 +18,6 @@ function getCursorPosition(e) {
return [x,y]; return [x,y];
} }
// TODO: apply the function below to every getCursorPosition call
// TODO: FIX THIS BELOW
//get cursor position relative to canvas //get cursor position relative to canvas
function getCursorPositionRelative(e, layer) { function getCursorPositionRelative(e, layer) {
var x; var x;

View File

@ -3,26 +3,259 @@ var redoStates = [];
const undoLogStyle = 'background: #87ff1c; color: black; padding: 5px;'; const undoLogStyle = 'background: #87ff1c; color: black; padding: 5px;';
//prototype for undoing canvas changes function HistoryStateFlattenVisible(flattened) {
function HistoryStateEditCanvas () { this.nFlattened = flattened;
this.canvas = currentLayer.context.getImageData(0, 0, canvasSize[0], canvasSize[1]);
this.undo = function () { this.undo = function() {
var currentCanvas = currentLayer.context.getImageData(0, 0, canvasSize[0], canvasSize[1]); for (let i=0; i<this.nFlattened; i++) {
currentLayer.context.putImageData(this.canvas, 0, 0); undo();
}
this.canvas = currentCanvas;
redoStates.push(this); redoStates.push(this);
}; };
this.redo = function () { this.redo = function() {
var currentCanvas = currentLayer.context.getImageData(0, 0, canvasSize[0], canvasSize[1]); for (let i=0; i<this.nFlattened; i++) {
currentLayer.context.putImageData(this.canvas, 0, 0); redo();
}
this.canvas = currentCanvas;
undoStates.push(this); undoStates.push(this);
}; };
saveHistoryState(this);
}
function HistoryStateFlattenTwoVisibles(belowImageData, afterAbove, layerIndex, aboveLayer, belowLayer) {
this.aboveLayer = aboveLayer;
this.belowLayer = belowLayer;
this.belowImageData = belowImageData;
this.undo = function() {
console.log(afterAbove.menuEntry);
canvasView.append(aboveLayer.canvas);
layerList.insertBefore(aboveLayer.menuEntry, afterAbove);
belowLayer.context.clearRect(0, 0, belowLayer.canvasSize[0], belowLayer.canvasSize[1]);
belowLayer.context.putImageData(this.belowImageData, 0, 0);
belowLayer.updateLayerPreview();
layers.splice(layerIndex, 0, aboveLayer);
redoStates.push(this);
};
this.redo = function() {
mergeLayers(belowLayer.context, aboveLayer.context);
// Deleting the above layer
aboveLayer.canvas.remove();
aboveLayer.menuEntry.remove();
layers.splice(layers.indexOf(aboveLayer), 1);
undoStates.push(this);
};
saveHistoryState(this);
}
function HistoryStateFlattenAll(nFlattened) {
this.nFlattened = nFlattened;
this.undo = function() {
for (let i=0; i<this.nFlattened - 2; i++) {
undo();
}
redoStates.push(this);
};
this.redo = function() {
for (let i=0; i<this.nFlattened - 2; i++) {
redo();
}
undoStates.push(this);
};
saveHistoryState(this);
}
function HistoryStateMergeLayer(aboveIndex, aboveLayer, belowData, belowLayer) {
this.aboveIndex = aboveIndex;
this.belowData = belowData;
this.aboveLayer = aboveLayer;
this.belowLayer = belowLayer;
this.undo = function() {
layerList.insertBefore(this.aboveLayer.menuEntry, this.belowLayer.menuEntry);
canvasView.append(this.aboveLayer.canvas);
belowLayer.context.clearRect(0, 0, this.belowLayer.canvasSize[0], this.belowLayer.canvasSize[1]);
belowLayer.context.putImageData(this.belowData, 0, 0);
belowLayer.updateLayerPreview();
layers.splice(this.aboveIndex, 0, this.aboveLayer);
redoStates.push(this);
};
this.redo = function() {
aboveLayer.selectLayer();
merge(false);
undoStates.push(this);
};
saveHistoryState(this);
}
function HistoryStateRenameLayer(oldName, newName, layer) {
this.edited = layer;
this.oldName = oldName;
this.newName = newName;
this.undo = function() {
layer.menuEntry.getElementsByTagName("p")[0].innerHTML = oldName;
redoStates.push(this);
};
this.redo = function() {
layer.menuEntry.getElementsByTagName("p")[0].innerHTML = newName;
undoStates.push(this);
};
saveHistoryState(this);
}
function HistoryStateDeleteLayer(layerData, before, index) {
this.deleted = layerData;
this.before = before;
this.index = index;
this.undo = function() {
canvasView.append(this.deleted.canvas);
if (this.before != null) {
layerList.insertBefore(this.deleted.menuEntry, this.before);
}
else {
layerList.prepend(this.deleted.menuEntry);
}
layers.splice(this.index, 0, this.deleted);
redoStates.push(this);
};
this.redo = function() {
this.deleted.selectLayer();
deleteLayer(false);
undoStates.push(this);
};
saveHistoryState(this);
}
function HistoryStateMoveTwoLayers(layer, oldIndex, newIndex) {
this.layer = layer;
this.oldIndex = oldIndex;
this.newIndex = newIndex;
this.undo = function() {
layer.canvas.style.zIndex = oldIndex;
redoStates.push(this);
};
this.redo = function() {
layer.canvas.style.zIndex = newIndex;
undoStates.push(this);
};
saveHistoryState(this);
}
function HistoryStateMoveLayer(afterToDrop, toDrop, static, nMoved) {
this.beforeToDrop = afterToDrop;
this.toDrop = toDrop;
this.undo = function() {
toDrop.menuEntry.remove();
if (afterToDrop != null) {
layerList.insertBefore(toDrop.menuEntry, afterToDrop)
}
else {
layerList.append(toDrop.menuEntry);
}
for (let i=0; i<nMoved; i++) {
undo();
}
redoStates.push(this);
};
this.redo = function() {
moveLayers(toDrop.menuEntry.id, static.menuEntry.id, true);
undoStates.push(this);
};
saveHistoryState(this);
}
function HistoryStateAddLayer(layerData, index) {
this.added = layerData;
this.index = index;
this.undo = function() {
redoStates.push(this);
this.added.canvas.remove();
this.added.menuEntry.remove();
layers.splice(index, 1);
};
this.redo = function() {
undoStates.push(this);
canvasView.append(this.added.canvas);
layerList.prepend(this.added.menuEntry);
layers.splice(this.index, 0, this.added);
};
saveHistoryState(this);
}
//prototype for undoing canvas changes
function HistoryStateEditCanvas () {
this.canvasState = currentLayer.context.getImageData(0, 0, canvasSize[0], canvasSize[1]);
this.layerID = currentLayer.id;
this.undo = function () {
var stateLayer = getLayerByID(this.layerID);
var currentCanvas = stateLayer.context.getImageData(0, 0, canvasSize[0], canvasSize[1]);
stateLayer.context.putImageData(this.canvasState, 0, 0);
this.canvasState = currentCanvas;
redoStates.push(this);
stateLayer.updateLayerPreview();
};
this.redo = function () {
console.log("YEET");
var stateLayer = getLayerByID(this.layerID);
var currentCanvas = stateLayer.context.getImageData(0, 0, canvasSize[0], canvasSize[1]);
stateLayer.context.putImageData(this.canvasState, 0, 0);
this.canvasState = currentCanvas;
undoStates.push(this);
stateLayer.updateLayerPreview();
};
//add self to undo array //add self to undo array
saveHistoryState(this); saveHistoryState(this);
} }
@ -123,9 +356,6 @@ function HistoryStateEditColor (newColorValue, oldColorValue) {
//rename to add undo state //rename to add undo state
function saveHistoryState (state) { function saveHistoryState (state) {
//console.log('%csaving history state', undoLogStyle);
//console.log(state);
//get current canvas data and save to undoStates array //get current canvas data and save to undoStates array
undoStates.push(state); undoStates.push(state);
@ -139,9 +369,6 @@ function saveHistoryState (state) {
//there should be no redoStates after an undoState is saved //there should be no redoStates after an undoState is saved
redoStates = []; redoStates = [];
//console.log(undoStates);
//console.log(redoStates);
} }
function undo () { function undo () {
@ -149,26 +376,22 @@ function undo () {
//if there are any states saved to undo //if there are any states saved to undo
if (undoStates.length > 0) { if (undoStates.length > 0) {
document.getElementById('redo-button').classList.remove('disabled'); document.getElementById('redo-button').classList.remove('disabled');
//get state //get state
var undoState = undoStates[undoStates.length-1]; var undoState = undoStates[undoStates.length-1];
//console.log(undoState); //console.log(undoState);
//restore the state
undoState.undo();
//remove from the undo list //remove from the undo list
undoStates.splice(undoStates.length-1,1); undoStates.splice(undoStates.length-1,1);
//restore the state
undoState.undo();
//if theres none left to undo, disable the option //if theres none left to undo, disable the option
if (undoStates.length == 0) if (undoStates.length == 0)
document.getElementById('undo-button').classList.add('disabled'); document.getElementById('undo-button').classList.add('disabled');
} }
//console.log(undoStates);
//console.log(redoStates);
} }
function redo () { function redo () {
@ -181,14 +404,13 @@ function redo () {
//get state //get state
var redoState = redoStates[redoStates.length-1]; var redoState = redoStates[redoStates.length-1];
//console.log(redoState);
//remove from redo array (do this before restoring the state, else the flatten state will break)
redoStates.splice(redoStates.length-1,1);
//restore the state //restore the state
redoState.redo(); redoState.redo();
//remove from redo array
redoStates.splice(redoStates.length-1,1);
//if theres none left to redo, disable the option //if theres none left to redo, disable the option
if (redoStates.length == 0) if (redoStates.length == 0)
document.getElementById('redo-button').classList.add('disabled'); document.getElementById('redo-button').classList.add('disabled');

View File

@ -1,27 +1,15 @@
var spacePressed = false; var spacePressed = false;
/**
Copy / paste / cut logic:
- The user selects an area
- Pressing ctrl+c copies the selection
- Pressing ctrl+v ends the current selection and copies the clipboard in the tmp layer:
the editor enters move mode and lets the user move the copied selection around.
Pressing ctrl+v while moving a copy has the same effect of pressing ctrl+v after a ctrl+c
- The behaviour of ctrl+v is the same and doesn't depend on how the selected area was obtained
(with ctrl+c or with ctrl+v)
- Selecting a different tool while moving the copied or cut selection has the same effect of selecting
a different tool while moving a standard selection
- You can paste at any other time
BUGS:
-
*/
function KeyPress(e) { function KeyPress(e) {
var keyboardEvent = window.event? event : e; var keyboardEvent = window.event? event : e;
//if the user is typing in an input field, ignore these hotkeys //if the user is typing in an input field or renaming a layer, ignore these hotkeys, unless it's an enter key
if (document.activeElement.tagName == 'INPUT') return; if (document.activeElement.tagName == 'INPUT' || isRenamingLayer) {
if (e.keyCode == 13) {
currentLayer.closeOptionsMenu();
}
return;
}
//if no document has been created yet, //if no document has been created yet,
//orthere is a dialog box open //orthere is a dialog box open
@ -42,7 +30,6 @@ function KeyPress(e) {
break; break;
// copy tool c // copy tool c
case 67: case 99: case 67: case 99:
console.log("Copying");
if (keyboardEvent.ctrlKey && !dragging && currentTool.name == 'moveselection') { if (keyboardEvent.ctrlKey && !dragging && currentTool.name == 'moveselection') {
copySelection(); copySelection();
} }
@ -65,7 +52,6 @@ function KeyPress(e) {
break; break;
// eraser -6, r // eraser -6, r
case 54: case 82: case 54: case 82:
console.log("Pressed r");
tool.eraser.switchTo() tool.eraser.switchTo()
break; break;
// Rectangular selection // Rectangular selection
@ -74,13 +60,11 @@ function KeyPress(e) {
break; break;
// Paste tool // Paste tool
case 86: case 118: case 86: case 118:
console.log("Pasting");
if (keyboardEvent.ctrlKey && !dragging) { if (keyboardEvent.ctrlKey && !dragging) {
pasteSelection(); pasteSelection();
} }
break; break;
case 88: case 120: case 88: case 120:
console.log("Cutting");
if (keyboardEvent.ctrlKey && !dragging && currentTool.name == 'moveselection') { if (keyboardEvent.ctrlKey && !dragging && currentTool.name == 'moveselection') {
cutSelectionTool(); cutSelectionTool();
tool.pencil.switchTo(); tool.pencil.switchTo();
@ -88,7 +72,6 @@ function KeyPress(e) {
break; break;
//Z //Z
case 90: case 90:
console.log('PRESSED Z ', keyboardEvent.ctrlKey)
//CTRL+ALT+Z redo //CTRL+ALT+Z redo
if (keyboardEvent.altKey && keyboardEvent.ctrlKey) if (keyboardEvent.altKey && keyboardEvent.ctrlKey)
redo(); redo();

View File

@ -1,12 +1,32 @@
/** TODO LIST FOR LAYERS /** TODO LIST FOR LAYERS
- move the tmp layer so that it's always right below the active layer
- when the move tool is selected (to move a selection), the tmp layer must be put right above the GENERAL REQUIREMENTS:
active layer to show a preview - Saving the state of an artwork to a .lospec file so that people can work on it later keeping
- mouse events will always have at least a canvas target, so evey time there's an event, we'll have to check the layers they created? That'd be cool, even for the app users, that could just double click on a lospec
the actual element type instead of the current layer and then apply the tool on the currentLayer, not on file for it to be opened right in the pixel editor
the first one in order of z-index
OPTIONAL:
1 - Fix issues
*/ */
let layerList;
let layerListEntry;
let layerDragSource = null;
let layerCount = 1;
let maxZIndex = 3;
let unusedIDs = [];
let currentID = layerCount;
let idToDelete;
let layerOptions = document.getElementById("layer-properties-menu");
let isRenamingLayer = false;
let oldLayerName = null;
on('click',"add-layer-button", addLayer, false);
/** Handler class for a single canvas (a single layer) /** Handler class for a single canvas (a single layer)
* *
@ -14,12 +34,48 @@
* @param height Canvas height * @param height Canvas height
* @param canvas HTML canvas element * @param canvas HTML canvas element
*/ */
function Layer(width, height, canvas) { class Layer {
this.canvasSize = [width, height]; constructor(width, height, canvas, menuEntry) {
this.canvas = canvas; this.canvasSize = [width, height];
this.context = this.canvas.getContext('2d'); this.canvas = canvas;
this.context = this.canvas.getContext('2d');
this.isSelected = false;
this.isVisible = true;
this.isLocked = false;
this.menuEntry = menuEntry;
let id = unusedIDs.pop();
console.log("id creato: " + id);
if (id == null) {
id = currentID;
currentID++;
}
this.id = "layer" + id;
if (menuEntry != null) {
this.name = menuEntry.getElementsByTagName("p")[0].innerHTML;
menuEntry.id = "layer" + id;
menuEntry.onclick = () => this.selectLayer();
menuEntry.getElementsByTagName("button")[0].onclick = () => this.toggleLock();
menuEntry.getElementsByTagName("button")[1].onclick = () => this.toggleVisibility();
menuEntry.addEventListener("mouseup", this.openOptionsMenu, false);
menuEntry.addEventListener("dragstart", this.layerDragStart, false);
menuEntry.addEventListener("drop", this.layerDragDrop, false);
menuEntry.addEventListener("dragover", this.layerDragOver, false);
menuEntry.addEventListener("dragleave", this.layerDragLeave, false);
menuEntry.addEventListener("dragend", this.layerDragEnd, false);
menuEntry.getElementsByTagName("canvas")[0].getContext('2d').imageSmoothingEnabled = false;
}
this.initialize();
}
// Initializes the canvas // Initializes the canvas
this.initialize = function() { initialize() {
var maxHorizontalZoom = Math.floor(window.innerWidth/this.canvasSize[0]*0.75); var maxHorizontalZoom = Math.floor(window.innerWidth/this.canvasSize[0]*0.75);
var maxVerticalZoom = Math.floor(window.innerHeight/this.canvasSize[1]*0.75); var maxVerticalZoom = Math.floor(window.innerHeight/this.canvasSize[1]*0.75);
@ -41,21 +97,494 @@ function Layer(width, height, canvas) {
this.context.imageSmoothingEnabled = false; this.context.imageSmoothingEnabled = false;
this.context.mozImageSmoothingEnabled = false; this.context.mozImageSmoothingEnabled = false;
}; }
setID(id) {
this.id = id;
if (this.menuEntry != null) {
this.menuEntry.id = id;
}
}
layerDragStart(element) {
layerDragSource = this;
element.dataTransfer.effectAllowed = 'move';
element.dataTransfer.setData('text/html', this.id);
this.classList.add('dragElem');
}
layerDragOver(element) {
if (element.preventDefault) {
element.preventDefault(); // Necessary. Allows us to drop.
}
this.classList.add('layerdragover');
element.dataTransfer.dropEffect = 'move';
return false;
}
layerDragLeave(element) {
this.classList.remove('layerdragover');
}
layerDragDrop(element) {
if (element.stopPropagation) {
element.stopPropagation(); // Stops some browsers from redirecting.
}
// Don't do anything if dropping the same column we're dragging.
if (layerDragSource != this) {
let toDropID = element.dataTransfer.getData('text/html');
let thisID = this.id;
moveLayers(toDropID, thisID);
}
this.classList.remove('layerdragover');
dragging = false;
return false;
}
layerDragEnd(element) {
this.classList.remove('layerdragover');
}
// Resizes canvas // Resizes canvas
this.resize = function() { resize() {
let newWidth = (this.canvas.width * zoom) + 'px'; let newWidth = (this.canvas.width * zoom) + 'px';
let newHeight = (this.canvas.height *zoom)+ 'px'; let newHeight = (this.canvas.height *zoom)+ 'px';
this.canvas.style.width = newWidth; this.canvas.style.width = newWidth;
this.canvas.style.height = newHeight; this.canvas.style.height = newHeight;
}; }
// Copies the otherCanvas' position and size // Copies the otherCanvas' position and size
this.copyData = function(otherCanvas) { copyData(otherCanvas) {
this.canvas.style.width = otherCanvas.canvas.style.width; this.canvas.style.width = otherCanvas.canvas.style.width;
this.canvas.style.height = otherCanvas.canvas.style.height; this.canvas.style.height = otherCanvas.canvas.style.height;
this.canvas.style.left = otherCanvas.canvas.style.left; this.canvas.style.left = otherCanvas.canvas.style.left;
this.canvas.style.top = otherCanvas.canvas.style.top; this.canvas.style.top = otherCanvas.canvas.style.top;
}; }
openOptionsMenu(event) {
if (event.which == 3) {
let target = event.target;
let offsets = getElementAbsolutePosition(this);
while (target != null && target.classList != null && !target.classList.contains("layers-menu-entry")) {
target = target.parentElement;
}
idToDelete = target.id;
layerOptions.style.visibility = "visible";
layerOptions.style.top = "0";
layerOptions.style.marginTop = "" + (event.clientY - 25) + "px";
getLayerByID(idToDelete).selectLayer();
}
}
closeOptionsMenu(event) {
layerOptions.style.visibility = "hidden";
currentLayer.menuEntry.getElementsByTagName("p")[0].setAttribute("contenteditable", false);
isRenamingLayer = false;
if (oldLayerName != null) {
let name = this.menuEntry.getElementsByTagName("p")[0].innerHTML;
this.name = name;
new HistoryStateRenameLayer(oldLayerName, name, currentLayer);
oldLayerName = null;
}
}
selectLayer(layer) {
if (layer == null) {
// Deselecting the old layer
currentLayer.deselectLayer();
// Selecting the current layer
this.isSelected = true;
this.menuEntry.classList.add("selected-layer");
currentLayer = getLayerByName(this.menuEntry.getElementsByTagName("p")[0].innerHTML);
}
else {
currentLayer.deselectLayer();
layer.isSelected = true;
layer.menuEntry.classList.add("selected-layer");
currentLayer = layer;
}
canvas = currentLayer.canvas;
context = currentLayer.context;
}
toggleLock() {
if (this.isLocked) {
this.unlock();
}
else {
this.lock();
}
}
toggleVisibility() {
if (this.isVisible) {
this.hide();
}
else {
this.show();
}
}
deselectLayer() {
this.isSelected = false;
this.menuEntry.classList.remove("selected-layer");
}
lock() {
this.isLocked = true;
this.menuEntry.getElementsByClassName("layer-button")[0].style.visibility = "visible";
this.menuEntry.getElementsByClassName("default-icon")[0].style.display = "none";
this.menuEntry.getElementsByClassName("edited-icon")[0].style.display = "inline-block";
}
unlock() {
this.isLocked = false;
this.menuEntry.getElementsByClassName("layer-button")[0].style.visibility = "hidden";
this.menuEntry.getElementsByClassName("default-icon")[0].style.display = "inline-block";
this.menuEntry.getElementsByClassName("edited-icon")[0].style.display = "none";
}
show() {
this.isVisible = true;
this.canvas.style.visibility = "visible";
this.menuEntry.getElementsByClassName("layer-button")[1].style.visibility = "hidden";
// Changing icon
this.menuEntry.getElementsByClassName("default-icon")[1].style.display = "inline-block";
this.menuEntry.getElementsByClassName("edited-icon")[1].style.display = "none";
}
hide() {
this.isVisible = false;
this.canvas.style.visibility = "hidden";
this.menuEntry.getElementsByClassName("layer-button")[1].style.visibility = "visible";
// Changing icon
this.menuEntry.getElementsByClassName("default-icon")[1].style.display = "none";
this.menuEntry.getElementsByClassName("edited-icon")[1].style.display = "inline-block";
}
updateLayerPreview() {
// Getting the canvas
let destination = this.menuEntry.getElementsByTagName("canvas")[0];
let widthRatio = this.canvasSize[0] / this.canvasSize[1];
let heightRatio = this.canvasSize[1] / this.canvasSize[0];
// Computing width and height for the preview image
let previewWidth = destination.width;
let previewHeight = destination.height;
// If the sprite is rectangular, I apply the ratio to the preview as well
if (widthRatio < 1) {
previewWidth = destination.width * widthRatio;
}
else if (widthRatio > 1) {
previewHeight = destination.height * heightRatio;
}
// La appiccico sulla preview
destination.getContext('2d').clearRect(0, 0, destination.width, destination.height);
destination.getContext('2d').drawImage(this.canvas,
// This is necessary to center the preview in the canvas
(destination.width - previewWidth) / 2, (destination.height - previewHeight) / 2,
previewWidth, previewHeight);
}
} }
function flatten(onlyVisible) {
if (!onlyVisible) {
// Selecting the first layer
let firstLayer = layerList.firstElementChild;
let nToFlatten = layerList.childElementCount - 1;
getLayerByID(firstLayer.id).selectLayer();
for (let i = 0; i < nToFlatten; i++) {
merge();
}
new HistoryStateFlattenAll(nToFlatten);
}
else {
// Getting all the visible layers
let visibleLayers = [];
let nToFlatten = 0;
for (let i=0; i<layers.length; i++) {
console.log(layers[i].name);
if (layers[i].menuEntry != null && layers[i].isVisible) {
visibleLayers.push(layers[i]);
}
}
console.log("da piallare: " + visibleLayers.length);
// Sorting them by z-index
visibleLayers.sort((a, b) => (a.canvas.style.zIndex > b.canvas.style.zIndex) ? -1 : 1);
// Selecting the last visible layer (the only one that won't get deleted)
visibleLayers[visibleLayers.length - 1].selectLayer();
// Merging all the layer but the last one
for (let i=0; i<visibleLayers.length - 1; i++) {
nToFlatten++;
console.log(visibleLayers[i].menuEntry.nextElementSibling);
new HistoryStateFlattenTwoVisibles(
visibleLayers[i + 1].context.getImageData(0, 0, visibleLayers[i].canvasSize[0], visibleLayers[i].canvasSize[1]),
visibleLayers[i].menuEntry.nextElementSibling,
layers.indexOf(visibleLayers[i]),
visibleLayers[i], visibleLayers[i + 1]
);
mergeLayers(visibleLayers[i + 1].context, visibleLayers[i].context);
// Deleting the above layer
visibleLayers[i].canvas.remove();
visibleLayers[i].menuEntry.remove();
layers.splice(layers.indexOf(visibleLayers[i]), 1);
}
new HistoryStateFlattenVisible(nToFlatten);
// Updating the layer preview
currentLayer.updateLayerPreview();
}
}
function merge(saveHistory = true) {
// Saving the layer that should be merged
let toMerge = currentLayer;
let toMergeIndex = layers.indexOf(toMerge);
// Getting layer below
let layerBelow = getLayerByID(currentLayer.menuEntry.nextElementSibling.id);
// If I have something to merge with
if (layerBelow != null) {
// Selecting that layer
layerBelow.selectLayer();
if (saveHistory) {
new HistoryStateMergeLayer(toMergeIndex, toMerge,
layerBelow.context.getImageData(0, 0, layerBelow.canvasSize[0], layerBelow.canvasSize[1]),
layerBelow);
}
mergeLayers(currentLayer.context, toMerge.context);
// Deleting the above layer
toMerge.canvas.remove();
toMerge.menuEntry.remove();
layers.splice(toMergeIndex, 1);
// Updating the layer preview
currentLayer.updateLayerPreview();
}
}
function deleteLayer(saveHistory = true) {
// Cannot delete all the layers
if (layers.length != 4) {
let layerIndex = layers.indexOf(currentLayer);
let toDelete = layers[layerIndex];
let previousSibling = toDelete.menuEntry.previousElementSibling;
// Adding the ids to the unused ones
console.log("id cancellato: " + toDelete.id);
unusedIDs.push(toDelete.id);
// Selecting the next layer
if (layerIndex != (layers.length - 3)) {
layers[layerIndex + 1].selectLayer();
}
// or the previous one if the next one doesn't exist
else {
layers[layerIndex - 1].selectLayer();
}
// Deleting canvas and entry
toDelete.canvas.remove();
toDelete.menuEntry.remove();
// Removing the layer from the list
layers.splice(layerIndex, 1);
if (saveHistory) {
new HistoryStateDeleteLayer(toDelete, previousSibling, layerIndex);
}
}
// Closing the menu
currentLayer.closeOptionsMenu();
}
function renameLayer(event) {
let layerIndex = layers.indexOf(currentLayer);
let toRename = currentLayer;
let p = currentLayer.menuEntry.getElementsByTagName("p")[0];
oldLayerName = p.innerHTML;
p.setAttribute("contenteditable", true);
p.classList.add("layer-name-editable");
p.focus();
simulateInput(65, true, false, false);
isRenamingLayer = true;
}
// Swaps two layer entries in the layer menu
function moveLayers(toDropLayer, staticLayer, saveHistory = true) {
let toDrop = getLayerByID(toDropLayer);
let static = getLayerByID(staticLayer);
let layerCopy = layers.slice();
let beforeToDrop = toDrop.menuEntry.nextElementSibling;
let nMoved = 0;
layerCopy.sort((a, b) => (a.canvas.style.zIndex > b.canvas.style.zIndex) ? 1 : -1);
let toDropIndex = layerCopy.indexOf(toDrop);
let staticIndex = layerCopy.indexOf(static);
layerList.insertBefore(toDrop.menuEntry, static.menuEntry);
if (toDropIndex < staticIndex) {
let tmp = toDrop.canvas.style.zIndex;
let tmp2;
for (let i=toDropIndex+1; i<=staticIndex; i++) {
tmp2 = layerCopy[i].canvas.style.zIndex;
if (saveHistory) {
new HistoryStateMoveTwoLayers(layerCopy[i], tmp2, tmp);
}
layerCopy[i].canvas.style.zIndex = tmp;
tmp = tmp2;
nMoved++;
}
if (saveHistory) {
new HistoryStateMoveTwoLayers(toDrop, toDrop.canvas.style.zIndex, tmp);
}
toDrop.canvas.style.zIndex = tmp;
if (saveHistory) {
new HistoryStateMoveLayer(beforeToDrop, toDrop, static, nMoved);
}
}
else {
// BUG QUI
let tmp = toDrop.canvas.style.zIndex;
let tmp2;
for (let i=toDropIndex-1; i>staticIndex; i--) {
tmp2 = layerCopy[i].canvas.style.zIndex;
if (saveHistory) {
new HistoryStateMoveTwoLayers(layerCopy[i], tmp2, tmp);
}
layerCopy[i].canvas.style.zIndex = tmp;
tmp = tmp2;
nMoved++;
}
if (saveHistory) {
new HistoryStateMoveTwoLayers(toDrop, toDrop.canvas.style.zIndex, tmp);
}
toDrop.canvas.style.zIndex = tmp;
if (saveHistory) {
new HistoryStateMoveLayer(beforeToDrop, toDrop, static, nMoved);
}
}
}
// Finds a layer given its name
function getLayerByName(name) {
for (let i=0; i<layers.length; i++) {
if (layers[i].menuEntry != null) {
if (layers[i].menuEntry.getElementsByTagName("p")[0].innerHTML == name) {
return layers[i];
}
}
}
return null;
}
// Finds a layer given its id
function getLayerByID(id) {
for (let i=0; i<layers.length; i++) {
if (layers[i].menuEntry != null) {
if (layers[i].menuEntry.id == id) {
return layers[i];
}
}
}
return null;
}
function addLayer(id, saveHistory = true) {
// layers.length - 3
let index = layers.length - 3;
// Creating a new canvas
let newCanvas = document.createElement("canvas");
// Setting up the new canvas
canvasView.append(newCanvas);
maxZIndex++;
newCanvas.style.zIndex = maxZIndex;
newCanvas.classList.add("drawingCanvas");
console.log("Tela creata: " + newCanvas);
// Clone the default layer
let toAppend = layerListEntry.cloneNode(true);
// Setting the default name for the layer
toAppend.getElementsByTagName('p')[0].innerHTML = "Layer " + layerCount;
// Removing the selected class
toAppend.classList.remove("selected-layer");
// Adding the layer to the list
layerCount++;
// Creating a layer object
let newLayer = new Layer(currentLayer.canvasSize[0], currentLayer.canvasSize[1], newCanvas, toAppend);
newLayer.context.fillStyle = currentLayer.context.fillStyle;
newLayer.copyData(currentLayer);
layers.splice(index, 0, newLayer);
// Insert it before the Add layer button
layerList.insertBefore(toAppend, layerList.childNodes[0]);
if (id != null && typeof(id) == "string") {
newLayer.setID(id);
}
// Basically "if I'm not adding a layer because redo() is telling meto do so", then I can save the history
if (saveHistory) {
new HistoryStateAddLayer(newLayer, index);
}
return newLayer;
}

View File

@ -1,61 +1,42 @@
document.getElementById('open-image-browse-holder').addEventListener('change', function () { document.getElementById('open-image-browse-holder').addEventListener('change', function () {
let fileName = document.getElementById("open-image-browse-holder").value;
let extension = fileName.substring(fileName.lastIndexOf('.')+1, fileName.length) || fileName;
if (this.files && this.files[0]) { if (this.files && this.files[0]) {
if (extension == 'png' || extension == 'gif' || extension == 'lpe') {
if (extension == 'lpe') {
let file = this.files[0];
let reader = new FileReader();
//make sure file is allowed filetype reader.readAsText(file, "UTF-8");
var fileContentType = this.files[0].type; reader.onload = function (e) {
if (fileContentType == 'image/png' || fileContentType == 'image/gif') { let dictionary = JSON.parse(e.target.result);
//load file newPixel(dictionary['canvasWidth'], dictionary['canvasHeight'], dictionary['editorMode'], dictionary);
var fileReader = new FileReader(); }
fileReader.onload = function(e) { }
var img = new Image(); else {
img.onload = function() { //load file
var fileReader = new FileReader();
fileReader.onload = function(e) {
var img = new Image();
img.onload = function() {
//create a new pixel with the images dimentions
newPixel(this.width, this.height, 'Advanced');
//create a new pixel with the images dimentions //draw the image onto the canvas
newPixel(this.width, this.height, []); currentLayer.context.drawImage(img, 0, 0);
createPaletteFromLayers();
//draw the image onto the canvas //track google event
currentLayer.context.drawImage(img, 0, 0); ga('send', 'event', 'Pixel Editor Load', colorPalette.length, this.width+'/'+this.height); /*global ga*/
var colorPalette = {};
var imagePixelData = currentLayer.context.getImageData(0,0,this.width, this.height).data;
var imagePixelDataLength = imagePixelData.length;
console.log(imagePixelData);
for (var i = 0; i < imagePixelDataLength; i += 4) {
var color = imagePixelData[i]+','+imagePixelData[i + 1]+','+imagePixelData[i + 2];
if (!colorPalette[color]) {
colorPalette[color] = {r:imagePixelData[i],g:imagePixelData[i + 1],b:imagePixelData[i + 2]};
//don't allow more than 256 colors to be added
if (Object.keys(colorPalette).length >= settings.maxColorsOnImportedImage) {
alert('The image loaded seems to have more than '+settings.maxColorsOnImportedImage+' colors.');
break;
}
}
}
//create array out of colors object
var colorPaletteArray = [];
for (var color in colorPalette) {
if( colorPalette.hasOwnProperty(color) ) {
colorPaletteArray.push('#'+rgbToHex(colorPalette[color]));
}
}
console.log('COLOR PALETTE ARRAY', colorPaletteArray);
//create palette form colors array
createColorPalette(colorPaletteArray, false);
//track google event
ga('send', 'event', 'Pixel Editor Load', colorPalette.length, this.width+'/'+this.height); /*global ga*/
};
img.src = e.target.result;
}; };
img.src = e.target.result; fileReader.readAsDataURL(this.files[0]);
}; }
fileReader.readAsDataURL(this.files[0]);
} }
else alert('Only PNG and GIF files are allowed at this time.'); else alert('Only .lpe project files, PNG and GIF files are allowed at this time.');
} }
}); });

View File

@ -1,5 +1,4 @@
var currentMouseEvent; var currentMouseEvent;
// TODO: replace every position calculation with lastMousePos
var lastMousePos; var lastMousePos;
//mousedown - start drawing //mousedown - start drawing
@ -8,8 +7,8 @@ window.addEventListener("mousedown", function (mouseEvent) {
currentMouseEvent = mouseEvent; currentMouseEvent = mouseEvent;
canDraw = true; canDraw = true;
//if no document has been created yet, or this is a dialog open //if no document has been created yet, or this is a dialog open, or the currentLayer is locked
if (!documentCreated || dialogueOpen) return; if (!documentCreated || dialogueOpen || currentLayer.isLocked || !currentLayer.isVisible) return;
//prevent right mouse clicks and such, which will open unwanted menus //prevent right mouse clicks and such, which will open unwanted menus
//mouseEvent.preventDefault(); //mouseEvent.preventDefault();
@ -32,11 +31,10 @@ window.addEventListener("mousedown", function (mouseEvent) {
canDraw = false; canDraw = false;
} }
} }
//saveHistoryState({type: 'canvas', canvas: context.getImageData(0, 0, canvasSize[0], canvasSize[1])});
currentTool.updateCursor(); currentTool.updateCursor();
if (canDraw) { if (!currentLayer.isLocked && !currentLayer.isVisible && canDraw) {
draw(mouseEvent); draw(mouseEvent);
} }
} }
@ -45,7 +43,6 @@ window.addEventListener("mousedown", function (mouseEvent) {
tool.pencil.previousBrushSize = tool.pencil.brushSize; tool.pencil.previousBrushSize = tool.pencil.brushSize;
} }
else if (currentTool.name == 'eraser' && mouseEvent.which == 3) { else if (currentTool.name == 'eraser' && mouseEvent.which == 3) {
console.log('resize eraser')
currentTool = tool.resizeeraser; currentTool = tool.resizeeraser;
tool.eraser.previousBrushSize = tool.eraser.brushSize; tool.eraser.previousBrushSize = tool.eraser.brushSize;
} }
@ -68,30 +65,38 @@ window.addEventListener("mouseup", function (mouseEvent) {
currentMouseEvent = mouseEvent; currentMouseEvent = mouseEvent;
closeMenu(); closeMenu();
if (currentLayer != null && !isChildOfByClass(mouseEvent.target, "layers-menu-entry")) {
currentLayer.closeOptionsMenu();
}
if (!documentCreated || dialogueOpen) return; if (!documentCreated || dialogueOpen || !currentLayer.isVisible || currentLayer.isLocked) return;
if (currentTool.name == 'eyedropper' && mouseEvent.target.className == 'drawingCanvas') { if (currentTool.name == 'eyedropper' && mouseEvent.target.className == 'drawingCanvas') {
var cursorLocation = getCursorPosition(mouseEvent); var cursorLocation = getCursorPosition(mouseEvent);
var selectedColor = context.getImageData(Math.floor(cursorLocation[0]/zoom),Math.floor(cursorLocation[1]/zoom),1,1); var selectedColor = getEyedropperColor(cursorLocation);
var newColor = rgbToHex(selectedColor.data[0],selectedColor.data[1],selectedColor.data[2]); var newColor = rgbToHex(selectedColor[0],selectedColor[1],selectedColor[2]);
currentGlobalColor = "#" + newColor; currentGlobalColor = "#" + newColor;
for (let i=1; i<layers.length - 1; i++) {
layers[i].context.fillStyle = currentGlobalColor;
}
var colors = document.getElementsByClassName('color-button'); var colors = document.getElementsByClassName('color-button');
for (var i = 0; i < colors.length; i++) { for (var i = 0; i < colors.length; i++) {
console.log(colors[i].jscolor.toString());
//if picked color matches this color //if picked color matches this color
if (newColor == colors[i].jscolor.toString()) { if (newColor == colors[i].jscolor.toString()) {
console.log('color found');
//remove current color selection //remove current color selection
var selectedColor = document.querySelector("#colors-menu li.selected") var selectedColor = document.querySelector("#colors-menu li.selected")
if (selectedColor) selectedColor.classList.remove("selected"); if (selectedColor) selectedColor.classList.remove("selected");
//set current color //set current color
context.fillStyle = '#'+newColor;
for (let i=2; i<layers.length; i++) {
layers[i].context.fillStyle = '#' + newColor;
}
//make color selected //make color selected
colors[i].parentElement.classList.add('selected'); colors[i].parentElement.classList.add('selected');
@ -102,7 +107,6 @@ window.addEventListener("mouseup", function (mouseEvent) {
} }
} }
else if (currentTool.name == 'fill' && mouseEvent.target.className == 'drawingCanvas') { else if (currentTool.name == 'fill' && mouseEvent.target.className == 'drawingCanvas') {
console.log('filling')
//get cursor postion //get cursor postion
var cursorLocation = getCursorPosition(mouseEvent); var cursorLocation = getCursorPosition(mouseEvent);
@ -111,8 +115,9 @@ window.addEventListener("mouseup", function (mouseEvent) {
cursorLocation[0] += 2; cursorLocation[0] += 2;
cursorLocation[1] += 12; cursorLocation[1] += 12;
//fill starting at the location //fill starting at the location
fill(cursorLocation); fill(cursorLocation);
currentLayer.updateLayerPreview();
} }
else if (currentTool.name == 'zoom' && mouseEvent.target.className == 'drawingCanvas') { else if (currentTool.name == 'zoom' && mouseEvent.target.className == 'drawingCanvas') {
let mode; let mode;
@ -120,21 +125,6 @@ window.addEventListener("mouseup", function (mouseEvent) {
mode = "in"; mode = "in";
} }
} }
else if (currentTool == 'fill' && mouseEvent.target.className == 'drawingCanvas') {
console.log('filling');
//if you clicked on anything but the canvas, do nothing
if (!mouseEvent.target == currentLayer.canvas) return;
//get cursor postion
var cursorLocation = getCursorPosition(mouseEvent);
//offset to match cursor point
cursorLocation[0] += 2;
cursorLocation[1] += 12;
//fill starting at the location
fill(cursorLocation);
}
else if (currentTool == 'zoom' && mouseEvent.target.className == 'drawingCanvas') { else if (currentTool == 'zoom' && mouseEvent.target.className == 'drawingCanvas') {
let mode; let mode;
if (mouseEvent.which == 1){ if (mouseEvent.which == 1){
@ -153,8 +143,9 @@ window.addEventListener("mouseup", function (mouseEvent) {
else if (currentTool.name == 'rectselect' && isRectSelecting) { else if (currentTool.name == 'rectselect' && isRectSelecting) {
endRectSelection(mouseEvent); endRectSelection(mouseEvent);
} }
else if (currentTool.name == 'rectangle') { else if (currentTool.name == 'rectangle' && isDrawingRect) {
endRectDrawing(mouseEvent); endRectDrawing(mouseEvent);
currentLayer.updateLayerPreview();
} }
dragging = false; dragging = false;
@ -165,6 +156,7 @@ window.addEventListener("mouseup", function (mouseEvent) {
}, false); }, false);
// TODO: Make it snap to the pixel grid
function setPreviewPosition(preview, cursor, size){ function setPreviewPosition(preview, cursor, size){
preview.style.left = ( preview.style.left = (
currentLayer.canvas.offsetLeft currentLayer.canvas.offsetLeft
@ -190,8 +182,8 @@ function draw (mouseEvent) {
var cursorLocation = lastMousePos; var cursorLocation = lastMousePos;
//if a document hasnt yet been created, exit this function //if a document hasnt yet been created or the current layer is locked, exit this function
if (!documentCreated || dialogueOpen) return; if (!documentCreated || dialogueOpen || !currentLayer.isVisible || currentLayer.isLocked) return;
eyedropperPreview.style.display = 'none'; eyedropperPreview.style.display = 'none';
@ -222,6 +214,8 @@ function draw (mouseEvent) {
//for the darkest 50% of colors, change the brush preview to dark mode //for the darkest 50% of colors, change the brush preview to dark mode
if (colorLightness>127) brushPreview.classList.remove('dark'); if (colorLightness>127) brushPreview.classList.remove('dark');
else brushPreview.classList.add('dark'); else brushPreview.classList.add('dark');
currentLayer.updateLayerPreview();
} }
// Decided to write a different implementation in case of differences between the brush and the eraser tool // Decided to write a different implementation in case of differences between the brush and the eraser tool
else if (currentTool.name == 'eraser') { else if (currentTool.name == 'eraser') {
@ -243,6 +237,8 @@ function draw (mouseEvent) {
lastPos = cursorLocation; lastPos = cursorLocation;
} }
} }
currentLayer.updateLayerPreview();
} }
else if (currentTool.name == 'rectangle') else if (currentTool.name == 'rectangle')
{ {
@ -272,7 +268,7 @@ function draw (mouseEvent) {
} }
} }
else if (currentTool.name == 'eyedropper' && dragging && mouseEvent.target.className == 'drawingCanvas') { else if (currentTool.name == 'eyedropper' && dragging && mouseEvent.target.className == 'drawingCanvas') {
var selectedColor = context.getImageData(Math.floor(cursorLocation[0]/zoom),Math.floor(cursorLocation[1]/zoom),1,1).data; let selectedColor = getEyedropperColor(cursorLocation);
eyedropperPreview.style.borderColor = '#'+rgbToHex(selectedColor[0],selectedColor[1],selectedColor[2]); eyedropperPreview.style.borderColor = '#'+rgbToHex(selectedColor[0],selectedColor[1],selectedColor[2]);
eyedropperPreview.style.display = 'block'; eyedropperPreview.style.display = 'block';
@ -340,7 +336,6 @@ function draw (mouseEvent) {
else if (currentTool.name == 'rectselect') { else if (currentTool.name == 'rectselect') {
if (dragging && !isRectSelecting && mouseEvent.target.className == 'drawingCanvas') { if (dragging && !isRectSelecting && mouseEvent.target.className == 'drawingCanvas') {
isRectSelecting = true; isRectSelecting = true;
console.log("cominciata selezione su " + mouseEvent.target.className);
startRectSelection(mouseEvent); startRectSelection(mouseEvent);
} }
else if (dragging && isRectSelecting) { else if (dragging && isRectSelecting) {
@ -382,4 +377,4 @@ canvasView.addEventListener("wheel", function(mouseEvent){
} }
} }
}); });

View File

@ -84,6 +84,7 @@ function endSelection() {
isPasting = false; isPasting = false;
isCutting = false; isCutting = false;
lastMovePos = undefined; lastMovePos = undefined;
currentLayer.updateLayerPreview();
new HistoryStateEditCanvas(); new HistoryStateEditCanvas();
} }

View File

@ -1,26 +1,72 @@
function newPixel (width, height, palette) { let firstPixel = true;
// Setting the current layer
currentLayer = new Layer(width, height, canvas); function newPixel (width, height, editorMode, fileContent = null) {
currentLayer.initialize(); pixelEditorMode = editorMode;
currentPalette = [];
if (firstPixel) {
layerList = document.getElementById("layers-menu");
layerListEntry = layerList.firstElementChild;
// Setting up the current layer
currentLayer = new Layer(width, height, canvas, layerListEntry);
canvas.style.zIndex = 2;
}
else {
let nLayers = layers.length;
for (let i=2; i < layers.length - 2; i++) {
let currentEntry = layers[i].menuEntry;
let associatedLayer;
if (currentEntry != null) {
// Getting the associated layer
associatedLayer = getLayerByID(currentEntry.id);
// Deleting its canvas
associatedLayer.canvas.remove();
// Adding the id to the unused ones
unusedIDs.push(currentEntry.id);
// Removing the entry from the menu
currentEntry.remove();
}
}
// Removing the old layers from the list
for (let i=2; i<nLayers - 2; i++) {
layers.splice(2, 1);
}
// Setting up the current layer
layers[1] = new Layer(width, height, layers[1].canvas, layers[1].menuEntry);
currentLayer = layers[1];
canvas = currentLayer.canvas;
context = currentLayer.context;
canvas.style.zIndex = 2;
}
// Adding the checkerboard behind it // Adding the checkerboard behind it
checkerBoard = new Layer(width, height, checkerBoardCanvas); checkerBoard = new Layer(width, height, checkerBoardCanvas);
checkerBoard.initialize();
// Creating the vfx layer on top of everything // Creating the vfx layer on top of everything
VFXLayer = new Layer(width, height, VFXCanvas); VFXLayer = new Layer(width, height, VFXCanvas);
VFXLayer.initialize();
// Tmp layer to draw previews on
TMPLayer = new Layer(width, height, TMPCanvas); TMPLayer = new Layer(width, height, TMPCanvas);
TMPLayer.initialize();
canvasSize = currentLayer.canvasSize; canvasSize = currentLayer.canvasSize;
// Adding the first layer and the checkerboard to the list of layers if (firstPixel) {
layers.push(checkerBoard); // Cloning the entry so that when I change something on the first layer, those changes aren't
layers.push(currentLayer); // propagated to the other ones
layers.push(VFXLayer); layerListEntry = layerListEntry.cloneNode(true);
layers.push(TMPLayer); // Adding the first layer and the checkerboard to the list of layers
layers.push(checkerBoard);
layers.push(currentLayer);
layers.push(VFXLayer);
layers.push(TMPLayer);
}
//remove current palette //remove current palette
colors = document.getElementsByClassName('color-button'); colors = document.getElementsByClassName('color-button');
@ -30,7 +76,7 @@ function newPixel (width, height, palette) {
//add colors from selected palette //add colors from selected palette
var selectedPalette = getText('palette-button'); var selectedPalette = getText('palette-button');
if (selectedPalette != 'Choose a palette...') { if (selectedPalette != 'Choose a palette...' && fileContent == null) {
//if this palette isnt the one specified in the url, then reset the url //if this palette isnt the one specified in the url, then reset the url
if (!palettes[selectedPalette].specified) if (!palettes[selectedPalette].specified)
@ -39,30 +85,21 @@ function newPixel (width, height, palette) {
//fill the palette with specified palette //fill the palette with specified palette
createColorPalette(palettes[selectedPalette].colors,true); createColorPalette(palettes[selectedPalette].colors,true);
} }
else { else if (fileContent == null) {
//this wasn't a specified palette, so reset the url //this wasn't a specified palette, so reset the url
history.pushState(null, null, '/pixel-editor/app'); history.pushState(null, null, '/pixel-editor/app');
//generate default colors //generate default colors
var fg = hslToRgb(Math.floor(Math.random()*255), 230,70); var fg = hslToRgb(Math.floor(Math.random()*255), 230,70);
var bg = hslToRgb(Math.floor(Math.random()*255), 230,170); var bg = hslToRgb(Math.floor(Math.random()*255), 230,170);
//convert colors to hex //convert colors to hex
var defaultForegroundColor = rgbToHex(fg.r,fg.g,fg.b); var defaultForegroundColor = rgbToHex(fg.r,fg.g,fg.b);
var defaultBackgroundColor = rgbToHex(bg.r,bg.g,bg.b); var defaultBackgroundColor = rgbToHex(bg.r,bg.g,bg.b);
//add colors to paletee //add colors to palette
addColor(defaultForegroundColor).classList.add('selected'); addColor(defaultForegroundColor).classList.add('selected');
addColor(defaultBackgroundColor); addColor(defaultBackgroundColor);
//fill background of canvas with bg color
fillCheckerboard();
/*
currentLayer.context.fillStyle = '#'+defaultBackgroundColor;
currentLayer.context.fillRect(0, 0, canvasSize[0], canvasSize[1]);
console.log('#'+defaultBackgroundColor)
*/
//set current drawing color as foreground color //set current drawing color as foreground color
currentLayer.context.fillStyle = '#'+defaultForegroundColor; currentLayer.context.fillStyle = '#'+defaultForegroundColor;
@ -70,6 +107,9 @@ function newPixel (width, height, palette) {
selectedPalette = 'none'; selectedPalette = 'none';
} }
//fill background of canvas with bg color
fillCheckerboard();
//reset undo and redo states //reset undo and redo states
undoStates = []; undoStates = [];
redoStates = []; redoStates = [];
@ -77,7 +117,53 @@ function newPixel (width, height, palette) {
closeDialogue(); closeDialogue();
currentTool.updateCursor(); currentTool.updateCursor();
document.getElementById('save-as-button').classList.remove('disabled'); document.getElementById('export-button').classList.remove('disabled');
documentCreated = true; documentCreated = true;
} firstPixel = false;
if (fileContent != null) {
for (let i=0; i<fileContent['nLayers']; i++) {
let layerData = fileContent['layer' + i];
let layerImage = fileContent['layer' + i + 'ImageData'];
if (layerData != null) {
// Setting id
let createdLayer = addLayer(layerData.id, false);
// Setting name
createdLayer.menuEntry.getElementsByTagName("p")[0].innerHTML = layerData.name;
// Adding the image (I can do that because they're sorted by increasing z-index)
let img = new Image();
img.onload = function() {
createdLayer.context.drawImage(img, 0, 0);
createdLayer.updateLayerPreview();
if (i == (fileContent['nLayers'] - 1)) {
createPaletteFromLayers();
}
};
img.src = layerImage;
// Setting visibility and lock options
if (!layerData.isVisible) {
createdLayer.hide();
}
if (layerData.isLocked) {
createdLayer.lock();
}
}
}
// Deleting the default layer
deleteLayer(false);
}
if (pixelEditorMode == 'Basic') {
switchMode('Advanced', false);
}
else {
switchMode('Basic', false);
}
}

View File

@ -5,7 +5,7 @@ window.onload = function(){
//if the user specified dimentions //if the user specified dimentions
if (specifiedDimentions) if (specifiedDimentions)
//create a new pixel //create a new pixel
newPixel(getValue('size-width'),getValue('size-height'),''); newPixel(getValue('size-width'),getValue('size-height'), getValue('editor-mode'));
else else
//otherwise show the new pixel dialog //otherwise show the new pixel dialog
showDialogue('new-pixel', false); showDialogue('new-pixel', false);

View File

@ -44,7 +44,6 @@ on('click', 'load-palette-button', function () {
on('click', 'palette-button', function (e){ on('click', 'palette-button', function (e){
toggle('palette-button'); toggle('palette-button');
toggle('palette-menu'); toggle('palette-menu');
@ -55,6 +54,7 @@ on('click', 'palette-button', function (e){
}); });
on('click', 'new-pixel', function (){ on('click', 'new-pixel', function (){
deselect('editor-mode-menu');
deselect('preset-button'); deselect('preset-button');
deselect('preset-menu'); deselect('preset-menu');
deselect('palette-button'); deselect('palette-button');

View File

@ -1,7 +1,108 @@
function mergeLayers(belowLayer, topLayer) {
// Copying the above content on the layerBelow
let belowImageData = belowLayer.getImageData(0, 0, canvas.width, canvas.height);
let toMergeImageData = topLayer.getImageData(0, 0, canvas.width, canvas.height);
for (let i=0; i<belowImageData.data.length; i+=4) {
let currentMovePixel = [
toMergeImageData.data[i], toMergeImageData.data[i+1],
toMergeImageData.data[i+2], toMergeImageData.data[i+3]
];
let currentUnderlyingPixel = [
belowImageData.data[i], belowImageData.data[i+1],
belowImageData.data[i+2], belowImageData.data[i+3]
];
if (isPixelEmpty(currentMovePixel)) {
if (!isPixelEmpty(belowImageData)) {
toMergeImageData.data[i] = currentUnderlyingPixel[0];
toMergeImageData.data[i+1] = currentUnderlyingPixel[1];
toMergeImageData.data[i+2] = currentUnderlyingPixel[2];
toMergeImageData.data[i+3] = currentUnderlyingPixel[3];
}
}
}
belowLayer.putImageData(toMergeImageData, 0, 0);
}
function simulateInput(keyCode, ctrl, alt, shift) {
let keyboardEvent = document.createEvent("KeyboardEvent");
let initMethod = typeof keyboardEvent.initKeyboardEvent !== 'undefined' ? "initKeyboardEvent" : "initKeyEvent";
keyboardEvent[initMethod](
"keydown", // event type: keydown, keyup, keypress
true, // bubbles
true, // cancelable
window, // view: should be window
ctrl, // ctrlKey
alt, // altKey
shift, // shiftKey
false, // metaKey
keyCode, // keyCode: unsigned long - the virtual key code, else 0
keyCode // charCode: unsigned long - the Unicode character associated with the depressed key, else 0
);
document.dispatchEvent(keyboardEvent);
}
function isPixelEmpty(pixel) { function isPixelEmpty(pixel) {
if (pixel == null || pixel === undefined) {
return false;
}
if ((pixel[0] == 0 && pixel[1] == 0 && pixel[2] == 0) || pixel[3] == 0) { if ((pixel[0] == 0 && pixel[1] == 0 && pixel[2] == 0) || pixel[3] == 0) {
return true; return true;
} }
return false; return false;
}
function isChildOfByClass(element, className) {
while (element != null && element.classList != null && !element.classList.contains(className)) {
element = element.parentElement;
}
if (element != null && element.classList != null && element.classList.contains(className)) {
return true;
}
return false;
}
function getEyedropperColor(cursorLocation) {
let max = -1;
let tmpColour;
let selectedColor;
for (let i=1; i<layers.length; i++) {
tmpColour = layers[i].context.getImageData(Math.floor(cursorLocation[0]/zoom),Math.floor(cursorLocation[1]/zoom),1,1).data;
if (layers[i].canvas.style.zIndex > max || isPixelEmpty(selectedColor) || selectedColor === undefined) {
max = layers[i].canvas.style.zIndex;
if (!isPixelEmpty(tmpColour)) {
selectedColor = tmpColour;
}
}
}
if (isPixelEmpty(tmpColour) && selectedColor === undefined) {
selectedColor = [0, 0, 0];
}
return selectedColor;
}
function getElementAbsolutePosition(element) {
let curleft = curtop = 0;
if (element.offsetParent) {
do {
curleft += element.offsetLeft;
curtop += element.offsetTop;
} while (element = element.offsetParent);
}
return [curleft,curtop];
} }

View File

@ -49,7 +49,6 @@ Object.keys(presets).forEach(function(presetName,index) {
}); });
on('click', 'preset-button', function (e){ on('click', 'preset-button', function (e){
//open or close the preset menu //open or close the preset menu
toggle('preset-button'); toggle('preset-button');

View File

@ -98,8 +98,6 @@ function drawRect(x, y) {
vfxContext.rect(startX, startY, x - startX, y - startY); vfxContext.rect(startX, startY, x - startX, y - startY);
vfxContext.stroke(); vfxContext.stroke();
// TODO: make the rect blink from black to white in case of dark backgrounds
} }
function applyChanges() { function applyChanges() {

View File

@ -16,6 +16,7 @@ on('click',"pencil-smaller-button", function(){
//eraser //eraser
on('click',"eraser-button", function(){ on('click',"eraser-button", function(){
console.log("selecting eraser");
tool.eraser.switchTo(); tool.eraser.switchTo();
}, false); }, false);
@ -31,7 +32,7 @@ on('click',"eraser-smaller-button", function(e){
}, false); }, false);
// rectangle // rectangle
on('click','rectangle-button', function(){ on('click','rectangle-button', function(e){
// If the user clicks twice on the button, they change the draw mode // If the user clicks twice on the button, they change the draw mode
if (currentTool.name == 'rectangle') { if (currentTool.name == 'rectangle') {
if (drawMode == 'empty') { if (drawMode == 'empty') {

View File

@ -40,9 +40,6 @@ class Tool {
//switch to this tool (replaced global changeTool()) //switch to this tool (replaced global changeTool())
switchTo () { switchTo () {
console.log('changing tool to',this.name)
// Ending any selection in progress // Ending any selection in progress
if (currentTool.name.includes("select") && !this.name.includes("select") && !selectionCanceled) { if (currentTool.name.includes("select") && !this.name.includes("select") && !selectionCanceled) {
endSelection(); endSelection();

View File

@ -4,6 +4,7 @@ var dragging = false;
var lastPos = [0,0]; var lastPos = [0,0];
var dialogueOpen = false; var dialogueOpen = false;
var documentCreated = false; var documentCreated = false;
var pixelEditorMode;
// Checkerboard management // Checkerboard management
// Checkerboard color 1 // Checkerboard color 1

View File

@ -19,6 +19,7 @@
//=include _settings.js //=include _settings.js
/**dropdown formatting**/ /**dropdown formatting**/
//=include _editorMode.js
//=include _presets.js //=include _presets.js
//=include _palettes.js //=include _palettes.js

View File

@ -1,10 +1,4 @@
<!DOCTYPE html> <!DOCTYPE html>
<!--todo: credit RECTANGULAR SELECTION ICON BY https://www.flaticon.com/authors/pixel-perfect or
design an original icon (this is just a placeholder)
Must also credit this guy for the rectangle tool
Icons made by <a href="https://www.flaticon.com/authors/freepik" title="Freepik">Freepik</a> from <a href="https://www.flaticon.com/" title="Flaticon"> www.flaticon.com</a>
-->
<html> <html>
<head> <head>
@ -49,8 +43,9 @@
<button>File</button> <button>File</button>
<ul> <ul>
<li><button>New</button></li> <li><button>New</button></li>
<li><button>Save project</button></li>
<li><button>Open</button></li> <li><button>Open</button></li>
<li><button id="save-as-button" class="disabled">Save as...</button></li> <li><button id="export-button" class="disabled">Export</button></li>
<li><a href="/pixel-editor">Exit</a></li> <li><a href="/pixel-editor">Exit</a></li>
</ul> </ul>
</li> </li>
@ -61,6 +56,18 @@
<li><button id="redo-button" class="disabled">Redo</button></li> <li><button id="redo-button" class="disabled">Redo</button></li>
</ul> </ul>
</li> </li>
<li>
<button id = "layer-button">Layer</button>
<ul>
<li><button onclick = "addLayer()">New layer</button></li>
<li><button onclick = "renameLayer()">Rename</button></li>
<li><button onclick = "deleteLayer()">Delete</button></li>
<li><button onclick = "merge()">Merge below</button></li>
<li><button onclick = "flatten(false)">Flatten all</button></li>
<li><button onclick = "flatten(true)">Flatten visible</button></li>
</ul>
</li>
<li> <li>
<button>Selection</button> <button>Selection</button>
<ul> <ul>
@ -69,6 +76,14 @@
<li><button id="paste-button">Paste</button></li> <li><button id="paste-button">Paste</button></li>
<li><button id="cancelSelection-button">Cancel</button></li> <li><button id="cancelSelection-button">Cancel</button></li>
</ul> </ul>
</li>
<li>
<button>Editor</button>
<ul>
<li><button id="switch-mode-button">Switch to basic mode</button></li>
</ul>
</li>
<li> <li>
<button>Help</button> <button>Help</button>
<ul> <ul>
@ -96,8 +111,8 @@
<li class="expanded"> <li class="expanded">
<button title="Rectangle Tool (U)" id="rectangle-button">{{svg "rectangle.svg" width="32" height="32" id = "empty-button-svg"}} <button title="Rectangle Tool (U)" id="rectangle-button">{{svg "rectangle.svg" width="32" height="32" id = "empty-button-svg"}}
{{svg "fullrect.svg" width="32" height="32" id = "full-button-svg" display = "none"}}</button> {{svg "fullrect.svg" width="32" height="32" id = "full-button-svg" display = "none"}}</button>
<button title="Increase Brush Size" id="rectangle-bigger-button" class="tools-menu-sub-button">{{svg "plus.svg" width="12" height="12"}}</button> <button title="Increase Rectangle Size" id="rectangle-bigger-button" class="tools-menu-sub-button">{{svg "plus.svg" width="12" height="12"}}</button>
<button title="Decrease Brush Size" id="rectangle-smaller-button" class="tools-menu-sub-button">{{svg "minus.svg" width="12" height="12"}}</button> <button title="Decrease Rectangle Size" id="rectangle-smaller-button" class="tools-menu-sub-button">{{svg "minus.svg" width="12" height="12"}}</button>
</li> </li>
<li><button title="Fill Tool (F)" id="fill-button">{{svg "fill.svg" width="32" height="32"}}</button></li> <li><button title="Fill Tool (F)" id="fill-button">{{svg "fill.svg" width="32" height="32"}}</button></li>
@ -114,6 +129,7 @@
<li><button title = "Rectangular Selection Tool (M)" id = "rectselect-button">{{svg "rectselect.svg" width = "32" height = "32"}}</button><li> <li><button title = "Rectangular Selection Tool (M)" id = "rectselect-button">{{svg "rectselect.svg" width = "32" height = "32"}}</button><li>
</ul> </ul>
<!-- PALETTE -->
<ul id="colors-menu"> <ul id="colors-menu">
{{! {{!
@ -123,9 +139,58 @@
<li class="noshrink"><button title="Add Current Color To Palette" id="add-color-button">{{svg "./plus.svg" width="30" height="30"}}</button></li> <li class="noshrink"><button title="Add Current Color To Palette" id="add-color-button">{{svg "./plus.svg" width="30" height="30"}}</button></li>
</ul> </ul>
<!-- LAYER MENU -->
<ul id = "layers-menu">
<li class = "layers-menu-entry selected-layer" draggable = "true">
<canvas class = "preview-canvas"></canvas>
<ul class="layer-buttons">
<li class = "layer-button">
<button title="Lock layer" class="lock-layer-button">
{{svg "unlockedpadlock.svg" width="15" height="15" class = "default-icon"}}
{{svg "lockedpadlock.svg" width="15" height="15" class = "edited-icon" display = "none"}}
</button>
</li>
<li class = "layer-button">
<button title="Show / hide layer" class="hide-layer-button">
{{svg "visible.svg" width="15" height="15" class = "default-icon"}}
{{svg "invisible.svg" width="15" height="15" class = "edited-icon" display = "none"}}
</button>
</li>
</ul>
<p>Layer 0<div class = "gradient"></div></p>
</li>
<li>
<button id = "add-layer-button">
{{svg "plus.svg" width="20" height="20"}} Add layer
</button>
</li>
</ul>
<ul id = "layer-properties-menu">
<li>
<button onclick = "deleteLayer()">Delete</button>
</li>
<li>
<button onclick = "renameLayer()">Rename</button>
</li>
<li>
<button onclick = "merge()">Merge below</button>
</li>
<li>
<button onclick = "flatten(true)">Flatten visible</button>
</li>
<li>
<button onclick = "flatten(false)">Flatten all</button>
</li>
</ul>
<div id="eyedropper-preview"></div> <div id="eyedropper-preview"></div>
<div id="brush-preview"></div> <div id="brush-preview"></div>
<!-- CANVASES -->
<div id="canvas-view"> <div id="canvas-view">
<canvas id="vfx-canvas" class = "drawingCanvas"></canvas> <canvas id="vfx-canvas" class = "drawingCanvas"></canvas>
<canvas id = "tmp-canvas" class = "drawingCanvas"></canvas> <canvas id = "tmp-canvas" class = "drawingCanvas"></canvas>
@ -136,7 +201,8 @@
<div id="data-holders"> <div id="data-holders">
<a id="save-image-link-holder" href="#">dl</a> <a id="save-image-link-holder" href="#">dl</a>
<input id="open-image-browse-holder" type="file" accept="image/png, image/gif"/> <a id="save-project-link-holder" href="#">dl</a>
<input id="open-image-browse-holder" type="file" accept="image/png, image/gif, .lpe"/>
<input id="load-palette-browse-holder" type="file" accept="image/png, image/gif"/> <input id="load-palette-browse-holder" type="file" accept="image/png, image/gif"/>
<canvas id="load-palette-canvas-holder"></canvas> <canvas id="load-palette-canvas-holder"></canvas>
</div> </div>
@ -156,6 +222,14 @@
<button class="close-button">{{svg "x.svg" width="20" height="20"}}</button> <button class="close-button">{{svg "x.svg" width="20" height="20"}}</button>
<h1>New Pixel</h1> <h1>New Pixel</h1>
<!-- Editor mode-->
<h2>Editor mode</h2>
<button id = "editor-mode-button" class = "dropdown-button">Choose a mode...</button>
<div id = "editor-mode-menu" class = "dropdown-menu"></div>
<input id="editor-mode" value="{{#if mode}}{{mode}}{{else}}'Advanced'{{/if}}" autocomplete="off" />
<p id = "editor-mode-info"></p>
<!-- Preset-->
<h2>Preset</h2> <h2>Preset</h2>
<button id="preset-button" class="dropdown-button">Choose a preset...</button> <button id="preset-button" class="dropdown-button">Choose a preset...</button>
<div id="preset-menu" class="dropdown-menu"></div> <div id="preset-menu" class="dropdown-menu"></div>
@ -243,8 +317,6 @@
</div> </div>
</div> </div>
</div>
<script> <script>
palettes = { {{#palettes}} palettes = { {{#palettes}}
'{{title}}': { '{{title}}': {