Merge branch 'master' into hot-reload

This commit is contained in:
Julian Webb 2021-04-26 11:04:47 -07:00 committed by GitHub
commit ea357dab04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 6903 additions and 473 deletions

3
.gitignore vendored
View File

@ -1,4 +1,5 @@
_ext /_ext/*
!/_ext/svg/
routes routes
build build
node_modules node_modules

View File

@ -12,16 +12,14 @@ The next version is mostly focused on adding missing essential features and port
Suggestions / Planned features: Suggestions / Planned features:
- Documentation
- Possibility to hide and resize menus (layers, palette)
- Line tool - Line tool
- Tiled mode - Tiled mode
- Load palette from LPE file - Load palette from LPE file
- Move colours in (advanced) palette editor
- Symmetry options - Symmetry options
- Custom color picker
- custom code without dependencies
- more features such as sliders / color modes
- Mobile - Mobile
- Touch equivalent for mouse clicks - Touch equivalent for mouse clicks
- Hide or scale ui - Hide or scale ui
@ -32,15 +30,13 @@ Suggestions / Planned features:
- Possibly add collaborate function - Possibly add collaborate function
- Polish: - Polish:
- ctrl + a to select everything / selection -> all, same for deselection - ctrl a to select everything / selection -> all, same for deselection
- Show colors which would need to be added to palette
- Warning windows for wrong inputs - Warning windows for wrong inputs
- Palette option remove unused colors - Palette option remove unused colors
- Move selection with arrows - Move selection with arrows
- Update pivot buttons when resizing canvas
- Update borders by dragging the canvas' edges with the mouse when resizing canvas - Update borders by dragging the canvas' edges with the mouse when resizing canvas
- Move the canvases so they're centered after resizing the canvas (maybe a .center() method in layer class) - Move the canvases so they're centered after resizing the canvas (maybe a .center() method in layer class)
- Trim canvas - Scale selection
## How to Contribute ## How to Contribute
@ -48,9 +44,9 @@ Requirements: you must have node.js and git installed.
1. Click **Fork** above. It will automatically create a copy of this repository and add it to your account. 1. Click **Fork** above. It will automatically create a copy of this repository and add it to your account.
2. Clone the repository to your computer. 2. Clone the repository to your computer.
3. Open the folder in command prompt and run **npm install** 3. Open the folder in command prompt and run **`npm install`**
4. Make any changes you would like to suggest. 4. Make any changes you would like to suggest.
5. In command prompt run **node build.js** which will compile it to the */build* folder, where you can make sure it works 5. In command prompt run **`npm run build`** which will compile it to the `/build` folder, where you can make sure it works the easiest way to do so is to run **`npm run serve`**. You can also do both at once by running `npm test`.
6. Add, Commit and Push your changes to your fork. 6. Add, Commit and Push your changes to your fork.
7. On the github page for your fork, click **New Pull Request** above the file list. 7. On the github page for your fork, click **New Pull Request** above the file list.
8. Change the **head repository** dropdown to your fork. 8. Change the **head repository** dropdown to your fork.

41
_ext/svg/line.svg Normal file
View File

@ -0,0 +1,41 @@
<?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 512.001 512.001" style="enable-background:new 0 0 512.001 512.001;" xml:space="preserve">
<g>
<g>
<path d="M506.143,5.859c-7.811-7.811-20.475-7.811-28.285,0l-472,472c-7.811,7.811-7.811,20.474,0,28.284
c3.905,3.906,9.024,5.858,14.142,5.858s10.237-1.953,14.143-5.858l472-472C513.954,26.333,513.954,13.67,506.143,5.859z"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 799 B

View File

@ -15,7 +15,7 @@ console.log('Building Pixel Editor');
function copy_images(){ function copy_images(){
gulp.src('./images/') gulp.src('./images/*.png')
.pipe(gulp.dest(path.join(BUILDDIR, SLUG))); .pipe(gulp.dest(path.join(BUILDDIR, SLUG)));
} }

View File

@ -125,7 +125,7 @@ body {
z-index: 1120; z-index: 1120;
list-style-type: none; list-style-type: none;
overflow-y:scroll; overflow-y:scroll;
overflow-x:hidden; // TODO: make the scroll bar a bit fancier overflow-x:hidden;
#add-layer-button { #add-layer-button {
path { path {
fill: $baseicon; fill: $baseicon;
@ -178,7 +178,7 @@ body {
.layers-menu-entry { .layers-menu-entry {
cursor: pointer; cursor: pointer;
margin-top: 2px; margin-bottom: 2px;
font-size: 1em; font-size: 1em;
color: $basetext; color: $basetext;
background-color: lighten($basecolor, 4%); background-color: lighten($basecolor, 4%);
@ -301,7 +301,7 @@ svg {
} }
#pixel-canvas { #pixel-canvas {
z-index: 2; z-index: 3;
background: transparent; background: transparent;
} }
@ -311,7 +311,7 @@ svg {
} }
#tmp-canvas { #tmp-canvas {
z-index: 4; z-index: 5;
background: transparent; background: transparent;
} }
@ -687,14 +687,16 @@ svg {
#tools-menu li button#pencil-bigger-button, #tools-menu li button#pencil-bigger-button,
#tools-menu li button#zoom-in-button, #tools-menu li button#zoom-in-button,
#tools-menu li button#eraser-bigger-button, #tools-menu li button#eraser-bigger-button,
#tools-menu li button#rectangle-bigger-button { #tools-menu li button#rectangle-bigger-button,
#tools-menu li button#line-bigger-button {
left: 0; left: 0;
} }
#tools-menu li button#pencil-smaller-button, #tools-menu li button#pencil-smaller-button,
#tools-menu li button#zoom-out-button, #tools-menu li button#zoom-out-button,
#tools-menu li button#eraser-smaller-button, #tools-menu li button#eraser-smaller-button,
#tools-menu li button#rectangle-smaller-button { #tools-menu li button#rectangle-smaller-button,
#tools-menu li button#line-smaller-button {
right: 0; right: 0;
} }
@ -705,7 +707,9 @@ svg {
#tools-menu li.selected button#eraser-bigger-button, #tools-menu li.selected button#eraser-bigger-button,
#tools-menu li.selected button#eraser-smaller-button, #tools-menu li.selected button#eraser-smaller-button,
#tools-menu li.selected button#rectangle-bigger-button, #tools-menu li.selected button#rectangle-bigger-button,
#tools-menu li.selected button#rectangle-smaller-button { #tools-menu li.selected button#rectangle-smaller-button,
#tools-menu li.selected button#line-bigger-button,
#tools-menu li.selected button#line-smaller-button {
display: block; display: block;
} }
@ -775,8 +779,32 @@ svg {
} }
} }
div.update {
input {
background: $indent;
border: none;
border-radius: 4px;
color: $indenttext;
padding: 10px 20px;
margin: 0;
width: 60px;
text-align: center;
}
}
/*
input {
background: $indent;
border: none;
border-radius: 4px;
color: $indenttext;
padding: 10px 20px;
margin: 0;
width: 60px;
text-align: center;
}
*/
button.default { button.default {
float: right;
background: $basehover; background: $basehover;
border: none; border: none;
border-radius: 4px; border-radius: 4px;
@ -789,17 +817,6 @@ svg {
} }
} }
input {
background: $indent;
border: none;
border-radius: 4px;
color: $indenttext;
padding: 10px 20px;
margin: 0;
width: 60px;
text-align: center;
}
.dropdown-button { .dropdown-button {
background: $basehover url('/pixel-editor/dropdown-arrow.png') right center no-repeat; background: $basehover url('/pixel-editor/dropdown-arrow.png') right center no-repeat;
border: none; border: none;
@ -1038,9 +1055,9 @@ svg {
input[type=number] { input[type=number] {
position:relative; position:relative;
margin-left:10px; margin-left:10px;
height:15px; height:15px !important;
width:40px; width:40px !important;
padding:8px; padding:8px !important;
} }
input[type=number]::-webkit-outer-spin-button, input[type=number]::-webkit-outer-spin-button,
@ -1212,4 +1229,544 @@ svg {
background: $baseselected; background: $baseselected;
} }
} }
}
/***********************COLOUR PICKER*****************************/
#colour-picker {
background-color:$basecolor;
width:250px;
height:350px;
position:absolute;
display:inline-block;
input[type=text] {
background-color:$basetext;
color:$basecolor;
box-shadow:none;
border:none;
}
input[type=range] {
width: 100%;
margin: 2.2px 0;
background-color: transparent;
-webkit-appearance: none;
}
input[type=range]::-webkit-slider-runnable-track {
background: #484d4d;
border: 0;
width: 100%;
height: 25.6px;
cursor: pointer;
}
input[type=range]::-webkit-slider-thumb {
margin-top: -2.2px;
width: 18px;
height: 30px;
background: $basetext;
border: 0;
cursor: pointer;
-webkit-appearance: none;
}
input[type=range]::-moz-range-track {
background: #484d4d;
border: 0;
width: 100%;
height: 25.6px;
cursor: pointer;
}
input[type=range]::-moz-range-thumb {
width: 18px;
height: 30px;
background: $basetextweak;
border: 0;
cursor: pointer;
}
/*TODO: Use one of the selectors from https://stackoverflow.com/a/20541859/7077589 and figure out
how to remove the vertical space around the range input in IE*/
@supports (-ms-ime-align:auto) {
/* Pre-Chromium Edge only styles, selector taken from hhttps://stackoverflow.com/a/32202953/7077589 */
input[type=range].slider {
margin: 0;
/*Edge starts the margin from the thumb, not the track as other browsers do*/
}
}
}
#cp-modes {
margin: 0 0 0 0;
font-size:0;
height:40px;
float:left;
display:flex;
font-family: 'Roboto', sans-serif;
background-color:$basetextweak;
width:100%;
button {
font-size:14px;
left:0;
right:0;
margin:0 0 0 0;
border: none;
border-radius: 0;
height:100%;
width:37x;
background-color:$basehover;
color:$basetext;
cursor:pointer;
}
button:hover {
background-color:$baseicon;
color:$basetext;
}
button.cp-selected-mode {
background-color:$baseicon;
color:$basetext;
}
input {
width:50px;
right:0;
position:absolute;
}
div {
background-color:yellow;
width:100%;
height:100%;
z-index:2;
position:relative;
}
}
#sliders-container {
padding:10px;
}
.cp-slider-entry {
width:100%;
height:30px;
display:flex;
align-items:center;
margin-top:2px;
position:relative;
label {
width: 20px;
font-size:15px;
font-style: bold;
}
input[type=text] {
text-align:center;
width: 30px;
overflow:visible;
margin-left:4px;
}
}
.colour-picker-slider {
width:90%;
}
#cp-minipicker {
width:100%;
height:100px;
position:relative;
margin: 0 0 0 0;
z-index: inherit 2000;
input {
width:100%;
margin: none;
padding: none;
}
.cp-colours-previews {
width:100%;
position:relative;
}
.cp-colour-preview {
width:100%;
position:relative;
background-color:blue;
color:$basecolor;
float:left;
height:30px;
justify-content: center;
display:flex;
align-items: center;
font-size:12px;
}
#cp-colour-picking-modes {
width:100%;
position:relative;
}
button {
font-size:14px;
left:0;
right:0;
margin:0 0 0 0;
border: none;
border-radius: 0;
height:30px;
width:16.66666%;
float:left;
overflow:hidden;
background-color:$basehover;
color:$basetext;
cursor:pointer;
}
button:hover {
background-color:$baseicon;
color:$basetext;
}
button.cp-selected-mode {
background-color:$baseicon;
color:$basetext;
}
}
#cp-canvas-container {
width:100%;
height:100%;
position:relative;
}
#cp-spectrum {
width:100%;
height:100px;
position:absolute;
background-color: transparent;
}
.cp-picker-icon{
width:16px;
height:16px;
border-radius:100%;
position:absolute;
background-color:white;
z-index:2;
border:2px solid black;
}
/***************PALETTE BLOCK****************/
div#palette-block {
z-index:1000;
position:relative;
resize: horizontal;
margin: 0 0 0 0;
width:600px;
height:400px;
}
div#palette-container {
display:inline-block;
background-color: #232125;
position:absolute;
scrollbar-color: #332f35 #232125;
scroll-behavior: smooth;
left:300px;
width:300px;
height:320px;
overflow-y: scroll;
&::-webkit-scrollbar {
background: #232125;
width: 0.5em;
}
&::-webkit-scrollbar-track {
margin-top: -0.125em;
width: 0.5em;
}
&::-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;
}
}
ul#palette-list {
list-style:none;
margin: 0 0 0 0;
padding: 0 0 0 0;
position:relative;
display:inline-block;
li {
float:left;
width:50px;
height:50px;
border:none;
min-width:20px;
min-height:20px;
max-width:75px;
max-height:75px;
}
}
div#pb-options {
position:relative;
left:280px;
height:30px;
width:300px;
top:300px;
button {
border-radius:none;
position:relative;
float:left;
width:50%;
height:100%;
text-align:center;
cursor: pointer;
font-size:16px;
background-color:$baseicon;
border:none;
}
button:hover {
color: $basehovertext;
background-color: $basehover;
}
}
/********FEATURES LOG*************/
#splash {
width:100% !important;
height:100% !important;
background-color: #232125 !important;
opacity: 1 !important;
overflow-y: scroll;
#splash-input {
width:74%;
height:100vh !important;
color:$baselink;
#splash-menu {
position:relative;
height:100%;
left:0;
top:0;
}
#editor-logo {
font-weight:bold;
text-transform:uppercase;
font-size:20px;
height:35vh;
width:100%;
position:relative;
background-image:url('https://cdn.discordapp.com/attachments/506277390050131978/795660870221955082/final.png');
background-size:cover;
background-position:center;
}
#black {
width:100%;
height:100%;
position:relative;
background-color:rgba(0,0,0,0.2);
}
#sp-coverdata {
padding:20px;
p, a {
font-size:15px;
position:absolute;
text-transform:none;
right:20px;
}
p {
top:0px;
}
a {
font-size:17px;
bottom:20px;
text-decoration:underline;
}
}
}
#sp-quickstart-container {
left:250px;
height:65vh;
width:650px;
display:inline;
float:right;
}
#sp-quickstart-title {
padding:20px;
font-size:27px;
text-transform: uppercase;
font-weight: bold;
}
.sp-template {
display:table;
vertical-align: middle;
text-transform: uppercase;
position:relative;
width:100px;
height:100px;
border-radius:5%;
margin-right:30px;
margin-top:30px;
background-color:$basecolor;
float:left;
font-size: 18px;
text-align:center;
font-weight: bold;
&:hover {
cursor:pointer;
background-color:$baseselected;
}
p {
span {
display:block;
font-size:14px !important;
margin: 0 0 0 0;
padding: 0 0 0 0;
}
display:table-cell;
width:100%;
height:100%;
vertical-align: middle;
}
}
#sp-newpixel {
display: inline-block;
width:220px;
height:64vh;
padding:20px;
position:relative;
background-color:$basecolor;
.sp-np-entry {
width:100%;
text-align:center;
}
input {
border:none;
background-color: #232125;
color:$basetext;
font-size:14px;
width:40px;
padding:7px;
text-align:center;
}
#create-button {
font-size:18px;
width:150px;
margin-top:40px;
font-weight: bold;
}
#sp-mode-palette {
text-align: center;
position: relative;
float:bottom;
font-size:16px;
font-weight: bold;
div.button-menu {
border:2px solid $basetextweak;
border-radius:5px;
position:relative;
display:inline-block;
padding: 0 0 0 0;
text-align:left;
div {
border:none;
padding:none;
margin:none;
background-color:transparent;
width:100px;
float:left;
text-align: center;
height:25px;
cursor:pointer;
z-index:1;
p {
z-index:0;
-ms-transform: translateY(-60%);
transform: translateY(-60%);
}
}
.bm-right {
border-left:3px solid $basetextweak;
}
.sp-interface-selected {
background-color: $basetextweak;
}
}
}
}
#splash-news {
position:relative;
right:20px;
height:95%;
top:2.5%;
background-color: #232125 !important;
}
#latest-update {
font-size:15px;
width:350px !important;
height:90%;
overflow-y: scroll;
line-height: 1.5;
position:absolute;
top:40px;
right:0px;
&::-webkit-scrollbar {
background: #232125;
width: 0.5em;
}
&::-webkit-scrollbar-track {
margin-top: -0.125em;
width: 0.5em;
}
&::-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;
}
}
} }

1
debug.log Normal file
View File

@ -0,0 +1 @@
[0114/110029.536:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3)

BIN
images/sked.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

View File

@ -1,8 +1,10 @@
let currentPalette = []; let currentPalette = [];
//adds the given color to the palette /** Adds the given color to the palette
//input hex color string *
//returns list item element * @param {*} newColor the colour to add
* @return the list item containing the added colour
*/
function addColor (newColor) { function addColor (newColor) {
//add # at beginning if not present //add # at beginning if not present
if (newColor.charAt(0) != '#') if (newColor.charAt(0) != '#')
@ -17,6 +19,7 @@ function addColor (newColor) {
button.style.backgroundColor = newColor; button.style.backgroundColor = newColor;
button.addEventListener('mouseup', clickedColor); button.addEventListener('mouseup', clickedColor);
listItem.appendChild(button); listItem.appendChild(button);
listItem.classList.add("draggable-colour")
//insert new listItem element at the end of the colors menu (right before add button) //insert new listItem element at the end of the colors menu (right before add button)
colorsMenu.insertBefore(listItem, colorsMenu.children[colorsMenu.children.length-1]); colorsMenu.insertBefore(listItem, colorsMenu.children[colorsMenu.children.length-1]);
@ -35,11 +38,19 @@ function addColor (newColor) {
//hide edit button //hide edit button
button.parentElement.lastChild.classList.add('hidden'); button.parentElement.lastChild.classList.add('hidden');
//show jscolor picker //show jscolor picker, if basic mode is enabled
button.parentElement.firstChild.jscolor.show(); if (pixelEditorMode == 'Basic')
button.parentElement.firstChild.jscolor.show();
else
showDialogue("palette-block", false);
}); });
console.log(currentPalette);
return listItem; return listItem;
} }
new Sortable(document.getElementById("colors-menu"), {
animation:100,
filter: ".noshrink",
draggable: ".draggable-colour",
onEnd: makeIsDraggingFalse
});

View File

@ -1,4 +1,4 @@
//add color button // add-color-button management
on('click', 'add-color-button', function(){ on('click', 'add-color-button', function(){
if (!documentCreated) return; if (!documentCreated) return;

185
js/_algorithms.js Normal file
View File

@ -0,0 +1,185 @@
// CONSTS
// Degrees to radiants
let degreesToRad = Math.PI / 180;
// I'm pretty sure that precision is necessary
let referenceWhite = {x: 95.05, y: 100, z: 108.89999999999999};
/**********************SECTION: COLOUR CONVERSIONS****************************** */
/**
* Converts an HSL color value to RGB. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes h, s, and l are contained in the set [0, 1] and
* returns r, g, and b in the set [0, 255].
*
* @param {number} h The hue
* @param {number} s The saturation
* @param {number} l The lightness
* @return {Array} The RGB representation
*/
function cpHslToRgb(h, s, l){
var r, g, b;
h /= 360;
s /= 100;
l /= 100;
if(s == 0){
r = g = b = l; // achromatic
}else{
var hue2rgb = function hue2rgb(p, q, t){
if(t < 0) t += 1;
if(t > 1) t -= 1;
if(t < 1/6) return p + (q - p) * 6 * t;
if(t < 1/2) return q;
if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
}
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}
function hsvToRgb(h, s, v) {
var r, g, b;
h /= 360;
s /= 100;
v /= 100;
var i = Math.floor(h * 6);
var f = h * 6 - i;
var p = v * (1 - s);
var q = v * (1 - f * s);
var t = v * (1 - (1 - f) * s);
switch (i % 6) {
case 0: r = v, g = t, b = p; break;
case 1: r = q, g = v, b = p; break;
case 2: r = p, g = v, b = t; break;
case 3: r = p, g = q, b = v; break;
case 4: r = t, g = p, b = v; break;
case 5: r = v, g = p, b = q; break;
}
return [ r * 255, g * 255, b * 255 ];
}
function hslToHex(h, s, l) {
h /= 360;
s /= 100;
l /= 100;
let r, g, b;
if (s === 0) {
r = g = b = l; // achromatic
} else {
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
const toHex = x => {
const hex = Math.round(x * 255).toString(16);
return hex.length === 1 ? '0' + hex : hex;
};
return `${toHex(r)}${toHex(g)}${toHex(b)}`;
}
function rgbToHsl(col) {
let r = col.r;
let g = col.g;
let b = col.b;
r /= 255, g /= 255, b /= 255;
let max = Math.max(r, g, b), min = Math.min(r, g, b);
let myH, myS, myL = (max + min) / 2;
if (max == min) {
myH = myS = 0; // achromatic
}
else {
let d = max - min;
myS = myL > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: myH = (g - b) / d + (g < b ? 6 : 0); break;
case g: myH = (b - r) / d + 2; break;
case b: myH = (r - g) / d + 4; break;
}
myH /= 6;
}
return {h: myH, s: myS, l: myL };
}
function rgbToHsv(col) {
let r = col.r;
let g = col.g;
let b = col.b;
r /= 255, g /= 255, b /= 255;
let max = Math.max(r, g, b), min = Math.min(r, g, b);
let myH, myS, myV = max;
let d = max - min;
myS = max == 0 ? 0 : d / max;
if (max == min) {
myH = 0; // achromatic
}
else {
switch (max) {
case r: myH = (g - b) / d + (g < b ? 6 : 0); break;
case g: myH = (b - r) / d + 2; break;
case b: myH = (r - g) / d + 4; break;
}
myH /= 6;
}
return {h: myH, s: myS, v: myV};
}
function RGBtoCIELAB(rgbColour) {
// Convert to XYZ first via matrix transformation
let x = 0.412453 * rgbColour.r + 0.357580 * rgbColour.g + 0.180423 * rgbColour.b;
let y = 0.212671 * rgbColour.r + 0.715160 * rgbColour.g + 0.072169 * rgbColour.b;
let z = 0.019334 * rgbColour.r + 0.119193 * rgbColour.g + 0.950227 * rgbColour.b;
let xFunc = CIELABconvF(x / referenceWhite.x);
let yFunc = CIELABconvF(y / referenceWhite.y);
let zFunc = CIELABconvF(z / referenceWhite.z);
let myL = 116 * yFunc - 16;
let myA = 500 * (xFunc - yFunc);
let myB = 200 * (yFunc - zFunc);
return {l: myL, a: myA, b: myB};
}
function CIELABconvF(value) {
if (value > Math.pow(6/29, 3)) {
return Math.cbrt(value);
}
return 1/3 * Math.pow(6/29, 2) * value + 4/29;
}

View File

@ -1,4 +1,9 @@
function changeZoom (layer, direction, cursorLocation) { /** Changes the zoom level of the canvas
* @param {*} direction 'in' or 'out'
* @param {*} cursorLocation The position of the cursor when the user zoomed
*/
function changeZoom (direction, cursorLocation) {
// Computing current width and height
var oldWidth = canvasSize[0] * zoom; var oldWidth = canvasSize[0] * zoom;
var oldHeight = canvasSize[1] * zoom; var oldHeight = canvasSize[1] * zoom;
var newWidth, newHeight; var newWidth, newHeight;
@ -11,7 +16,9 @@ function changeZoom (layer, direction, cursorLocation) {
newHeight = canvasSize[1] * zoom; newHeight = canvasSize[1] * zoom;
//adjust canvas position //adjust canvas position
layer.setCanvasOffset(layer.canvas.offsetLeft + (oldWidth - newWidth) *cursorLocation[0]/oldWidth, layer.canvas.offsetTop + (oldHeight - newHeight) *cursorLocation[1]/oldWidth); layers[0].setCanvasOffset(
layers[0].canvas.offsetLeft + (oldWidth - newWidth) * cursorLocation[0]/oldWidth,
layers[0].canvas.offsetTop + (oldHeight - newHeight) * cursorLocation[1]/oldWidth);
} }
//if you want to zoom in //if you want to zoom in
else if (direction == 'in' && zoom + Math.ceil(zoom/10) < window.innerHeight/4){ else if (direction == 'in' && zoom + Math.ceil(zoom/10) < window.innerHeight/4){
@ -20,11 +27,13 @@ function changeZoom (layer, direction, cursorLocation) {
newHeight = canvasSize[1] * zoom; newHeight = canvasSize[1] * zoom;
//adjust canvas position //adjust canvas position
layer.setCanvasOffset(layer.canvas.offsetLeft - Math.round((newWidth - oldWidth)*cursorLocation[0]/oldWidth), layer.canvas.offsetTop - Math.round((newHeight - oldHeight)*cursorLocation[1]/oldHeight)); layers[0].setCanvasOffset(
layers[0].canvas.offsetLeft - Math.round((newWidth - oldWidth)*cursorLocation[0]/oldWidth),
layers[0].canvas.offsetTop - Math.round((newHeight - oldHeight)*cursorLocation[1]/oldHeight));
} }
//resize canvas //resize canvas
layer.resize(); layers[0].resize();
// adjust brush size // adjust brush size
currentTool.updateCursor(); currentTool.updateCursor();

View File

@ -13,7 +13,9 @@ var currentColor = firstCheckerBoardColor;
// Saving number of squares filled until now // Saving number of squares filled until now
var nSquaresFilled = 0; var nSquaresFilled = 0;
/** Fills the checkerboard canvas with squares with alternating colours
*
*/
function fillCheckerboard() { function fillCheckerboard() {
// Getting checkerboard context // Getting checkerboard context
var context = checkerBoard.context; var context = checkerBoard.context;

View File

@ -17,7 +17,7 @@ function clickedColor (e){
e.target.parentElement.classList.add('selected'); e.target.parentElement.classList.add('selected');
} else if (e.which == 3) { //right clicked color } else if (e.which == 3) { //right clicked color
console.log('right clicked color button'); //console.log('right clicked color button');
//hide edit color button (to prevent it from showing) //hide edit color button (to prevent it from showing)
e.target.parentElement.lastChild.classList.add('hidden'); e.target.parentElement.lastChild.classList.add('hidden');

View File

@ -23,7 +23,7 @@ on('input', 'jscolor-hex-input', function (e) {
//changes all of one color to another after being changed from color picker //changes all of one color to another after being changed from color picker
function colorChanged(e) { function colorChanged(e) {
console.log('colorChanged() to ' + e.target.value); //console.log('colorChanged() to ' + e.target.value);
//get colors //get colors
var newColor = hexToRgb(e.target.value); var newColor = hexToRgb(e.target.value);
var oldColor = e.target.oldColor; var oldColor = e.target.oldColor;
@ -41,7 +41,7 @@ function colorChanged(e) {
//check if selected color already matches another color //check if selected color already matches another color
colors = document.getElementsByClassName('color-button'); colors = document.getElementsByClassName('color-button');
console.log(colors); //console.log(colors);
var colorCheckingStyle = 'background: #bc60c4; color: white'; var colorCheckingStyle = 'background: #bc60c4; color: white';
var newColorHex = e.target.value.toLowerCase(); var newColorHex = e.target.value.toLowerCase();
@ -54,11 +54,11 @@ function colorChanged(e) {
//if generated color matches this color //if generated color matches this color
if (newColorHex == colors[i].jscolor.toString()) { if (newColorHex == colors[i].jscolor.toString()) {
console.log('%ccolor already exists'+(colors[i].parentElement.classList.contains('jscolor-active')?' (but is the current color)':''), colorCheckingStyle); //console.log('%ccolor already exists'+(colors[i].parentElement.classList.contains('jscolor-active')?' (but is the current color)':''), colorCheckingStyle);
//if the color isnt the one that has the picker currently open //if the color isnt the one that has the picker currently open
if (!colors[i].parentElement.classList.contains('jscolor-active')) { if (!colors[i].parentElement.classList.contains('jscolor-active')) {
console.log('%cColor is duplicate', colorCheckingStyle); //console.log('%cColor is duplicate', colorCheckingStyle);
//show the duplicate color warning //show the duplicate color warning
duplicateColorWarning.style.visibility = 'visible'; duplicateColorWarning.style.visibility = 'visible';

806
js/_colorPicker.js Normal file
View File

@ -0,0 +1,806 @@
let sliders = document.getElementsByClassName("cp-slider-entry");
let colourPreview = document.getElementById("cp-colour-preview");
let colourValue = document.getElementById("cp-hex");
let currentPickerMode = "rgb";
let currentPickingMode = "mono";
let styleElement = document.createElement("style");
let miniPickerCanvas = document.getElementById("cp-spectrum");
let miniPickerSlider = document.getElementById("cp-minipicker-slider");
let activePickerIcon = document.getElementById("cp-active-icon");
let pickerIcons = [activePickerIcon];
let hexContainers = [document.getElementById("cp-colours-previews").children[0],null,null,null];
let startPickerIconPos = [[0,0],[0,0],[0,0],[0,0]];
let currPickerIconPos = [[0,0], [0,0],[0,0],[0,0]];
let styles = ["",""];
let draggingCursor = false;
cpInit();
function cpInit() {
// Appending the palette styles
document.getElementsByTagName("head")[0].appendChild(styleElement);
// Saving first icon position
startPickerIconPos[0] = [miniPickerCanvas.getBoundingClientRect().left, miniPickerCanvas.getBoundingClientRect().top];
// Set the correct size of the canvas
miniPickerCanvas.height = miniPickerCanvas.getBoundingClientRect().height;
miniPickerCanvas.width = miniPickerCanvas.getBoundingClientRect().width;
// Update picker position
updatePickerByHex(colourValue.value);
// Startup updating
updateAllSliders();
// Fill minislider
updateMiniSlider(colourValue.value);
// Fill minipicker
updatePickerByHex(colourValue.value);
updateMiniPickerSpectrum();
}
function hexUpdated() {
updatePickerByHex(colourValue.value);
updateSlidersByHex(colourValue.value);
}
// Applies the styles saved in the style array to the style element in the head of the document
function updateStyles() {
styleElement.innerHTML = styles[0] + styles[1];
}
/** Updates the background gradients of the sliders given their value
* Updates the hex colour and its preview
* Updates the minipicker according to the computed hex colour
*
*/
function updateSliderValue (sliderIndex, updateMini = true) {
let toUpdate;
let slider;
let input;
let hexColour;
let sliderValues;
toUpdate = sliders[sliderIndex - 1];
slider = toUpdate.getElementsByTagName("input")[0];
input = toUpdate.getElementsByTagName("input")[1];
// Update label value
input.value = slider.value;
// Update preview colour
// get slider values
sliderValues = getSlidersValues();
// Generate preview colour
switch (currentPickerMode) {
case 'rgb':
hexColour = rgbToHex(sliderValues[0], sliderValues[1], sliderValues[2]);
break;
case 'hsv':
let tmpRgb = hsvToRgb(sliderValues[0], sliderValues[1], sliderValues[2]);
hexColour = rgbToHex(parseInt(tmpRgb[0]), parseInt(tmpRgb[1]), parseInt(tmpRgb[2]));
break;
case 'hsl':
hexColour = hslToHex(sliderValues[0], sliderValues[1], sliderValues[2]);
break;
default:
console.log("wtf select a decent picker mode");
return;
}
// Update preview colour div
colourPreview.style.backgroundColor = '#' + hexColour;
colourValue.value = '#' + hexColour;
// Update sliders background
// there's no other way than creating a custom css file, appending it to the head and
// specify the sliders' backgrounds here
styles[0] = '';
for (let i=0; i<sliders.length; i++) {
styles[0] += getSliderCSS(i + 1, sliderValues);
}
updateStyles();
if (updateMini) {
updatePickerByHex(colourValue.value);
updateMiniPickerSpectrum();
}
}
// Calculates the css gradient for a slider
function getSliderCSS(index, sliderValues) {
let ret = 'input[type=range]#';
let sliderId;
let gradientMin;
let gradientMax;
let hueGradient;
let rgbColour;
switch (index) {
case 1:
sliderId = 'first-slider';
switch (currentPickerMode) {
case 'rgb':
gradientMin = 'rgba(0,' + sliderValues[1] + ',' + sliderValues[2] + ',1)';
gradientMax = 'rgba(255,' + sliderValues[1] + ',' + sliderValues[2] + ',1)';
break;
case 'hsv':
hueGradient = getHueGradientHSV(sliderValues);
break;
case 'hsl':
// Hue gradient
hueGradient = getHueGradientHSL(sliderValues);
break;
}
break;
case 2:
sliderId = 'second-slider';
switch (currentPickerMode) {
case 'rgb':
gradientMin = 'rgba(' + sliderValues[0] + ',0,' + sliderValues[2] + ',1)';
gradientMax = 'rgba(' + sliderValues[0] + ',255,' + sliderValues[2] + ',1)';
break;
case 'hsv':
rgbColour = hsvToRgb(sliderValues[0], 0, sliderValues[2]);
gradientMin = 'rgba(' + rgbColour[0] + ',' + rgbColour[1] + ',' + rgbColour[2] + ',1)';
rgbColour = hsvToRgb(sliderValues[0], 100, sliderValues[2]);
gradientMax = 'rgba(' + rgbColour[0] + ',' + rgbColour[1] + ',' + rgbColour[2] + ',1)';
break;
case 'hsl':
rgbColour = cpHslToRgb(sliderValues[0], 0, sliderValues[2]);
gradientMin = 'rgba(' + rgbColour[0] + ',' + rgbColour[1] + ',' + rgbColour[2] + ',1)';
rgbColour = cpHslToRgb(sliderValues[0], 100, sliderValues[2]);
gradientMax = 'rgba(' + rgbColour[0] + ',' + rgbColour[1] + ',' + rgbColour[2] + ',1)';
break;
}
break;
case 3:
sliderId = 'third-slider';
switch (currentPickerMode) {
case 'rgb':
gradientMin = 'rgba(' + sliderValues[0] + ',' + sliderValues[1] + ',0,1)';
gradientMax = 'rgba(' + sliderValues[0] + ',' + sliderValues[1] + ',255,1)';
break;
case 'hsv':
rgbColour = hsvToRgb(sliderValues[0], sliderValues[1], 0);
gradientMin = 'rgba(' + rgbColour[0] + ',' + rgbColour[1] + ',' + rgbColour[2] + ',1)';
rgbColour = hsvToRgb(sliderValues[0], sliderValues[1], 100);
gradientMax = 'rgba(' + rgbColour[0] + ',' + rgbColour[1] + ',' + rgbColour[2] + ',1)';
break;
case 'hsl':
gradientMin = 'rgba(0,0,0,1)';
gradientMax = 'rgba(255,255,255,1)';
break;
}
break;
default:
return '';
}
ret += sliderId;
ret += '::-webkit-slider-runnable-track {';
switch (currentPickerMode) {
case 'rgb':
ret += 'background: linear-gradient(90deg, rgba(2,0,36,1) 0%, ' +
gradientMin + ' 0%, ' + gradientMax + '100%)';
break;
case 'hsv':
case 'hsl':
ret += 'background: ';
if (index == 1) {
ret += hueGradient;
}
else {
ret += 'linear-gradient(90deg, rgba(2,0,36,1) 0%, ' + gradientMin + ' 0%, ';
// For hsl I also have to add a middle point
if (currentPickerMode == 'hsl' && index == 3) {
let rgb = cpHslToRgb(sliderValues[0], sliderValues[1], 50);
ret += 'rgba(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ',1) 50%,';
}
ret += gradientMax + '100%);';
}
break;
}
ret += '}'
ret += ret.replace('::-webkit-slider-runnable-track', '::-moz-range-track');
return ret;
}
// Computes the hue gradient used for hsl
function getHueGradientHSL(sliderValues) {
return 'linear-gradient(90deg, rgba(2,0,36,1) 0%, \
hsl(0,' + sliderValues[1] + '%,' + sliderValues[2]+ '%) 0%, \
hsl(60,' + sliderValues[1] + '%,' + sliderValues[2]+ '%) 16.6666%, \
hsl(120,' + sliderValues[1] + '%,' + sliderValues[2]+ '%) 33.3333333333%, \
hsl(180,'+ sliderValues[1] + '%,' + sliderValues[2]+ '%) 50%, \
hsl(240,' + sliderValues[1] + '%,' + sliderValues[2]+ '%) 66.66666%, \
hsl(300,'+ sliderValues[1] + '%,' + sliderValues[2]+ '%) 83.333333%, \
hsl(360,'+ sliderValues[1] + '%,' + sliderValues[2]+ '%) 100%);';
}
// Computes the hue gradient used for hsv
function getHueGradientHSV(sliderValues) {
let col = hsvToRgb(0, sliderValues[1], sliderValues[2]);
let ret = 'linear-gradient(90deg, rgba(2,0,36,1) 0%, ';
ret += 'rgba(' + col[0] + ',' + col[1] + ',' + col[2] + ',1) 0%,'
col = hsvToRgb(60, sliderValues[1], sliderValues[2]);
ret += 'rgba(' + col[0] + ',' + col[1] + ',' + col[2] + ',1) 16.6666%,';
col = hsvToRgb(120, sliderValues[1], sliderValues[2]);
ret += 'rgba(' + col[0] + ',' + col[1] + ',' + col[2] + ',1) 33.3333333333%,';
col = hsvToRgb(180, sliderValues[1], sliderValues[2]);
ret += 'rgba(' + col[0] + ',' + col[1] + ',' + col[2] + ',1) 50%,';
col = hsvToRgb(240, sliderValues[1], sliderValues[2]);
ret += 'rgba(' + col[0] + ',' + col[1] + ',' + col[2] + ',1) 66.66666%,';
col = hsvToRgb(300, sliderValues[1], sliderValues[2]);
ret += 'rgba(' + col[0] + ',' + col[1] + ',' + col[2] + ',1) 83.333333%,';
col = hsvToRgb(360, sliderValues[1], sliderValues[2]);
ret += 'rgba(' + col[0] + ',' + col[1] + ',' + col[2] + ',1) 100%);';
return ret;
}
// Fired when the values in the labels are changed
function inputChanged(target, index) {
let sliderIndex = index - 1;
sliders[sliderIndex].getElementsByTagName("input")[0].value = target.value;
updateSliderValue(index);
}
// Updates the colour model used to pick colours
function changePickerMode(target, newMode) {
let maxRange;
let colArray;
let rgbTmp;
let hexColour = colourValue.value.replace('#', '');
currentPickerMode = newMode;
document.getElementsByClassName("cp-selected-mode")[0].classList.remove("cp-selected-mode");
target.classList.add("cp-selected-mode");
switch (newMode)
{
case 'rgb':
maxRange = [255,255,255];
sliders[0].getElementsByTagName("label")[0].innerHTML = 'R';
sliders[1].getElementsByTagName("label")[0].innerHTML = 'G';
sliders[2].getElementsByTagName("label")[0].innerHTML = 'B';
break;
case 'hsv':
maxRange = [360, 100, 100];
sliders[0].getElementsByTagName("label")[0].innerHTML = 'H';
sliders[1].getElementsByTagName("label")[0].innerHTML = 'S';
sliders[2].getElementsByTagName("label")[0].innerHTML = 'V';
break;
case 'hsl':
maxRange = [360, 100, 100];
sliders[0].getElementsByTagName("label")[0].innerHTML = 'H';
sliders[1].getElementsByTagName("label")[0].innerHTML = 'S';
sliders[2].getElementsByTagName("label")[0].innerHTML = 'L';
break;
default:
console.log("wtf select a decent picker mode");
break;
}
for (let i=0; i<sliders.length; i++) {
let slider = sliders[i].getElementsByTagName("input")[0];
slider.setAttribute("max", maxRange[i]);
}
// Putting the current colour in the new slider
switch(currentPickerMode) {
case 'rgb':
colArray = hexToRgb(hexColour);
colArray = [colArray.r, colArray.g, colArray.b];
break;
case 'hsv':
rgbTmp = hexToRgb(hexColour);
colArray = rgbToHsv(rgbTmp);
colArray.h *= 360;
colArray.s *= 100;
colArray.v *= 100;
colArray = [colArray.h, colArray.s, colArray.v];
break;
case 'hsl':
rgbTmp = hexToRgb(hexColour);
colArray = rgbToHsl(rgbTmp);
colArray.h *= 360;
colArray.s *= 100;
colArray.l *= 100;
colArray = [colArray.h, colArray.s, colArray.l];
break;
default:
break;
}
for (let i=0; i<3; i++) {
sliders[i].getElementsByTagName("input")[0].value = colArray[i];
}
updateAllSliders();
}
// Returns an array containing the values of the sliders
function getSlidersValues() {
return [parseInt(sliders[0].getElementsByTagName("input")[0].value),
parseInt(sliders[1].getElementsByTagName("input")[0].value),
parseInt(sliders[2].getElementsByTagName("input")[0].value)];
}
// Updates every slider
function updateAllSliders(updateMini=true) {
for (let i=1; i<=3; i++) {
updateSliderValue(i, updateMini);
}
}
/******************SECTION: MINIPICKER******************/
// Moves the picker icon according to the mouse position on the canvas
function movePickerIcon(event) {
event.preventDefault();
if (event.which == 1 || draggingCursor) {
let cursorPos = getCursorPosMinipicker(event);
let canvasRect = miniPickerCanvas.getBoundingClientRect();
let left = (cursorPos[0] - startPickerIconPos[0][0] - 8);
let top = (cursorPos[1] - startPickerIconPos[0][1] - 8);
if (left > -8 && top > -8 && left < canvasRect.width-8 && top < canvasRect.height-8){
activePickerIcon.style["left"] = "" + left + "px";
activePickerIcon.style["top"]= "" + top + "px";
currPickerIconPos[0] = [left, top];
}
updateMiniPickerColour();
updateOtherIcons();
}
}
// Updates the main sliders given a hex value computed with the minipicker
function updateSlidersByHex(hex, updateMini = true) {
let colour;
let mySliders = [sliders[0].getElementsByTagName("input")[0],
sliders[1].getElementsByTagName("input")[0],
sliders[2].getElementsByTagName("input")[0]];
switch (currentPickerMode) {
case 'rgb':
colour = hexToRgb(hex);
mySliders[0].value = colour.r;
mySliders[1].value = colour.g;
mySliders[2].value = colour.b;
break;
case 'hsv':
colour = rgbToHsv(hexToRgb(hex));
mySliders[0].value = colour.h * 360;
mySliders[1].value = colour.s * 100;
mySliders[2].value = colour.v * 100;
break;
case 'hsl':
colour = rgbToHsl(hexToRgb(hex));
mySliders[0].value = colour.h * 360;
mySliders[1].value = colour.s * 100;
mySliders[2].value = colour.l * 100;
break;
default:
break;
}
updateAllSliders(false);
}
// Gets the position of the picker cursor relative to the canvas
function getCursorPosMinipicker(e) {
var x;
var y;
if (e.pageX != undefined && e.pageY != undefined) {
x = e.pageX;
y = e.pageY;
}
else {
x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop;
}
x -= miniPickerCanvas.offsetLeft;
y -= miniPickerCanvas.offsetTop;
return [Math.round(x), Math.round(y)];
}
// Updates the minipicker given a hex computed by the main sliders
// Moves the cursor
function updatePickerByHex(hex) {
let hsv = rgbToHsv(hexToRgb(hex));
let xPos = miniPickerCanvas.width * hsv.h - 8;
let yPos = miniPickerCanvas.height * hsv.s + 8;
miniPickerSlider.value = hsv.v * 100;
currPickerIconPos[0][0] = xPos;
currPickerIconPos[0][1] = miniPickerCanvas.height - yPos;
if (currPickerIconPos[0][1] >= 92)
{
currPickerIconPos[0][1] = 91.999;
}
activePickerIcon.style.left = '' + xPos + 'px';
activePickerIcon.style.top = '' + (miniPickerCanvas.height - yPos) + 'px';
activePickerIcon.style.backgroundColor = '#' + getMiniPickerColour();
colourPreview.style.backgroundColor = hex;
updateOtherIcons();
updateMiniSlider(hex);
}
// Fired when the value of the minislider changes: updates the spectrum gradient and the hex colour
function miniSliderInput(event) {
let newHex;
let newHsv = rgbToHsv(hexToRgb(getMiniPickerColour()));
let rgb;
// Adding slider value to value
newHsv.v = parseInt(event.target.value);
// Updating hex
rgb = hsvToRgb(newHsv.h * 360, newHsv.s * 100, newHsv.v);
newHex = rgbToHex(Math.round(rgb[0]), Math.round(rgb[1]), Math.round(rgb[2]));
colourValue.value = newHex;
updateMiniPickerSpectrum();
updateMiniPickerColour();
}
// Updates the hex colour after having changed the minislider (MERGE)
function updateMiniPickerColour() {
let hex = getMiniPickerColour();
activePickerIcon.style.backgroundColor = '#' + hex;
// Update hex and sliders based on hex
colourValue.value = '#' + hex;
colourPreview.style.backgroundColor = '#' + hex;
updateSlidersByHex(hex);
updateMiniSlider(hex);
updateOtherIcons();
}
// Returns the current colour of the minipicker
function getMiniPickerColour() {
let hex;
let pickedColour;
pickedColour = miniPickerCanvas.getContext('2d').getImageData(currPickerIconPos[0][0] + 8,
currPickerIconPos[0][1] + 8, 1, 1).data;
hex = rgbToHex(pickedColour[0], pickedColour[1], pickedColour[2]);
return hex;
}
// Update the background gradient of the slider in the minipicker
function updateMiniSlider(hex) {
let rgb = hexToRgb(hex);
styles[1] = "input[type=range]#cp-minipicker-slider::-webkit-slider-runnable-track { background: rgb(2,0,36);";
styles[1] += "background: linear-gradient(90deg, rgba(2,0,36,1) 0%, rgba(0,0,0,1) 0%, " +
"rgba(" + rgb.r + "," + rgb.g + "," + rgb.b + ",1) 100%);}";
updateMiniPickerSpectrum();
updateStyles();
}
// Updates the gradient of the spectrum canvas in the minipicker
function updateMiniPickerSpectrum() {
let ctx = miniPickerCanvas.getContext('2d');
let hsv = rgbToHsv(hexToRgb(colourValue.value));
let tmp;
let white = {h:hsv.h * 360, s:0, v: parseInt(miniPickerSlider.value)};
white = hsvToRgb(white.h, white.s, white.v);
ctx.clearRect(0, 0, miniPickerCanvas.width, miniPickerCanvas.height);
// Drawing hues
var hGrad = ctx.createLinearGradient(0, 0, miniPickerCanvas.width, 0);
for (let i=0; i<7; i++) {
tmp = hsvToRgb(60 * i, 100, hsv.v * 100);
hGrad.addColorStop(i / 6, '#' + rgbToHex(Math.round(tmp[0]), Math.round(tmp[1]), Math.round(tmp[2])));
}
ctx.fillStyle = hGrad;
ctx.fillRect(0, 0, miniPickerCanvas.width, miniPickerCanvas.height);
// Drawing sat / lum
var vGrad = ctx.createLinearGradient(0, 0, 0, miniPickerCanvas.height);
vGrad.addColorStop(0, 'rgba(' + white[0] +',' + white[1] + ',' + white[2] + ',0)');
/*
vGrad.addColorStop(0.1, 'rgba(255,255,255,0)');
vGrad.addColorStop(0.9, 'rgba(255,255,255,1)');
*/
vGrad.addColorStop(1, 'rgba(' + white[0] +',' + white[1] + ',' + white[2] + ',1)');
ctx.fillStyle = vGrad;
ctx.fillRect(0, 0, miniPickerCanvas.width, miniPickerCanvas.height);
}
function toggleDraggingCursor() {
draggingCursor = !draggingCursor;
}
function changePickingMode(event, newMode) {
let nIcons = pickerIcons.length;
let canvasContainer = document.getElementById("cp-canvas-container");
// Number of hex containers to add
let nHexContainers;
// Remove selected class from previous mode
document.getElementById("cp-colour-picking-modes").getElementsByClassName("cp-selected-mode")[0].classList.remove("cp-selected-mode");
// Updating mode
currentPickingMode = newMode;
// Adding selected class to new mode
event.target.classList.add("cp-selected-mode");
for (let i=1; i<nIcons; i++) {
// Deleting extra icons
pickerIcons.pop();
canvasContainer.removeChild(canvasContainer.children[2]);
// Deleting extra hex containers
hexContainers[0].parentElement.removeChild(hexContainers[0].parentElement.children[1]);
hexContainers[i] = null;
}
// Resetting first hex container size
hexContainers[0].style.width = '100%';
switch (currentPickingMode)
{
case 'analog':
createIcon();
createIcon();
nHexContainers = 2;
break;
case 'cmpt':
// Easiest one, add 180 to the H value and move the icon
createIcon();
nHexContainers = 1;
break;
case 'tri':
createIcon();
createIcon();
nHexContainers = 2;
break
case 'scmpt':
createIcon();
createIcon();
nHexContainers = 2;
break;
case 'tetra':
for (let i=0; i<3; i++) {
createIcon();
}
nHexContainers = 3;
break;
default:
console.log("How did you select the " + currentPickingMode + ", hackerman?");
break;
}
// Editing the size of the first container
hexContainers[0].style.width = '' + 100 / (nHexContainers + 1) + '%';
// Adding hex preview containers
for (let i=0; i<nHexContainers; i++) {
let newContainer = document.createElement("div");
newContainer.classList.add("cp-colour-preview");
newContainer.style.width = "" + (100 / (nHexContainers + 1)) + "%";
hexContainers[0].parentElement.appendChild(newContainer);
hexContainers[i + 1] = newContainer;
}
function createIcon() {
let newIcon = document.createElement("div");
newIcon.classList.add("cp-picker-icon");
pickerIcons.push(newIcon);
canvasContainer.appendChild(newIcon);
}
updateOtherIcons();
}
function updateOtherIcons() {
let currentColorHex = colourValue.value;
let currentColourHsv = rgbToHsv(hexToRgb(currentColorHex));
let newColourHsv = {h:currentColourHsv.h, s:currentColourHsv.s, v:currentColourHsv.v};
let newColourHexes = ['', '', ''];
let tmpRgb;
// Salvo tutti i
switch (currentPickingMode)
{
case 'mono':
break;
case 'analog':
// First colour
newColourHsv.h = (((currentColourHsv.h*360 + 40) % 360) / 360);
currPickerIconPos[1][0] = miniPickerCanvas.width * newColourHsv.h - 8;
currPickerIconPos[1][1] = miniPickerCanvas.height - (miniPickerCanvas.height * newColourHsv.s + 8);
tmpRgb = hsvToRgb(newColourHsv.h*360, newColourHsv.s*100, newColourHsv.v*100);
newColourHexes[0] = rgbToHex(Math.round(tmpRgb[0]), Math.round(tmpRgb[1]), Math.round(tmpRgb[2]));
// Second colour
newColourHsv.h = (((currentColourHsv.h*360 - 40) % 360) / 360);
if (newColourHsv.h < 0) {
newColourHsv.h += 1;
}
currPickerIconPos[2][0] = miniPickerCanvas.width * newColourHsv.h - 8;
currPickerIconPos[2][1] = miniPickerCanvas.height - (miniPickerCanvas.height * newColourHsv.s + 8);
tmpRgb = hsvToRgb(newColourHsv.h*360, newColourHsv.s*100, newColourHsv.v*100);
newColourHexes[1] = rgbToHex(Math.round(tmpRgb[0]), Math.round(tmpRgb[1]), Math.round(tmpRgb[2]));
break;
case 'cmpt':
newColourHsv.h = (((currentColourHsv.h*360 + 180) % 360) / 360);
currPickerIconPos[1][0] = miniPickerCanvas.width * newColourHsv.h - 8;
currPickerIconPos[1][1] = miniPickerCanvas.height - (miniPickerCanvas.height * newColourHsv.s + 8);
tmpRgb = hsvToRgb(newColourHsv.h*360, newColourHsv.s*100, newColourHsv.v*100);
newColourHexes[0] = rgbToHex(Math.round(tmpRgb[0]), Math.round(tmpRgb[1]), Math.round(tmpRgb[2]));
break;
case 'tri':
for (let i=1; i< 3; i++) {
newColourHsv.h = (((currentColourHsv.h*360 + 120*i) % 360) / 360);
currPickerIconPos[i][0] = miniPickerCanvas.width * newColourHsv.h - 8;
currPickerIconPos[i][1] = miniPickerCanvas.height - (miniPickerCanvas.height * newColourHsv.s + 8);
tmpRgb = hsvToRgb(newColourHsv.h*360, newColourHsv.s*100, newColourHsv.v*100);
newColourHexes[i - 1] = rgbToHex(Math.round(tmpRgb[0]), Math.round(tmpRgb[1]), Math.round(tmpRgb[2]));
}
break
case 'scmpt':
// First colour
newColourHsv.h = (((currentColourHsv.h*360 + 210) % 360) / 360);
currPickerIconPos[1][0] = miniPickerCanvas.width * newColourHsv.h - 8;
currPickerIconPos[1][1] = miniPickerCanvas.height - (miniPickerCanvas.height * newColourHsv.s + 8);
tmpRgb = hsvToRgb(newColourHsv.h*360, newColourHsv.s*100, newColourHsv.v*100);
newColourHexes[0] = rgbToHex(Math.round(tmpRgb[0]), Math.round(tmpRgb[1]), Math.round(tmpRgb[2]));
// Second colour
newColourHsv.h = (((currentColourHsv.h*360 + 150) % 360) / 360);
currPickerIconPos[2][0] = miniPickerCanvas.width * newColourHsv.h - 8;
currPickerIconPos[2][1] = miniPickerCanvas.height - (miniPickerCanvas.height * newColourHsv.s + 8);
tmpRgb = hsvToRgb(newColourHsv.h*360, newColourHsv.s*100, newColourHsv.v*100);
newColourHexes[1] = rgbToHex(Math.round(tmpRgb[0]), Math.round(tmpRgb[1]), Math.round(tmpRgb[2]));
break;
case 'tetra':
for (let i=1; i< 4; i++) {
newColourHsv.h = (((currentColourHsv.h*360 + 90*i) % 360) / 360);
currPickerIconPos[i][0] = miniPickerCanvas.width * newColourHsv.h - 8;
currPickerIconPos[i][1] = miniPickerCanvas.height - (miniPickerCanvas.height * newColourHsv.s + 8);
tmpRgb = hsvToRgb(newColourHsv.h*360, newColourHsv.s*100, newColourHsv.v*100);
newColourHexes[i - 1] = rgbToHex(Math.round(tmpRgb[0]), Math.round(tmpRgb[1]), Math.round(tmpRgb[2]));
}
break;
default:
console.log("How did you select the " + currentPickingMode + ", hackerman?");
break;
}
hexContainers[0].style.color = getHexPreviewColour(colourValue.value);
for (let i=1; i<pickerIcons.length; i++) {
pickerIcons[i].style.left = '' + currPickerIconPos[i][0] + 'px';
pickerIcons[i].style.top = '' + currPickerIconPos[i][1] + 'px';
pickerIcons[i].style.backgroundColor = '#' + newColourHexes[i - 1];
}
if (currentPickingMode != "analog") {
hexContainers[0].style.backgroundColor = colourValue.value;
hexContainers[0].innerHTML = colourValue.value;
for (let i=0; i<pickerIcons.length - 1; i++) {
hexContainers[i + 1].style.backgroundColor = '#' + newColourHexes[i];
hexContainers[i + 1].innerHTML = '#' + newColourHexes[i];
hexContainers[i + 1].style.color = getHexPreviewColour(newColourHexes[i]);
}
}
// If I'm using analogous mode, I place the current colour in the middle
else {
hexContainers[1].style.backgroundColor = colourValue.value;
hexContainers[1].innerHTML = colourValue.value;
hexContainers[2].style.backgroundColor = '#' + newColourHexes[0];
hexContainers[2].innerHTML = '#' + newColourHexes[0];
hexContainers[0].style.backgroundColor = '#' + newColourHexes[1];
hexContainers[0].innerHTML = '#' + newColourHexes[1];
for (let i=1; i<3; i++) {
hexContainers[i].style.color = getHexPreviewColour(newColourHexes[i - 1]);
}
}
}
function getSelectedColours() {
let ret = [];
for (let i=0; i<hexContainers.length; i++) {
if (hexContainers[i] != null) {
ret.push(hexContainers[i].innerHTML);
}
}
return ret;
}
function getHexPreviewColour(hex) {
//if brightness is over threshold, make the text dark
if (colorBrightness(hex) > 110) {
return '#332f35'
}
else {
return '#c2bbc7';
}
//take in a color and return its brightness
function colorBrightness (color) {
var r = parseInt(color.slice(1, 3), 16);
var g = parseInt(color.slice(3, 5), 16);
var b = parseInt(color.slice(5, 7), 16);
return Math.round(((parseInt(r) * 299) + (parseInt(g) * 587) + (parseInt(b) * 114)) / 1000);
}
}

View File

@ -1,11 +1,17 @@
// Data saved when copying or cutting
let clipboardData; let clipboardData;
// Tells if the user is pasting something or not
let isPasting = false; let isPasting = false;
// Coordinates of the copied (or cut) selection
let copiedStartX; let copiedStartX;
let copiedStartY; let copiedStartY;
let copiedEndX; let copiedEndX;
let copiedEndY; let copiedEndY;
/** Copies the current selection to the clipboard
*
*/
function copySelection() { function copySelection() {
copiedEndX = endX; copiedEndX = endX;
copiedEndY = endY; copiedEndY = endY;
@ -16,13 +22,19 @@ function copySelection() {
clipboardData = currentLayer.context.getImageData(startX, startY, endX - startX + 1, endY - startY + 1); clipboardData = currentLayer.context.getImageData(startX, startY, endX - startX + 1, endY - startY + 1);
} }
/** Pastes the clipboard data onto the current layer
*
*/
function pasteSelection() { function pasteSelection() {
// Can't paste if the layer is locked // Can't paste if the layer is locked
if (currentLayer.isLocked) { if (currentLayer.isLocked) {
return; return;
} }
// Cancel the current selection
endSelection(); endSelection();
// I'm pasting
isPasting = true; isPasting = true;
// Putting the image data on the tmp layer // Putting the image data on the tmp layer
TMPLayer.context.putImageData(clipboardData, copiedStartX, copiedStartY); TMPLayer.context.putImageData(clipboardData, copiedStartX, copiedStartY);
@ -43,9 +55,11 @@ function pasteSelection() {
//drawRect(copiedStartX, copiedEndX, copiedStartY, copiedEndY); //drawRect(copiedStartX, copiedEndX, copiedStartY, copiedEndY);
} }
/** Cuts the current selection and copies it to the clipboard
*
*/
function cutSelectionTool() { function cutSelectionTool() {
console.log("Taglio"); // Saving the coordinates
copiedEndX = endX; copiedEndX = endX;
copiedEndY = endY; copiedEndY = endY;
@ -53,13 +67,19 @@ function cutSelectionTool() {
copiedStartY = startY; copiedStartY = startY;
// Getting the selected pixels // Getting the selected pixels
// If I'm already moving a selection
if (imageDataToMove !== undefined) { if (imageDataToMove !== undefined) {
// I just save that selection in the clipboard
clipboardData = imageDataToMove; clipboardData = imageDataToMove;
// And clear the underlying space
TMPLayer.context.clearRect(0, 0, TMPLayer.canvas.width, TMPLayer.canvas.height); TMPLayer.context.clearRect(0, 0, TMPLayer.canvas.width, TMPLayer.canvas.height);
// The image has been cleared, so I don't have anything to move anymore
imageDataToMove = undefined; imageDataToMove = undefined;
} }
else { else {
clipboardData = currentLayer.context.getImageData(startX, startY, endX - startX + 1, endY - startY + 1); // Otherwise, I copy the current selection into the clipboard
copySelection();
// And clear the selection
currentLayer.context.clearRect(startX - 0.5, startY - 0.5, endX - startX + 1, endY - startY + 1); currentLayer.context.clearRect(startX - 0.5, startY - 0.5, endX - startX + 1, endY - startY + 1);
} }
} }

View File

@ -1,7 +1,51 @@
function create(isSplash) {
var splashPostfix = '';
// If I'm creating from the splash menu, I append '-splash' so I get the corresponding values
if (isSplash) {
splashPostfix = '-splash';
}
var width = getValue('size-width' + splashPostfix);
var height = getValue('size-height' + splashPostfix);
// If I'm creating from the splash screen, I use the splashMode variable
var mode = isSplash ? splashMode : getValue('editor-mode');
newPixel(width, height, mode);
// If I'm not creating from the splash page, then this is not the first project I've created
if (!isSplash)
document.getElementById('new-pixel-warning').style.display = 'block';
//get selected palette name
var selectedPalette = getText('palette-button' + splashPostfix);
if (selectedPalette == 'Choose a palette...')
selectedPalette = 'none';
//track google event
ga('send', 'event', 'Pixel Editor New', selectedPalette, width+'/'+height); /*global ga*/
//reset new form
setValue('size-width', 64);
setValue('size-height', 64);
setValue("editor-mode", 'Advanced')
setText('editor-mode-button', 'Choose a mode...');
setText('palette-button', 'Choose a palette...');
setText('preset-button', 'Choose a preset...');
}
/** Triggered when the "Create" button in the new pixel dialogue is pressed
*
*/
on('click', 'create-button', function (){ on('click', 'create-button', function (){
// Getting the values of the form
var width = getValue('size-width'); var width = getValue('size-width');
var height = getValue('size-height'); var height = getValue('size-height');
var mode = getValue("editor-mode"); var mode = getValue("editor-mode");
// Creating a new pixel with those properties
newPixel(width, height, mode); newPixel(width, height, mode);
document.getElementById('new-pixel-warning').style.display = 'block'; document.getElementById('new-pixel-warning').style.display = 'block';
@ -23,3 +67,37 @@ on('click', 'create-button', function (){
setText('palette-button', 'Choose a palette...'); setText('palette-button', 'Choose a palette...');
setText('preset-button', 'Choose a preset...'); setText('preset-button', 'Choose a preset...');
}); });
/** Triggered when the "Create" button in the new pixel dialogue is pressed
*
*/
on('click', 'create-button-splash', function (){
// Getting the values of the form
var width = getValue('size-width-splash');
var height = getValue('size-height-splash');
var mode = pixelEditorMode;
if (mode == 'Advanced')
mode = "Basic";
else
mode = "Advanced";
// Creating a new pixel with those properties
newPixel(width, height, mode);
//track google event
ga('send', 'event', 'Pixel Editor New', selectedPalette, width+'/'+height); /*global ga*/
document.getElementById('new-pixel-warning').style.display = 'block';
// Resetting the new pixel values
selectedPalette = 'none';
//reset new pixel form
setValue('size-width-splash', 64);
setValue('size-height-splash', 64);
setValue("editor-mode", 'Advanced')
setText('editor-mode-button', 'Choose a mode...');
setText('palette-button', 'Choose a palette...');
setText('preset-button', 'Choose a preset...');
});

View File

@ -1,5 +1,11 @@
function createColorPalette(paletteColors, fillBackground, deletePreviousPalette = true) { /** Creates the colour palette
*
* @param {*} paletteColors The colours of the palette
* @param {*} deletePreviousPalette Tells if the app should delete the previous palette or not
* (used when opening a file, for example)
*/
function createColorPalette(paletteColors, deletePreviousPalette = true) {
//remove current palette //remove current palette
if (deletePreviousPalette) { if (deletePreviousPalette) {
colors = document.getElementsByClassName('color-button'); colors = document.getElementsByClassName('color-button');
@ -11,6 +17,7 @@ function createColorPalette(paletteColors, fillBackground, deletePreviousPalette
var lightestColor = '#000000'; var lightestColor = '#000000';
var darkestColor = '#ffffff'; var darkestColor = '#ffffff';
// Adding all the colours in the array
for (var i = 0; i < paletteColors.length; i++) { for (var i = 0; i < paletteColors.length; i++) {
var newColor = paletteColors[i]; var newColor = paletteColors[i];
var newColorElement = addColor(newColor); var newColorElement = addColor(newColor);
@ -42,6 +49,9 @@ function createColorPalette(paletteColors, fillBackground, deletePreviousPalette
currentLayer.context.fillStyle = darkestColor; currentLayer.context.fillStyle = darkestColor;
} }
/** Creates the palette with the colours used in all the layers
*
*/
function createPaletteFromLayers() { function createPaletteFromLayers() {
let colors = {}; let colors = {};
@ -68,8 +78,6 @@ function createPaletteFromLayers() {
} }
} }
console.log(colors);
//create array out of colors object //create array out of colors object
let colorPaletteArray = []; let colorPaletteArray = [];
for (let color in colors) { for (let color in colors) {
@ -77,8 +85,7 @@ function createPaletteFromLayers() {
colorPaletteArray.push('#'+rgbToHex(colors[color])); colorPaletteArray.push('#'+rgbToHex(colors[color]));
} }
} }
console.log('COLOR PALETTE ARRAY', colorPaletteArray);
//create palette form colors array //create palette from colors array
createColorPalette(colorPaletteArray, false); createColorPalette(colorPaletteArray, true);
} }

View File

@ -9,19 +9,19 @@ function deleteColor (color) {
//if color is a string, then find the corresponding button //if color is a string, then find the corresponding button
if (typeof color === 'string') { if (typeof color === 'string') {
console.log('trying to find ',color); //console.log('trying to find ',color);
//get all colors in palette //get all colors in palette
colors = document.getElementsByClassName('color-button'); colors = document.getElementsByClassName('color-button');
//loop through colors //loop through colors
for (var i = 0; i < colors.length; i++) { for (var i = 0; i < colors.length; i++) {
console.log(color,'=',colors[i].jscolor.toString()); //console.log(color,'=',colors[i].jscolor.toString());
if (color == colors[i].jscolor.toString()) { if (color == colors[i].jscolor.toString()) {
console.log('match'); //console.log('match');
//set color to the color button //set color to the color button
color = colors[i]; color = colors[i];
console.log('found color', color); //console.log('found color', color);
//exit loop //exit loop
break; break;
@ -30,7 +30,7 @@ function deleteColor (color) {
//if the color wasn't found //if the color wasn't found
if (typeof color === 'string') { if (typeof color === 'string') {
console.log('color not found'); //console.log('color not found');
//exit function //exit function
return; return;
} }

View File

@ -1,27 +1,55 @@
let currentOpenDialogue = "";
/** Shows the dialogue window called dialogueName, which is a child of pop-up-container in pixel-editor.hbs
*
* @param {*} dialogueName The name of the window to show
* @param {*} trackEvent Should I track the GA event?
*/
function showDialogue (dialogueName, trackEvent) { function showDialogue (dialogueName, trackEvent) {
if (typeof trackEvent === 'undefined') trackEvent = true; if (typeof trackEvent === 'undefined') trackEvent = true;
// Updating currently open dialogue
currentOpenDialogue = dialogueName;
// The pop up window is open
dialogueOpen = true; dialogueOpen = true;
// Showing the pop up container
popUpContainer.style.display = 'block'; popUpContainer.style.display = 'block';
// Showing the window
document.getElementById(dialogueName).style.display = 'block'; document.getElementById(dialogueName).style.display = 'block';
// If I'm opening the palette window, I initialize the colour picker
if (dialogueName == 'palette-block' && documentCreated) {
cpInit();
pbInit();
}
//track google event //track google event
if (trackEvent) if (trackEvent)
ga('send', 'event', 'Palette Editor Dialogue', dialogueName); /*global ga*/ ga('send', 'event', 'Palette Editor Dialogue', dialogueName); /*global ga*/
} }
/** Closes the current dialogue by hiding the window and the pop-up-container
*
*/
function closeDialogue () { function closeDialogue () {
popUpContainer.style.display = 'none'; popUpContainer.style.display = 'none';
var popups = popUpContainer.children; var popups = popUpContainer.children;
for (var i = 0; i < popups.length; i++) { for (var i = 0; i < popups.length; i++) {
popups[i].style.display = 'none'; popups[i].style.display = 'none';
} }
dialogueOpen = false; dialogueOpen = false;
if (currentOpenDialogue == "palette-block") {
pbAddToSimplePalette();
}
} }
/** Closes a dialogue window if the user clicks everywhere but in the current window
*
*/
popUpContainer.addEventListener('click', function (e) { popUpContainer.addEventListener('click', function (e) {
if (e.target == popUpContainer) if (e.target == popUpContainer)
closeDialogue(); closeDialogue();
@ -31,6 +59,6 @@ popUpContainer.addEventListener('click', function (e) {
var cancelButtons = popUpContainer.getElementsByClassName('close-button'); var cancelButtons = popUpContainer.getElementsByClassName('close-button');
for (var i = 0; i < cancelButtons.length; i++) { for (var i = 0; i < cancelButtons.length; i++) {
cancelButtons[i].addEventListener('click', function () { cancelButtons[i].addEventListener('click', function () {
closeDialogue(); closeDialogue();
}); });
} }

View File

@ -1,4 +1,4 @@
//draw a line between two points on canvas //draws 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);

View File

@ -3,27 +3,48 @@ let modes = {
description: 'Basic mode is perfect if you want to create simple sprites or try out palettes.' description: 'Basic mode is perfect if you want to create simple sprites or try out palettes.'
}, },
'Advanced' : { 'Advanced' : {
description: 'Choose advanced mode to gain access to features such as layers.' description: 'Choose advanced mode to gain access to more advanced features such as layers.'
} }
} }
let infoBox = document.getElementById('editor-mode-info'); let infoBox = document.getElementById('editor-mode-info');
let currentSplashButton = document.getElementById("sp-mode-palette").children[0].children[1];
function switchMode(currentMode, mustConfirm = true) { function splashMode(mouseEvent, mode) {
console.log("UE");
if (currentSplashButton == undefined) {
currentSplashButton = mouseEvent.target.parentElement;
}
if (mode !== pixelEditorMode) {
console.log("Mode target: " + mouseEvent.target);
// Remove selected class to old button
currentSplashButton.classList.remove("sp-interface-selected");
// Add selected class to new button
mouseEvent.target.parentElement.classList.add("sp-interface-selected");
// Setting the new mode
pixelEditorMode = mode;
}
// Setting the new selected button
currentSplashButton = mouseEvent.target.parentElement;
}
function switchMode(mustConfirm = true) {
//switch to advanced mode //switch to advanced mode
if (currentMode == 'Basic') { if (pixelEditorMode == 'Basic') {
// Switch to advanced ez pez lemon squez // Switch to advanced ez pez lemon squez
document.getElementById('switch-mode-button').innerHTML = 'Switch to basic mode'; document.getElementById('switch-mode-button').innerHTML = 'Switch to basic mode';
// Show the layer menus // Show the layer menus
layerList.style.display = "inline-block"; layerList.style.display = "inline-block";
document.getElementById('layer-button').style.display = 'inline-block'; document.getElementById('layer-button').style.display = 'inline-block';
// Move the palette menu // Hide the palette menu
document.getElementById('colors-menu').style.right = '200px'; document.getElementById('colors-menu').style.right = '200px'
pixelEditorMode = 'Advanced'; pixelEditorMode = 'Advanced';
} }
//switch to basic mode //switch to basic mode
else { else {
//if there is a current layer (a document is active) //if there is a current layer (a document is active)
@ -47,15 +68,17 @@ function switchMode(currentMode, mustConfirm = true) {
// Hide the layer menus // Hide the layer menus
layerList.style.display = 'none'; layerList.style.display = 'none';
document.getElementById('layer-button').style.display = 'none'; document.getElementById('layer-button').style.display = 'none';
// Move the palette menu // Show the palette menu
document.getElementById('colors-menu').style.right = '0px'; document.getElementById('colors-menu').style.display = 'flex';
// Move the palette menu
document.getElementById('colors-menu').style.right = '0px';
pixelEditorMode = 'Basic'; pixelEditorMode = 'Basic';
} }
} }
on('click', 'switch-mode-button', function (e) { on('click', 'switch-mode-button', function (e) {
switchMode(pixelEditorMode); switchMode();
}); });
// Makes the menu open // Makes the menu open

1
js/_featuresLog.js Normal file
View File

@ -0,0 +1 @@
showDialogue("splash", false);

View File

@ -18,8 +18,6 @@ for (var i = 1; i < mainMenuItems.length; i++) {
var subMenu = menuItem.children[1]; var subMenu = menuItem.children[1];
var subMenuItems = subMenu.children; var subMenuItems = subMenu.children;
//when you click an item within a menu button //when you click an item within a menu button
for (var j = 0; j < subMenuItems.length; j++) { for (var j = 0; j < subMenuItems.length; j++) {

View File

@ -7,11 +7,6 @@ function fill(cursorLocation) {
tempImage.data[pixelPos + 1] = fillColor.g; tempImage.data[pixelPos + 1] = fillColor.g;
tempImage.data[pixelPos + 2] = fillColor.b; tempImage.data[pixelPos + 2] = fillColor.b;
tempImage.data[pixelPos + 3] = 255; tempImage.data[pixelPos + 3] = 255;
/*
tempImage.data[pixelPos] = fillColor.r;
tempImage.data[pixelPos + 1] = fillColor.g;
tempImage.data[pixelPos + 2] = fillColor.b;
*/
} }
//change x y to color value passed from the function and use that as the original color //change x y to color value passed from the function and use that as the original color

View File

@ -1,4 +1,4 @@
//get cursor position relative to canvas //gets cursor position relative to canvas
function getCursorPosition(e) { function getCursorPosition(e) {
var x; var x;
var y; var y;

View File

@ -1,3 +1,17 @@
/** How the history works
* - undoStates stores the states that can be undone
* - redoStates stores the states that can be redone
* - undo() undoes an action and adds it to the redoStates
* - redo() redoes an action and adds it to the undoStates
* - Each HistoryState must implement an undo() and redo() function
* Those functions actually implement the undo and redo mechanism for that action,
* so you'll need to save the data you need as attributes in the constructor. For example,
* for the HistoryStateAddColour, the added colour is saved so that it can be removed in
* undo() or added back in redo().
* - Each HistoryState must call saveHistoryState(this) so that it gets added to the stack
*
*/
var undoStates = []; var undoStates = [];
var redoStates = []; var redoStates = [];
@ -473,8 +487,6 @@ function saveHistoryState (state) {
} }
function undo () { function undo () {
//console.log('%cundo', undoLogStyle);
//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');
@ -496,8 +508,6 @@ function undo () {
} }
function redo () { function redo () {
//console.log('%credo', undoLogStyle);
if (redoStates.length > 0) { if (redoStates.length > 0) {
//enable undo button //enable undo button

View File

@ -1,5 +1,9 @@
var spacePressed = false; var spacePressed = false;
/** Just listens to hotkeys and calls the linked functions
*
* @param {*} e
*/
function KeyPress(e) { function KeyPress(e) {
var keyboardEvent = window.event? event : e; var keyboardEvent = window.event? event : e;
@ -46,6 +50,9 @@ function KeyPress(e) {
case 52: case 80: case 52: case 80:
tool.pan.switchTo(); tool.pan.switchTo();
break; break;
case 76:
tool.line.switchTo();
break;
//zoom - 5 //zoom - 5
case 53: case 53:
tool.zoom.switchTo(); tool.zoom.switchTo();

View File

@ -1,5 +1,7 @@
// NEXTPULL: to remove when the new palette system is added
//format a color button
//formats a color button
function initColor (colorElement) { function initColor (colorElement) {
//console.log('initColor()'); //console.log('initColor()');
//console.log(document.getElementById('jscolor-hex-input')) //console.log(document.getElementById('jscolor-hex-input'))

View File

@ -1,3 +1,5 @@
// NEXTPULL: to remove when the new palette system is added
/** /**
* jscolor - JavaScript Color Picker * jscolor - JavaScript Color Picker
* *

View File

@ -1,31 +1,29 @@
/** TODO LIST FOR LAYERS // HTML element that contains the layer entries
GENERAL REQUIREMENTS:
- Saving the state of an artwork to a .lospec file so that people can work on it later keeping
the layers they created? That'd be cool, even for the app users, that could just double click on a lospec
file for it to be opened right in the pixel editor
OPTIONAL:
1 - Fix issues
*/
let layerList; let layerList;
// A single layer entry (used as a prototype to create the new ones)
let layerListEntry; let layerListEntry;
// NEXTPULL: remove the drag n drop system and use Sortable.js instead
let layerDragSource = null; let layerDragSource = null;
// Number of layers at the beginning
let layerCount = 1; let layerCount = 1;
// Current max z index (so that I know which z-index to assign to new layers)
let maxZIndex = 3; let maxZIndex = 3;
// When a layer is deleted, its id is added to this array and can be reused
let unusedIDs = []; let unusedIDs = [];
// Id for the next added layer
let currentID = layerCount; let currentID = layerCount;
let idToDelete; // Layer menu
let layerOptions = document.getElementById("layer-properties-menu"); let layerOptions = document.getElementById("layer-properties-menu");
// Is the user currently renaming a layer?
let isRenamingLayer = false; let isRenamingLayer = false;
// I need to save this, trust me
let oldLayerName = null; let oldLayerName = null;
let dragStartLayer;
// Binding the add layer button to the function
on('click',"add-layer-button", addLayer, false); 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)
@ -54,6 +52,7 @@ class Layer {
this.id = "layer" + id; this.id = "layer" + id;
// Binding the events
if (menuEntry != null) { if (menuEntry != null) {
this.name = menuEntry.getElementsByTagName("p")[0].innerHTML; this.name = menuEntry.getElementsByTagName("p")[0].innerHTML;
menuEntry.id = "layer" + id; menuEntry.id = "layer" + id;
@ -78,20 +77,13 @@ class Layer {
// Initializes the canvas // Initializes the canvas
initialize() { initialize() {
/*
var maxHorizontalZoom = Math.floor(window.innerWidth/this.canvasSize[0]*0.75);
var maxVerticalZoom = Math.floor(window.innerHeight/this.canvasSize[1]*0.75);
zoom = Math.min(maxHorizontalZoom,maxVerticalZoom);
if (zoom < 1) zoom = 1;
*/
//resize canvas //resize canvas
this.canvas.width = this.canvasSize[0]; this.canvas.width = this.canvasSize[0];
this.canvas.height = this.canvasSize[1]; this.canvas.height = this.canvasSize[1];
this.canvas.style.width = (this.canvas.width*zoom)+'px'; this.canvas.style.width = (this.canvas.width*zoom)+'px';
this.canvas.style.height = (this.canvas.height*zoom)+'px'; this.canvas.style.height = (this.canvas.height*zoom)+'px';
//unhide canvas //show canvas
this.canvas.style.display = 'block'; this.canvas.style.display = 'block';
//center canvas in window //center canvas in window
@ -103,7 +95,7 @@ class Layer {
} }
hover() { hover() {
// Hide all the layers but the current one // Hides all the layers but the current one
for (let i=1; i<layers.length - nAppLayers; i++) { for (let i=1; i<layers.length - nAppLayers; i++) {
if (layers[i] !== this) { if (layers[i] !== this) {
layers[i].canvas.style.opacity = 0.3; layers[i].canvas.style.opacity = 0.3;
@ -112,7 +104,7 @@ class Layer {
} }
unhover() { unhover() {
// Show all the layers again // Shows all the layers again
for (let i=1; i<layers.length - nAppLayers; i++) { for (let i=1; i<layers.length - nAppLayers; i++) {
if (layers[i] !== this) { if (layers[i] !== this) {
layers[i].canvas.style.opacity = 1; layers[i].canvas.style.opacity = 1;
@ -127,52 +119,6 @@ class Layer {
} }
} }
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
resize() { resize() {
let newWidth = (this.canvas.width * zoom) + 'px'; let newWidth = (this.canvas.width * zoom) + 'px';
@ -217,20 +163,20 @@ class Layer {
openOptionsMenu(event) { openOptionsMenu(event) {
if (event.which == 3) { if (event.which == 3) {
let selectedId;
let target = event.target; let target = event.target;
let offsets = getElementAbsolutePosition(this);
while (target != null && target.classList != null && !target.classList.contains("layers-menu-entry")) { while (target != null && target.classList != null && !target.classList.contains("layers-menu-entry")) {
target = target.parentElement; target = target.parentElement;
} }
idToDelete = target.id; selectedId = target.id;
layerOptions.style.visibility = "visible"; layerOptions.style.visibility = "visible";
layerOptions.style.top = "0"; layerOptions.style.top = "0";
layerOptions.style.marginTop = "" + (event.clientY - 25) + "px"; layerOptions.style.marginTop = "" + (event.clientY - 25) + "px";
getLayerByID(idToDelete).selectLayer(); getLayerByID(selectedId).selectLayer();
} }
} }
@ -265,10 +211,6 @@ class Layer {
layer.menuEntry.classList.add("selected-layer"); layer.menuEntry.classList.add("selected-layer");
currentLayer = layer; currentLayer = layer;
} }
/*
canvas = currentLayer.canvas;
context = currentLayer.context;
*/
} }
toggleLock() { toggleLock() {
@ -442,7 +384,6 @@ function merge(saveHistory = true) {
// Updating the layer preview // Updating the layer preview
currentLayer.updateLayerPreview(); currentLayer.updateLayerPreview();
} }
} }
function deleteLayer(saveHistory = true) { function deleteLayer(saveHistory = true) {
@ -456,7 +397,7 @@ function deleteLayer(saveHistory = true) {
unusedIDs.push(toDelete.id); unusedIDs.push(toDelete.id);
// Selecting the next layer // Selecting the next layer
if (layerIndex != (layers.length - 3)) { if (layerIndex != (layers.length - 4)) {
layers[layerIndex + 1].selectLayer(); layers[layerIndex + 1].selectLayer();
} }
// or the previous one if the next one doesn't exist // or the previous one if the next one doesn't exist
@ -489,13 +430,13 @@ function duplicateLayer(event, saveHistory = true) {
for (let i=getMenuEntryIndex(menuEntries, toDuplicate.menuEntry) - 1; i>=0; i--) { for (let i=getMenuEntryIndex(menuEntries, toDuplicate.menuEntry) - 1; i>=0; i--) {
getLayerByID(menuEntries[i].id).canvas.style.zIndex++; getLayerByID(menuEntries[i].id).canvas.style.zIndex++;
} }
maxZIndex++; maxZIndex+=2;
// Creating a new canvas // Creating a new canvas
let newCanvas = document.createElement("canvas"); let newCanvas = document.createElement("canvas");
// Setting up the new canvas // Setting up the new canvas
canvasView.append(newCanvas); canvasView.append(newCanvas);
newCanvas.style.zIndex = parseInt(currentLayer.canvas.style.zIndex) + 1; newCanvas.style.zIndex = parseInt(currentLayer.canvas.style.zIndex) + 2;
newCanvas.classList.add("drawingCanvas"); newCanvas.classList.add("drawingCanvas");
if (!layerListEntry) return console.warn('skipping adding layer because no document'); if (!layerListEntry) return console.warn('skipping adding layer because no document');
@ -545,78 +486,6 @@ function renameLayer(event) {
isRenamingLayer = true; isRenamingLayer = true;
} }
// Swaps two layer entries in the layer menu
function moveLayers(toDropLayer, staticLayer, saveHistory = true) {
let toDrop = getLayerByID(toDropLayer);
let staticc = 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(staticc);
layerList.insertBefore(toDrop.menuEntry, staticc.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, staticc, 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, staticc, nMoved);
}
}
}
function getMenuEntryIndex(list, entry) { function getMenuEntryIndex(list, entry) {
for (let i=0; i<list.length; i++) { for (let i=0; i<list.length; i++) {
if (list[i] === entry) { if (list[i] === entry) {
@ -660,7 +529,7 @@ function addLayer(id, saveHistory = true) {
let newCanvas = document.createElement("canvas"); let newCanvas = document.createElement("canvas");
// Setting up the new canvas // Setting up the new canvas
canvasView.append(newCanvas); canvasView.append(newCanvas);
maxZIndex++; maxZIndex+=2;
newCanvas.style.zIndex = maxZIndex; newCanvas.style.zIndex = maxZIndex;
newCanvas.classList.add("drawingCanvas"); newCanvas.classList.add("drawingCanvas");
@ -696,4 +565,49 @@ function addLayer(id, saveHistory = true) {
return newLayer; return newLayer;
} }
layerList = document.getElementById("layers-menu"); /** Saves the layer that is being moved when the dragging starts
*
* @param {*} event
*/
function layerDragStart(event) {
dragStartLayer = getLayerByID(layerList.children[event.oldIndex].id);
}
/** Sets the z indexes of the layers when the user drops the layer in the menu
*
* @param {*} event
*/
function layerDragDrop(event) {
let oldIndex = event.oldDraggableIndex;
let newIndex = event.newDraggableIndex;
let movedZIndex = dragStartLayer.canvas.style.zIndex;
if (oldIndex > newIndex)
{
for (let i=newIndex; i<oldIndex; i++) {
getLayerByID(layerList.children[i].id).canvas.style.zIndex = getLayerByID(layerList.children[i + 1].id).canvas.style.zIndex;
}
}
else
{
for (let i=newIndex; i>oldIndex; i--) {
getLayerByID(layerList.children[i].id).canvas.style.zIndex = getLayerByID(layerList.children[i - 1].id).canvas.style.zIndex;
}
}
getLayerByID(layerList.children[oldIndex].id).canvas.style.zIndex = movedZIndex;
dragging = false;
}
layerList = document.getElementById("layers-menu");
// Making the layers list sortable
new Sortable(document.getElementById("layers-menu"), {
animation: 100,
filter: ".layer-button",
draggable: ".layers-menu-entry",
onStart: layerDragStart,
onEnd: layerDragDrop
});

45
js/_line.js Normal file
View File

@ -0,0 +1,45 @@
function diagLine(lastMouseClickPos, zoom, cursorLocation) {
let x0 = Math.floor(lastMouseClickPos[0]/zoom);
let y0 = Math.floor(lastMouseClickPos[1]/zoom);
let x1 = Math.floor(cursorLocation[0]/zoom);
let y1 = Math.floor(cursorLocation[1]/zoom);
let dx = Math.abs(x1-x0);
let dy = Math.abs(y1-y0);
let sx = (x0 < x1 ? 1 : -1);
let sy = (y0 < y1 ? 1 : -1);
let err = dx-dy;
const brushSize = tool.line.brushSize;
const canvas = document.getElementById('tmp-canvas');
const context = canvas.getContext('2d');
context.fillStyle=currentGlobalColor;
context.clearRect(0, 0, canvas.width, canvas.height);
canvas.style.zIndex = parseInt(currentLayer.canvas.style.zIndex, 10) + 1;
//console.log(canvas.style.zIndex, currentLayer.canvas.style.zIndex);
while (true) {
if (currentTool.name !== 'line') return;
context.fillRect(x0-Math.floor(brushSize/2), y0-Math.floor(brushSize/2), brushSize, brushSize);
//if we've reached the end goal, exit the loop
if ((x0==x1) && (y0==y1)) break;
var e2 = 2*err;
if (e2 >-dy) {
err -=dy;
x0+=sx;
}
if (e2 < dx) {
err +=dx;
y0+=sy;
}
}
}

View File

@ -1,18 +1,27 @@
/** Loads a file (.png or .lpe)
*
*/
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 fileName = document.getElementById("open-image-browse-holder").value;
// Getting the extension
let extension = fileName.substring(fileName.lastIndexOf('.')+1, fileName.length) || fileName; let extension = fileName.substring(fileName.lastIndexOf('.')+1, fileName.length) || fileName;
// I didn't write this check and I have no idea what it does
if (this.files && this.files[0]) { if (this.files && this.files[0]) {
// Btw, checking if the extension is supported
if (extension == 'png' || extension == 'gif' || extension == 'lpe') { if (extension == 'png' || extension == 'gif' || extension == 'lpe') {
// If it's a Lospec Pixel Editor tm file, I load the project
if (extension == 'lpe') { if (extension == 'lpe') {
let file = this.files[0]; let file = this.files[0];
let reader = new FileReader(); let reader = new FileReader();
// Getting all the data
reader.readAsText(file, "UTF-8"); reader.readAsText(file, "UTF-8");
// Converting the data to a json object and creating a new pixel (see _newPixel.js for more)
reader.onload = function (e) { reader.onload = function (e) {
let dictionary = JSON.parse(e.target.result); let dictionary = JSON.parse(e.target.result);
let mode = dictionary['editorMode'] == 'Advanced' ? 'Basic' : 'Advanced';
newPixel(dictionary['canvasWidth'], dictionary['canvasHeight'], dictionary['editorMode'], dictionary); newPixel(dictionary['canvasWidth'], dictionary['canvasHeight'], mode, dictionary);
} }
} }
else { else {

View File

@ -1,8 +1,8 @@
//this is called when a user picks a file after selecting "load palette" from the new pixel dialogue //this is called when a user picks a file after selecting "load palette" from the new pixel dialogue
// TODO: load palette from .lpe file
document.getElementById('load-palette-browse-holder').addEventListener('change', function () { document.getElementById('load-palette-browse-holder').addEventListener('change', function () {
if (this.files && this.files[0]) { if (this.files && this.files[0]) {
//make sure file is allowed filetype //make sure file is allowed filetype
var fileContentType = this.files[0].type; var fileContentType = this.files[0].type;
if (fileContentType == 'image/png' || fileContentType == 'image/gif') { if (fileContentType == 'image/png' || fileContentType == 'image/gif') {
@ -26,8 +26,6 @@ document.getElementById('load-palette-browse-holder').addEventListener('change',
var colorPalette = []; var colorPalette = [];
var imagePixelData = loadPaletteContext.getImageData(0,0,this.width, this.height).data; var imagePixelData = loadPaletteContext.getImageData(0,0,this.width, this.height).data;
console.log(imagePixelData);
//loop through pixels looking for colors to add to palette //loop through pixels looking for colors to add to palette
for (var i = 0; i < imagePixelData.length; i += 4) { for (var i = 0; i < imagePixelData.length; i += 4) {
var color = '#'+rgbToHex(imagePixelData[i],imagePixelData[i + 1],imagePixelData[i + 2]); var color = '#'+rgbToHex(imagePixelData[i],imagePixelData[i + 1],imagePixelData[i + 2]);
@ -40,6 +38,8 @@ document.getElementById('load-palette-browse-holder').addEventListener('change',
palettes['Loaded palette'] = {}; palettes['Loaded palette'] = {};
palettes['Loaded palette'].colors = colorPalette; palettes['Loaded palette'].colors = colorPalette;
setText('palette-button', 'Loaded palette'); setText('palette-button', 'Loaded palette');
setText('palette-button-splash', 'Loaded palette');
toggle('palette-menu-splash');
}; };
img.src = e.target.result; img.src = e.target.result;
}; };

View File

@ -22,7 +22,7 @@ window.addEventListener("mousedown", function (mouseEvent) {
else if (mouseEvent.altKey) else if (mouseEvent.altKey)
currentTool = tool.eyedropper; currentTool = tool.eyedropper;
else if (mouseEvent.target.className == 'drawingCanvas' && else if (mouseEvent.target.className == 'drawingCanvas' &&
(currentTool.name == 'pencil' || currentTool.name == 'eraser' || currentTool.name == 'rectangle')) (currentTool.name == 'pencil' || currentTool.name == 'eraser' || currentTool.name == 'rectangle' || currentTool.name === 'line'))
new HistoryStateEditCanvas(); new HistoryStateEditCanvas();
else if (currentTool.name == 'moveselection') { else if (currentTool.name == 'moveselection') {
if (!cursorInSelectedArea() && if (!cursorInSelectedArea() &&
@ -50,6 +50,10 @@ window.addEventListener("mousedown", function (mouseEvent) {
currentTool = tool.resizerectangle; currentTool = tool.resizerectangle;
tool.rectangle.previousBrushSize = tool.rectangle.brushSize; tool.rectangle.previousBrushSize = tool.rectangle.brushSize;
} }
else if (currentTool.name == 'line' && mouseEvent.which == 3) {
currentTool = tool.resizeline;
tool.line.previousBrushSize = tool.line.brushSize;
}
if (currentTool.name == 'eyedropper' && mouseEvent.target.className == 'drawingCanvas') if (currentTool.name == 'eyedropper' && mouseEvent.target.className == 'drawingCanvas')
eyedropperPreview.style.display = 'block'; eyedropperPreview.style.display = 'block';
@ -70,6 +74,15 @@ window.addEventListener("mouseup", function (mouseEvent) {
currentLayer.closeOptionsMenu(); currentLayer.closeOptionsMenu();
} }
// If the user finished placing down a line, clear the tmp canvas and copy the data to the current layer
if (currentTool.name === "line") {
const tmpCanvas = document.getElementById('tmp-canvas');
currentLayer.context.drawImage(tmpCanvas, 0, 0);
const tmpContext = tmpCanvas.getContext('2d');
tmpContext.clearRect(0, 0, tmpCanvas.width, tmpCanvas.height);
}
if (!documentCreated || dialogueOpen || !currentLayer.isVisible || currentLayer.isLocked) 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') {
@ -134,7 +147,7 @@ window.addEventListener("mouseup", function (mouseEvent) {
mode = 'out'; mode = 'out';
} }
changeZoom(layers[0], mode, getCursorPosition(mouseEvent)); changeZoom(mode, getCursorPosition(mouseEvent));
for (let i=1; i<layers.length; i++) { for (let i=1; i<layers.length; i++) {
layers[i].copyData(layers[0]); layers[i].copyData(layers[0]);
@ -325,6 +338,21 @@ function draw (mouseEvent) {
tool.rectangle.moveBrushPreview(lastMouseClickPos); tool.rectangle.moveBrushPreview(lastMouseClickPos);
currentTool.updateCursor(); currentTool.updateCursor();
} }
else if (currentTool.name == 'resizeline' && dragging) {
//get new brush size based on x distance from original clicking location
var distanceFromClick = cursorLocation[0] - lastMouseClickPos[0];
//var roundingAmount = 20 - Math.round(distanceFromClick/10);
//this doesnt work in reverse... because... it's not basing it off of the brush size which it should be
var lineSizeChange = Math.round(distanceFromClick/10);
var newLineSize = tool.line.previousBrushSize + lineSizeChange;
//set the brush to the new size as long as its bigger than 1
tool.line.brushSize = Math.max(1, newLineSize);
//fix offset so the cursor stays centered
tool.line.moveBrushPreview(lastMouseClickPos);
currentTool.updateCursor();
}
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;
@ -346,6 +374,19 @@ function draw (mouseEvent) {
updateMovePreview(getCursorPosition(mouseEvent)); updateMovePreview(getCursorPosition(mouseEvent));
} }
} }
else if (currentTool.name === "line") {
if (mouseEvent.target.className == 'drawingCanvas'|| mouseEvent.target.className == 'drawingCanvas') {
brushPreview.style.visibility = 'visible';
} else {
brushPreview.style.visibility = 'hidden';
}
if (dragging) {
if (mouseEvent.target.className == 'drawingCanvas' || mouseEvent.target.className == 'drawingCanvas') {
diagLine(lastMouseClickPos, zoom, cursorLocation);
}
}
currentLayer.updateLayerPreview();
}
} }
} }
@ -360,7 +401,7 @@ canvasView.addEventListener("wheel", function(mouseEvent){
} }
// Changing zoom and position of the first layer // Changing zoom and position of the first layer
changeZoom(layers[0], mode, getCursorPosition(mouseEvent)); changeZoom(mode, getCursorPosition(mouseEvent));
for (let i=1; i<layers.length; i++) { for (let i=1; i<layers.length; i++) {
// Copying first layer's data into the other layers // Copying first layer's data into the other layers

View File

@ -6,9 +6,15 @@ var selectionCanceled = true;
var firstTimeMove = true; var firstTimeMove = true;
// TODO: move with arrows // TODO: move with arrows
/** Updates the move preview so that is placed in the right position
*
* @param {*} mousePosition The position of the cursor
*/
function updateMovePreview(mousePosition) { function updateMovePreview(mousePosition) {
// I haven't canceled the selection
selectionCanceled = false; selectionCanceled = false;
// If it's the first time that I move the selection, I cut it from its original position
if (firstTimeMove) { if (firstTimeMove) {
cutSelection(mousePosition); cutSelection(mousePosition);
} }
@ -18,26 +24,34 @@ function updateMovePreview(mousePosition) {
lastMousePos = mousePosition; lastMousePos = mousePosition;
// clear the entire tmp layer // clear the entire tmp layer
TMPLayer.context.clearRect(0, 0, TMPLayer.canvas.width, TMPLayer.canvas.height); TMPLayer.context.clearRect(0, 0, TMPLayer.canvas.width, TMPLayer.canvas.height);
// put the image data with offset // put the image data on the tmp layer with offset
TMPLayer.context.putImageData( TMPLayer.context.putImageData(
imageDataToMove, imageDataToMove,
Math.round(lastMousePos[0] / zoom) - imageDataToMove.width / 2, Math.round(lastMousePos[0] / zoom) - imageDataToMove.width / 2,
Math.round(lastMousePos[1] / zoom) - imageDataToMove.height / 2); Math.round(lastMousePos[1] / zoom) - imageDataToMove.height / 2);
lastMovePos = lastMousePos; lastMovePos = lastMousePos;
// Moving the the rectangular ants
moveSelection(lastMousePos[0] / zoom, lastMousePos[1] / zoom, imageDataToMove.width, imageDataToMove.height); moveSelection(lastMousePos[0] / zoom, lastMousePos[1] / zoom, imageDataToMove.width, imageDataToMove.height);
} }
/** Ends a selection, meaning that it makes the changes definitive and creates the history states
*
*/
function endSelection() { function endSelection() {
// Clearing the tmp (move preview) and vfx (ants) layers
TMPLayer.context.clearRect(0, 0, TMPLayer.canvas.width, TMPLayer.canvas.height); TMPLayer.context.clearRect(0, 0, TMPLayer.canvas.width, TMPLayer.canvas.height);
VFXLayer.context.clearRect(0, 0, VFXLayer.canvas.width, VFXLayer.canvas.height); VFXLayer.context.clearRect(0, 0, VFXLayer.canvas.width, VFXLayer.canvas.height);
// Preparing an empty imageData with the size of the canvas
let cleanImageData = new ImageData(endX - startX, endY - startY); let cleanImageData = new ImageData(endX - startX, endY - startY);
// If I was moving something
if (imageDataToMove !== undefined) { if (imageDataToMove !== undefined) {
console.log("definito");
// Saving the current clipboard before editing it in order to merge it with the current layer // Saving the current clipboard before editing it in order to merge it with the current layer
cleanImageData.data.set(imageDataToMove.data); cleanImageData.data.set(imageDataToMove.data);
// I have to save the underlying data, so that the transparent pixels in the clipboard
// don't override the coloured pixels in the canvas
let underlyingImageData = currentLayer.context.getImageData(startX, startY, endX - startX, endY - startY); let underlyingImageData = currentLayer.context.getImageData(startX, startY, endX - startX, endY - startY);
for (let i=0; i<underlyingImageData.data.length; i+=4) { for (let i=0; i<underlyingImageData.data.length; i+=4) {
@ -51,6 +65,7 @@ function endSelection() {
underlyingImageData.data[i+2], underlyingImageData.data[i+3] underlyingImageData.data[i+2], underlyingImageData.data[i+3]
]; ];
// If the pixel of the clipboard is empty, but the one below it isn't, I use the pixel below
if (isPixelEmpty(currentMovePixel)) { if (isPixelEmpty(currentMovePixel)) {
if (!isPixelEmpty(underlyingImageData)) { if (!isPixelEmpty(underlyingImageData)) {
imageDataToMove.data[i] = currentUnderlyingPixel[0]; imageDataToMove.data[i] = currentUnderlyingPixel[0];
@ -61,13 +76,16 @@ function endSelection() {
} }
} }
// If I moved the selection before confirming it
if (lastMovePos !== undefined) { if (lastMovePos !== undefined) {
// I put it in the new position
currentLayer.context.putImageData( currentLayer.context.putImageData(
imageDataToMove, imageDataToMove,
Math.round(lastMovePos[0] / zoom) - imageDataToMove.width / 2, Math.round(lastMovePos[0] / zoom) - imageDataToMove.width / 2,
Math.round(lastMovePos[1] / zoom) - imageDataToMove.height / 2); Math.round(lastMovePos[1] / zoom) - imageDataToMove.height / 2);
} }
else { else {
// I put it in the same position
currentLayer.context.putImageData( currentLayer.context.putImageData(
imageDataToMove, imageDataToMove,
copiedStartX, copiedStartX,
@ -77,6 +95,7 @@ function endSelection() {
imageDataToMove.data.set(cleanImageData.data); imageDataToMove.data.set(cleanImageData.data);
} }
// Resetting all the flags
selectionCanceled = true; selectionCanceled = true;
isRectSelecting = false; isRectSelecting = false;
firstTimeMove = true; firstTimeMove = true;
@ -86,5 +105,6 @@ function endSelection() {
lastMovePos = undefined; lastMovePos = undefined;
currentLayer.updateLayerPreview(); currentLayer.updateLayerPreview();
// Saving the history
new HistoryStateEditCanvas(); new HistoryStateEditCanvas();
} }

View File

@ -1,16 +1,32 @@
let firstPixel = true; let firstPixel = true;
/** Creates a new, empty file
*
* @param {*} width Start width of the canvas
* @param {*} height Start height of the canvas
* @param {*} editorMode The editor mode chosen by the user (advanced or basic)
* @param {*} fileContent If fileContent != null, then the newPixel is being called from the open menu
*/
function newPixel (width, height, editorMode, fileContent = null) { function newPixel (width, height, editorMode, fileContent = null) {
// Saving the editor mode
pixelEditorMode = editorMode; pixelEditorMode = editorMode;
// The palette is empty, at the beginning
currentPalette = []; currentPalette = [];
// If this is the first pixel I'm creating since the app has started
if (firstPixel) { if (firstPixel) {
// I configure the layers elements
layerListEntry = layerList.firstElementChild; layerListEntry = layerList.firstElementChild;
// Creating the first layer
currentLayer = new Layer(width, height, canvas, layerListEntry); currentLayer = new Layer(width, height, canvas, layerListEntry);
currentLayer.canvas.style.zIndex = 2; currentLayer.canvas.style.zIndex = 2;
} }
else { else {
// If it's not the first Pixel, I have to reset the app
// Deleting all the extra layers and canvases, leaving only one
let nLayers = layers.length; let nLayers = layers.length;
for (let i=2; i < layers.length - nAppLayers; i++) { for (let i=2; i < layers.length - nAppLayers; i++) {
let currentEntry = layers[i].menuEntry; let currentEntry = layers[i].menuEntry;
@ -38,10 +54,9 @@ function newPixel (width, height, editorMode, fileContent = null) {
// Setting up the current layer // Setting up the current layer
layers[1] = new Layer(width, height, layers[1].canvas, layers[1].menuEntry); layers[1] = new Layer(width, height, layers[1].canvas, layers[1].menuEntry);
currentLayer = layers[1]; currentLayer = layers[1];
currentLayer.canvas.style.zIndex = 2; currentLayer.canvas.style.zIndex = 2;
// Updating canvas size // Updating canvas size to the new size
for (let i=0; i<nLayers; i++) { for (let i=0; i<nLayers; i++) {
layers[i].canvasSize = [width, height]; layers[i].canvasSize = [width, height];
} }
@ -58,7 +73,7 @@ function newPixel (width, height, editorMode, fileContent = null) {
// Pixel grid // Pixel grid
pixelGrid = new Layer(width, height, pixelGridCanvas); pixelGrid = new Layer(width, height, pixelGridCanvas);
// Setting the general canvasSize
canvasSize = currentLayer.canvasSize; canvasSize = currentLayer.canvasSize;
if (firstPixel) { if (firstPixel) {
@ -80,16 +95,23 @@ function newPixel (width, height, editorMode, fileContent = null) {
} }
//add colors from selected palette //add colors from selected palette
var selectedPalette = getText('palette-button'); var selectedPalette;
if (!firstPixel)
var selectedPalette = getText('palette-button');
else
var selectedPalette = getText('palette-button-splash');
// If the user selected a palette and isn't opening a file, I load the selected palette
if (selectedPalette != 'Choose a palette...' && fileContent == null) { 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)
history.pushState(null, null, '/pixel-editor/app'); history.pushState(null, null, '/pixel-editor/app');
//fill the palette with specified palette //fill the palette with specified colours
createColorPalette(palettes[selectedPalette].colors,true); createColorPalette(palettes[selectedPalette].colors,true);
} }
// Otherwise, I just generate 2 semirandom colours
else if (fileContent == null) { 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');
@ -120,15 +142,21 @@ function newPixel (width, height, editorMode, fileContent = null) {
undoStates = []; undoStates = [];
redoStates = []; redoStates = [];
// Closing the "New Pixel dialogue"
closeDialogue(); closeDialogue();
// Updating the cursor of the current tool
currentTool.updateCursor(); currentTool.updateCursor();
// The user is now able to export the Pixel
document.getElementById('export-button').classList.remove('disabled'); document.getElementById('export-button').classList.remove('disabled');
documentCreated = true; documentCreated = true;
// This is not the first Pixel anymore
firstPixel = false; firstPixel = false;
// Now, if I opened an LPE file
if (fileContent != null) { if (fileContent != null) {
// I add every layer the file had in it
for (let i=0; i<fileContent['nLayers']; i++) { for (let i=0; i<fileContent['nLayers']; i++) {
let layerData = fileContent['layer' + i]; let layerData = fileContent['layer' + i];
let layerImage = fileContent['layer' + i + 'ImageData']; let layerImage = fileContent['layer' + i + 'ImageData'];
@ -166,10 +194,23 @@ function newPixel (width, height, editorMode, fileContent = null) {
deleteLayer(false); deleteLayer(false);
} }
// Applying the correct editor mode
if (pixelEditorMode == 'Basic') { if (pixelEditorMode == 'Basic') {
switchMode('Advanced', false); switchMode(false);
} }
else { else {
switchMode('Basic', false); switchMode(false);
} }
}
function newFromTemplate(preset, x, y) {
if (preset != '') {
setText('palette-button-splash', presets[preset].palette);
setText('palette-button', presets[preset].palette);
x = presets[preset].width;
y = presets[preset].height;
}
newPixel(x, y, pixelEditorMode == 'Advanced' ? 'Basic' : 'Advanced');
} }

View File

@ -8,5 +8,5 @@ window.onload = function(){
newPixel(getValue('size-width'),getValue('size-height'), getValue('editor-mode')); 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('splash', false);
}; };

335
js/_paletteBlock.js Normal file
View File

@ -0,0 +1,335 @@
/** INIT is called when it shouldn't **/
let coloursList = document.getElementById("palette-list");
let rampMenu = document.getElementById("pb-ramp-options");
let pbRampDialogue = document.getElementById("pb-ramp-dialogue");
let currentSquareSize = coloursList.children[0].clientWidth;
let blockData = {blockWidth: 300, blockHeight: 320, squareSize: 40};
let isRampSelecting = false;
let ramps = [];
let currentSelection = {startIndex:0, endIndex:0, startCoords:[], endCoords: [], name: "", colour: "", label: null};
// Making the palette list sortable
new Sortable(document.getElementById("palette-list"), {
animation: 100,
onEnd: updateRampSelection
});
// Listening for the palette block resize
new ResizeObserver(updateSizeData).observe(coloursList.parentElement);
// Initializes the palette block
function pbInit() {
let simplePalette = document.getElementById("colors-menu");
let childCount = coloursList.childElementCount;
currentSquareSize = coloursList.children[0].clientWidth;
coloursList = document.getElementById("palette-list");
// Remove all the colours
for (let i=0; i<childCount; i++) {
coloursList.children[0].remove();
}
// Add all the colours from the simplepalette
for (let i=0; i<simplePalette.childElementCount-1; i++) {
addSingleColour(cssToHex(simplePalette.children[i].children[0].style.backgroundColor));
}
}
/** Listens for the mouse wheel, used to change the size of the squares in the palette list
*
*/
coloursList.parentElement.addEventListener("wheel", function (mouseEvent) {
// Only resize when pressing alt, used to distinguish between scrolling through the palette and
// resizing it
if (mouseEvent.altKey) {
resizeSquares(mouseEvent);
}
});
/** Tells whether a colour is in the palette or not
*
* @param {*} colour The colour to add
*/
function hasColour(colour) {
for (let i=0; i<coloursList.childElementCount; i++) {
let currentCol = coloursList.children[i].style.backgroundColor;
let currentHex = cssToHex(currentCol);
if (currentHex == colour) {
return true;
}
}
return false;
}
/** Adds a single colour to the palette
*
* @param {*} colour The colour to add
*/
function addSingleColour(colour) {
if (!hasColour(colour)) {
let li = document.createElement("li");
li.style.width = currentSquareSize + "px";
li.style.height = currentSquareSize + "px";
li.style.backgroundColor = colour;
li.addEventListener("mousedown", startRampSelection);
li.addEventListener("mouseup", endRampSelection);
li.addEventListener("mousemove", updateRampSelection);
li.addEventListener("onclick", endRampSelection);
coloursList.appendChild(li);
}
}
/** Adds all the colours currently selected in the colour picker
*
*/
function pbAddColours() {
let colours = getSelectedColours();
for (let i=0; i<colours.length; i++) {
addSingleColour(colours[i]);
}
}
/** Removes all the currently selected colours from the palette
*
*/
function pbRemoveColours() {
let startIndex = currentSelection.startIndex;
let endIndex = currentSelection.endIndex;
if (startIndex > endIndex) {
let tmp = startIndex;
startIndex = endIndex;
endIndex = tmp;
}
for (let i=startIndex; i<=endIndex; i++) {
coloursList.removeChild(coloursList.children[startIndex]);
}
clearBorders();
// TODO: make it so that ramps update correctly (change start and end indexes if necessary)
}
/** Starts selecting a ramp. Saves the data needed to draw the outline.
*
* @param {*} mouseEvent
*/
function startRampSelection(mouseEvent) {
if (mouseEvent.which == 3) {
let index = getElementIndex(mouseEvent.target);
isRampSelecting = true;
currentSelection.startIndex = index;
currentSelection.endIndex = index;
currentSelection.startCoords = getColourCoordinates(index);
currentSelection.endCoords = getColourCoordinates(index);
}
}
/** Updates the outline for the current selection.
*
* @param {*} mouseEvent
*/
function updateRampSelection(mouseEvent) {
if (mouseEvent != null && mouseEvent.which == 3) {
currentSelection.endIndex = getElementIndex(mouseEvent.target);
}
if (mouseEvent == null || mouseEvent.which == 3) {
let startCoords = getColourCoordinates(currentSelection.startIndex);
let endCoords = getColourCoordinates(currentSelection.endIndex);
let startIndex = currentSelection.startIndex;
let endIndex = currentSelection.endIndex;
if (currentSelection.startIndex > endIndex) {
let tmp = startIndex;
startIndex = endIndex;
endIndex = tmp;
tmp = startCoords;
startCoords = endCoords;
endCoords = tmp;
}
clearBorders();
for (let i=startIndex; i<=endIndex; i++) {
let currentSquare = coloursList.children[i];
let currentCoords = getColourCoordinates(i);
let borderStyle = "3px solid white";
let bordersToSet = [];
// Deciding which borders to use to make the outline
if (i == 0 || i == startIndex) {
bordersToSet.push("border-left");
}
if (currentCoords[1] == startCoords[1] || ((currentCoords[1] == startCoords[1] + 1)) && currentCoords[0] < startCoords[0]) {
bordersToSet.push("border-top");
}
if (currentCoords[1] == endCoords[1] || ((currentCoords[1] == endCoords[1] - 1)) && currentCoords[0] > endCoords[0]) {
bordersToSet.push("border-bottom");
}
if ((i == coloursList.childElementCount - 1) || (currentCoords[0] == Math.floor(blockData.blockWidth / blockData.squareSize) - 1)
|| i == endIndex) {
bordersToSet.push("border-right");
}
if (bordersToSet != []) {
currentSquare.style["box-sizing"] = "border-box";
for (let i=0; i<bordersToSet.length; i++) {
currentSquare.style[bordersToSet[i]] = borderStyle;
}
}
}
}
}
/** Removes all the borders from all the squares. The borders are cleared only for the
* current selection, so every border that is not white is kept.
*
*/
function clearBorders() {
for (let i=0; i<coloursList.childElementCount; i++) {
coloursList.children[i].style["border-top"] = "none";
coloursList.children[i].style["border-left"] = "none";
coloursList.children[i].style["border-right"] = "none";
coloursList.children[i].style["border-bottom"] = "none";
}
}
/** Ends the current selection, opens the ramp menu
*
* @param {*} mouseEvent
*/
function endRampSelection(mouseEvent) {
let col;
if (currentSelection.startCoords.length == 0) {
currentSelection.endIndex = getElementIndex(mouseEvent.target);
currentSelection.startIndex = currentSelection.endIndex;
currentSelection.startCoords = getColourCoordinates(currentSelection.startIndex);
}
// I'm not selecting a ramp anymore
isRampSelecting = false;
// Setting the end coordinates
currentSelection.endCoords = getColourCoordinates(getElementIndex(mouseEvent.target));
// Setting the colour in the colour picker
col = cssToHex(coloursList.children[currentSelection.startIndex].style.backgroundColor);
updatePickerByHex(col);
updateSlidersByHex(col);
updateMiniPickerColour();
updateRampSelection();
currentSelection.startCoords = [];
}
function closeAllSubmenus() {
let menus = document.getElementsByClassName("pb-submenu");
for (let i=0; i<menus.length; i++) {
menus[i].style.display = "none";
}
}
/** Updates the current data about the size of the palette list (height, width and square size).
* It also updates the outline after doing so.
*
*/
function updateSizeData() {
blockData.blockHeight = coloursList.parentElement.clientHeight;
blockData.blockWidth = coloursList.parentElement.clientWidth;
blockData.squareSize = coloursList.children[0].clientWidth;
updateRampSelection();
}
/** Gets the colour coordinates relative to the colour list seen as a matrix. Coordinates
* start from the top left angle.
*
* @param {*} index The index of the colour in the list seen as a linear array
*/
function getColourCoordinates(index) {
let yIndex = Math.floor(index / Math.floor(blockData.blockWidth / blockData.squareSize));
let xIndex = Math.floor(index % Math.floor(blockData.blockWidth / blockData.squareSize));
return [xIndex, yIndex];
}
/** Returns the index of the element in the colour list
*
* @param {*} element The element of which we need to get the index
*/
function getElementIndex(element) {
for (let i=0; i<coloursList.childElementCount; i++) {
if (element == coloursList.children[i]) {
return i;
}
}
}
/** Resizes the squares depending on the scroll amount (only resizes if the user is
* also holding alt)
*
* @param {*} mouseEvent
*/
function resizeSquares(mouseEvent) {
let amount = mouseEvent.deltaY > 0 ? -5 : 5;
currentSquareSize += amount;
for (let i=0; i<coloursList.childElementCount; i++) {
let currLi = coloursList.children[i];
currLi.style["box-sizing"] = "content-box";
currLi.style.width = currLi.clientWidth + amount + "px";
currLi.style.height = currLi.clientHeight + amount + "px";
}
updateSizeData();
}
/** Converts a CSS colour eg rgb(x,y,z) to a hex string
*
* @param {*} rgb
*/
function cssToHex(rgb) {
rgb = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
function hex(x) {
return ("0" + parseInt(x).toString(16)).slice(-2);
}
return "#" + hex(rgb[1]) + hex(rgb[2]) + hex(rgb[3]);
}
function pbAddToSimplePalette() {
let simplePalette = document.getElementById("colors-menu");
let childCount = simplePalette.childElementCount;
// Removing all the colours
for (let i=0; i<childCount-1; i++) {
simplePalette.removeChild(simplePalette.children[0]);
}
// Adding the new ones
for (let i=0; i<coloursList.childElementCount; i++) {
let col = coloursList.children[i].style.backgroundColor;
if (col.includes("rgb")) {
addColor(cssToHex(col));
}
else {
addColor(col);
}
}
}

View File

@ -2,56 +2,83 @@
Object.keys(palettes).forEach(function(paletteName,index) { Object.keys(palettes).forEach(function(paletteName,index) {
var palettesMenu = document.getElementById('palette-menu'); var palettesMenu = document.getElementById('palette-menu');
var splashPalettes = document.getElementById('palette-menu-splash');
//create button //create button
var button = document.createElement('button'); var button = document.createElement('button');
button.appendChild(document.createTextNode(paletteName)); button.appendChild(document.createTextNode(paletteName));
//insert new element
palettesMenu.appendChild(button);
//if the palette was specified by the user, change the dropdown to it //if the palette was specified by the user, change the dropdown to it
if (palettes[paletteName].specified == true) { if (palettes[paletteName].specified == true) {
setText('palette-button', paletteName); setText('palette-button', paletteName);
setText('palette-button-splash', paletteName)
//Show empty palette option //Show empty palette option
document.getElementById('no-palette-button').style.display = 'block'; document.getElementById('no-palette-button').style.display = 'block';
} }
on('click', button, function() { var buttonEvent = function() {
//hide the dropdown menu //hide the dropdown menu
deselect('palette-menu'); deselect('palette-menu');
deselect('palette-button'); deselect('palette-button');
deselect('palette-menu-splash');
deselect('palette-button-splash');
//show empty palette option //show empty palette option
document.getElementById('no-palette-button').style.display = 'block'; document.getElementById('no-palette-button').style.display = 'block';
//set the text of the dropdown to the newly selected preset //set the text of the dropdown to the newly selected preset
setText('palette-button', paletteName); setText('palette-button', paletteName);
}); setText('palette-button-splash', paletteName);
};
on('click', button, buttonEvent);
//insert new element
palettesMenu.appendChild(button);
// Making a copy for the splash page too
var copyButton = button.cloneNode(true);
// Attaching the same event
on('click', copyButton, buttonEvent);
// Appending it to the splash palette menu
splashPalettes.appendChild(copyButton);
}); });
//select no palette var noPaletteButtonClickEvent = function () {
on('click', 'no-palette-button', function () {
document.getElementById('no-palette-button').style.display = 'none'; document.getElementById('no-palette-button').style.display = 'none';
setText('palette-button', 'Choose a palette...'); setText('palette-button', 'Choose a palette...');
}); }
//select load palette var loadPaletteButtonEvent = function () {
on('click', 'load-palette-button', function () {
document.getElementById('load-palette-browse-holder').click(); document.getElementById('load-palette-browse-holder').click();
}); }
var clickPaletteButtonEvent = function (e){
on('click', 'palette-button', function (e){
toggle('palette-button'); toggle('palette-button');
toggle('palette-menu'); toggle('palette-menu');
deselect('preset-button'); deselect('preset-button');
deselect('preset-menu'); deselect('preset-menu');
// Splash version
toggle('palette-button-splash');
toggle('palette-menu-splash');
e.stopPropagation(); e.stopPropagation();
}); }
//select no palette
on('click', 'no-palette-button', noPaletteButtonClickEvent);
//select load palette
on('click', 'load-palette-button', loadPaletteButtonEvent);
//select load palette
on('click', 'load-palette-button-splash', loadPaletteButtonEvent);
// Palette menu click
on('click', 'palette-button', clickPaletteButtonEvent);
on('click', 'palette-button-splash', clickPaletteButtonEvent);
on('click', 'new-pixel', function (){ on('click', 'new-pixel', function (){
deselect('editor-mode-menu'); deselect('editor-mode-menu');
@ -59,4 +86,8 @@ on('click', 'new-pixel', function (){
deselect('preset-menu'); deselect('preset-menu');
deselect('palette-button'); deselect('palette-button');
deselect('palette-menu'); deselect('palette-menu');
});
// Splash version
deselect('palette-button-splash');
deselect('palette-menu-splash');
});

View File

@ -1,3 +1,10 @@
/***********MISCELLANEOUS UTILITY FUNCTIONS**************/
/** Merges topLayer onto belowLayer
*
* @param {*} belowLayer The layer on the bottom of the layer stack
* @param {*} topLayer The layer on the top of the layer stack
*/
function mergeLayers(belowLayer, topLayer) { function mergeLayers(belowLayer, topLayer) {
// Copying the above content on the layerBelow // Copying the above content on the layerBelow
let belowImageData = belowLayer.getImageData(0, 0, canvas.width, canvas.height); let belowImageData = belowLayer.getImageData(0, 0, canvas.width, canvas.height);
@ -24,10 +31,19 @@ function mergeLayers(belowLayer, topLayer) {
} }
} }
// Putting the top data into the belowdata
belowLayer.putImageData(toMergeImageData, 0, 0); belowLayer.putImageData(toMergeImageData, 0, 0);
} }
/** Used to programmatically create an input event
*
* @param {*} keyCode KeyCode of the key to press
* @param {*} ctrl Is ctrl pressed?
* @param {*} alt Is alt pressed?
* @param {*} shift Is shift pressed?
*/
function simulateInput(keyCode, ctrl, alt, shift) { function simulateInput(keyCode, ctrl, alt, shift) {
// I just copy pasted this from stack overflow lol please have mercy
let keyboardEvent = document.createEvent("KeyboardEvent"); let keyboardEvent = document.createEvent("KeyboardEvent");
let initMethod = typeof keyboardEvent.initKeyboardEvent !== 'undefined' ? "initKeyboardEvent" : "initKeyEvent"; let initMethod = typeof keyboardEvent.initKeyboardEvent !== 'undefined' ? "initKeyboardEvent" : "initKeyEvent";
@ -46,38 +62,62 @@ function simulateInput(keyCode, ctrl, alt, shift) {
document.dispatchEvent(keyboardEvent); document.dispatchEvent(keyboardEvent);
} }
/** Tells if a pixel is empty (has alpha = 0)
*
* @param {*} pixel
*/
function isPixelEmpty(pixel) { function isPixelEmpty(pixel) {
if (pixel == null || pixel === undefined) { if (pixel == null || pixel === undefined) {
return false; return false;
} }
if ((pixel[0] == 0 && pixel[1] == 0 && pixel[2] == 0) || pixel[3] == 0) { // If the alpha channel is 0, the current pixel is empty
if (pixel[3] == 0) {
return true; return true;
} }
return false; return false;
} }
/** Tells if element is a child of an element with class className
*
* @param {*} element
* @param {*} className
*/
function isChildOfByClass(element, className) { function isChildOfByClass(element, className) {
// Getting the element with class className
while (element != null && element.classList != null && !element.classList.contains(className)) { while (element != null && element.classList != null && !element.classList.contains(className)) {
element = element.parentElement; element = element.parentElement;
} }
// If that element exists and its class is the correct one
if (element != null && element.classList != null && element.classList.contains(className)) { if (element != null && element.classList != null && element.classList.contains(className)) {
// Then element is a chld of an element with class className
return true; return true;
} }
return false; return false;
} }
/** Gets the eyedropped colour (the colour of the pixel pointed by the cursor when the user is using the eyedropper).
* It takes the colour of the canvas with the biggest z-index, basically the one the user can see, since it doesn't
* make much sense to sample a colour which is hidden behind a different layer
*
* @param {*} cursorLocation The position of the cursor
*/
function getEyedropperColor(cursorLocation) { function getEyedropperColor(cursorLocation) {
// Making sure max will take some kind of value
let max = -1; let max = -1;
// Using tmpColour to sample the sprite
let tmpColour; let tmpColour;
// Returned colour
let selectedColor; let selectedColor;
for (let i=1; i<layers.length; i++) { for (let i=1; i<layers.length; i++) {
// Getting the colour of the pixel in the cursorLocation
tmpColour = layers[i].context.getImageData(Math.floor(cursorLocation[0]/zoom),Math.floor(cursorLocation[1]/zoom),1,1).data; tmpColour = layers[i].context.getImageData(Math.floor(cursorLocation[0]/zoom),Math.floor(cursorLocation[1]/zoom),1,1).data;
// If it's not empty, I check if it's on the top of the previous colour
if (layers[i].canvas.style.zIndex > max || isPixelEmpty(selectedColor) || selectedColor === undefined) { if (layers[i].canvas.style.zIndex > max || isPixelEmpty(selectedColor) || selectedColor === undefined) {
max = layers[i].canvas.style.zIndex; max = layers[i].canvas.style.zIndex;
@ -87,6 +127,7 @@ function getEyedropperColor(cursorLocation) {
} }
} }
// If the final colour was empty, I return black
if (isPixelEmpty(tmpColour) && selectedColor === undefined) { if (isPixelEmpty(tmpColour) && selectedColor === undefined) {
selectedColor = [0, 0, 0]; selectedColor = [0, 0, 0];
} }
@ -94,7 +135,12 @@ function getEyedropperColor(cursorLocation) {
return selectedColor; return selectedColor;
} }
/** Gets the absolute position of the element (position on the screen)
*
* @param {*} element The element of which we have to get the position
*/
function getElementAbsolutePosition(element) { function getElementAbsolutePosition(element) {
// Probably copy pasted this from stack overflow too, if not I don't recall how it works
let curleft = curtop = 0; let curleft = curtop = 0;
if (element.offsetParent) { if (element.offsetParent) {
@ -107,9 +153,15 @@ function getElementAbsolutePosition(element) {
return [curleft,curtop]; return [curleft,curtop];
} }
/** Nearest neighbor algorithm to scale a sprite
*
* @param {*} src The source imageData
* @param {*} dst The destination imageData
*/
function nearestNeighbor (src, dst) { function nearestNeighbor (src, dst) {
let pos = 0 let pos = 0
// Just applying the nearest neighbor algorithm
for (let y = 0; y < dst.height; y++) { for (let y = 0; y < dst.height; y++) {
for (let x = 0; x < dst.width; x++) { for (let x = 0; x < dst.width; x++) {
const srcX = Math.floor(x * src.width / dst.width) const srcX = Math.floor(x * src.width / dst.width)
@ -125,7 +177,14 @@ function nearestNeighbor (src, dst) {
} }
} }
/** Bilinear interpolation used to scale a sprite
*
* @param {*} src The source imageData
* @param {*} dst The destination imageData
*/
function bilinearInterpolation (src, dst) { function bilinearInterpolation (src, dst) {
// Applying the bilinear interpolation algorithm
function interpolate (k, kMin, kMax, vMin, vMax) { function interpolate (k, kMin, kMax, vMin, vMax) {
return Math.round((k - kMin) * vMax + (kMax - k) * vMin) return Math.round((k - kMin) * vMax + (kMax - k) * vMin)
} }
@ -167,14 +226,21 @@ function bilinearInterpolation (src, dst) {
} }
} }
/** Resizes an imageData depending on the algorithm and on the new width and height
*
* @param {*} image The imageData to scale
* @param {*} width The new width of the imageData
* @param {*} height The new height of the imageData
* @param {*} algorithm Scaling algorithm chosen by the user in the dialogue
*/
function resizeImageData (image, width, height, algorithm) { function resizeImageData (image, width, height, algorithm) {
algorithm = algorithm || 'bilinear-interpolation' algorithm = algorithm || 'bilinear-interpolation'
let resize let resize;
switch (algorithm) { switch (algorithm) {
case 'nearest-neighbor': resize = nearestNeighbor; break case 'nearest-neighbor': resize = nearestNeighbor; break
case 'bilinear-interpolation': resize = bilinearInterpolation; break case 'bilinear-interpolation': resize = bilinearInterpolation; break
default: throw new Error(`Unknown algorithm: ${algorithm}`) default: return image;
} }
const result = new ImageData(width, height) const result = new ImageData(width, height)
@ -184,10 +250,21 @@ function resizeImageData (image, width, height, algorithm) {
return result return result
} }
/** Gets the position in (x, y) format of the pixel with index "index"
*
* @param {*} index The index of the pixel of which we need the (x, y) position
*/
function getPixelPosition(index) { function getPixelPosition(index) {
let linearIndex = index / 4; let linearIndex = index / 4;
let x = linearIndex % layers[0].canvasSize[0]; let x = linearIndex % layers[0].canvasSize[0];
let y = Math.floor(linearIndex / layers[0].canvasSize[0]); let y = Math.floor(linearIndex / layers[0].canvasSize[0]);
return [Math.ceil(x), Math.ceil(y)]; return [Math.ceil(x), Math.ceil(y)];
}
/** Sets isDragging to false, used when the user interacts with sortable lists
*
*/
function makeIsDraggingFalse(event) {
dragging = false;
} }

View File

@ -1,27 +1,44 @@
// Start colour of the pixel grid (can be changed in the preferences)
let pixelGridColor = "#0000FF"; let pixelGridColor = "#0000FF";
// Distance between one line and another in HTML pixels
let lineDistance = 12; let lineDistance = 12;
// The grid is not visible at the beginning
let pixelGridVisible = false; let pixelGridVisible = false;
// Saving the canvas containing the pixel grid
pixelGridCanvas = document.getElementById("pixel-grid"); pixelGridCanvas = document.getElementById("pixel-grid");
/** Shows or hides the pixel grid depening on its current visibility
* (triggered by the show pixel grid button in the top menu)
*
*/
function togglePixelGrid(event) { function togglePixelGrid(event) {
// Getting the button because I have to change its text
let button = document.getElementById("toggle-pixelgrid-button"); let button = document.getElementById("toggle-pixelgrid-button");
// Toggling the state
pixelGridVisible = !pixelGridVisible; pixelGridVisible = !pixelGridVisible;
// If it was visible, I hide it
if (pixelGridVisible) { if (pixelGridVisible) {
button.innerHTML = "Hide pixel grid"; button.innerHTML = "Hide pixel grid";
pixelGridCanvas.style.display = "inline-block"; pixelGridCanvas.style.display = "inline-block";
} }
// Otherwise, I show it
else { else {
button.innerHTML = "Show pixel grid"; button.innerHTML = "Show pixel grid";
pixelGridCanvas.style.display = "none"; pixelGridCanvas.style.display = "none";
} }
} }
/** Fills the pixelGridCanvas with the pixelgrid
*
*/
function fillPixelGrid() { function fillPixelGrid() {
let context = pixelGridCanvas.getContext("2d"); let context = pixelGridCanvas.getContext("2d");
let originalSize = layers[0].canvasSize; let originalSize = layers[0].canvasSize;
// The pixelGridCanvas is lineDistance times bigger so that the lines don't take 1 canvas pixel
// (which would cover the whole canvas with the pixelGridColour), but they take 1/lineDistance canvas pixels
pixelGridCanvas.width = originalSize[0] * lineDistance; pixelGridCanvas.width = originalSize[0] * lineDistance;
pixelGridCanvas.height = originalSize[1] * lineDistance; pixelGridCanvas.height = originalSize[1] * lineDistance;

View File

@ -1,4 +1,4 @@
//prests //presets
var presets = { var presets = {
'Gameboy Color': { 'Gameboy Color': {
width: 240, width: 240,

View File

@ -4,6 +4,10 @@ let startY;
let endX; let endX;
let endY; let endY;
/** Starts the selection: saves the canvas, sets the start coordinates
*
* @param {*} mouseEvent
*/
function startRectSelection(mouseEvent) { function startRectSelection(mouseEvent) {
// Saving the canvas // Saving the canvas
new HistoryStateEditCanvas(); new HistoryStateEditCanvas();
@ -35,6 +39,10 @@ function startRectSelection(mouseEvent) {
selectionCanceled = false; selectionCanceled = false;
} }
/** Updates the selection
*
* @param {*} mouseEvent
*/
function updateRectSelection(mouseEvent) { function updateRectSelection(mouseEvent) {
let pos = getCursorPosition(mouseEvent); let pos = getCursorPosition(mouseEvent);
@ -42,6 +50,10 @@ function updateRectSelection(mouseEvent) {
drawRect(Math.round(pos[0] / zoom) + 0.5, Math.round(pos[1] / zoom) + 0.5); drawRect(Math.round(pos[0] / zoom) + 0.5, Math.round(pos[1] / zoom) + 0.5);
} }
/** Ends the selection: sets the end coordiantes
*
* @param {*} mouseEvent
*/
function endRectSelection(mouseEvent) { function endRectSelection(mouseEvent) {
// Getting the end position // Getting the end position
let currentPos = getCursorPosition(mouseEvent); let currentPos = getCursorPosition(mouseEvent);
@ -72,6 +84,10 @@ function endRectSelection(mouseEvent) {
currentTool.updateCursor(); currentTool.updateCursor();
} }
/** Cuts the selection from its canvas and puts it in the tmp layer so it can be moved
*
* @param {*} mousePosition
*/
function cutSelection(mousePosition) { function cutSelection(mousePosition) {
// Getting the selected pixels // Getting the selected pixels
imageDataToMove = currentLayer.context.getImageData(startX, startY, endX - startX + 1, endY - startY + 1); imageDataToMove = currentLayer.context.getImageData(startX, startY, endX - startX + 1, endY - startY + 1);
@ -79,10 +95,13 @@ function cutSelection(mousePosition) {
currentLayer.context.clearRect(startX - 0.5, startY - 0.5, endX - startX + 1, endY - startY + 1); currentLayer.context.clearRect(startX - 0.5, startY - 0.5, endX - startX + 1, endY - startY + 1);
// Moving those pixels from the current layer to the tmp layer // Moving those pixels from the current layer to the tmp layer
TMPLayer.context.putImageData(imageDataToMove, startX + 1, startY); TMPLayer.context.putImageData(imageDataToMove, startX + 1, startY);
//originalDataPosition = [currentPos[0], currentPos[1]];
} }
/** Draws a dashed rectangle representing the selection
*
* @param {*} x Current end x coordinate of the selection
* @param {*} y Current end y coordinate of the selection
*/
function drawRect(x, y) { function drawRect(x, y) {
// Getting the vfx context // Getting the vfx context
let vfxContext = VFXCanvas.getContext('2d'); let vfxContext = VFXCanvas.getContext('2d');
@ -106,10 +125,13 @@ function applyChanges() {
// Checks whether the pointer is inside the selected area or not // Checks whether the pointer is inside the selected area or not
function cursorInSelectedArea() { function cursorInSelectedArea() {
// Getting the cursor position
let cursorPos = getCursorPosition(currentMouseEvent); let cursorPos = getCursorPosition(currentMouseEvent);
// Getting the coordinates relatively to the canvas
let x = cursorPos[0] / zoom; let x = cursorPos[0] / zoom;
let y = cursorPos[1] / zoom; let y = cursorPos[1] / zoom;
// This is to avoid rightX or topY being less than leftX or bottomY
let leftX = Math.min(startX, endX); let leftX = Math.min(startX, endX);
let rightX = Math.max(startX, endX); let rightX = Math.max(startX, endX);
let topY = Math.max(startY, endY); let topY = Math.max(startY, endY);
@ -126,6 +148,13 @@ function cursorInSelectedArea() {
return false; return false;
} }
/** Moves the rect ants to the specified position
*
* @param {*} x X coordinate of the rect ants
* @param {*} y Y coordinat of the rect ants
* @param {*} width Width of the selection
* @param {*} height Height of the selectin
*/
function moveSelection(x, y, width, height) { function moveSelection(x, y, width, height) {
// Getting the vfx context // Getting the vfx context
let vfxContext = VFXCanvas.getContext('2d'); let vfxContext = VFXCanvas.getContext('2d');
@ -135,6 +164,7 @@ function moveSelection(x, y, width, height) {
vfxContext.lineWidth = 1; vfxContext.lineWidth = 1;
vfxContext.setLineDash([4]); vfxContext.setLineDash([4]);
// Fixing the coordinates
startX = Math.round(Math.round(x) - 0.5 - Math.round(width / 2)) + 0.5; startX = Math.round(Math.round(x) - 0.5 - Math.round(width / 2)) + 0.5;
startY = Math.round(Math.round(y) - 0.5 - Math.round(height / 2)) + 0.5; startY = Math.round(Math.round(y) - 0.5 - Math.round(height / 2)) + 0.5;
endX = startX + Math.round(width); endX = startX + Math.round(width);

View File

@ -1,19 +1,26 @@
// Saving the empty rect svg
var emptySVG = document.getElementById("empty-button-svg"); var emptySVG = document.getElementById("empty-button-svg");
// and the full rect svg so that I can change them when the user changes rect modes
var fullSVG = document.getElementById("full-button-svg"); var fullSVG = document.getElementById("full-button-svg");
// The start mode is empty rectangle
var drawMode = 'empty'; var drawMode = 'empty';
// I'm not drawing a rectangle at the beginning
var isDrawingRect = false; var isDrawingRect = false;
// Rect coordinates
let startRectX; let startRectX;
let startRectY; let startRectY;
let endRectX; let endRectX;
let endRectY; let endRectY;
/** Starts drawing the rect, saves the start coordinates
*
* @param {*} mouseEvent
*/
function startRectDrawing(mouseEvent) { function startRectDrawing(mouseEvent) {
// Putting the vfx layer on top of everything // Putting the vfx layer on top of everything
VFXCanvas.style.zIndex = MAX_Z_INDEX; VFXCanvas.style.zIndex = parseInt(currentLayer.canvas.style.zIndex, 10) + 1;;
// Updating flag // Updating flag
isDrawingRect = true; isDrawingRect = true;
@ -25,13 +32,22 @@ function startRectDrawing(mouseEvent) {
drawRectangle(startRectX, startRectY); drawRectangle(startRectX, startRectY);
} }
/** Updates the rectangle preview depending on the position of the mouse
*
* @param {*} mouseEvent The mouseEvent from which we'll get the mouse position
*/
function updateRectDrawing(mouseEvent) { function updateRectDrawing(mouseEvent) {
let pos = getCursorPosition(mouseEvent); let pos = getCursorPosition(mouseEvent);
// Drawing the rect // Drawing the rect at the right position
drawRectangle(Math.round(pos[0] / zoom) + 0.5, Math.round(pos[1] / zoom) + 0.5); drawRectangle(Math.round(pos[0] / zoom) + 0.5, Math.round(pos[1] / zoom) + 0.5);
} }
/** Finishes drawing the rect, decides the end coordinates and moves the preview rectangle to the
* current layer
*
* @param {*} mouseEvent event from which we'll get the mouse position
*/
function endRectDrawing(mouseEvent) { function endRectDrawing(mouseEvent) {
// Getting the end position // Getting the end position
let currentPos = getCursorPosition(mouseEvent); let currentPos = getCursorPosition(mouseEvent);
@ -61,14 +77,17 @@ function endRectDrawing(mouseEvent) {
endRectX -= 0.5; endRectX -= 0.5;
startRectX -= 0.5; startRectX -= 0.5;
// Setting the correct linewidth and colour
currentLayer.context.lineWidth = tool.rectangle.brushSize; currentLayer.context.lineWidth = tool.rectangle.brushSize;
currentLayer.context.fillStyle = currentGlobalColor; currentLayer.context.fillStyle = currentGlobalColor;
// Drawing the rect using 4 lines
line(startRectX, startRectY, endRectX, startRectY, tool.rectangle.brushSize); line(startRectX, startRectY, endRectX, startRectY, tool.rectangle.brushSize);
line(endRectX, startRectY, endRectX, endRectY, tool.rectangle.brushSize); line(endRectX, startRectY, endRectX, endRectY, tool.rectangle.brushSize);
line(endRectX, endRectY, startRectX, endRectY, tool.rectangle.brushSize); line(endRectX, endRectY, startRectX, endRectY, tool.rectangle.brushSize);
line(startRectX, endRectY, startRectX, startRectY, tool.rectangle.brushSize); line(startRectX, endRectY, startRectX, startRectY, tool.rectangle.brushSize);
// If I have to fill it, I do so
if (drawMode == 'fill') { if (drawMode == 'fill') {
currentLayer.context.fillRect(startRectX, startRectY, endRectX - startRectX, endRectY - startRectY); currentLayer.context.fillRect(startRectX, startRectY, endRectX - startRectX, endRectY - startRectY);
} }
@ -77,6 +96,12 @@ function endRectDrawing(mouseEvent) {
vfxContext.clearRect(0, 0, VFXCanvas.width, VFXCanvas.height); vfxContext.clearRect(0, 0, VFXCanvas.width, VFXCanvas.height);
} }
/** Draws a rectangle with end coordinates given by x and y on the VFX layer (draws
* the preview for the rectangle tool)
*
* @param {*} x The current end x of the rectangle
* @param {*} y The current end y of the rectangle
*/
function drawRectangle(x, y) { function drawRectangle(x, y) {
// Getting the vfx context // Getting the vfx context
let vfxContext = VFXCanvas.getContext("2d"); let vfxContext = VFXCanvas.getContext("2d");
@ -98,10 +123,12 @@ function drawRectangle(x, y) {
} }
vfxContext.setLineDash([]); vfxContext.setLineDash([]);
vfxContext.stroke(); vfxContext.stroke();
} }
/** Sets the correct tool icon depending on its mode
*
*/
function setRectToolSvg() { function setRectToolSvg() {
if (drawMode == 'empty') { if (drawMode == 'empty') {
emptySVG.setAttribute('display', 'visible'); emptySVG.setAttribute('display', 'visible');
@ -114,5 +141,5 @@ function setRectToolSvg() {
} }
function applyChanges() { function applyChanges() {
VFXCanvas.style.zIndex = MIN_Z_INDEX; //VFXCanvas.style.zIndex = MIN_Z_INDEX;
} }

View File

@ -1,16 +1,31 @@
let resizeCanvasContainer = document.getElementById("resize-canvas"); /* This scripts contains all the code used to handle the canvas resizing */
let rcPivot = "middle";
let currentPivotObject;
let borders = {left: 0, right: 0, top: 0, bottom: 0};
// Resize canvas pop up window
let resizeCanvasContainer = document.getElementById("resize-canvas");
// Start pivot
let rcPivot = "middle";
// Selected pivot button
let currentPivotObject;
// Border offsets
let rcBorders = {left: 0, right: 0, top: 0, bottom: 0};
/** Opens the canvas resize window
*
*/
function openResizeCanvasWindow() { function openResizeCanvasWindow() {
// Initializes the inputs
initResizeCanvasInputs(); initResizeCanvasInputs();
showDialogue('resize-canvas'); showDialogue('resize-canvas');
} }
/** Initializes the canvas resizing input
*
*/
function initResizeCanvasInputs() { function initResizeCanvasInputs() {
// Getting the pivot buttons
let buttons = document.getElementsByClassName("pivot-button"); let buttons = document.getElementsByClassName("pivot-button");
// Adding the event handlers for them
for (let i=0; i<buttons.length; i++) { for (let i=0; i<buttons.length; i++) {
buttons[i].addEventListener("click", changePivot); buttons[i].addEventListener("click", changePivot);
if (buttons[i].getAttribute("value").includes("middle")) { if (buttons[i].getAttribute("value").includes("middle")) {
@ -33,13 +48,21 @@ function initResizeCanvasInputs() {
console.log("Pivot selezionato: " + currentPivotObject); console.log("Pivot selezionato: " + currentPivotObject);
} }
/** Fired when a border offset is changed: it updates the width and height
*
* @param {*} event
*/
function rcChangedBorder(event) { function rcChangedBorder(event) {
rcUpdateBorders(); rcUpdateBorders();
document.getElementById("rc-width").value = parseInt(layers[0].canvasSize[0]) + borders.left + borders.right; document.getElementById("rc-width").value = parseInt(layers[0].canvasSize[0]) + rcBorders.left + rcBorders.right;
document.getElementById("rc-height").value = parseInt(layers[0].canvasSize[1]) + borders.top + borders.bottom; document.getElementById("rc-height").value = parseInt(layers[0].canvasSize[1]) + rcBorders.top + rcBorders.bottom;
} }
/** Fired when width or height are changed: updates the border offsets
*
* @param {*} event
*/
function rcChangedSize(event) { function rcChangedSize(event) {
let widthOffset = Math.abs(document.getElementById("rc-width").value) - layers[0].canvasSize[0]; let widthOffset = Math.abs(document.getElementById("rc-width").value) - layers[0].canvasSize[0];
let heightOffset = Math.abs(document.getElementById("rc-height").value) - layers[0].canvasSize[1]; let heightOffset = Math.abs(document.getElementById("rc-height").value) - layers[0].canvasSize[1];
@ -54,12 +77,19 @@ function rcChangedSize(event) {
document.getElementById("rc-border-top").value = top; document.getElementById("rc-border-top").value = top;
document.getElementById("rc-border-bottom").value = bottom; document.getElementById("rc-border-bottom").value = bottom;
borders.left = left; rcBorders.left = left;
borders.right = right; rcBorders.right = right;
borders.top = top; rcBorders.top = top;
borders.bottom = bottom; rcBorders.bottom = bottom;
} }
/** Resizes the canvas
*
* @param {*} event The event that triggered the canvas resizing
* @param {*} size The new size of the picture
* @param {*} customData Used when ctrl+z ing
* @param {*} saveHistory Should I save the history? You shouldn't if you're undoing
*/
function resizeCanvas(event, size, customData, saveHistory = true) { function resizeCanvas(event, size, customData, saveHistory = true) {
let imageDatas = []; let imageDatas = [];
let leftOffset = 0; let leftOffset = 0;
@ -87,8 +117,8 @@ function resizeCanvas(event, size, customData, saveHistory = true) {
if (saveHistory) { if (saveHistory) {
// Saving history // Saving history
new HistoryStateResizeCanvas( new HistoryStateResizeCanvas(
{x: parseInt(layers[0].canvasSize[0]) + borders.left + borders.right, {x: parseInt(layers[0].canvasSize[0]) + rcBorders.left + rcBorders.right,
y: parseInt(layers[0].canvasSize[1]) + borders.top + borders.bottom}, y: parseInt(layers[0].canvasSize[1]) + rcBorders.top + rcBorders.bottom},
{x: layers[0].canvasSize[0], {x: layers[0].canvasSize[0],
y: layers[0].canvasSize[1]}, y: layers[0].canvasSize[1]},
@ -100,8 +130,8 @@ function resizeCanvas(event, size, customData, saveHistory = true) {
// Resize the canvases // Resize the canvases
for (let i=0; i<layers.length; i++) { for (let i=0; i<layers.length; i++) {
layers[i].canvasSize[0] = parseInt(layers[i].canvasSize[0]) + borders.left + borders.right; layers[i].canvasSize[0] = parseInt(layers[i].canvasSize[0]) + rcBorders.left + rcBorders.right;
layers[i].canvasSize[1] = parseInt(layers[i].canvasSize[1]) + borders.top + borders.bottom; layers[i].canvasSize[1] = parseInt(layers[i].canvasSize[1]) + rcBorders.top + rcBorders.bottom;
layers[i].canvas.width = layers[i].canvasSize[0]; layers[i].canvas.width = layers[i].canvasSize[0];
layers[i].canvas.height = layers[i].canvasSize[1]; layers[i].canvas.height = layers[i].canvasSize[1];
@ -121,42 +151,43 @@ function resizeCanvas(event, size, customData, saveHistory = true) {
topOffset = 0; topOffset = 0;
break; break;
case 'top': case 'top':
leftOffset = (borders.left + borders.right) / 2; leftOffset = (rcBorders.left + rcBorders.right) / 2;
topOffset = 0; topOffset = 0;
break; break;
case 'topright': case 'topright':
leftOffset = borders.left + borders.right; leftOffset = rcBorders.left + rcBorders.right;
topOffset = 0; topOffset = 0;
break; break;
case 'left': case 'left':
leftOffset = 0; leftOffset = 0;
topOffset = (borders.top + borders.bottom) / 2; topOffset = (rcBorders.top + rcBorders.bottom) / 2;
break; break;
case 'middle': case 'middle':
leftOffset = (borders.left + borders.right) / 2; leftOffset = (rcBorders.left + rcBorders.right) / 2;
topOffset = (borders.top + borders.bottom) / 2; topOffset = (rcBorders.top + rcBorders.bottom) / 2;
break; break;
case 'right': case 'right':
leftOffset = borders.left + borders.right; leftOffset = rcBorders.left + rcBorders.right;
topOffset = (borders.top + borders.bottom) / 2; topOffset = (rcBorders.top + rcBorders.bottom) / 2;
break; break;
case 'bottomleft': case 'bottomleft':
leftOffset = 0; leftOffset = 0;
topOffset = borders.top + borders.bottom; topOffset = rcBorders.top + rcBorders.bottom;
break; break;
case 'bottom': case 'bottom':
leftOffset = (borders.left + borders.right) / 2; leftOffset = (rcBorders.left + rcBorders.right) / 2;
topOffset = borders.top + borders.bottom; topOffset = rcBorders.top + rcBorders.bottom;
break; break;
case 'bottomright': case 'bottomright':
leftOffset = borders.left + borders.right; leftOffset = rcBorders.left + rcBorders.right;
topOffset = borders.top + borders.bottom; topOffset = rcBorders.top + rcBorders.bottom;
break; break;
default: default:
console.log('Pivot does not exist, please report an issue at https://github.com/lospec/pixel-editor'); console.log('Pivot does not exist, please report an issue at https://github.com/lospec/pixel-editor');
break; break;
} }
// Putting all the data for each layer with the right offsets (decided by the pivot)
for (let i=0; i<layers.length; i++) { for (let i=0; i<layers.length; i++) {
if (layers[i].menuEntry != null) { if (layers[i].menuEntry != null) {
if (customData == undefined) { if (customData == undefined) {
@ -176,6 +207,11 @@ function resizeCanvas(event, size, customData, saveHistory = true) {
closeDialogue(); closeDialogue();
} }
/** Trims the canvas so tat the sprite is perfectly contained in it
*
* @param {*} event
* @param {*} saveHistory Should I save the history? You shouldn't if you're undoing
*/
function trimCanvas(event, saveHistory) { function trimCanvas(event, saveHistory) {
let minY = Infinity; let minY = Infinity;
let minX = Infinity; let minX = Infinity;
@ -189,6 +225,7 @@ function trimCanvas(event, saveHistory) {
rcPivot = "topleft"; rcPivot = "topleft";
console.log("debug"); console.log("debug");
// Computing the min and max coordinates in which there's a non empty pixel
for (let i=1; i<layers.length - nAppLayers; i++) { for (let i=1; i<layers.length - nAppLayers; i++) {
let imageData = layers[i].context.getImageData(0, 0, layers[0].canvasSize[0], layers[0].canvasSize[1]); let imageData = layers[i].context.getImageData(0, 0, layers[0].canvasSize[0], layers[0].canvasSize[1]);
let pixelPosition; let pixelPosition;
@ -226,10 +263,11 @@ function trimCanvas(event, saveHistory) {
minY = layers[0].canvasSize[1] - minY; minY = layers[0].canvasSize[1] - minY;
maxY = layers[0].canvasSize[1] - maxY; maxY = layers[0].canvasSize[1] - maxY;
borders.right = (maxX - layers[0].canvasSize[0]) + 1; // Setting the borders coherently with the values I've just computed
borders.left = -minX; rcBorders.right = (maxX - layers[0].canvasSize[0]) + 1;
borders.top = maxY - layers[0].canvasSize[1] + 1; rcBorders.left = -minX;
borders.bottom = -minY; rcBorders.top = maxY - layers[0].canvasSize[1] + 1;
rcBorders.bottom = -minY;
// Saving the data // Saving the data
for (let i=0; i<layers.length; i++) { for (let i=0; i<layers.length; i++) {
@ -241,11 +279,12 @@ function trimCanvas(event, saveHistory) {
console.log(imageDatas); console.log(imageDatas);
//console.log("sx: " + borders.left + "dx: " + borders.right + "top: " + borders.top + "btm: " + borders.bottom); //console.log("sx: " + borders.left + "dx: " + borders.right + "top: " + borders.top + "btm: " + borders.bottom);
document.getElementById("rc-border-left").value = borders.left; document.getElementById("rc-border-left").value = rcBorders.left;
document.getElementById("rc-border-right").value = borders.right; document.getElementById("rc-border-right").value = rcBorders.right;
document.getElementById("rc-border-top").value = borders.top; document.getElementById("rc-border-top").value = rcBorders.top;
document.getElementById("rc-border-bottom").value = borders.bottom; document.getElementById("rc-border-bottom").value = rcBorders.bottom;
// Resizing the canvas with the decided border offsets
resizeCanvas(null, null, imageDatas.slice(), historySave); resizeCanvas(null, null, imageDatas.slice(), historySave);
// Resetting the previous pivot // Resetting the previous pivot
rcPivot = prevPivot; rcPivot = prevPivot;
@ -253,16 +292,16 @@ function trimCanvas(event, saveHistory) {
function rcUpdateBorders() { function rcUpdateBorders() {
// Getting input // Getting input
borders.left = document.getElementById("rc-border-left").value; rcBorders.left = document.getElementById("rc-border-left").value;
borders.right = document.getElementById("rc-border-right").value; rcBorders.right = document.getElementById("rc-border-right").value;
borders.top = document.getElementById("rc-border-top").value; rcBorders.top = document.getElementById("rc-border-top").value;
borders.bottom = document.getElementById("rc-border-bottom").value; rcBorders.bottom = document.getElementById("rc-border-bottom").value;
// Validating input // Validating input
borders.left == "" ? borders.left = 0 : borders.left = Math.round(parseInt(borders.left)); rcBorders.left == "" ? rcBorders.left = 0 : rcBorders.left = Math.round(parseInt(rcBorders.left));
borders.right == "" ? borders.right = 0 : borders.right = Math.round(parseInt(borders.right)); rcBorders.right == "" ? rcBorders.right = 0 : rcBorders.right = Math.round(parseInt(rcBorders.right));
borders.top == "" ? borders.top = 0 : borders.top = Math.round(parseInt(borders.top)); rcBorders.top == "" ? rcBorders.top = 0 : rcBorders.top = Math.round(parseInt(rcBorders.top));
borders.bottom == "" ? borders.bottom = 0 : borders.bottom = Math.round(parseInt(borders.bottom)); rcBorders.bottom == "" ? rcBorders.bottom = 0 : rcBorders.bottom = Math.round(parseInt(rcBorders.bottom));
} }
function changePivot(event) { function changePivot(event) {

View File

@ -1,15 +1,26 @@
/* This scripts contains all the code used to handle the sprite scaling */
// Should I keep the sprite ratio?
let keepRatio = true; let keepRatio = true;
// Used to store the current ratio
let currentRatio; let currentRatio;
// The currenty selected resizing algorithm (nearest-neighbor or bilinear-interpolation)
let currentAlgo = 'nearest-neighbor'; let currentAlgo = 'nearest-neighbor';
// Current resize data
let data = {width: 0, height: 0, widthPercentage: 100, heightPercentage: 100}; let data = {width: 0, height: 0, widthPercentage: 100, heightPercentage: 100};
// Start resize data
let startData = {width: 0, height:0, widthPercentage: 100, heightPercentage: 100}; let startData = {width: 0, height:0, widthPercentage: 100, heightPercentage: 100};
/** Opens the sprite resizing window
*
*/
function openResizeSpriteWindow() { function openResizeSpriteWindow() {
// Inits the sprie resize inputs
initResizeSpriteInputs(); initResizeSpriteInputs();
// Computing the current ratio
currentRatio = layers[0].canvasSize[0] / layers[0].canvasSize[1]; currentRatio = layers[0].canvasSize[0] / layers[0].canvasSize[1];
// Initializing the input fields
data.width = layers[0].canvasSize[0]; data.width = layers[0].canvasSize[0];
data.height = layers[1].canvasSize[1]; data.height = layers[1].canvasSize[1];
@ -18,9 +29,13 @@ function openResizeSpriteWindow() {
startData.heightPercentage = 100; startData.heightPercentage = 100;
startData.widthPercentage = 100; startData.widthPercentage = 100;
// Opening the pop up now that it's ready
showDialogue('resize-sprite'); showDialogue('resize-sprite');
} }
/** Initalizes the input values and binds the elements to their events
*
*/
function initResizeSpriteInputs() { function initResizeSpriteInputs() {
document.getElementById("rs-width").value = layers[0].canvasSize[0]; document.getElementById("rs-width").value = layers[0].canvasSize[0];
document.getElementById("rs-height").value = layers[0].canvasSize[1]; document.getElementById("rs-height").value = layers[0].canvasSize[1];
@ -40,11 +55,21 @@ function initResizeSpriteInputs() {
document.getElementById("resize-algorithm-combobox").addEventListener("change", changedAlgorithm); document.getElementById("resize-algorithm-combobox").addEventListener("change", changedAlgorithm);
} }
/** Resizes (scales) the sprite
*
* @param {*} event
* @param {*} ratio Keeps infos about the x ratio and y ratio
*/
function resizeSprite(event, ratio) { function resizeSprite(event, ratio) {
// Old data
let oldWidth, oldHeight; let oldWidth, oldHeight;
// New data
let newWidth, newHeight; let newWidth, newHeight;
// Current imageDatas
let rsImageDatas = []; let rsImageDatas = [];
// Index that will be used a few lines below
let layerIndex = 0; let layerIndex = 0;
// Copy of the imageDatas that will be stored in the history
let imageDatasCopy = []; let imageDatasCopy = [];
oldWidth = layers[0].canvasSize[0]; oldWidth = layers[0].canvasSize[0];
@ -70,6 +95,7 @@ function resizeSprite(event, ratio) {
break; break;
} }
// Computing newWidth and newHeight
if (ratio == null) { if (ratio == null) {
newWidth = data.width; newWidth = data.width;
newHeight = data.height; newHeight = data.height;
@ -88,8 +114,11 @@ function resizeSprite(event, ratio) {
} }
} }
// ratio is null when the user is undoing
if (ratio == null) { if (ratio == null) {
// Copying the image data
imageDatasCopy = rsImageDatas.slice(); imageDatasCopy = rsImageDatas.slice();
// Saving the history
new HistoryStateResizeSprite(newWidth / oldWidth, newHeight / oldHeight, currentAlgo, imageDatasCopy); new HistoryStateResizeSprite(newWidth / oldWidth, newHeight / oldHeight, currentAlgo, imageDatasCopy);
} }
@ -124,6 +153,12 @@ function resizeSprite(event, ratio) {
closeDialogue(); closeDialogue();
} }
/* Trust me, the math for the functions below works. If you want to optimize them feel free to have a look, though */
/** Fired when the input field for width is changed. Updates th othe input fields consequently
*
* @param {*} event
*/
function changedWidth(event) { function changedWidth(event) {
let oldValue = data.width; let oldValue = data.width;
let ratio; let ratio;
@ -150,6 +185,10 @@ function changedWidth(event) {
document.getElementById("rs-width-percentage").value = newWidthPerc; document.getElementById("rs-width-percentage").value = newWidthPerc;
} }
/**Fired when the input field for width is changed. Updates the other input fields consequently
*
* @param {*} event
*/
function changedHeight(event) { function changedHeight(event) {
let oldValue = 100; let oldValue = 100;
let ratio; let ratio;
@ -176,6 +215,10 @@ function changedHeight(event) {
data.heightPercentage = newHeightPerc; data.heightPercentage = newHeightPerc;
} }
/**Fired when the input field for width percentage is changed. Updates the other input fields consequently
*
* @param {*} event
*/
function changedWidthPercentage(event) { function changedWidthPercentage(event) {
let oldValue = 100; let oldValue = 100;
let ratio; let ratio;
@ -204,6 +247,10 @@ function changedWidthPercentage(event) {
data.width = newWidth; data.width = newWidth;
} }
/**Fired when the input field for height percentage is changed. Updates the other input fields consequently
*
* @param {*} event
*/
function changedHeightPercentage(event) { function changedHeightPercentage(event) {
let oldValue = data.heightPercentage; let oldValue = data.heightPercentage;
let ratio; let ratio;
@ -230,10 +277,18 @@ function changedHeightPercentage(event) {
data.height = newHeight; data.height = newHeight;
} }
/** Toggles the keepRatio value (fired by the checkbox in the pop up window)
*
* @param {*} event
*/
function toggleRatio(event) { function toggleRatio(event) {
keepRatio = !keepRatio; keepRatio = !keepRatio;
} }
/** Changes the scaling algorithm (fired by the combobox in the pop up window)
*
* @param {*} event
*/
function changedAlgorithm(event) { function changedAlgorithm(event) {
currentAlgo = event.target.value; currentAlgo = event.target.value;
} }

View File

@ -26,8 +26,9 @@ else{
console.log(settings); console.log(settings);
//on clicking the save button in the settings dialog //on clicking the save button in the settings dialog
on('click', 'save-settings', function (){ on('click', 'save-settings', saveSettings);
function saveSettings() {
//check if values are valid //check if values are valid
if (isNaN(getValue('setting-numberOfHistoryStates'))) { if (isNaN(getValue('setting-numberOfHistoryStates'))) {
alert('Invalid value for numberOfHistoryStates'); alert('Invalid value for numberOfHistoryStates');
@ -46,4 +47,4 @@ on('click', 'save-settings', function (){
//close window //close window
closeDialogue(); closeDialogue();
}); }

View File

@ -75,33 +75,24 @@ on('click',"eyedropper-button", function(){
tool.eyedropper.switchTo(); tool.eyedropper.switchTo();
}, false); }, false);
//zoom tool button
on('click',"zoom-button", function(){
tool.zoom.switchTo();
}, false);
//zoom in button
on('click','zoom-in-button', function(){
//changeZoom('in',[window.innerWidth/2-canvas.offsetLeft,window.innerHeight/2-canvas.offsetTop]);
changeZoom(layers[0],'in', [canvasSize[0] * zoom / 2, canvasSize[1] * zoom / 2]);
for (let i=1; i<layers.length; i++) {
layers[i].copyData(layers[0]);
}
}, false);
//zoom out button
on('click','zoom-out-button', function(){
changeZoom(layers[0],'out',[canvasSize[0]*zoom/2,canvasSize[1]*zoom/2]);
for (let i=1; i<layers.length; i++) {
layers[i].copyData(layers[0]);
}
}, false);
//rectangular selection button //rectangular selection button
on('click', "rectselect-button", function(){ on('click', "rectselect-button", function(){
tool.rectselect.switchTo(); tool.rectselect.switchTo();
}, false); }, false);
//line
on('click',"line-button", function(){
tool.line.switchTo();
}, false);
on('click',"line-bigger-button", function(){
tool.line.brushSize++;
}, false);
on('click',"line-smaller-button", function(){
if(tool.line.brushSize > 1)
tool.line.brushSize--;
}, false);
/*global on */ /*global on */

View File

@ -5,7 +5,7 @@ var dragging = false;
var lastMouseClickPos = [0,0]; var lastMouseClickPos = [0,0];
var dialogueOpen = false; var dialogueOpen = false;
var documentCreated = false; var documentCreated = false;
var pixelEditorMode; var pixelEditorMode = "Advanced";
//common elements //common elements
var brushPreview = document.getElementById("brush-preview"); var brushPreview = document.getElementById("brush-preview");

View File

@ -12,6 +12,8 @@
//=include utilities/hslToRgb.js //=include utilities/hslToRgb.js
//=include libraries/cookies.js //=include libraries/cookies.js
//=include _pixelEditorUtility.js //=include _pixelEditorUtility.js
//=include sortable.js
//=include _algorithms.js
/**init**/ /**init**/
//=include _consts.js //=include _consts.js
@ -34,10 +36,12 @@
//=include _colorChanged.js //=include _colorChanged.js
//=include _initColor.js //=include _initColor.js
//=include _dialogue.js //=include _dialogue.js
//=include _featuresLog.js
//=include _updateCursor.js //=include _updateCursor.js
//=include _drawLine.js //=include _drawLine.js
//=include _getCursorPosition.js //=include _getCursorPosition.js
//=include _fill.js //=include _fill.js
//=include _line.js
//=include _history.js //=include _history.js
//=include _deleteColor.js //=include _deleteColor.js
//=include _replaceAllOfColor.js //=include _replaceAllOfColor.js
@ -47,6 +51,8 @@
//=include _copyPaste.js //=include _copyPaste.js
//=include _resizeCanvas.js //=include _resizeCanvas.js
//=include _resizeSprite.js //=include _resizeSprite.js
//=include _colorPicker.js
//=include _paletteBlock.js
/**load file**/ /**load file**/
//=include _loadImage.js //=include _loadImage.js

3709
js/sortable.js Normal file

File diff suppressed because it is too large Load Diff

64
js/tools/_all.js Normal file
View File

@ -0,0 +1,64 @@
new Tool('eraser', {
cursor: 'crosshair',
brushPreview: true,
});
new Tool('resizeeraser', {
cursor: 'default',
});
new Tool('eyedropper', {
imageCursor: 'eyedropper',
});
new Tool('fill', {
imageCursor: 'fill',
});
new Tool('line', {
cursor: 'crosshair',
brushPreview: true,
});
new Tool('resizeline', {
cursor: 'default',
});
new Tool('pan', {
cursor: function () {
if (dragging) return 'url(\'/pixel-editor/pan-held.png\'), auto';
else return 'url(\'/pixel-editor/pan.png\'), auto';
},
});
new Tool('pencil', {
cursor: 'crosshair',
brushPreview: true,
});
new Tool('resizebrush', {
cursor: 'default',
});
new Tool('rectangle', {
cursor: 'crosshair',
brushPreview: true,
});
new Tool('resizerectangle', {
cursor: 'default',
});
new Tool('rectselect', {
cursor: 'crosshair',
brushPreview: true,
});
new Tool('moveselection', {
cursor: 'crosshair',
});
new Tool('zoom', {
imageCursor: 'zoom-in',
});
//set a default tool
var currentTool = tool.pencil;
var currentToolTemp = tool.pencil;

View File

@ -1,13 +0,0 @@
new Tool('eraser', {
cursor: 'crosshair',
brushPreview: true,
});
new Tool('resizeeraser', {
cursor: 'default',
});
/*global Tool, tool*/

View File

@ -1,7 +0,0 @@
new Tool('eyedropper', {
imageCursor: 'eyedropper',
});
/*global Tool, tool*/

View File

@ -1,7 +0,0 @@
new Tool('fill', {
imageCursor: 'fill',
});
/*global Tool, tool*/

View File

@ -1,10 +0,0 @@
new Tool('pan', {
cursor: function () {
if (dragging) return 'url(\'/pixel-editor/pan-held.png\'), auto';
else return 'url(\'/pixel-editor/pan.png\'), auto';
},
});
/*global Tool, tool*/

View File

@ -1,17 +0,0 @@
new Tool('pencil', {
cursor: 'crosshair',
brushPreview: true,
});
new Tool('resizebrush', {
cursor: 'default',
});
//set as default tool
var currentTool = tool.pencil;
var currentToolTemp = tool.pencil;
/*global Tool, tool*/

View File

@ -1,14 +0,0 @@
new Tool('rectangle', {
cursor: 'crosshair',
brushPreview: true,
});
new Tool('resizerectangle', {
cursor: 'default',
});
/*global Tool, tool*/

View File

@ -1,14 +0,0 @@
new Tool('rectselect', {
cursor: 'crosshair',
brushPreview: true,
});
new Tool('moveselection', {
cursor: 'crosshair',
});
/*global Tool, tool*/

View File

@ -1,6 +0,0 @@
new Tool('zoom', {
imageCursor: 'zoom-in',
});
/*global Tool, tool*/

147
package-lock.json generated
View File

@ -11556,7 +11556,14 @@
"resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.1.0.tgz", "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.1.0.tgz",
"integrity": "sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ==", "integrity": "sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ==",
"requires": { "requires": {
"ini": "1.3.7" "ini": "1.3.7
},
"dependencies": {
"ini": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz",
"integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ=="
}
} }
}, },
"global-modules": { "global-modules": {
@ -12265,6 +12272,11 @@
"resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz",
"integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==" "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw=="
}, },
"is-yarn-global": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz",
"integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw=="
},
"isarray": { "isarray": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
@ -12996,6 +13008,139 @@
} }
} }
}, },
"nodemon": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.7.tgz",
"integrity": "sha512-XHzK69Awgnec9UzHr1kc8EomQh4sjTQ8oRf8TsGrSmHDx9/UmiGG9E/mM3BuTfNeFwdNBvrqQq/RHL0xIeyFOA==",
"requires": {
"chokidar": "^3.2.2",
"debug": "^3.2.6",
"ignore-by-default": "^1.0.1",
"minimatch": "^3.0.4",
"pstree.remy": "^1.1.7",
"semver": "^5.7.1",
"supports-color": "^5.5.0",
"touch": "^3.1.0",
"undefsafe": "^2.0.3",
"update-notifier": "^4.1.0"
},
"dependencies": {
"anymatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
"integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
"requires": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
}
},
"binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA=="
},
"braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"requires": {
"fill-range": "^7.0.1"
}
},
"chokidar": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz",
"integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==",
"requires": {
"anymatch": "~3.1.1",
"braces": "~3.0.2",
"fsevents": "~2.3.1",
"glob-parent": "~5.1.0",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.5.0"
}
},
"debug": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"requires": {
"ms": "^2.1.1"
}
},
"fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"requires": {
"to-regex-range": "^5.0.1"
}
},
"fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"optional": true
},
"glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"requires": {
"is-glob": "^4.0.1"
}
},
"is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"requires": {
"binary-extensions": "^2.0.0"
}
},
"is-glob": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
"integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
"requires": {
"is-extglob": "^2.1.1"
}
},
"is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
},
"ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
},
"readdirp": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz",
"integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==",
"requires": {
"picomatch": "^2.2.1"
}
},
"to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"requires": {
"is-number": "^7.0.0"
}
}
}
},
"nopt": { "nopt": {
"version": "3.0.6", "version": "3.0.6",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",

View File

@ -10,13 +10,14 @@ const PORT = process.argv[3] || 3000;
app.get('/', (req, res) => { app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, BUILDDIR, 'index.htm'), {}, function (err) { res.sendFile(path.join(__dirname, BUILDDIR, 'index.htm'), {}, function (err) {
if(err){ if(err){
console.log('error sending file',err); console.log('error sending file', err);
} else { } else {
console.log("Server: Successfully served index.html");
setTimeout(()=>{ setTimeout(()=>{
console.log('closing server'); console.log('closing server');
res.app.server.close(); res.app.server.close();
process.exit(); process.exit();
},1000*10); },1000*10);
} }
}); });
}); });

View File

@ -13,7 +13,6 @@
</head> </head>
<body oncontextmenu="return false;"> <body oncontextmenu="return false;">
<div id="compatibility-warning"> <div id="compatibility-warning">
<div><div> <div><div>
<p><strong>Warning: a modern, desktop, web browser is required to use this tool.</strong></p> <p><strong>Warning: a modern, desktop, web browser is required to use this tool.</strong></p>
@ -91,6 +90,7 @@
<button>Editor</button> <button>Editor</button>
<ul> <ul>
<li><button id="switch-mode-button">Switch to basic mode</button></li> <li><button id="switch-mode-button">Switch to basic mode</button></li>
<li><button onclick="showDialogue('splash', false)">Return to splash page</button></li>
<li><button>Settings</button></li> <li><button>Settings</button></li>
</ul> </ul>
</li> </li>
@ -130,29 +130,23 @@
<li><button title="Eyedropper Tool (E)" id="eyedropper-button">{{svg "eyedropper.svg" width="32" height="32"}}</button></li> <li><button title="Eyedropper Tool (E)" id="eyedropper-button">{{svg "eyedropper.svg" width="32" height="32"}}</button></li>
<li><button title="Pan Tool (P)" id="pan-button">{{svg "pan.svg" width="32" height="32"}}</button></li> <li><button title="Pan Tool (P)" id="pan-button">{{svg "pan.svg" width="32" height="32"}}</button></li>
<li class="expanded"> <li class="expanded">
<button title="Zoom Tool (Z)" id="zoom-button">{{svg "zoom.svg" width="32" height="32"}}</button> <button title="Line Tool (L)" id="line-button">{{svg "line.svg" width="32" height="32"}}</button>
<button title="Zoom In" id="zoom-in-button" class="tools-menu-sub-button">{{svg "plus.svg" width="12" height="12"}}</button> <button title="Increase Line Size" id="line-bigger-button" class="tools-menu-sub-button">{{svg "plus.svg" width="12" height="12"}}</button>
<button title="Zoom Out" id="zoom-out-button" class="tools-menu-sub-button">{{svg "minus.svg" width="12" height="12"}}</button> <button title="Decrease Line Size" id="line-smaller-button" class="tools-menu-sub-button">{{svg "minus.svg" width="12" height="12"}}</button>
</li> </li>
<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">
{{!
<li class="noshrink"><button id="current-color" class="jscolor {valueElement: 'current-color-value', styleElement: 'current-color-preview', onFineChange:'setColor(this)', width:151, position: 'left', padding:0,
borderWidth:14, borderColor: '#332f35',backgroundColor: '#332f35', insetColor:'transparent'}"><div id="current-color-preview"></div></button><input id="current-color-value" class="color-value" value="#000000" autocomplete="off" /></li>
}}
<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 --> <!-- LAYER MENU -->
<ul id = "layers-menu"> <ul id = "layers-menu">
<li class = "layers-menu-entry selected-layer" draggable = "true"> <li class = "layers-menu-entry selected-layer">
<canvas class = "preview-canvas"></canvas> <canvas class = "preview-canvas"></canvas>
<ul class="layer-buttons"> <ul class="layer-buttons">
<li class = "layer-button"> <li class = "layer-button">
@ -232,9 +226,9 @@
{{svg "adjust.svg" width="20" height="20" }} {{svg "adjust.svg" width="20" height="20" }}
</div> </div>
<div id="pop-up-container" id = "new-pixel-container"> <div id="pop-up-container">
<!-- NEW PIXEL --> <!-- NEW PIXEL -->
<div id="new-pixel"> <div id="new-pixel" class="update">
<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>
@ -262,8 +256,78 @@
</div> </div>
</div> </div>
<!-- Splash page -->
<div id = "splash">
<div class="splash-news">
<div id="latest-update">
<h1>Latest updates</h1>
</div>
</div>
<div id="splash-input">
<div class="splash-menu">
<div id="editor-logo">
<div id="black">
<div id="sp-coverdata">
<img src="https://lospec.com/brand/lospec_logo_3x.png"/> pixel editor
<p>Version 1.4.0</p>
<a href="https://cdn.discordapp.com/attachments/506277390050131978/795660870221955082/final.png">Art by Unsettled</a>
</div>
</div>
</div>
<div id="sp-newpixel">
<h1>New Custom Pixel</h1>
<!-- Editor mode-->
<h2>Editor mode</h2>
<div class="sp-np-entry" id="sp-mode-palette">
<div class="button-menu">
<div class="bm-left" onclick="splashMode(event,'Basic')"><p>Basic</p></div>
<div class="sp-interface-selected bm-right" onclick="splashMode(event,'Advanced')"><p>Advanced</p></div>
</div>
</div>
<h2>Size</h2>
<div class="sp-np-entry">
<input id="size-width-splash" value="{{#if width}}{{width}}{{else}}64{{/if}}" autocomplete="off" />{{svg "x.svg" width="16" height="16" class="dimentions-x"}}<input id="size-height-splash" value="{{#if height}}{{height}}{{else}}64{{/if}}" autocomplete="off" />
</div>
<h2>Palette</h2>
<button id="palette-button-splash" class="dropdown-button">Choose a palette...</button>
<div id="palette-menu-splash" class="dropdown-menu"><button id="load-palette-button-splash">Load palette...</button></div>
<div id="new-pixel-warning">Creating a new pixel will discard your current one.</div>
<div class="sp-np-entry">
<button id="create-button-splash" class="default">Create</button>
</div>
</div>
<div id = "sp-quickstart-container">
<div id="sp-quickstart-title">
Quickstart
</div>
<div id="sp-quickstart">
<div class="sp-template" onclick="document.getElementById('open-image-browse-holder').click()"><p>Load</p></div>
<div class="sp-template" onclick="newFromTemplate('Gameboy Color')"><p><span>New</span> Gameboy</p></div>
<div class="sp-template" onclick="newFromTemplate('Commodore 64')"><p><span>New</span> C64</p></div>
<div class="sp-template" onclick="newFromTemplate('PICO-8')"><p><span>New</span> Pico8</p></div>
<div class="sp-template" onclick="newFromTemplate('',16,16)"><p><span>New</span> 16x16</p></div>
<div class="sp-template" onclick="newFromTemplate('',32,32)"><p><span>New</span> 32x32</p></div>
<div class="sp-template" onclick="newFromTemplate('',64,64)"><p><span>New</span> 64x64</p></div>
<div class="sp-template" onclick="newFromTemplate('',128,128)"><p><span>New</span> 128x128</p></div>
<div class="sp-template" onclick="newFromTemplate('',256,256)"><p><span>New</span> 256x256</p></div>
<div class="sp-template" onclick="newFromTemplate('',512,512)"><p><span>New</span> 512x512</p></div>
</div>
</div>
</div>
</div>
</div>
<!--SPRITE RESIZE--> <!--SPRITE RESIZE-->
<div id = "resize-sprite"> <div class="update" id = "resize-sprite">
<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>Scale sprite</h1> <h1>Scale sprite</h1>
<!-- SIZE--> <!-- SIZE-->
@ -313,7 +377,7 @@
</div> </div>
<!--CANVAS RESIZE--> <!--CANVAS RESIZE-->
<div id = "resize-canvas"> <div class="update" id = "resize-canvas">
<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>Resize canvas</h1> <h1>Resize canvas</h1>
@ -371,6 +435,82 @@
<button id = "resize-canvas-confirm">Resize canvas</button> <button id = "resize-canvas-confirm">Resize canvas</button>
</span> </span>
</div> </div>
<!-- PALETTE -->
<div id = "palette-block">
<button class="close-button">{{svg "x.svg" width="20" height="20"}}</button>
<h1>Edit palette</h1>
<div id = "colour-picker">
<div id = "cp-modes">
<button id="cp-rgb" class="cp-selected-mode" onclick="changePickerMode(this, 'rgb')">RGB</button>
<button id="cp-hsv" onclick="changePickerMode(this, 'hsv')">HSV</button>
<button id="cp-hsl" onclick="changePickerMode(this, 'hsl')">HSL</button>
<div id="cp-colour-preview" class="cp-colour-preview"></div>
<input id="cp-hex" type="text" value="#123456" onchange="hexUpdated()"/>
</div>
<div id = "sliders-container">
<div class = "cp-slider-entry">
<label for = "first-slider">R</label>
<input type="range" min="0" max="255" class="colour-picker-slider" id="first-slider" onmousemove="updateSliderValue(1)" onclick="updateSliderValue(1)"/>
<input type = "text" value = "128" onchange="inputChanged(this,1)"/>
</div>
<div class = "cp-slider-entry">
<label for = "second-slider">G</label>
<input type="range" min="0" max ="255" class="colour-picker-slider" id="second-slider" onmousemove="updateSliderValue(2)" onclick="updateSliderValue(2)"/>
<input type = "text" value = "128" onchange="inputChanged(this,2)"/>
</div>
<div class = "cp-slider-entry">
<label for = "third-slider">B</label>
<input type="range" min = "0" max = "255" class = "colour-picker-slider" id = "third-slider" onmousemove="updateSliderValue(3)" onclick="updateSliderValue(3)"/>
<input type = "text" value = "128" onchange="inputChanged(this,3)"/>
</div>
</div>
<div id = "cp-minipicker">
<input type = "range" min = "0" max = "100" id = "cp-minipicker-slider" onmousemove="miniSliderInput(event)" onclick="miniSliderInput(event)"/>
<div id="cp-canvas-container" onmousemove="movePickerIcon(event)">
<canvas id = "cp-spectrum"></canvas>
<div id="cp-active-icon" class="cp-picker-icon"></div>
</div>
<div id = "cp-colours-previews">
<div class = "cp-colour-preview">
#123456
</div>
</div>
<div id = "cp-colour-picking-modes">
<button class="cp-selected-mode" onclick="changePickingMode(event,'mono')">Mono</button>
<button onclick="changePickingMode(event,'analog')">Nlgs</button>
<button onclick="changePickingMode(event,'cmpt')">Cmpt</button>
<button onclick="changePickingMode(event,'tri')">Tri</button>
<button onclick="changePickingMode(event,'scmpt')">Scm</button>
<button onclick="changePickingMode(event,'tetra')">Tetra</button>
</div>
</div>
</div>
<div id = "palette-container">
<ul id = "palette-list">
<li style = "background-color:rgb(255,0,0);width:40px;height:40px;" onmousedown="startRampSelection(event)"
onmousemove="updateRampSelection(event)" onmouseup="endRampSelection(event)"></li>
<li style = "background-color:rgb(0,255,0);width:40px;height:40px;"onmousedown="startRampSelection(event)"
onmousemove="updateRampSelection(event)" onmouseup="endRampSelection(event)"></li>
</ul>
</div>
<div id="pb-options">
<button title="Add colours to palette" onclick="pbAddColours()">Add colours</button>
<button title="Remove colours from palette" onclick="pbRemoveColours()">Remove colours</button>
</div>
</div>
<div id="help"> <div id="help">
<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>Help</h1> <h1>Help</h1>