Merge pull request #68 from unsettledgames/master

Code refactor
This commit is contained in:
Lospec 2021-12-26 15:13:23 -05:00 committed by GitHub
commit 8ee0214fad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
133 changed files with 6920 additions and 7324 deletions

View File

@ -4,39 +4,41 @@ This is a browser based software for creating pixel art
The tool can be viewed online here: https://lospec.com/pixel-editor
## How to contribute
Please do not submit pull requests with new features or core changes. Instead, please file an issue first for discussion.
## What to Contribute
Any changes that fix bugs or add features are welcome.
Any changes that fix bugs or add features are welcome. Check out the issues if you don't know where to start: if
you're new to the editor, we suggest you check out the Wiki first.
The next version is mostly focused on adding missing essential features and porting to mobile.
Suggestions / Planned features:
- Documentation
- Possibility to hide and resize menus (layers, palette)
- Line tool
- Tiled mode
- Load palette from LPE file
- Symmetry options
- Symmetry options (currently being worked on)
- Make a palette grid instead of having a huge stack on the right when colours are too many
- Possibly add collaborate function
- Mobile
- Touch equivalent for mouse clicks
- Hide or scale ui
- Maybe rearrange UI on portrait
- Stack colors when too many
- Fix popups
- Possibly add collaborate function
- Polish:
- ctrl a to select everything / selection -> all, same for deselection
- CTRL+A to select everything / selection -> all, same for deselection
- Warning windows for wrong inputs
- Palette option remove unused colors
- Move selection with arrows
- 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)
- Scale selection
- Scale / rotate selection
## How to Contribute

View File

@ -21,6 +21,8 @@ function copy_images(){
gulp.src('./images/Splash images/*.png').pipe(gulp.dest(BUILDDIR));
// Logs images
gulp.src('./images/Logs/*.gif').pipe(gulp.dest(BUILDDIR));
// Logs images
gulp.src('./images/Logs/*.png').pipe(gulp.dest(BUILDDIR));
}
function copy_logs() {
@ -49,7 +51,7 @@ function compile_page(){
.pipe(include({includePaths: ['/svg']}))
.pipe(handlebars({encoding: 'utf8', debug: true, bustCache: true})
.partials('./views/[!index]*.hbs')
.partials('./views/[!index]*.hbs').partials('./views/popups/*.hbs')
//.helpers({ svg: hb_svg })
.helpers('./helpers/**/*.js')
.data({

View File

@ -69,6 +69,8 @@
}
#brush-preview {
background-color:black;
opacity:0.3;
position: absolute;
border: solid 1px #fff;
z-index: 1200;

View File

@ -77,7 +77,7 @@
}
}
#tools-menu li button#pencil-bigger-button,
#tools-menu li button#brush-bigger-button,
#tools-menu li button#zoom-in-button,
#tools-menu li button#eraser-bigger-button,
#tools-menu li button#rectangle-bigger-button,
@ -86,7 +86,7 @@
left: 0;
}
#tools-menu li button#pencil-smaller-button,
#tools-menu li button#brush-smaller-button,
#tools-menu li button#zoom-out-button,
#tools-menu li button#eraser-smaller-button,
#tools-menu li button#rectangle-smaller-button,
@ -95,8 +95,8 @@
right: 0;
}
#tools-menu li.selected button#pencil-bigger-button,
#tools-menu li.selected button#pencil-smaller-button,
#tools-menu li.selected button#brush-bigger-button,
#tools-menu li.selected button#brush-smaller-button,
#tools-menu li.selected button#zoom-in-button,
#tools-menu li.selected button#zoom-out-button,
#tools-menu li.selected button#eraser-bigger-button,

BIN
images/Logs/grid.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

180
js/Color.js Normal file
View File

@ -0,0 +1,180 @@
// OPTIMIZABLE: add a normalize function that returns the normalized colour in the current
// format.
class Color {
constructor(fmt, v1, v2, v3, v4) {
this.fmt = fmt;
switch (fmt) {
case 'hsv':
this.hsv = {h: v1, s: v2, v: v3};
this.rgb = Color.hsvToRgb(this.hsv);
this.hsl = Color.rgbToHsl(this.rgb);
this.hex = Color.rgbToHex(this.rgb);
break;
case 'hsl':
this.hsl = {h: v1, s: v2, l: v3};
this.rgb = Color.hslToRgb(this.hsl);
this.hsv = Color.rgbToHsv(this.rgb);
this.hex = Color.rgbToHex(this.rgb);
break;
case 'rgb':
this.rgb = {r: v1, g: v2, b: v3};
this.hsl = Color.rgbToHsl(this.rgb);
this.hsv = Color.rgbToHsv(this.rgb);
this.hex = Color.rgbToHex(this.rgb);
break;
case 'hex':
this.hex = v1;
this.rgb = Color.hexToRgb(this.hex);
this.hsl = Color.rgbToHsl(this.rgb);
this.hsv = Color.rgbToHsv(this.rgb);
break;
default:
console.error("Unsupported color mode " + fmt);
break;
}
}
static hexToRgb(hex, divisor) {
//if divisor isn't set, set it to one (so it has no effect)
divisor = divisor || 1;
//split given hex code into array of 3 values
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex.trim());
return result ? {
r: parseInt(result[1], 16)/divisor,
g: parseInt(result[2], 16)/divisor,
b: parseInt(result[3], 16)/divisor
} : null;
}
static rgbToHex(rgb) {
function componentToHex (c) {
var hex = Math.round(c).toString(16);
return hex.length == 1 ? "0" + hex : hex.substring(0, 2);
}
return componentToHex(rgb.r) + componentToHex(rgb.g) + componentToHex(rgb.b);
}
static hslToRgb(hsl) {
let r, g, b;
let h = hsl.h, s = hsl.s, l = hsl.l;
h /= 360;
s /= 100;
l /= 100;
if(s == 0){
r = g = b = l; // achromatic
}else{
const 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;
}
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);
}
return {
r:Math.round(r * 255),
g:Math.round(g * 255),
b:Math.round(b * 255)
};
}
static rgbToHsl(rgb) {
let r, g, b;
r = rgb.r; g = rgb.g; b = rgb.b;
r /= 255, g /= 255, b /= 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
let hue, saturation, luminosity = (max + min) / 2;
if(max == min){
hue = saturation = 0; // achromatic
}else{
const d = max - min;
saturation = luminosity > 0.5 ? d / (2 - max - min) : d / (max + min);
switch(max){
case r: hue = (g - b) / d + (g < b ? 6 : 0); break;
case g: hue = (b - r) / d + 2; break;
case b: hue = (r - g) / d + 4; break;
}
hue /= 6;
}
return {h:hue*360, s:saturation*100, l:luminosity*100};
}
static hsvToRgb(hsv) {
let r, g, b, h, s, v;
h = hsv.h; s = hsv.s; v = hsv.v;
h /= 360;
s /= 100;
v /= 100;
const i = Math.floor(h * 6);
const f = h * 6 - i;
const p = v * (1 - s);
const q = v * (1 - f * s);
const 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: r * 255, g: g * 255, b: b * 255 };
}
static rgbToHsv(rgb) {
let r = rgb.r, g = rgb.g, b = rgb.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 * 360, s: myS * 100, v: myV * 100};
}
/** Converts a CSS colour eg rgb(x,y,z) to a hex string
*
* @param {*} rgb
*/
static 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]);
}
}

451
js/ColorModule.js Normal file
View File

@ -0,0 +1,451 @@
/** ColorModule holds the functions used to implement the basic-mode palette.
*
*/
const ColorModule = (() => {
// Array containing the colours of the current palette
let currentPalette = [];
// Reference to the HTML palette
const coloursList = document.getElementById("palette-list");
// Reference to the colours menu
const colorsMenu = document.getElementById("colors-menu");
// Binding events to callbacks
document.getElementById('jscolor-hex-input').addEventListener('change',colorChanged, false);
document.getElementById('jscolor-hex-input').addEventListener('input', colorChanged, false);
document.getElementById('add-color-button').addEventListener('click', addColorButtonEvent, false);
// Making the colours in the HTML menu sortable
new Sortable(document.getElementById("colors-menu"), {
animation:100,
filter: ".noshrink",
draggable: ".draggable-colour",
onEnd: function() {Events.simulateMouseEvent(window, "mouseup");}
});
/** Changes all of one color to another after being changed from the color picker
*
* @param {*} colorHexElement The element that has been changed
* @returns
*/
function colorChanged(colorHexElement) {
// Get old and new colors from the element
const hexElement = colorHexElement.target;
const hexElementValue = hexElement.value;
const newColor = Color.hexToRgb(hexElementValue);
const oldColor = hexElement.oldColor;
//if the color is not a valid hex color, exit this function and do nothing
const newColorHex = hexElementValue.toLowerCase();
if (/^[0-9a-f]{6}$/i.test(newColorHex) == false) return;
currentPalette.splice(currentPalette.indexOf("#" + newColor), 1);
newColor.a = 255;
//save undo state
new HistoryState().EditColor(hexElementValue.toLowerCase(), Color.rgbToHex(oldColor));
//get the currently selected color
const currentlyEditedColor = document.getElementsByClassName('jscolor-active')[0];
const duplicateColorWarning = document.getElementById('duplicate-color-warning');
//check if selected color already matches another color
colors = document.getElementsByClassName('color-button');
//loop through all colors in palette
for (let i = 0; i < colors.length; i++) {
//if generated color matches this color
if (newColorHex == colors[i].jscolor.toString()) {
//if the color isnt the one that has the picker currently open
if (!colors[i].parentElement.classList.contains('jscolor-active')) {
//console.log('%cColor is duplicate', colorCheckingStyle);
//show the duplicate color warning
duplicateColorWarning.style.visibility = 'visible';
//shake warning icon
duplicateColorWarning.classList.remove('shake');
void duplicateColorWarning.offsetWidth;
duplicateColorWarning.classList.add('shake');
//exit function without updating color
return;
}
}
}
//if the color being edited has a duplicate color warning, remove it
duplicateColorWarning.style.visibility = 'hidden';
currentlyEditedColor.firstChild.jscolor.fromString(newColorHex);
ColorModule.replaceAllOfColor(oldColor, newColor);
//set new old color to changed color
hexElement.oldColor = newColor;
currentPalette.push('#' + newColorHex);
//if this is the current color, update the drawing color
if (hexElement.colorElement.parentElement.classList.contains('selected')) {
updateCurrentColor('#' + Color.rgbToHex(newColor));
}
}
/** Callback triggered when the user clicks on a colour in the palette menu on the right
*
* @param {*} e The event that triggered the callback
*/
function clickedColor (e){
//left clicked color
if (e.which == 1) {
// remove current color selection
document.querySelector('#colors-menu li.selected')?.classList.remove('selected');
//set current color
updateCurrentColor(Color.cssToHex(e.target.style.backgroundColor));
//make color selected
e.target.parentElement.classList.add('selected');
}
//right clicked color
else if (e.which == 3) {
//hide edit color button (to prevent it from showing)
e.target.parentElement.lastChild.classList.add('hidden');
//show color picker
e.target.jscolor.show();
}
}
/** Called whenever the user presses the button used to add a new colour to the palette
*
*/
function addColorButtonEvent() {
//generate random color
const newColor = new Color("hsv", Math.floor(Math.random()*360), Math.floor(Math.random()*100), Math.floor(Math.random()*100)).hex;
//remove current color selection
document.querySelector('#colors-menu li.selected')?.classList.remove('selected');
//add new color and make it selected
let addedColor = addColor(newColor);
addedColor.classList.add('selected');
updateCurrentColor(newColor);
//add history state
new HistoryState().AddColor(addedColor.firstElementChild.jscolor.toString());
//show color picker
addedColor.firstElementChild.jscolor.show();
//hide edit button
addedColor.lastChild.classList.add('hidden');
}
/** Adds the colors that have been added through the advanced-mode color picker to the
* basic-mode palette.
*
*/
function addToSimplePalette() {
const simplePalette = document.getElementById("colors-menu");
const 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++) {
const col = coloursList.children[i].style.backgroundColor;
if (col.includes("rgb")) {
addColor(Color.cssToHex(col));
}
else {
addColor(col);
}
}
}
/** Initializes jscolor for the element passed as a parameter
*
* @param {*} colorElement The element of which we need to setup jscolor
*/
function initColor (colorElement) {
//add jscolor picker for this color
colorElement.jscolor = new jscolor(colorElement.parentElement, {
valueElement: null,
styleElement: colorElement,
width:151,
position: 'left',
padding:0,
borderWidth:14,
borderColor: '#332f35',
backgroundColor: '#332f35',
insetColor: 'transparent',
value: colorElement.style.backgroundColor,
deleteButton: true,
});
}
/** Adds a color to the palette
*
* @param {*} newColor The color to add in hex format
* @returns The HTML palette item that has been created
*/
function addColor (newColor) {
//add # at beginning if not present
if (newColor.charAt(0) != '#')
newColor = '#' + newColor;
currentPalette.push(newColor);
//create list item
const listItem = document.createElement('li');
//create button
const button = document.createElement('button');
button.classList.add('color-button');
button.style.backgroundColor = newColor;
button.addEventListener('mouseup', clickedColor);
listItem.appendChild(button);
listItem.classList.add("draggable-colour")
//insert new listItem element at the end of the colors menu (right before add button)
colorsMenu.insertBefore(listItem, colorsMenu.children[colorsMenu.children.length-1]);
//add jscolor functionality
initColor(button);
//add edit button
const editButtonTemplate = document.getElementsByClassName('color-edit-button')[0];
newEditButton = editButtonTemplate.cloneNode(true);
listItem.appendChild(newEditButton);
newEditButton.addEventListener('click', (event) => {
//hide edit button
event.target.parentElement.lastChild.classList.add('hidden');
//show jscolor picker, if basic mode is enabled
if (EditorState.getCurrentMode() == 'Basic')
event.target.parentElement.firstChild.jscolor.show();
else
Dialogue.showDialogue("palette-block", false);
});
return listItem;
}
/** Deletes a color from the palette
*
* @param {*} color A string in hex format or the HTML element corresponding to the color
* that should be removed.
*/
function deleteColor (color) {
const logStyle = 'background: #913939; color: white; padding: 5px;';
//if color is a string, then find the corresponding button
if (typeof color === 'string') {
if (color[0] === '#')
color = color.substr(1, color.length - 1);
//get all colors in palette
let colors = document.getElementsByClassName('color-button');
//loop through colors
for (var i = 0; i < colors.length; i++) {
//console.log(color,'=',colors[i].jscolor.toString());
if (color == colors[i].jscolor.toString()) {
//set color to the color button
currentPalette.splice(i, 1);
color = colors[i];
break;
}
}
//if the color wasn't found
if (typeof color === 'string') {
//exit function
return;
}
}
//hide color picker
color.jscolor.hide();
//find lightest color in palette
let colors = document.getElementsByClassName('color-button');
let lightestColor = [0,null];
for (let i = 0; i < colors.length; i++) {
//get colors lightness
let lightness = Color.rgbToHsl(colors[i].jscolor.toRgb()).l;
//if not the color we're deleting
if (colors[i] != color) {
//if lighter than the current lightest, set as the new lightest
if (lightness > lightestColor[0]) {
lightestColor[0] = lightness;
lightestColor[1] = colors[i];
}
}
}
//replace deleted color with lightest color
ColorModule.replaceAllOfColor(color.jscolor.toString(),lightestColor[1].jscolor.toString());
//if the color you are deleting is the currently selected color
if (color.parentElement.classList.contains('selected')) {
//set current color TO LIGHTEST COLOR
lightestColor[1].parentElement.classList.add('selected');
updateCurrentColor('#'+lightestColor[1].jscolor.toString());
}
//delete the element
colorsMenu.removeChild(color.parentElement);
}
/** Replaces all of a single color on the canvas with a different color
*
* @param {*} oldColor Old colour in {r,g,b} object format
* @param {*} newColor New colour in {r,g,b} object format
*/
function replaceAllOfColor (oldColor, newColor) {
//convert strings to objects if nessesary
if (typeof oldColor === 'string') oldColor = Color.hexToRgb(oldColor);
if (typeof newColor === 'string') newColor = Color.hexToRgb(newColor);
//create temporary image from canvas to search through
var tempImage = currFile.currentLayer.context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
//loop through all pixels
for (var i=0;i<tempImage.data.length;i+=4) {
//check if pixel matches old color
if(tempImage.data[i]==oldColor.r && tempImage.data[i+1]==oldColor.g && tempImage.data[i+2]==oldColor.b){
//change to new color
tempImage.data[i]=newColor.r;
tempImage.data[i+1]=newColor.g;
tempImage.data[i+2]=newColor.b;
}
}
//put temp image back onto canvas
currFile.currentLayer.context.putImageData(tempImage,0,0);
}
function getCurrentPalette() {
return currentPalette;
}
function resetPalette() {
currentPalette = [];
}
/** Creates the colour palette when starting up the editor from _newPixel.js
*
* @param {*} paletteColors The colours of the palette
*/
function createColorPalette(paletteColors) {
console.log("creating palette");
//remove current palette
while (colorsMenu.childElementCount > 1)
colorsMenu.children[0].remove();
var lightestColor = new Color("hex", '#000000');
var darkestColor = new Color("hex", '#ffffff');
// Adding all the colours in the array
for (var i = 0; i < paletteColors.length; i++) {
var newColor = new Color("hex", paletteColors[i]);
var newColorElement = ColorModule.addColor(newColor.hex);
var newColRgb = newColor.rgb;
var lightestColorRgb = lightestColor.rgb;
if (newColRgb.r + newColRgb.g + newColRgb.b > lightestColorRgb.r + lightestColorRgb.g + lightestColorRgb.b)
lightestColor = newColor;
var darkestColorRgb = darkestColor.rgb;
if (newColRgb.r + newColRgb.g + newColRgb.b < darkestColorRgb.r + darkestColorRgb.g + darkestColorRgb.b) {
//remove current color selection
document.querySelector('#colors-menu li.selected')?.classList.remove('selected');
//set as current color
newColorElement.classList.add('selected');
darkestColor = newColor;
}
}
//prepend # if not present
if (!darkestColor.hex.includes('#')) darkestColor.hex = '#' + darkestColor.hex;
//set as current color
updateCurrentColor(darkestColor.hex);
}
/** Creates the palette with the colours used in all the layers
*
*/
function createPaletteFromLayers() {
let colors = {};
let nColors = 0;
//create array out of colors object
let colorPaletteArray = [];
for (let i=0; i<currFile.layers.length; i++) {
if (currFile.layers[i].hasCanvas()) {
let imageData = currFile.layers[i].context.getImageData(
0, 0, currFile.canvasSize[0], currFile.canvasSize[1]).data;
let dataLength = imageData.length;
for (let j=0; j<dataLength; j += 4) {
if (!Util.isPixelEmpty(imageData[j])) {
let color = imageData[j]+','+imageData[j + 1]+','+imageData[j + 2];
if (!colors[color]) {
colorPaletteArray.push('#' + new Color("rgb", imageData[j], imageData[j + 1], imageData[j + 2]).hex);
colors[color] = new Color("rgb", imageData[j], imageData[j + 1], imageData[j + 2]).rgb;
nColors++;
//don't allow more than 256 colors to be added
if (nColors >= Settings.getCurrSettings().maxColorsOnImportedImage) {
alert('The image loaded seems to have more than '+Settings.getCurrSettings().maxColorsOnImportedImage+' colors.');
break;
}
}
}
}
}
}
//create palette from colors array
createColorPalette(colorPaletteArray);
console.log("Done 2");
}
function updateCurrentColor(color, refLayer) {
if (color[0] != '#')
color = '#' + color;
if (refLayer)
color = refLayer.context.fillStyle;
for (let i=0; i<currFile.layers.length - 1; i++) {
currFile.layers[i].context.fillStyle = color;
currFile.layers[i].context.strokeStyle = color;
}
}
return {
getCurrentPalette,
addColor,
deleteColor,
replaceAllOfColor,
addToSimplePalette,
resetPalette,
createColorPalette,
createPaletteFromLayers,
updateCurrentColor,
}
})();

777
js/ColorPicker.js Normal file
View File

@ -0,0 +1,777 @@
const ColorPicker = (() => {
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;
// Picker mode events
Events.on("click", "cp-rgb", changePickerMode, 'rgb');
Events.on("click", "cp-hsv", changePickerMode, 'hsv');
Events.on("click", "cp-hsl", changePickerMode, 'hsl');
// Hex-related events
Events.on("change", "cp-hex", hexUpdated);
// Slider events
Events.on("mousemove", "first-slider", updateSliderValue, 1);
Events.on("mousemove", "second-slider", updateSliderValue, 2);
Events.on("mousemove", "third-slider", updateSliderValue, 3);
Events.on("click", "first-slider", updateSliderValue, 1);
Events.on("click", "second-slider", updateSliderValue, 2);
Events.on("click", "third-slider", updateSliderValue, 3);
// Slider textbox events
Events.on("change", "cp-sliderText1", inputChanged, 1);
Events.on("change", "cp-sliderText2", inputChanged, 2);
Events.on("change", "cp-sliderText3", inputChanged, 3);
// Minipicker events
Events.on("mousemove", "cp-minipicker-slider", miniSliderInput);
Events.on("click", "cp-minipicker-slider", miniSliderInput);
Events.on("mousemove", "cp-canvas-container", movePickerIcon);
Events.on("click", "cp-mono", changePickingMode, "mono");
Events.on("click", "cp-analog", changePickingMode, "analog");
Events.on("click", "cp-cmpt", changePickingMode, "cmpt");
Events.on("click", "cp-tri", changePickingMode, "tri");
Events.on("click", "cp-scmpt", changePickingMode, "scmpt");
Events.on("click", "cp-tetra", changePickingMode, "tetra");
init();
function init() {
// 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
hexColour = new Color(currentPickerMode, sliderValues[0], sliderValues[1], sliderValues[2]);
// Update preview colour div
colourPreview.style.backgroundColor = '#' + hexColour.hex;
colourValue.value = '#' + hexColour.hex;
// 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 = Color.hsvToRgb({h:sliderValues[0], s:0, v:sliderValues[2]});
gradientMin = 'rgba(' + rgbColour.r + ',' + rgbColour.g + ',' + rgbColour.b + ',1)';
rgbColour = Color.hsvToRgb({h:sliderValues[0], s:100, v:sliderValues[2]});
gradientMax = 'rgba(' + rgbColour.r + ',' + rgbColour.g + ',' + rgbColour.b + ',1)';
break;
case 'hsl':
rgbColour = Color.hslToRgb({h:sliderValues[0], s:0, l:sliderValues[2]});
gradientMin = 'rgba(' + rgbColour.r + ',' + rgbColour.g + ',' + rgbColour.b + ',1)';
rgbColour = Color.hslToRgb({h:sliderValues[0], s:100, l:sliderValues[2]});
gradientMax = 'rgba(' + rgbColour.r + ',' + rgbColour.g + ',' + rgbColour.b + ',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 = Color.hsvToRgb({h:sliderValues[0], s:sliderValues[1], v:0});
gradientMin = 'rgba(' + rgbColour.r + ',' + rgbColour.g + ',' + rgbColour.b + ',1)';
rgbColour = Color.hsvToRgb({h:sliderValues[0], s:sliderValues[1], v:100});
gradientMax = 'rgba(' + rgbColour.r + ',' + rgbColour.g + ',' + rgbColour.b + ',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 = Color.hslToRgb({h:sliderValues[0], s:sliderValues[1], l:50});
ret += 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',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 ret = 'linear-gradient(90deg, rgba(2,0,36,1) 0%, ';
let consts = ['0%,', '16.6666%,', '33.3333%,', '50%,', '66.6666%,', '83.3333%,', '100%);'];
for (let i=0; i<consts.length; i++) {
let col = Color.hsvToRgb({h:i * 60, s:sliderValues[1], v:sliderValues[2]});
ret += 'rgba(' + col.r + ',' + col.g + ',' + col.b + ',1) ' + consts[i];
}
return ret.substr(0, ret.length - 1);
}
// Fired when the values in the labels are changed
function inputChanged(index) {
let sliderIndex = index - 1;
let target = document.getElementById("cp-sliderText" + index);
sliders[sliderIndex].getElementsByTagName("input")[0].value = target.value;
updateSliderValue(index);
}
// Updates the colour model used to pick colours
function changePickerMode(newMode) {
let maxRange;
let colArray;
let hexColour = colourValue.value.replace('#', '');
let currColor = new Color("hex", hexColour);
currentPickerMode = newMode;
document.getElementsByClassName("cp-selected-mode")[0].classList.remove("cp-selected-mode");
document.getElementById("cp-" + newMode).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:
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 = currColor.rgb;
colArray = [colArray.r, colArray.g, colArray.b];
break;
case 'hsv':
colArray = currColor.hsv;
colArray = [colArray.h, colArray.s, colArray.v];
break;
case 'hsl':
colArray = currColor.hsl;
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.buttons == 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 = new Color("hex", hex);
let mySliders = [sliders[0].getElementsByTagName("input")[0],
sliders[1].getElementsByTagName("input")[0],
sliders[2].getElementsByTagName("input")[0]];
switch (currentPickerMode) {
case 'rgb':
colour = colour.rgb;
mySliders[0].value = colour.r;
mySliders[1].value = colour.g;
mySliders[2].value = colour.b;
break;
case 'hsv':
colour = colour.hsv;
mySliders[0].value = colour.h;
mySliders[1].value = colour.s;
mySliders[2].value = colour.v;
break;
case 'hsl':
colour = colour.hsl;
mySliders[0].value = colour.h;
mySliders[1].value = colour.s;
mySliders[2].value = colour.l;
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 = new Color("hex", hex).hsv;
let xPos = miniPickerCanvas.width * hsv.h/360 - 8;
let yPos = miniPickerCanvas.height * hsv.s/100 + 8;
miniPickerSlider.value = hsv.v;
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 currColor = new Color("hex", getMiniPickerColour());
let newHsv = currColor.hsv;
let newHex;
// Adding slider value to value
newHsv = new Color("hsv", newHsv.h, newHsv.s, parseInt(event.target.value));
// Updating hex
newHex = newHsv.hex;
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 pickedColour;
pickedColour = miniPickerCanvas.getContext('2d').getImageData(currPickerIconPos[0][0] + 8,
currPickerIconPos[0][1] + 8, 1, 1).data;
return new Color("rgb", Math.round(pickedColour[0]), Math.round(pickedColour[1]), Math.round(pickedColour[2])).hex;
}
// Update the background gradient of the slider in the minipicker
function updateMiniSlider(hex) {
let rgb = Color.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%);}";
styles[1] += "input[type=range]#cp-minipicker-slider::-moz-range-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 = new Color("hex", colourValue.value).hsv;
let white = new Color("hsv", hsv.h, 0, parseInt(miniPickerSlider.value)).rgb;
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++) {
let stopHex = new Color("hsv", 60*i, 100, hsv.v);
hGrad.addColorStop(i / 6, '#' + stopHex.hex);
}
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.r +',' + white.g + ',' + white.b + ',0)');
vGrad.addColorStop(1, 'rgba(' + white.r +',' + white.g + ',' + white.b + ',1)');
ctx.fillStyle = vGrad;
ctx.fillRect(0, 0, miniPickerCanvas.width, miniPickerCanvas.height);
}
function changePickingMode(newMode, event) {
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:
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 = new Color("hex", colourValue.value).hex;
let currentColourHsv = new Color("hex", currentColorHex).hsv;
let newColourHsv;
let newColourHexes = ['', '', ''];
switch (currentPickingMode)
{
case 'mono':
break;
case 'analog':
// First colour
newColourHsv = new Color("hsv", ((currentColourHsv.h + 40) % 360), currentColourHsv.s, currentColourHsv.v);
currPickerIconPos[1][0] = miniPickerCanvas.width * newColourHsv.hsv.h/360 - 8;
currPickerIconPos[1][1] = miniPickerCanvas.height - (miniPickerCanvas.height * newColourHsv.hsv.s/100 + 8);
newColourHexes[0] = newColourHsv.hex;
// Second colour
newColourHsv = new Color("hsv", ((currentColourHsv.h + 320) % 360), currentColourHsv.s, currentColourHsv.v);
currPickerIconPos[2][0] = miniPickerCanvas.width * newColourHsv.hsv.h/360 - 8;
currPickerIconPos[2][1] = miniPickerCanvas.height - (miniPickerCanvas.height * newColourHsv.hsv.s/100 + 8);
newColourHexes[1] = newColourHsv.hex;
break;
case 'cmpt':
newColourHsv = new Color("hsv", ((currentColourHsv.h + 180) % 360), currentColourHsv.s, currentColourHsv.v);
currPickerIconPos[1][0] = miniPickerCanvas.width * newColourHsv.hsv.h/360 - 8;
currPickerIconPos[1][1] = miniPickerCanvas.height - (miniPickerCanvas.height * newColourHsv.hsv.s/100 + 8);
newColourHexes[0] = newColourHsv.hex;
break;
case 'tri':
for (let i=1; i< 3; i++) {
newColourHsv = new Color("hsv", (currentColourHsv.h + 120*i) % 360, currentColourHsv.s, currentColourHsv.v);
currPickerIconPos[i][0] = miniPickerCanvas.width * newColourHsv.hsv.h/360 - 8;
currPickerIconPos[i][1] = miniPickerCanvas.height - (miniPickerCanvas.height * newColourHsv.hsv.s/100 + 8);
newColourHexes[i - 1] = newColourHsv.hex;
}
break
case 'scmpt':
// First colour
newColourHsv = new Color("hsv", (currentColourHsv.h + 210) % 360, currentColourHsv.s, currentColourHsv.v);
currPickerIconPos[1][0] = miniPickerCanvas.width * newColourHsv.hsv.h/360 - 8;
currPickerIconPos[1][1] = miniPickerCanvas.height - (miniPickerCanvas.height * newColourHsv.hsv.s/100 + 8);
newColourHexes[0] = newColourHsv.hex;
// Second colour
newColourHsv = new Color("hsv", (currentColourHsv.h + 150) % 360, currentColourHsv.s, currentColourHsv.v);
currPickerIconPos[2][0] = miniPickerCanvas.width * newColourHsv.hsv.h/360 - 8;
currPickerIconPos[2][1] = miniPickerCanvas.height - (miniPickerCanvas.height * newColourHsv.hsv.s/100 + 8);
newColourHexes[1] = newColourHsv.hex;
break;
case 'tetra':
for (let i=1; i< 4; i++) {
newColourHsv = new Color("hsv", (currentColourHsv.h + 90*i) % 360, currentColourHsv.s, currentColourHsv.v);
currPickerIconPos[i][0] = miniPickerCanvas.width * newColourHsv.hsv.h/360 - 8;
currPickerIconPos[i][1] = miniPickerCanvas.height - (miniPickerCanvas.height * newColourHsv.hsv.s/100 + 8);
newColourHexes[i - 1] = newColourHsv.hex;
}
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);
}
}
return {
init,
getSelectedColours,
updatePickerByHex,
updateSlidersByHex,
updateMiniPickerColour
}
})();

86
js/Dialogue.js Normal file
View File

@ -0,0 +1,86 @@
/** Handles the pop up windows (NewPixel, ResizeCanvas ecc)
*
*/
const Dialogue = (() => {
let currentOpenDialogue = "";
let dialogueOpen = true;
const popUpContainer = document.getElementById("pop-up-container");
const cancelButtons = popUpContainer.getElementsByClassName('close-button');
Events.onCustom("esc-pressed", closeDialogue);
// Add click handlers for all cancel buttons
for (var i = 0; i < cancelButtons.length; i++) {
cancelButtons[i].addEventListener('click', function () {
closeDialogue();
});
}
/** Closes a dialogue window if the user clicks everywhere but in the current window
*
*/
popUpContainer.addEventListener('click', function (e) {
if (e.target == popUpContainer)
closeDialogue();
});
/** 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) {
if (typeof trackEvent === 'undefined') trackEvent = true;
// Updating currently open dialogue
currentOpenDialogue = dialogueName;
// The pop up window is open
dialogueOpen = true;
// Showing the pop up container
popUpContainer.style.display = 'block';
// Showing the window
document.getElementById(dialogueName).style.display = 'block';
// If I'm opening the palette window, I initialize the colour picker
if (dialogueName == 'palette-block' && EditorState.documentCreated()) {
ColorPicker.init();
PaletteBlock.init();
}
//track google event
if (trackEvent && typeof ga !== 'undefined')
ga('send', 'event', 'Palette Editor Dialogue', dialogueName); /*global ga*/
}
/** Closes the current dialogue by hiding the window and the pop-up-container
*
*/
function closeDialogue () {
popUpContainer.style.display = 'none';
var popups = popUpContainer.children;
for (var i = 0; i < popups.length; i++) {
popups[i].style.display = 'none';
}
dialogueOpen = false;
if (currentOpenDialogue == "palette-block") {
ColorModule.addToSimplePalette();
}
}
function isOpen() {
return dialogueOpen;
}
return {
showDialogue,
closeDialogue,
isOpen
}
})();
console.log("Dialog: " + Dialogue);

79
js/EditorState.js Normal file
View File

@ -0,0 +1,79 @@
const EditorState = (() => {
let pixelEditorMode = "Basic";
let firstFile = true;
Events.on('click', 'switch-editor-mode-splash', chooseMode);
Events.on('click', 'switch-mode-button', toggleMode);
function getCurrentMode() {
return pixelEditorMode;
}
function switchMode(newMode) {
if (!firstFile && newMode == "Basic" && !confirm('Switching to basic mode will flatten all the visible layers. Are you sure you want to continue?')) {
return;
}
//switch to advanced mode
if (newMode == 'Advanced') {
Events.emit("switchedToAdvanced");
// Hide the palette menu
document.getElementById('colors-menu').style.right = '200px'
pixelEditorMode = 'Advanced';
document.getElementById("switch-mode-button").innerHTML = 'Switch to basic mode';
}
//switch to basic mode
else {
Events.emit("switchedToBasic");
// Show the palette menu
document.getElementById('colors-menu').style.display = 'flex';
// Move the palette menu
document.getElementById('colors-menu').style.right = '0px';
pixelEditorMode = 'Basic';
document.getElementById("switch-mode-button").innerHTML = 'Switch to advanced mode';
}
}
function chooseMode() {
let prevMode = pixelEditorMode.toLowerCase();
if (pixelEditorMode === "Basic") {
pixelEditorMode = "Advanced";
}
else {
pixelEditorMode = "Basic";
}
//change splash text
document.querySelector('#sp-quickstart-container .mode-switcher').classList.remove(prevMode + '-mode');
document.querySelector('#sp-quickstart-container .mode-switcher').classList.add(pixelEditorMode.toLowerCase() + '-mode');
}
function toggleMode() {
if (pixelEditorMode == 'Advanced')
switchMode('Basic');
else
switchMode('Advanced');
}
function documentCreated() {
return !firstFile;
}
function firstPixel() {
return firstFile;
}
function created() {
firstFile = false;
}
return {
getCurrentMode,
switchMode,
documentCreated,
created,
firstPixel
}
})();

160
js/Events.js Normal file
View File

@ -0,0 +1,160 @@
const Events = (() => {
let customCallback = {};
/** 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) {
// I just copy pasted this from stack overflow lol please have mercy
let keyboardEvent = document.createEvent("KeyboardEvent");
let initMethod = typeof keyboardEvent.initKeyboardEvent !== 'undefined' ? "initKeyboardEvent" : "initKeyEvent";
keyboardEvent[initMethod](
"keydown", // event type: keydown, keyup, keypress
true, // bubbles
true, // cancelable
window, // view: should be window
ctrl, // ctrlKey
alt, // altKey
shift, // shiftKey
false, // metaKey
keyCode, // keyCode: unsigned long - the virtual key code, else 0
keyCode // charCode: unsigned long - the Unicode character associated with the depressed key, else 0
);
document.dispatchEvent(keyboardEvent);
}
/** Simulates a mouse event (https://stackoverflow.com/questions/6157929/how-to-simulate-a-mouse-click-using-javascript)
*
* @param {*} element The element that triggered the event
* @param {*} eventName The name of the event
* @returns
*/
function simulateMouseEvent (element, eventName)
{
function extend(destination, source) {
for (let property in source)
destination[property] = source[property];
return destination;
}
let eventMatchers = {
'HTMLEvents': /^(?:load|unload|abort|error|select|change|submit|reset|focus|blur|resize|scroll)$/,
'MouseEvents': /^(?:click|dblclick|mouse(?:down|up|over|move|out))$/
}
let defaultOptions = {
pointerX: 0,
pointerY: 0,
button: 0,
ctrlKey: false,
altKey: false,
shiftKey: false,
metaKey: false,
bubbles: true,
cancelable: true
}
let options = extend(defaultOptions, arguments[2] || {});
let oEvent, eventType = null;
for (let name in eventMatchers)
if (eventMatchers[name].test(eventName)) { eventType = name; break; }
if (!eventType)
throw new SyntaxError('Only HTMLEvents and MouseEvents interfaces are supported');
if (document.createEvent) {
oEvent = document.createEvent(eventType);
if (eventType == 'HTMLEvents')
{
oEvent.initEvent(eventName, options.bubbles, options.cancelable);
}
else
{
oEvent.initMouseEvent(eventName, options.bubbles, options.cancelable, document.defaultView,
options.button, options.pointerX, options.pointerY, options.pointerX, options.pointerY,
options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, element);
}
element.dispatchEvent(oEvent);
}
else {
options.clientX = options.pointerX;
options.clientY = options.pointerY;
var evt = document.createEventObject();
oEvent = extend(evt, options);
element.fireEvent('on' + eventName, oEvent);
}
return element;
}
/** Register a callback for a certain window event
*
* @param {*} event The event to register to
* @param {*} elementId The id of the element that will listen to the event
* @param {*} functionCallback The function to callback when the event is shoot
* @param {...any} args Arguments for the callback
*/
function on(event, elementId, functionCallback, ...args) {
//if element provided is string, get the actual element
const element = Util.getElement(elementId);
element.addEventListener(event,
function (e) {
functionCallback(...args, e);
});
}
/** Register a callback for a certain window event that is shot by the children of
* an element passed as an argument
*
* @param {*} event The event to register to
* @param {*} elementId The id of the element whose children will listen to the event
* @param {*} functionCallback The function to callback when the event is shoot
* @param {...any} args Arguments for the callback
*/
function onChildren(event, parentElement, functionCallback, ...args) {
parentElement = Util.getElement(parentElement);
const children = parentElement.children;
//loop through children and add onClick listener
for (var i = 0; i < children.length; i++) {
on(event, children[i], functionCallback, ...args);
}
}
/** Registers a callback for a custom (non HTML) event
*
* @param {*} event The event to register to
* @param {*} functionCallback The function to call
*/
function onCustom(event, functionCallback) {
if (customCallback[event] === undefined)
customCallback[event] = [];
customCallback[event].push(functionCallback);
}
/** Emits a custom (non HTML) event
*
* @param {*} event The event to emit
* @param {...any} args The arguments for that event
*/
function emit(event, ...args) {
if (customCallback[event] != undefined)
for (let i=0; i<customCallback[event].length; i++)
customCallback[event][i](args);
}
return {
simulateInput,
simulateMouseEvent,
on,
onChildren,
onCustom,
emit
}
})();

692
js/File.js Normal file
View File

@ -0,0 +1,692 @@
class File {
// Canvas, canvas state
canvasSize = [];
zoom = 7;
canvasView = document.getElementById("canvas-view");
// Layers
layers = [];
currentLayer = undefined;
VFXLayer = undefined;
TMPLayer = undefined;
pixelGrid = undefined;
checkerBoard = undefined
// Canvas resize attributes
// Resize canvas pop up window
resizeCanvasContainer = document.getElementById("resize-canvas");
// Start pivot
rcPivot = "middle";
// Selected pivot button
currentPivotObject = undefined;
// Border offsets
rcBorders = {left: 0, right: 0, top: 0, bottom: 0};
// Sprite scaling attributes
// Should I keep the sprite ratio?
keepRatio = true;
// Used to store the current ratio
currentRatio = undefined;
// The currenty selected resizing algorithm (nearest-neighbor or bilinear-interpolation)
currentAlgo = 'nearest-neighbor';
// Current resize data
data = {width: 0, height: 0, widthPercentage: 100, heightPercentage: 100};
// Start resize data
startData = {width: 0, height:0, widthPercentage: 100, heightPercentage: 100};
// Sprite scaling attributes
openResizeCanvasWindow() {
// Initializes the inputs
this.initResizeCanvasInputs();
Dialogue.showDialogue('resize-canvas');
}
initResizeCanvasInputs() {
// Getting the pivot buttons
let buttons = document.getElementsByClassName("pivot-button");
// Adding the event handlers for them
for (let i=0; i<buttons.length; i++) {
Events.on("click", buttons[i], this.changePivot.bind(this));
if (buttons[i].getAttribute("value").includes("middle")) {
this.currentPivotObject = buttons[i];
}
}
document.getElementById("rc-width").value = currFile.canvasSize[0];
document.getElementById("rc-height").value = currFile.canvasSize[1];
Events.on("change", "rc-border-left", this.rcChangedBorder.bind(this));
Events.on("change", "rc-border-right", this.rcChangedBorder.bind(this));
Events.on("change", "rc-border-top", this.rcChangedBorder.bind(this));
Events.on("change", "rc-border-bottom", this.rcChangedBorder.bind(this));
Events.on("change", "rc-width", this.rcChangedSize.bind(this));
Events.on("change", "rc-height", this.rcChangedSize.bind(this));
Events.on("click", "resize-canvas-confirm", this.resizeCanvas.bind(this));
}
/** Fired when a border offset is changed: it updates the width and height
*/
rcChangedBorder() {
this.rcUpdateBorders();
document.getElementById("rc-width").value = parseInt(currFile.canvasSize[0]) +
this.rcBorders.left + this.rcBorders.right;
document.getElementById("rc-height").value = parseInt(currFile.canvasSize[1]) +
this.rcBorders.top + this.rcBorders.bottom;
}
/** Fired when width or height are changed: updates the border offsets
*/
rcChangedSize() {
let widthOffset = Math.abs(document.getElementById("rc-width").value) - currFile.canvasSize[0];
let heightOffset = Math.abs(document.getElementById("rc-height").value) - currFile.canvasSize[1];
let left = Math.round(widthOffset / 2);
let right = widthOffset - left;
let top = Math.round(heightOffset / 2);
let bottom = heightOffset - top;
document.getElementById("rc-border-left").value = left;
document.getElementById("rc-border-right").value = right;
document.getElementById("rc-border-top").value = top;
document.getElementById("rc-border-bottom").value = bottom;
this.rcBorders.left = left;
this.rcBorders.right = right;
this.rcBorders.top = top;
this.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
*/
resizeCanvas(event, size, customData, saveHistory = true) {
let imageDatas = [];
let leftOffset = 0;
let topOffset = 0;
let copiedDataIndex = 0;
// If I'm undoing and I'm not trimming, I manually put the values in the window
if (size != null && customData == null) {
document.getElementById("rc-width").value = size.x;
document.getElementById("rc-height").value = size.y;
this.rcChangedSize();
}
this.rcUpdateBorders();
// Save all imageDatas
for (let i=0; i<currFile.layers.length; i++) {
if (currFile.layers[i].hasCanvas()) {
imageDatas.push(currFile.layers[i].context.getImageData(
0, 0, currFile.canvasSize[0], currFile.canvasSize[1])
);
}
}
// Saving the history only if I'm not already undoing or redoing
if (saveHistory && event != null) {
// Saving history
new HistoryState().ResizeCanvas(
{x: parseInt(currFile.canvasSize[0]) + this.rcBorders.left + this.rcBorders.right,
y: parseInt(currFile.canvasSize[1]) + this.rcBorders.top + this.rcBorders.bottom},
{x: currFile.canvasSize[0],
y: currFile.canvasSize[1]},
imageDatas.slice(), customData != null && saveHistory
);
}
currFile.canvasSize[0] = parseInt(currFile.canvasSize[0]) +
this.rcBorders.left + this.rcBorders.right;
currFile.canvasSize[1] = parseInt(currFile.canvasSize[1]) +
this.rcBorders.top + this.rcBorders.bottom;
// Resize the canvases
for (let i=0; i<currFile.layers.length; i++) {
currFile.layers[i].canvas.width = currFile.canvasSize[0];
currFile.layers[i].canvas.height = currFile.canvasSize[1];
currFile.layers[i].resize();
}
// Regenerate the checkerboard
currFile.checkerBoard.fillCheckerboard();
currFile.pixelGrid.fillPixelGrid();
// Put the imageDatas in the right position
switch (this.rcPivot)
{
case 'topleft':
leftOffset = 0;
topOffset = 0;
break;
case 'top':
leftOffset = (this.rcBorders.left + this.rcBorders.right) / 2;
topOffset = 0;
break;
case 'topright':
leftOffset = this.rcBorders.left + this.rcBorders.right;
topOffset = 0;
break;
case 'left':
leftOffset = 0;
topOffset = (this.rcBorders.top + this.rcBorders.bottom) / 2;
break;
case 'middle':
leftOffset = (this.rcBorders.left + this.rcBorders.right) / 2;
topOffset = (this.rcBorders.top + this.rcBorders.bottom) / 2;
break;
case 'right':
leftOffset = this.rcBorders.left + this.rcBorders.right;
topOffset = (this.rcBorders.top + this.rcBorders.bottom) / 2;
break;
case 'bottomleft':
leftOffset = 0;
topOffset = this.rcBorders.top + this.rcBorders.bottom;
break;
case 'bottom':
leftOffset = (this.rcBorders.left + this.rcBorders.right) / 2;
topOffset = this.rcBorders.top + this.rcBorders.bottom;
break;
case 'bottomright':
leftOffset = this.rcBorders.left + this.rcBorders.right;
topOffset = this.rcBorders.top + this.rcBorders.bottom;
break;
default:
break;
}
// Putting all the data for each layer with the right offsets (decided by the pivot)
for (let i=0; i<currFile.layers.length; i++) {
if (currFile.layers[i].hasCanvas()) {
if (customData == undefined) {
currFile.layers[i].context.putImageData(imageDatas[copiedDataIndex], leftOffset, topOffset);
}
else {
currFile.layers[i].context.putImageData(customData[copiedDataIndex], 0, 0);
}
currFile.layers[i].updateLayerPreview();
copiedDataIndex++;
}
}
Dialogue.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
*/
trimCanvas(event, saveHistory) {
let minY = Infinity;
let minX = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
let tmp;
let imageDatas = [];
let historySave = saveHistory == null;
let prevPivot = rcPivot;
this.rcPivot = "topleft";
// Computing the min and max coordinates in which there's a non empty pixel
for (let i=1; i<currFile.layers.length - nAppLayers; i++) {
let imageData = currFile.layers[i].context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
let pixelPosition;
for (let i=imageData.data.length - 1; i>= 0; i-=4) {
if (!Util.isPixelEmpty(
[imageData.data[i - 3], imageData.data[i - 2],
-imageData.data[i - 1], imageData.data[i]])) {
pixelPosition = getPixelPosition(i);
// max x
if (pixelPosition[0] > maxX) {
maxX = pixelPosition[0];
}
// min x
if (pixelPosition[0] < minX) {
minX = pixelPosition[0];
}
// max y
if (pixelPosition[1] > maxY) {
maxY = pixelPosition[1];
}
// min y
if (pixelPosition[1] < minY) {
minY = pixelPosition[1];
}
}
}
}
tmp = minY;
minY = maxY;
maxY = tmp;
minY = currFile.canvasSize[1] - minY;
maxY = currFile.canvasSize[1] - maxY;
// Setting the borders coherently with the values I've just computed
this.rcBorders.right = (maxX - currFile.canvasSize[0]) + 1;
this.rcBorders.left = -minX;
this.rcBorders.top = maxY - currFile.canvasSize[1] + 1;
this.rcBorders.bottom = -minY;
// Saving the data
for (let i=0; i<currFile.layers.length; i++) {
if (currFile.layers[i].hasCanvas()) {
imageDatas.push(currFile.layers[i].context.getImageData(minX - 1, currFile.layers[i].canvasSize[1] - maxY, maxX-minX + 1, maxY-minY + 1));
}
}
//console.log("sx: " + borders.left + "dx: " + borders.right + "top: " + borders.top + "btm: " + borders.bottom);
document.getElementById("rc-border-left").value = this.rcBorders.left;
document.getElementById("rc-border-right").value = this.rcBorders.right;
document.getElementById("rc-border-top").value = this.rcBorders.top;
document.getElementById("rc-border-bottom").value = this.rcBorders.bottom;
// Resizing the canvas with the decided border offsets
this.resizeCanvas(null, null, imageDatas.slice(), historySave);
// Resetting the previous pivot
this.rcPivot = prevPivot;
}
rcUpdateBorders() {
// Getting input
this.rcBorders.left = document.getElementById("rc-border-left").value;
this.rcBorders.right = document.getElementById("rc-border-right").value;
this.rcBorders.top = document.getElementById("rc-border-top").value;
this.rcBorders.bottom = document.getElementById("rc-border-bottom").value;
// Validating input
this.rcBorders.left == "" ? this.rcBorders.left = 0 : this.rcBorders.left = Math.round(parseInt(this.rcBorders.left));
this.rcBorders.right == "" ? this.rcBorders.right = 0 : this.rcBorders.right = Math.round(parseInt(this.rcBorders.right));
this.rcBorders.top == "" ? this.rcBorders.top = 0 : this.rcBorders.top = Math.round(parseInt(this.rcBorders.top));
this.rcBorders.bottom == "" ? this.rcBorders.bottom = 0 : this.rcBorders.bottom = Math.round(parseInt(this.rcBorders.bottom));
}
changePivot(event) {
this.rcPivot = event.target.getAttribute("value");
// Setting the selected class
this.currentPivotObject.classList.remove("rc-selected-pivot");
this.currentPivotObject = event.target;
this.currentPivotObject.classList.add("rc-selected-pivot");
}
/** Opens the sprite resizing window
*
*/
openResizeSpriteWindow() {
// Inits the sprie resize inputs
this.initResizeSpriteInputs();
// Computing the current ratio
this.currentRatio = currFile.canvasSize[0] / currFile.canvasSize[1];
// Initializing the input fields
this.data.width = currFile.canvasSize[0];
this.data.height = currFile.canvasSize[1];
this.startData.width = parseInt(this.data.width);
this.startData.height = parseInt(this.data.height);
this.startData.heightPercentage = 100;
this.startData.widthPercentage = 100;
// Opening the pop up now that it's ready
Dialogue.showDialogue('resize-sprite');
}
/** Initalizes the input values and binds the elements to their events
*
*/
initResizeSpriteInputs() {
document.getElementById("rs-width").value = currFile.canvasSize[0];
document.getElementById("rs-height").value = currFile.canvasSize[1];
document.getElementById("rs-width-percentage").value = 100;
document.getElementById("rs-height-percentage").value = 100;
document.getElementById("rs-keep-ratio").checked = true;
Events.on("change", "rs-width", this.changedWidth.bind(this));
Events.on("change", "rs-height", this.changedHeight.bind(this));
Events.on("change", "rs-width-percentage", this.changedWidthPercentage.bind(this));
Events.on("change", "rs-height-percentage", this.changedHeightPercentage.bind(this));
Events.on("click", "resize-sprite-confirm", this.resizeSprite.bind(this));
Events.on("click", "rs-keep-ratio", this.toggleRatio.bind(this));
Events.on("change", "resize-algorithm-combobox", this.changedAlgorithm.bind(this));
}
/** Resizes (scales) the sprite
*
* @param {*} event
* @param {*} ratio Keeps infos about the x ratio and y ratio
*/
resizeSprite(event, ratio) {
// Old data
let oldWidth, oldHeight;
// New data
let newWidth, newHeight;
// Current imageDatas
let rsImageDatas = [];
// Index that will be used a few lines below
let layerIndex = 0;
// Copy of the imageDatas that will be stored in the history
let imageDatasCopy = [];
oldWidth = currFile.canvasSize[0];
oldHeight = currFile.canvasSize[1];
this.rcPivot = "middle";
// Updating values if the user didn't press enter
switch (document.activeElement.id) {
case "rs-width-percentage":
this.changedWidthPercentage();
break;
case "rs-width":
this.changedWidth();
break;
case "rs-height-percentage":
this.changedHeightPercentage();
break;
case "rs-height":
this.changedHeight();
break;
default:
// In this case everything has been updated correctly
break;
}
// Computing newWidth and newHeight
if (ratio == null) {
newWidth = this.data.width;
newHeight = this.data.height;
}
else {
newWidth = currFile.canvasSize[0] * ratio[0];
newHeight = currFile.canvasSize[1] * ratio[1];
}
// Get all the image datas
for (let i=0; i<currFile.layers.length; i++) {
if (currFile.layers[i].hasCanvas()) {
rsImageDatas.push(currFile.layers[i].context.getImageData(
0, 0, currFile.canvasSize[0], currFile.canvasSize[1])
);
}
}
// event is null when the user is undoing
if (event != null) {
// Copying the image data
imageDatasCopy = rsImageDatas.slice();
// Saving the history
new HistoryState().ResizeSprite(newWidth / oldWidth, newHeight / oldHeight, this.currentAlgo, imageDatasCopy);
}
// Resizing the canvas
currFile.resizeCanvas(null, {x: newWidth, y: newHeight});
// Put the image datas on the new canvases
for (let i=0; i<currFile.layers.length; i++) {
if (currFile.layers[i].hasCanvas()) {
currFile.layers[i].context.putImageData(
this.resizeImageData(rsImageDatas[layerIndex], newWidth, newHeight, this.currentAlgo), 0, 0
);
currFile.layers[i].updateLayerPreview();
layerIndex++;
}
}
// Updating start values when I finish scaling the sprite
// OPTIMIZABLE? Can't I just assign data to startData? Is js smart enough to understand?
if (ratio == null) {
this.startData.width = this.data.width;
this.startData.height = this.data.height;
}
else {
this.startData.width = currFile.canvasSize[0];
this.startData.height = currFile.canvasSize[1];
}
this.startData.widthPercentage = 100;
this.startData.heightPercentage = 100;
Dialogue.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
*/
changedWidth(event) {
let newHeight, newHeightPerc, newWidthPerc;
this.data.width = event.target.value;
newHeight = this.data.width / this.currentRatio;
newHeightPerc = (newHeight * 100) / this.startData.height;
newWidthPerc = (this.data.width * 100) / this.startData.width;
if (this.keepRatio) {
document.getElementById("rs-height").value = newHeight;
this.data.height = newHeight;
document.getElementById("rs-height-percentage").value = newHeightPerc;
this.data.heightPercentage = newHeightPerc;
}
document.getElementById("rs-width-percentage").value = newWidthPerc;
}
/**Fired when the input field for width is changed. Updates the other input fields consequently
*
* @param {*} event
*/
changedHeight(event) {
let newWidth, newWidthPerc, newHeightPerc;
this.data.height = event.target.value;
newWidth = this.data.height * this.currentRatio;
newWidthPerc = (newWidth * 100) / this.startData.width;
newHeightPerc = (this.data.height * 100) / this.startData.height;
if (this.keepRatio) {
document.getElementById("rs-width").value = newWidth;
this.data.width = newWidth;
document.getElementById("rs-width-percentage").value = newWidthPerc;
this.data.widthPercentage = newWidthPerc;
}
document.getElementById("rs-height-percentage").value = newHeightPerc;
this.data.heightPercentage = newHeightPerc;
}
/**Fired when the input field for width percentage is changed. Updates the other input fields consequently
*
* @param {*} event
*/
changedWidthPercentage(event) {
let oldValue = 100;
let ratio;
let newWidth, newHeight, newHeightPerc;
this.data.widthPercentage = event.target.value;
ratio = this.data.widthPercentage / oldValue;
newHeight = this.startData.height * ratio;
newHeightPerc = this.data.widthPercentage;
newWidth = this.startData.width * ratio;
if (this.keepRatio) {
document.getElementById("rs-height-percentage").value = newHeightPerc;
this.data.heightPercentage = newHeightPerc;
document.getElementById("rs-height").value = newHeight
this.data.height = newHeight;
}
document.getElementById("rs-width").value = newWidth;
this.data.width = newWidth;
}
/**Fired when the input field for height percentage is changed. Updates the other input fields consequently
*
* @param {*} event
*/
changedHeightPercentage(event) {
let oldValue = this.data.heightPercentage;
let ratio;
let newHeight, newWidth, newWidthPerc;
this.data.heightPercentage = event.target.value;
ratio = this.data.heightPercentage / oldValue;
newWidth = this.startData.width * ratio;
newWidthPerc = this.data.heightPercentage;
newHeight = this.startData.height * ratio;
if (this.keepRatio) {
document.getElementById("rs-width-percentage").value = this.data.heightPercentage * currentRatio;
this.data.widthPercentage = newWidthPerc;
document.getElementById("rs-width").value = newWidth;
this.data.width = newWidth;
}
document.getElementById("rs-height").value = newHeight;
this.data.height = newHeight;
}
/** Toggles the keepRatio value (fired by the checkbox in the pop up window)
*/
toggleRatio() {
this.keepRatio = !this.keepRatio;
}
/** Changes the scaling algorithm (fired by the combobox in the pop up window)
*
* @param {*} event
*/
changedAlgorithm(event) {
this.currentAlgo = event.target.value;
}
/** 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
*/
resizeImageData (image, width, height, algorithm) {
algorithm = algorithm || 'bilinear-interpolation'
let resize;
switch (algorithm) {
case 'nearest-neighbor': resize = this.nearestNeighbor; break
case 'bilinear-interpolation': resize = this.bilinearInterpolation; break
default: return image;
}
const result = new ImageData(width, height)
resize(image, result)
return result
}
/** Nearest neighbor algorithm to scale a sprite
*
* @param {*} src The source imageData
* @param {*} dst The destination imageData
*/
nearestNeighbor (src, dst) {
let pos = 0
// Just applying the nearest neighbor algorithm
for (let y = 0; y < dst.height; y++) {
for (let x = 0; x < dst.width; x++) {
const srcX = Math.floor(x * src.width / dst.width)
const srcY = Math.floor(y * src.height / dst.height)
let srcPos = ((srcY * src.width) + srcX) * 4
dst.data[pos++] = src.data[srcPos++] // R
dst.data[pos++] = src.data[srcPos++] // G
dst.data[pos++] = src.data[srcPos++] // B
dst.data[pos++] = src.data[srcPos++] // A
}
}
}
/** Bilinear interpolation used to scale a sprite
*
* @param {*} src The source imageData
* @param {*} dst The destination imageData
*/
bilinearInterpolation (src, dst) {
// Applying the bilinear interpolation algorithm
function interpolate (k, kMin, kMax, vMin, vMax) {
return Math.round((k - kMin) * vMax + (kMax - k) * vMin)
}
function interpolateHorizontal (offset, x, y, xMin, xMax) {
const vMin = src.data[((y * src.width + xMin) * 4) + offset]
if (xMin === xMax) return vMin
const vMax = src.data[((y * src.width + xMax) * 4) + offset]
return interpolate(x, xMin, xMax, vMin, vMax)
}
function interpolateVertical (offset, x, xMin, xMax, y, yMin, yMax) {
const vMin = interpolateHorizontal(offset, x, yMin, xMin, xMax)
if (yMin === yMax) return vMin
const vMax = interpolateHorizontal(offset, x, yMax, xMin, xMax)
return interpolate(y, yMin, yMax, vMin, vMax)
}
let pos = 0
for (let y = 0; y < dst.height; y++) {
for (let x = 0; x < dst.width; x++) {
const srcX = x * src.width / dst.width
const srcY = y * src.height / dst.height
const xMin = Math.floor(srcX)
const yMin = Math.floor(srcY)
const xMax = Math.min(Math.ceil(srcX), src.width - 1)
const yMax = Math.min(Math.ceil(srcY), src.height - 1)
dst.data[pos++] = interpolateVertical(0, srcX, xMin, xMax, srcY, yMin, yMax) // R
dst.data[pos++] = interpolateVertical(1, srcX, xMin, xMax, srcY, yMin, yMax) // G
dst.data[pos++] = interpolateVertical(2, srcX, xMin, xMax, srcY, yMin, yMax) // B
dst.data[pos++] = interpolateVertical(3, srcX, xMin, xMax, srcY, yMin, yMax) // A
}
}
}
}
let currFile = new File();

279
js/FileManager.js Normal file
View File

@ -0,0 +1,279 @@
const FileManager = (() => {
// Binding the browse holder change event to file loading
const browseHolder = document.getElementById('open-image-browse-holder');
const browsePaletteHolder = document.getElementById('load-palette-browse-holder');
Events.on('change', browseHolder, loadFile);
Events.on('change', browsePaletteHolder, loadPalette);
function openSaveProjectWindow() {
//create name
let selectedPalette = Util.getText('palette-button');
if (selectedPalette != 'Choose a palette...'){
var paletteAbbreviation = palettes[selectedPalette].abbreviation;
var fileName = 'pixel-'+paletteAbbreviation+'-'+currFile.canvasSize[0]+'x'+currFile.canvasSize[1];
} else {
var fileName = 'pixel-'+currFile.canvasSize[0]+'x'+currFile.canvasSize[1];
selectedPalette = 'none';
}
Util.setValue('lpe-file-name', fileName);
Events.on("click", "save-project-confirm", saveProject);
Dialogue.showDialogue('save-project', false);
}
function openPixelExportWindow() {
let selectedPalette = Util.getText('palette-button');
if (selectedPalette != 'Choose a palette...'){
var paletteAbbreviation = palettes[selectedPalette].name;
var fileName = 'pixel-'+paletteAbbreviation+'-'+canvasSize[0]+'x'+canvasSize[1]+'.png';
} else {
var fileName = 'pixel-'+currFile.canvasSize[0]+'x'+currFile.canvasSize[1]+'.png';
selectedPalette = 'none';
}
Util.setValue('export-file-name', fileName);
Events.on("click", "export-confirm", exportProject);
Dialogue.showDialogue('export', false);
}
function saveProject() {
// Get name
let fileName = Util.getValue("lpe-file-name") + ".lpe";
let selectedPalette = Util.getText('palette-button');
//set download link
const linkHolder = document.getElementById('save-project-link-holder');
// create file content
const content = getProjectData();
linkHolder.href = 'data:text/plain;charset=utf-8,' + encodeURIComponent(content);
linkHolder.download = fileName;
linkHolder.click();
if (typeof ga !== 'undefined')
ga('send', 'event', 'Pixel Editor Save', selectedPalette, currFile.canvasSize[0]+'/'+currFile.canvasSize[1]); /*global ga*/
}
function exportProject() {
if (EditorState.documentCreated()) {
//create name
let fileName = Util.getValue("export-file-name");
//set download link
let linkHolder = document.getElementById('save-image-link-holder');
// Creating a tmp canvas to flatten everything
let exportCanvas = document.createElement("canvas");
let emptyCanvas = document.createElement("canvas");
let layersCopy = currFile.layers.slice();
exportCanvas.width = currFile.canvasSize[0];
exportCanvas.height = currFile.canvasSize[1];
emptyCanvas.width = currFile.canvasSize[0];
emptyCanvas.height = currFile.canvasSize[1];
// Sorting the layers by z index
layersCopy.sort((a, b) => (a.canvas.style.zIndex > b.canvas.style.zIndex) ? 1 : -1);
// Merging every layer on the export canvas
for (let i=0; i<layersCopy.length; i++) {
if (layersCopy[i].hasCanvas() && layersCopy[i].isVisible) {
LayerList.mergeLayers(exportCanvas.getContext('2d'), layersCopy[i].context);
}
// I'm not going to find out why the layer ordering screws up if you don't copy
// a blank canvas when layers[i] is not set as visible, but if you have time to
// spend, feel free to investigate (comment the else, create 3 layers: hide the
// middle one and export, the other 2 will be swapped in their order)
else {
LayerList.mergeLayers(exportCanvas.getContext('2d'), emptyCanvas.getContext('2d'));
}
}
linkHolder.href = exportCanvas.toDataURL();
linkHolder.download = fileName;
linkHolder.click();
emptyCanvas.remove();
exportCanvas.remove();
//track google event
if (typeof ga !== 'undefined')
ga('send', 'event', 'Pixel Editor Export', selectedPalette, currFile.canvasSize[0]+'/'+currFile.canvasSize[1]); /*global ga*/
}
}
function open() {
//if a document exists
if (EditorState.documentCreated()) {
//check if the user wants to overwrite
if (confirm('Opening a pixel will discard your current one. Are you sure you want to do that?'))
//open file selection dialog
document.getElementById('open-image-browse-holder').click();
}
else
//open file selection dialog
document.getElementById('open-image-browse-holder').click();
}
function loadFile() {
let fileName = document.getElementById("open-image-browse-holder").value;
// Getting the extension
let extension = (fileName.substring(fileName.lastIndexOf('.')+1, fileName.length) || fileName).toLowerCase();
if (browseHolder.files && browseHolder.files[0]) {
// Checking if the extension is supported
if (extension == 'png' || extension == 'gif' || extension == 'lpe') {
// If it's a Lospec Pixel Editor tm file, I load the project
if (extension == 'lpe') {
openProject();
}
else {
openFile();
}
}
else alert('Only .LPE project files, PNG and GIF files are allowed at this time.');
}
browseHolder.value = null;
}
function openFile() {
//load file
var fileReader = new FileReader();
fileReader.onload = function(e) {
var img = new Image();
img.onload = function() {
//create a new pixel with the images dimentions
Startup.newPixel(this.width, this.height);
EditorState.switchMode('Advanced');
//draw the image onto the canvas
currFile.currentLayer.context.drawImage(img, 0, 0);
ColorModule.createPaletteFromLayers();
//track google event
if (typeof ga !== 'undefined')
ga('send', 'event', 'Pixel Editor Load', colorPalette.length, this.width+'/'+this.height); /*global ga*/
};
img.src = e.target.result;
};
fileReader.readAsDataURL(browseHolder.files[0]);
}
function openProject() {
let file = browseHolder.files[0];
let reader = new FileReader();
// Getting all the data
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) {
let dictionary = JSON.parse(e.target.result);
Startup.newPixel(dictionary['canvasWidth'], dictionary['canvasHeight'], dictionary);
for (let i=0; dictionary['color' + i] != null; i++) {
ColorModule.addColor(dictionary['color'+i]);
}
// Removing the default colours
ColorModule.deleteColor(ColorModule.getCurrentPalette()[0]);
ColorModule.deleteColor(ColorModule.getCurrentPalette()[0]);
}
}
function getProjectData() {
// use a dictionary
let dictionary = {};
// sorting layers by increasing z-index
let layersCopy = currFile.layers.slice();
layersCopy.sort((a, b) => (a.canvas.style.zIndex > b.canvas.style.zIndex) ? 1 : -1);
// save canvas size
dictionary['canvasWidth'] = currFile.canvasSize[0];
dictionary['canvasHeight'] = currFile.canvasSize[1];
// save editor mode
dictionary['editorMode'] = EditorState.getCurrentMode();
// save palette
for (let i=0; i<ColorModule.getCurrentPalette().length; i++) {
dictionary["color" + i] = ColorModule.getCurrentPalette()[i];
}
// save number of layers
dictionary["nLayers"] = layersCopy.length;
// save layers
for (let i=0; i<layersCopy.length; i++) {
// Only saving the layers the user has access to (no vfx, tmp or checkerboard layers)
if (layersCopy[i].menuEntry != null) {
dictionary["layer" + i] = layersCopy[i];
dictionary["layer" + i + "ImageData"] = layersCopy[i].canvas.toDataURL();
}
}
return JSON.stringify(dictionary);
}
function loadPalette() {
if (browsePaletteHolder.files && browsePaletteHolder.files[0]) {
//make sure file is allowed filetype
var fileContentType = browsePaletteHolder.files[0].type;
if (fileContentType == 'image/png' || fileContentType == 'image/gif') {
//load file
var fileReader = new FileReader();
fileReader.onload = function(e) {
var img = new Image();
img.onload = function() {
//draw image onto the temporary canvas
var loadPaletteCanvas = document.getElementById('load-palette-canvas-holder');
var loadPaletteContext = loadPaletteCanvas.getContext('2d');
loadPaletteCanvas.width = img.width;
loadPaletteCanvas.height = img.height;
loadPaletteContext.drawImage(img, 0, 0);
//create array to hold found colors
var colorPalette = [];
var imagePixelData = loadPaletteContext.getImageData(0,0,this.width, this.height).data;
//loop through pixels looking for colors to add to palette
for (var i = 0; i < imagePixelData.length; i += 4) {
const newColor = {r:imagePixelData[i],g:imagePixelData[i + 1],b:imagePixelData[i + 2]};
var color = '#' + Color.rgbToHex(newColor);
if (colorPalette.indexOf(color) == -1) {
colorPalette.push(color);
}
}
//add to palettes so that it can be loaded when they click okay
palettes['Loaded palette'] = {};
palettes['Loaded palette'].colors = colorPalette;
Util.setText('palette-button', 'Loaded palette');
Util.setText('palette-button-splash', 'Loaded palette');
Util.toggle('palette-menu-splash');
};
img.src = e.target.result;
};
fileReader.readAsDataURL(browsePaletteHolder.files[0]);
}
else alert('Only PNG and GIF files are supported at this time.');
}
browsePaletteHolder.value = null;
}
return {
saveProject,
exportProject,
openPixelExportWindow,
openSaveProjectWindow,
open
}
})();

442
js/History.js Normal file
View File

@ -0,0 +1,442 @@
/** 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
*
*/
const History = (() => {
const undoLogStyle = 'background: #87ff1c; color: black; padding: 5px;';
let undoStates = [];
let redoStates = [];
Events.on('click', 'undo-button', undo);
Events.on('click', 'redo-button', redo);
//rename to add undo state
function saveHistoryState (state) {
//get current canvas data and save to undoStates array
undoStates.push(state);
//limit the number of states to settings.numberOfHistoryStates
if (undoStates.length > Settings.getCurrSettings().numberOfHistoryStates) {
undoStates = undoStates.splice(-Settings.getCurrSettings().numberOfHistoryStates, Settings.getCurrSettings().numberOfHistoryStates);
}
//there is now definitely at least 1 undo state, so the button shouldnt be disabled
document.getElementById('undo-button').classList.remove('disabled');
//there should be no redoStates after an undoState is saved
redoStates = [];
}
function undo () {
console.log("undoing");
undoOrRedo('undo');
}
function redo () {
console.log("redoing");
undoOrRedo('redo');
}
function undoOrRedo(mode) {
if (redoStates.length <= 0 && mode == 'redo') return;
if (undoStates.length <= 0 && mode == 'undo') return;
// Enable button
document.getElementById(mode + '-button').classList.remove('disabled');
if (mode == 'undo') {
const undoState = undoStates.pop();
redoStates.push(undoState);
undoState.undo();
}
else {
const redoState = redoStates.pop();
undoStates.push(redoState);
redoState.redo();
}
// if theres none left, disable the option
if (redoStates.length == 0) document.getElementById('redo-button').classList.add('disabled');
if (undoStates.length == 0) document.getElementById('undo-button').classList.add('disabled');
}
return {
redo,
undo,
saveHistoryState
}
})();
class HistoryState {
constructor() {
History.saveHistoryState(this);
}
ResizeSprite (xRatio, yRatio, algo, oldData) {
this.xRatio = xRatio;
this.yRatio = yRatio;
this.algo = algo;
this.oldData = oldData;
this.undo = function() {
let layerIndex = 0;
currFile.currentAlgo = algo;
currFile.resizeSprite(null, [1 / this.xRatio, 1 / this.yRatio]);
// Also putting the old data
for (let i=0; i<currFile.layers.length; i++) {
if (currFile.layers[i].hasCanvas()) {
currFile.layers[i].context.putImageData(this.oldData[layerIndex], 0, 0);
layerIndex++;
currFile.layers[i].updateLayerPreview();
}
}
};
this.redo = function() {
currFile.currentAlgo = algo;
currFile.resizeSprite(null, [this.xRatio, this.yRatio]);
};
}
ResizeCanvas (newSize, oldSize, imageDatas, trim) {
this.oldSize = oldSize;
this.newSize = newSize;
this.imageDatas = imageDatas;
this.trim = trim;
this.undo = function() {
let dataIndex = 0;
// Resizing the canvas
currFile.resizeCanvas(null, oldSize, null, false);
// Putting the image datas
for (let i=0; i<currFile.layers.length; i++) {
if (currFile.layers[i].hasCanvas()) {
currFile.layers[i].context.putImageData(this.imageDatas[dataIndex], 0, 0);
dataIndex++;
}
}
};
this.redo = function() {
if (!this.trim) {
currFile.resizeCanvas(null, newSize, null, false);
}
else {
currFile.trimCanvas(null, false);
}
};
}
FlattenVisible(flattened) {
this.nFlattened = flattened;
this.undo = function() {
for (let i=0; i<this.nFlattened; i++) {
undo();
}
};
this.redo = function() {
for (let i=0; i<this.nFlattened; i++) {
redo();
}
};
}
FlattenTwoVisibles(belowImageData, afterAbove, layerIndex, aboveLayer, belowLayer) {
this.aboveLayer = aboveLayer;
this.belowLayer = belowLayer;
this.belowImageData = belowImageData;
this.undo = function() {
currFile.canvasView.append(aboveLayer.canvas);
LayerList.getLayerListEntries().insertBefore(aboveLayer.menuEntry, afterAbove);
belowLayer.context.clearRect(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
belowLayer.context.putImageData(this.belowImageData, 0, 0);
belowLayer.updateLayerPreview();
currFile.layers.splice(layerIndex, 0, aboveLayer);
};
this.redo = function() {
LayerList.mergeLayers(belowLayer.context, aboveLayer.context);
// Deleting the above layer
aboveLayer.canvas.remove();
aboveLayer.menuEntry.remove();
currFile.layers.splice(currFile.layers.indexOf(aboveLayer), 1);
};
}
FlattenAll(nFlattened) {
this.nFlattened = nFlattened;
this.undo = function() {
for (let i=0; i<this.nFlattened - nAppLayers; i++) {
undo();
}
};
this.redo = function() {
for (let i=0; i<this.nFlattened - nAppLayers; i++) {
redo();
}
};
}
MergeLayer(aboveIndex, aboveLayer, belowData, belowLayer) {
this.aboveIndex = aboveIndex;
this.belowData = belowData;
this.aboveLayer = aboveLayer;
this.belowLayer = belowLayer;
this.undo = function() {
LayerList.getLayerListEntries().insertBefore(this.aboveLayer.menuEntry, this.belowLayer.menuEntry);
currFile.canvasView.append(this.aboveLayer.canvas);
belowLayer.context.clearRect(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
belowLayer.context.putImageData(this.belowData, 0, 0);
belowLayer.updateLayerPreview();
currFile.layers.splice(this.aboveIndex, 0, this.aboveLayer);
};
this.redo = function() {
aboveLayer.selectLayer();
LayerList.merge(false);
};
}
RenameLayer(oldName, newName, layer) {
this.edited = layer;
this.oldName = oldName;
this.newName = newName;
this.undo = function() {
layer.menuEntry.getElementsByTagName("p")[0].innerHTML = oldName;
};
this.redo = function() {
layer.menuEntry.getElementsByTagName("p")[0].innerHTML = newName;
};
}
DuplicateLayer(addedLayer, copiedLayer) {
this.addedLayer = addedLayer;
this.copiedLayer = copiedLayer;
this.undo = function() {
addedLayer.selectLayer();
LayerList.deleteLayer(false);
};
this.redo = function() {
copiedLayer.selectLayer();
LayerList.duplicateLayer(null, false);
};
}
DeleteLayer(layerData, before, index) {
this.deleted = layerData;
this.before = before;
this.index = index;
this.undo = function() {
currFile.canvasView.append(this.deleted.canvas);
if (this.before != null) {
LayerList.getLayerListEntries().insertBefore(this.deleted.menuEntry, this.before);
}
else {
LayerList.getLayerListEntries().prepend(this.deleted.menuEntry);
}
currFile.layers.splice(this.index, 0, this.deleted);
};
this.redo = function() {
this.deleted.selectLayer();
LayerList.deleteLayer(false);
};
}
MoveTwoLayers(layer, oldIndex, newIndex) {
this.layer = layer;
this.oldIndex = oldIndex;
this.newIndex = newIndex;
this.undo = function() {
layer.canvas.style.zIndex = oldIndex;
};
this.redo = function() {
layer.canvas.style.zIndex = newIndex;
};
}
MoveLayer(afterToDrop, toDrop, staticc, nMoved) {
this.beforeToDrop = afterToDrop;
this.toDrop = toDrop;
this.undo = function() {
toDrop.menuEntry.remove();
if (afterToDrop != null) {
LayerList.getLayerListEntries().insertBefore(toDrop.menuEntry, afterToDrop)
}
else {
LayerList.getLayerListEntries().append(toDrop.menuEntry);
}
for (let i=0; i<nMoved; i++) {
undo();
}
};
this.redo = function() {
moveLayers(toDrop.menuEntry.id, staticc.menuEntry.id, true);
};
}
AddLayer(layerData, index) {
this.added = layerData;
this.index = index;
this.undo = function() {
if (currFile.layers.length - nAppLayers > this.index + 1) {
currFile.layers[this.index + 1].selectLayer();
}
else {
currFile.layers[this.index - 1].selectLayer();
}
this.added.canvas.remove();
this.added.menuEntry.remove();
currFile.layers.splice(index, 1);
};
this.redo = function() {
currFile.canvasView.append(this.added.canvas);
LayerList.getLayerListEntries().prepend(this.added.menuEntry);
layers.splice(this.index, 0, this.added);
};
}
//prototype for undoing canvas changes
EditCanvas() {
this.canvasState = currFile.currentLayer.context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
this.layerID = currFile.currentLayer.id;
this.undo = function () {
var stateLayer = LayerList.getLayerByID(this.layerID);
var currentCanvas = stateLayer.context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
stateLayer.context.putImageData(this.canvasState, 0, 0);
this.canvasState = currentCanvas;
stateLayer.updateLayerPreview();
};
this.redo = function () {
var stateLayer = LayerList.getLayerByID(this.layerID);
var currentCanvas = stateLayer.context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
stateLayer.context.putImageData(this.canvasState, 0, 0);
this.canvasState = currentCanvas;
stateLayer.updateLayerPreview();
};
}
//prototype for undoing added colors
AddColor(colorValue) {
this.colorValue = colorValue;
this.undo = function () {
ColorModule.deleteColor(this.colorValue);
};
this.redo = function () {
ColorModule.addColor(this.colorValue);
};
}
//prototype for undoing deleted colors
DeleteColor(colorValue) {
this.colorValue = colorValue;
this.canvas = currFile.currentLayer.context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
this.undo = function () {
var currentCanvas = currFile.currentLayer.context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
currFile.currentLayer.context.putImageData(this.canvas, 0, 0);
ColorModule.addColor(this.colorValue);
this.canvas = currentCanvas;
};
this.redo = function () {
var currentCanvas = currFile.currentLayer.context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
currFile.currentLayer.context.putImageData(this.canvas, 0, 0);
ColorModule.deleteColor(this.colorValue);
this.canvas = currentCanvas;
};
}
//prototype for undoing colors edits
EditColor(newColorValue, oldColorValue) {
this.newColorValue = newColorValue;
this.oldColorValue = oldColorValue;
this.canvas = currFile.currentLayer.context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
this.undo = function () {
let currentCanvas = currFile.currentLayer.context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
currFile.currentLayer.context.putImageData(this.canvas, 0, 0);
//find new color in palette and change it back to old color
let colors = document.getElementsByClassName('color-button');
for (let i = 0; i < colors.length; i++) {
//console.log(newColorValue, '==', colors[i].jscolor.toString());
if (newColorValue == colors[i].jscolor.toString()) {
colors[i].jscolor.fromString(oldColorValue);
break;
}
}
this.canvas = currentCanvas;
};
this.redo = function () {
let currentCanvas = currFile.currentLayer.context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
currFile.currentLayer.context.putImageData(this.canvas, 0, 0);
//find old color in palette and change it back to new color
let colors = document.getElementsByClassName('color-button');
for (let i = 0; i < colors.length; i++) {
//console.log(oldColorValue, '==', colors[i].jscolor.toString());
if (oldColorValue == colors[i].jscolor.toString()) {
colors[i].jscolor.fromString(newColorValue);
break;
}
}
this.canvas = currentCanvas;
};
}
}

199
js/Input.js Normal file
View File

@ -0,0 +1,199 @@
const Input = (() => {
let dragging = false;
let currentMouseEvent = undefined;
let lastMouseTarget = undefined;
let spacePressed = false;
let altPressed = false;
let ctrlPressed = false;
// Hotkeys when pressing a key
Events.on("keydown", document, KeyPress);
// Update held keys when releasing a key
Events.on("keyup", window, function (e) {
if (e.keyCode == 32) spacePressed = false;
if (!e.altKey) altPressed = false;
if (!e.ctrlKey) ctrlPressed = false;
});
// Update variables on mouse clicks
Events.on("mousedown", window, onMouseDown);
Events.on("mouseup", window, onMouseUp);
function onMouseDown(event) {
lastMouseTarget = event.target;
currentMouseEvent = event;
dragging = true;
if (!Util.isChildOfByClass(event.target, "editor-top-menu")) {
TopMenuModule.closeMenu();
}
}
function onMouseUp(event) {
currentMouseEvent = event;
dragging = false;
if (currFile.currentLayer != null && !Util.isChildOfByClass(event.target, "layers-menu-entry")) {
LayerList.closeOptionsMenu();
}
}
function getCursorPosition(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 -= currFile.currentLayer.canvas.offsetLeft;
y -= currFile.currentLayer.canvas.offsetTop;
return [Math.round(x), Math.round(y)];
}
/** Just listens to hotkeys and calls the linked functions
*
* @param {*} e
*/
function KeyPress(e) {
var keyboardEvent = window.event? event : e;
altPressed = e.altKey;
ctrlPressed = e.ctrlKey;
//if the user is typing in an input field or renaming a layer, ignore these hotkeys, unless it's an enter key
if (document.activeElement.tagName == 'INPUT' || LayerList.isRenamingLayer()) {
if (e.keyCode == 13) {
LayerList.closeOptionsMenu();
}
return;
}
//if no document has been created yet or there is a dialog box open ignore hotkeys
if (!EditorState.documentCreated()) return;
if (e.key === "Escape") {
Events.emit("esc-pressed");
}
else if (!Dialogue.isOpen()){
switch (keyboardEvent.keyCode) {
//pencil tool - 1, b
case 49: case 66:
Events.emit("tool-shortcut", "brush");
break;
// copy tool c
case 67: case 99:
if (keyboardEvent.ctrlKey) {
Events.emit("ctrl+c");
}
break;
//fill tool - 2, f
case 50: case 70:
Events.emit("tool-shortcut", "fill");
break;
//eyedropper - 3, e
case 51: case 69:
Events.emit("tool-shortcut", "eyedropper");
break;
//pan - 4, p,
case 52: case 80:
Events.emit("tool-shortcut", "pan");
break;
// line - l
case 76:
Events.emit("tool-shortcut", "line");
break;
// eraser -6, r
case 54: case 82:
Events.emit("tool-shortcut", "eraser");
break;
// Rectangular selection m
case 77: case 109:
Events.emit("tool-shortcut", "rectselect");
break;
// TODO: [ELLIPSE] Decide on a shortcut to use. "s" was chosen without any in-team consultation.
// ellipse tool, s
case 83:
//Events.emit("tool-shortcut", "ellipse");
break;
// rectangle tool, u
case 85:
Events.emit("tool-shortcut", "rectangle");
break;
// Paste tool
case 86: case 118:
if (keyboardEvent.ctrlKey) {
Events.emit("ctrl+v");
}
break;
case 88: case 120:
if (keyboardEvent.ctrlKey) {
Events.emit("ctrl+x");
}
break;
//Z
case 90: case 122:
//CTRL+ALT+Z redo
if (keyboardEvent.altKey && keyboardEvent.ctrlKey) {
History.redo();
}
//CTRL+Z undo
else if (keyboardEvent.ctrlKey) {
History.undo();
}
break;
//redo - ctrl y
case 89:
if (keyboardEvent.ctrlKey)
History.redo();
break;
case 32:
spacePressed = true;
break;
case 46:
console.log("Pressed del");
Events.emit("del");
break;
}
}
}
function isDragging() {
return dragging;
}
function getCurrMouseEvent() {
return currentMouseEvent;
}
function isAltPressed() {
return altPressed;
}
function isCtrlPressed() {
return ctrlPressed;
}
function isSpacePressed() {
return spacePressed;
}
function getLastTarget() {
return lastMouseTarget;
}
return {
isDragging,
getCurrMouseEvent,
getCursorPosition,
isAltPressed,
isCtrlPressed,
isSpacePressed,
getLastTarget
}
})();

417
js/LayerList.js Normal file
View File

@ -0,0 +1,417 @@
const LayerList = (() => {
let layerList = document.getElementById("layers-menu");
let layerListEntry = layerList.firstElementChild;
let renamingLayer = false;
let dragStartLayer;
// Binding the right click menu
Events.on("mousedown", layerList, openOptionsMenu);
// Binding the add layer button to the right function
Events.on('click',"add-layer-button", addLayer, false);
// Listening to the switch mode event so I can change the layout
Events.onCustom("switchedToAdvanced", showMenu);
Events.onCustom("switchedToBasic", hideMenu);
// Making the layers list sortable
new Sortable(layerList, {
animation: 100,
filter: ".layer-button",
draggable: ".layers-menu-entry",
onStart: layerDragStart,
onEnd: layerDragDrop
});
function showMenu() {
layerList.style.display = "inline-block";
document.getElementById('layer-button').style.display = 'inline-block';
}
function hideMenu() {
if (EditorState.documentCreated()) {
// Selecting the current layer
currFile.currentLayer.selectLayer();
// Flatten the layers
flatten(true);
}
layerList.style.display = "none";
document.getElementById('layer-button').style.display = 'none';
}
function addLayer(id, saveHistory = true) {
// layers.length - 3
let index = currFile.layers.length - 3;
// Creating a new canvas
let newCanvas = document.createElement("canvas");
// Setting up the new canvas
currFile.canvasView.append(newCanvas);
Layer.maxZIndex+=2;
newCanvas.style.zIndex = Layer.maxZIndex;
newCanvas.classList.add("drawingCanvas");
if (!layerListEntry) return console.warn('skipping adding layer because no document');
// Clone the default layer
let toAppend = layerListEntry.cloneNode(true);
// Setting the default name for the layer
toAppend.getElementsByTagName('p')[0].innerHTML = "Layer " + Layer.layerCount;
// Removing the selected class
toAppend.classList.remove("selected-layer");
// Adding the layer to the list
Layer.layerCount++;
// Creating a layer object
let newLayer = new Layer(currFile.canvasSize[0], currFile.canvasSize[1], newCanvas, toAppend);
newLayer.context.fillStyle = currFile.currentLayer.context.fillStyle;
newLayer.copyData(currFile.currentLayer);
currFile.layers.splice(index, 0, newLayer);
// Insert it before the Add layer button
layerList.insertBefore(toAppend, layerList.childNodes[0]);
if (id != null && typeof(id) == "string") {
newLayer.setID(id);
}
// Basically "if I'm not adding a layer because redo() is telling meto do so", then I can save the history
if (saveHistory) {
new HistoryState().AddLayer(newLayer, index);
}
return newLayer;
}
/** 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) {
// Copying the above content on the layerBelow
let belowImageData = belowLayer.getImageData(0, 0, belowLayer.canvas.width, belowLayer.canvas.height);
let toMergeImageData = topLayer.getImageData(0, 0, topLayer.canvas.width, topLayer.canvas.height);
for (let i=0; i<belowImageData.data.length; i+=4) {
let currentMovePixel = [
toMergeImageData.data[i], toMergeImageData.data[i+1],
toMergeImageData.data[i+2], toMergeImageData.data[i+3]
];
let currentUnderlyingPixel = [
belowImageData.data[i], belowImageData.data[i+1],
belowImageData.data[i+2], belowImageData.data[i+3]
];
if (Util.isPixelEmpty(currentMovePixel)) {
if (!Util.isPixelEmpty(belowImageData)) {
toMergeImageData.data[i] = currentUnderlyingPixel[0];
toMergeImageData.data[i+1] = currentUnderlyingPixel[1];
toMergeImageData.data[i+2] = currentUnderlyingPixel[2];
toMergeImageData.data[i+3] = currentUnderlyingPixel[3];
}
}
}
// Putting the top data into the belowdata
belowLayer.putImageData(toMergeImageData, 0, 0);
}
/** 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;
Events.simulateMouseEvent(window, "mouseup");
}
/** Saves the layer that is being moved when the dragging starts
*
* @param {*} event
*/
function layerDragStart(event) {
dragStartLayer = getLayerByID(layerList.children[event.oldIndex].id);
}
// Finds a layer given its id
function getLayerByID(id) {
for (let i=0; i<currFile.layers.length; i++) {
if (currFile.layers[i].hasCanvas()) {
if (currFile.layers[i].menuEntry.id == id) {
return currFile.layers[i];
}
}
}
return null;
}
// Finds a layer given its name
function getLayerByName(name) {
for (let i=0; i<currFile.layers.length; i++) {
if (currFile.layers[i].hasCanvas()) {
if (currFile.layers[i].menuEntry.getElementsByTagName("p")[0].innerHTML == name) {
return currFile.layers[i];
}
}
}
return null;
}
function startRenamingLayer(event) {
let p = currFile.currentLayer.menuEntry.getElementsByTagName("p")[0];
currFile.currentLayer.oldLayerName = p.innerHTML;
p.setAttribute("contenteditable", true);
p.classList.add("layer-name-editable");
p.focus();
Events.simulateInput(65, true, false, false);
renamingLayer = true;
}
function duplicateLayer(event, saveHistory = true) {
function getMenuEntryIndex(list, entry) {
for (let i=0; i<list.length; i++) {
if (list[i] === entry) {
return i;
}
}
return -1;
}
let layerIndex = currFile.layers.indexOf(currFile.currentLayer);
let toDuplicate = currFile.currentLayer;
let menuEntries = layerList.children;
// Increasing z-indexes of the layers above
for (let i=getMenuEntryIndex(menuEntries, toDuplicate.menuEntry) - 1; i>=0; i--) {
LayerList.getLayerByID(menuEntries[i].id).canvas.style.zIndex++;
}
Layer.maxZIndex+=2;
// Creating a new canvas
let newCanvas = document.createElement("canvas");
// Setting up the new canvas
currFile.canvasView.append(newCanvas);
newCanvas.style.zIndex = parseInt(currFile.currentLayer.canvas.style.zIndex) + 2;
newCanvas.classList.add("drawingCanvas");
if (!layerListEntry) return console.warn('skipping adding layer because no document');
// Clone the default layer
let toAppend = currFile.currentLayer.menuEntry.cloneNode(true);
// Setting the default name for the layer
toAppend.getElementsByTagName('p')[0].innerHTML += " copy";
// Removing the selected class
toAppend.classList.remove("selected-layer");
// Adding the layer to the list
Layer.layerCount++;
// Creating a layer object
let newLayer = new Layer(currFile.canvasSize[0], currFile.canvasSize[1], newCanvas, toAppend);
newLayer.context.fillStyle = currFile.currentLayer.context.fillStyle;
newLayer.copyData(currFile.currentLayer);
currFile.layers.splice(layerIndex, 0, newLayer);
// Insert it before the Add layer button
layerList.insertBefore(toAppend, currFile.currentLayer.menuEntry);
// Copy the layer content
newLayer.context.putImageData(currFile.currentLayer.context.getImageData(
0, 0, currFile.canvasSize[0], currFile.canvasSize[1]), 0, 0);
newLayer.updateLayerPreview();
// Basically "if I'm not adding a layer because redo() is telling meto do so", then I can save the history
if (saveHistory) {
new HistoryState().DuplicateLayer(newLayer, currFile.currentLayer);
}
}
function deleteLayer(saveHistory = true) {
// Cannot delete all the layers
if (currFile.layers.length != 4) {
let layerIndex = currFile.layers.indexOf(currFile.currentLayer);
let toDelete = currFile.layers[layerIndex];
let previousSibling = toDelete.menuEntry.previousElementSibling;
// Adding the ids to the unused ones
Layer.unusedIDs.push(toDelete.id);
// Selecting the next layer
if (layerIndex != (currFile.layers.length - 4)) {
currFile.layers[layerIndex + 1].selectLayer();
}
// or the previous one if the next one doesn't exist
else {
currFile.layers[layerIndex - 1].selectLayer();
}
// Deleting canvas and entry
toDelete.canvas.remove();
toDelete.menuEntry.remove();
// Removing the layer from the list
currFile.layers.splice(layerIndex, 1);
if (saveHistory) {
new HistoryState().DeleteLayer(toDelete, previousSibling, layerIndex);
}
}
// Closing the menu
closeOptionsMenu();
}
function merge(saveHistory = true) {
// Saving the layer that should be merged
let toMerge = currFile.currentLayer;
let toMergeIndex = currFile.layers.indexOf(toMerge);
// Getting layer below
let layerBelow = LayerList.getLayerByID(currFile.currentLayer.menuEntry.nextElementSibling.id);
// If I have something to merge with
if (layerBelow != null) {
// Selecting that layer
layerBelow.selectLayer();
if (saveHistory) {
new HistoryState().MergeLayer(toMergeIndex, toMerge,
layerBelow.context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]),
layerBelow);
}
LayerList.mergeLayers(currFile.currentLayer.context, toMerge.context);
// Deleting the above layer
toMerge.canvas.remove();
toMerge.menuEntry.remove();
currFile.layers.splice(toMergeIndex, 1);
// Updating the layer preview
currFile.currentLayer.updateLayerPreview();
}
}
function flatten(onlyVisible) {
if (!onlyVisible) {
// Selecting the first layer
let firstLayer = layerList.firstElementChild;
let nToFlatten = layerList.childElementCount - 1;
LayerList.getLayerByID(firstLayer.id).selectLayer();
for (let i = 0; i < nToFlatten; i++) {
merge();
}
new HistoryState().FlattenAll(nToFlatten);
}
else {
// Getting all the visible layers
let visibleLayers = [];
let nToFlatten = 0;
for (let i=0; i<currFile.layers.length; i++) {
if (currFile.layers[i].hasCanvas() && currFile.layers[i].isVisible) {
visibleLayers.push(currFile.layers[i]);
}
}
// Sorting them by z-index
visibleLayers.sort((a, b) => (a.canvas.style.zIndex > b.canvas.style.zIndex) ? -1 : 1);
// Selecting the last visible layer (the only one that won't get deleted)
visibleLayers[visibleLayers.length - 1].selectLayer();
// Merging all the layer but the last one
for (let i=0; i<visibleLayers.length - 1; i++) {
nToFlatten++;
new HistoryState().FlattenTwoVisibles(
visibleLayers[i + 1].context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]),
visibleLayers[i].menuEntry.nextElementSibling,
currFile.layers.indexOf(visibleLayers[i]),
visibleLayers[i], visibleLayers[i + 1]
);
LayerList.mergeLayers(visibleLayers[i + 1].context, visibleLayers[i].context);
// Deleting the above layer
visibleLayers[i].canvas.remove();
visibleLayers[i].menuEntry.remove();
currFile.layers.splice(currFile.layers.indexOf(visibleLayers[i]), 1);
}
new HistoryState().FlattenVisible(nToFlatten);
// Updating the layer preview
currFile.currentLayer.updateLayerPreview();
}
}
function openOptionsMenu(event) {
if (event.which == 3) {
let selectedId;
let target = event.target;
while (target != null && target.classList != null && !target.classList.contains("layers-menu-entry")) {
target = target.parentElement;
}
selectedId = target.id;
Layer.layerOptions.style.visibility = "visible";
Layer.layerOptions.style.top = "0";
Layer.layerOptions.style.marginTop = "" + (event.clientY - 25) + "px";
getLayerByID(selectedId).selectLayer(false);
}
}
function closeOptionsMenu(event) {
Layer.layerOptions.style.visibility = "hidden";
currFile.currentLayer.rename();
renamingLayer = false;
}
function getLayerListEntries() {
return layerList;
}
function isRenamingLayer() {
return renamingLayer;
}
return {
addLayer,
mergeLayers,
getLayerByID,
getLayerByName,
renameLayer: startRenamingLayer,
duplicateLayer,
deleteLayer,
merge,
flatten,
closeOptionsMenu,
getLayerListEntries,
isRenamingLayer
}
})();

299
js/PaletteBlock.js Normal file
View File

@ -0,0 +1,299 @@
const PaletteBlock = (() => {
// HTML elements
let coloursList = document.getElementById("palette-list");
// PaletteBlock-specific data
let currentSquareSize = coloursList.children[0].clientWidth;
let blockData = {blockWidth: 300, blockHeight: 320, squareSize: 40};
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);
Events.on("click", "pb-addcolours", addColours);
Events.on("click", "pb-removecolours", removeColours);
/** 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);
}
});
// Initializes the palette block
function init() {
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(Color.cssToHex(simplePalette.children[i].children[0].style.backgroundColor));
}
}
/** 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 = Color.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.bind(this));
li.addEventListener("mouseup", endRampSelection.bind(this));
li.addEventListener("mousemove", updateRampSelection.bind(this));
li.addEventListener("onclick", endRampSelection.bind(this));
coloursList.appendChild(li);
}
}
/** Adds all the colours currently selected in the colour picker
*
*/
function addColours() {
let colours = ColorPicker.getSelectedColours();
for (let i=0; i<colours.length; i++) {
addSingleColour(colours[i]);
}
}
/** Removes all the currently selected colours from the palette
*
*/
function removeColours() {
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();
}
/** 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);
}
else if (mouseEvent.which == 1) {
endRampSelection(mouseEvent);
}
}
/** Updates the outline for the current selection.
*
* @param {*} mouseEvent
*/
function updateRampSelection(mouseEvent) {
if (mouseEvent != null && mouseEvent.buttons == 2) {
currentSelection.endIndex = getElementIndex(mouseEvent.target);
}
if (mouseEvent == null || mouseEvent.buttons == 2) {
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 = Color.cssToHex(coloursList.children[currentSelection.startIndex].style.backgroundColor);
ColorPicker.updatePickerByHex(col);
ColorPicker.updateSlidersByHex(col);
ColorPicker.updateMiniPickerColour();
updateRampSelection();
currentSelection.startCoords = [];
}
/** 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();
}
return {
init
}
})();

View File

@ -17,6 +17,7 @@ const PresetModule = (() => {
presetsMenu.appendChild(button);
button.addEventListener('click', () => {
console.log("Preset: " + presetName);
//change dimentions on new pixel form
Util.setValue('size-width', presets[presetName].width);
Util.setValue('size-height', presets[presetName].height);
@ -30,7 +31,6 @@ const PresetModule = (() => {
//set the text of the dropdown to the newly selected preset
Util.setText('preset-button', presetName);
});
});

66
js/Settings.js Normal file
View File

@ -0,0 +1,66 @@
const Settings = (() => {
let settings;
let settingsFromCookie;
//on clicking the save button in the settings dialog
Events.on('click', 'save-settings', saveSettings);
init();
function init() {
if (!Cookies.enabled) {
document.getElementById('cookies-disabled-warning').style.display = 'block';
}
settingsFromCookie = Cookies.get('pixelEditorSettings');
if(!settingsFromCookie) {
console.log('settings cookie not found');
settings = {
switchToChangedColor: true,
enableDynamicCursorOutline: true, //unused - performance
enableBrushPreview: true, //unused - performance
enableEyedropperPreview: true, //unused - performance
numberOfHistoryStates: 256,
maxColorsOnImportedImage: 128,
pixelGridColour: '#000000'
};
}
else{
console.log('settings cookie found');
console.log(settingsFromCookie);
settings = JSON.parse(settingsFromCookie);
}
}
function saveSettings() {
//check if values are valid
if (isNaN(Util.getValue('setting-numberOfHistoryStates'))) {
alert('Invalid value for numberOfHistoryStates');
return;
}
//save new settings to settings object
settings.numberOfHistoryStates = Util.getValue('setting-numberOfHistoryStates');
settings.pixelGridColour = Util.getValue('setting-pixelGridColour');
// Filling pixel grid again if colour changed
Events.emit("refreshPixelGrid");
//save settings object to cookie
let cookieValue = JSON.stringify(settings);
Cookies.set('pixelEditorSettings', cookieValue, { expires: Infinity });
//close window
Dialogue.closeDialogue();
}
function getCurrSettings() {
return settings;
}
return {
getCurrSettings
}
})();

39
js/SplashPage.js Normal file
View File

@ -0,0 +1,39 @@
const SplashPage = (() => {
const images = [
new SplashCoverImage('Rayquaza', 'Unsettled', 'https://lospec.com/unsettled'),
new SplashCoverImage('Mountains', 'Skeddles', 'https://lospec.com/skeddles'),
new SplashCoverImage('Sweetie', 'GrafxKid', 'https://twitter.com/GrafxKid'),
new SplashCoverImage('Glacier', 'WindfallApples', 'https://lospec.com/windfallapples'),
new SplashCoverImage('Polyphorge1', 'Polyphorge', 'https://lospec.com/poly-phorge'),
new SplashCoverImage('Fusionnist', 'Fusionnist', 'https://lospec.com/fusionnist')
];
const coverImage = document.getElementById('editor-logo');
const authorLink = coverImage.getElementsByTagName('a')[0];
const chosenImage = images[Math.round(Math.random() * (images.length - 1))];
initSplashPage();
function initSplashPage() {
coverImage.style.backgroundImage = 'url("' + chosenImage.path + '.png")';
authorLink.setAttribute('href', chosenImage.link);
authorLink.innerHTML = 'Art by ' + chosenImage.author;
Dialogue.showDialogue("splash", false);
}
function SplashCoverImage(path, author, link) {
this.path = path;
this.author = author;
this.link = link;
}
return {
}
})();

238
js/Startup.js Normal file
View File

@ -0,0 +1,238 @@
const Startup = (() => {
let splashPostfix = '';
Events.on('click', 'create-button', create, false);
Events.on('click', 'create-button-splash', create, true);
function create(isSplash) {
// If I'm creating from the splash menu, I append '-splash' so I get the corresponding values
if (isSplash)
splashPostfix = '-splash';
else
splashPostfix = '';
var width = Util.getValue('size-width' + splashPostfix);
var height = Util.getValue('size-height' + splashPostfix);
var selectedPalette = Util.getText('palette-button' + splashPostfix);
newPixel(width, height);
resetInput();
//track google event
if (typeof ga !== 'undefined')
ga('send', 'event', 'Pixel Editor New', selectedPalette, width+'/'+height); /*global ga*/
}
/** Creates a new, empty file
*
* @param {*} width Start width of the canvas
* @param {*} height Start height of the canvas
* @param {*} fileContent If fileContent != null, then the newPixel is being called from the open menu
*/
function newPixel (width, height, fileContent = null) {
// The palette is empty, at the beginning
ColorModule.resetPalette();
initLayers(width, height);
initPalette();
// Closing the "New Pixel dialogue"
Dialogue.closeDialogue();
// Updating the cursor of the current tool
ToolManager.currentTool().updateCursor();
// The user is now able to export the Pixel
document.getElementById('export-button').classList.remove('disabled');
// Now, if I opened an LPE file
if (fileContent != null) {
loadFromLPE(fileContent);
// Deleting the default layer
LayerList.deleteLayer(false);
// Selecting the new one
currFile.layers[1].selectLayer();
}
EditorState.switchMode(EditorState.getCurrentMode());
// This is not the first Pixel anymore
EditorState.created();
}
function initLayers(width, height) {
// Setting the general canvasSize
currFile.canvasSize = [width, height];
// If this is the first pixel I'm creating since the app has started
if (EditorState.firstPixel()) {
// Creating the first layer
currFile.currentLayer = new Layer(width, height, 'pixel-canvas', "");
currFile.currentLayer.canvas.style.zIndex = 2;
}
else {
// Deleting all the extra layers and canvases, leaving only one
let nLayers = currFile.layers.length;
for (let i=2; i < currFile.layers.length - nAppLayers; i++) {
let currentEntry = currFile.layers[i].menuEntry;
let associatedLayer;
if (currentEntry != null) {
// Getting the associated layer
associatedLayer = LayerList.getLayerByID(currentEntry.id);
// Deleting its canvas
associatedLayer.canvas.remove();
// Adding the id to the unused ones
Layer.unusedIDs.push(currentEntry.id);
// Removing the entry from the menu
currentEntry.remove();
}
}
// Removing the old layers from the list
for (let i=2; i<nLayers - nAppLayers; i++) {
currFile.layers.splice(2, 1);
}
// Setting up the current layer
currFile.layers[1] = new Layer(width, height, currFile.layers[1].canvas, currFile.layers[1].menuEntry);
currFile.currentLayer = currFile.layers[1];
currFile.currentLayer.canvas.style.zIndex = 2;
}
// Adding the checkerboard behind it
currFile.checkerBoard = new Checkerboard(width, height, null);
// Pixel grid
console.log("CREATED GRID");
currFile.pixelGrid = new PixelGrid(width, height, "pixel-grid");
// Creating the vfx layer on top of everything
currFile.VFXLayer = new Layer(width, height, 'vfx-canvas');
// Tmp layer to draw previews on
currFile.TMPLayer = new Layer(width, height, 'tmp-canvas');
if (EditorState.firstPixel()) {
// Adding the first layer and the checkerboard to the list of layers
currFile.layers.push(currFile.checkerBoard);
currFile.layers.push(currFile.currentLayer);
currFile.layers.push(currFile.TMPLayer);
currFile.layers.push(currFile.pixelGrid);
currFile.layers.push(currFile.VFXLayer);
}
}
function initPalette() {
// Get selected palette
let selectedPalette = Util.getText('palette-button' + splashPostfix);
//remove current palette
let colors = document.getElementsByClassName('color-button');
while (colors.length > 0) {
colors[0].parentElement.remove();
}
// If the user selected a palette and isn't opening a file, I load the selected palette
if (selectedPalette != 'Choose a palette...') {
if (selectedPalette === 'Loaded palette') {
ColorModule.createColorPalette(palettes['Loaded palette'].colors);
}
else {
//if this palette isnt the one specified in the url, then reset the url
if (!palettes[selectedPalette].specified)
history.pushState(null, null, '/pixel-editor');
//fill the palette with specified colours
ColorModule.createColorPalette(palettes[selectedPalette].colors);
}
}
// Otherwise, I just generate 2 semirandom colours
else {
//this wasn't a specified palette, so reset the url
history.pushState(null, null, '/pixel-editor');
//generate default colors
var fg = new Color("hsv", Math.floor(Math.random()*360), 50, 50).rgb;
var bg = new Color("hsv", Math.floor(Math.random()*360), 80, 100).rgb;
//convert colors to hex
var defaultForegroundColor = Color.rgbToHex(fg);
var defaultBackgroundColor = Color.rgbToHex(bg);
//add colors to palette
ColorModule.addColor(defaultForegroundColor).classList.add('selected');
ColorModule.addColor(defaultBackgroundColor);
//set current drawing color as foreground color
ColorModule.updateCurrentColor('#'+defaultForegroundColor);
selectedPalette = 'none';
}
}
function loadFromLPE(fileContent) {
// I add every layer the file had in it
for (let i=0; i<fileContent['nLayers']; i++) {
let layerData = fileContent['layer' + i];
let layerImage = fileContent['layer' + i + 'ImageData'];
if (layerData != null) {
// Setting id
let createdLayer = LayerList.addLayer(layerData.id, false);
// Setting name
createdLayer.menuEntry.getElementsByTagName("p")[0].innerHTML = layerData.name;
// Adding the image (I can do that because they're sorted by increasing z-index)
let img = new Image();
img.onload = function() {
createdLayer.context.drawImage(img, 0, 0);
createdLayer.updateLayerPreview();
if (i == (fileContent['nLayers'] - 1)) {
ColorModule.createPaletteFromLayers();
}
};
img.src = layerImage;
// Setting visibility and lock options
if (!layerData.isVisible) {
createdLayer.hide();
}
if (layerData.isLocked) {
createdLayer.lock();
}
}
}
}
function resetInput() {
//reset new form
Util.setValue('size-width', 64);
Util.setValue('size-height', 64);
Util.setText('palette-button', 'Choose a palette...');
Util.setText('preset-button', 'Choose a preset...');
}
function newFromTemplate(preset, x, y) {
if (preset != '') {
const presetProperties = PresetModule.propertiesOf(preset);
Util.setText('palette-button-splash', presetProperties.palette);
Util.setText('palette-button', presetProperties.palette);
x = presetProperties.width;
y = presetProperties.height;
}
newPixel(x, y);
}
function splashEditorMode(mode) {
editorMode = mode;
}
return {
create,
newPixel,
newFromTemplate,
splashEditorMode
}
})();

126
js/Tool.js Normal file
View File

@ -0,0 +1,126 @@
// REFACTOR: this is a nice base for the Tool class
//tools container / list, automatically managed when you create a new Tool();
var tool = {};
//class for tools
class Tool {
name = "AbstractTool";
isSelected = false;
// Cursor and brush size
cursorType = {};
cursor = undefined;
cursorHTMLElement = undefined;
// Useful coordinates
startMousePos = {};
currMousePos = {};
prevMousePos = {};
endMousePos = {};
// HTML elements
mainButton = undefined;
biggerButton = undefined;
smallerButton = undefined;
brushPreview = document.getElementById("brush-preview");
constructor (name, options) {
this.name = name;
this.cursorType = options;
this.mainButton = document.getElementById(name + "-button");
this.biggerButton = document.getElementById(name + "-bigger-button");
this.smallerButton = document.getElementById(name + "-smaller-button");
}
onSelect() {
if (this.mainButton != undefined)
this.mainButton.parentElement.classList.add("selected");
this.isSelected = true;
switch (this.cursorType.type) {
case 'html':
currFile.canvasView.style.cursor = 'none';
break;
case 'cursor':
this.cursor = this.cursorType.style;
currFile.canvasView.style.cursor = this.cursor || 'default';
break;
default:
break;
}
}
updateCursor() {
this.brushPreview.style.display = 'block';
this.brushPreview.style.width = this.currSize * currFile.zoom + 'px';
this.brushPreview.style.height = this.currSize * currFile.zoom + 'px';
}
onMouseWheel(mousePos, mode) {}
onHover(cursorLocation, cursorTarget) {
this.prevMousePos = this.currMousePos;
this.currMousePos = cursorLocation;
this.updateCursor();
let toSub = 1;
// Prevents the brush to be put in the middle of pixels
if (this.currSize % 2 == 0) {
toSub = 0.5;
}
this.brushPreview.style.left = (Math.floor(cursorLocation[0] / currFile.zoom) * currFile.zoom + currFile.currentLayer.canvas.offsetLeft - this.currSize * currFile.zoom / 2 - currFile.zoom / 2 + toSub * currFile.zoom) + 'px';
this.brushPreview.style.top = (Math.floor(cursorLocation[1] / currFile.zoom) * currFile.zoom + currFile.currentLayer.canvas.offsetTop - this.currSize * currFile.zoom / 2 - currFile.zoom / 2 + toSub * currFile.zoom) + 'px';
if (this.cursorType.type == 'html') {
if (cursorTarget.className == 'drawingCanvas'|| cursorTarget.className == 'drawingCanvas') {
this.brushPreview.style.visibility = 'visible';
currFile.canvasView.style.cursor = 'none';
}
else {
this.brushPreview.style.visibility = 'hidden';
currFile.canvasView.style.cursor = 'default';
}
}
}
onDeselect() {
if (this.mainButton != undefined)
this.mainButton.parentElement.classList.remove("selected");
this.isSelected = false;
this.brushPreview.style.visibility = 'hidden';
currFile.canvasView.style.cursor = 'default';
}
onStart(mousePos) {
this.startMousePos = mousePos;
}
onDrag(mousePos) {
}
onEnd(mousePos) {
this.endMousePos = mousePos;
}
increaseSize() {
if (this.currSize < 128) {
this.currSize++;
this.updateCursor();
}
}
decreaseSize() {
if (this.currSize > 1) {
this.currSize--;
this.updateCursor();
}
}
get size() {
return this.currSize;
}
}

154
js/ToolManager.js Normal file
View File

@ -0,0 +1,154 @@
const ToolManager = (() => {
tools = {};
isPanning = false;
tools["brush"] = new BrushTool("brush", {type: 'html'}, switchTool);
tools["eraser"] = new EraserTool("eraser", {type: 'html'}, switchTool);
tools["rectangle"] = new RectangleTool("rectangle", {type: 'html'}, switchTool);
tools["line"] = new LineTool("line", {type: 'html'}, switchTool);
tools["fill"] = new FillTool("fill", {type: 'cursor', style: 'crosshair'}, switchTool);
tools["eyedropper"] = new EyedropperTool("eyedropper", {type: 'cursor', style: 'crosshair'}, switchTool);
tools["pan"] = new PanTool("pan", {type: 'custom'}, switchTool);
tools["zoom"] = new ZoomTool("zoom", {type:'custom'});
tools["moveselection"] = new MoveSelectionTool("moveselection",
{type:'cursor', style:'crosshair'}, switchTool, tools["brush"]);
tools["rectselect"] = new RectangularSelectionTool("rectselect",
{type: 'cursor', style:'crosshair'}, switchTool, tools["moveselection"]);
currTool = tools["brush"];
currTool.onSelect();
currFile.canvasView.style.cursor = 'default';
Events.on("mouseup", window, onMouseUp);
Events.on("mousemove", window, onMouseMove);
Events.on("mousedown", window, onMouseDown);
Events.on("wheel", window, onMouseWheel);
// Bind tool shortcuts
Events.onCustom("tool-shortcut", onShortcut);
function onShortcut(tool) {
if (!EditorState.documentCreated || Dialogue.isOpen())
return;
switchTool(tools[tool]);
}
function onMouseWheel(mouseEvent) {
console.log("MOUSE WHEEL");
if (!EditorState.documentCreated || Dialogue.isOpen())
return;
let mousePos = Input.getCursorPosition(mouseEvent);
tools["zoom"].onMouseWheel(mousePos, mouseEvent.deltaY < 0 ? 'in' : 'out');
}
function onMouseDown(mouseEvent) {
if (!EditorState.documentCreated() || Dialogue.isOpen())
return;
let mousePos = Input.getCursorPosition(mouseEvent);
if (!Input.isDragging()) {
switch(mouseEvent.which) {
case 1:
if (Input.isSpacePressed()) {
tools["pan"].onStart(mousePos, mouseEvent.target);
}
else if (Input.isAltPressed()) {
tools["eyedropper"].onStart(mousePos, mouseEvent.target);
}
else if (!currFile.currentLayer.isLocked || !((Object.getPrototypeOf(currTool) instanceof DrawingTool))) {
currTool.onStart(mousePos, mouseEvent.target);
}
break;
case 2:
tools["pan"].onStart(mousePos, mouseEvent.target);
break;
case 3:
currTool.onRightStart(mousePos, mouseEvent.target);
break;
default:
break;
}
}
}
function onMouseMove(mouseEvent) {
if (!EditorState.documentCreated() || Dialogue.isOpen())
return;
let mousePos = Input.getCursorPosition(mouseEvent);
// Call the hover event
currTool.onHover(mousePos, mouseEvent.target);
if (Input.isDragging()) {
switch (mouseEvent.buttons) {
case 1:
if (Input.isSpacePressed()) {
tools["pan"].onDrag(mousePos, mouseEvent.target);
}
else if (Input.isAltPressed()) {
tools["eyedropper"].onDrag(mousePos, mouseEvent.target);
}
else if (!currFile.currentLayer.isLocked || !((Object.getPrototypeOf(currTool) instanceof DrawingTool))){
currTool.onDrag(mousePos, mouseEvent.target);
}
break;
case 4:
tools["pan"].onDrag(mousePos, mouseEvent.target);
break;
case 2:
currTool.onRightDrag(mousePos, mouseEvent.target);
break;
default:
console.log("wtf");
break;
}
}
}
function onMouseUp(mouseEvent) {
if (!EditorState.documentCreated())
return;
let mousePos = Input.getCursorPosition(mouseEvent);
if (Input.isDragging()) {
switch(mouseEvent.which) {
case 1:
if (Input.isSpacePressed()) {
tools["pan"].onEnd(mousePos, mouseEvent.target);
}
else if (Input.isAltPressed()) {
tools["eyedropper"].onEnd(mousePos, mouseEvent.target);
}
else if (!currFile.currentLayer.isLocked || !((Object.getPrototypeOf(currTool) instanceof DrawingTool))) {
currTool.onEnd(mousePos);
}
break;
case 2:
tools["pan"].onEnd(mousePos);
break;
case 3:
currTool.onRightEnd(mousePos, mouseEvent.target);
break;
default:
break;
}
}
}
function currentTool() {
return currTool;
}
function switchTool(newTool) {
currTool.onDeselect();
currTool = newTool;
currTool.onSelect();
}
return {
currentTool
}
})();

99
js/TopMenuModule.js Normal file
View File

@ -0,0 +1,99 @@
const TopMenuModule = (() => {
const mainMenuItems = document.getElementById('main-menu').children;
initMenu();
function initMenu() {
//for each button in main menu (starting at 1 to avoid logo)
for (let i = 1; i < mainMenuItems.length; i++) {
//get the button that's in the list item
const menuItem = mainMenuItems[i];
const menuButton = menuItem.children[0];
//when you click a main menu items button
Events.on('click', menuButton, function (e) {
// Close the already open menus
closeMenu();
// Select the item
Util.select(e.target.parentElement);
});
const subMenu = menuItem.children[1];
const subMenuItems = subMenu.children;
//when you click an item within a menu button
for (var j = 0; j < subMenuItems.length; j++) {
const currSubmenuItem = subMenuItems[j];
const currSubmenuButton = currSubmenuItem.children[0];
switch (currSubmenuButton.textContent) {
case 'New':
Events.on('click', currSubmenuButton, Dialogue.showDialogue, 'new-pixel');
break;
case 'Save project':
Events.on('click', currSubmenuButton, FileManager.openSaveProjectWindow);
break;
case 'Open':
Events.on('click', currSubmenuButton, FileManager.open);
break;
case 'Export':
Events.on('click', currSubmenuButton, FileManager.openPixelExportWindow);
break;
case 'Exit':
//if a document exists, make sure they want to delete it
if (EditorState.documentCreated()) {
//ask user if they want to leave
if (confirm('Exiting will discard your current pixel. Are you sure you want to do that?'))
//skip onbeforeunload prompt
window.onbeforeunload = null;
else
e.preventDefault();
}
break;
// REFACTOR: move the binding to the Selection IIFE or something like that once it's done
case 'Paste':
Events.on('click', currSubmenuButton, function(){Events.emit("ctrl+v");});
break;
case 'Copy':
Events.on('click', currSubmenuButton, function(){Events.emit("ctrl+c");});
break;
case 'Cut':
Events.on('click', currSubmenuButton, function(){Events.emit("ctrl+x");});
break;
case 'Cancel':
Events.on('click', currSubmenuButton, function(){Events.emit("esc-pressed")});
break;
//Help Menu
case 'Settings':
Events.on('click', currSubmenuButton, Dialogue.showDialogue, 'settings');
break;
case 'Help':
Events.on('click', currSubmenuButton, Dialogue.showDialogue, 'help');
break;
case 'About':
Events.on('click', currSubmenuButton, Dialogue.showDialogue, 'about');
break;
case 'Changelog':
Events.on('click', currSubmenuButton, Dialogue.showDialogue, 'changelog');
break;
}
Events.on('click', currSubmenuButton, function() {TopMenuModule.closeMenu();});
}
}
}
function closeMenu () {
//remove .selected class from all menu buttons
for (var i = 0; i < mainMenuItems.length; i++) {
Util.deselect(mainMenuItems[i]);
}
}
return {
closeMenu
}
})();

View File

@ -1,35 +1,89 @@
// Acts as a public static class
class Util {
static getElement(elementOrElementId) {
return typeof elementOrElementId
? document.getElementById(elementOrElementId)
: elementOrElementId;
}
static getText(elementId) {
return this.getElement(elementId).textContent;
/** Tells if a pixel is empty (has alpha = 0)
*
* @param {*} pixel
*/
static isPixelEmpty(pixel) {
if (pixel == null || pixel === undefined) {
return false;
}
// If the alpha channel is 0, the current pixel is empty
if (pixel[3] == 0) {
return true;
}
return false;
}
/** Tells if element is a child of an element with class className
*
* @param {*} element
* @param {*} className
*/
static isChildOfByClass(element, className) {
// Getting the element with class className
while (element != null && element.classList != null && !element.classList.contains(className)) {
element = element.parentElement;
}
// If that element exists and its class is the correct one
if (element != null && element.classList != null && element.classList.contains(className)) {
// Then element is a chld of an element with class className
return true;
}
return false;
}
/** Returns elementOrElementId if the argument is already an element, otherwise it finds
* the element by its ID (given by the argument) and returns it
*
* @param {*} elementOrElementId The element to return, or the ID of the element to return
* @returns The desired element
*/
static getElement(elementOrElementId) {
if (typeof(elementOrElementId) == "object") {
return elementOrElementId;
}
else if (typeof(elementOrElementId) == "string") {
return document.getElementById(elementOrElementId);
}
else {
console.log("Type not supported: " + typeof(elementOrElementId));
}
}
// Returns the text content of the element with ID elementId
static getText(elementId) {
return Util.getElement(elementId).textContent;
}
// Sets the text content of the element with ID elementId
static setText(elementId, text) {
this.getElement(elementId).textContent = text;
Util.getElement(elementId).textContent = text;
}
// Gets the value of the element with ID elementId
static getValue(elementId) {
return this.getElement(elementId).value;
return Util.getElement(elementId).value;
}
// Sets the value of the element with ID elementId
static setValue(elementId, value) {
this.getElement(elementId).value = value;
Util.getElement(elementId).value = value;
}
//add class .selected to specified element
static select(elementId) {
this.getElement(elementId).classList.add('selected');
Util.getElement(elementId).classList.add('selected');
}
//remove .selected class from specified element
static deselect(elementId) {
this.getElement(elementId).classList.remove('selected');
Util.getElement(elementId).classList.remove('selected');
}
//toggle the status of the .selected class on the specified element
static toggle(elementId) {
this.getElement(elementId).classList.toggle('selected');
Util.getElement(elementId).classList.toggle('selected');
}
}

View File

@ -1,56 +0,0 @@
let currentPalette = [];
/** Adds the given color to the palette
*
* @param {*} newColor the colour to add
* @return the list item containing the added colour
*/
function addColor (newColor) {
//add # at beginning if not present
if (newColor.charAt(0) != '#')
newColor = '#' + newColor;
currentPalette.push(newColor);
//create list item
var listItem = document.createElement('li');
//create button
var button = document.createElement('button');
button.classList.add('color-button');
button.style.backgroundColor = newColor;
button.addEventListener('mouseup', clickedColor);
listItem.appendChild(button);
listItem.classList.add("draggable-colour")
//insert new listItem element at the end of the colors menu (right before add button)
colorsMenu.insertBefore(listItem, colorsMenu.children[colorsMenu.children.length-1]);
//add jscolor functionality
initColor(button);
//add edit button
var editButtonTemplate = document.getElementsByClassName('color-edit-button')[0];
newEditButton = editButtonTemplate.cloneNode(true);
listItem.appendChild(newEditButton);
//when you click the edit button
on('click', newEditButton, function (event, button) {
//hide edit button
button.parentElement.lastChild.classList.add('hidden');
//show jscolor picker, if basic mode is enabled
if (pixelEditorMode == 'Basic')
button.parentElement.firstChild.jscolor.show();
else
showDialogue("palette-block", false);
});
return listItem;
}
new Sortable(document.getElementById("colors-menu"), {
animation:100,
filter: ".noshrink",
draggable: ".draggable-colour",
onEnd: makeIsDraggingFalse
});

View File

@ -1,59 +0,0 @@
// add-color-button management
on('click', 'add-color-button', function(){
if (!documentCreated) return;
var colorCheckingStyle = `
color: white;
background: #3c4cc2;
`;
var colorIsUnique = true;
do {
//console.log('%cchecking for unique colors', colorCheckingStyle)
//generate random color
var hue = Math.floor(Math.random()*255);
var sat = 130+Math.floor(Math.random()*100);
var lit = 70+Math.floor(Math.random()*100);
var newColorRgb = hslToRgb(hue,sat,lit);
var newColor = rgbToHex(newColorRgb.r,newColorRgb.g,newColorRgb.b);
var newColorHex = newColor;
//check if color has been used before
colors = document.getElementsByClassName('color-button');
colorCheckingLoop: for (var i = 0; i < colors.length; i++) {
//console.log('%c'+newColorHex +' '+ colors[i].jscolor.toString(), colorCheckingStyle)
//if generated color matches this color
if (newColorHex == colors[i].jscolor.toString()) {
//console.log('%ccolor already exists', colorCheckingStyle)
//start loop again
colorIsUnique = false;
//exit
break colorCheckingLoop;
}
}
}
while (colorIsUnique == false);
//remove current color selection
document.querySelector('#colors-menu li.selected')?.classList.remove('selected');
//add new color and make it selected
var addedColor = addColor(newColor);
addedColor.classList.add('selected');
currentLayer.context.fillStyle = '#' + newColor;
//add history state
//saveHistoryState({type: 'addcolor', colorValue: addedColor.firstElementChild.jscolor.toString()});
new HistoryStateAddColor(addedColor.firstElementChild.jscolor.toString());
//show color picker
addedColor.firstElementChild.jscolor.show();
console.log('showing picker');
//hide edit button
addedColor.lastChild.classList.add('hidden');
}, false);

View File

@ -1,185 +0,0 @@
// 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,40 +0,0 @@
/** 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 oldHeight = canvasSize[1] * zoom;
var newWidth, newHeight;
//change zoom level
//if you want to zoom out, and the zoom isnt already at the smallest level
if (direction == 'out' && zoom > 1) {
zoom -= Math.ceil(zoom / 10);
newWidth = canvasSize[0] * zoom;
newHeight = canvasSize[1] * zoom;
//adjust canvas position
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
else if (direction == 'in' && zoom + Math.ceil(zoom/10) < window.innerHeight/4){
zoom += Math.ceil(zoom/10);
newWidth = canvasSize[0] * zoom;
newHeight = canvasSize[1] * zoom;
//adjust canvas position
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
layers[0].resize();
// adjust brush size
currentTool.updateCursor();
}

View File

@ -1,19 +0,0 @@
/////=include libraries/bowser.js
function closeCompatibilityWarning () {
document.getElementById('compatibility-warning').style.visibility = 'hidden';
}
console.log('checking compatibility');
//check browser/version
if ((bowser.msie && bowser.version < 11) ||
(bowser.firefox && bowser.version < 28) ||
(bowser.chrome && bowser.version < 29) ||
(bowser.msedge && bowser.version < 12) ||
(bowser.safari && bowser.version < 9) ||
(bowser.opera && bowser.version < 17) )
//show warning
document.getElementById('compatibility-warning').style.visibility = 'visible';
else alert(bowser.name+' '+bowser.version+' is fine!');

View File

@ -1,71 +0,0 @@
// This script contains all the functions used to manage the checkboard
// Checkerboard color 1
var firstCheckerBoardColor = 'rgba(179, 173, 182, 1)';
// Checkerboard color 2
var secondCheckerBoardColor = 'rgba(204, 200, 206, 1)';
// Square size for the checkerboard
var checkerBoardSquareSize = 16;
// Checkerboard canvas
var checkerBoardCanvas = document.getElementById('checkerboard');
// Setting current colour (each square has a different colour
var currentColor = firstCheckerBoardColor;
// Saving number of squares filled until now
var nSquaresFilled = 0;
/** Fills the checkerboard canvas with squares with alternating colours
*
*/
function fillCheckerboard() {
// Getting checkerboard context
var context = checkerBoard.context;
context.clearRect(0, 0, canvasSize[0], canvasSize[1]);
// Cycling through the canvas (using it as a matrix)
for (var i=0; i<canvasSize[0] / checkerBoardSquareSize; i++) {
nSquaresFilled = 0;
for (var j=0; j<canvasSize[1] / checkerBoardSquareSize; j++) {
var rectX;
var rectY;
// Managing the not perfect squares (the ones at the sides if the canvas' sizes are not powers of checkerBoardSquareSize
if (i * checkerBoardSquareSize < canvasSize[0]) {
rectX = i * checkerBoardSquareSize;
}
else {
rectX = canvasSize[0];
}
if (j * checkerBoardSquareSize < canvasSize[1]) {
rectY = j * checkerBoardSquareSize;
}
else {
rectY = canvasSize[1];
}
// Selecting the colour
context.fillStyle = currentColor;
context.fillRect(rectX, rectY, checkerBoardSquareSize, checkerBoardSquareSize);
// Changing colour
changeCheckerboardColor();
nSquaresFilled++;
}
// If the number of filled squares was even, I change colour for the next column
if ((nSquaresFilled % 2) == 0) {
changeCheckerboardColor();
}
}
}
// Simply switches the checkerboard colour
function changeCheckerboardColor(isVertical) {
if (currentColor == firstCheckerBoardColor) {
currentColor = secondCheckerBoardColor;
} else if (currentColor == secondCheckerBoardColor) {
currentColor = firstCheckerBoardColor;
}
}

View File

@ -1,28 +0,0 @@
//color in palette has been clicked
function clickedColor (e){
//left clicked color
if (e.which == 1) {
// remove current color selection
document.querySelector('#colors-menu li.selected')?.classList.remove('selected');
//set current color
for (let i=1; i<layers.length - nAppLayers; i++) {
layers[i].context.fillStyle = this.style.backgroundColor;
}
currentGlobalColor = this.style.backgroundColor;
//make color selected
e.target.parentElement.classList.add('selected');
} else if (e.which == 3) { //right clicked color
//console.log('right clicked color button');
//hide edit color button (to prevent it from showing)
e.target.parentElement.lastChild.classList.add('hidden');
//show color picker
e.target.jscolor.show();
}
}

View File

@ -1,101 +0,0 @@
document.getElementById('jscolor-hex-input').addEventListener('change', colorChanged, false);
on('input', 'jscolor-hex-input', function (e) {
//get hex value
var newColorHex = e.target.value.toLowerCase();
//if the color is not (yet) a valid hex color, exit this function and do nothing
if (/^[0-9a-f]{6}$/i.test(newColorHex) == false)
return;
//get currently editing color
var currentlyEditedColor = document.getElementsByClassName('jscolor-active')[0];
//update the actual color picker to the inputted color
currentlyEditedColor.firstChild.jscolor.fromString(newColorHex);
colorChanged(e);
});
//changes all of one color to another after being changed from color picker
function colorChanged(e) {
//console.log('colorChanged() to ' + e.target.value);
//get colors
var newColor = hexToRgb(e.target.value);
var oldColor = e.target.oldColor;
currentPalette.splice(currentPalette.indexOf("#" + newColor), 1);
newColor.a = 255;
//save undo state
new HistoryStateEditColor(e.target.value.toLowerCase(), rgbToHex(oldColor));
//get the currently selected color
var currentlyEditedColor = document.getElementsByClassName('jscolor-active')[0];
var duplicateColorWarning = document.getElementById('duplicate-color-warning');
//check if selected color already matches another color
colors = document.getElementsByClassName('color-button');
//console.log(colors);
var colorCheckingStyle = 'background: #bc60c4; color: white';
var newColorHex = e.target.value.toLowerCase();
//if the color is not a valid hex color, exit this function and do nothing
if (/^[0-9a-f]{6}$/i.test(newColorHex) == false)
return;
//loop through all colors in palette
for (var i = 0; i < colors.length; i++) {
//if generated color matches this color
if (newColorHex == colors[i].jscolor.toString()) {
//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 (!colors[i].parentElement.classList.contains('jscolor-active')) {
//console.log('%cColor is duplicate', colorCheckingStyle);
//show the duplicate color warning
duplicateColorWarning.style.visibility = 'visible';
//shake warning icon
duplicateColorWarning.classList.remove('shake');
void duplicateColorWarning.offsetWidth;
duplicateColorWarning.classList.add('shake');
//exit function without updating color
return;
}
}
}
//if the color being edited has a duplicate color warning, remove it
duplicateColorWarning.style.visibility = 'hidden';
currentlyEditedColor.firstChild.jscolor.fromString(newColorHex);
replaceAllOfColor(oldColor, newColor);
//set new old color to changed color
e.target.oldColor = newColor;
currentPalette.push('#' + newColorHex);
//if this is the current color, update the drawing color
if (e.target.colorElement.parentElement.classList.contains('selected')) {
for (let i=1; i<layers.length - nAppLayers; i++) {
layers[i].context.fillStyle = '#'+ rgbToHex(newColor.r,newColor.g,newColor.b);
}
currentGlobalColor = newColor;
}
/* this is wrong and bad
if (settings.switchToChangedColor) {
}*/
}

View File

@ -1,806 +0,0 @@
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,2 +0,0 @@
const MIN_Z_INDEX = -5000;
const MAX_Z_INDEX = 5000;

View File

@ -1,85 +0,0 @@
// Data saved when copying or cutting
let clipboardData;
// Tells if the user is pasting something or not
let isPasting = false;
// Coordinates of the copied (or cut) selection
let copiedStartX;
let copiedStartY;
let copiedEndX;
let copiedEndY;
/** Copies the current selection to the clipboard
*
*/
function copySelection() {
copiedEndX = endX;
copiedEndY = endY;
copiedStartX = startX;
copiedStartY = startY;
// Getting the selected pixels
clipboardData = currentLayer.context.getImageData(startX, startY, endX - startX + 1, endY - startY + 1);
}
/** Pastes the clipboard data onto the current layer
*
*/
function pasteSelection() {
// Can't paste if the layer is locked
if (currentLayer.isLocked) {
return;
}
// Cancel the current selection
endSelection();
// I'm pasting
isPasting = true;
// Putting the image data on the tmp layer
TMPLayer.context.putImageData(clipboardData, copiedStartX, copiedStartY);
// Setting up the move tool to move the pasted value
selectionCanceled = false;
imageDataToMove = clipboardData;
firstTimeMove = false;
isRectSelecting = false;
// Switching to the move tool
tool.moveselection.switchTo();
// Updating the rectangle preview
moveSelection(
copiedStartX + (copiedEndX - copiedStartX) / 2,
copiedStartY + (copiedEndY - copiedStartY) / 2,
clipboardData.width, clipboardData.height);
//drawRect(copiedStartX, copiedEndX, copiedStartY, copiedEndY);
}
/** Cuts the current selection and copies it to the clipboard
*
*/
function cutSelectionTool() {
// Saving the coordinates
copiedEndX = endX;
copiedEndY = endY;
copiedStartX = startX;
copiedStartY = startY;
// Getting the selected pixels
// If I'm already moving a selection
if (imageDataToMove !== undefined) {
// I just save that selection in the clipboard
clipboardData = imageDataToMove;
// And clear the underlying space
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;
}
else {
// 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);
}
}

View File

@ -1,96 +0,0 @@
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 : pixelEditorMode;
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);
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 (){
// Getting the values of the form
var width = getValue('size-width');
var height = getValue('size-height');
// Creating a new pixel with those properties
newPixel(width, height);
document.getElementById('new-pixel-warning').style.display = 'block';
//get selected palette name
var selectedPalette = getText('palette-button');
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);
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-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);
setText('palette-button', 'Choose a palette...');
setText('preset-button', 'Choose a preset...');
});

View File

@ -1,90 +0,0 @@
/** 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
if (deletePreviousPalette) {
colors = document.getElementsByClassName('color-button');
while (colors.length > 0) {
colors[0].parentElement.remove();
}
}
var lightestColor = '#000000';
var darkestColor = '#ffffff';
// Adding all the colours in the array
for (var i = 0; i < paletteColors.length; i++) {
var newColor = paletteColors[i];
var newColorElement = addColor(newColor);
var newColorHex = hexToRgb(newColor);
var lightestColorHex = hexToRgb(lightestColor);
if (newColorHex.r + newColorHex.g + newColorHex.b > lightestColorHex.r + lightestColorHex.g + lightestColorHex.b)
lightestColor = newColor;
var darkestColorHex = hexToRgb(darkestColor);
if (newColorHex.r + newColorHex.g + newColorHex.b < darkestColorHex.r + darkestColorHex.g + darkestColorHex.b) {
//remove current color selection
document.querySelector('#colors-menu li.selected')?.classList.remove('selected');
//set as current color
newColorElement.classList.add('selected');
darkestColor = newColor;
}
}
//prepend # if not present
if (!darkestColor.includes('#')) darkestColor = '#' + darkestColor;
//set as current color
currentLayer.context.fillStyle = darkestColor;
}
/** Creates the palette with the colours used in all the layers
*
*/
function createPaletteFromLayers() {
let colors = {};
for (let i=0; i<layers.length; i++) {
if (layers[i].menuEntry != null) {
let imageData = layers[i].context.getImageData(0, 0, layers[i].canvasSize[0], layers[i].canvasSize[1]).data;
let dataLength = imageData.length;
for (let j=0; j<dataLength; j += 4) {
if (!isPixelEmpty(imageData[j])) {
let color = imageData[j]+','+imageData[j + 1]+','+imageData[j + 2];
if (!colors[color]) {
colors[color] = {r:imageData[j],g:imageData[j + 1],b:imageData[j + 2]};
//don't allow more than 256 colors to be added
if (Object.keys(colors).length >= settings.maxColorsOnImportedImage) {
alert('The image loaded seems to have more than '+settings.maxColorsOnImportedImage+' colors.');
break;
}
}
}
}
}
}
//create array out of colors object
let colorPaletteArray = [];
for (let color in colors) {
if (colors.hasOwnProperty(color)) {
colorPaletteArray.push('#'+rgbToHex(colors[color]));
}
}
//create palette from colors array
createColorPalette(colorPaletteArray, true);
}

View File

@ -1,84 +0,0 @@
//called when the delete button is pressed on color picker
//input color button or hex string
function deleteColor (color) {
const logStyle = 'background: #913939; color: white; padding: 5px;';
//console.log('%c'+'deleting color', logStyle);
//if color is a string, then find the corresponding button
if (typeof color === 'string') {
//console.log('trying to find ',color);
//get all colors in palette
colors = document.getElementsByClassName('color-button');
//loop through colors
for (var i = 0; i < colors.length; i++) {
//console.log(color,'=',colors[i].jscolor.toString());
if (color == colors[i].jscolor.toString()) {
//console.log('match');
//set color to the color button
color = colors[i];
//console.log('found color', color);
//exit loop
break;
}
}
//if the color wasn't found
if (typeof color === 'string') {
//console.log('color not found');
//exit function
return;
}
}
//hide color picker
color.jscolor.hide();
//find lightest color in palette
var colors = document.getElementsByClassName('color-button');
var lightestColor = [0,null];
for (var i = 0; i < colors.length; i++) {
//get colors lightness
var lightness = rgbToHsl(colors[i].jscolor.toRgb()).l;
//console.log('%c'+lightness, logStyle)
//if not the color we're deleting
if (colors[i] != color) {
//if lighter than the current lightest, set as the new lightest
if (lightness > lightestColor[0]) {
lightestColor[0] = lightness;
lightestColor[1] = colors[i];
}
}
}
//console.log('%c'+'replacing with lightest color: '+lightestColor[1].jscolor.toString(), logStyle)
//replace deleted color with lightest color
replaceAllOfColor(color.jscolor.toString(),lightestColor[1].jscolor.toString());
//if the color you are deleting is the currently selected color
if (color.parentElement.classList.contains('selected')) {
//console.log('%c'+'deleted color is currently selected', logStyle);
//set current color TO LIGHTEST COLOR
lightestColor[1].parentElement.classList.add('selected');
currentLayer.context.fillStyle = '#'+lightestColor[1].jscolor.toString();
}
//delete the element
colorsMenu.removeChild(color.parentElement);
}

View File

@ -1,64 +0,0 @@
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) {
if (typeof trackEvent === 'undefined') trackEvent = true;
// Updating currently open dialogue
currentOpenDialogue = dialogueName;
// The pop up window is open
dialogueOpen = true;
// Showing the pop up container
popUpContainer.style.display = 'block';
// Showing the window
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
if (trackEvent)
ga('send', 'event', 'Palette Editor Dialogue', dialogueName); /*global ga*/
}
/** Closes the current dialogue by hiding the window and the pop-up-container
*
*/
function closeDialogue () {
popUpContainer.style.display = 'none';
var popups = popUpContainer.children;
for (var i = 0; i < popups.length; i++) {
popups[i].style.display = 'none';
}
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) {
if (e.target == popUpContainer)
closeDialogue();
});
//add click handlers for all cancel buttons
var cancelButtons = popUpContainer.getElementsByClassName('close-button');
for (var i = 0; i < cancelButtons.length; i++) {
cancelButtons[i].addEventListener('click', function () {
closeDialogue();
});
}

View File

@ -1,34 +0,0 @@
//draws a line between two points on canvas
function line(x0,y0,x1,y1, brushSize) {
var dx = Math.abs(x1-x0);
var dy = Math.abs(y1-y0);
var sx = (x0 < x1 ? 1 : -1);
var sy = (y0 < y1 ? 1 : -1);
var err = dx-dy;
while (true) {
//set pixel
// If the current tool is the brush
if (currentTool.name == 'pencil' || currentTool.name == 'rectangle' || currentTool.name == 'ellipse') {
// I fill the rect
currentLayer.context.fillRect(x0-Math.floor(brushSize/2), y0-Math.floor(brushSize/2), brushSize, brushSize);
} else if (currentTool.name == 'eraser') {
// In case I'm using the eraser I must clear the rect
currentLayer.context.clearRect(x0-Math.floor(tool.eraser.brushSize/2), y0-Math.floor(tool.eraser.brushSize/2), tool.eraser.brushSize, tool.eraser.brushSize);
}
//if we've reached the end goal, exit the loop
if ((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,74 +0,0 @@
let modes = {
'Basic' : {
description: 'Basic mode is perfect if you want to create simple sprites or try out palettes.'
},
'Advanced' : {
description: 'Choose advanced mode to gain access to more advanced features such as layers.'
}
}
on('click', 'switch-editor-mode-splash', function (e) {
console.log('switching mode')
switchMode();
});
function switchMode(mustConfirm = true) {
console.log('switching mode', 'current:',pixelEditorMode)
//switch to advanced mode
if (pixelEditorMode == 'Basic') {
// Switch to advanced ez pez lemon squez
document.getElementById('switch-mode-button').innerHTML = 'Switch to basic mode';
// Show the layer menus
layerList.style.display = "inline-block";
document.getElementById('layer-button').style.display = 'inline-block';
// Hide the palette menu
document.getElementById('colors-menu').style.right = '200px'
//change splash text
document.querySelector('#sp-quickstart-container .mode-switcher').classList.add('advanced-mode');
pixelEditorMode = 'Advanced';
//turn pixel grid off
togglePixelGrid('off');
}
//switch to basic mode
else {
//if there is a current layer (a document is active)
if (currentLayer) {
//confirm with user before flattening image
if (mustConfirm ) {
if (!confirm('Switching to basic mode will flatten all the visible layers. Are you sure you want to continue?')) {
return;
}
}
// Selecting the current layer
currentLayer.selectLayer();
// Flatten the layers
flatten(true);
}
//change menu text
document.getElementById('switch-mode-button').innerHTML = 'Switch to advanced mode';
// Hide the layer menus
layerList.style.display = 'none';
document.getElementById('layer-button').style.display = 'none';
// Show the palette menu
document.getElementById('colors-menu').style.display = 'flex';
// Move the palette menu
document.getElementById('colors-menu').style.right = '0px';
//change splash text
document.querySelector('#sp-quickstart-container .mode-switcher').classList.remove('advanced-mode');
pixelEditorMode = 'Basic';
togglePixelGrid('on');
}
}
on('click', 'switch-mode-button', function (e) {
switchMode();
});

View File

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

View File

@ -1,155 +0,0 @@
var mainMenuItems = document.getElementById('main-menu').children;
//for each button in main menu (starting at 1 to avoid logo)
for (var i = 1; i < mainMenuItems.length; i++) {
//get the button that's in the list item
var menuItem = mainMenuItems[i];
var menuButton = menuItem.children[0];
//when you click a main menu items button
on('click', menuButton, function (e, button) {
select(button.parentElement);
});
var subMenu = menuItem.children[1];
var subMenuItems = subMenu.children;
//when you click an item within a menu button
for (var j = 0; j < subMenuItems.length; j++) {
var subMenuItem = subMenuItems[j];
var subMenuButton = subMenuItem.children[0];
subMenuButton.addEventListener('click', function (e) {
switch(this.textContent) {
//File Menu
case 'New':
showDialogue('new-pixel');
break;
case 'Save project':
openSaveProjectWindow();
break;
case 'Open':
//if a document exists
if (documentCreated) {
//check if the user wants to overwrite
if (confirm('Opening a pixel will discard your current one. Are you sure you want to do that?'))
//open file selection dialog
document.getElementById('open-image-browse-holder').click();
}
else
//open file selection dialog
document.getElementById('open-image-browse-holder').click();
break;
case 'Export':
openPixelExportWindow();
break;
case 'Exit':
console.log('exit');
//if a document exists, make sure they want to delete it
if (documentCreated) {
//ask user if they want to leave
if (confirm('Exiting will discard your current pixel. Are you sure you want to do that?'))
//skip onbeforeunload prompt
window.onbeforeunload = null;
else
e.preventDefault();
}
break;
//Edit Menu
case 'Undo':
undo();
break;
case 'Redo':
redo();
break;
//Palette Menu
case 'Add color':
addColor('#eeeeee');
break;
// SELECTION MENU
case 'Paste':
pasteSelection();
break;
case 'Copy':
copySelection();
tool.pencil.switchTo();
break;
case 'Cut':
cutSelectionTool();
tool.pencil.switchTo();
break;
case 'Cancel':
tool.pencil.switchTo();
break;
//Help Menu
case 'Settings':
//fill form with current settings values
setValue('setting-numberOfHistoryStates', settings.numberOfHistoryStates);
showDialogue('settings');
break;
//Help Menu
case 'Help':
showDialogue('help');
break;
case 'About':
showDialogue('about');
break;
case 'Changelog':
showDialogue('changelog');
break;
}
closeMenu();
});
}
}
function closeMenu () {
//remove .selected class from all menu buttons
for (var i = 0; i < mainMenuItems.length; i++) {
deselect(mainMenuItems[i]);
}
}
function getProjectData() {
// use a dictionary
let dictionary = {};
// sorting layers by increasing z-index
let layersCopy = layers.slice();
layersCopy.sort((a, b) => (a.canvas.style.zIndex > b.canvas.style.zIndex) ? 1 : -1);
// save canvas size
dictionary['canvasWidth'] = currentLayer.canvasSize[0];
dictionary['canvasHeight'] = currentLayer.canvasSize[1];
// save editor mode
dictionary['editorMode'] = pixelEditorMode;
// save palette
for (let i=0; i<currentPalette.length; i++) {
dictionary["color" + i] = currentPalette[i];
}
// save number of layers
dictionary["nLayers"] = layersCopy.length;
// save layers
for (let i=0; i<layersCopy.length; i++) {
// Only saving the layers the user has access to (no vfx, tmp or checkerboard layers)
if (layersCopy[i].menuEntry != null) {
dictionary["layer" + i] = layersCopy[i];
dictionary["layer" + i + "ImageData"] = layersCopy[i].canvas.toDataURL();
}
}
return JSON.stringify(dictionary);
}

View File

@ -1,111 +0,0 @@
function fill(cursorLocation) {
//changes a pixels color
function colorPixel(tempImage, pixelPos, fillColor) {
//console.log('colorPixel:',pixelPos);
tempImage.data[pixelPos] = fillColor.r;
tempImage.data[pixelPos + 1] = fillColor.g;
tempImage.data[pixelPos + 2] = fillColor.b;
tempImage.data[pixelPos + 3] = 255;
}
//change x y to color value passed from the function and use that as the original color
function matchStartColor(tempImage, pixelPos, color) {
//console.log('matchPixel:',x,y)
var r = tempImage.data[pixelPos];
var g = tempImage.data[pixelPos + 1];
var b = tempImage.data[pixelPos + 2];
var a = tempImage.data[pixelPos + 3];
//console.log(r == color[0] && g == color[1] && b == color[2]);
return (r == color[0] && g == color[1] && b == color[2] && a == color[3]);
}
//save history state
new HistoryStateEditCanvas();
//saveHistoryState({type: 'canvas', canvas: context.getImageData(0, 0, canvasSize[0], canvasSize[1])});
//console.log('filling at '+ Math.floor(cursorLocation[0]/zoom) + ','+ Math.floor(cursorLocation[1]/zoom));
//temporary image holds the data while we change it
var tempImage = currentLayer.context.getImageData(0, 0, canvasSize[0], canvasSize[1]);
//this is an array that holds all of the pixels at the top of the cluster
var topmostPixelsArray = [[Math.floor(cursorLocation[0]/zoom), Math.floor(cursorLocation[1]/zoom)]];
//console.log('topmostPixelsArray:',topmostPixelsArray)
//the offset of the pixel in the temp image data to start with
var startingPosition = (topmostPixelsArray[0][1] * canvasSize[0] + topmostPixelsArray[0][0]) * 4;
//the color of the cluster that is being filled
var clusterColor = [tempImage.data[startingPosition],tempImage.data[startingPosition+1],tempImage.data[startingPosition+2], tempImage.data[startingPosition+3]];
//the new color to fill with
var fillColor = hexToRgb(currentLayer.context.fillStyle);
console.log("here");
//if you try to fill with the same color that's already there, exit the function
if (clusterColor[0] == fillColor.r &&
clusterColor[1] == fillColor.g &&
clusterColor[2] == fillColor.b &&
clusterColor[3] != 0) {
console.log("Returned");
return;
}
//loop until there are no more values left in this array
while (topmostPixelsArray.length) {
var reachLeft, reachRight;
//move the most recent pixel from the array and set it as our current working pixels
var currentPixel = topmostPixelsArray.pop();
//set the values of this pixel to x/y variables just for readability
var x = currentPixel[0];
var y = currentPixel[1];
//this variable holds the index of where the starting values for the current pixel are in the data array
//we multiply the number of rows down (y) times the width of each row, then add x. at the end we multiply by 4 because
//each pixel has 4 values, rgba
var pixelPos = (y * canvasSize[0] + x) * 4;
//move up in the image until you reach the top or the pixel you hit was not the right color
while (y-- >= 0 && matchStartColor(tempImage, pixelPos, clusterColor)) {
pixelPos -= canvasSize[0] * 4;
}
pixelPos += canvasSize[0] * 4;
++y;
reachLeft = false;
reachRight = false;
while (y++ < canvasSize[1] - 1 && matchStartColor(tempImage, pixelPos, clusterColor)) {
colorPixel(tempImage, pixelPos, fillColor);
if (x > 0) {
if (matchStartColor(tempImage, pixelPos - 4, clusterColor)) {
if (!reachLeft) {
topmostPixelsArray.push([x - 1, y]);
reachLeft = true;
}
}
else if (reachLeft) {
reachLeft = false;
}
}
if (x < canvasSize[0] - 1) {
if (matchStartColor(tempImage, pixelPos + 4, clusterColor)) {
if (!reachRight) {
topmostPixelsArray.push([x + 1, y]);
reachRight = true;
}
}
else if (reachRight) {
reachRight = false;
}
}
pixelPos += canvasSize[0] * 4;
}
}
currentLayer.context.putImageData(tempImage, 0, 0);
//console.log('done filling')
}

View File

@ -1,19 +0,0 @@
//gets cursor position relative to canvas
function getCursorPosition(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 -= currentLayer.canvas.offsetLeft;
y -= currentLayer.canvas.offsetTop;
return [Math.round(x), Math.round(y)];
}

View File

@ -1,529 +0,0 @@
/** 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 redoStates = [];
const undoLogStyle = 'background: #87ff1c; color: black; padding: 5px;';
function HistoryStateResizeSprite(xRatio, yRatio, algo, oldData) {
this.xRatio = xRatio;
this.yRatio = yRatio;
this.algo = algo;
this.oldData = oldData;
this.undo = function() {
let layerIndex = 0;
currentAlgo = algo;
resizeSprite(null, [1 / this.xRatio, 1 / this.yRatio]);
// Also putting the old data
for (let i=0; i<layers.length; i++) {
if (layers[i].menuEntry != null) {
layers[i].context.putImageData(this.oldData[layerIndex], 0, 0);
layerIndex++;
layers[i].updateLayerPreview();
}
}
redoStates.push(this);
};
this.redo = function() {
console.log("REDOOOO");
console.log("Ratio: " + this.xRatio + "," + this.yRatio);
currentAlgo = algo;
resizeSprite(null, [this.xRatio, this.yRatio]);
undoStates.push(this);
};
saveHistoryState(this);
}
function HistoryStateResizeCanvas(newSize, oldSize, imageDatas, trim) {
this.oldSize = oldSize;
this.newSize = newSize;
this.imageDatas = imageDatas;
this.trim = trim;
this.undo = function() {
let dataIndex = 0;
// Resizing the canvas
resizeCanvas(null, oldSize, null, false);
// Putting the image datas
for (let i=0; i<layers.length; i++) {
if (layers[i].menuEntry != null) {
layers[i].context.putImageData(this.imageDatas[dataIndex], 0, 0);
dataIndex++;
}
}
redoStates.push(this);
};
this.redo = function() {
if (!this.trim) {
resizeCanvas(null, newSize, null, false);
}
else {
trimCanvas(null, false);
}
undoStates.push(this);
};
saveHistoryState(this);
}
function HistoryStateFlattenVisible(flattened) {
this.nFlattened = flattened;
this.undo = function() {
for (let i=0; i<this.nFlattened; i++) {
undo();
}
redoStates.push(this);
};
this.redo = function() {
for (let i=0; i<this.nFlattened; i++) {
redo();
}
undoStates.push(this);
};
saveHistoryState(this);
}
function HistoryStateFlattenTwoVisibles(belowImageData, afterAbove, layerIndex, aboveLayer, belowLayer) {
this.aboveLayer = aboveLayer;
this.belowLayer = belowLayer;
this.belowImageData = belowImageData;
this.undo = function() {
canvasView.append(aboveLayer.canvas);
layerList.insertBefore(aboveLayer.menuEntry, afterAbove);
belowLayer.context.clearRect(0, 0, belowLayer.canvasSize[0], belowLayer.canvasSize[1]);
belowLayer.context.putImageData(this.belowImageData, 0, 0);
belowLayer.updateLayerPreview();
layers.splice(layerIndex, 0, aboveLayer);
redoStates.push(this);
};
this.redo = function() {
mergeLayers(belowLayer.context, aboveLayer.context);
// Deleting the above layer
aboveLayer.canvas.remove();
aboveLayer.menuEntry.remove();
layers.splice(layers.indexOf(aboveLayer), 1);
undoStates.push(this);
};
saveHistoryState(this);
}
function HistoryStateFlattenAll(nFlattened) {
this.nFlattened = nFlattened;
this.undo = function() {
for (let i=0; i<this.nFlattened - nAppLayers; i++) {
undo();
}
redoStates.push(this);
};
this.redo = function() {
for (let i=0; i<this.nFlattened - nAppLayers; i++) {
redo();
}
undoStates.push(this);
};
saveHistoryState(this);
}
function HistoryStateMergeLayer(aboveIndex, aboveLayer, belowData, belowLayer) {
this.aboveIndex = aboveIndex;
this.belowData = belowData;
this.aboveLayer = aboveLayer;
this.belowLayer = belowLayer;
this.undo = function() {
layerList.insertBefore(this.aboveLayer.menuEntry, this.belowLayer.menuEntry);
canvasView.append(this.aboveLayer.canvas);
belowLayer.context.clearRect(0, 0, this.belowLayer.canvasSize[0], this.belowLayer.canvasSize[1]);
belowLayer.context.putImageData(this.belowData, 0, 0);
belowLayer.updateLayerPreview();
layers.splice(this.aboveIndex, 0, this.aboveLayer);
redoStates.push(this);
};
this.redo = function() {
aboveLayer.selectLayer();
merge(false);
undoStates.push(this);
};
saveHistoryState(this);
}
function HistoryStateRenameLayer(oldName, newName, layer) {
this.edited = layer;
this.oldName = oldName;
this.newName = newName;
this.undo = function() {
layer.menuEntry.getElementsByTagName("p")[0].innerHTML = oldName;
redoStates.push(this);
};
this.redo = function() {
layer.menuEntry.getElementsByTagName("p")[0].innerHTML = newName;
undoStates.push(this);
};
saveHistoryState(this);
}
function HistoryStateDuplicateLayer(addedLayer, copiedLayer) {
this.addedLayer = addedLayer;
this.copiedLayer = copiedLayer;
this.undo = function() {
addedLayer.selectLayer();
deleteLayer(false);
redoStates.push(this);
};
this.redo = function() {
copiedLayer.selectLayer();
duplicateLayer(null, false);
undoStates.push(this);
};
saveHistoryState(this);
}
function HistoryStateDeleteLayer(layerData, before, index) {
this.deleted = layerData;
this.before = before;
this.index = index;
this.undo = function() {
canvasView.append(this.deleted.canvas);
if (this.before != null) {
layerList.insertBefore(this.deleted.menuEntry, this.before);
}
else {
layerList.prepend(this.deleted.menuEntry);
}
layers.splice(this.index, 0, this.deleted);
redoStates.push(this);
};
this.redo = function() {
this.deleted.selectLayer();
deleteLayer(false);
undoStates.push(this);
};
saveHistoryState(this);
}
function HistoryStateMoveTwoLayers(layer, oldIndex, newIndex) {
this.layer = layer;
this.oldIndex = oldIndex;
this.newIndex = newIndex;
this.undo = function() {
layer.canvas.style.zIndex = oldIndex;
redoStates.push(this);
};
this.redo = function() {
layer.canvas.style.zIndex = newIndex;
undoStates.push(this);
};
saveHistoryState(this);
}
function HistoryStateMoveLayer(afterToDrop, toDrop, staticc, nMoved) {
this.beforeToDrop = afterToDrop;
this.toDrop = toDrop;
this.undo = function() {
toDrop.menuEntry.remove();
if (afterToDrop != null) {
layerList.insertBefore(toDrop.menuEntry, afterToDrop)
}
else {
layerList.append(toDrop.menuEntry);
}
for (let i=0; i<nMoved; i++) {
undo();
}
redoStates.push(this);
};
this.redo = function() {
moveLayers(toDrop.menuEntry.id, staticc.menuEntry.id, true);
undoStates.push(this);
};
saveHistoryState(this);
}
function HistoryStateAddLayer(layerData, index) {
this.added = layerData;
this.index = index;
this.undo = function() {
redoStates.push(this);
if (layers.length - nAppLayers > this.index + 1) {
layers[this.index + 1].selectLayer();
}
else {
layers[this.index - 1].selectLayer();
}
this.added.canvas.remove();
this.added.menuEntry.remove();
layers.splice(index, 1);
};
this.redo = function() {
undoStates.push(this);
canvasView.append(this.added.canvas);
layerList.prepend(this.added.menuEntry);
layers.splice(this.index, 0, this.added);
};
saveHistoryState(this);
}
//prototype for undoing canvas changes
function HistoryStateEditCanvas () {
this.canvasState = currentLayer.context.getImageData(0, 0, canvasSize[0], canvasSize[1]);
this.layerID = currentLayer.id;
this.undo = function () {
var stateLayer = getLayerByID(this.layerID);
var currentCanvas = stateLayer.context.getImageData(0, 0, canvasSize[0], canvasSize[1]);
stateLayer.context.putImageData(this.canvasState, 0, 0);
this.canvasState = currentCanvas;
redoStates.push(this);
stateLayer.updateLayerPreview();
};
this.redo = function () {
console.log("YEET");
var stateLayer = getLayerByID(this.layerID);
var currentCanvas = stateLayer.context.getImageData(0, 0, canvasSize[0], canvasSize[1]);
stateLayer.context.putImageData(this.canvasState, 0, 0);
this.canvasState = currentCanvas;
undoStates.push(this);
stateLayer.updateLayerPreview();
};
//add self to undo array
saveHistoryState(this);
}
//prototype for undoing added colors
function HistoryStateAddColor (colorValue) {
this.colorValue = colorValue;
this.undo = function () {
redoStates.push(this);
deleteColor(this.colorValue);
};
this.redo = function () {
addColor(this.colorValue);
undoStates.push(this);
};
//add self to undo array
saveHistoryState(this);
}
//prototype for undoing deleted colors
function HistoryStateDeleteColor (colorValue) {
this.colorValue = colorValue;
this.canvas = currentLayer.context.getImageData(0, 0, canvasSize[0], canvasSize[1]);
this.undo = function () {
var currentCanvas = currentLayer.context.getImageData(0, 0, canvasSize[0], canvasSize[1]);
currentLayer.context.putImageData(this.canvas, 0, 0);
addColor(this.colorValue);
this.canvas = currentCanvas;
redoStates.push(this);
};
this.redo = function () {
var currentCanvas = currentLayer.context.getImageData(0, 0, canvasSize[0], canvasSize[1]);
currentLayer.context.putImageData(this.canvas, 0, 0);
deleteColor(this.colorValue);
this.canvas = currentCanvas;
undoStates.push(this);
};
//add self to undo array
saveHistoryState(this);
}
//prototype for undoing colors edits
function HistoryStateEditColor (newColorValue, oldColorValue) {
this.newColorValue = newColorValue;
this.oldColorValue = oldColorValue;
this.canvas = currentLayer.context.getImageData(0, 0, canvasSize[0], canvasSize[1]);
this.undo = function () {
var currentCanvas = currentLayer.context.getImageData(0, 0, canvasSize[0], canvasSize[1]);
currentLayer.context.putImageData(this.canvas, 0, 0);
//find new color in palette and change it back to old color
var colors = document.getElementsByClassName('color-button');
for (var i = 0; i < colors.length; i++) {
//console.log(newColorValue, '==', colors[i].jscolor.toString());
if (newColorValue == colors[i].jscolor.toString()) {
colors[i].jscolor.fromString(oldColorValue);
break;
}
}
this.canvas = currentCanvas;
redoStates.push(this);
};
this.redo = function () {
var currentCanvas = currentLayer.context.getImageData(0, 0, canvasSize[0], canvasSize[1]);
currentLayer.context.putImageData(this.canvas, 0, 0);
//find old color in palette and change it back to new color
var colors = document.getElementsByClassName('color-button');
for (var i = 0; i < colors.length; i++) {
//console.log(oldColorValue, '==', colors[i].jscolor.toString());
if (oldColorValue == colors[i].jscolor.toString()) {
colors[i].jscolor.fromString(newColorValue);
break;
}
}
this.canvas = currentCanvas;
undoStates.push(this);
};
//add self to undo array
saveHistoryState(this);
}
//rename to add undo state
function saveHistoryState (state) {
//get current canvas data and save to undoStates array
undoStates.push(state);
//limit the number of states to settings.numberOfHistoryStates
if (undoStates.length > settings.numberOfHistoryStates) {
undoStates = undoStates.splice(-settings.numberOfHistoryStates, settings.numberOfHistoryStates);
}
//there is now definitely at least 1 undo state, so the button shouldnt be disabled
document.getElementById('undo-button').classList.remove('disabled');
//there should be no redoStates after an undoState is saved
redoStates = [];
}
function undo () {
//if there are any states saved to undo
if (undoStates.length > 0) {
document.getElementById('redo-button').classList.remove('disabled');
//get state
var undoState = undoStates[undoStates.length-1];
//console.log(undoState);
//remove from the undo list
undoStates.splice(undoStates.length-1,1);
//restore the state
undoState.undo();
//if theres none left to undo, disable the option
if (undoStates.length == 0)
document.getElementById('undo-button').classList.add('disabled');
}
}
function redo () {
if (redoStates.length > 0) {
//enable undo button
document.getElementById('undo-button').classList.remove('disabled');
//get state
var redoState = redoStates[redoStates.length-1];
//remove from redo array (do this before restoring the state, else the flatten state will break)
redoStates.splice(redoStates.length-1,1);
//restore the state
redoState.redo();
//if theres none left to redo, disable the option
if (redoStates.length == 0)
document.getElementById('redo-button').classList.add('disabled');
}
//console.log(undoStates);
//console.log(redoStates);
}

View File

@ -1,126 +0,0 @@
var spacePressed = false;
/** Just listens to hotkeys and calls the linked functions
*
* @param {*} e
*/
function KeyPress(e) {
var keyboardEvent = window.event? event : e;
//if the user is typing in an input field or renaming a layer, ignore these hotkeys, unless it's an enter key
if (document.activeElement.tagName == 'INPUT' || isRenamingLayer) {
if (e.keyCode == 13) {
currentLayer.closeOptionsMenu();
}
return;
}
//if no document has been created yet,
//orthere is a dialog box open
//ignore hotkeys
if (!documentCreated || dialogueOpen) return;
//
if (e.key === "Escape") {
if (!selectionCanceled) {
tool.pencil.switchTo();
}
}
else {
switch (keyboardEvent.keyCode) {
//pencil tool - 1, b
case 49: case 66:
tool.pencil.switchTo();
break;
// copy tool c
case 67: case 99:
if (keyboardEvent.ctrlKey && !dragging && currentTool.name == 'moveselection') {
copySelection();
}
break;
//fill tool - 2, f
case 50: case 70:
tool.fill.switchTo();
break;
//eyedropper - 3, e
case 51: case 69:
tool.eyedropper.switchTo();
break;
//pan - 4, p,
case 52: case 80:
tool.pan.switchTo();
break;
case 76:
tool.line.switchTo();
break;
//zoom - 5
case 53:
tool.zoom.switchTo();
break;
// eraser -6, r
case 54: case 82:
tool.eraser.switchTo()
break;
// Rectangular selection
case 77: case 109:
tool.rectselect.switchTo()
break;
// TODO: [ELLIPSE] Decide on a shortcut to use. "s" was chosen without any in-team consultation.
// ellipse tool, s
case 83:
tool.ellipse.switchTo()
break;
// rectangle tool, u
case 85:
tool.rectangle.switchTo()
break;
// Paste tool
case 86: case 118:
if (keyboardEvent.ctrlKey && !dragging) {
pasteSelection();
}
break;
case 88: case 120:
if (keyboardEvent.ctrlKey && !dragging && currentTool.name == 'moveselection') {
cutSelectionTool();
tool.pencil.switchTo();
}
break;
//Z
case 90:
//CTRL+ALT+Z redo
if (keyboardEvent.altKey && keyboardEvent.ctrlKey)
redo();
if (!selectionCanceled) {
tool.pencil.switchTo()
}
//CTRL+Z undo
else if (keyboardEvent.ctrlKey) {
undo();
if (!selectionCanceled) {
tool.pencil.switchTo()
}
}
//Z switch to zoom tool
else
tool.zoom.switchTo()
break;
//redo - ctrl y
case 89:
if (keyboardEvent.ctrlKey)
redo();
break;
case 32:
spacePressed=true;
break;
}
}
}
document.onkeydown = KeyPress;
window.addEventListener("keyup", function (e) {
if (e.keyCode == 32) spacePressed = false;
});

View File

@ -1,25 +0,0 @@
// NEXTPULL: to remove when the new palette system is added
//formats a color button
function initColor (colorElement) {
//console.log('initColor()');
//console.log(document.getElementById('jscolor-hex-input'))
//add jscolor picker for this color
colorElement.jscolor = new jscolor(colorElement.parentElement, {
valueElement: null, //if you dont set this to null, it turns the button (colorElement) into text, we set it when you open the picker
styleElement: colorElement,
width:151,
position: 'left',
padding:0,
borderWidth:14,
borderColor: '#332f35',
backgroundColor: '#332f35',
insetColor: 'transparent',
value: colorElement.style.backgroundColor,
deleteButton: true,
});
}

View File

@ -1,613 +0,0 @@
// HTML element that contains the layer entries
let layerList;
// A single layer entry (used as a prototype to create the new ones)
let layerListEntry;
// NEXTPULL: remove the drag n drop system and use Sortable.js instead
let layerDragSource = null;
// Number of layers at the beginning
let layerCount = 1;
// Current max z index (so that I know which z-index to assign to new layers)
let maxZIndex = 3;
// When a layer is deleted, its id is added to this array and can be reused
let unusedIDs = [];
// Id for the next added layer
let currentID = layerCount;
// Layer menu
let layerOptions = document.getElementById("layer-properties-menu");
// Is the user currently renaming a layer?
let isRenamingLayer = false;
// I need to save this, trust me
let oldLayerName = null;
let dragStartLayer;
// Binding the add layer button to the function
on('click',"add-layer-button", addLayer, false);
/** Handler class for a single canvas (a single layer)
*
* @param width Canvas width
* @param height Canvas height
* @param canvas HTML canvas element
*/
class Layer {
constructor(width, height, canvas, menuEntry) {
this.canvasSize = [width, height];
this.canvas = canvas;
this.context = this.canvas.getContext('2d');
this.isSelected = false;
this.isVisible = true;
this.isLocked = false;
this.menuEntry = menuEntry;
let id = unusedIDs.pop();
console.log("id creato: " + id);
if (id == null) {
id = currentID;
currentID++;
}
this.id = "layer" + id;
// Binding the events
if (menuEntry != null) {
this.name = menuEntry.getElementsByTagName("p")[0].innerHTML;
menuEntry.id = "layer" + id;
menuEntry.onmouseover = () => this.hover();
menuEntry.onmouseout = () => this.unhover();
menuEntry.onclick = () => this.selectLayer();
menuEntry.getElementsByTagName("button")[0].onclick = () => this.toggleLock();
menuEntry.getElementsByTagName("button")[1].onclick = () => this.toggleVisibility();
menuEntry.addEventListener("mouseup", this.openOptionsMenu, false);
menuEntry.addEventListener("dragstart", this.layerDragStart, false);
menuEntry.addEventListener("drop", this.layerDragDrop, false);
menuEntry.addEventListener("dragover", this.layerDragOver, false);
menuEntry.addEventListener("dragleave", this.layerDragLeave, false);
menuEntry.addEventListener("dragend", this.layerDragEnd, false);
menuEntry.getElementsByTagName("canvas")[0].getContext('2d').imageSmoothingEnabled = false;
}
this.initialize();
}
// Initializes the canvas
initialize() {
//resize canvas
this.canvas.width = this.canvasSize[0];
this.canvas.height = this.canvasSize[1];
this.canvas.style.width = (this.canvas.width*zoom)+'px';
this.canvas.style.height = (this.canvas.height*zoom)+'px';
//show canvas
this.canvas.style.display = 'block';
//center canvas in window
this.canvas.style.left = 64+canvasView.clientWidth/2-(this.canvasSize[0]*zoom/2)+'px';
this.canvas.style.top = 48+canvasView.clientHeight/2-(this.canvasSize[1]*zoom/2)+'px';
this.context.imageSmoothingEnabled = false;
this.context.mozImageSmoothingEnabled = false;
}
hover() {
// Hides all the layers but the current one
for (let i=1; i<layers.length - nAppLayers; i++) {
if (layers[i] !== this) {
layers[i].canvas.style.opacity = 0.3;
}
}
}
unhover() {
// Shows all the layers again
for (let i=1; i<layers.length - nAppLayers; i++) {
if (layers[i] !== this) {
layers[i].canvas.style.opacity = 1;
}
}
}
setID(id) {
this.id = id;
if (this.menuEntry != null) {
this.menuEntry.id = id;
}
}
// Resizes canvas
resize() {
let newWidth = (this.canvas.width * zoom) + 'px';
let newHeight = (this.canvas.height *zoom)+ 'px';
this.canvas.style.width = newWidth;
this.canvas.style.height = newHeight;
}
setCanvasOffset (offsetLeft, offsetTop) {
//horizontal offset
var minXOffset = -this.canvasSize[0] * zoom;
var maxXOffset = window.innerWidth - 300;
if (offsetLeft < minXOffset)
this.canvas.style.left = minXOffset +'px';
else if (offsetLeft > maxXOffset)
this.canvas.style.left = maxXOffset +'px';
else
this.canvas.style.left = offsetLeft +'px';
//vertical offset
var minYOffset = -this.canvasSize[1] * zoom + 164;
var maxYOffset = window.innerHeight - 100;
if (offsetTop < minYOffset)
this.canvas.style.top = minYOffset +'px';
else if (offsetTop > maxYOffset)
this.canvas.style.top = maxYOffset +'px';
else
this.canvas.style.top = offsetTop +'px';
}
// Copies the otherLayer's position and size
copyData(otherLayer) {
this.canvas.style.width = otherLayer.canvas.style.width;
this.canvas.style.height = otherLayer.canvas.style.height;
this.canvas.style.left = otherLayer.canvas.style.left;
this.canvas.style.top = otherLayer.canvas.style.top;
}
openOptionsMenu(event) {
if (event.which == 3) {
let selectedId;
let target = event.target;
while (target != null && target.classList != null && !target.classList.contains("layers-menu-entry")) {
target = target.parentElement;
}
selectedId = target.id;
layerOptions.style.visibility = "visible";
layerOptions.style.top = "0";
layerOptions.style.marginTop = "" + (event.clientY - 25) + "px";
getLayerByID(selectedId).selectLayer();
}
}
closeOptionsMenu(event) {
layerOptions.style.visibility = "hidden";
currentLayer.menuEntry.getElementsByTagName("p")[0].setAttribute("contenteditable", false);
isRenamingLayer = false;
if (oldLayerName != null) {
let name = this.menuEntry.getElementsByTagName("p")[0].innerHTML;
this.name = name;
new HistoryStateRenameLayer(oldLayerName, name, currentLayer);
oldLayerName = null;
}
}
selectLayer(layer) {
if (layer == null) {
// Deselecting the old layer
currentLayer.deselectLayer();
// Selecting the current layer
this.isSelected = true;
this.menuEntry.classList.add("selected-layer");
currentLayer = getLayerByName(this.menuEntry.getElementsByTagName("p")[0].innerHTML);
}
else {
currentLayer.deselectLayer();
layer.isSelected = true;
layer.menuEntry.classList.add("selected-layer");
currentLayer = layer;
}
}
toggleLock() {
if (this.isLocked) {
this.unlock();
}
else {
this.lock();
}
}
toggleVisibility() {
if (this.isVisible) {
this.hide();
}
else {
this.show();
}
}
deselectLayer() {
this.isSelected = false;
this.menuEntry.classList.remove("selected-layer");
}
lock() {
this.isLocked = true;
this.menuEntry.getElementsByClassName("layer-button")[0].style.visibility = "visible";
this.menuEntry.getElementsByClassName("default-icon")[0].style.display = "none";
this.menuEntry.getElementsByClassName("edited-icon")[0].style.display = "inline-block";
}
unlock() {
this.isLocked = false;
this.menuEntry.getElementsByClassName("layer-button")[0].style.visibility = "hidden";
this.menuEntry.getElementsByClassName("default-icon")[0].style.display = "inline-block";
this.menuEntry.getElementsByClassName("edited-icon")[0].style.display = "none";
}
show() {
this.isVisible = true;
this.canvas.style.visibility = "visible";
this.menuEntry.getElementsByClassName("layer-button")[1].style.visibility = "hidden";
// Changing icon
this.menuEntry.getElementsByClassName("default-icon")[1].style.display = "inline-block";
this.menuEntry.getElementsByClassName("edited-icon")[1].style.display = "none";
}
hide() {
this.isVisible = false;
this.canvas.style.visibility = "hidden";
this.menuEntry.getElementsByClassName("layer-button")[1].style.visibility = "visible";
// Changing icon
this.menuEntry.getElementsByClassName("default-icon")[1].style.display = "none";
this.menuEntry.getElementsByClassName("edited-icon")[1].style.display = "inline-block";
}
updateLayerPreview() {
// Getting the canvas
let destination = this.menuEntry.getElementsByTagName("canvas")[0];
let widthRatio = this.canvasSize[0] / this.canvasSize[1];
let heightRatio = this.canvasSize[1] / this.canvasSize[0];
// Computing width and height for the preview image
let previewWidth = destination.width;
let previewHeight = destination.height;
// If the sprite is rectangular, I apply the ratio to the preview as well
if (widthRatio < 1) {
previewWidth = destination.width * widthRatio;
}
else if (widthRatio > 1) {
previewHeight = destination.height * heightRatio;
}
// La appiccico sulla preview
destination.getContext('2d').clearRect(0, 0, destination.width, destination.height);
destination.getContext('2d').drawImage(this.canvas,
// This is necessary to center the preview in the canvas
(destination.width - previewWidth) / 2, (destination.height - previewHeight) / 2,
previewWidth, previewHeight);
}
}
function flatten(onlyVisible) {
if (!onlyVisible) {
// Selecting the first layer
let firstLayer = layerList.firstElementChild;
let nToFlatten = layerList.childElementCount - 1;
getLayerByID(firstLayer.id).selectLayer();
for (let i = 0; i < nToFlatten; i++) {
merge();
}
new HistoryStateFlattenAll(nToFlatten);
}
else {
// Getting all the visible layers
let visibleLayers = [];
let nToFlatten = 0;
for (let i=0; i<layers.length; i++) {
console.log(layers[i].name);
if (layers[i].menuEntry != null && layers[i].isVisible) {
visibleLayers.push(layers[i]);
}
}
console.log("da piallare: " + visibleLayers.length);
// Sorting them by z-index
visibleLayers.sort((a, b) => (a.canvas.style.zIndex > b.canvas.style.zIndex) ? -1 : 1);
// Selecting the last visible layer (the only one that won't get deleted)
visibleLayers[visibleLayers.length - 1].selectLayer();
// Merging all the layer but the last one
for (let i=0; i<visibleLayers.length - 1; i++) {
nToFlatten++;
console.log(visibleLayers[i].menuEntry.nextElementSibling);
new HistoryStateFlattenTwoVisibles(
visibleLayers[i + 1].context.getImageData(0, 0, visibleLayers[i].canvasSize[0], visibleLayers[i].canvasSize[1]),
visibleLayers[i].menuEntry.nextElementSibling,
layers.indexOf(visibleLayers[i]),
visibleLayers[i], visibleLayers[i + 1]
);
mergeLayers(visibleLayers[i + 1].context, visibleLayers[i].context);
// Deleting the above layer
visibleLayers[i].canvas.remove();
visibleLayers[i].menuEntry.remove();
layers.splice(layers.indexOf(visibleLayers[i]), 1);
}
new HistoryStateFlattenVisible(nToFlatten);
// Updating the layer preview
currentLayer.updateLayerPreview();
}
}
function merge(saveHistory = true) {
// Saving the layer that should be merged
let toMerge = currentLayer;
let toMergeIndex = layers.indexOf(toMerge);
// Getting layer below
let layerBelow = getLayerByID(currentLayer.menuEntry.nextElementSibling.id);
// If I have something to merge with
if (layerBelow != null) {
// Selecting that layer
layerBelow.selectLayer();
if (saveHistory) {
new HistoryStateMergeLayer(toMergeIndex, toMerge,
layerBelow.context.getImageData(0, 0, layerBelow.canvasSize[0], layerBelow.canvasSize[1]),
layerBelow);
}
mergeLayers(currentLayer.context, toMerge.context);
// Deleting the above layer
toMerge.canvas.remove();
toMerge.menuEntry.remove();
layers.splice(toMergeIndex, 1);
// Updating the layer preview
currentLayer.updateLayerPreview();
}
}
function deleteLayer(saveHistory = true) {
// Cannot delete all the layers
if (layers.length != 4) {
let layerIndex = layers.indexOf(currentLayer);
let toDelete = layers[layerIndex];
let previousSibling = toDelete.menuEntry.previousElementSibling;
// Adding the ids to the unused ones
console.log("id cancellato: " + toDelete.id);
unusedIDs.push(toDelete.id);
// Selecting the next layer
if (layerIndex != (layers.length - 4)) {
layers[layerIndex + 1].selectLayer();
}
// or the previous one if the next one doesn't exist
else {
layers[layerIndex - 1].selectLayer();
}
// Deleting canvas and entry
toDelete.canvas.remove();
toDelete.menuEntry.remove();
// Removing the layer from the list
layers.splice(layerIndex, 1);
if (saveHistory) {
new HistoryStateDeleteLayer(toDelete, previousSibling, layerIndex);
}
}
// Closing the menu
currentLayer.closeOptionsMenu();
}
function duplicateLayer(event, saveHistory = true) {
let layerIndex = layers.indexOf(currentLayer);
let toDuplicate = currentLayer;
let menuEntries = layerList.children;
// Increasing z-indexes of the layers above
for (let i=getMenuEntryIndex(menuEntries, toDuplicate.menuEntry) - 1; i>=0; i--) {
getLayerByID(menuEntries[i].id).canvas.style.zIndex++;
}
maxZIndex+=2;
// Creating a new canvas
let newCanvas = document.createElement("canvas");
// Setting up the new canvas
canvasView.append(newCanvas);
newCanvas.style.zIndex = parseInt(currentLayer.canvas.style.zIndex) + 2;
newCanvas.classList.add("drawingCanvas");
if (!layerListEntry) return console.warn('skipping adding layer because no document');
// Clone the default layer
let toAppend = currentLayer.menuEntry.cloneNode(true);
// Setting the default name for the layer
toAppend.getElementsByTagName('p')[0].innerHTML += " copy";
// Removing the selected class
toAppend.classList.remove("selected-layer");
// Adding the layer to the list
layerCount++;
// Creating a layer object
let newLayer = new Layer(currentLayer.canvasSize[0], currentLayer.canvasSize[1], newCanvas, toAppend);
newLayer.context.fillStyle = currentLayer.context.fillStyle;
newLayer.copyData(currentLayer);
layers.splice(layerIndex, 0, newLayer);
// Insert it before the Add layer button
layerList.insertBefore(toAppend, currentLayer.menuEntry);
// Copy the layer content
newLayer.context.putImageData(currentLayer.context.getImageData(
0, 0, currentLayer.canvasSize[0], currentLayer.canvasSize[1]), 0, 0);
newLayer.updateLayerPreview();
// Basically "if I'm not adding a layer because redo() is telling meto do so", then I can save the history
if (saveHistory) {
new HistoryStateDuplicateLayer(newLayer, currentLayer);
}
}
function renameLayer(event) {
let layerIndex = layers.indexOf(currentLayer);
let toRename = currentLayer;
let p = currentLayer.menuEntry.getElementsByTagName("p")[0];
oldLayerName = p.innerHTML;
p.setAttribute("contenteditable", true);
p.classList.add("layer-name-editable");
p.focus();
simulateInput(65, true, false, false);
isRenamingLayer = true;
}
function getMenuEntryIndex(list, entry) {
for (let i=0; i<list.length; i++) {
if (list[i] === entry) {
return i;
}
}
return -1;
}
// Finds a layer given its name
function getLayerByName(name) {
for (let i=0; i<layers.length; i++) {
if (layers[i].menuEntry != null) {
if (layers[i].menuEntry.getElementsByTagName("p")[0].innerHTML == name) {
return layers[i];
}
}
}
return null;
}
// Finds a layer given its id
function getLayerByID(id) {
for (let i=0; i<layers.length; i++) {
if (layers[i].menuEntry != null) {
if (layers[i].menuEntry.id == id) {
return layers[i];
}
}
}
return null;
}
function addLayer(id, saveHistory = true) {
// layers.length - 3
let index = layers.length - 3;
// Creating a new canvas
let newCanvas = document.createElement("canvas");
// Setting up the new canvas
canvasView.append(newCanvas);
maxZIndex+=2;
newCanvas.style.zIndex = maxZIndex;
newCanvas.classList.add("drawingCanvas");
if (!layerListEntry) return console.warn('skipping adding layer because no document');
// Clone the default layer
let toAppend = layerListEntry.cloneNode(true);
// Setting the default name for the layer
toAppend.getElementsByTagName('p')[0].innerHTML = "Layer " + layerCount;
// Removing the selected class
toAppend.classList.remove("selected-layer");
// Adding the layer to the list
layerCount++;
// Creating a layer object
let newLayer = new Layer(currentLayer.canvasSize[0], currentLayer.canvasSize[1], newCanvas, toAppend);
newLayer.context.fillStyle = currentLayer.context.fillStyle;
newLayer.copyData(currentLayer);
layers.splice(index, 0, newLayer);
// Insert it before the Add layer button
layerList.insertBefore(toAppend, layerList.childNodes[0]);
if (id != null && typeof(id) == "string") {
newLayer.setID(id);
}
// Basically "if I'm not adding a layer because redo() is telling meto do so", then I can save the history
if (saveHistory) {
new HistoryStateAddLayer(newLayer, index);
}
return newLayer;
}
/** 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
});

View File

@ -1,45 +0,0 @@
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,55 +0,0 @@
/** Loads a file (.png or .lpe)
*
*/
document.getElementById('open-image-browse-holder').addEventListener('change', function () {
let fileName = document.getElementById("open-image-browse-holder").value;
// Getting the extension
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]) {
// Btw, checking if the extension is supported
if (extension == 'png' || extension == 'gif' || extension == 'lpe') {
// If it's a Lospec Pixel Editor tm file, I load the project
if (extension == 'lpe') {
let file = this.files[0];
let reader = new FileReader();
// Getting all the data
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) {
let dictionary = JSON.parse(e.target.result);
let mode = dictionary['editorMode'] == 'Advanced' ? 'Basic' : 'Advanced';
newPixel(dictionary['canvasWidth'], dictionary['canvasHeight'], mode, dictionary);
for (let i=0; i<dictionary['color' + i] != null; i++) {
addColor(dictionary['color'+i]);
}
}
}
else {
//load file
var fileReader = new FileReader();
fileReader.onload = function(e) {
var img = new Image();
img.onload = function() {
//create a new pixel with the images dimentions
newPixel(this.width, this.height, 'Advanced');
//draw the image onto the canvas
currentLayer.context.drawImage(img, 0, 0);
createPaletteFromLayers();
//track google event
ga('send', 'event', 'Pixel Editor Load', colorPalette.length, this.width+'/'+this.height); /*global ga*/
};
img.src = e.target.result;
};
fileReader.readAsDataURL(this.files[0]);
}
}
else alert('Only .lpe project files, PNG and GIF files are allowed at this time.');
}
});

View File

@ -1,50 +0,0 @@
//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 () {
if (this.files && this.files[0]) {
//make sure file is allowed filetype
var fileContentType = this.files[0].type;
if (fileContentType == 'image/png' || fileContentType == 'image/gif') {
//load file
var fileReader = new FileReader();
fileReader.onload = function(e) {
var img = new Image();
img.onload = function() {
//draw image onto the temporary canvas
var loadPaletteCanvas = document.getElementById('load-palette-canvas-holder');
var loadPaletteContext = loadPaletteCanvas.getContext('2d');
loadPaletteCanvas.width = img.width;
loadPaletteCanvas.height = img.height;
loadPaletteContext.drawImage(img, 0, 0);
//create array to hold found colors
var colorPalette = [];
var imagePixelData = loadPaletteContext.getImageData(0,0,this.width, this.height).data;
//loop through pixels looking for colors to add to palette
for (var i = 0; i < imagePixelData.length; i += 4) {
var color = '#'+rgbToHex(imagePixelData[i],imagePixelData[i + 1],imagePixelData[i + 2]);
if (colorPalette.indexOf(color) == -1) {
colorPalette.push(color);
}
}
//add to palettes so that it can be loaded when they click okay
palettes['Loaded palette'] = {};
palettes['Loaded palette'].colors = colorPalette;
setText('palette-button', 'Loaded palette');
setText('palette-button-splash', 'Loaded palette');
toggle('palette-menu-splash');
};
img.src = e.target.result;
};
fileReader.readAsDataURL(this.files[0]);
}
else alert('Only PNG and GIF files are supported at this time.');
}
});

View File

@ -1,453 +0,0 @@
var currentMouseEvent;
var lastMouseMovePos;
//mousedown - start drawing
window.addEventListener("mousedown", function (mouseEvent) {
// Saving the event in case something else needs it
currentMouseEvent = mouseEvent;
canDraw = true;
//if no document has been created yet, or this is a dialog open, or the currentLayer is locked
if (!documentCreated || dialogueOpen || currentLayer.isLocked || !currentLayer.isVisible) return;
//prevent right mouse clicks and such, which will open unwanted menus
//mouseEvent.preventDefault();
lastMouseClickPos = getCursorPosition(mouseEvent);
dragging = true;
//left or right click ?
if (mouseEvent.which == 1) {
if (spacePressed)
currentTool = tool.pan;
else if (mouseEvent.altKey)
currentTool = tool.eyedropper;
else if (mouseEvent.target.className == 'drawingCanvas' &&
(currentTool.name == 'pencil' || currentTool.name == 'eraser' || currentTool.name == 'rectangle' || currentTool.name == 'ellipse' || currentTool.name === 'line'))
new HistoryStateEditCanvas();
else if (currentTool.name == 'moveselection') {
if (!cursorInSelectedArea() &&
((mouseEvent.target.id == 'canvas-view') || mouseEvent.target.className == 'drawingCanvas')) {
tool.pencil.switchTo();
canDraw = false;
}
}
if (!currentLayer.isLocked && !currentLayer.isVisible && canDraw) {
draw(mouseEvent);
}
}
else if (mouseEvent.which == 2) {
tool.pan.brushSize = currentTool.brushSize;
currentTool = tool.pan;
}
else if (currentTool.name == 'pencil' && mouseEvent.which == 3) {
currentTool = tool.resizebrush;
tool.pencil.previousBrushSize = tool.pencil.brushSize;
}
else if (currentTool.name == 'eraser' && mouseEvent.which == 3) {
currentTool = tool.resizeeraser;
tool.eraser.previousBrushSize = tool.eraser.brushSize;
}
// TODO: [ELLIPSE] Do we need similar logic related to ellipse?
else if (currentTool.name == 'rectangle' && mouseEvent.which == 3) {
currentTool = tool.resizerectangle;
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')
eyedropperPreview.style.display = 'block';
return false;
}, false);
//mouseup - end drawing
window.addEventListener("mouseup", function (mouseEvent) {
// Saving the event in case something else needs it
currentMouseEvent = mouseEvent;
closeMenu();
if (currentLayer != null && !isChildOfByClass(mouseEvent.target, "layers-menu-entry")) {
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 (currentTool.name == 'eyedropper' && mouseEvent.target.className == 'drawingCanvas') {
var cursorLocation = getCursorPosition(mouseEvent);
var selectedColor = getEyedropperColor(cursorLocation);
var newColor = rgbToHex(selectedColor[0],selectedColor[1],selectedColor[2]);
currentGlobalColor = "#" + newColor;
for (let i=1; i<layers.length - 1; i++) {
layers[i].context.fillStyle = currentGlobalColor;
}
var colors = document.getElementsByClassName('color-button');
for (var i = 0; i < colors.length; i++) {
//if picked color matches this color
if (newColor == colors[i].jscolor.toString()) {
//remove current color selection
document.querySelector("#colors-menu li.selected")?.classList.remove("selected");
//set current color
for (let i=2; i<layers.length; i++) {
layers[i].context.fillStyle = '#' + newColor;
}
//make color selected
colors[i].parentElement.classList.add('selected');
//hide eyedropper
eyedropperPreview.style.display = 'none';
}
}
}
else if (currentTool.name == 'fill' && mouseEvent.target.className == 'drawingCanvas') {
//get cursor postion
var cursorLocation = getCursorPosition(mouseEvent);
//offset to match cursor point
cursorLocation[0] += 2;
cursorLocation[1] += 12;
//fill starting at the location
fill(cursorLocation);
currentLayer.updateLayerPreview();
}
else if (currentTool.name == 'zoom' && mouseEvent.target.className == 'drawingCanvas') {
let mode;
if (mouseEvent.which == 1){
mode = "in";
}
}
else if (currentTool == 'zoom' && mouseEvent.target.className == 'drawingCanvas') {
let mode;
if (mouseEvent.which == 1){
mode = 'in';
}
else if (mouseEvent.which == 3){
mode = 'out';
}
changeZoom(mode, getCursorPosition(mouseEvent));
for (let i=1; i<layers.length; i++) {
layers[i].copyData(layers[0]);
}
}
else if (currentTool.name == 'rectselect' && isRectSelecting) {
endRectSelection(mouseEvent);
}
else if (currentTool.name == 'rectangle' && isDrawingRect) {
endRectDrawing(mouseEvent);
currentLayer.updateLayerPreview();
}
else if (currentTool.name == 'ellipse' && isDrawingEllipse) {
endEllipseDrawing(mouseEvent);
currentLayer.updateLayerPreview();
}
dragging = false;
currentTool = currentToolTemp;
currentTool.updateCursor();
var cursorLocation = getCursorPosition(mouseEvent);
currentTool.moveBrushPreview(cursorLocation);
}, false);
function setPreviewPosition(preview, size){
let toAdd = 0;
// This prevents the brush to be placed in the middle of pixels
if (size % 2 == 0) {
toAdd = 0.5;
}
preview.style.left = (
currentLayer.canvas.offsetLeft
+ Math.floor(cursor[0]/zoom) * zoom
- Math.floor(size / 2) * zoom + toAdd
) + 'px';
preview.style.top = (
currentLayer.canvas.offsetTop
+ Math.floor(cursor[1]/zoom) * zoom
- Math.floor(size / 2) * zoom + toAdd
) + 'px';
}
// OPTIMIZABLE: redundant || mouseEvent.target.className in currentTool ifs
//mouse is moving on canvas
window.addEventListener("mousemove", draw, false);
window.addEventListener("mousedown", draw, false);
function draw (mouseEvent) {
if (!dialogueOpen)
{
lastMouseMovePos = getCursorPosition(mouseEvent);
// Saving the event in case something else needs it
currentMouseEvent = mouseEvent;
var cursorLocation = lastMouseMovePos;
//if a document hasnt yet been created or the current layer is locked, exit this function
if (!documentCreated || dialogueOpen || !currentLayer.isVisible || currentLayer.isLocked) return;
// Hiding eyedropper, will be shown if it's needed
eyedropperPreview.style.display = 'none';
if (currentTool.name == 'pencil') {
//hide brush preview outside of canvas / canvas view
if (mouseEvent.target.className == 'drawingCanvas'|| mouseEvent.target.className == 'drawingCanvas')
brushPreview.style.visibility = 'visible';
else
brushPreview.style.visibility = 'hidden';
//draw line to current pixel
if (dragging) {
if (mouseEvent.target.className == 'drawingCanvas' || mouseEvent.target.className == 'drawingCanvas') {
line(Math.floor(lastMouseClickPos[0]/zoom),
Math.floor(lastMouseClickPos[1]/zoom),
Math.floor(cursorLocation[0]/zoom),
Math.floor(cursorLocation[1]/zoom),
tool.pencil.brushSize
);
lastMouseClickPos = cursorLocation;
}
}
//get lightness value of color
var selectedColor = currentLayer.context.getImageData(Math.floor(cursorLocation[0]/zoom),Math.floor(cursorLocation[1]/zoom),1,1).data;
var colorLightness = Math.max(selectedColor[0],selectedColor[1],selectedColor[2])
//for the darkest 50% of colors, change the brush preview to dark mode
if (colorLightness>127) brushPreview.classList.remove('dark');
else brushPreview.classList.add('dark');
currentLayer.updateLayerPreview();
}
// Decided to write a different implementation in case of differences between the brush and the eraser tool
else if (currentTool.name == 'eraser') {
//hide brush preview outside of canvas / canvas view
if (mouseEvent.target.className == 'drawingCanvas' || mouseEvent.target.className == 'drawingCanvas')
brushPreview.style.visibility = 'visible';
else
brushPreview.style.visibility = 'hidden';
//draw line to current pixel
if (dragging) {
if (mouseEvent.target.className == 'drawingCanvas' || mouseEvent.target.className == 'drawingCanvas') {
line(Math.floor(lastMouseClickPos[0]/zoom),Math.floor(lastMouseClickPos[1]/zoom),Math.floor(cursorLocation[0]/zoom),Math.floor(cursorLocation[1]/zoom), currentTool.brushSize);
lastMouseClickPos = cursorLocation;
}
}
currentLayer.updateLayerPreview();
}
else if (currentTool.name == 'rectangle')
{
//hide brush preview outside of canvas / canvas view
if (mouseEvent.target.className == 'drawingCanvas'|| mouseEvent.target.className == 'drawingCanvas')
brushPreview.style.visibility = 'visible';
else
brushPreview.style.visibility = 'hidden';
if (!isDrawingRect && dragging) {
startRectDrawing(mouseEvent);
}
else if (dragging){
updateRectDrawing(mouseEvent);
}
}
else if (currentTool.name == 'ellipse')
{
//hide brush preview outside of canvas / canvas view
if (mouseEvent.target.className == 'drawingCanvas'|| mouseEvent.target.className == 'drawingCanvas')
brushPreview.style.visibility = 'visible';
else
brushPreview.style.visibility = 'hidden';
if (!isDrawingEllipse && dragging) {
startEllipseDrawing(mouseEvent);
}
else if (dragging){
updateEllipseDrawing(mouseEvent);
}
}
else if (currentTool.name == 'pan' && dragging) {
// Setting first layer position
layers[0].setCanvasOffset(layers[0].canvas.offsetLeft + (cursorLocation[0] - lastMouseClickPos[0]), layers[0].canvas.offsetTop + (cursorLocation[1] - lastMouseClickPos[1]));
// Copying that position to the other layers
for (let i=1; i<layers.length; i++) {
layers[i].copyData(layers[0]);
}
// Updating cursorLocation with new layer position
lastMouseMovePos = getCursorPosition(mouseEvent);
cursorLocation = lastMouseMovePos;
}
else if (currentTool.name == 'eyedropper' && dragging && mouseEvent.target.className == 'drawingCanvas') {
let selectedColor = getEyedropperColor(cursorLocation);
eyedropperPreview.style.borderColor = '#'+rgbToHex(selectedColor[0],selectedColor[1],selectedColor[2]);
eyedropperPreview.style.display = 'block';
eyedropperPreview.style.left = cursorLocation[0] + currentLayer.canvas.offsetLeft - 30 + 'px';
eyedropperPreview.style.top = cursorLocation[1] + currentLayer.canvas.offsetTop - 30 + 'px';
var colorLightness = Math.max(selectedColor[0],selectedColor[1],selectedColor[2]);
//for the darkest 50% of colors, change the eyedropper preview to dark mode
if (colorLightness>127) eyedropperPreview.classList.remove('dark');
else eyedropperPreview.classList.add('dark');
}
else if (currentTool.name == 'resizebrush' && 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 brushSizeChange = Math.round(distanceFromClick/10);
var newBrushSize = tool.pencil.previousBrushSize + brushSizeChange;
//set the brush to the new size as long as its bigger than 1
tool.pencil.brushSize = Math.max(1,newBrushSize);
//fix offset so the cursor stays centered
tool.pencil.moveBrushPreview(lastMouseClickPos);
currentTool.updateCursor();
}
else if (currentTool.name == 'resizeeraser' && 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 eraserSizeChange = Math.round(distanceFromClick/10);
var newEraserSizeChange = tool.eraser.previousBrushSize + eraserSizeChange;
//set the brush to the new size as long as its bigger than 1
tool.eraser.brushSize = Math.max(1,newEraserSizeChange);
//fix offset so the cursor stays centered
tool.eraser.moveBrushPreview(lastMouseClickPos);
currentTool.updateCursor();
}
else if (currentTool.name == 'resizerectangle' && 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 rectangleSizeChange = Math.round(distanceFromClick/10);
// TODO: [ELLIPSE] Do we need similar logic related to ellipse?
var newRectangleSize = tool.rectangle.previousBrushSize + rectangleSizeChange;
//set the brush to the new size as long as its bigger than 1
// TODO: [ELLIPSE] Do we need similar logic related to ellipse?
tool.rectangle.brushSize = Math.max(1,newRectangleSize);
//fix offset so the cursor stays centered
// TODO: [ELLIPSE] Do we need similar logic related to ellipse?
tool.rectangle.moveBrushPreview(lastMouseClickPos);
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') {
if (dragging && !isRectSelecting && mouseEvent.target.className == 'drawingCanvas') {
isRectSelecting = true;
startRectSelection(mouseEvent);
}
else if (dragging && isRectSelecting) {
updateRectSelection(mouseEvent);
}
else if (isRectSelecting) {
endRectSelection();
}
}
else if (currentTool.name == 'moveselection') {
// Updating the cursor (move if inside rect, cross if not)
currentTool.updateCursor();
// If I'm dragging, I move the preview
if (dragging && cursorInSelectedArea()) {
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();
}
// Moving brush preview
currentTool.moveBrushPreview(cursorLocation);
}
if (mouseEvent.target.className == 'drawingCanvas')
currentTool.updateCursor();
else
canvasView.style.cursor = 'default';
console.log("Cursor: " + canvasView.style.cursor);
}
//mousewheel scroll
canvasView.addEventListener("wheel", function(mouseEvent){
let mode;
if (mouseEvent.deltaY < 0){
mode = 'in';
}
else if (mouseEvent.deltaY > 0) {
mode = 'out';
}
// Changing zoom and position of the first layer
changeZoom(mode, getCursorPosition(mouseEvent));
for (let i=1; i<layers.length; i++) {
// Copying first layer's data into the other layers
layers[i].copyData(layers[0]);
}
});

View File

@ -1,110 +0,0 @@
var imageDataToMove;
var originalDataPosition;
var canMoveSelection = false;
var lastMovePos;
var selectionCanceled = true;
var firstTimeMove = true;
// 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) {
// I haven't canceled the selection
selectionCanceled = false;
// If it's the first time that I move the selection, I cut it from its original position
if (firstTimeMove) {
cutSelection(mousePosition);
}
firstTimeMove = false;
lastMousePos = mousePosition;
// clear the entire tmp layer
TMPLayer.context.clearRect(0, 0, TMPLayer.canvas.width, TMPLayer.canvas.height);
// put the image data on the tmp layer with offset
TMPLayer.context.putImageData(
imageDataToMove,
Math.round(lastMousePos[0] / zoom) - imageDataToMove.width / 2,
Math.round(lastMousePos[1] / zoom) - imageDataToMove.height / 2);
lastMovePos = lastMousePos;
// Moving the the rectangular ants
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() {
// Clearing the tmp (move preview) and vfx (ants) layers
TMPLayer.context.clearRect(0, 0, TMPLayer.canvas.width, TMPLayer.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);
// If I was moving something
if (imageDataToMove !== undefined) {
// Saving the current clipboard before editing it in order to merge it with the current layer
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);
for (let i=0; i<underlyingImageData.data.length; i+=4) {
let currentMovePixel = [
imageDataToMove.data[i], imageDataToMove.data[i+1],
imageDataToMove.data[i+2], imageDataToMove.data[i+3]
];
let currentUnderlyingPixel = [
underlyingImageData.data[i], underlyingImageData.data[i+1],
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(underlyingImageData)) {
imageDataToMove.data[i] = currentUnderlyingPixel[0];
imageDataToMove.data[i+1] = currentUnderlyingPixel[1];
imageDataToMove.data[i+2] = currentUnderlyingPixel[2];
imageDataToMove.data[i+3] = currentUnderlyingPixel[3];
}
}
}
// If I moved the selection before confirming it
if (lastMovePos !== undefined) {
// I put it in the new position
currentLayer.context.putImageData(
imageDataToMove,
Math.round(lastMovePos[0] / zoom) - imageDataToMove.width / 2,
Math.round(lastMovePos[1] / zoom) - imageDataToMove.height / 2);
}
else {
// I put it in the same position
currentLayer.context.putImageData(
imageDataToMove,
copiedStartX,
copiedStartY);
}
imageDataToMove.data.set(cleanImageData.data);
}
// Resetting all the flags
selectionCanceled = true;
isRectSelecting = false;
firstTimeMove = true;
imageDataToMove = undefined;
isPasting = false;
isCutting = false;
lastMovePos = undefined;
currentLayer.updateLayerPreview();
// Saving the history
new HistoryStateEditCanvas();
}

View File

@ -1,220 +0,0 @@
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) {
// Saving the editor mode
pixelEditorMode = editorMode;
// The palette is empty, at the beginning
currentPalette = [];
// If this is the first pixel I'm creating since the app has started
if (firstPixel) {
// I configure the layers elements
layerListEntry = layerList.firstElementChild;
// Creating the first layer
currentLayer = new Layer(width, height, canvas, layerListEntry);
currentLayer.canvas.style.zIndex = 2;
}
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;
for (let i=2; i < layers.length - nAppLayers; i++) {
let currentEntry = layers[i].menuEntry;
let associatedLayer;
if (currentEntry != null) {
// Getting the associated layer
associatedLayer = getLayerByID(currentEntry.id);
// Deleting its canvas
associatedLayer.canvas.remove();
// Adding the id to the unused ones
unusedIDs.push(currentEntry.id);
// Removing the entry from the menu
currentEntry.remove();
}
}
// Removing the old layers from the list
for (let i=2; i<nLayers - nAppLayers; i++) {
layers.splice(2, 1);
}
// Setting up the current layer
layers[1] = new Layer(width, height, layers[1].canvas, layers[1].menuEntry);
currentLayer = layers[1];
currentLayer.canvas.style.zIndex = 2;
// Updating canvas size to the new size
for (let i=0; i<nLayers; i++) {
layers[i].canvasSize = [width, height];
}
}
// Adding the checkerboard behind it
checkerBoard = new Layer(width, height, checkerBoardCanvas);
// Creating the vfx layer on top of everything
VFXLayer = new Layer(width, height, VFXCanvas);
// Tmp layer to draw previews on
TMPLayer = new Layer(width, height, TMPCanvas);
// Pixel grid
pixelGrid = new Layer(width, height, pixelGridCanvas);
// Setting the general canvasSize
canvasSize = currentLayer.canvasSize;
if (firstPixel) {
// Cloning the entry so that when I change something on the first layer, those changes aren't
// propagated to the other ones
layerListEntry = layerListEntry.cloneNode(true);
// Adding the first layer and the checkerboard to the list of layers
layers.push(checkerBoard);
layers.push(currentLayer);
layers.push(VFXLayer);
layers.push(TMPLayer);
layers.push(pixelGrid);
}
//remove current palette
colors = document.getElementsByClassName('color-button');
while (colors.length > 0) {
colors[0].parentElement.remove();
}
//add colors from selected palette
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) {
console.log('HELO', selectedPalette, palettes[selectedPalette])
//if this palette isnt the one specified in the url, then reset the url
if (!palettes[selectedPalette].specified)
history.pushState(null, null, '/pixel-editor');
//fill the palette with specified colours
createColorPalette(palettes[selectedPalette].colors,true);
}
// Otherwise, I just generate 2 semirandom colours
else if (fileContent == null) {
//this wasn't a specified palette, so reset the url
history.pushState(null, null, '/pixel-editor');
//generate default colors
var fg = hslToRgb(Math.floor(Math.random()*255), 230,70);
var bg = hslToRgb(Math.floor(Math.random()*255), 230,170);
//convert colors to hex
var defaultForegroundColor = rgbToHex(fg.r,fg.g,fg.b);
var defaultBackgroundColor = rgbToHex(bg.r,bg.g,bg.b);
//add colors to palette
addColor(defaultForegroundColor).classList.add('selected');
addColor(defaultBackgroundColor);
//set current drawing color as foreground color
currentLayer.context.fillStyle = '#'+defaultForegroundColor;
currentGlobalColor = '#' + defaultForegroundColor;
selectedPalette = 'none';
}
//fill background of canvas with bg color
fillCheckerboard();
fillPixelGrid();
//reset undo and redo states
undoStates = [];
redoStates = [];
// Closing the "New Pixel dialogue"
closeDialogue();
// Updating the cursor of the current tool
currentTool.updateCursor();
// The user is now able to export the Pixel
document.getElementById('export-button').classList.remove('disabled');
documentCreated = true;
// This is not the first Pixel anymore
firstPixel = false;
// Now, if I opened an LPE file
if (fileContent != null) {
// I add every layer the file had in it
for (let i=0; i<fileContent['nLayers']; i++) {
let layerData = fileContent['layer' + i];
let layerImage = fileContent['layer' + i + 'ImageData'];
if (layerData != null) {
// Setting id
let createdLayer = addLayer(layerData.id, false);
// Setting name
createdLayer.menuEntry.getElementsByTagName("p")[0].innerHTML = layerData.name;
// Adding the image (I can do that because they're sorted by increasing z-index)
let img = new Image();
img.onload = function() {
createdLayer.context.drawImage(img, 0, 0);
createdLayer.updateLayerPreview();
if (i == (fileContent['nLayers'] - 1)) {
createPaletteFromLayers();
}
};
img.src = layerImage;
// Setting visibility and lock options
if (!layerData.isVisible) {
createdLayer.hide();
}
if (layerData.isLocked) {
createdLayer.lock();
}
}
}
// Deleting the default layer
deleteLayer(false);
}
// Applying the correct editor mode
if (pixelEditorMode == 'Basic') {
switchMode(false);
}
else {
switchMode(false);
}
// Resetting history
undoStates = [];
redoStates = [];
}
function newFromTemplate(preset, x, y) {
if (preset != '') {
const presetProperties = PresetModule.propertiesOf(preset);
Util.setText('palette-button-splash', presetProperties.palette);
Util.setText('palette-button', presetProperties.palette);
x = presetProperties.width;
y = presetProperties.height;
}
newPixel(x, y, pixelEditorMode == 'Advanced' ? 'Basic' : 'Advanced');
}

View File

@ -1,68 +0,0 @@
//when the page is done loading, you can get ready to start
window.onload = function () {
featureToggles.onLoad();
currentTool.updateCursor();
//check if there are any url parameters
if (window.location.pathname.replace('/pixel-editor/','').length <= 1) {
console.log('no url parameters were found');
//show splash screen
showDialogue('splash', false);
}
//url parameters were specified
else {
console.log('loading preset from url parameters', window.location.pathname);
let args = window.location.pathname.split('/');
let paletteSlug = args[2];
let dimentions = args[3];
//fetch palette via lospec palette API
fetch('https://lospec.com/palette-list/'+paletteSlug+'.json')
.then(response => response.json())
.then(data => {
//palette loaded successfully
console.log('loaded palette', data);
palettes[paletteSlug] = data;
palettes[paletteSlug].specified = true;
//refresh list of palettes
document.getElementById('palette-menu-splash').refresh();
//if the dimentions were specified
if (dimentions && dimentions.length >= 3 && dimentions.includes('x')) {
let width = dimentions.split('x')[0];
let height = dimentions.split('x')[1];
console.log('dimentions were specified',width,'x',height)
//firstPixel = false;
//create new document
newPixel(width, height);
}
//dimentions were not specified -- show splash screen with palette preselected
else {
//show splash
showDialogue('new-pixel', false);
}
})
//error fetching url (either palette doesn't exist, or lospec is down)
.catch((error) => {
console.warn('failed to load palette "'+paletteSlug+'"', error);
//proceed to splash screen
showDialogue('splash', false);
});
}
};

View File

@ -1,7 +0,0 @@
//prevent user from leaving page with unsaved data
window.onbeforeunload = function() {
if (documentCreated)
return 'You will lose your pixel if it\'s not saved!';
else return;
};

View File

@ -1,333 +0,0 @@
/** 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();
}
/** 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

@ -1,270 +0,0 @@
/***********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) {
// Copying the above content on the layerBelow
let belowImageData = belowLayer.getImageData(0, 0, belowLayer.canvas.width, belowLayer.canvas.height);
let toMergeImageData = topLayer.getImageData(0, 0, topLayer.canvas.width, topLayer.canvas.height);
for (let i=0; i<belowImageData.data.length; i+=4) {
let currentMovePixel = [
toMergeImageData.data[i], toMergeImageData.data[i+1],
toMergeImageData.data[i+2], toMergeImageData.data[i+3]
];
let currentUnderlyingPixel = [
belowImageData.data[i], belowImageData.data[i+1],
belowImageData.data[i+2], belowImageData.data[i+3]
];
if (isPixelEmpty(currentMovePixel)) {
if (!isPixelEmpty(belowImageData)) {
toMergeImageData.data[i] = currentUnderlyingPixel[0];
toMergeImageData.data[i+1] = currentUnderlyingPixel[1];
toMergeImageData.data[i+2] = currentUnderlyingPixel[2];
toMergeImageData.data[i+3] = currentUnderlyingPixel[3];
}
}
}
// Putting the top data into the belowdata
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) {
// I just copy pasted this from stack overflow lol please have mercy
let keyboardEvent = document.createEvent("KeyboardEvent");
let initMethod = typeof keyboardEvent.initKeyboardEvent !== 'undefined' ? "initKeyboardEvent" : "initKeyEvent";
keyboardEvent[initMethod](
"keydown", // event type: keydown, keyup, keypress
true, // bubbles
true, // cancelable
window, // view: should be window
ctrl, // ctrlKey
alt, // altKey
shift, // shiftKey
false, // metaKey
keyCode, // keyCode: unsigned long - the virtual key code, else 0
keyCode // charCode: unsigned long - the Unicode character associated with the depressed key, else 0
);
document.dispatchEvent(keyboardEvent);
}
/** Tells if a pixel is empty (has alpha = 0)
*
* @param {*} pixel
*/
function isPixelEmpty(pixel) {
if (pixel == null || pixel === undefined) {
return false;
}
// If the alpha channel is 0, the current pixel is empty
if (pixel[3] == 0) {
return true;
}
return false;
}
/** Tells if element is a child of an element with class className
*
* @param {*} element
* @param {*} className
*/
function isChildOfByClass(element, className) {
// Getting the element with class className
while (element != null && element.classList != null && !element.classList.contains(className)) {
element = element.parentElement;
}
// If that element exists and its class is the correct one
if (element != null && element.classList != null && element.classList.contains(className)) {
// Then element is a chld of an element with class className
return true;
}
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) {
// Making sure max will take some kind of value
let max = -1;
// Using tmpColour to sample the sprite
let tmpColour;
// Returned colour
let selectedColor;
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;
// 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) {
max = layers[i].canvas.style.zIndex;
if (!isPixelEmpty(tmpColour)) {
selectedColor = tmpColour;
}
}
}
// If the final colour was empty, I return black
if (isPixelEmpty(tmpColour) && selectedColor === undefined) {
selectedColor = [0, 0, 0];
}
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) {
// Probably copy pasted this from stack overflow too, if not I don't recall how it works
let curleft = curtop = 0;
if (element.offsetParent) {
do {
curleft += element.offsetLeft;
curtop += element.offsetTop;
} while (element = element.offsetParent);
}
return [curleft,curtop];
}
/** Nearest neighbor algorithm to scale a sprite
*
* @param {*} src The source imageData
* @param {*} dst The destination imageData
*/
function nearestNeighbor (src, dst) {
let pos = 0
// Just applying the nearest neighbor algorithm
for (let y = 0; y < dst.height; y++) {
for (let x = 0; x < dst.width; x++) {
const srcX = Math.floor(x * src.width / dst.width)
const srcY = Math.floor(y * src.height / dst.height)
let srcPos = ((srcY * src.width) + srcX) * 4
dst.data[pos++] = src.data[srcPos++] // R
dst.data[pos++] = src.data[srcPos++] // G
dst.data[pos++] = src.data[srcPos++] // B
dst.data[pos++] = src.data[srcPos++] // A
}
}
}
/** Bilinear interpolation used to scale a sprite
*
* @param {*} src The source imageData
* @param {*} dst The destination imageData
*/
function bilinearInterpolation (src, dst) {
// Applying the bilinear interpolation algorithm
function interpolate (k, kMin, kMax, vMin, vMax) {
return Math.round((k - kMin) * vMax + (kMax - k) * vMin)
}
function interpolateHorizontal (offset, x, y, xMin, xMax) {
const vMin = src.data[((y * src.width + xMin) * 4) + offset]
if (xMin === xMax) return vMin
const vMax = src.data[((y * src.width + xMax) * 4) + offset]
return interpolate(x, xMin, xMax, vMin, vMax)
}
function interpolateVertical (offset, x, xMin, xMax, y, yMin, yMax) {
const vMin = interpolateHorizontal(offset, x, yMin, xMin, xMax)
if (yMin === yMax) return vMin
const vMax = interpolateHorizontal(offset, x, yMax, xMin, xMax)
return interpolate(y, yMin, yMax, vMin, vMax)
}
let pos = 0
for (let y = 0; y < dst.height; y++) {
for (let x = 0; x < dst.width; x++) {
const srcX = x * src.width / dst.width
const srcY = y * src.height / dst.height
const xMin = Math.floor(srcX)
const yMin = Math.floor(srcY)
const xMax = Math.min(Math.ceil(srcX), src.width - 1)
const yMax = Math.min(Math.ceil(srcY), src.height - 1)
dst.data[pos++] = interpolateVertical(0, srcX, xMin, xMax, srcY, yMin, yMax) // R
dst.data[pos++] = interpolateVertical(1, srcX, xMin, xMax, srcY, yMin, yMax) // G
dst.data[pos++] = interpolateVertical(2, srcX, xMin, xMax, srcY, yMin, yMax) // B
dst.data[pos++] = interpolateVertical(3, srcX, xMin, xMax, srcY, yMin, yMax) // A
}
}
}
/** 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) {
algorithm = algorithm || 'bilinear-interpolation'
let resize;
switch (algorithm) {
case 'nearest-neighbor': resize = nearestNeighbor; break
case 'bilinear-interpolation': resize = bilinearInterpolation; break
default: return image;
}
const result = new ImageData(width, height)
resize(image, 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) {
let linearIndex = index / 4;
let x = linearIndex % layers[0].canvasSize[0];
let y = Math.floor(linearIndex / layers[0].canvasSize[0]);
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,70 +0,0 @@
/**
* Opens the export window and initializes events for export customization.
*/
function openPixelExportWindow() {
var selectedPalette = getText('palette-button');
if (selectedPalette != 'Choose a palette...'){
var paletteAbbreviation = palettes[selectedPalette].name;
var fileName = 'pixel-'+paletteAbbreviation+'-'+layers[0].canvasSize[0]+'x'+layers[0].canvasSize[1]+'.png';
} else {
var fileName = 'pixel-'+layers[0].canvasSize[0]+'x'+layers[0].canvasSize[1]+'.png';
selectedPalette = 'none';
}
setValue('file-name', fileName);
document.getElementById("export-confirm").addEventListener("click", exportPixel);
showDialogue('export', false);
}
/**
* Exports the pixel based on the export dialogue.
*/
function exportPixel() {
if (documentCreated) {
var fileName = getValue('file-name');
//set download link
var linkHolder = document.getElementById('save-image-link-holder');
// Creating a tmp canvas to flatten everything
var exportCanvas = document.createElement("canvas");
var emptyCanvas = document.createElement("canvas");
var layersCopy = layers.slice();
exportCanvas.width = layers[0].canvasSize[0];
exportCanvas.height = layers[0].canvasSize[1];
emptyCanvas.width = layers[0].canvasSize[0];
emptyCanvas.height = layers[0].canvasSize[1];
// Sorting the layers by z index
layersCopy.sort((a, b) => (a.canvas.style.zIndex > b.canvas.style.zIndex) ? 1 : -1);
// Merging every layer on the export canvas
for (let i=0; i<layersCopy.length; i++) {
if (layersCopy[i].menuEntry != null && layersCopy[i].isVisible) {
mergeLayers(exportCanvas.getContext('2d'), layersCopy[i].context);
}
// I'm not going to find out why the layer ordering screws up if you don't copy
// a blank canvas when layers[i] is not set as visible, but if you have time to
// spend, feel free to investigate (comment the else, create 3 layers: hide the
// middle one and export, the other 2 will be swapped in their order)
else {
mergeLayers(exportCanvas.getContext('2d'), emptyCanvas.getContext('2d'));
}
}
linkHolder.href = exportCanvas.toDataURL();
linkHolder.download = fileName;
linkHolder.click();
emptyCanvas.remove();
exportCanvas.remove();
//track google event
ga('send', 'event', 'Pixel Editor Export', selectedPalette, canvasSize[0]+'/'+canvasSize[1]); /*global ga*/
}
}

View File

@ -1,72 +0,0 @@
// Start colour of the pixel grid (can be changed in the preferences)
let pixelGridColor = "#000000";
// Distance between one line and another in HTML pixels
let lineDistance = 12;
// The grid is visible by default
let pixelGridVisible = true;
// Saving the canvas containing the 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(newState) {
console.log('toggling pixel grid', newState)
// Getting the button because I have to change its text
let button = document.getElementById("toggle-pixelgrid-button");
//Set the state based on the passed newState variable, otherwise just toggle it
if (newState == 'on') pixelGridVisible = true;
else if (newState == 'off') pixelGridVisible = false;
else pixelGridVisible = !pixelGridVisible;
// If it was visible, I hide it
if (pixelGridVisible) {
button.innerHTML = "Hide pixel grid";
pixelGridCanvas.style.display = "inline-block";
}
// Otherwise, I show it
else {
button.innerHTML = "Show pixel grid";
pixelGridCanvas.style.display = "none";
}
}
/** Fills the pixelGridCanvas with the pixelgrid
*
*/
function fillPixelGrid() {
let context = pixelGridCanvas.getContext("2d");
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.height = originalSize[1] * lineDistance;
// OPTIMIZABLE, could probably be a bit more elegant
// Draw horizontal lines
for (let i=0; i<pixelGridCanvas.width / lineDistance; i++) {
context.strokeStyle = settings.pixelGridColour;
context.beginPath();
context.moveTo(i * lineDistance + 0.5, 0);
context.lineTo(i * lineDistance + 0.5, originalSize[1] * lineDistance);
context.stroke();
context.closePath();
}
// Draw vertical lines
for (let i=0; i<pixelGridCanvas.height / lineDistance; i++) {
context.beginPath();
context.moveTo(0, i * lineDistance + 0.5);
context.lineTo(originalSize[0] * lineDistance, i * lineDistance + 0.5);
context.stroke();
context.closePath();
}
if (!pixelGridVisible) {
pixelGridCanvas.style.display = 'none';
}
}

View File

@ -1,178 +0,0 @@
var isRectSelecting = false;
let startX;
let startY;
let endX;
let endY;
/** Starts the selection: saves the canvas, sets the start coordinates
*
* @param {*} mouseEvent
*/
function startRectSelection(mouseEvent) {
// Saving the canvas
new HistoryStateEditCanvas();
// Putting the vfx layer on top of everything
VFXCanvas.style.zIndex = MAX_Z_INDEX;
// Saving the start coords of the rect
let cursorPos = getCursorPosition(mouseEvent);
startX = Math.round(cursorPos[0] / zoom) - 0.5;
startY = Math.round(cursorPos[1] / zoom) - 0.5;
// Avoiding external selections
if (startX < 0) {
startX = 0;
}
else if (startX > currentLayer.canvas.width) {
startX = currentLayer.canvas.width;
}
if (startY < 0) {
startY = 0;
}
else if (startY > currentLayer.canvas.height) {
startY = currentLayer.canvas.height;
}
// Drawing the rect
drawRect(startX, startY);
selectionCanceled = false;
}
/** Updates the selection
*
* @param {*} mouseEvent
*/
function updateRectSelection(mouseEvent) {
let pos = getCursorPosition(mouseEvent);
// Drawing the rect
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) {
// Getting the end position
let currentPos = getCursorPosition(mouseEvent);
endX = Math.round(currentPos[0] / zoom) + 0.5;
endY = Math.round(currentPos[1] / zoom) + 0.5;
// Inverting end and start (start must always be the top left corner)
if (endX < startX) {
let tmp = endX;
endX = startX;
startX = tmp;
}
// Same for the y
if (endY < startY) {
let tmp = endY;
endY = startY;
startY = tmp;
}
// Selecting the move tool
currentTool = tool.moveselection;
currentToolTemp = currentTool;
// Resetting this
isRectSelecting = false;
// Updating the cursor
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) {
// Getting the selected pixels
imageDataToMove = currentLayer.context.getImageData(startX, startY, 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
TMPLayer.context.putImageData(imageDataToMove, startX + 1, startY);
}
/** 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) {
// Getting the vfx context
let vfxContext = VFXCanvas.getContext('2d');
// Clearing the vfx canvas
vfxContext.clearRect(0, 0, VFXCanvas.width, VFXCanvas.height);
vfxContext.lineWidth = 1;
vfxContext.strokeStyle = 'black';
vfxContext.setLineDash([4]);
// Drawing the rect
vfxContext.beginPath();
vfxContext.rect(startX, startY, x - startX, y - startY);
vfxContext.stroke();
}
function applyChanges() {
VFXCanvas.style.zIndex = MIN_Z_INDEX;
}
// Checks whether the pointer is inside the selected area or not
function cursorInSelectedArea() {
// Getting the cursor position
let cursorPos = getCursorPosition(currentMouseEvent);
// Getting the coordinates relatively to the canvas
let x = cursorPos[0] / 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 rightX = Math.max(startX, endX);
let topY = Math.max(startY, endY);
let bottomY = Math.min(startY, endY);
if (leftX <= x && x <= rightX) {
if (bottomY <= y && y <= topY) {
return true;
}
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) {
// Getting the vfx context
let vfxContext = VFXCanvas.getContext('2d');
// Clearing the vfx canvas
vfxContext.clearRect(0, 0, VFXCanvas.width, VFXCanvas.height);
vfxContext.lineWidth = 1;
vfxContext.setLineDash([4]);
// Fixing the coordinates
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;
endX = startX + Math.round(width);
endY = startY + Math.round(height);
// Drawing the rect
vfxContext.beginPath();
vfxContext.rect(startX, startY, width, height);
vfxContext.stroke();
}

View File

@ -1,145 +0,0 @@
// Saving the empty rect svg
var emptyRectangleSVG = document.getElementById("rectangle-empty-button-svg");
// and the full rect svg so that I can change them when the user changes rect modes
var fullRectangleSVG = document.getElementById("rectangle-full-button-svg");
// The start mode is empty rectangle
var rectangleDrawMode = 'empty';
// I'm not drawing a rectangle at the beginning
var isDrawingRect = false;
// Rect coordinates
let startRectX;
let startRectY;
let endRectX;
let endRectY;
/** Starts drawing the rect, saves the start coordinates
*
* @param {*} mouseEvent
*/
function startRectDrawing(mouseEvent) {
// Putting the vfx layer on top of everything
VFXCanvas.style.zIndex = parseInt(currentLayer.canvas.style.zIndex, 10) + 1;;
// Updating flag
isDrawingRect = true;
// Saving the start coords of the rect
let cursorPos = getCursorPosition(mouseEvent);
startRectX = Math.floor(cursorPos[0] / zoom) + 0.5;
startRectY = Math.floor(cursorPos[1] / zoom) + 0.5;
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) {
let pos = getCursorPosition(mouseEvent);
// Drawing the rect at the right position
drawRectangle(Math.floor(pos[0] / zoom) + 0.5, Math.floor(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) {
// Getting the end position
let currentPos = getCursorPosition(mouseEvent);
let vfxContext = VFXCanvas.getContext("2d");
endRectX = Math.floor(currentPos[0] / zoom) + 0.5;
endRectY = Math.floor(currentPos[1] / zoom) + 0.5;
// Inverting end and start (start must always be the top left corner)
if (endRectX < startRectX) {
let tmp = endRectX;
endRectX = startRectX;
startRectX = tmp;
}
// Same for the y
if (endRectY < startRectY) {
let tmp = endRectY;
endRectY = startRectY;
startRectY = tmp;
}
// Resetting this
isDrawingRect = false;
// Drawing the rect
startRectY -= 0.5;
endRectY -= 0.5;
endRectX -= 0.5;
startRectX -= 0.5;
// Setting the correct linewidth and colour
currentLayer.context.lineWidth = tool.rectangle.brushSize;
currentLayer.context.fillStyle = currentGlobalColor;
// Drawing the rect using 4 lines
line(startRectX, startRectY, endRectX, startRectY, tool.rectangle.brushSize);
line(endRectX, startRectY, endRectX, endRectY, tool.rectangle.brushSize);
line(endRectX, endRectY, startRectX, endRectY, tool.rectangle.brushSize);
line(startRectX, endRectY, startRectX, startRectY, tool.rectangle.brushSize);
// If I have to fill it, I do so
if (rectangleDrawMode == 'fill') {
currentLayer.context.fillRect(startRectX, startRectY, endRectX - startRectX, endRectY - startRectY);
}
// Clearing the vfx canvas
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) {
// Getting the vfx context
let vfxContext = VFXCanvas.getContext("2d");
// Clearing the vfx canvas
vfxContext.clearRect(0, 0, VFXCanvas.width, VFXCanvas.height);
// Drawing the rect
vfxContext.lineWidth = tool.rectangle.brushSize;
vfxContext.strokeStyle = currentGlobalColor;
// Drawing the rect
vfxContext.beginPath();
if ((tool.rectangle.brushSize % 2 ) == 0) {
vfxContext.rect(startRectX - 0.5, startRectY - 0.5, x - startRectX, y - startRectY);
}
else {
vfxContext.rect(startRectX, startRectY, x - startRectX, y - startRectY);
}
vfxContext.setLineDash([]);
vfxContext.stroke();
}
/** Sets the correct tool icon depending on its mode
*
*/
function setRectToolSvg() {
if (rectangleDrawMode == 'empty') {
emptyRectangleSVG.setAttribute('display', 'visible');
fullRectangleSVG.setAttribute('display', 'none');
}
else {
emptyRectangleSVG.setAttribute('display', 'none');
fullRectangleSVG.setAttribute('display', 'visible');
}
}
function applyChanges() {
//VFXCanvas.style.zIndex = MIN_Z_INDEX;
}

View File

@ -1,25 +0,0 @@
//replaces all of a single color on the canvas with a different color
//input two rgb color objects {r:0,g:0,b:0}
function replaceAllOfColor (oldColor, newColor) {
//convert strings to objects if nessesary
if (typeof oldColor === 'string') oldColor = hexToRgb(oldColor);
if (typeof newColor === 'string') newColor = hexToRgb(newColor);
//create temporary image from canvas to search through
var tempImage = currentLayer.context.getImageData(0, 0, canvasSize[0], canvasSize[1]);
//loop through all pixels
for (var i=0;i<tempImage.data.length;i+=4) {
//check if pixel matches old color
if(tempImage.data[i]==oldColor.r && tempImage.data[i+1]==oldColor.g && tempImage.data[i+2]==oldColor.b){
//change to new color
tempImage.data[i]=newColor.r;
tempImage.data[i+1]=newColor.g;
tempImage.data[i+2]=newColor.b;
}
}
//put temp image back onto canvas
currentLayer.context.putImageData(tempImage,0,0);
}

View File

@ -1,312 +0,0 @@
/* This scripts contains all the code used to handle the canvas resizing */
// 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() {
// Initializes the inputs
initResizeCanvasInputs();
showDialogue('resize-canvas');
}
/** Initializes the canvas resizing input
*
*/
function initResizeCanvasInputs() {
// Getting the pivot buttons
let buttons = document.getElementsByClassName("pivot-button");
// Adding the event handlers for them
for (let i=0; i<buttons.length; i++) {
buttons[i].addEventListener("click", changePivot);
if (buttons[i].getAttribute("value").includes("middle")) {
currentPivotObject = buttons[i];
}
}
document.getElementById("rc-width").value = layers[0].canvasSize[0];
document.getElementById("rc-height").value = layers[0].canvasSize[1];
document.getElementById("rc-border-left").addEventListener("change", rcChangedBorder);
document.getElementById("rc-border-right").addEventListener("change", rcChangedBorder);
document.getElementById("rc-border-top").addEventListener("change", rcChangedBorder);
document.getElementById("rc-border-bottom").addEventListener("change", rcChangedBorder);
document.getElementById("rc-width").addEventListener("change", rcChangedSize);
document.getElementById("rc-height").addEventListener("change", rcChangedSize);
document.getElementById("resize-canvas-confirm").addEventListener("click", resizeCanvas);
console.log("Pivot selezionato: " + currentPivotObject);
}
/** Fired when a border offset is changed: it updates the width and height
*
* @param {*} event
*/
function rcChangedBorder(event) {
rcUpdateBorders();
document.getElementById("rc-width").value = parseInt(layers[0].canvasSize[0]) + rcBorders.left + rcBorders.right;
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) {
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 left = Math.round(widthOffset / 2);
let right = widthOffset - left;
let top = Math.round(heightOffset / 2);
let bottom = heightOffset - top;
document.getElementById("rc-border-left").value = left;
document.getElementById("rc-border-right").value = right;
document.getElementById("rc-border-top").value = top;
document.getElementById("rc-border-bottom").value = bottom;
rcBorders.left = left;
rcBorders.right = right;
rcBorders.top = top;
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) {
let imageDatas = [];
let leftOffset = 0;
let topOffset = 0;
let copiedDataIndex = 0;
// If I'm undoing and I'm not trimming, I manually put the values in the window
if (size != null && customData == null) {
document.getElementById("rc-width").value = size.x;
document.getElementById("rc-height").value = size.y;
rcChangedSize();
}
rcUpdateBorders();
// Save all imageDatas
for (let i=0; i<layers.length; i++) {
if (layers[i].menuEntry != null) {
imageDatas.push(layers[i].context.getImageData(0, 0, layers[0].canvasSize[0], layers[0].canvasSize[1]));
}
}
// Saving the history only if I'm not already undoing or redoing
if (saveHistory && event != null) {
// Saving history
new HistoryStateResizeCanvas(
{x: parseInt(layers[0].canvasSize[0]) + rcBorders.left + rcBorders.right,
y: parseInt(layers[0].canvasSize[1]) + rcBorders.top + rcBorders.bottom},
{x: layers[0].canvasSize[0],
y: layers[0].canvasSize[1]},
imageDatas.slice(), customData != null && saveHistory
);
console.log("salvata");
}
// Resize the canvases
for (let i=0; i<layers.length; i++) {
layers[i].canvasSize[0] = parseInt(layers[i].canvasSize[0]) + rcBorders.left + rcBorders.right;
layers[i].canvasSize[1] = parseInt(layers[i].canvasSize[1]) + rcBorders.top + rcBorders.bottom;
layers[i].canvas.width = layers[i].canvasSize[0];
layers[i].canvas.height = layers[i].canvasSize[1];
layers[i].resize();
layers[i].context.fillStyle = currentGlobalColor;
}
// Regenerate the checkerboard
fillCheckerboard();
fillPixelGrid();
// Put the imageDatas in the right position
switch (rcPivot)
{
case 'topleft':
leftOffset = 0;
topOffset = 0;
break;
case 'top':
leftOffset = (rcBorders.left + rcBorders.right) / 2;
topOffset = 0;
break;
case 'topright':
leftOffset = rcBorders.left + rcBorders.right;
topOffset = 0;
break;
case 'left':
leftOffset = 0;
topOffset = (rcBorders.top + rcBorders.bottom) / 2;
break;
case 'middle':
leftOffset = (rcBorders.left + rcBorders.right) / 2;
topOffset = (rcBorders.top + rcBorders.bottom) / 2;
break;
case 'right':
leftOffset = rcBorders.left + rcBorders.right;
topOffset = (rcBorders.top + rcBorders.bottom) / 2;
break;
case 'bottomleft':
leftOffset = 0;
topOffset = rcBorders.top + rcBorders.bottom;
break;
case 'bottom':
leftOffset = (rcBorders.left + rcBorders.right) / 2;
topOffset = rcBorders.top + rcBorders.bottom;
break;
case 'bottomright':
leftOffset = rcBorders.left + rcBorders.right;
topOffset = rcBorders.top + rcBorders.bottom;
break;
default:
console.log('Pivot does not exist, please report an issue at https://github.com/lospec/pixel-editor');
break;
}
// Putting all the data for each layer with the right offsets (decided by the pivot)
for (let i=0; i<layers.length; i++) {
if (layers[i].menuEntry != null) {
if (customData == undefined) {
layers[i].context.putImageData(imageDatas[copiedDataIndex], leftOffset, topOffset);
}
else {
layers[i].context.putImageData(customData[copiedDataIndex], 0, 0);
}
layers[i].updateLayerPreview();
copiedDataIndex++;
}
}
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) {
let minY = Infinity;
let minX = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
let tmp;
let imageDatas = [];
let historySave = saveHistory == null;
let prevPivot = rcPivot;
rcPivot = "topleft";
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++) {
let imageData = layers[i].context.getImageData(0, 0, layers[0].canvasSize[0], layers[0].canvasSize[1]);
let pixelPosition;
for (let i=imageData.data.length - 1; i>= 0; i-=4) {
if (!isPixelEmpty(
[imageData.data[i - 3], imageData.data[i - 2],
-imageData.data[i - 1], imageData.data[i]])) {
pixelPosition = getPixelPosition(i);
// max x
if (pixelPosition[0] > maxX) {
maxX = pixelPosition[0];
}
// min x
if (pixelPosition[0] < minX) {
minX = pixelPosition[0];
}
// max y
if (pixelPosition[1] > maxY) {
maxY = pixelPosition[1];
}
// min y
if (pixelPosition[1] < minY) {
minY = pixelPosition[1];
}
}
}
}
tmp = minY;
minY = maxY;
maxY = tmp;
minY = layers[0].canvasSize[1] - minY;
maxY = layers[0].canvasSize[1] - maxY;
// Setting the borders coherently with the values I've just computed
rcBorders.right = (maxX - layers[0].canvasSize[0]) + 1;
rcBorders.left = -minX;
rcBorders.top = maxY - layers[0].canvasSize[1] + 1;
rcBorders.bottom = -minY;
// Saving the data
for (let i=0; i<layers.length; i++) {
if (layers[i].menuEntry != null) {
imageDatas.push(layers[i].context.getImageData(minX - 1, layers[i].canvasSize[1] - maxY, maxX-minX + 1, maxY-minY + 1));
}
}
console.log(imageDatas);
//console.log("sx: " + borders.left + "dx: " + borders.right + "top: " + borders.top + "btm: " + borders.bottom);
document.getElementById("rc-border-left").value = rcBorders.left;
document.getElementById("rc-border-right").value = rcBorders.right;
document.getElementById("rc-border-top").value = rcBorders.top;
document.getElementById("rc-border-bottom").value = rcBorders.bottom;
// Resizing the canvas with the decided border offsets
resizeCanvas(null, null, imageDatas.slice(), historySave);
// Resetting the previous pivot
rcPivot = prevPivot;
}
function rcUpdateBorders() {
// Getting input
rcBorders.left = document.getElementById("rc-border-left").value;
rcBorders.right = document.getElementById("rc-border-right").value;
rcBorders.top = document.getElementById("rc-border-top").value;
rcBorders.bottom = document.getElementById("rc-border-bottom").value;
// Validating input
rcBorders.left == "" ? rcBorders.left = 0 : rcBorders.left = Math.round(parseInt(rcBorders.left));
rcBorders.right == "" ? rcBorders.right = 0 : rcBorders.right = Math.round(parseInt(rcBorders.right));
rcBorders.top == "" ? rcBorders.top = 0 : rcBorders.top = Math.round(parseInt(rcBorders.top));
rcBorders.bottom == "" ? rcBorders.bottom = 0 : rcBorders.bottom = Math.round(parseInt(rcBorders.bottom));
}
function changePivot(event) {
rcPivot = event.target.getAttribute("value");
// Setting the selected class
currentPivotObject.classList.remove("rc-selected-pivot");
currentPivotObject = event.target;
currentPivotObject.classList.add("rc-selected-pivot");
}

View File

@ -1,296 +0,0 @@
/* This scripts contains all the code used to handle the sprite scaling */
// Should I keep the sprite ratio?
let keepRatio = true;
// Used to store the current ratio
let currentRatio;
// The currenty selected resizing algorithm (nearest-neighbor or bilinear-interpolation)
let currentAlgo = 'nearest-neighbor';
// Current resize data
let data = {width: 0, height: 0, widthPercentage: 100, heightPercentage: 100};
// Start resize data
let startData = {width: 0, height:0, widthPercentage: 100, heightPercentage: 100};
/** Opens the sprite resizing window
*
*/
function openResizeSpriteWindow() {
// Inits the sprie resize inputs
initResizeSpriteInputs();
// Computing the current ratio
currentRatio = layers[0].canvasSize[0] / layers[0].canvasSize[1];
console.log("Current ratio: " + currentRatio);
// Initializing the input fields
data.width = layers[0].canvasSize[0];
data.height = layers[1].canvasSize[1];
startData.width = parseInt(data.width);
startData.height = parseInt(data.height);
startData.heightPercentage = 100;
startData.widthPercentage = 100;
// Opening the pop up now that it's ready
showDialogue('resize-sprite');
}
/** Initalizes the input values and binds the elements to their events
*
*/
function initResizeSpriteInputs() {
document.getElementById("rs-width").value = layers[0].canvasSize[0];
document.getElementById("rs-height").value = layers[0].canvasSize[1];
document.getElementById("rs-width-percentage").value = 100;
document.getElementById("rs-height-percentage").value = 100;
document.getElementById("rs-keep-ratio").checked = true;
document.getElementById("rs-width").addEventListener("change", changedWidth);
document.getElementById("rs-height").addEventListener("change", changedHeight);
document.getElementById("rs-width-percentage").addEventListener("change", changedWidthPercentage);
document.getElementById("rs-height-percentage").addEventListener("change", changedHeightPercentage);
document.getElementById("resize-sprite-confirm").addEventListener("click", resizeSprite);
document.getElementById("rs-keep-ratio").addEventListener("click", toggleRatio);
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) {
// Old data
let oldWidth, oldHeight;
// New data
let newWidth, newHeight;
// Current imageDatas
let rsImageDatas = [];
// Index that will be used a few lines below
let layerIndex = 0;
// Copy of the imageDatas that will be stored in the history
let imageDatasCopy = [];
oldWidth = layers[0].canvasSize[0];
oldHeight = layers[1].canvasSize[1];
rcPivot = "middle";
// Updating values if the user didn't press enter
switch (document.activeElement.id) {
case "rs-width-percentage":
changedWidthPercentage();
break;
case "rs-width":
changedWidth();
break;
case "rs-height-percentage":
changedHeightPercentage();
break;
case "rs-height":
changedHeight();
break;
default:
// In this case everything has been updated correctly
break;
}
// Computing newWidth and newHeight
if (ratio == null) {
newWidth = data.width;
newHeight = data.height;
}
else {
newWidth = layers[0].canvasSize[0] * ratio[0];
newHeight = layers[1].canvasSize[1] * ratio[1];
}
// Get all the image datas
for (let i=0; i<layers.length; i++) {
if (layers[i].menuEntry != null) {
rsImageDatas.push(layers[i].context.getImageData(
0, 0, layers[0].canvasSize[0], layers[0].canvasSize[1])
);
}
}
// event is null when the user is undoing
if (event != null) {
// Copying the image data
imageDatasCopy = rsImageDatas.slice();
// Saving the history
new HistoryStateResizeSprite(newWidth / oldWidth, newHeight / oldHeight, currentAlgo, imageDatasCopy);
}
// Resizing the canvas
resizeCanvas(null, {x: newWidth, y: newHeight});
// Put the image datas on the new canvases
for (let i=0; i<layers.length; i++) {
if (layers[i].menuEntry != null) {
layers[i].context.putImageData(
resizeImageData(rsImageDatas[layerIndex], newWidth, newHeight, currentAlgo), 0, 0
);
layers[i].updateLayerPreview();
layerIndex++;
}
}
// Updating start values when I finish scaling the sprite
// OPTIMIZABLE? Can't I just assign data to startData? Is js smart enough to understand?
if (ratio == null) {
startData.width = data.width;
startData.height = data.height;
}
else {
startData.width = layers[0].canvasSize[0];
startData.height = layers[0].canvasSize[1];
}
startData.widthPercentage = 100;
startData.heightPercentage = 100;
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) {
let oldValue = data.width;
let ratio;
let percentageRatio;
let newHeight, newHeightPerc, newWidthPerc;
data.width = event.target.value;
delta = data.width - oldValue;
ratio = data.width / oldValue;
newHeight = data.width / currentRatio;
newHeightPerc = (newHeight * 100) / startData.height;
newWidthPerc = (data.width * 100) / startData.width;
if (keepRatio) {
document.getElementById("rs-height").value = newHeight;
data.height = newHeight;
document.getElementById("rs-height-percentage").value = newHeightPerc;
data.heightPercentage = newHeightPerc;
}
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) {
let oldValue = 100;
let ratio;
let newWidth, newWidthPerc, newHeightPerc;
data.height = event.target.value;
delta = data.height - oldValue;
ratio = data.height / oldValue;
newWidth = data.height * currentRatio;
newWidthPerc = (newWidth * 100) / startData.width;
newHeightPerc = (data.height * 100) / startData.height;
if (keepRatio) {
document.getElementById("rs-width").value = newWidth;
data.width = newWidth;
document.getElementById("rs-width-percentage").value = newWidthPerc;
data.widthPercentage = newWidthPerc;
}
document.getElementById("rs-height-percentage").value = 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) {
let oldValue = 100;
let ratio;
let newWidth, newHeight, newHeightPerc;
data.widthPercentage = event.target.value;
delta = data.widthPercentage - oldValue;
ratio = data.widthPercentage / oldValue;
console.log("old value: " + oldValue + ", ratio: " + ratio);
newHeight = startData.height * ratio;
newHeightPerc = data.widthPercentage;
newWidth = startData.width * ratio;
if (keepRatio) {
document.getElementById("rs-height-percentage").value = newHeightPerc;
data.heightPercentage = newHeightPerc;
document.getElementById("rs-height").value = newHeight
data.height = newHeight;
}
document.getElementById("rs-width").value = 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) {
let oldValue = data.heightPercentage;
let ratio;
let newHeight, newWidth, newWidthPerc;
data.heightPercentage = event.target.value;
delta = data.heightPercentage - oldValue;
ratio = data.heightPercentage / oldValue;
newWidth = startData.width * ratio;
newWidthPerc = data.heightPercentage;
newHeight = startData.height * ratio;
if (keepRatio) {
document.getElementById("rs-width-percentage").value = data.heightPercentage * currentRatio;
data.widthPercentage = newWidthPerc;
document.getElementById("rs-width").value = newWidth;
data.width = newWidth;
}
document.getElementById("rs-height").value = newHeight;
data.height = newHeight;
}
/** Toggles the keepRatio value (fired by the checkbox in the pop up window)
*
* @param {*} event
*/
function toggleRatio(event) {
keepRatio = !keepRatio;
}
/** Changes the scaling algorithm (fired by the combobox in the pop up window)
*
* @param {*} event
*/
function changedAlgorithm(event) {
currentAlgo = event.target.value;
}

View File

@ -1,39 +0,0 @@
/**
* Opens the save project window and initializes events for save project customization.
*/
function openSaveProjectWindow() {
//create name
var selectedPalette = getText('palette-button');
if (selectedPalette != 'Choose a palette...'){
var paletteAbbreviation = palettes[selectedPalette].abbreviation;
var fileName = 'pixel-'+paletteAbbreviation+'-'+canvasSize[0]+'x'+canvasSize[1];
} else {
var fileName = 'pixel-'+canvasSize[0]+'x'+canvasSize[1];
selectedPalette = 'none';
}
setValue('lpe-file-name', fileName);
document.getElementById("save-project-confirm").addEventListener("click", saveProject);
showDialogue('save-project', false);
}
/**
* Downloads the .lpe file for the current project.
*/
function saveProject() {
var fileName = `${getValue('lpe-file-name')}.lpe`;
// create file content
var content = getProjectData();
//set download link
var linkHolder = document.getElementById('save-project-link-holder');
linkHolder.href = 'data:text/plain;charset=utf-8,' + encodeURIComponent(content);
linkHolder.download = fileName;
linkHolder.click();
ga('send', 'event', 'Pixel Editor Save', selectedPalette, canvasSize[0]+'/'+canvasSize[1]); /*global ga*/
}

View File

@ -1,50 +0,0 @@
var settings;
if (!Cookies.enabled) {
document.getElementById('cookies-disabled-warning').style.display = 'block';
}
//try to load settings from cookie
var settingsFromCookie = Cookies.get('pixelEditorSettings');
if(!settingsFromCookie) {
console.log('settings cookie not found');
settings = {
switchToChangedColor: true,
enableDynamicCursorOutline: true, //unused - performance
enableBrushPreview: true, //unused - performance
enableEyedropperPreview: true, //unused - performance
numberOfHistoryStates: 20,
maxColorsOnImportedImage: 128,
pixelGridColour: '#000000'
};
}
else{
console.log('settings cookie found');
console.log(settingsFromCookie);
var settings = JSON.parse(settingsFromCookie);
}
console.log(settings);
//on clicking the save button in the settings dialog
on('click', 'save-settings', saveSettings);
function saveSettings() {
//check if values are valid
if (isNaN(getValue('setting-numberOfHistoryStates'))) {
alert('Invalid value for numberOfHistoryStates');
return;
}
//save new settings to settings object
settings.numberOfHistoryStates = getValue('setting-numberOfHistoryStates');
settings.pixelGridColour = getValue('setting-pixelGridColour');
// Filling pixel grid again if colour changed
fillPixelGrid();
//save settings object to cookie
var cookieValue = JSON.stringify(settings);
Cookies.set('pixelEditorSettings', cookieValue, { expires: Infinity });
//close window
closeDialogue();
}

View File

@ -1,23 +0,0 @@
function SplashCoverImage(path, author, link) {
this.path = path;
this.author = author;
this.link = link;
}
let images = [
new SplashCoverImage('Rayquaza', 'Unsettled', 'https://lospec.com/unsettled'),
new SplashCoverImage('Mountains', 'Skeddles', 'https://lospec.com/skeddles'),
new SplashCoverImage('Sweetie', 'GrafxKid', 'https://twitter.com/GrafxKid'),
new SplashCoverImage('Glacier', 'WindfallApples', 'https://lospec.com/windfallapples'),
new SplashCoverImage('Polyphorge1', 'Polyphorge', 'https://lospec.com/poly-phorge'),
new SplashCoverImage('Fusionnist', 'Fusionnist', 'https://lospec.com/fusionnist')
];
let coverImage = document.getElementById('editor-logo');
let authorLink = coverImage.getElementsByTagName('a')[0];
let chosenImage = images[Math.round(Math.random() * (images.length - 1))];
coverImage.style.backgroundImage = 'url("' + chosenImage.path + '.png")';
authorLink.setAttribute('href', chosenImage.link);
authorLink.innerHTML = 'Art by ' + chosenImage.author;

View File

@ -1,127 +0,0 @@
//pencil
on('click',"pencil-button", function(){
tool.pencil.switchTo();
}, false);
//pencil bigger
on('click',"pencil-bigger-button", function(){
tool.pencil.brushSize++;
}, false);
//pencil smaller
on('click',"pencil-smaller-button", function(){
if(tool.pencil.brushSize > 1)
tool.pencil.brushSize--;
}, false);
//eraser
on('click',"eraser-button", function(){
console.log("selecting eraser");
tool.eraser.switchTo();
}, false);
//eraser bigger
on('click',"eraser-bigger-button", function(){
tool.eraser.brushSize++;
}, false);
//eraser smaller
on('click',"eraser-smaller-button", function(e){
if(tool.eraser.brushSize > 1)
tool.eraser.brushSize--;
}, false);
// rectangle
on('click','rectangle-button', function(e){
// If the user clicks twice on the button, they change the draw mode
if (currentTool.name == 'rectangle') {
if (rectangleDrawMode == 'empty') {
rectangleDrawMode = 'fill';
setRectToolSvg();
}
else {
rectangleDrawMode = 'empty';
setRectToolSvg();
}
}
else {
tool.rectangle.switchTo();
}
}, false);
// ellipse
on('click','ellipse-button', function(e){
// If the user clicks twice on the button, they change the draw mode
if (currentTool.name == 'ellipse') {
if (ellipseDrawMode == 'empty') {
ellipseDrawMode = 'fill';
setEllipseToolSvg();
}
else {
ellipseDrawMode = 'empty';
setEllipseToolSvg();
}
}
else {
tool.ellipse.switchTo();
}
}, false);
// rectangle bigger
on('click',"rectangle-bigger-button", function(){
tool.rectangle.brushSize++;
}, false);
// rectangle smaller
on('click',"rectangle-smaller-button", function(e){
if(tool.rectangle.brushSize > 1)
tool.rectangle.brushSize--;
}, false);
// ellipse bigger
on('click',"ellipse-bigger-button", function(){
tool.ellipse.brushSize++;
}, false);
// ellipse smaller
on('click',"ellipse-smaller-button", function(e){
if(tool.ellipse.brushSize > 1)
tool.ellipse.brushSize--;
}, false);
//fill
on('click',"fill-button", function(){
tool.fill.switchTo();
}, false);
//pan
on('click',"pan-button", function(){
tool.pan.switchTo();
}, false);
//eyedropper
on('click',"eyedropper-button", function(){
tool.eyedropper.switchTo();
}, false);
//rectangular selection button
on('click', "rectselect-button", function(){
tool.rectselect.switchTo();
}, 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 */

View File

@ -1,109 +0,0 @@
//tools container / list, automatically managed when you create a new Tool();
var tool = {};
//class for tools
class Tool {
constructor (name, options) {
//stores the name in object, only needed for legacy functions from when currentTool was just a string
this.name = name;
//copy options to this object
if (options.cursor) {
//passed statically as a string
if (typeof options.cursor == 'string') this.cursor = options.cursor;
//passed a function which should be used as a getter function
if (typeof options.cursor == 'function') Object.defineProperty(this, 'cursor', { get: options.cursor});
}
if (options.imageCursor) this.cursor = "url(\'/pixel-editor/"+options.imageCursor+".png\'), auto";
this.currentBrushSize = 1;
this.previousBrushSize = 1;
if (options.brushPreview) {
this.brushPreview = true;
}
//add to tool object so it can be referenced
tool[name] = this;
}
get brushSize () {
return this.currentBrushSize;
}
set brushSize (value) {
this.currentBrushSize = value;
this.updateCursor();
}
//switch to this tool (replaced global changeTool())
switchTo () {
// Ending any selection in progress
if (currentTool.name.includes("select") && !this.name.includes("select") && !selectionCanceled) {
endSelection();
}
//set tool and temp tje tje tpp <--- he's speaking the language of the gods, don't delete
currentTool = this;
currentToolTemp = this;
var tools = document.getElementById("tools-menu").children;
for (var i = 0; i < tools.length; i++) {
tools[i].classList.remove("selected");
}
let buttonNode = document.getElementById(this.name + "-button");
//give the button of the selected tool the .selected class if the tool has a button
if(buttonNode != null && buttonNode.parentNode != null) {
document.getElementById(this.name+"-button").parentNode.classList.add("selected");
}
//change cursor
this.updateCursor();
}
updateCursor () {
//switch to that tools cursor
canvasView.style.cursor = this.cursor || 'default';
//if the tool uses a brush preview, make it visible and update the size
if (this.brushPreview) {
//console.log('brush size',this.currentBrushSize)
brushPreview.style.display = 'block';
brushPreview.style.width = this.currentBrushSize * zoom + 'px';
brushPreview.style.height = this.currentBrushSize * zoom + 'px';
}
//show / hide eyedropper color preview
if (this.eyedropperPreview) eyedropperPreview.style.display = 'block';
else eyedropperPreview.style.display = 'none';
//moveSelection
if (currentTool.name == 'moveselection') {
if (cursorInSelectedArea()) {
canMoveSelection = true;
canvasView.style.cursor = 'move';
brushPreview.style.display = 'none';
}
else {
canvasView.style.cursor = 'crosshair';
}
}
}
moveBrushPreview(cursorLocation) {
let toSub = 1;
// Prevents the brush to be put in the middle of pixels
if (this.currentBrushSize % 2 == 0) {
toSub = 0.5;
}
brushPreview.style.left = (Math.floor(cursorLocation[0] / zoom) * zoom + currentLayer.canvas.offsetLeft - this.currentBrushSize * zoom / 2 - zoom / 2 + toSub * zoom) + 'px';
brushPreview.style.top = (Math.floor(cursorLocation[1] / zoom) * zoom + currentLayer.canvas.offsetTop - this.currentBrushSize * zoom / 2 - zoom / 2 + toSub * zoom) + 'px';
}
}
/*global dragging currentTool, currentToolTemp, selectionCanceled, endSelection*/

View File

@ -1,45 +0,0 @@
//init variables
var canvasSize;
var zoom = 7;
var dragging = false;
var lastMouseClickPos = [0,0];
var dialogueOpen = true;
var documentCreated = false;
var pixelEditorMode = "Basic";
//common elements
var brushPreview = document.getElementById("brush-preview");
var eyedropperPreview = document.getElementById("eyedropper-preview");
var canvasView = document.getElementById("canvas-view");
var colors = document.getElementsByClassName("color-button");
var colorsMenu = document.getElementById("colors-menu");
var popUpContainer = document.getElementById("pop-up-container");
// main canvas
var canvas = document.getElementById('pixel-canvas');
var currentGlobalColor;
// Layers
var layers = [];
// Currently selected layer
var currentLayer;
// VFX layer used to draw previews of the selection and things like that
var VFXLayer;
// VFX canvas
var VFXCanvas = document.getElementById('vfx-canvas');
// TMP layer
var TMPLayer;
// TMP canvas
var TMPCanvas = document.getElementById('tmp-canvas');
// Pixel grid layer
var pixelGrid;
// Pixel grid canvas
var pixelGridCanvas;
// Index of the first layer the user can use in the layers array
var firstUserLayerIndex = 2;
// Number of layers that are only used by the editor
var nAppLayers = 3;

View File

@ -1,15 +0,0 @@
function closeCompatibilityWarning() {
document.getElementById("compatibility-warning").style.visibility = "hidden";
}
//check browser/version
if (
(bowser.firefox && bowser.version >= 28) ||
(bowser.chrome && bowser.version >= 29) ||
(!bowser.mobile && !bowser.tablet)
)
console.log("compatibility check passed");
//show warning
else document.getElementById("compatibility-warning").style.visibility = "visible";

9
js/data/consts.js Normal file
View File

@ -0,0 +1,9 @@
const MIN_Z_INDEX = -5000;
const MAX_Z_INDEX = 5000;
// Index of the first layer the user can use in the layers array
const firstUserLayerIndex = 2;
// Number of layers that are only used by the editor
const nAppLayers = 3;
const MIN_ZOOM_LEVEL = 0.5;

View File

@ -66,8 +66,6 @@ palettes["Gameboy Color"] = {"name":"Nintendo Gameboy (Black Zero)","author":"",
const loadPaletteButtonSplash = document.getElementById('load-palette-button-splash');
splashPalettes.refresh = function () {
splashPalettes.innerHTML = '';
palettesMenu.innerHTML = '';
Object.keys(palettes).forEach((paletteName,) => {
@ -110,7 +108,6 @@ palettes["Gameboy Color"] = {"name":"Nintendo Gameboy (Black Zero)","author":"",
splashPalettes.refresh();
const loadPaletteButtonEvent = () => {
document.getElementById('load-palette-browse-holder').click();
}

81
js/layers/Checkerboard.js Normal file
View File

@ -0,0 +1,81 @@
class Checkerboard extends Layer {
// Checkerboard color 1
firstCheckerBoardColor = 'rgba(179, 173, 182, 1)';
// Checkerboard color 2
secondCheckerBoardColor = 'rgba(204, 200, 206, 1)';
// Square size for the checkerboard
checkerBoardSquareSize = 16;
// Setting current colour (each square has a different colour
currentColor = undefined;
// Saving number of squares filled until now
nSquaresFilled = 0;
constructor(width, height, canvas, menuEntry) {
super(width, height, document.getElementById('checkerboard'), menuEntry);
this.initialize();
}
initialize() {
super.initialize();
console.log("Square size: " + this.checkerBoardSquareSize);
this.currentColor = this.firstCheckerBoardColor;
this.fillCheckerboard();
}
/** Fills the checkerboard canvas with squares with alternating colours
*
*/
fillCheckerboard() {
this.context.clearRect(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
// Cycling through the canvas (using it as a matrix)
for (let i=0; i<currFile.canvasSize[0] / this.checkerBoardSquareSize; i++) {
this.nSquaresFilled = 0;
for (let j=0; j<currFile.canvasSize[1] / this.checkerBoardSquareSize; j++) {
let rectX;
let rectY;
// Managing the not perfect squares (the ones at the sides if the canvas' sizes are not powers of checkerBoardSquareSize
if (i * this.checkerBoardSquareSize < currFile.canvasSize[0]) {
rectX = i * this.checkerBoardSquareSize;
}
else {
rectX = currFile.canvasSize[0];
}
if (j * this.checkerBoardSquareSize < currFile.canvasSize[1]) {
rectY = j * this.checkerBoardSquareSize;
}
else {
rectY = currFile.canvasSize[1];
}
// Selecting the colour
this.context.fillStyle = this.currentColor;
this.context.fillRect(rectX, rectY, this.checkerBoardSquareSize, this.checkerBoardSquareSize);
// Changing colour
this.changeCheckerboardColor();
this.nSquaresFilled++;
}
// If the number of filled squares was even, I change colour for the next column
if ((this.nSquaresFilled % 2) == 0) {
this.changeCheckerboardColor();
}
}
}
// Simply switches the checkerboard colour
changeCheckerboardColor() {
if (this.currentColor == this.firstCheckerBoardColor) {
this.currentColor = this.secondCheckerBoardColor;
} else if (this.currentColor == this.secondCheckerBoardColor) {
this.currentColor = this.firstCheckerBoardColor;
}
}
}

319
js/layers/Layer.js Normal file
View File

@ -0,0 +1,319 @@
// TODO: add undo for layer drag n drop
/** Handler class for a single canvas (a single layer)
*
* @param width Canvas width
* @param height Canvas height
* @param canvas HTML canvas element or the ID of the canvas related to the layer
*/
class Layer {
static layerCount = 1;
static maxZIndex = 3;
static unusedIDs = [];
static currentID = 1;
static layerOptions = document.getElementById("layer-properties-menu");
// TODO: this is simply terrible. menuEntry can either be an HTML element, a string or undefined.
// If it's an HTML element it is added, if it's a string nothing happens, if it's undefined the
// first menuEntry is used (the one that appears on load)
constructor(width, height, canvas, menuEntry) {
// REFACTOR: the canvas should actually be a Canvas instance
this.canvas = Util.getElement(canvas);
this.canvas.width = width;
this.canvas.height = height;
// REFACTOR: the context could be an attribute of the canvas class, but it's a bit easier
// to just type layer.context, we should discuss this
this.context = this.canvas.getContext('2d');
this.isSelected = false;
this.isVisible = true;
this.isLocked = false;
this.oldLayerName = null;
if (typeof menuEntry == "string")
this.menuEntry = document.getElementById("layers-menu").firstElementChild;
else if (menuEntry !== undefined)
this.menuEntry = menuEntry;
let id = Layer.unusedIDs.pop();
if (id == null) {
id = Layer.currentID;
Layer.currentID++;
}
this.id = "layer" + id;
// Binding the events
if (this.menuEntry !== undefined) {
this.name = this.menuEntry.getElementsByTagName("p")[0].innerHTML;
this.menuEntry.id = "layer" + id;
this.menuEntry.onmouseover = () => this.hover();
this.menuEntry.onmouseout = () => this.unhover();
this.menuEntry.onclick = () => this.selectLayer();
this.menuEntry.getElementsByTagName("button")[0].onclick = () => this.toggleLock();
this.menuEntry.getElementsByTagName("button")[1].onclick = () => this.toggleVisibility();
this.menuEntry.addEventListener("mouseup", this.openOptionsMenu, false);
this.menuEntry.addEventListener("dragstart", this.layerDragStart, false);
this.menuEntry.addEventListener("drop", this.layerDragDrop, false);
this.menuEntry.addEventListener("dragover", this.layerDragOver, false);
this.menuEntry.addEventListener("dragleave", this.layerDragLeave, false);
this.menuEntry.addEventListener("dragend", this.layerDragEnd, false);
Events.onCustom("del", this.tryDelete.bind(this));
this.menuEntry.getElementsByTagName("canvas")[0].getContext('2d').imageSmoothingEnabled = false;
}
this.initialize();
}
hasCanvas() {
return this.menuEntry != null;
}
tryDelete() {
if (Input.getLastTarget() != this.menuEntry && Input.getLastTarget().parentElement != this.menuEntry)
return;
LayerList.deleteLayer();
}
// Initializes the canvas
initialize() {
//resize canvas
this.canvas.style.width = (this.canvas.width*currFile.zoom)+'px';
this.canvas.style.height = (this.canvas.height*currFile.zoom)+'px';
//show canvas
this.canvas.style.display = 'block';
//center canvas in window
this.canvas.style.left = 64+currFile.canvasView.clientWidth/2-(currFile.canvasSize[0]*currFile.zoom/2)+'px';
this.canvas.style.top = 48+currFile.canvasView.clientHeight/2-(currFile.canvasSize[1]*currFile.zoom/2)+'px';
this.context.imageSmoothingEnabled = false;
this.context.mozImageSmoothingEnabled = false;
}
rename() {
this.menuEntry.getElementsByTagName("p")[0].setAttribute("contenteditable", false);
if (this.oldLayerName != null) {
let name = this.menuEntry.getElementsByTagName("p")[0].innerHTML;
for (let i=0; i<currFile.layers.length; i++) {
if (name === currFile.layers[i].name) {
name += ' (1)';
i = 0;
}
}
this.name = name;
this.menuEntry.getElementsByTagName("p")[0].innerHTML = name;
new HistoryState().RenameLayer(this.oldLayerName, name, currFile.currentLayer);
this.oldLayerName = null;
}
}
hover() {
// Hides all the layers but the current one
for (let i=1; i<currFile.layers.length - nAppLayers; i++) {
if (currFile.layers[i] !== this) {
currFile.layers[i].canvas.style.opacity = 0.3;
}
}
}
unhover() {
// Shows all the layers again
for (let i=1; i<currFile.layers.length - nAppLayers; i++) {
if (currFile.layers[i] !== this) {
currFile.layers[i].canvas.style.opacity = 1;
}
}
}
setID(id) {
this.id = id;
if (this.menuEntry != null) {
this.menuEntry.id = id;
}
}
// Resizes canvas
resize() {
let newWidth = (this.canvas.width * currFile.zoom) + 'px';
let newHeight = (this.canvas.height *currFile.zoom)+ 'px';
this.canvas.style.width = newWidth;
this.canvas.style.height = newHeight;
}
setCanvasOffset (offsetLeft, offsetTop) {
//horizontal offset
var minXOffset = -currFile.canvasSize[0] * currFile.zoom;
var maxXOffset = window.innerWidth - 300;
if (offsetLeft < minXOffset)
this.canvas.style.left = minXOffset +'px';
else if (offsetLeft > maxXOffset)
this.canvas.style.left = maxXOffset +'px';
else
this.canvas.style.left = offsetLeft +'px';
//vertical offset
var minYOffset = -currFile.canvasSize[1] * currFile.zoom + 164;
var maxYOffset = window.innerHeight - 100;
if (offsetTop < minYOffset)
this.canvas.style.top = minYOffset +'px';
else if (offsetTop > maxYOffset)
this.canvas.style.top = maxYOffset +'px';
else
this.canvas.style.top = offsetTop +'px';
}
// Copies the otherLayer's position and size
copyData(otherLayer) {
this.canvas.style.width = otherLayer.canvas.style.width;
this.canvas.style.height = otherLayer.canvas.style.height;
this.canvas.style.left = otherLayer.canvas.style.left;
this.canvas.style.top = otherLayer.canvas.style.top;
}
selectLayer(hideOptions = true) {
if (hideOptions)
LayerList.closeOptionsMenu();
// Deselecting the old layer
currFile.currentLayer.deselectLayer();
// Selecting the current layer
this.isSelected = true;
this.menuEntry.classList.add("selected-layer");
currFile.currentLayer = this;
}
toggleLock() {
if (this.isLocked) {
this.unlock();
}
else {
this.lock();
}
}
toggleVisibility() {
if (this.isVisible) {
this.hide();
}
else {
this.show();
}
}
deselectLayer() {
this.isSelected = false;
this.menuEntry.classList.remove("selected-layer");
}
lock() {
this.isLocked = true;
this.menuEntry.getElementsByClassName("layer-button")[0].style.visibility = "visible";
this.menuEntry.getElementsByClassName("default-icon")[0].style.display = "none";
this.menuEntry.getElementsByClassName("edited-icon")[0].style.display = "inline-block";
}
unlock() {
this.isLocked = false;
this.menuEntry.getElementsByClassName("layer-button")[0].style.visibility = "hidden";
this.menuEntry.getElementsByClassName("default-icon")[0].style.display = "inline-block";
this.menuEntry.getElementsByClassName("edited-icon")[0].style.display = "none";
}
show() {
this.isVisible = true;
this.canvas.style.visibility = "visible";
this.menuEntry.getElementsByClassName("layer-button")[1].style.visibility = "hidden";
// Changing icon
this.menuEntry.getElementsByClassName("default-icon")[1].style.display = "inline-block";
this.menuEntry.getElementsByClassName("edited-icon")[1].style.display = "none";
}
hide() {
this.isVisible = false;
this.canvas.style.visibility = "hidden";
this.menuEntry.getElementsByClassName("layer-button")[1].style.visibility = "visible";
// Changing icon
this.menuEntry.getElementsByClassName("default-icon")[1].style.display = "none";
this.menuEntry.getElementsByClassName("edited-icon")[1].style.display = "inline-block";
}
updateLayerPreview() {
// Getting the canvas
let destination = this.menuEntry.getElementsByTagName("canvas")[0];
let widthRatio = currFile.canvasSize[0] / currFile.canvasSize[1];
let heightRatio = currFile.canvasSize[1] / currFile.canvasSize[0];
// Computing width and height for the preview image
let previewWidth = destination.width;
let previewHeight = destination.height;
// If the sprite is rectangular, I apply the ratio to the preview as well
if (widthRatio < 1) {
previewWidth = destination.width * widthRatio;
}
else if (widthRatio > 1) {
previewHeight = destination.height * heightRatio;
}
// La appiccico sulla preview
destination.getContext('2d').clearRect(0, 0, destination.width, destination.height);
destination.getContext('2d').drawImage(this.canvas,
// This is necessary to center the preview in the canvas
(destination.width - previewWidth) / 2, (destination.height - previewHeight) / 2,
previewWidth, previewHeight);
}
drawLine(x0,y0,x1,y1, brushSize) {
var dx = Math.abs(x1-x0);
var dy = Math.abs(y1-y0);
var sx = (x0 < x1 ? 1 : -1);
var sy = (y0 < y1 ? 1 : -1);
var err = dx-dy;
while (true) {
//set pixel
// If the current tool is the brush
if (ToolManager.currentTool().name == 'brush' || ToolManager.currentTool().name == 'rectangle' || ToolManager.currentTool().name == 'ellipse') {
// I fill the rect
currFile.currentLayer.context.fillRect(x0-Math.floor(brushSize/2), y0-Math.floor(brushSize/2), brushSize, brushSize);
} else if (ToolManager.currentTool().name == 'eraser') {
// In case I'm using the eraser I must clear the rect
currFile.currentLayer.context.clearRect(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;
}
}
}
}

112
js/layers/PixelGrid.js Normal file
View File

@ -0,0 +1,112 @@
// OPTIMIZABLE: render the grid only for the current viewport
class PixelGrid extends Layer {
// Start colour of the pixel grid (can be changed in the preferences)
pixelGridColor = "#000000";
// Distance between one line and another in HTML pixels
lineDistance = 10;
// The grid is not visible by default
pixelGridVisible = false;
// The grid is enabled, but is disabled in order to save performance with big sprites
pixelGridEnabled = true;
constructor(width, height, canvas, menuEntry) {
super(width, height, canvas, menuEntry);
this.initialize();
Events.onCustom("refreshPixelGrid", this.fillPixelGrid.bind(this));
Events.onCustom("switchedToAdvanced", this.togglePixelGrid.bind(this));
Events.onCustom("switchedToBasic", this.togglePixelGrid.bind(this));
}
initialize() {
super.initialize();
this.fillPixelGrid();
}
disablePixelGrid() {
this.pixelGridEnabled = false;
this.canvas.style.display = "none";
}
enablePixelGrid() {
if (!this.pixelGridVisible)
return;
this.pixelGridEnabled = true;
this.canvas.style.display = "inline-block";
}
hidePixelGrid() {
let button = document.getElementById("toggle-pixelgrid-button");
this.pixelGridVisible = false;
button.innerHTML = "Show pixel grid";
this.canvas.style.display = "none";
}
showPixelGrid() {
let button = document.getElementById("toggle-pixelgrid-button");
this.pixelGridVisible = true;
button.innerHTML = "Hide pixel grid";
this.canvas.style.display = "inline-block";
}
repaintPixelGrid(factor) {
this.lineDistance += factor;
this.fillPixelGrid();
}
/** Shows or hides the pixel grid depening on its current visibility
* (triggered by the show pixel grid button in the top menu)
*
*/
togglePixelGrid(newState) {
//Set the state based on the passed newState variable, otherwise just toggle it
if (newState == 'on')
this.showPixelGrid();
else if (newState == 'off')
this.hidePixelGrid();
else
if (this.pixelGridVisible)
this.hidePixelGrid();
else
this.showPixelGrid();
}
/** Fills the pixelGridCanvas with the pixelgrid
*
*/
fillPixelGrid() {
let originalSize = currFile.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
this.canvas.width = originalSize[0] * Math.round(this.lineDistance);
this.canvas.height = originalSize[1] * Math.round(this.lineDistance);
this.context.strokeStyle = Settings.getCurrSettings().pixelGridColour;
console.log("Line ditance: " + this.lineDistance)
// OPTIMIZABLE, could probably be a bit more elegant
// Draw horizontal lines
for (let i=0; i<this.canvas.width / Math.round(this.lineDistance); i++) {
this.context.beginPath();
this.context.moveTo(i * Math.round(this.lineDistance) + 0.5, 0);
this.context.lineTo(i * Math.round(this.lineDistance) + 0.5,
originalSize[1] * Math.round(this.lineDistance));
this.context.stroke();
this.context.closePath();
}
// Draw vertical lines
for (let i=0; i<this.canvas.height / Math.round(this.lineDistance); i++) {
this.context.beginPath();
this.context.moveTo(0, i * Math.round(this.lineDistance) + 0.5);
this.context.lineTo(originalSize[0] * Math.round(this.lineDistance),
i * Math.round(this.lineDistance) + 0.5);
this.context.stroke();
this.context.closePath();
}
if (!this.pixelGridVisible) {
this.canvas.style.display = 'none';
}
}
}

View File

@ -550,10 +550,9 @@ if (!window.jscolor) { window.jscolor = (function () {
//console.log(e.target,'=====================================')
//if they clicked on the delete button [lospec]
if (e.target.className == 'delete-color-button') {
//saveHistoryState({type: 'deletecolor', colorValue: jsc.picker.owner.toString(), canvas: canvas.context.getImageData(0, 0, canvasSize[0], canvasSize[1])});
new HistoryStateDeleteColor(jsc.picker.owner.toString());
new HistoryState().DeleteColor(jsc.picker.owner.toString());
deleteColor(jsc.picker.owner.styleElement);
ColorModule.deleteColor(jsc.picker.owner.styleElement);
}
else if (e.target.className == 'jscolor-picker-bottom') {
//console.log('clicked color picker bottom')
@ -1071,18 +1070,12 @@ if (!window.jscolor) { window.jscolor = (function () {
this.hide = function () {
///console.log(this.styleElement)
if (isPickerOwner()) {
//console.log('color picker hidden')
//set the color to old color, in case the color is a duplicate that hasn't been resolved yet [lospec]
var hexInput = document.getElementById('jscolor-hex-input');
var oldColor = '#'+rgbToHex(hexInput.oldColor);
var oldColor = '#'+Color.rgbToHex(hexInput.oldColor);
this.fromString(oldColor);
document.getElementById('duplicate-color-warning').style.visibility = 'hidden';
//dialog is closed
dialogueOpen = false;
detachPicker();
}
};
@ -1091,9 +1084,6 @@ if (!window.jscolor) { window.jscolor = (function () {
this.show = function () {
drawPicker();
//a dialog is open
dialogueOpen = true;
//[lospec]
//find the hex input element
var hexInput = document.getElementById('jscolor-hex-input');
@ -1105,21 +1095,21 @@ if (!window.jscolor) { window.jscolor = (function () {
this.exportColor();
//set old color for updating colors on canvas
hexInput.oldColor = hexToRgb(hexInput.value);
hexInput.oldColor = Color.hexToRgb(hexInput.value);
//set the color element to the clicked button
hexInput.colorElement = this.styleElement;
//disable delete button if last color
var colors = document.getElementsByClassName('color-button');
var deleteButton = document.getElementsByClassName('delete-color-button')[0];
let colors = document.getElementsByClassName('color-button');
let deleteButton = document.getElementsByClassName('delete-color-button')[0];
if(colors.length == 1)
deleteButton.classList.add('disabled');
else
deleteButton.classList.remove('disabled');
//hide duplicate color warning
var duplicateColorWarning = document.getElementById('duplicate-color-warning');
let duplicateColorWarning = document.getElementById('duplicate-color-warning');
duplicateColorWarning.style.visibility = 'hidden';
};

View File

@ -1,89 +1,140 @@
/**utilities**/
//=include util/on.js
//=include util/onChildren.js
//=include util/onClick.js
//=include util/onClickChildren.js
//=include util/select.js
//=include util/getSetText.js
//=include util/getSetValue.js
//=include util/hexToRgb.js
//=include util/rgbToHex.js
//=include util/rgbToHsl.js
//=include util/hslToRgb.js
/** EXTERNALS AND LIBRARIES **/
//=include lib/cookies.js
//=include _pixelEditorUtility.js
//=include lib/jscolor.js
//=include lib/sortable.js
//=include _algorithms.js
//=include data/consts.js
//=include data/palettes.js
/** UTILITY AND INPUT **/
//=include Util.js
//=include Events.js
//=include Dialogue.js
//=include History.js
//=include Settings.js
//=include EditorState.js
/**init**/
//=include _consts.js
//=include _variables.js
//=include _settings.js
/** COLOR-RELATED **/
//=include Color.js
//=include ColorPicker.js
//=include PaletteBlock.js
/**dropdown formatting**/
//=include _editorMode.js
//=include _presets.js
//=include _palettes.js
/** BASE CLASSES **/
//=include File.js
//=include Tool.js
//=include layers/Layer.js
/**functions**/
//=include _tools.js
//=include tools/*.js
//=include _newPixel.js
//=include _createColorPalette.js
//=include _changeZoom.js
//=include _addColor.js
//=include _colorChanged.js
//=include _initColor.js
//=include _dialogue.js
//!=include _featuresLog.js
//=include _drawLine.js
//=include _getCursorPosition.js
//=include _fill.js
//=include _line.js
//=include _history.js
//=include _deleteColor.js
//=include _replaceAllOfColor.js
//=include _checkerboard.js
//=include _pixelGrid.js
//=include _layer.js
//=include _copyPaste.js
//=include _resizeCanvas.js
//=include _resizeSprite.js
//=include _colorPicker.js
//=include _paletteBlock.js
//=include _splashPage.js
//=include _pixelExport.js
//=include _saveProject.js
/** SPECIAL LAYERS **/
//=include layers/Checkerboard.js
//=include layers/PixelGrid.js
/**load file**/
//=include _loadImage.js
//=include _loadPalette.js
/** TOOLS **/
//=include tools/DrawingTool.js
//=include tools/ResizableTool.js
//=include tools/SelectionTool.js
/**event listeners**/
//=include _hotkeyListener.js
//=include _mouseEvents.js
//=include tools/BrushTool.js
//=include tools/EraserTool.js
//=include tools/LineTool.js
//=include tools/RectangleTool.js
//=include tools/FillTool.js
//=include tools/EyedropperTool.js
//=include tools/PanTool.js
//=include tools/ZoomTool.js
//=include tools/RectangularSelectionTool.js
//=include tools/MoveSelectionTool.js
/**buttons**/
//=include _toolButtons.js
//=include _addColorButton.js
//=include _clickedColor.js
//=include _fileMenu.js
//=include _createButton.js
//=include _rectSelect.js
//=include _move.js
//=include _rectangle.js
//=include _ellipse.js
/** MODULES AND MENUS **/
//=include SplashPage.js
//=include PresetModule.js
//=include ColorModule.js
//=include ToolManager.js
//=include LayerList.js
/**onload**/
//=include _onLoad.js
//=include _onbeforeunload.js
/** STARTUP AND FILE MANAGEMENT **/
//=include Startup.js
//=include FileManager.js
//=include TopMenuModule.js
/**libraries**/
//=include _jscolor.js
/** HTML INPUT EVENTS **/
//=include Input.js
/**feature toggles**/
//=include _featureToggles.js
/** IHER **/
//=include FeatureToggles.js
// Controls execution of this preset module
PresetModule.instrumentPresetMenu();
PresetModule.instrumentPresetMenu();
//when the page is done loading, you can get ready to start
window.onload = function () {
featureToggles.onLoad();
ToolManager.currentTool().updateCursor();
//check if there are any url parameters
if (window.location.pathname.replace('/pixel-editor/','').length <= 1) {
//show splash screen
Dialogue.showDialogue('splash', false);
}
//url parameters were specified
else {
let args = window.location.pathname.split('/');
let paletteSlug = args[2];
let dimentions = args[3];
//fetch palette via lospec palette API
fetch('https://lospec.com/palette-list/'+paletteSlug+'.json')
.then(response => response.json())
.then(data => {
//palette loaded successfully
palettes[paletteSlug] = data;
palettes[paletteSlug].specified = true;
//refresh list of palettes
document.getElementById('palette-menu-splash').refresh();
//if the dimentions were specified
if (dimentions && dimentions.length >= 3 && dimentions.includes('x')) {
let width = dimentions.split('x')[0];
let height = dimentions.split('x')[1];
//create new document
Startup.newPixel(width, height);
}
//dimentions were not specified -- show splash screen with palette preselected
else {
//show splash
Dialogue.showDialogue('new-pixel', false);
}
})
//error fetching url (either palette doesn't exist, or lospec is down)
.catch((error) => {
console.warn('failed to load palette "'+paletteSlug+'"', error);
//proceed to splash screen
Dialogue.showDialogue('splash', false);
});
}
};
//prevent user from leaving page with unsaved data
window.onbeforeunload = function() {
if (EditorState.documentCreated)
return 'You will lose your pixel if it\'s not saved!';
else return;
};
// Compatibility functions
function closeCompatibilityWarning() {
document.getElementById("compatibility-warning").style.visibility = "hidden";
}
//check browser/version
if (
(bowser.firefox && bowser.version >= 28) ||
(bowser.chrome && bowser.version >= 29) ||
(!bowser.mobile && !bowser.tablet)
)
console.log("compatibility check passed");
//show warning
else document.getElementById("compatibility-warning").style.visibility = "visible";

44
js/tools/BrushTool.js Normal file
View File

@ -0,0 +1,44 @@
class BrushTool extends ResizableTool {
constructor(name, options, switchFunction) {
super(name, options);
Events.on('click', this.mainButton, switchFunction, this);
Events.on('click', this.biggerButton, this.increaseSize.bind(this));
Events.on('click', this.smallerButton, this.decreaseSize.bind(this));
}
onStart(mousePos) {
super.onStart(mousePos);
new HistoryState().EditCanvas();
}
onDrag(mousePos, cursorTarget) {
super.onDrag(mousePos);
if (cursorTarget === undefined)
return;
//draw line to current pixel
if (cursorTarget.className == 'drawingCanvas' || cursorTarget.className == 'drawingCanvas') {
currFile.currentLayer.drawLine(Math.floor(this.prevMousePos[0]/currFile.zoom),
Math.floor(this.prevMousePos[1]/currFile.zoom),
Math.floor(this.currMousePos[0]/currFile.zoom),
Math.floor(this.currMousePos[1]/currFile.zoom),
this.currSize
);
}
currFile.currentLayer.updateLayerPreview();
}
onEnd(mousePos) {
super.onEnd(mousePos);
}
onSelect() {
super.onSelect();
}
onDeselect() {
super.onDeselect();
}
}

5
js/tools/DrawingTool.js Normal file
View File

@ -0,0 +1,5 @@
class DrawingTool extends Tool {
constructor (name, options) {
super(name, options);
}
}

45
js/tools/EraserTool.js Normal file
View File

@ -0,0 +1,45 @@
class EraserTool extends ResizableTool {
constructor(name, options, switchFunction) {
super(name, options);
Events.on('click', this.mainButton, switchFunction, this);
Events.on('click', this.biggerButton, this.increaseSize.bind(this));
Events.on('click', this.smallerButton, this.decreaseSize.bind(this));
}
onStart(mousePos) {
super.onStart(mousePos);
new HistoryState().EditCanvas();
}
onDrag(mousePos, cursorTarget) {
super.onDrag(mousePos);
if (cursorTarget === undefined)
return;
//draw line to current pixel
if (cursorTarget.className == 'drawingCanvas' || cursorTarget.className == 'drawingCanvas') {
currFile.currentLayer.drawLine(Math.floor(this.prevMousePos[0]/currFile.zoom),
Math.floor(this.prevMousePos[1]/currFile.zoom),
Math.floor(this.currMousePos[0]/currFile.zoom),
Math.floor(this.currMousePos[1]/currFile.zoom),
this.currSize
);
}
currFile.currentLayer.updateLayerPreview();
}
onEnd(mousePos) {
super.onEnd(mousePos);
this.endMousePos = mousePos;
}
onSelect() {
super.onSelect();
}
onDeselect() {
super.onDeselect();
}
}

111
js/tools/EyeDropperTool.js Normal file
View File

@ -0,0 +1,111 @@
class EyedropperTool extends Tool {
eyedropperPreview = document.getElementById("eyedropper-preview");
selectedColor = {r:0, g:0, b:0};
constructor(name, options, switchFunction) {
super(name, options);
Events.on('click', this.mainButton, switchFunction, this);
}
onStart(mousePos, target) {
super.onStart(mousePos);
if (target.className != 'drawingCanvas')
return;
this.eyedropperPreview.style.display = 'block';
this.onDrag(mousePos);
}
onDrag(mousePos) {
super.onDrag(mousePos);
this.selectedColor = this.pickColor(mousePos);
this.eyedropperPreview.style.borderColor = '#' + Color.rgbToHex(this.selectedColor);
this.eyedropperPreview.style.left = mousePos[0] + currFile.currentLayer.canvas.offsetLeft - 30 + 'px';
this.eyedropperPreview.style.top = mousePos[1] + currFile.currentLayer.canvas.offsetTop - 30 + 'px';
const colorLightness = Math.max(this.selectedColor.r,this.selectedColor.g,this.selectedColor.b);
//for the darkest 50% of colors, change the eyedropper preview to dark mode
if (colorLightness>127) this.eyedropperPreview.classList.remove('dark');
else this.eyedropperPreview.classList.add('dark');
this.changeColor();
}
onEnd(mousePos) {
super.onEnd(mousePos);
this.eyedropperPreview.style.display = 'none';
}
changeColor() {
let colorHex = Color.rgbToHex(this.selectedColor);
ColorModule.updateCurrentColor('#' + Color.rgbToHex(this.selectedColor));
let colors = document.getElementsByClassName('color-button');
for (let i = 0; i < colors.length; i++) {
//if picked color matches this color
if (colorHex == colors[i].jscolor.toString()) {
//remove current color selection
document.querySelector("#colors-menu li.selected")?.classList.remove("selected");
//set current color
for (let i=2; i<currFile.layers.length; i++) {
currFile.layers[i].context.fillStyle = '#' + colorHex;
}
//make color selected
colors[i].parentElement.classList.add('selected');
}
}
}
/** 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
*/
pickColor(cursorLocation) {
// Making sure max will take some kind of value
let max = -1;
// Using tmpColour to sample the sprite
let tmpColour;
// Returned colour
let selectedColor;
for (let i=1; i<currFile.layers.length-3; i++) {
// Getting the colour of the pixel in the cursorLocation
tmpColour = currFile.layers[i].context.getImageData(Math.floor(cursorLocation[0]/currFile.zoom),Math.floor(cursorLocation[1]/currFile.zoom),1,1).data;
// If it's not empty, I check if it's on the top of the previous colour
if (currFile.layers[i].canvas.style.zIndex > max || Util.isPixelEmpty(selectedColor) || selectedColor === undefined) {
max = currFile.layers[i].canvas.style.zIndex;
if (!Util.isPixelEmpty(tmpColour)) {
selectedColor = tmpColour;
}
}
}
// If the final colour was empty, I return black
if (Util.isPixelEmpty(tmpColour) && selectedColor === undefined) {
selectedColor = [0, 0, 0];
}
return {r:selectedColor[0], g:selectedColor[1], b:selectedColor[2]};
}
onSelect() {
super.onSelect();
}
onDeselect() {
super.onDeselect();
this.eyedropperPreview.style.display = 'none';
}
}

138
js/tools/FillTool.js Normal file
View File

@ -0,0 +1,138 @@
class FillTool extends DrawingTool {
constructor(name, options, switchFunction) {
super(name, options);
Events.on('click', this.mainButton, switchFunction, this);
}
onStart(mousePos, target) {
super.onStart(mousePos);
if (target.className != 'drawingCanvas')
return;
this.fill(mousePos);
currFile.currentLayer.updateLayerPreview();
new HistoryState().EditCanvas();
}
fill(cursorLocation) {
//changes a pixels color
function colorPixel(tempImage, pixelPos, fillColor) {
//console.log('colorPixel:',pixelPos);
tempImage.data[pixelPos] = fillColor.r;
tempImage.data[pixelPos + 1] = fillColor.g;
tempImage.data[pixelPos + 2] = fillColor.b;
tempImage.data[pixelPos + 3] = 255;
}
//change x y to color value passed from the function and use that as the original color
function matchStartColor(tempImage, pixelPos, color) {
//console.log('matchPixel:',x,y)
let r = tempImage.data[pixelPos];
let g = tempImage.data[pixelPos + 1];
let b = tempImage.data[pixelPos + 2];
let a = tempImage.data[pixelPos + 3];
//console.log(r == color[0] && g == color[1] && b == color[2]);
return (r == color[0] && g == color[1] && b == color[2] && a == color[3]);
}
//temporary image holds the data while we change it
let tempImage = currFile.currentLayer.context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
//this is an array that holds all of the pixels at the top of the cluster
let topmostPixelsArray = [[Math.floor(cursorLocation[0]/currFile.zoom), Math.floor(cursorLocation[1]/currFile.zoom)]];
//console.log('topmostPixelsArray:',topmostPixelsArray)
//the offset of the pixel in the temp image data to start with
let startingPosition = (topmostPixelsArray[0][1] * currFile.canvasSize[0] + topmostPixelsArray[0][0]) * 4;
//the color of the cluster that is being filled
let clusterColor = [tempImage.data[startingPosition],tempImage.data[startingPosition+1],tempImage.data[startingPosition+2], tempImage.data[startingPosition+3]];
//the color to fill with
let fillColor = Color.hexToRgb(currFile.currentLayer.context.fillStyle);
//if you try to fill with the same color that's already there, exit the function
if (clusterColor[0] == fillColor.r &&
clusterColor[1] == fillColor.g &&
clusterColor[2] == fillColor.b &&
clusterColor[3] != 0) {
return;
}
//loop until there are no more values left in this array
while (topmostPixelsArray.length) {
let reachLeft, reachRight;
//move the most recent pixel from the array and set it as our current working pixels
let currentPixel = topmostPixelsArray.pop();
//set the values of this pixel to x/y variables just for readability
let x = currentPixel[0];
let y = currentPixel[1];
//this variable holds the index of where the starting values for the current pixel are in the data array
//we multiply the number of rows down (y) times the width of each row, then add x. at the end we multiply by 4 because
//each pixel has 4 values, rgba
let pixelPos = (y * currFile.canvasSize[0] + x) * 4;
//move up in the image until you reach the top or the pixel you hit was not the right color
while (y-- >= 0 && matchStartColor(tempImage, pixelPos, clusterColor)) {
pixelPos -= currFile.canvasSize[0] * 4;
}
pixelPos += currFile.canvasSize[0] * 4;
++y;
reachLeft = false;
reachRight = false;
while (y++ < currFile.canvasSize[1] - 1 && matchStartColor(tempImage, pixelPos, clusterColor)) {
colorPixel(tempImage, pixelPos, fillColor);
if (x > 0) {
if (matchStartColor(tempImage, pixelPos - 4, clusterColor)) {
if (!reachLeft) {
topmostPixelsArray.push([x - 1, y]);
reachLeft = true;
}
}
else if (reachLeft) {
reachLeft = false;
}
}
if (x < currFile.canvasSize[0] - 1) {
if (matchStartColor(tempImage, pixelPos + 4, clusterColor)) {
if (!reachRight) {
topmostPixelsArray.push([x + 1, y]);
reachRight = true;
}
}
else if (reachRight) {
reachRight = false;
}
}
pixelPos += currFile.canvasSize[0] * 4;
}
}
currFile.currentLayer.context.putImageData(tempImage, 0, 0);
}
onDrag(mousePos, cursorTarget) {
}
onEnd(mousePos) {
super.onEnd(mousePos);
}
onSelect() {
super.onSelect();
}
onDeselect() {
super.onDeselect();
}
}

95
js/tools/LineTool.js Normal file
View File

@ -0,0 +1,95 @@
class LineTool extends ResizableTool {
constructor(name, options, switchFunction) {
super(name, options);
Events.on('click', this.mainButton, switchFunction, this);
Events.on('click', this.biggerButton, this.increaseSize.bind(this));
Events.on('click', this.smallerButton, this.decreaseSize.bind(this));
}
onStart(mousePos) {
super.onStart(mousePos);
// Putting the tmp layer on top of everything
currFile.TMPLayer.canvas.style.zIndex = parseInt(currFile.currentLayer.canvas.style.zIndex, 10) + 1;
this.startMousePos[0] = Math.floor(mousePos[0]) + 0.5;
this.startMousePos[1] = Math.floor(mousePos[1]) + 0.5;
new HistoryState().EditCanvas();
}
onDrag(mousePos, cursorTarget) {
super.onDrag(mousePos);
// Drawing the line at the right position
this.drawLine(mousePos);
}
/** Finishes drawing the rect, decides the end coordinates and moves the preview rectangle to the
* current layer
*
* @param {*} mousePos The position of the mouse when the user stopped dragging
*/
onEnd(mousePos) {
super.onEnd(mousePos);
const tmpContext = currFile.TMPLayer.context;
const tmpCanvas = currFile.TMPLayer.canvas;
// Setting the correct linewidth and colour
currFile.currentLayer.context.lineWidth = this.currSize;
// Drawing the line
currFile.currentLayer.context.drawImage(tmpCanvas, 0, 0);
// Update the layer preview
currFile.currentLayer.updateLayerPreview();
// Clearing the tmp canvas
tmpContext.clearRect(0, 0, currFile.TMPLayer.canvas.width, currFile.TMPLayer.canvas.height);
}
onSelect() {
super.onSelect();
}
onDeselect() {
super.onDeselect();
}
drawLine(mousePos) {
let x0 = Math.floor(this.startMousePos[0]/currFile.zoom);
let y0 = Math.floor(this.startMousePos[1]/currFile.zoom);
let x1 = Math.floor(mousePos[0]/currFile.zoom);
let y1 = Math.floor(mousePos[1]/currFile.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 canvas = document.getElementById('tmp-canvas');
const context = canvas.getContext('2d');
context.clearRect(0, 0, canvas.width, canvas.height);
canvas.style.zIndex = parseInt(currFile.currentLayer.canvas.style.zIndex, 10) + 1;
while (true) {
context.fillRect(x0-Math.floor(this.currSize/2), y0-Math.floor(this.currSize/2), this.currSize, this.currSize);
//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

@ -0,0 +1,174 @@
class MoveSelectionTool extends DrawingTool {
currSelection = undefined;
selectionTool = undefined;
endTool = undefined;
switchFunc = undefined;
lastCopiedSelection = undefined;
cutting = false;
constructor (name, options, switchFunc, endTool) {
super(name, options, switchFunc);
this.switchFunc = switchFunc;
this.endTool = endTool;
Events.onCustom("esc-pressed", this.endSelection.bind(this));
Events.onCustom("ctrl+c", this.copySelection.bind(this));
Events.onCustom("ctrl+x", this.cutSelection.bind(this));
Events.onCustom("ctrl+v", this.pasteSelection.bind(this));
}
copySelection() {
this.lastCopiedSelection = this.currSelection;
this.cutting = false;
}
cutSelection() {
if (currFile.currentLayer.isLocked)
return;
this.cutting = true;
this.lastCopiedSelection = this.currSelection;
this.endSelection();
this.currSelection = this.lastCopiedSelection;
// Cut the data
currFile.currentLayer.context.clearRect(this.currSelection.left-0.5, this.currSelection.top-0.5,
this.currSelection.width, this.currSelection.height);
}
pasteSelection() {
if (currFile.currentLayer.isLocked)
return;
if (this.lastCopiedSelection === undefined)
return;
// Finish the current selection and start a new one with the same data
if (!this.cutting) {
this.endSelection();
}
this.cutting = false;
this.switchFunc(this);
this.currSelection = this.lastCopiedSelection;
// Putting the vfx layer on top of everything
currFile.VFXLayer.canvas.style.zIndex = MAX_Z_INDEX;
this.onDrag(this.currMousePos);
new HistoryState().EditCanvas();
}
onStart(mousePos, mouseTarget) {
super.onStart(mousePos, mouseTarget);
if (!this.cursorInSelectedArea(mousePos) &&
!Util.isChildOfByClass(mouseTarget, "editor-top-menu")) {
this.endSelection();
}
}
onDrag(mousePos) {
super.onDrag(mousePos);
this.currSelection = this.selectionTool.moveAnts(mousePos[0]/currFile.zoom,
mousePos[1]/currFile.zoom, this.currSelection.width, this.currSelection.height);
// clear the entire tmp layer
currFile.TMPLayer.context.clearRect(0, 0, currFile.TMPLayer.canvas.width, currFile.TMPLayer.canvas.height);
// put the image data on the tmp layer with offset
currFile.TMPLayer.context.putImageData(
this.currSelection.data,
Math.round(mousePos[0] / currFile.zoom) - this.currSelection.width / 2,
Math.round(mousePos[1] / currFile.zoom) - this.currSelection.height / 2);
}
onEnd(mousePos) {
super.onEnd(mousePos);
}
onSelect() {
super.onSelect();
}
onDeselect() {
super.onDeselect();
}
setSelectionData(data, tool) {
this.currSelection = data;
this.selectionTool = tool;
this.firstTimeMove = true;
}
onHover(mousePos) {
super.onHover(mousePos);
if (this.cursorInSelectedArea(mousePos)) {
currFile.canvasView.style.cursor = 'move';
}
else {
currFile.canvasView.style.cursor = 'default';
}
}
cursorInSelectedArea(cursorPos) {
// Getting the coordinates relatively to the canvas
let x = cursorPos[0] / currFile.zoom;
let y = cursorPos[1] / currFile.zoom;
if (this.currSelection.left <= x && x <= this.currSelection.right) {
if (y <= this.currSelection.bottom && y >= this.currSelection.top) {
return true;
}
return false;
}
return false;
}
endSelection() {
if (this.currSelection == undefined)
return;
// Clearing the tmp (move preview) and vfx (ants) layers
currFile.TMPLayer.context.clearRect(0, 0, currFile.TMPLayer.canvas.width, currFile.TMPLayer.canvas.height);
currFile.VFXLayer.context.clearRect(0, 0, currFile.VFXLayer.canvas.width, currFile.VFXLayer.canvas.height);
// 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 = currFile.currentLayer.context.getImageData(
this.currSelection.left, this.currSelection.top,
this.currSelection.width+1, this.currSelection.height+1
);
let pasteData = this.currSelection.data.data.slice();
for (let i=0; i<underlyingImageData.data.length; i+=4) {
let currentMovePixel = [
pasteData[i], pasteData[i+1], pasteData[i+2], pasteData[i+3]
];
let currentUnderlyingPixel = [
underlyingImageData.data[i], underlyingImageData.data[i+1],
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 (Util.isPixelEmpty(currentMovePixel)) {
if (!Util.isPixelEmpty(currentUnderlyingPixel)) {
pasteData[i] = currentUnderlyingPixel[0];
pasteData[i+1] = currentUnderlyingPixel[1];
pasteData[i+2] = currentUnderlyingPixel[2];
pasteData[i+3] = currentUnderlyingPixel[3];
}
}
}
currFile.currentLayer.context.putImageData(new ImageData(pasteData, this.currSelection.width+1),
this.currSelection.left, this.currSelection.top
);
this.currSelection = undefined;
currFile.currentLayer.updateLayerPreview();
currFile.VFXLayer.canvas.style.zIndex = MIN_Z_INDEX;
// Switch to brush
this.switchFunc(this.endTool);
}
}

45
js/tools/PanTool.js Normal file
View File

@ -0,0 +1,45 @@
class PanTool extends Tool {
constructor(name, options, switchFunction) {
super(name, options);
Events.on('click', this.mainButton, switchFunction, this);
}
onStart(mousePos, target) {
super.onStart(mousePos);
if (target.className != 'drawingCanvas')
return;
currFile.canvasView.style.cursor = "url(\'/pixel-editor/pan-held.png\'), auto";
}
onDrag(mousePos, target) {
super.onDrag(mousePos);
if (target.className != 'drawingCanvas')
return;
// Setting first layer position
currFile.layers[0].setCanvasOffset(currFile.layers[0].canvas.offsetLeft + (mousePos[0] - this.startMousePos[0]), currFile.layers[0].canvas.offsetTop + (mousePos[1] - this.startMousePos[1]));
// Copying that position to the other layers
for (let i=1; i<currFile.layers.length; i++) {
currFile.layers[i].copyData(currFile.layers[0]);
}
}
onEnd(mousePos, target) {
super.onEnd(mousePos);
if (target.className != 'drawingCanvas')
return;
currFile.canvasView.style.cursor = "url(\'/pixel-editor/pan.png\'), auto";
}
onSelect() {
super.onSelect();
currFile.canvasView.style.cursor = "url(\'/pixel-editor/pan.png\'), auto";
}
onDeselect() {
super.onDeselect();
}
}

145
js/tools/RectangleTool.js Normal file
View File

@ -0,0 +1,145 @@
// TODO: FIX SELECTION
class RectangleTool extends ResizableTool {
// Saving the empty rect svg
emptyRectangleSVG = document.getElementById("rectangle-empty-button-svg");
// and the full rect svg so that I can change them when the user changes rect modes
fullRectangleSVG = document.getElementById("rectangle-full-button-svg");
// Current fill mode
currFillMode = 'empty';
switchFunction = null;
constructor(name, options, switchFunction) {
super(name, options);
this.switchFunction = switchFunction;
Events.on('click', this.mainButton, this.changeFillType.bind(this));
Events.on('click', this.biggerButton, this.increaseSize.bind(this));
Events.on('click', this.smallerButton, this.decreaseSize.bind(this));
}
changeFillType() {
if (this.isSelected)
if (this.currFillMode == 'empty') {
this.currFillMode = 'fill';
this.emptyRectangleSVG.setAttribute('display', 'none');
this.fullRectangleSVG.setAttribute('display', 'visible');
}
else {
this.currFillMode = 'empty'
this.emptyRectangleSVG.setAttribute('display', 'visible');
this.fullRectangleSVG.setAttribute('display', 'none');
}
else
this.switchFunction(this);
}
onStart(mousePos) {
super.onStart(mousePos);
// Putting the tmp layer on top of everything
currFile.TMPLayer.canvas.style.zIndex = parseInt(currFile.currentLayer.canvas.style.zIndex, 10) + 1;
this.startMousePos[0] = Math.floor(mousePos[0] / currFile.zoom) + 0.5;
this.startMousePos[1] = Math.floor(mousePos[1] / currFile.zoom) + 0.5;
new HistoryState().EditCanvas();
}
onDrag(mousePos, cursorTarget) {
// Drawing the rect at the right position
this.drawRect(Math.floor(mousePos[0] / currFile.zoom) + 0.5, Math.floor(mousePos[1] / currFile.zoom) + 0.5);
}
/** Finishes drawing the rect, decides the end coordinates and moves the preview rectangle to the
* current layer
*
* @param {*} mousePos The position of the mouse when the user stopped dragging
*/
onEnd(mousePos) {
super.onEnd(mousePos);
let tmpContext = currFile.TMPLayer.context;
let endRectX = Math.floor(mousePos[0] / currFile.zoom) + 0.5;
let endRectY = Math.floor(mousePos[1] / currFile.zoom) + 0.5;
let startRectX = this.startMousePos[0];
let startRectY = this.startMousePos[1];
// Inverting end and start (start must always be the top left corner)
if (endRectX < startRectX) {
let tmp = endRectX;
endRectX = startRectX;
startRectX = tmp;
}
// Same for the y
if (endRectY < startRectY) {
let tmp = endRectY;
endRectY = startRectY;
startRectY = tmp;
}
// Drawing the rect
startRectY -= 0.5;
endRectY -= 0.5;
endRectX -= 0.5;
startRectX -= 0.5;
// Setting the correct linewidth and colour
currFile.currentLayer.context.lineWidth = this.currSize;
// Drawing the rect using 4 lines
currFile.currentLayer.drawLine(startRectX, startRectY, endRectX, startRectY, this.currSize);
currFile.currentLayer.drawLine(endRectX, startRectY, endRectX, endRectY, this.currSize);
currFile.currentLayer.drawLine(endRectX, endRectY, startRectX, endRectY, this.currSize);
currFile.currentLayer.drawLine(startRectX, endRectY, startRectX, startRectY, this.currSize);
// If I have to fill it, I do so
if (this.currFillMode == 'fill') {
currFile.currentLayer.context.fillRect(startRectX, startRectY, endRectX - startRectX, endRectY - startRectY);
}
// Update the layer preview
currFile.currentLayer.updateLayerPreview();
// Clearing the tmp canvas
tmpContext.clearRect(0, 0, currFile.TMPLayer.canvas.width, currFile.TMPLayer.canvas.height);
}
onSelect() {
super.onSelect();
}
onDeselect() {
super.onDeselect();
}
/** Draws a rectangle with end coordinates given by x and y on the tmp 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
*/
drawRect(x, y) {
// Getting the tmp context
let tmpContext = currFile.TMPLayer.context;
// Clearing the tmp canvas
tmpContext.clearRect(0, 0, currFile.TMPLayer.canvas.width, currFile.TMPLayer.canvas.height);
// Drawing the rect
tmpContext.lineWidth = this.currSize;
// Drawing the rect
tmpContext.beginPath();
if ((this.currSize % 2 ) == 0) {
tmpContext.rect(this.startMousePos[0] - 0.5, this.startMousePos[1] - 0.5, x - this.startMousePos[0], y - this.startMousePos[1]);
}
else {
tmpContext.rect(this.startMousePos[0], this.startMousePos[1], x - this.startMousePos[0], y - this.startMousePos[1]);
}
tmpContext.setLineDash([]);
tmpContext.stroke();
}
}

View File

@ -0,0 +1,157 @@
class RectangularSelectionTool extends SelectionTool {
switchFunc = undefined;
moveTool = undefined;
currSelection = {};
constructor (name, options, switchFunc, moveTool) {
super(name, options, switchFunc);
this.switchFunc = switchFunc;
this.moveTool = moveTool;
Events.on('click', this.mainButton, switchFunc, this);
}
onStart(mousePos) {
super.onStart(mousePos);
// Putting the vfx layer on top of everything
currFile.VFXLayer.canvas.style.zIndex = MAX_Z_INDEX;
// Saving the start coords of the rect
this.startMousePos[0] = Math.round(this.startMousePos[0] / currFile.zoom) - 0.5;
this.startMousePos[1] = Math.round(this.startMousePos[1] / currFile.zoom) - 0.5;
// Avoiding external selections
if (this.startMousePos[0] < 0) {
this.startMousePos[0] = 0;
}
else if (this.startMousePos[0] > currFile.currentLayer.canvas.width) {
this.startMousePos[0] = currFile.currentLayer.canvas.width;
}
if (this.startMousePos[1] < 0) {
this.startMousePos[1] = 0;
}
else if (this.startMousePos[1] > currFile.currentLayer.canvas.height) {
this.startMousePos[1] = currFile.currentLayer.canvas.height;
}
// Drawing the rect
this.drawSelection(this.startMousePos[0], this.startMousePos[1]);
}
onDrag(mousePos) {
super.onDrag(mousePos);
// Drawing the rect
this.drawSelection(Math.round(mousePos[0] / currFile.zoom) + 0.5, Math.round(mousePos[1] / currFile.zoom) + 0.5);
}
onEnd(mousePos) {
super.onEnd(mousePos);
new HistoryState().EditCanvas();
// Getting the end position
this.endMousePos[0] = Math.round(this.endMousePos[0] / currFile.zoom) + 0.5;
this.endMousePos[1] = Math.round(this.endMousePos[1] / currFile.zoom) + 0.5;
// Inverting end and start (start must always be the top left corner)
if (this.endMousePos[0] < this.startMousePos[0]) {
let tmp = this.endMousePos[0];
this.endMousePos[0] = this.startMousePos[0];
this.startMousePos[0] = tmp;
}
// Same for the y
if (this.endMousePos[1] < this.startMousePos[1]) {
let tmp = this.endMousePos[1];
this.endMousePos[1] = this.startMousePos[1];
this.startMousePos[1] = tmp;
}
// Switch to the move tool so that the user can move the selection
this.switchFunc(this.moveTool);
// Preparing data for the move tool
let dataWidth = this.endMousePos[0] - this.startMousePos[0];
let dataHeight = this.endMousePos[1] - this.startMousePos[1];
this.currSelection = {
left: this.startMousePos[0], right: this.endMousePos[0],
top: this.startMousePos[1], bottom: this.endMousePos[1],
width: dataWidth,
height: dataHeight,
data: currFile.currentLayer.context.getImageData(
this.startMousePos[0], this.startMousePos[1],
dataWidth + 1, dataHeight + 1)
};
// Moving the selection to the TMP layer. It will be moved back to the original
// layer if the user will cancel or end the selection
currFile.currentLayer.context.clearRect(this.startMousePos[0] - 0.5, this.startMousePos[1] - 0.5,
dataWidth + 1, dataHeight + 1);
// Moving those pixels from the current layer to the tmp layer
currFile.TMPLayer.context.putImageData(this.currSelection.data, this.startMousePos[0], this.startMousePos[1]);
this.moveTool.setSelectionData(this.currSelection, this);
console.log("data set");
}
onSelect() {
super.onSelect();
}
onDeselect() {
super.onDeselect();
}
drawSelection(x, y) {
// Getting the vfx context
let vfxContext = currFile.VFXLayer.context;
// Clearing the vfx canvas
vfxContext.clearRect(0, 0, currFile.VFXLayer.canvas.width, currFile.VFXLayer.canvas.height);
vfxContext.lineWidth = 1;
vfxContext.strokeStyle = 'black';
vfxContext.setLineDash([4]);
// Drawing the rect
vfxContext.beginPath();
vfxContext.rect(this.startMousePos[0], this.startMousePos[1], x - this.startMousePos[0], y - this.startMousePos[1]);
vfxContext.stroke();
}
/** 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 selectione
*
* @return The data regarding the current position and size of the selection
*/
moveAnts(x, y, width, height) {
// Getting the vfx context
let vfxContext = currFile.VFXLayer.context;
let ret = this.currSelection;
// Clearing the vfx canvas
vfxContext.clearRect(0, 0, currFile.VFXLayer.canvas.width, currFile.VFXLayer.canvas.height);
vfxContext.lineWidth = 1;
vfxContext.setLineDash([4]);
// Fixing the coordinates
this.currSelection.left = Math.round(Math.round(x) - 0.5 - Math.round(width / 2)) + 0.5;
this.currSelection.top = Math.round(Math.round(y) - 0.5 - Math.round(height / 2)) + 0.5;
this.currSelection.right = this.currSelection.left + Math.round(width);
this.currSelection.bottom = this.currSelection.top + Math.round(height);
// Drawing the rect
vfxContext.beginPath();
vfxContext.rect(this.currSelection.left, this.currSelection.top, width, height);
vfxContext.stroke();
return ret;
}
}

32
js/tools/ResizableTool.js Normal file
View File

@ -0,0 +1,32 @@
class ResizableTool extends DrawingTool {
startResizePos = undefined;
currSize = 1;
prevSize = 1;
constructor(name, options, switchFunc) {
super(name, options, switchFunc);
}
onRightStart(mousePos, mouseEvent) {
this.startResizePos = mousePos;
}
onRightDrag(mousePos, mouseEvent) {
//get new brush size based on x distance from original clicking location
let distanceFromClick = mousePos[0]/currFile.zoom - this.startResizePos[0]/currFile.zoom;
let brushSizeChange = Math.round(distanceFromClick/10);
let newBrushSize = this.currSize + brushSizeChange;
//set the brush to the new size as long as its bigger than 1 and less than 128
this.currSize = Math.min(Math.max(1,newBrushSize), 128);
//fix offset so the cursor stays centered
this.updateCursor();
this.onHover(this.startResizePos, mouseEvent);
}
onRightEnd(mousePos, mouseEvent) {
}
}

View File

@ -0,0 +1,5 @@
class SelectionTool extends Tool {
constructor(name, options, switchFunc) {
super(name, options, switchFunc);
}
}

82
js/tools/ZoomTool.js Normal file
View File

@ -0,0 +1,82 @@
class ZoomTool extends Tool {
constructor (name, options) {
super(name, options, undefined);
}
onMouseWheel(mousePos, mode) {
super.onMouseWheel(mousePos, mode);
// Computing current width and height
let oldWidth = currFile.canvasSize[0] * currFile.zoom;
let oldHeight = currFile.canvasSize[1] * currFile.zoom;
let newWidth, newHeight;
let prevZoom = currFile.zoom;
let zoomed = false;
//change zoom level
//if you want to zoom out, and the zoom isnt already at the smallest level
if (mode == 'out' && currFile.zoom > MIN_ZOOM_LEVEL) {
zoomed = true;
if (currFile.zoom > 2)
currFile.zoom -= Math.ceil(currFile.zoom / 10);
else
currFile.zoom -= Math.ceil(currFile.zoom * 2 / 10) / 2;
newWidth = currFile.canvasSize[0] * currFile.zoom;
newHeight = currFile.canvasSize[1] * currFile.zoom;
//adjust canvas position
currFile.layers[0].setCanvasOffset(
currFile.layers[0].canvas.offsetLeft + (oldWidth - newWidth) * mousePos[0]/oldWidth,
currFile.layers[0].canvas.offsetTop + (oldHeight - newHeight) * mousePos[1]/oldHeight);
}
//if you want to zoom in
else if (mode == 'in' && currFile.zoom + Math.ceil(currFile.zoom/10) < window.innerHeight/4) {
zoomed = true;
if (currFile.zoom > 2)
currFile.zoom += Math.ceil(currFile.zoom/10);
else {
if (currFile.zoom + currFile.zoom/10 > 2) {
currFile.zoom += Math.ceil(currFile.zoom/10);
currFile.zoom = Math.ceil(currFile.zoom);
}
else {
currFile.zoom += Math.ceil(currFile.zoom * 2 / 10) / 2;
}
}
newWidth = currFile.canvasSize[0] * currFile.zoom;
newHeight = currFile.canvasSize[1] * currFile.zoom;
//adjust canvas position
currFile.layers[0].setCanvasOffset(
currFile.layers[0].canvas.offsetLeft - Math.round((newWidth - oldWidth)*mousePos[0]/oldWidth),
currFile.layers[0].canvas.offsetTop - Math.round((newHeight - oldHeight)*mousePos[1]/oldHeight));
}
//resize canvas
currFile.layers[0].resize();
// adjust brush size
ToolManager.currentTool().updateCursor();
// Adjust pixel grid thickness
if (zoomed) {
if (currFile.zoom <= 7)
currFile.pixelGrid.disablePixelGrid();
else if (currFile.zoom >= 20 && mode == 'in') {
currFile.pixelGrid.enablePixelGrid();
currFile.pixelGrid.repaintPixelGrid((currFile.zoom - prevZoom) * 0.6);
}
else if (prevZoom >= 20 && mode == 'out') {
currFile.pixelGrid.enablePixelGrid();
currFile.pixelGrid.repaintPixelGrid((currFile.zoom - prevZoom) * 0.6);
}
else {
currFile.pixelGrid.enablePixelGrid();
}
}
for (let i=1; i<currFile.layers.length; i++) {
currFile.layers[i].copyData(currFile.layers[0]);
}
}
}

View File

@ -1,68 +0,0 @@
new Tool('eraser', {
cursor: 'none',
brushPreview: true,
});
new Tool('resizeeraser', {
cursor: 'default',
});
new Tool('eyedropper', {
imageCursor: 'eyedropper',
});
new Tool('fill', {
imageCursor: 'fill',
});
new Tool('line', {
cursor: 'none',
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: 'none',
brushPreview: true,
});
new Tool('resizebrush', {
cursor: 'default',
});
new Tool('rectangle', {
cursor: 'none',
brushPreview: true,
});
new Tool('ellipse', {
cursor: 'none',
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

@ -21,12 +21,12 @@ let endEllipseY;
*/
function startEllipseDrawing(mouseEvent) {
// Putting the vfx layer on top of everything
VFXCanvas.style.zIndex = parseInt(currentLayer.canvas.style.zIndex, 10) + 1;;
VFXLayer.canvas.style.zIndex = parseInt(currentLayer.canvas.style.zIndex, 10) + 1;;
// Updating flag
isDrawingEllipse = true;
// Saving the start coords of the ellipse
let cursorPos = getCursorPosition(mouseEvent);
let cursorPos = Input.getCursorPosition(mouseEvent);
startEllipseX = Math.floor(cursorPos[0] / zoom) + 0.5;
startEllipseY = Math.floor(cursorPos[1] / zoom) + 0.5;
@ -39,7 +39,7 @@ function startEllipseDrawing(mouseEvent) {
* @param {*} mouseEvent The mouseEvent from which we'll get the mouse position
*/
function updateEllipseDrawing(mouseEvent) {
let pos = getCursorPosition(mouseEvent);
let pos = Input.getCursorPosition(mouseEvent);
// Drawing the ellipse at the right position
drawEllipse(Math.round(pos[0] / zoom) + 0.5, Math.round(pos[1] / zoom) + 0.5);
@ -53,8 +53,8 @@ function updateEllipseDrawing(mouseEvent) {
*/
function endEllipseDrawing(mouseEvent) {
// Getting the end position
let currentPos = getCursorPosition(mouseEvent);
let vfxContext = VFXCanvas.getContext("2d");
let currentPos = Input.getCursorPosition(mouseEvent);
let vfxContext = VFXLayer.context;
endEllipseX = Math.round(currentPos[0] / zoom) + 0.5;
endEllipseY = Math.round(currentPos[1] / zoom) + 0.5;
@ -80,15 +80,14 @@ function endEllipseDrawing(mouseEvent) {
endEllipseX -= 0.5;
startEllipseX -= 0.5;
// Setting the correct linewidth and colour
// Setting the correct linewidth
currentLayer.context.lineWidth = tool.ellipse.brushSize;
currentLayer.context.fillStyle = currentGlobalColor;
// Drawing the ellipse using 4 lines
line(startEllipseX, startEllipseY, endEllipseX, startEllipseY, tool.ellipse.brushSize);
line(endEllipseX, startEllipseY, endEllipseX, endEllipseY, tool.ellipse.brushSize);
line(endEllipseX, endEllipseY, startEllipseX, endEllipseY, tool.ellipse.brushSize);
line(startEllipseX, endEllipseY, startEllipseX, startEllipseY, tool.ellipse.brushSize);
currentLayer.drawLine(startEllipseX, startEllipseY, endEllipseX, startEllipseY, tool.ellipse.brushSize);
currentLayer.drawLine(endEllipseX, startEllipseY, endEllipseX, endEllipseY, tool.ellipse.brushSize);
currentLayer.drawLine(endEllipseX, endEllipseY, startEllipseX, endEllipseY, tool.ellipse.brushSize);
currentLayer.drawLine(startEllipseX, endEllipseY, startEllipseX, startEllipseY, tool.ellipse.brushSize);
// If I have to fill it, I do so
if (ellipseDrawMode == 'fill') {
@ -96,7 +95,7 @@ function endEllipseDrawing(mouseEvent) {
}
// Clearing the vfx canvas
vfxContext.clearRect(0, 0, VFXCanvas.width, VFXCanvas.height);
vfxContext.clearRect(0, 0, VFXLayer.canvas.width, VFXLayer.canvas.height);
}
// TODO: [ELLIPSE] Make it draw ellipse instead of copy-pasted rectangle
@ -108,14 +107,13 @@ function endEllipseDrawing(mouseEvent) {
*/
function drawEllipse(x, y) {
// Getting the vfx context
let vfxContext = VFXCanvas.getContext("2d");
let vfxContext = VFXLayer.context;
// Clearing the vfx canvas
vfxContext.clearRect(0, 0, VFXCanvas.width, VFXCanvas.height);
vfxContext.clearRect(0, 0, VFXLayer.canvas.width, VFXLayer.canvas.height);
// Drawing the ellipse
vfxContext.lineWidth = tool.ellipse.brushSize;
vfxContext.strokeStyle = currentGlobalColor;
// Drawing the ellipse
vfxContext.beginPath();

View File

@ -1,79 +0,0 @@
//GET: ajax(String url, Function success [,timeout])
//POST: ajax(String url, Object postData, Function success [,timeout])
Util.ajax = function (url, arg2, arg3, arg4) {
if (typeof arg2 == 'function') {
var success = arg2;
var timeout = arg3 || 10000;
}
else {
var postData = arg2;
var success = arg3;
var timeout = arg4 || 10000;
}
var start = new Date();
console.log('AJAX - STARTING REQUEST', url, '(' + timeout + ')');
//start new request
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
var result;
//try to parse as json
try {
result = JSON.parse(this.response);
console.log('AJAX - COMPLETE ('+(new Date()-start)+'ms) - json:', result);
}
catch (e) {
result = this.response;
console.log('AJAX - COMPLETE ('+(new Date()-start)+'ms) - string:', this.response, e);
}
//return result
success(result);
xhr = null;
}
else if (this.readyState == 4) {
console.log('ajax failed', this.readyState, this.status);
success({ error: 'failure' });
}
};
xhr.ontimeout = function(e) {
console.log('ajax request timed out')
success({ error: 'timeout' });
};
if (postData) {
//post request
console.log('post data: ', postData instanceof FormData, postData);
//the the input isn't already formdata, convert it to form data
if (!(postData instanceof FormData)) {
console.log('converting to form data');
var formData = new FormData();
for (var key in postData) {
formData.append(key, postData[key]);
}
postData = formData;
}
//send to server
xhr.open("POST", url, true);
xhr.timeout = timeout;
xhr.send(postData);
}
else {
//get request
xhr.open("GET", url, true);
xhr.timeout = timeout;
xhr.send();
}
return xhr;
}

View File

@ -1,10 +0,0 @@
//get text of specified element
function getText(elementId) {
var element = (typeof elementId == 'string' ? document.getElementById(elementId) : elementId);
return element.textContent;
}
function setText(elementId, text) {
var element = (typeof elementId == 'string' ? document.getElementById(elementId) : elementId);
element.textContent = text;
}

View File

@ -1,12 +0,0 @@
function getValue(elementId) {
var element = (typeof elementId == 'string' ? document.getElementById(elementId) : elementId);
console.log("setting: " + elementId + ": " + element.value);
return element.value;
}
function setValue(elementId, value) {
var element = (typeof elementId == 'string' ? document.getElementById(elementId) : elementId);
element.value = value;
}

Some files were not shown because too many files have changed in this diff Show More