diff --git a/css/_canvas.scss b/css/_canvas.scss index 1b8d602..6e3eee1 100644 --- a/css/_canvas.scss +++ b/css/_canvas.scss @@ -24,12 +24,12 @@ height: 400px; position: fixed; display: none; - box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.64); background-color: transparent; } #checkerboard { z-index: 1; + box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.64); } #pixel-canvas { diff --git a/hbs_parser.js b/hbs_parser.js new file mode 100644 index 0000000..c70e81f --- /dev/null +++ b/hbs_parser.js @@ -0,0 +1,52 @@ +const fs = require('fs'); +const path = require('path'); +const HANDLEBARS = require('handlebars'); +const sass = require('sass'); + +const result = sass.compile('./css/pixel-editor.scss'); +fs.writeFileSync("./css/pixel-editor.css",result.css); + +// fs.readFile('/css/pixel-editor.scss', function(err, scssFile) { +// compiler.compile(scssFile.toString(), function(err, css) { +// }); +// }); + +const hbsArr = [ + ...fs.readdirSync("./views/").filter(n=>n.slice(-3)==="hbs").map(n=>"./views/" + n), + ...fs.readdirSync("./views/components/").filter(n=>n.slice(-3)==="hbs").map(n=>"./views/components/" + n), + ...fs.readdirSync("./views/logs/").filter(n=>n.slice(-3)==="hbs").map(n=>"./views/logs/" + n), + ...fs.readdirSync("./views/popups/").filter(n=>n.slice(-3)==="hbs").map(n=>"./views/popups/" + n), +]; +const HBS_STR_MAP = {}; +const HBS_SPEC_MAP = {}; +const HBS_TEMPLATE_MAP = {}; +const HBS_META_DATA = hbsArr.reduce((r,filePath,i)=>{ + + const fileStr = fs.readFileSync(filePath,"utf8"); + const sp0 = fileStr.split("{{> ").slice(1); + const partialArr = sp0.map(n=>n.split("}}")[0]); + const sp1 = fileStr.split("{{").slice(1); + + if(sp0.length || sp1.length) { + const dblCurlsArr = sp1.map(n=>n.split("}}")[0]); + + HBS_STR_MAP[filePath] = fileStr; + + HBS_SPEC_MAP[filePath] = HANDLEBARS.precompile(fileStr); + // HBS_TEMPLATE_MAP[filePath] = HANDLEBARS.template(HBS_SPEC_MAP[filePath]); + + r[filePath] = { + fileStr, + filePath, + dblCurlsArr, + partialArr + }; + } + + return r; +},{}); + +fs.writeFileSync( + "./js/HBS_META_DATA.js", + "const HBS_META_DATA = " + JSON.stringify(HBS_META_DATA,null,4) +); \ No newline at end of file diff --git a/images/icons_14x14.png b/images/icons_14x14.png new file mode 100644 index 0000000..25c3b15 Binary files /dev/null and b/images/icons_14x14.png differ diff --git a/images/le_button_18x19.png b/images/le_button_18x19.png new file mode 100644 index 0000000..05f34bc Binary files /dev/null and b/images/le_button_18x19.png differ diff --git a/images/lospec_mock1.png b/images/lospec_mock1.png new file mode 100644 index 0000000..e37efd2 Binary files /dev/null and b/images/lospec_mock1.png differ diff --git a/images/lospec_mock2.png b/images/lospec_mock2.png new file mode 100644 index 0000000..bb3a023 Binary files /dev/null and b/images/lospec_mock2.png differ diff --git a/images/rotate_test_8x8_x16.png b/images/rotate_test_8x8_x16.png new file mode 100644 index 0000000..550777b Binary files /dev/null and b/images/rotate_test_8x8_x16.png differ diff --git a/images/sked_tree_32x32.png b/images/sked_tree_32x32.png new file mode 100644 index 0000000..3bf4582 Binary files /dev/null and b/images/sked_tree_32x32.png differ diff --git a/images/sked_x1.png b/images/sked_x1.png new file mode 100644 index 0000000..d77eacf Binary files /dev/null and b/images/sked_x1.png differ diff --git a/images/sked_x10.png b/images/sked_x10.png new file mode 100644 index 0000000..2e9cb88 Binary files /dev/null and b/images/sked_x10.png differ diff --git a/images/test_8x8.png b/images/test_8x8.png new file mode 100644 index 0000000..6b6f319 Binary files /dev/null and b/images/test_8x8.png differ diff --git a/images/wang_tilesets_32x32.png b/images/wang_tilesets_32x32.png new file mode 100644 index 0000000..c7e0d4f Binary files /dev/null and b/images/wang_tilesets_32x32.png differ diff --git a/js/Color.js b/js/Color.js index 71827f7..7f09f85 100644 --- a/js/Color.js +++ b/js/Color.js @@ -30,7 +30,7 @@ class Color { this.hsv = Color.rgbToHsv(this.rgb); break; default: - console.error("Unsupported color mode " + fmt); + //console.error("Unsupported color mode " + fmt); break; } } diff --git a/js/ColorModule.js b/js/ColorModule.js index 9b90237..9087397 100644 --- a/js/ColorModule.js +++ b/js/ColorModule.js @@ -57,7 +57,7 @@ const ColorModule = (() => { 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); + //////console.log('%cColor is duplicate', colorCheckingStyle); //show the duplicate color warning duplicateColorWarning.style.visibility = 'visible'; @@ -251,7 +251,7 @@ const ColorModule = (() => { //loop through colors for (var i = 0; i < colors.length; i++) { - //console.log(color,'=',colors[i].jscolor.toString()); + //////console.log(color,'=',colors[i].jscolor.toString()); if (color == colors[i].jscolor.toString()) { //set color to the color button @@ -346,7 +346,7 @@ const ColorModule = (() => { * @param {*} paletteColors The colours of the palette */ function createColorPalette(paletteColors) { - console.log("creating palette"); + ////console.log("creating palette"); //remove current palette while (colorsMenu.childElementCount > 1) colorsMenu.children[0].remove(); @@ -358,7 +358,7 @@ const ColorModule = (() => { for (var i = 0; i < paletteColors.length; i++) { var newColor = new Color("hex", paletteColors[i]); var newColorElement = ColorModule.addColor(newColor.hex); - + ////console.log('newColor.hex === ',newColor.hex); var newColRgb = newColor.rgb; var lightestColorRgb = lightestColor.rgb; @@ -382,6 +382,8 @@ const ColorModule = (() => { //set as current color updateCurrentColor(darkestColor.hex); + + ////console.log('getCurrentPalette() === ',getCurrentPalette()); } /** Creates the palette with the colours used in all the layers @@ -421,7 +423,7 @@ const ColorModule = (() => { //create palette from colors array createColorPalette(colorPaletteArray); - console.log("Done 2"); + ////console.log("Done 2"); } function updateCurrentColor(color, refLayer) { diff --git a/js/ColorPicker.js b/js/ColorPicker.js index 5f971a1..0f47dda 100644 --- a/js/ColorPicker.js +++ b/js/ColorPicker.js @@ -696,7 +696,7 @@ const ColorPicker = (() => { } break; default: - console.log("How did you select the " + currentPickingMode + ", hackerman?"); + ////console.log("How did you select the " + currentPickingMode + ", hackerman?"); break; } diff --git a/js/Dialogue.js b/js/Dialogue.js index 7ace4b8..b5d7061 100644 --- a/js/Dialogue.js +++ b/js/Dialogue.js @@ -5,7 +5,7 @@ const Dialogue = (() => { let currentOpenDialogue = ""; let dialogueOpen = true; - const popUpContainer = document.getElementById("pop-up-container"); + const popUpContainer = document.getElementById("pop-up-container") ?? document.createElement("div"); const cancelButtons = popUpContainer.getElementsByClassName('close-button'); Events.onCustom("esc-pressed", closeDialogue); @@ -31,6 +31,9 @@ const Dialogue = (() => { * @param {*} trackEvent Should I track the GA event? */ function showDialogue (dialogueName, trackEvent) { + + + if (typeof trackEvent === 'undefined') trackEvent = true; // Updating currently open dialogue @@ -83,4 +86,4 @@ const Dialogue = (() => { } })(); -console.log("Dialog: " + Dialogue); \ No newline at end of file +////console.log("Dialog: " + Dialogue); \ No newline at end of file diff --git a/js/EditorState.js b/js/EditorState.js index 56f16db..95d712f 100644 --- a/js/EditorState.js +++ b/js/EditorState.js @@ -9,8 +9,10 @@ const EditorState = (() => { 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?')) { + function switchMode(newMode, skipConfirm = false) { + ////console.trace(); + const switchText = 'Switching to basic mode will flatten all the visible layers. Are you sure you want to continue?'; + if (!firstFile && newMode == "Basic" && !skipConfirm && !confirm(switchText)) { return; } //switch to advanced mode diff --git a/js/Events.js b/js/Events.js index 780e44e..b4058f8 100644 --- a/js/Events.js +++ b/js/Events.js @@ -100,8 +100,9 @@ const Events = (() => { */ function on(event, elementId, functionCallback, ...args) { //if element provided is string, get the actual element + if(!elementId)return; const element = Util.getElement(elementId); - + if(!element)return; element.addEventListener(event, function (e) { functionCallback(...args, e); diff --git a/js/File.js b/js/File.js index b20a652..28be431 100644 --- a/js/File.js +++ b/js/File.js @@ -2,11 +2,12 @@ class File { // Canvas, canvas state canvasSize = []; zoom = 7; - canvasView = document.getElementById("canvas-view"); + canvasView = document.getElementById("canvas-view") ?? document.createElement("canvas"); inited = false; // Layers layers = []; + sublayers = []; currentLayer = undefined; VFXLayer = undefined; TMPLayer = undefined; @@ -129,11 +130,9 @@ class File { // Save all imageDatas for (let i=0; i { // Binding the browse holder change event to file loading @@ -6,6 +7,7 @@ const FileManager = (() => { Events.on('change', browseHolder, loadFile); Events.on('change', browsePaletteHolder, loadPalette); + Events.on("click", "save-project-confirm", saveProject); function openSaveProjectWindow() { //create name @@ -20,7 +22,6 @@ const FileManager = (() => { } Util.setValue('lpe-file-name', fileName); - Events.on("click", "save-project-confirm", saveProject); Dialogue.showDialogue('save-project', false); } @@ -42,6 +43,7 @@ const FileManager = (() => { function saveProject() { // Get name + // debugger; let fileName = Util.getValue("lpe-file-name") + ".lpe"; let selectedPalette = Util.getText('palette-button'); //set download link @@ -55,6 +57,9 @@ const FileManager = (() => { if (typeof ga !== 'undefined') ga('send', 'event', 'Pixel Editor Save', selectedPalette, currFile.canvasSize[0]+'/'+currFile.canvasSize[1]); /*global ga*/ + + + LayerList.closeOptionsMenu(); // is this the right place for this? } function exportProject() { @@ -117,7 +122,39 @@ const FileManager = (() => { //open file selection dialog document.getElementById('open-image-browse-holder').click(); } - + function localStorageCheck() { + return !!localStorage.getItem("lpe-cache"); + } + function localStorageSave() { + const lpeStr = getProjectData(); + const lpe = JSON.parse(lpeStr); + //console.log('LPE saved === ',lpe); + if(lpe.colors.length < 1)lpe.colors.push("#000000"); + if(!lpe.canvasWidth)lpe.canvasWidth = 16; + if(!lpe.canvasHeight)lpe.canvasHeight = 16; + localStorage.setItem("lpe-cache", JSON.stringify(lpe)); + } + function localStorageReset() { + localStorage.setItem("lpe-cache", JSON.stringify({ + "canvasWidth":16, + "canvasHeight":16, + "editorMode":"Advanced", + "colors":["#000000","#0b6082","#1d8425","#cc1919"], + "selectedLayer":0, + "layers":[ + {"canvas":{},"context":{"mozImageSmoothingEnabled":false},"isSelected":true,"isVisible":true,"isLocked":false,"oldLayerName":null,"menuEntry":{},"id":"layer0","name":"Layer 0","src":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAHZJREFUOE9jZKAQMFKon4EuBvxHcyWKpdhcgK4BpB+nS9ElYJqJ9hqyQpI1ozsNZABRNnMnNIEt+7qgjhGrBpgCWOCBFKJHN0gNTgOQFSPbhi5OlAHYEhpBL+DThO4tgoGGHB7YwgKvAbj8j+xCgi4glNkoNgAA3JApEbHObDkAAAAASUVORK5CYII="}, + {"canvas":{},"context":{"mozImageSmoothingEnabled":false},"isSelected":false,"isVisible":true,"isLocked":false,"oldLayerName":null,"menuEntry":{},"id":"layer1","name":"Layer 1","src":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAGNJREFUOE9jZKAQMCLrl21R/Q/iP665jSKOzw7qGgCyCeQKsl0AM4AUb2D1KymuoJ0BxHoDZ3QRG6V445uYsCBoACGvEExxhFxBlAH4XEHQAEKpkygDkFMoumuINgCWI9HDBAChJjwRzAXQUwAAAABJRU5ErkJggg=="}, + {"canvas":{},"context":{"mozImageSmoothingEnabled":false},"isSelected":false,"isVisible":true,"isLocked":false,"oldLayerName":null,"menuEntry":{},"id":"layer2","name":"Layer 2","src":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAGNJREFUOE9jZKAQMJKi/4yk5H+YepPnz8F68RqArAFdI4yP1QBsNuFyKYYBMM0wJxLyIlYDiNWMNQxALhimBuCKUoKBSChKcRpASCPOhESsRoIGEBuVKF4g1XaMhERqMgYZAACIaEgR0hnFxgAAAABJRU5ErkJggg=="} + ] + })); + } + function localStorageLoad() { + ////console.log("loading from localStorage"); + ////console.log('JSON.parse(localStorage.getItem("lpe-cache") ?? "{}") === ',JSON.parse(localStorage.getItem("lpe-cache") ?? "{}")); + const lpe = JSON.parse(localStorage.getItem("lpe-cache") ?? "{}"); + //console.log('LPE loaded === ',lpe); + return lpe; + } function loadFile() { let fileName = document.getElementById("open-image-browse-holder").value; // Getting the extension @@ -139,7 +176,6 @@ const FileManager = (() => { browseHolder.value = null; } - function openFile() { //load file var fileReader = new FileReader(); @@ -165,59 +201,88 @@ const FileManager = (() => { }; fileReader.readAsDataURL(browseHolder.files[0]); } - - function openProject() { - let file = browseHolder.files[0]; - let reader = new FileReader(); - + function openProject(lpeData) { + // Getting all the data - reader.readAsText(file, "UTF-8"); - // Converting the data to a json object and creating a new pixel (see _newPixel.js for more) - reader.onload = function (e) { - let dictionary = JSON.parse(e.target.result); - Startup.newPixel(dictionary['canvasWidth'], dictionary['canvasHeight'], dictionary); - - for (let i=0; dictionary['color' + i] != null; i++) { - ColorModule.addColor(dictionary['color'+i]); + if(lpeData){ + _parseLPE(lpeData); + } else { + let file = uri ?? browseHolder.files[0]; + let reader = new FileReader(); + 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); + _parseLPE(dictionary); } - - // Removing the default colours - ColorModule.deleteColor(ColorModule.getCurrentPalette()[0]); - ColorModule.deleteColor(ColorModule.getCurrentPalette()[0]); + } + + function _parseLPE(dictionary) { + Startup.newPixel(dictionary['canvasWidth'], dictionary['canvasHeight'], dictionary, true); } } + function loadFromLPE(dictionary) { + ColorModule.resetPalette(); + //console.log('dictionary === ',dictionary); + + dictionary = FileManager.upgradeLPE(dictionary); + + EditorState.switchMode(dictionary.editorMode ?? 'Advanced'); + + // I add every layer the file had in it + // dictionary.layers.forEach((layerData,i)=>{ + // let layerImage = layerData.src; + // if (layerData != null) { + // // Setting id + // let createdLayer = LayerList.addLayer(layerData.id, false, layerData.name); + // // Setting name + // createdLayer.menuEntry.getElementsByTagName("p")[0].innerHTML = layerData.name; + + // // Adding the image (I can do that because they're sorted by increasing z-index) + // let img = new Image(); + // img.onload = function() { + // createdLayer.context.drawImage(img, 0, 0); + // createdLayer.updateLayerPreview(); + // }; + + // img.src = layerImage; + + // // Setting visibility and lock options + // if (!layerData.isVisible) { + // createdLayer.hide(); + // } + // if (layerData.isLocked) { + // createdLayer.lock(); + // } + // } + // }) + if(dictionary.colors)ColorModule.createColorPalette(dictionary.colors); + } function getProjectData() { // use a dictionary let dictionary = {}; // sorting layers by increasing z-index - let layersCopy = currFile.layers.slice(); - layersCopy.sort((a, b) => (a.canvas.style.zIndex > b.canvas.style.zIndex) ? 1 : -1); - // save canvas size + let layersCopy = currFile.layers.filter(n=>!!n.menuEntry).slice(); + // layersCopy.sort((a, b) => (a.canvas.style.zIndex > b.canvas.style.zIndex) ? 1 : -1); dictionary['canvasWidth'] = currFile.canvasSize[0]; dictionary['canvasHeight'] = currFile.canvasSize[1]; - // save editor mode dictionary['editorMode'] = EditorState.getCurrentMode(); - // save palette - for (let i=0; i{ + //console.log('n.name === ',n.name); + if(n.isSelected)dictionary.selectedLayer = i; + return { + ...n, + src: n.canvas.toDataURL(), + }; + }); return JSON.stringify(dictionary); } - function loadPalette() { if (browsePaletteHolder.files && browsePaletteHolder.files[0]) { //make sure file is allowed filetype @@ -268,9 +333,45 @@ const FileManager = (() => { browsePaletteHolder.value = null; } + function upgradeLPE(dictionary) { + ////console.log('dictionary === ',dictionary); + if(dictionary.color0 && !dictionary.colors) { + dictionary.colors = []; + let colorIdx = 0; + while(dictionary[`color${colorIdx}`]) { + dictionary.colors.push(dictionary[`color${colorIdx}`]); + delete dictionary[`color${colorIdx}`]; + colorIdx++; + } + dictionary.layers = Object.keys(dictionary).reduce((r,k,i)=>{ + if(k.slice(0,5) === "layer"){ + if(dictionary[k].isSelected){ + dictionary.selectedLayer = r.length; + } + r.push({ + ...dictionary[k], + src: dictionary[`${k}ImageData`] + }); + + delete dictionary[k]; + delete dictionary[`${k}ImageData`]; + } + return r; + },[]); + } + return dictionary; + } return { + loadFromLPE, + getProjectData, + localStorageReset, + localStorageCheck, + localStorageSave, + localStorageLoad, + upgradeLPE, saveProject, + openProject, exportProject, openPixelExportWindow, openSaveProjectWindow, diff --git a/js/History.js b/js/History.js index f619b3e..1d54ec3 100644 --- a/js/History.js +++ b/js/History.js @@ -38,12 +38,12 @@ const History = (() => { } function undo () { - console.log("undoing"); + ////console.log("undoing"); undoOrRedo('undo'); } function redo () { - console.log("redoing"); + ////console.log("redoing"); undoOrRedo('redo'); } @@ -186,13 +186,13 @@ class HistoryState { this.nFlattened = nFlattened; this.undo = function() { - for (let i=0; i this.index + 1) { + if (currFile.layers.length > this.index + 1) { currFile.layers[this.index + 1].selectLayer(); } else { @@ -412,7 +416,7 @@ class HistoryState { //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()); + //////console.log(newColorValue, '==', colors[i].jscolor.toString()); if (newColorValue == colors[i].jscolor.toString()) { colors[i].jscolor.fromString(oldColorValue); break; @@ -429,7 +433,7 @@ class HistoryState { //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()); + //////console.log(oldColorValue, '==', colors[i].jscolor.toString()); if (oldColorValue == colors[i].jscolor.toString()) { colors[i].jscolor.fromString(newColorValue); break; diff --git a/js/Input.js b/js/Input.js index 8450ce7..954a766 100644 --- a/js/Input.js +++ b/js/Input.js @@ -163,7 +163,7 @@ const Input = (() => { spacePressed = true; break; case 46: - console.log("Pressed del"); + ////console.log("Pressed del"); Events.emit("del"); break; } diff --git a/js/LayerList.js b/js/LayerList.js index 9c52c47..2f47a2d 100644 --- a/js/LayerList.js +++ b/js/LayerList.js @@ -1,46 +1,36 @@ const LayerList = (() => { - let layerList = document.getElementById("layers-menu"); - let layerListEntry = layerList.firstElementChild; + // let layerListEntry = layerList.firstElementChild; + let layerListEntry = document.getElementById("default-layer-list-item"); 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); + Events.onCustom("switchedToAdvanced", showLayerList); + Events.onCustom("switchedToBasic", hideLayerList); - // Making the layers list sortable new Sortable(layerList, { animation: 100, filter: ".layer-button", draggable: ".layers-menu-entry", onStart: layerDragStart, - onEnd: layerDragDrop + onEnd: layerDragEnd }); - - function showMenu() { + function showLayerList() { layerList.style.display = "inline-block"; document.getElementById('layer-button').style.display = 'inline-block'; } - function hideMenu() { + function hideLayerList() { 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, layerName) { - function addLayer(id, saveHistory = true) { - // layers.length - 3 - let index = currFile.layers.length - 3; + let index = currFile.layers.length; // Creating a new canvas let newCanvas = document.createElement("canvas"); // Setting up the new canvas @@ -49,12 +39,16 @@ const LayerList = (() => { newCanvas.style.zIndex = Layer.maxZIndex; newCanvas.classList.add("drawingCanvas"); - if (!layerListEntry) return console.warn('skipping adding layer because no document'); + if (!layerListEntry) return //console.warn('skipping adding layer because no document'); // Clone the default layer let toAppend = layerListEntry.cloneNode(true); + toAppend.style.display = "flex"; + //console.log('toAppend === ',toAppend); // Setting the default name for the layer - toAppend.getElementsByTagName('p')[0].innerHTML = "Layer " + Layer.layerCount; + const _layerName = layerName ?? "Layer " + currFile.layers.length; + //console.log('_layerName === ',_layerName); + toAppend.getElementsByTagName('p')[0].innerHTML = _layerName; // Removing the selected class toAppend.classList.remove("selected-layer"); // Adding the layer to the list @@ -65,10 +59,11 @@ const LayerList = (() => { newLayer.context.fillStyle = currFile.currentLayer.context.fillStyle; newLayer.copyData(currFile.currentLayer); - currFile.layers.splice(index, 0, newLayer); + // currFile.layers.splice(index, 0, newLayer); + currFile.layers.push(newLayer); // Insert it before the Add layer button - layerList.insertBefore(toAppend, layerList.childNodes[0]); + layerList.insertBefore(toAppend, document.getElementById("add-layer-li")); if (id != null && typeof(id) == "string") { newLayer.setID(id); @@ -76,11 +71,11 @@ const LayerList = (() => { // 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); + FileManager.localStorageSave(); } return newLayer; } - /** Merges topLayer onto belowLayer * * @param {*} belowLayer The layer on the bottom of the layer stack @@ -97,7 +92,7 @@ const LayerList = (() => { toMergeImageData.data[i+2], toMergeImageData.data[i+3] ]; - let currentUnderlyingPixel = [ + let currentUnderlyingPixel = [ //TODO: I'd be curious to know if this array slows this function down belowImageData.data[i], belowImageData.data[i+1], belowImageData.data[i+2], belowImageData.data[i+3] ]; @@ -115,34 +110,54 @@ const LayerList = (() => { // Putting the top data into the belowdata belowLayer.putImageData(toMergeImageData, 0, 0); } - /** Sets the z indexes of the layers when the user drops the layer in the menu * * @param {*} event */ - function layerDragDrop(event) { - let oldIndex = event.oldDraggableIndex; - let newIndex = event.newDraggableIndex; + function layerDragEnd(event) { + // let oldIndex = event.oldDraggableIndex; + // let newIndex = event.newDraggableIndex; - let movedZIndex = dragStartLayer.canvas.style.zIndex; + // let movedZIndex = dragStartLayer.canvas.style.zIndex; - if (oldIndex > newIndex) - { - for (let i=newIndex; ioldIndex; i--) { - getLayerByID(layerList.children[i].id).canvas.style.zIndex = getLayerByID(layerList.children[i - 1].id).canvas.style.zIndex; - } - } + // if (oldIndex > 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; + // getLayerByID(layerList.children[oldIndex].id).canvas.style.zIndex = movedZIndex; Events.simulateMouseEvent(window, "mouseup"); - } + + const tempLayerCache = currFile.layers.reduce((r,n,i) => { + r[n.id] = n; + return r; + },{}); + + let selectedId; + const idArr = [...document.querySelectorAll(".layers-menu-entry")].map(elm => { + if([...elm.classList].includes("selected-layer")) { + selectedId = elm.id; + } + return elm.id; + }); + let selectedIdx = idArr.indexOf(selectedId); + idArr.forEach((id,i)=>{ + currFile.layers[i] = tempLayerCache[id]; + currFile.layers[i].isSelected = i===selectedIdx; + }); + + FileManager.localStorageSave(); + + } /** Saves the layer that is being moved when the dragging starts * * @param {*} event @@ -150,21 +165,27 @@ const LayerList = (() => { function layerDragStart(event) { dragStartLayer = getLayerByID(layerList.children[event.oldIndex].id); } - - // Finds a layer given its id function getLayerByID(id) { + //console.log(`getLayerByID(${id})`); + // for (let i=0; i { return null; } - function startRenamingLayer(event) { let p = currFile.currentLayer.menuEntry.getElementsByTagName("p")[0]; @@ -189,7 +209,6 @@ const LayerList = (() => { renamingLayer = true; } - function duplicateLayer(event, saveHistory = true) { function getMenuEntryIndex(list, entry) { for (let i=0; i { 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'); + if (!layerListEntry) return //console.warn('skipping adding layer because no document'); // Clone the default layer let toAppend = currFile.currentLayer.menuEntry.cloneNode(true); @@ -248,41 +267,52 @@ const LayerList = (() => { new HistoryState().DuplicateLayer(newLayer, currFile.currentLayer); } } + function clearLayers() { + //console.log('currFile.layers === ',currFile.layers); - 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); - } + currFile.layers.forEach(()=>deleteLayer()); + //console.log('currFile.layers.length === ',currFile.layers.length); + for(let i = 0; i < currFile.layers.length;i++){ + const layer = currFile.layers[i]; + //console.log('i === ', i); + //console.log('layer === ',layer); } + } + function deleteLayer(saveHistory = true) { + //console.log('deleting layer: ', currFile.currentLayer.name, currFile.currentLayer); + //console.trace(); + deleteLayerDirectly(currFile.currentLayer, saveHistory); // Closing the menu closeOptionsMenu(); } + function deleteLayerDirectly(layer, saveHistory = true) { + let layerIndex = currFile.layers.indexOf(layer); + let toDelete = currFile.layers[layerIndex]; + let previousSibling = toDelete.menuEntry.previousElementSibling; + // Adding the ids to the unused ones + Layer.unusedIDs.push(toDelete.id); + if(layer.isSelected) { + // Selecting the nearest layer + const nearestLayer = (currFile.layers[layerIndex + 1] ?? currFile.layers[layerIndex - 1]); + if(nearestLayer){ + nearestLayer.selectLayer(); + //console.log('changing to nearest layer'); + } + } + + // 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); + } + } function merge(saveHistory = true) { // Saving the layer that should be merged let toMerge = currFile.currentLayer; @@ -312,7 +342,6 @@ const LayerList = (() => { currFile.currentLayer.updateLayerPreview(); } } - function flatten(onlyVisible) { if (!onlyVisible) { // Selecting the first layer @@ -366,7 +395,6 @@ const LayerList = (() => { currFile.currentLayer.updateLayerPreview(); } } - function openOptionsMenu(event) { if (event.which == 3) { let selectedId; @@ -385,21 +413,17 @@ const LayerList = (() => { getLayerByID(selectedId).selectLayer(false); } } - function closeOptionsMenu(event) { Layer.layerOptions.style.visibility = "hidden"; currFile.currentLayer.rename(); renamingLayer = false; } - function getLayerListEntries() { return layerList; } - function isRenamingLayer() { return renamingLayer; } - return { addLayer, mergeLayers, @@ -407,7 +431,9 @@ const LayerList = (() => { getLayerByName, renameLayer: startRenamingLayer, duplicateLayer, + clearLayers, deleteLayer, + deleteLayerDirectly, merge, flatten, closeOptionsMenu, diff --git a/js/PresetModule.js b/js/PresetModule.js index 0b8d5eb..4e10c16 100644 --- a/js/PresetModule.js +++ b/js/PresetModule.js @@ -6,7 +6,7 @@ const PresetModule = (() => { }; function instrumentPresetMenu() { - console.info("Initializing presets.."); + //console.info("Initializing presets.."); // Add a button for all the presets available const presetsMenu = document.getElementById('preset-menu'); Object.keys(presets).forEach((presetName,) => { @@ -17,7 +17,7 @@ const PresetModule = (() => { presetsMenu.appendChild(button); button.addEventListener('click', () => { - console.log("Preset: " + presetName); + ////console.log("Preset: " + presetName); //change dimentions on new pixel form Util.setValue('size-width', presets[presetName].width); Util.setValue('size-height', presets[presetName].height); diff --git a/js/Settings.js b/js/Settings.js index bb9e63d..62f1aa6 100644 --- a/js/Settings.js +++ b/js/Settings.js @@ -14,7 +14,7 @@ const Settings = (() => { settingsFromCookie = Cookies.get('pixelEditorSettings'); if(!settingsFromCookie) { - console.log('settings cookie not found'); + ////console.log('settings cookie not found'); settings = { switchToChangedColor: true, @@ -27,8 +27,8 @@ const Settings = (() => { }; } else{ - console.log('settings cookie found'); - console.log(settingsFromCookie); + ////console.log('settings cookie found'); + ////console.log(settingsFromCookie); settings = JSON.parse(settingsFromCookie); } diff --git a/js/Startup.js b/js/Startup.js index 999bb0a..ff5f03f 100644 --- a/js/Startup.js +++ b/js/Startup.js @@ -1,6 +1,8 @@ const Startup = (() => { let splashPostfix = ''; + let cacheIntervalIdx; + Events.on('click', 'create-button', create, false); Events.on('click', 'create-button-splash', create, true); @@ -15,7 +17,10 @@ const Startup = (() => { var height = Util.getValue('size-height' + splashPostfix); var selectedPalette = Util.getText('palette-button' + splashPostfix); - newPixel(width, height); + newPixel({ + canvasWidth: width, + canvasHeight: height, + }); resetInput(); //track google event @@ -25,15 +30,16 @@ const Startup = (() => { /** 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 + * @param {*} skipModeConfirm If skipModeConfirm == true, then the mode switching confirmation will be skipped */ - function newPixel (width, height, fileContent = null) { + function newPixel (fileContent = null, skipModeConfirm = false) { + //console.log('called newPixel'); + //console.trace(); // The palette is empty, at the beginning ColorModule.resetPalette(); - initLayers(width, height); + initLayers(fileContent); initPalette(); // Closing the "New Pixel dialogue" @@ -46,64 +52,91 @@ const Startup = (() => { // 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(); + FileManager.loadFromLPE(fileContent); } - - EditorState.switchMode(EditorState.getCurrentMode()); + ////console.log('ColorModule.getCurrentPalette() === ',ColorModule.getCurrentPalette()); + + EditorState.switchMode(EditorState.getCurrentMode(), skipModeConfirm); // This is not the first Pixel anymore EditorState.created(); - } - function initLayers(width, height) { - // Setting the general canvasSize + ////console.log('ColorModule.getCurrentPalette() === ',ColorModule.getCurrentPalette()); + ////console.trace(); + } + function clearLayers() { + for(let i = 0; i < currFile.layers.length;i++) { + currFile.layers[i].delete(i); + } + for(let i = 0; i < currFile.sublayers.length;i++) { + currFile.sublayers[i].delete(i); + } + } + function initLayers(lpe) { + //console.group('called initLayers'); + //console.log('currFile.layers === ',currFile.layers); + + const width = lpe.canvasWidth; + const height = lpe.canvasHeight; + clearLayers(); + + // debugger; + // 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', ""); + if( lpe.layers && lpe.layers.length ) { + currFile.currentLayer = new Layer(width, height, `pixel-canvas`,"","layer-li-template"); 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; + currFile.sublayers.push(currFile.currentLayer); - if (currentEntry != null) { - // Getting the associated layer - associatedLayer = LayerList.getLayerByID(currentEntry.id); + let selectedIdx = lpe.selectedLayer ?? 0; - // 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(); + lpe.layers.forEach((layerData, i) => { + //console.log('lpe.layers[i] === ', i); + let layerImage = layerData.src; + if (layerData != null) { + // Setting id + let createdLayer = LayerList.addLayer(layerData.id, false, layerData.name); + if(i===selectedIdx)createdLayer.selectLayer(); + // Setting name + createdLayer.menuEntry.getElementsByTagName("p")[0].innerHTML = layerData.name; + + // Adding the image (I can do that because they're sorted by increasing z-index) + let img = new Image(); + img.onload = function() { + createdLayer.context.drawImage(img, 0, 0); + createdLayer.updateLayerPreview(); + }; + + img.src = layerImage; + + // Setting visibility and lock options + if (!layerData.isVisible) { + createdLayer.hide(); + } + if (layerData.isLocked) { + createdLayer.lock(); + } } - } + }); - // Removing the old layers from the list - for (let i=2; i { // Tmp layer to draw previews on currFile.TMPLayer = new Layer(width, height, 'tmp-canvas'); - if (EditorState.firstPixel()) { - // Adding the first layer and the checkerboard to the list of layers - currFile.layers.push(currFile.checkerBoard); - currFile.layers.push(currFile.currentLayer); - currFile.layers.push(currFile.TMPLayer); - currFile.layers.push(currFile.pixelGrid); - currFile.layers.push(currFile.VFXLayer); - } + currFile.sublayers.push(currFile.checkerBoard); + currFile.sublayers.push(currFile.TMPLayer); + currFile.sublayers.push(currFile.pixelGrid); + currFile.sublayers.push(currFile.VFXLayer); } function initPalette() { @@ -168,42 +197,6 @@ const Startup = (() => { } } - function loadFromLPE(fileContent) { - // I add every layer the file had in it - for (let i=0; i { currTool.onRightDrag(mousePos, mouseEvent.target); break; default: - console.log("wtf"); + ////console.log("wtf"); break; } } diff --git a/js/TopMenuModule.js b/js/TopMenuModule.js index 3b70d64..1e33237 100644 --- a/js/TopMenuModule.js +++ b/js/TopMenuModule.js @@ -1,7 +1,7 @@ const TopMenuModule = (() => { - const mainMenuItems = document.getElementById('main-menu').children; - let infoList = document.getElementById('editor-info'); + const mainMenuItems = document.getElementById('main-menu')?.children ?? []; + let infoList = document.getElementById('editor-info') ?? document.createElement("div"); let infoElements = {}; initMenu(); @@ -113,7 +113,12 @@ const TopMenuModule = (() => { } function updateField(fieldId, value) { - document.getElementById(fieldId).value = value; + const elm = document.getElementById(fieldId); + if(elm) { + elm.value = value; + } else { + //console.warn('elm === ', elm); + } } function addInfoElement(fieldId, field) { diff --git a/js/Util.js b/js/Util.js index a6ba566..cc0aaf8 100644 --- a/js/Util.js +++ b/js/Util.js @@ -83,7 +83,7 @@ class Util { return document.getElementById(elementOrElementId); } else { - console.log("Type not supported: " + typeof(elementOrElementId)); + ////console.log("Type not supported: " + typeof(elementOrElementId)); } } diff --git a/js/canvas_util.js b/js/canvas_util.js new file mode 100644 index 0000000..95b8cd5 --- /dev/null +++ b/js/canvas_util.js @@ -0,0 +1,211 @@ +function drawTinyNumber(ctx,str,x,y) { + const CHARS = [ + `0111110111011111`, + `0110111001101111`, + `1111001111001111`, + `1111011100111111`, + `1001100111110011`, + `1111110000111111`, + `1100111110111111`, + `1111001101100110`, + `1110101111010111`, + `1111101111110011`, + ] + .map(n=>n.split("").map(Number)) + ; + str.split("").reduce((xo,n)=>{ + let bitArr = CHARS[n]; + let cw = bitArr.length / 4; + bitArr.forEach((bit,i)=>{ + const _x = x + xo + (i%cw); + const _y = y + Math.floor(i/cw); + if(bit)ctx.fillRect(_x,_y,1,1); + }); + xo+=cw+1; + return xo; + },0); +} +function drawTinyText( ctx, str, x = 0, y = 0, font = "Verdana", w = 16, h = 16, xo = 0, yo = 0 ) { + for(let i = 0; i < 4;i++){ + drawTinyTextOne( ctx, str, x, y, font, w, h, xo+0, yo+i ); + } +} +function drawTinyTextOne( ctx, str, x = 0, y = 0, font = "Verdana", w = 16, h = 16, xo = 0, yo = 0 ) { + const CHARS = generateCharsFromFont(font, w, h, 8, 8, undefined, undefined, xo, yo) + .map(n=>n.split("").map(Number)) + ; + ////console.log('CHARS === ',CHARS); + str.split("").reduce((_xo,n)=>{ + const code = n.charCodeAt(0) - 33; + // ////console.log('n,code === ',n,code); + let charWidth = CHARS[code].length / w; + CHARS[code].forEach((bit,i)=>{ + const _x = x + _xo + (i%charWidth); + const _y = y + Math.floor(i/charWidth); + + // ////console.log('bit === ',bit); + if(bit)ctx.fillRect(_x,_y,1,1); + }); + _xo+=charWidth+1; + return _xo; + },0); +} +function generateCharsFromFont(font, charW = 7, charH = 7, sampleScale = 8, scale = 8, previewDiv, debugDiv, xo = 0, yo = 0) { + return [...Array(94)].map((_,i)=>{ + const char = String.fromCharCode(i+33); + const canvas = document.createElement('canvas'); + if(debugDiv)debugDiv.appendChild(canvas); + const sz = sampleScale; + const w = charW * sz; + const h = charH * sz; + canvas.width = w; + canvas.height = h; + const ctx = canvas.getContext('2d'); + ctx.font = `${h}px ` + font; + ctx.shadowColor="black"; + ctx.shadowBlur=sz*2; + ctx.textAlign = "center"; + ctx.textBaseline = "bottom"; + ctx.fillStyle = "black"; + ctx.fillText(char,w/2,h); + ctx.strokeStyle = "black"; + ctx.strokeText(char,w/2,h); + const imageData = ctx.getImageData(0,0,w,h); + // ////console.log('imageData === ',imageData); + let ret = ''; + ctx.fillStyle = "red"; + const previewCanvas = document.createElement('canvas'); + previewCanvas.width = charW; + previewCanvas.height = charH; + const ctx2 = previewCanvas.getContext('2d'); + if(previewDiv)previewDiv.appendChild(previewCanvas); + + for(let y = scale/2; y < h;y+=scale) { + for(let x = scale/2; x < w;x+=scale) { + const _x = (x-(scale/2))/scale; + const _y = (y-(scale/2))/scale; + const _imageData = ctx.getImageData(x+xo,y+yo,1,1); + let specResult = _imageData.data[3] > 128; + ctx2.fillStyle = "black"; + if(specResult) { + ctx2.fillRect(_x,_y,1,1); + ret += "1"; + } else { + ret += "0"; + } + ctx.fillStyle = specResult ? "#00ff00" : "#ff0000"; + ctx.fillRect(x,y,1,1); + } + } + return ret; + }) +} +function pixelButtonMeta(x, y, img, options) { + return Object.entries(options).reduce((r,n,i)=>{ + const [k,v] = n; + + }) +} +function pixelButton(x,y,xo,yo,img,colors=["#112","#334","#556","#778","#99A","#BBC"]) { + const canvas = document.createElement('canvas'); + const w = img.width+4; + const h = img.height+5; + canvas.width = w; + canvas.height = h; + const ctx = canvas.getContext('2d'); + ctx.fillStyle = colors[0]; + ctx.fillRect(x+0+xo,y+yo,img.width+4,img.height+5); + ctx.fillStyle = colors[1]; + ctx.fillRect(x+0+xo,y+yo,img.width+4,img.height+4); + ctx.fillStyle = colors[3]; + ctx.fillRect(x+1+xo,y+yo,img.width+2,img.height+4); + ctx.fillStyle = colors[3]; + ctx.fillRect(x+0+xo,y+yo,img.width+4,img.height+2); + ctx.fillStyle = colors[2]; + ctx.fillRect(x+1+xo,y+yo,img.width+2,img.height+2); + ctx.drawImage(img,x+2,y+2); + return canvas; +} +function scaleImageData(imageData, scale) { + if (scale === 1) return imageData; + var scaledImageData = document.createElement("canvas").getContext("2d").createImageData(imageData.width * scale, imageData.height * scale); + for (var row = 0; row < imageData.height; row++) { + for (var col = 0; col < imageData.width; col++) { + var sourcePixel = [ + imageData.data[(row * imageData.width + col) * 4 + 0], + imageData.data[(row * imageData.width + col) * 4 + 1], + imageData.data[(row * imageData.width + col) * 4 + 2], + imageData.data[(row * imageData.width + col) * 4 + 3] + ]; + for (var y = 0; y < scale; y++) { + var destRow = row * scale + y; + for (var x = 0; x < scale; x++) { + var destCol = col * scale + x; + for (var i = 0; i < 4; i++) { + scaledImageData.data[(destRow * scaledImageData.width + destCol) * 4 + i] = + sourcePixel[i]; + } + } + } + } + } + return scaledImageData; +} + +function imageChopper(img,tileHeight,tileWidth) { + const c = document.createElement('canvas'); + const w = c.width = img.width; + const h = c.height = img.height; + const ctx = c.getContext('2d'); + ctx.drawImage(img, 0, 0); + const arr = []; + for (let y = 0; y < h; y += tileHeight) { + for (let x = 0; x < w; x += tileWidth) { + const imageData = ctx.getImageData(x, y, tileWidth, tileHeight); + const tileCanvas = document.createElement('canvas'); + tileCanvas.width = tileWidth; + tileCanvas.height = tileHeight; + const tileCtx = tileCanvas.getContext('2d'); + tileCtx.putImageData(imageData,0,0); + arr.push(tileCanvas); + } + } + return arr; +} +function imageDataToCanvas(imageData, x = 0, y = 0) { + const canvas = document.createElement('canvas'); + canvas.width = imageData.width; + canvas.height = imageData.height; + const ctx = canvas.getContext('2d'); + ctx.putImageData(imageData, x, y); + return canvas; +} + +function tilesToCanvas(arr,columns,tilesData) { + const canvas = document.createElement('canvas'); + const rows = Math.floor(arr.length / columns); + if(rows !== (arr.length / columns)){ + debugger; + //console.error("wtf this should never happen..."); + } + const w = tilesData[0].width * columns; + const h = tilesData[0].height * rows; + canvas.width = w; + canvas.height = h; + const ctx = canvas.getContext('2d'); + + /* first draw the tiles... */ + arr.forEach((tileIdx,i) => { + if(tileIdx >= 0) { + const c = tilesData[tileIdx]; + const x = i%columns; + const y = Math.floor(i/columns); + document.body.appendChild(c); + ctx.drawImage(c, x * c.width, y * c.width); + } + }); + + /* then draw tile fringe? */ // TODO + + return canvas; +} \ No newline at end of file diff --git a/js/color_utils.js b/js/color_utils.js new file mode 100644 index 0000000..7629115 --- /dev/null +++ b/js/color_utils.js @@ -0,0 +1,385 @@ +// UTILITY + +let firstColor = "#000000", secondColor = "#000000"; +let log = document.getElementById("log"); + +// 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}; + +// COLOUR SIMILARITY + +// Min distance under which 2 colours are considered similar +let distanceThreshold = 10; + +// Threshold used to consider a colour "dark" +let darkColoursThreshold = 50; +// Threshold used to tell if 2 dark colours are similar +let darkColoursSimilarityThreshold = 40; + +// Threshold used to consider a colour "light" +let lightColoursThreshold = 190; +// Threshold used to tell if 2 light colours are similar +let lightColoursSimilarityThreshold = 30; + + +// document.getElementById("color1").addEventListener("change", updateColor); +// document.getElementById("color2").addEventListener("change", updateColor); + +function updateColor(e) { + ////console.log(e); + + switch (e.target.id) { + case "color1": + firstColor = e.target.value; + break; + case "color2": + secondColor = e.target.value; + break; + default: + break; + } + + updateWarnings(); +} + +function updateWarnings() { + let toSet = ""; + ////console.log("colors: " + firstColor + ", " + secondColor); + toSet += similarColours(firstColor, secondColor) ? 'Colours are similar!' + '\n' : ""; + + log.innerHTML = toSet; +} + +/**********************SECTION: COLOUR SIMILARITY*********************************/ + +function similarColours(rgb1, rgb2) { + let ret = differenceCiede2000(rgb1, rgb2); + const lightInRange = lightColoursCheck(rgb1, rgb2); + const darkInRange = darkColoursCheck(rgb1, rgb2); + + // if((ret < distanceThreshold && lightColoursCheck(rgb1, rgb2)) || darkColoursCheck(rgb1, rgb2)){ + // return ret; + // } + // return 100; + if((ret < distanceThreshold && lightInRange) || darkInRange) { + // ////console.log('GOOD ret === ',ret); + return ret; + } else { + // ////console.log('BAD ret === ',ret); + } + return ret; +} + +function lightColoursCheck(c1, c2) { + let rDelta = Math.abs(c1.r - c2.r); + let gDelta = Math.abs(c1.g - c2.g); + let bDelta = Math.abs(c1.b - c2.b); + + // Checking only if the colours are dark enough + if (c1.r > lightColoursThreshold && c1.g > lightColoursThreshold && c1.b > lightColoursThreshold && + c2.r > lightColoursThreshold && c2.g > lightColoursThreshold && c2.b > lightColoursThreshold) { + return rDelta < lightColoursSimilarityThreshold && gDelta < lightColoursSimilarityThreshold && + bDelta < lightColoursSimilarityThreshold; + } + + return true; +} + +function darkColoursCheck(c1, c2) { + let rDelta = Math.abs(c1.r - c2.r); + let gDelta = Math.abs(c1.g - c2.g); + let bDelta = Math.abs(c1.b - c2.b); + + // Checking only if the colours are dark enough + if (c1.r < darkColoursThreshold && c1.g < darkColoursThreshold && c1.b < darkColoursThreshold && + c2.r < darkColoursThreshold && c2.g < darkColoursThreshold && c2.b < darkColoursThreshold) { + return rDelta < darkColoursSimilarityThreshold && gDelta < darkColoursSimilarityThreshold && + bDelta < darkColoursSimilarityThreshold; + } + + return false; +} + +// Distance based on CIEDE2000 (https://en.wikipedia.org/wiki/Color_difference#CIEDE2000) +function differenceCiede2000(c1, c2) { + var kL = 1, kC = 1, kH = 0.9; + var LabStd = RGBtoCIELAB(c1); + var LabSmp = RGBtoCIELAB(c2); + + var lStd = LabStd.l; + var aStd = LabStd.a; + var bStd = LabStd.b; + var cStd = Math.sqrt(aStd * aStd + bStd * bStd); + + var lSmp = LabSmp.l; + var aSmp = LabSmp.a; + var bSmp = LabSmp.b; + var cSmp = Math.sqrt(aSmp * aSmp + bSmp * bSmp); + + var cAvg = (cStd + cSmp) / 2; + + var G = 0.5 * (1 - Math.sqrt(Math.pow(cAvg, 7) / (Math.pow(cAvg, 7) + Math.pow(25, 7)))); + + var apStd = aStd * (1 + G); + var apSmp = aSmp * (1 + G); + + var cpStd = Math.sqrt(apStd * apStd + bStd * bStd); + var cpSmp = Math.sqrt(apSmp * apSmp + bSmp * bSmp); + + var hpStd = Math.abs(apStd) + Math.abs(bStd) === 0 ? 0 : Math.atan2(bStd, apStd); + hpStd += (hpStd < 0) * 2 * Math.PI; + + var hpSmp = Math.abs(apSmp) + Math.abs(bSmp) === 0 ? 0 : Math.atan2(bSmp, apSmp); + hpSmp += (hpSmp < 0) * 2 * Math.PI; + + var dL = lSmp - lStd; + var dC = cpSmp - cpStd; + + var dhp = cpStd * cpSmp === 0 ? 0 : hpSmp - hpStd; + dhp -= (dhp > Math.PI) * 2 * Math.PI; + dhp += (dhp < -Math.PI) * 2 * Math.PI; + + var dH = 2 * Math.sqrt(cpStd * cpSmp) * Math.sin(dhp / 2); + + var Lp = (lStd + lSmp) / 2; + var Cp = (cpStd + cpSmp) / 2; + + var hp; + if (cpStd * cpSmp === 0) { + hp = hpStd + hpSmp; + } else { + hp = (hpStd + hpSmp) / 2; + hp -= (Math.abs(hpStd - hpSmp) > Math.PI) * Math.PI; + hp += (hp < 0) * 2 * Math.PI; + } + + var Lpm50 = Math.pow(Lp - 50, 2); + var T = 1 - + 0.17 * Math.cos(hp - Math.PI / 6) + + 0.24 * Math.cos(2 * hp) + + 0.32 * Math.cos(3 * hp + Math.PI / 30) - + 0.20 * Math.cos(4 * hp - 63 * Math.PI / 180); + + var Sl = 1 + (0.015 * Lpm50) / Math.sqrt(20 + Lpm50); + var Sc = 1 + 0.045 * Cp; + var Sh = 1 + 0.015 * Cp * T; + + var deltaTheta = 30 * Math.PI / 180 * Math.exp(-1 * Math.pow((180 / Math.PI * hp - 275)/25, 2)); + var Rc = 2 * Math.sqrt( + Math.pow(Cp, 7) / (Math.pow(Cp, 7) + Math.pow(25, 7)) + ); + + var Rt = -1 * Math.sin(2 * deltaTheta) * Rc; + + return Math.sqrt( + Math.pow(dL / (kL * Sl), 2) + + Math.pow(dC / (kC * Sc), 2) + + Math.pow(dH / (kH * Sh), 2) + + Rt * dC / (kC * Sc) * dH / (kH * Sh) + ); +} + +/**********************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 hslToRgb(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; +} + +function colorToRGB(color) { + if(window.colorCache && window.colorCache[color]){ + return window.colorCache[color]; + } + if (!window.cachedCtx) { + window.cachedCtx = document.createElement("canvas").getContext("2d"); + window.colorCache = {}; + } + let ctx = window.cachedCtx; + ctx.fillStyle = color; + return hexToRgb(ctx.fillStyle); + + function hexToRgb(hex) { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + }; + } +} \ No newline at end of file diff --git a/js/color_utils3.js b/js/color_utils3.js new file mode 100644 index 0000000..b9859a6 --- /dev/null +++ b/js/color_utils3.js @@ -0,0 +1,338 @@ +// Min distance under which 2 colours are considered similar +let distanceThreshold = 10; + +// Threshold used to consider a colour "dark" +let darkColoursThreshold = 50; +// Threshold used to tell if 2 dark colours are similar +let darkColoursSimilarityThreshold = 40; + +// Threshold used to consider a colour "light" +let lightColoursThreshold = 190; +// Threshold used to tell if 2 light colours are similar +let lightColoursSimilarityThreshold = 30; + +let referenceWhite = { + x: 95.05, + y: 100, + z: 108.89999999999999 +}; +const example = { + "red": [ + "#bf6f4a", + "#e07438", + "#c64524", + "#ff5000" + ], + "green": [ + "#99e65f", + "#5ac54f", + "#33984b" + ], + "blue": [ + "#0069aa", + "#0098dc", + "#00cdf9" + ], + "cyan": [ + "#0069aa", + "#0098dc", + "#00cdf9", + "#0cf1ff" + ], + "yellow": [ + "#ffa214", + "#ffc825", + "#ffeb57" + ], + "magenta": [ + "#db3ffd" + ], + "light": [ + "#ffffff", + "#f9e6cf", + "#fdd2ed" + ], + "dark": [ + "#131313", + "#1b1b1b", + "#272727", + "#3d3d3d", + "#5d5d5d" + ], + "brown": [ + "#e69c69", + "#f6ca9f", + "#f9e6cf", + "#edab50", + "#e07438", + "#ed7614", + "#ffa214", + "#ffc825", + "#ffeb57" + ], + "neon": [ + "#ff0040", + "#ff5000", + "#ed7614", + "#ffa214", + "#ffc825", + "#0098dc", + "#00cdf9", + "#0cf1ff", + "#7a09fa", + "#3003d9" + ] +}; +const COLOR_META = { + red: { color: "#ff0000", flux:{ h:25, v:40, s:40} }, + green: { color: "#00ff00", flux:{ h:35} }, + blue: { color: "#0077dd", flux:{ h:25, v:30, s:30} }, + cyan: { color: "#00ffff", flux:{ h:25, v:40, s:40} }, + yellow: { color: "#ffff00", flux:{ h:25, v:40, s:40} }, + magenta: { color: "#ff00ff", flux:{ h:15, v:40, s:40} }, + light: { color: "#ffffff", flux:{ v:10, s:30} }, + dark: { color: "#000000", flux:{ v:30, v:40, s:20} }, + brown: { color: "#ffaa00", flux:{ h:20} }, + neon: { color: "#00ffff", flux:{ s:20, v:20} }, +}; +Object.keys(COLOR_META).forEach(metaName=>{ + COLOR_META[metaName].colorMeta = colorMeta(COLOR_META[metaName].color); +}); +function paletteMeta(colorArr) { + const colorMetaArr = colorArr.map(colorMeta); + //////console.log('colorMetaArr === ',colorMetaArr); + + const ret = {}; + Object.keys(COLOR_META).forEach(metaName=>{ + const {color,colorMeta,flux} = COLOR_META[metaName]; + + const fluxKeys = Object.keys(flux); + + ret[metaName] = colorArr.filter((c,i)=>{ + const colorMeta2 = colorMetaArr[i]; + return fluxKeys.filter(k=>{ + return (colorMeta[k] + flux[k]) > colorMeta2[k] + && + (colorMeta[k] - flux[k]) < colorMeta2[k] + ; + }).length === fluxKeys.length; + }); + + }); + //////console.log(JSON.stringify(ret,null,4)); + return ret; +} +function colorMeta(colorStr) { + const rgb = colorToRGB(colorStr); + const hsv = rgb2hsv(rgb.r, rgb.g, rgb.b); + const lab = rgb2lab(rgb.r, rgb.g, rgb.b); + const cie = {c:lab.l,i:lab.a,e:lab.b}; + return { + ...rgb, + ...hsv, + ...cie + }; +} +function rgb2hex(r, g, b) { + return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); +} +function rgb2hsv(r, g, b) { + let rabs, gabs, babs, rr, gg, bb, h, s, v, diff, diffc, percentRoundFn; + rabs = r / 255; + gabs = g / 255; + babs = b / 255; + v = Math.max(rabs, gabs, babs), + diff = v - Math.min(rabs, gabs, babs); + diffc = c => (v - c) / 6 / diff + 1 / 2; + percentRoundFn = num => Math.round(num * 100) / 100; + if (diff == 0) { + h = s = 0; + } else { + s = diff / v; + rr = diffc(rabs); + gg = diffc(gabs); + bb = diffc(babs); + + if (rabs === v) { + h = bb - gg; + } else if (gabs === v) { + h = (1 / 3) + rr - bb; + } else if (babs === v) { + h = (2 / 3) + gg - rr; + } + if (h < 0) { + h += 1; + }else if (h > 1) { + h -= 1; + } + } + return { + h: Math.round(h * 360), + s: percentRoundFn(s * 100), + v: percentRoundFn(v * 100) + }; +} + +function similarColors(rgb1, rgb2) { + let ret = differenceCiede2000(rgb1, rgb2) + //////console.log(ret); + return (ret < distanceThreshold && lightColoursCheck(rgb1, rgb2)) || darkColoursCheck(rgb1, rgb2); +} +function lightColoursCheck(rgb1, rgb2) { + let rDelta = Math.abs(rgb1.r - rgb2.r); + let gDelta = Math.abs(rgb1.g - rgb2.g); + let bDelta = Math.abs(rgb1.b - rgb2.b); + + // Checking only if the colours are dark enough + if (rgb1.r > lightColoursThreshold && rgb1.g > lightColoursThreshold && rgb1.b > lightColoursThreshold && + rgb2.r > lightColoursThreshold && rgb2.g > lightColoursThreshold && rgb2.b > lightColoursThreshold) { + return rDelta < lightColoursSimilarityThreshold && gDelta < lightColoursSimilarityThreshold && + bDelta < lightColoursSimilarityThreshold; + } + + return true; +} +function darkColoursCheck(rgb1, rgb2) { + let rDelta = Math.abs(rgb1.r - rgb2.r); + let gDelta = Math.abs(rgb1.g - rgb2.g); + let bDelta = Math.abs(rgb1.b - rgb2.b); + + // Checking only if the colours are dark enough + if (rgb1.r < darkColoursThreshold && rgb1.g < darkColoursThreshold && rgb1.b < darkColoursThreshold && + rgb2.r < darkColoursThreshold && rgb2.g < darkColoursThreshold && rgb2.b < darkColoursThreshold) { + return rDelta < darkColoursSimilarityThreshold && gDelta < darkColoursSimilarityThreshold && + bDelta < darkColoursSimilarityThreshold; + } + + return false; +} +// Distance based on CIEDE2000 (https://en.wikipedia.org/wiki/Color_difference#CIEDE2000) +function differenceCiede2000(rgb1, rgb2) { + var kL = 1, + kC = 1, + kH = 0.9; + var LabStd = rgb2lab(rgb1); + var LabSmp = rgb2lab(rgb2); + + var lStd = LabStd.l; + var aStd = LabStd.a; + var bStd = LabStd.b; + var cStd = Math.sqrt(aStd * aStd + bStd * bStd); + + var lSmp = LabSmp.l; + var aSmp = LabSmp.a; + var bSmp = LabSmp.b; + var cSmp = Math.sqrt(aSmp * aSmp + bSmp * bSmp); + + var cAvg = (cStd + cSmp) / 2; + + var G = 0.5 * (1 - Math.sqrt(Math.pow(cAvg, 7) / (Math.pow(cAvg, 7) + Math.pow(25, 7)))); + + var apStd = aStd * (1 + G); + var apSmp = aSmp * (1 + G); + + var cpStd = Math.sqrt(apStd * apStd + bStd * bStd); + var cpSmp = Math.sqrt(apSmp * apSmp + bSmp * bSmp); + + var hpStd = Math.abs(apStd) + Math.abs(bStd) === 0 ? 0 : Math.atan2(bStd, apStd); + hpStd += (hpStd < 0) * 2 * Math.PI; + + var hpSmp = Math.abs(apSmp) + Math.abs(bSmp) === 0 ? 0 : Math.atan2(bSmp, apSmp); + hpSmp += (hpSmp < 0) * 2 * Math.PI; + + var dL = lSmp - lStd; + var dC = cpSmp - cpStd; + + var dhp = cpStd * cpSmp === 0 ? 0 : hpSmp - hpStd; + dhp -= (dhp > Math.PI) * 2 * Math.PI; + dhp += (dhp < -Math.PI) * 2 * Math.PI; + + var dH = 2 * Math.sqrt(cpStd * cpSmp) * Math.sin(dhp / 2); + + var Lp = (lStd + lSmp) / 2; + var Cp = (cpStd + cpSmp) / 2; + + var hp; + if (cpStd * cpSmp === 0) { + hp = hpStd + hpSmp; + } else { + hp = (hpStd + hpSmp) / 2; + hp -= (Math.abs(hpStd - hpSmp) > Math.PI) * Math.PI; + hp += (hp < 0) * 2 * Math.PI; + } + + var Lpm50 = Math.pow(Lp - 50, 2); + var T = 1 - + 0.17 * Math.cos(hp - Math.PI / 6) + + 0.24 * Math.cos(2 * hp) + + 0.32 * Math.cos(3 * hp + Math.PI / 30) - + 0.20 * Math.cos(4 * hp - 63 * Math.PI / 180); + + var Sl = 1 + (0.015 * Lpm50) / Math.sqrt(20 + Lpm50); + var Sc = 1 + 0.045 * Cp; + var Sh = 1 + 0.015 * Cp * T; + + var deltaTheta = 30 * Math.PI / 180 * Math.exp(-1 * Math.pow((180 / Math.PI * hp - 275) / 25, 2)); + var Rc = 2 * Math.sqrt( + Math.pow(Cp, 7) / (Math.pow(Cp, 7) + Math.pow(25, 7)) + ); + + var Rt = -1 * Math.sin(2 * deltaTheta) * Rc; + + return Math.sqrt( + Math.pow(dL / (kL * Sl), 2) + + Math.pow(dC / (kC * Sc), 2) + + Math.pow(dH / (kH * Sh), 2) + + Rt * dC / (kC * Sc) * dH / (kH * Sh) + ); +} +function rgb2lab(r, g, b) { + // Convert to XYZ first via matrix transformation + let x = 0.412453 * r + 0.357580 * g + 0.180423 * b; + let y = 0.212671 * r + 0.715160 * g + 0.072169 * b; + let z = 0.019334 * r + 0.119193 * g + 0.950227 * 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; +} + +function colorToRGB(color) { + if(window.colorCache && window.colorCache[color]){ + return window.colorCache[color]; + } + if (!window.cachedCtx) { + window.cachedCtx = document.createElement("canvas").getContext("2d"); + window.colorCache = {}; + } + let ctx = window.cachedCtx; + ctx.fillStyle = color; + return hexToRgb(ctx.fillStyle); + + function hexToRgb(hex) { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + }; + } +} \ No newline at end of file diff --git a/js/data/consts.js b/js/data/consts.js index f3903f8..c049d25 100644 --- a/js/data/consts.js +++ b/js/data/consts.js @@ -3,7 +3,5 @@ const MAX_Z_INDEX = 5000; // Index of the first layer the user can use in the layers array const firstUserLayerIndex = 2; -// Number of layers that are only used by the editor -const nAppLayers = 3; const MIN_ZOOM_LEVEL = 0.5; \ No newline at end of file diff --git a/js/layers/Layer.js b/js/layers/Layer.js index 501953b..43e653a 100644 --- a/js/layers/Layer.js +++ b/js/layers/Layer.js @@ -18,7 +18,7 @@ class Layer { // TODO: this is simply terrible. menuEntry can either be an HTML element, a string or undefined. // If it's an HTML element it is added, if it's a string nothing happens, if it's undefined the // first menuEntry is used (the one that appears on load) - constructor(width, height, canvas, menuEntry) { + constructor(width, height, canvas, menuEntry, id) { // REFACTOR: the canvas should actually be a Canvas instance this.canvas = Util.getElement(canvas); this.canvas.width = width; @@ -37,18 +37,24 @@ class Layer { else if (menuEntry !== undefined) this.menuEntry = menuEntry; - let id = Layer.unusedIDs.pop(); + let hadId = false; + if(typeof id !== "undefined"){ + hadId = true; + } else { + id = Layer.unusedIDs.pop(); + } if (id == null) { id = Layer.currentID; Layer.currentID++; } - this.id = "layer" + id; + this.id = hadId ? id : "layer" + id; // Binding the events if (this.menuEntry !== undefined) { this.name = this.menuEntry.getElementsByTagName("p")[0].innerHTML; + this.menuEntry.id = "layer" + id; this.menuEntry.onmouseover = () => this.hover(); @@ -69,16 +75,58 @@ class Layer { this.menuEntry.getElementsByTagName("canvas")[0].getContext('2d').imageSmoothingEnabled = false; } + if(hadId){ + this.menuEntry.classList.remove("layers-menu-entry"); + } else { + if(this.menuEntry)this.menuEntry.classList.add("layers-menu-entry"); + } + this.initialize(); + } hasCanvas() { return this.menuEntry != null; } - tryDelete() { + delete(layerIndex) { + //console.log('layerIndex === ',layerIndex); + let toDelete = currFile.layers[layerIndex]; + let previousSibling; + if(toDelete){ + //console.log('toDelete === ',toDelete); + previousSibling = toDelete.menuEntry.previousElementSibling; + //console.log('previousSibling === ',previousSibling); + // Adding the ids to the unused ones + // Deleting canvas and entry + toDelete.canvas.remove(); + toDelete.menuEntry.remove(); + } + + Layer.unusedIDs.push(this.id); + + if(this.isSelected) { + // Selecting the nearest layer + const nearestLayer = (currFile.layers[layerIndex + 1] ?? currFile.layers[layerIndex - 1]); + if(nearestLayer){ + nearestLayer.selectLayer(); + //console.log('changing to nearest layer'); + } + } + + + // Removing the layer from the list + currFile.layers.splice(layerIndex, 1); + + if(toDelete){ + new HistoryState().DeleteLayer(toDelete, previousSibling, layerIndex); + } + } + + tryDelete() { //TODO: quote yoda if (Input.getLastTarget() != this.menuEntry && Input.getLastTarget().parentElement != this.menuEntry) return; + LayerList.deleteLayer(); } @@ -121,7 +169,7 @@ class Layer { hover() { // Hides all the layers but the current one - for (let i=1; i response.json()) - .then(data => { - //palette loaded successfully - palettes[paletteSlug] = data; - palettes[paletteSlug].specified = true; + let args = window.location.pathname.split('/'); + let paletteSlug = args[2]; + let dimensions = args[3]; + // let prefillWidth = args[4] ?? 9; // TODO + // let prefill = args[5] ?? "110101111110100110111100110110101111"; + // let customColors = args[6] ?? ""; // ex: "#ffffff,#000000" + // console.log('prefill === ',prefill); + if(paletteSlug && dimensions) { - //refresh list of palettes - document.getElementById('palette-menu-splash').refresh(); - - //if the dimentions were specified - if (dimentions && dimentions.length >= 3 && dimentions.includes('x')) { - let width = dimentions.split('x')[0]; - let height = dimentions.split('x')[1]; - - //create new document - Startup.newPixel(width, height); - } - //dimentions were not specified -- show splash screen with palette preselected - else { - //show splash - Dialogue.showDialogue('new-pixel', false); - } - }) - //error fetching url (either palette doesn't exist, or lospec is down) - .catch((error) => { - console.warn('failed to load palette "'+paletteSlug+'"', error); - //proceed to splash screen - Dialogue.showDialogue('splash', false); - }); - } -}; + //fetch palette via lospec palette API + fetch('https://lospec.com/palette-list/'+paletteSlug+'.json') + .then(response => response.json()) + .then(data => { + //palette loaded successfully + palettes[paletteSlug] = data; + palettes[paletteSlug].specified = true; + //refresh list of palettes + document.getElementById('palette-menu-splash').refresh(); + + //if the dimensions were specified + if (dimensions && dimensions.length >= 3 && dimensions.includes('x')) { + let width = dimensions.split('x')[0]; + let height = dimensions.split('x')[1]; + const layers = []; + let selectedLayer; + Startup.newPixel({ + canvasWidth: width, + canvasHeight: height, + selectedLayer, + colors: data.colors.map(n=>"#"+n), + layers + }); + } + //dimensions were not specified -- show splash screen with palette preselected + else { + //show splash + Dialogue.showDialogue('new-pixel', false); + } + }) + //error fetching url (either palette doesn't exist, or lospec is down) + .catch((error) => { + //console.warn('failed to load palette "'+paletteSlug+'"', error); + //proceed to splash screen + Dialogue.showDialogue('splash', false); + }); + } else { + if(FileManager.localStorageCheck()) { + //load cached document + const lpe = FileManager.localStorageLoad(); + + Startup.newPixel(lpe); + } + //check if there are any url parameters + else if (window.location.pathname.replace('/pixel-editor/','').length <= 1) { + //show splash screen + Dialogue.showDialogue('splash', false); + } + } +} //prevent user from leaving page with unsaved data -window.onbeforeunload = function() { - if (EditorState.documentCreated) - return 'You will lose your pixel if it\'s not saved!'; +// window.onbeforeunload = function() { +// if (EditorState.documentCreated) +// return 'You will lose your pixel if it\'s not saved!'; - else return; -}; +// else return; +// }; // Compatibility functions function closeCompatibilityWarning() { diff --git a/js/tools/BrushTool.js b/js/tools/BrushTool.js index 8003484..23da542 100644 --- a/js/tools/BrushTool.js +++ b/js/tools/BrushTool.js @@ -12,7 +12,7 @@ class BrushTool extends ResizableTool { this.addTutorialKey("Left drag", " to draw a stroke"); this.addTutorialKey("Right drag", " to resize the brush"); this.addTutorialKey("+ or -", " to resize the brush"); - this.addTutorialImg("brush-tutorial.gif"); + this.addTutorialImg("/images/ToolTutorials/brush-tutorial.gif"); } onStart(mousePos, cursorTarget) { diff --git a/js/tools/EllipseTool.js b/js/tools/EllipseTool.js index 68fb4c9..6d6a19d 100644 --- a/js/tools/EllipseTool.js +++ b/js/tools/EllipseTool.js @@ -25,7 +25,7 @@ class EllipseTool extends ResizableTool { this.addTutorialKey("Left drag", " to draw an ellipse"); this.addTutorialKey("Right drag", " to resize the brush"); this.addTutorialKey("+ or -", " to resize the brush"); - this.addTutorialImg("ellipse-tutorial.gif"); + this.addTutorialImg("/images/ToolTutorials/ellipse-tutorial.gif"); } changeFillType() { diff --git a/js/tools/EraserTool.js b/js/tools/EraserTool.js index dfbf841..c691caa 100644 --- a/js/tools/EraserTool.js +++ b/js/tools/EraserTool.js @@ -12,7 +12,7 @@ class EraserTool extends ResizableTool { this.addTutorialKey("Left drag", " to erase an area"); this.addTutorialKey("Right drag", " to resize the eraser"); this.addTutorialKey("+ or -", " to resize the eraser"); - this.addTutorialImg("eraser-tutorial.gif"); + this.addTutorialImg("/images/ToolTutorials/eraser-tutorial.gif"); } onStart(mousePos) { diff --git a/js/tools/EyeDropperTool.js b/js/tools/EyeDropperTool.js index ef9ceef..76c9b6d 100644 --- a/js/tools/EyeDropperTool.js +++ b/js/tools/EyeDropperTool.js @@ -14,7 +14,7 @@ class EyeDropperTool extends Tool { this.addTutorialKey("Aòt + left drag", " to preview the picked colour"); this.addTutorialKey("Left click", " to select a colour"); this.addTutorialKey("Alt + click", " to select a colour"); - this.addTutorialImg("eyedropper-tutorial.gif"); + this.addTutorialImg("/images/ToolTutorials/eyedropper-tutorial.gif"); } onStart(mousePos, target) { diff --git a/js/tools/FillTool.js b/js/tools/FillTool.js index 0043f66..6738e63 100644 --- a/js/tools/FillTool.js +++ b/js/tools/FillTool.js @@ -8,7 +8,7 @@ class FillTool extends DrawingTool { this.addTutorialTitle("Fill tool"); this.addTutorialKey("F", " to select the fill tool"); this.addTutorialKey("Left click", " to fill a contiguous area"); - this.addTutorialImg("fill-tutorial.gif"); + this.addTutorialImg("/images/ToolTutorials/fill-tutorial.gif"); } onStart(mousePos, target) { @@ -25,7 +25,7 @@ class FillTool extends DrawingTool { static fill(cursorLocation, context) { //changes a pixels color function colorPixel(tempImage, pixelPos, fillColor) { - //console.log('colorPixel:',pixelPos); + //////console.log('colorPixel:',pixelPos); tempImage.data[pixelPos] = fillColor.r; tempImage.data[pixelPos + 1] = fillColor.g; tempImage.data[pixelPos + 2] = fillColor.b; @@ -34,13 +34,13 @@ class FillTool extends DrawingTool { //change x y to color value passed from the function and use that as the original color function matchStartColor(tempImage, pixelPos, color) { - //console.log('matchPixel:',x,y) + //////console.log('matchPixel:',x,y) let r = tempImage.data[pixelPos]; let g = tempImage.data[pixelPos + 1]; let b = tempImage.data[pixelPos + 2]; let a = tempImage.data[pixelPos + 3]; - //console.log(r == color[0] && g == color[1] && b == color[2]); + //////console.log(r == color[0] && g == color[1] && b == color[2]); return (r == color[0] && g == color[1] && b == color[2] && a == color[3]); } @@ -52,7 +52,7 @@ class FillTool extends DrawingTool { //this is an array that holds all of the pixels at the top of the cluster let topmostPixelsArray = [[Math.floor(cursorLocation[0]/currFile.zoom), Math.floor(cursorLocation[1]/currFile.zoom)]]; - //console.log('topmostPixelsArray:',topmostPixelsArray) + //////console.log('topmostPixelsArray:',topmostPixelsArray) //the offset of the pixel in the temp image data to start with let startingPosition = (topmostPixelsArray[0][1] * currFile.canvasSize[0] + topmostPixelsArray[0][0]) * 4; diff --git a/js/tools/LassoSelectionTool.js b/js/tools/LassoSelectionTool.js index f767e12..e60b125 100644 --- a/js/tools/LassoSelectionTool.js +++ b/js/tools/LassoSelectionTool.js @@ -15,7 +15,7 @@ class LassoSelectionTool extends SelectionTool { this.addTutorialKey("CTRL+C", " to copy a selection") this.addTutorialKey("CTRL+V", " to paste a selection") this.addTutorialKey("CTRL+X", " to cut a selection") - this.addTutorialImg("lassoselect-tutorial.gif"); + this.addTutorialImg("/images/ToolTutorials/lassoselect-tutorial.gif"); } onStart(mousePos, mouseTarget) { diff --git a/js/tools/LineTool.js b/js/tools/LineTool.js index 67662b5..d596cd6 100644 --- a/js/tools/LineTool.js +++ b/js/tools/LineTool.js @@ -12,7 +12,7 @@ class LineTool extends ResizableTool { this.addTutorialKey("Left drag", " to draw a line"); this.addTutorialKey("Right drag", " to resize the brush"); this.addTutorialKey("+ or -", " to resize the brush"); - this.addTutorialImg("line-tutorial.gif"); + this.addTutorialImg("/images/ToolTutorials/line-tutorial.gif"); } onStart(mousePos) { diff --git a/js/tools/MagicWandTool.js b/js/tools/MagicWandTool.js index d695ada..d07c20e 100644 --- a/js/tools/MagicWandTool.js +++ b/js/tools/MagicWandTool.js @@ -13,7 +13,7 @@ class MagicWandTool extends SelectionTool { this.addTutorialKey("CTRL+C", " to copy a selection"); this.addTutorialKey("CTRL+V", " to paste a selection"); this.addTutorialKey("CTRL+X", " to cut a selection"); - this.addTutorialImg("magicwand-tutorial.gif"); + this.addTutorialImg("/images/ToolTutorials/magicwand-tutorial.gif"); } onEnd(mousePos, mouseTarget) { @@ -22,7 +22,7 @@ class MagicWandTool extends SelectionTool { !Util.cursorInCanvas(currFile.canvasSize, [mousePos[0]/currFile.zoom, mousePos[1]/currFile.zoom])) return; - + ////console.log('this.moveTool === ',this.moveTool); this.switchFunc(this.moveTool); this.moveTool.setSelectionData(this.getSelection(), this); } @@ -52,8 +52,10 @@ class MagicWandTool extends SelectionTool { this.outlineData = new ImageData(currFile.canvasSize[0], currFile.canvasSize[1]); this.previewData = selectedData; this.drawSelectedArea(); - this.boundingBoxCenter = [this.boundingBox.minX + (this.boundingBox.maxX - this.boundingBox.minX) / 2, - this.boundingBox.minY + (this.boundingBox.maxY - this.boundingBox.minY) / 2]; + this.boundingBoxCenter = [ + this.boundingBox.minX + (this.boundingBox.maxX - this.boundingBox.minX) / 2, + this.boundingBox.minY + (this.boundingBox.maxY - this.boundingBox.minY) / 2 + ]; // Cut the selection this.cutSelection(); @@ -61,7 +63,7 @@ class MagicWandTool extends SelectionTool { currFile.TMPLayer.context.putImageData(this.previewData, 0, 0); // Draw the bounding box - this.drawBoundingBox(); + this.drawBoundingBox(1, 1); return selectedData; } diff --git a/js/tools/PanTool.js b/js/tools/PanTool.js index 1a952a0..8bd930e 100644 --- a/js/tools/PanTool.js +++ b/js/tools/PanTool.js @@ -10,7 +10,7 @@ class PanTool extends Tool { this.addTutorialKey("P", " to select the lasso selection tool"); this.addTutorialKey("Left drag", " to move the viewport"); this.addTutorialKey("Space + drag", " to move the viewport"); - this.addTutorialImg("pan-tutorial.gif"); + this.addTutorialImg("/images/ToolTutorials/pan-tutorial.gif"); } onStart(mousePos, target) { diff --git a/js/tools/RectangleTool.js b/js/tools/RectangleTool.js index 7ecd78a..2393ea8 100644 --- a/js/tools/RectangleTool.js +++ b/js/tools/RectangleTool.js @@ -23,7 +23,7 @@ class RectangleTool extends ResizableTool { this.addTutorialKey("Left drag", " to draw a rectangle"); this.addTutorialKey("Right drag", " to resize the brush"); this.addTutorialKey("+ or -", " to resize the brush"); - this.addTutorialImg("rectangle-tutorial.gif"); + this.addTutorialImg("/images/ToolTutorials/rectangle-tutorial.gif"); } changeFillType() { diff --git a/js/tools/RectangularSelectionTool.js b/js/tools/RectangularSelectionTool.js index f7bee0b..de5ff53 100644 --- a/js/tools/RectangularSelectionTool.js +++ b/js/tools/RectangularSelectionTool.js @@ -14,7 +14,7 @@ class RectangularSelectionTool extends SelectionTool { this.addTutorialKey("CTRL+C", " to copy a selection"); this.addTutorialKey("CTRL+V", " to paste a selection"); this.addTutorialKey("CTRL+X", " to cut a selection"); - this.addTutorialImg("rectselect-tutorial.gif"); + this.addTutorialImg("/images/ToolTutorials/rectselect-tutorial.gif"); } onStart(mousePos, mouseTarget) { diff --git a/js/tools/SelectionTool.js b/js/tools/SelectionTool.js index 35a7799..2f31a61 100644 --- a/js/tools/SelectionTool.js +++ b/js/tools/SelectionTool.js @@ -44,8 +44,10 @@ class SelectionTool extends Tool { this.currSelection = {}; this.moveOffset = [0, 0]; - this.updateBoundingBox(Math.min(Math.max(mouseX, 0), currFile.canvasSize[0]-1), - Math.min(Math.max(mouseY, 0), currFile.canvasSize[1]-1)); + this.updateBoundingBox( + Math.min(Math.max(mouseX, 0), currFile.canvasSize[0]-1), + Math.min(Math.max(mouseY, 0), currFile.canvasSize[1]-1) + ); } onDrag(mousePos) { @@ -66,8 +68,10 @@ class SelectionTool extends Tool { if (Util.cursorInCanvas(currFile.canvasSize, [mousePos[0]/currFile.zoom, mousePos[1]/currFile.zoom])) { - this.updateBoundingBox(Math.min(Math.max(mouseX, 0), currFile.canvasSize[0]-1), - Math.min(Math.max(mouseY, 0), currFile.canvasSize[1]-1)); + this.updateBoundingBox( + Math.min(Math.max(mouseX, 0), currFile.canvasSize[0]-1), + Math.min(Math.max(mouseY, 0), currFile.canvasSize[1]-1) + ); } } @@ -81,8 +85,10 @@ class SelectionTool extends Tool { let mouseY = mousePos[1] / currFile.zoom; if (Util.cursorInCanvas(currFile.canvasSize, [mousePos[0]/currFile.zoom, mousePos[1]/currFile.zoom])) { - this.updateBoundingBox(Math.min(Math.max(mouseX, 0), currFile.canvasSize[0]-1), - Math.min(Math.max(mouseY, 0), currFile.canvasSize[1]-1)); + this.updateBoundingBox( + Math.min(Math.max(mouseX, 0), currFile.canvasSize[0]-1), + Math.min(Math.max(mouseY, 0), currFile.canvasSize[1]-1) + ); } this.boundingBoxCenter = [this.boundingBox.minX + (this.boundingBox.maxX - this.boundingBox.minX) / 2, @@ -211,6 +217,7 @@ class SelectionTool extends Tool { } } } + ////console.log('this.currSelection === ',this.currSelection); // Save the selection outline this.outlineData = currFile.VFXLayer.context.getImageData(this.boundingBox.minX, @@ -246,16 +253,25 @@ class SelectionTool extends Tool { this.boundingBox.minY + this.moveOffset[1]); } - drawBoundingBox() { + drawBoundingBox(xo = 0, yo = 0) { currFile.VFXLayer.context.fillStyle = "red"; - currFile.VFXLayer.context.fillRect(this.boundingBox.minX + this.moveOffset[0], - this.boundingBox.minY + this.moveOffset[1], 1, 1); - currFile.VFXLayer.context.fillRect(this.boundingBox.minX+ this.moveOffset[0], - this.boundingBox.maxY + this.moveOffset[1], 1, 1); - currFile.VFXLayer.context.fillRect(this.boundingBox.maxX+ this.moveOffset[0], - this.boundingBox.minY + this.moveOffset[1], 1, 1); - currFile.VFXLayer.context.fillRect(this.boundingBox.maxX+ this.moveOffset[0], - this.boundingBox.maxY + this.moveOffset[1], 1, 1); + + currFile.VFXLayer.context.fillRect( + this.boundingBox.minX + this.moveOffset[0] - xo, + this.boundingBox.minY + this.moveOffset[1] - yo, + 1, 1); + currFile.VFXLayer.context.fillRect( + this.boundingBox.minX + this.moveOffset[0] - xo, + this.boundingBox.maxY + this.moveOffset[1] + yo, + 1, 1); + currFile.VFXLayer.context.fillRect( + this.boundingBox.maxX + this.moveOffset[0] + xo, + this.boundingBox.minY + this.moveOffset[1] - yo, + 1, 1); + currFile.VFXLayer.context.fillRect( + this.boundingBox.maxX + this.moveOffset[0] + xo, + this.boundingBox.maxY + this.moveOffset[1] + yo, + 1, 1); } isBorderOfBox(pixel) { diff --git a/js/tools/ZoomTool.js b/js/tools/ZoomTool.js index a945bb6..7da5acc 100644 --- a/js/tools/ZoomTool.js +++ b/js/tools/ZoomTool.js @@ -75,8 +75,11 @@ class ZoomTool extends Tool { } } - for (let i=1; i + + + + + + Document + + + + + +
+ +
+
+ +
+
+ +
+ + + + + + diff --git a/poc_pages/rotation_POC_latest.html b/poc_pages/rotation_POC_latest.html new file mode 100644 index 0000000..628e5d8 --- /dev/null +++ b/poc_pages/rotation_POC_latest.html @@ -0,0 +1,269 @@ + + + + + + + Document + + + + + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/poc_pages/rotation_POC_old.html b/poc_pages/rotation_POC_old.html new file mode 100644 index 0000000..8a59661 --- /dev/null +++ b/poc_pages/rotation_POC_old.html @@ -0,0 +1,268 @@ + + + + + + + Document + + + + + + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/poc_pages/tiny_text.html b/poc_pages/tiny_text.html new file mode 100644 index 0000000..edee703 --- /dev/null +++ b/poc_pages/tiny_text.html @@ -0,0 +1,45 @@ + + + + + + + + Document + + + + + + + + + + + + + + + + diff --git a/poc_pages/wang_tiles_16.html b/poc_pages/wang_tiles_16.html new file mode 100644 index 0000000..138f095 --- /dev/null +++ b/poc_pages/wang_tiles_16.html @@ -0,0 +1,130 @@ + + + Basic three.js template + + + + + + + + + + + \ No newline at end of file diff --git a/server.js b/server.js index 3fcb6b0..30534a5 100644 --- a/server.js +++ b/server.js @@ -33,13 +33,25 @@ app.use('/', express.static(FULLBUILDPATH, { } })); - - //ROUTE - match / or any route with just numbers letters and dashes, and return index.htm (all other routes should have been handled already) app.get('/', (req, res, next) => { - console.log('root') + //console.log('root') res.sendFile(path.join(__dirname, BUILDDIR, 'index.htm'), {}, function (err) { - console.log('sent file'); + //console.log('sent file'); + return next(); + }); +}); +app.get('/pixel-editor', (req, res, next) => { + //console.log('root') + res.sendFile(path.join(__dirname, BUILDDIR, 'index.htm'), {}, function (err) { + //console.log('sent file'); + return next(); + }); +}); +app.get('/pixel-editor/?:palette/?:resolution/?:patternWidth/?:patternBinStr', (req, res, next) => { + //console.log('root') + res.sendFile(path.join(__dirname, BUILDDIR, 'index.htm'), {}, function (err) { + //console.log('sent file'); return next(); }); });