diff --git a/README.md b/README.md index 2239b2c..b49a328 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/build.js b/build.js index 6d189df..5b4b4d0 100644 --- a/build.js +++ b/build.js @@ -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({ diff --git a/css/_canvas.scss b/css/_canvas.scss index 40633fb..5d02f4c 100644 --- a/css/_canvas.scss +++ b/css/_canvas.scss @@ -69,6 +69,8 @@ } #brush-preview { + background-color:black; + opacity:0.3; position: absolute; border: solid 1px #fff; z-index: 1200; diff --git a/css/_tools-menu.scss b/css/_tools-menu.scss index 2f35aae..de7dc48 100644 --- a/css/_tools-menu.scss +++ b/css/_tools-menu.scss @@ -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, diff --git a/images/Logs/grid.png b/images/Logs/grid.png new file mode 100644 index 0000000..b13f584 Binary files /dev/null and b/images/Logs/grid.png differ diff --git a/js/Color.js b/js/Color.js new file mode 100644 index 0000000..71827f7 --- /dev/null +++ b/js/Color.js @@ -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]); + } +} \ No newline at end of file diff --git a/js/ColorModule.js b/js/ColorModule.js new file mode 100644 index 0000000..9b90237 --- /dev/null +++ b/js/ColorModule.js @@ -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 { + //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 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= 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 { + 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 -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 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 + } +})(); + diff --git a/js/Dialogue.js b/js/Dialogue.js new file mode 100644 index 0000000..7ace4b8 --- /dev/null +++ b/js/Dialogue.js @@ -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); \ No newline at end of file diff --git a/js/EditorState.js b/js/EditorState.js new file mode 100644 index 0000000..56f16db --- /dev/null +++ b/js/EditorState.js @@ -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 + } +})(); \ No newline at end of file diff --git a/js/Events.js b/js/Events.js new file mode 100644 index 0000000..780e44e --- /dev/null +++ b/js/Events.js @@ -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= 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 { + + // 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 (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 { + + 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 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; + }; + } +} \ No newline at end of file diff --git a/js/Input.js b/js/Input.js new file mode 100644 index 0000000..55395b9 --- /dev/null +++ b/js/Input.js @@ -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 + } +})(); \ No newline at end of file diff --git a/js/LayerList.js b/js/LayerList.js new file mode 100644 index 0000000..9c52c47 --- /dev/null +++ b/js/LayerList.js @@ -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 newIndex) + { + for (let i=newIndex; ioldIndex; 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=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 (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 { + // 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 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 0 ? -5 : 5; + currentSquareSize += amount; + + for (let i=0; i { 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); - }); }); diff --git a/js/Settings.js b/js/Settings.js new file mode 100644 index 0000000..bb9e63d --- /dev/null +++ b/js/Settings.js @@ -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 + } + +})(); \ No newline at end of file diff --git a/js/SplashPage.js b/js/SplashPage.js new file mode 100644 index 0000000..e1737ba --- /dev/null +++ b/js/SplashPage.js @@ -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 { + + } +})(); + + + + + + diff --git a/js/Startup.js b/js/Startup.js new file mode 100644 index 0000000..999bb0a --- /dev/null +++ b/js/Startup.js @@ -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 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 1) { + this.currSize--; + this.updateCursor(); + } + } + + get size() { + return this.currSize; + } +} \ No newline at end of file diff --git a/js/ToolManager.js b/js/ToolManager.js new file mode 100644 index 0000000..b5eaa8b --- /dev/null +++ b/js/ToolManager.js @@ -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 + } +})(); \ No newline at end of file diff --git a/js/TopMenuModule.js b/js/TopMenuModule.js new file mode 100644 index 0000000..885ab38 --- /dev/null +++ b/js/TopMenuModule.js @@ -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 + } +})(); \ No newline at end of file diff --git a/js/Util.js b/js/Util.js index 760ca03..78d6cd9 100644 --- a/js/Util.js +++ b/js/Util.js @@ -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'); } } \ No newline at end of file diff --git a/js/_addColor.js b/js/_addColor.js deleted file mode 100644 index 943bccd..0000000 --- a/js/_addColor.js +++ /dev/null @@ -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 -}); \ No newline at end of file diff --git a/js/_addColorButton.js b/js/_addColorButton.js deleted file mode 100644 index 17871db..0000000 --- a/js/_addColorButton.js +++ /dev/null @@ -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); diff --git a/js/_algorithms.js b/js/_algorithms.js deleted file mode 100644 index 7e94fc8..0000000 --- a/js/_algorithms.js +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/js/_changeZoom.js b/js/_changeZoom.js deleted file mode 100644 index 8ff9759..0000000 --- a/js/_changeZoom.js +++ /dev/null @@ -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(); -} diff --git a/js/_checkCompatibility.js b/js/_checkCompatibility.js deleted file mode 100644 index 9edfb11..0000000 --- a/js/_checkCompatibility.js +++ /dev/null @@ -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!'); diff --git a/js/_checkerboard.js b/js/_checkerboard.js deleted file mode 100644 index 28b15c8..0000000 --- a/js/_checkerboard.js +++ /dev/null @@ -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 -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 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); - } -} \ No newline at end of file diff --git a/js/_consts.js b/js/_consts.js deleted file mode 100644 index de4fe38..0000000 --- a/js/_consts.js +++ /dev/null @@ -1,2 +0,0 @@ -const MIN_Z_INDEX = -5000; -const MAX_Z_INDEX = 5000; \ No newline at end of file diff --git a/js/_copyPaste.js b/js/_copyPaste.js deleted file mode 100644 index e97215f..0000000 --- a/js/_copyPaste.js +++ /dev/null @@ -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); - } -} \ No newline at end of file diff --git a/js/_createButton.js b/js/_createButton.js deleted file mode 100644 index bda39d5..0000000 --- a/js/_createButton.js +++ /dev/null @@ -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...'); -}); diff --git a/js/_createColorPalette.js b/js/_createColorPalette.js deleted file mode 100644 index c357088..0000000 --- a/js/_createColorPalette.js +++ /dev/null @@ -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= 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); -} \ No newline at end of file diff --git a/js/_deleteColor.js b/js/_deleteColor.js deleted file mode 100644 index 7656d30..0000000 --- a/js/_deleteColor.js +++ /dev/null @@ -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); - - - -} diff --git a/js/_dialogue.js b/js/_dialogue.js deleted file mode 100644 index 8157040..0000000 --- a/js/_dialogue.js +++ /dev/null @@ -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(); - }); -} diff --git a/js/_drawLine.js b/js/_drawLine.js deleted file mode 100644 index ab2f392..0000000 --- a/js/_drawLine.js +++ /dev/null @@ -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; - } - } -} \ No newline at end of file diff --git a/js/_editorMode.js b/js/_editorMode.js deleted file mode 100644 index 9d98cf2..0000000 --- a/js/_editorMode.js +++ /dev/null @@ -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(); -}); diff --git a/js/_featuresLog.js b/js/_featuresLog.js deleted file mode 100644 index 5fd6b89..0000000 --- a/js/_featuresLog.js +++ /dev/null @@ -1 +0,0 @@ -showDialogue("splash", false); \ No newline at end of file diff --git a/js/_fileMenu.js b/js/_fileMenu.js deleted file mode 100644 index 5ce18ed..0000000 --- a/js/_fileMenu.js +++ /dev/null @@ -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= 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') -} diff --git a/js/_getCursorPosition.js b/js/_getCursorPosition.js deleted file mode 100644 index 6aaed4e..0000000 --- a/js/_getCursorPosition.js +++ /dev/null @@ -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)]; -} \ No newline at end of file diff --git a/js/_history.js b/js/_history.js deleted file mode 100644 index ffc5fb6..0000000 --- a/js/_history.js +++ /dev/null @@ -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 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); -} diff --git a/js/_hotkeyListener.js b/js/_hotkeyListener.js deleted file mode 100644 index ecb74b8..0000000 --- a/js/_hotkeyListener.js +++ /dev/null @@ -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; - -}); diff --git a/js/_initColor.js b/js/_initColor.js deleted file mode 100644 index c1bcf60..0000000 --- a/js/_initColor.js +++ /dev/null @@ -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, - }); - -} diff --git a/js/_layer.js b/js/_layer.js deleted file mode 100644 index d18a8e8..0000000 --- a/js/_layer.js +++ /dev/null @@ -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 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 (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=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 newIndex) - { - for (let i=newIndex; ioldIndex; 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 -}); \ No newline at end of file diff --git a/js/_line.js b/js/_line.js deleted file mode 100644 index aa9f828..0000000 --- a/js/_line.js +++ /dev/null @@ -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; - } - } -} \ No newline at end of file diff --git a/js/_loadImage.js b/js/_loadImage.js deleted file mode 100644 index bbcc258..0000000 --- a/js/_loadImage.js +++ /dev/null @@ -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; i127) 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; i127) 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 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 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); - }); - } -}; \ No newline at end of file diff --git a/js/_onbeforeunload.js b/js/_onbeforeunload.js deleted file mode 100644 index ab4b920..0000000 --- a/js/_onbeforeunload.js +++ /dev/null @@ -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; -}; diff --git a/js/_paletteBlock.js b/js/_paletteBlock.js deleted file mode 100644 index 188db9f..0000000 --- a/js/_paletteBlock.js +++ /dev/null @@ -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 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 0 ? -5 : 5; - currentSquareSize += amount; - - for (let i=0; i