mirror of
https://github.com/lospec/pixel-editor.git
synced 2023-08-10 21:12:51 +03:00
10aa6134cc
Now if a layer is locked or hidden, the icon will always be shown.
271 lines
9.6 KiB
JavaScript
271 lines
9.6 KiB
JavaScript
/** TODO LIST FOR LAYERS
|
|
|
|
GENERAL REQUIREMENTS:
|
|
- The user shouldn't be able to draw on a hidden or locked layer
|
|
- Must delete the selected layer when right clicking on a layer and selecting that option
|
|
* We should think about selecting more than one layer at once.
|
|
* Rename layer
|
|
* Merge with bottom layer option
|
|
* Flatten visible option
|
|
* Flatten everything option
|
|
- Must move a layer when dragging it in the layer list (https://codepen.io/retrofuturistic/pen/tlbHE)
|
|
- When the user clicks on the eye icon, the icon must always be shown
|
|
- When the user clicks on the lock icon, the icon must be always be shown
|
|
- When a layer is locked or not visible, the corresponding icons are always shown
|
|
- When saving an artwork, the layers must be flattened to a temporary layer, which is then exported and deleted
|
|
- Saving the state of an artwork to a .lospec file so that people can work on it later keeping
|
|
the layers they created? That'd be cool, even for the app users, that could just double click on a lospec
|
|
file for it to be opened right in the pixel editor
|
|
|
|
ADDITIONAL LOGIC TO MAKE IT WORK WITH THE REST OF THE CODE:
|
|
|
|
- move the tmp layer so that it's always right below the active layer
|
|
- when the move tool is selected (to move a selection), the tmp layer must be put right above the
|
|
active layer to show a preview
|
|
- mouse events will always have at least a canvas target, so evey time there's an event, we'll have to check
|
|
the actual element type instead of the current layer and then apply the tool on the currentLayer, not on
|
|
the first one in order of z-index
|
|
- when changing the stroke colour, it should change for all the layers: it should already be happening,
|
|
just make sure it is
|
|
|
|
OPTIONAL:
|
|
|
|
1 - Fix issues
|
|
2 - Add a Replace feature so that people can replace a colour without editing the one in the palette
|
|
(right click->replace colour in layers? in that case we'd have to implement multiple layers selection)
|
|
|
|
KNOWN BUGS:
|
|
|
|
1 - Must delete existing layers when creating a new pixel
|
|
|
|
THINGS TO TEST:
|
|
|
|
1 - Undo / redo
|
|
2 - Copy / cut / paste selection
|
|
3 - Colour picking from underlying layer
|
|
4 - File export
|
|
*/
|
|
|
|
/*
|
|
MORE TODO LIST:
|
|
|
|
- Resize canvas (must make a simple editor to preview the changes)
|
|
- Resize sprite
|
|
*/
|
|
|
|
// Will probably need a class to handle all the html stuff
|
|
|
|
let layerList;
|
|
let layerListEntry;
|
|
let layerCount = 1;
|
|
let maxZIndex = 3;
|
|
|
|
on('click',"add-layer-button", function(){
|
|
// Creating a new canvas
|
|
let newCanvas = document.createElement("canvas");
|
|
// Setting up the new canvas
|
|
canvasView.append(newCanvas);
|
|
maxZIndex++;
|
|
newCanvas.style.zIndex = maxZIndex;
|
|
newCanvas.classList.add("drawingCanvas");
|
|
|
|
console.log("Tela creata: " + newCanvas);
|
|
|
|
// Clone the default layer
|
|
let toAppend = layerListEntry.cloneNode(true);
|
|
// Setting the default name for the layer
|
|
toAppend.getElementsByTagName('p')[0].innerHTML = "Layer " + layerCount;
|
|
// Removing the selected class
|
|
toAppend.classList.remove("selected-layer");
|
|
// Adding the layer to the list
|
|
layerCount++;
|
|
|
|
// Creating a layer object
|
|
let newLayer = new Layer(currentLayer.canvasSize[0], currentLayer.canvasSize[1], newCanvas, toAppend);
|
|
newLayer.context.fillStyle = currentLayer.context.fillStyle;
|
|
newLayer.copyData(currentLayer);
|
|
layers.splice(layers.length - 3, 0, newLayer);
|
|
|
|
// Insert it before the Add layer button
|
|
layerList.insertBefore(toAppend, layerList.childNodes[0]);
|
|
}, 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;
|
|
|
|
if (menuEntry != null) {
|
|
console.log("Aggiungo eventi");
|
|
menuEntry.onclick = () => this.select();
|
|
menuEntry.getElementsByTagName("button")[0].onclick = () => this.toggleLock();
|
|
menuEntry.getElementsByTagName("button")[1].onclick = () => this.toggleVisibility();
|
|
}
|
|
|
|
this.initialize();
|
|
}
|
|
|
|
// Initializes the canvas
|
|
initialize() {
|
|
var maxHorizontalZoom = Math.floor(window.innerWidth/this.canvasSize[0]*0.75);
|
|
var maxVerticalZoom = Math.floor(window.innerHeight/this.canvasSize[1]*0.75);
|
|
|
|
zoom = Math.min(maxHorizontalZoom,maxVerticalZoom);
|
|
if (zoom < 1) zoom = 1;
|
|
|
|
//resize canvas
|
|
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';
|
|
|
|
//unhide 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;
|
|
}
|
|
// Resizes canvas
|
|
resize() {
|
|
let newWidth = (this.canvas.width * zoom) + 'px';
|
|
let newHeight = (this.canvas.height *zoom)+ 'px';
|
|
|
|
this.canvas.style.width = newWidth;
|
|
this.canvas.style.height = newHeight;
|
|
}
|
|
// Copies the otherCanvas' position and size
|
|
copyData(otherCanvas) {
|
|
this.canvas.style.width = otherCanvas.canvas.style.width;
|
|
this.canvas.style.height = otherCanvas.canvas.style.height;
|
|
|
|
this.canvas.style.left = otherCanvas.canvas.style.left;
|
|
this.canvas.style.top = otherCanvas.canvas.style.top;
|
|
}
|
|
|
|
select() {
|
|
// Deselecting the old layer
|
|
currentLayer.deselect();
|
|
|
|
// Selecting the current layer
|
|
this.isSelected = true;
|
|
this.menuEntry.classList.add("selected-layer");
|
|
currentLayer = getLayerByName(this.menuEntry.getElementsByTagName("p")[0].innerHTML);
|
|
}
|
|
|
|
toggleLock() {
|
|
if (this.isLocked) {
|
|
console.log("unlocked");
|
|
this.unlock();
|
|
}
|
|
else {
|
|
console.log("locked");
|
|
this.lock();
|
|
}
|
|
}
|
|
|
|
toggleVisibility() {
|
|
console.log(this);
|
|
|
|
if (this.isVisible) {
|
|
this.hide();
|
|
}
|
|
else {
|
|
this.show();
|
|
}
|
|
}
|
|
|
|
deselect() {
|
|
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);
|
|
}
|
|
}
|
|
|
|
// Finds a layer given its name
|
|
function getLayerByName(name) {
|
|
for (let i=0; i<layers.length; i++) {
|
|
if (layers[i].menuEntry != null) {
|
|
if (layers[i].menuEntry.getElementsByTagName("p")[0].innerHTML == name) {
|
|
return layers[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
} |