Merge pull request #20 from unsettledgames/master

Added canvas resizing and sprite scaling
This commit is contained in:
Lospec 2020-12-30 13:06:15 -05:00 committed by GitHub
commit 74691e4007
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 1622 additions and 359 deletions

View File

@ -13,13 +13,10 @@ The next version is mostly focused on adding missing essential features and port
Suggestions / Planned features:
- Line tool
- Resize canvas
- Snap brush preview to pixel grid
- Move selection with arrows
- Tiled mode
- Load palette from LPE file
- Move colours in palette editor
- Duplicate layer
- Hide non-hovered layers
- Move colours in (advanced) palette editor
- Symmetry options
- Custom color picker
- custom code without dependencies
@ -31,17 +28,19 @@ Suggestions / Planned features:
- Maybe rearrange UI on portrait
- Stack colors when too many
- Fix popups
- Show colors which would need to be added to palette
- Palette option remove unused colors
- Pixel Grid
- Another currentLayer.canvas
- Must be rescaled each zoom
- Possibly add collaborate function
- Bug fix
- Alt + scroll broken
- Polish:
- ctrl + a to select everything / selection -> all, same for deselection
- Show colors which would need to be added to palette
- Warning windows for wrong inputs
- Palette option remove unused colors
- Move selection with arrows
- Update pivot buttons when resizing canvas
- Update borders by dragging the canvas' edges with the mouse when resizing canvas
- Move the canvases so they're centered after resizing the canvas (maybe a .center() method in layer class)
- Trim canvas
## How to Contribute

View File

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

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512.171 512.171" style="enable-background:new 0 0 512.171 512.171;" xml:space="preserve">
<g>
<g>
<path d="M479.046,283.925c-1.664-3.989-5.547-6.592-9.856-6.592H352.305V10.667C352.305,4.779,347.526,0,341.638,0H170.971
c-5.888,0-10.667,4.779-10.667,10.667v266.667H42.971c-4.309,0-8.192,2.603-9.856,6.571c-1.643,3.989-0.747,8.576,2.304,11.627
l212.8,213.504c2.005,2.005,4.715,3.136,7.552,3.136s5.547-1.131,7.552-3.115l213.419-213.504
C479.793,292.501,480.71,287.915,479.046,283.925z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 793 B

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 426.672 426.672" style="enable-background:new 0 0 426.672 426.672;" xml:space="preserve">
<g>
<g>
<path d="M423.553,131.12l-128-128c-4.16-4.16-10.923-4.16-15.083,0L96.001,187.589L18.22,109.787
c-3.051-3.051-7.616-3.947-11.627-2.304c-3.989,1.643-6.592,5.547-6.592,9.856v298.667c0,5.888,4.779,10.667,10.667,10.667
h298.667c4.309,0,8.213-2.603,9.856-6.592c1.664-3.989,0.747-8.576-2.304-11.627l-77.803-77.781l184.448-184.448
C427.713,142.043,427.713,135.301,423.553,131.12z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 782 B

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 426.672 426.672" style="enable-background:new 0 0 426.672 426.672;" xml:space="preserve">
<g>
<g>
<path d="M420.069,107.483c-3.968-1.643-8.576-0.747-11.627,2.304l-77.781,77.803L146.213,3.12c-4.16-4.16-10.923-4.16-15.083,0
l-128,128c-4.16,4.16-4.16,10.923,0,15.083l184.448,184.448l-77.781,77.781c-3.051,3.051-3.968,7.637-2.304,11.627
s5.525,6.613,9.835,6.613h298.667c5.888,0,10.667-4.779,10.667-10.667V117.339C426.661,113.029,424.059,109.125,420.069,107.483z"
/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 771 B

13
_ext/svg/arrows/left.svg Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512.171 512.171" style="enable-background:new 0 0 512.171 512.171;" xml:space="preserve">
<g>
<g>
<path d="M501.504,160.047H234.837V42.714c0-4.309-2.603-8.192-6.592-9.856c-3.989-1.664-8.576-0.747-11.627,2.304L3.115,248.495
C1.109,250.501,0,253.21,0,256.047c0,2.837,1.131,5.547,3.115,7.552l213.504,213.419c2.048,2.048,4.779,3.115,7.552,3.115
c1.365,0,2.752-0.256,4.075-0.811c3.989-1.664,6.592-5.547,6.592-9.856V352.047h266.667c5.888,0,10.667-4.779,10.667-10.667
V170.714C512.171,164.826,507.413,160.047,501.504,160.047z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 831 B

View File

@ -0,0 +1 @@
<svg id="Layer_1" enable-background="new 0 0 506.1 506.1" height="512" viewBox="0 0 506.1 506.1" width="512" xmlns="http://www.w3.org/2000/svg"><path d="m489.609 0h-473.118c-9.108 0-16.491 7.383-16.491 16.491v473.118c0 9.107 7.383 16.491 16.491 16.491h473.119c9.107 0 16.49-7.383 16.49-16.491v-473.118c0-9.108-7.383-16.491-16.491-16.491z"/></svg>

After

Width:  |  Height:  |  Size: 346 B

13
_ext/svg/arrows/right.svg Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512.171 512.171" style="enable-background:new 0 0 512.171 512.171;" xml:space="preserve">
<g>
<g>
<path d="M509.035,248.212l-213.504-212.8c-3.051-3.029-7.595-3.904-11.627-2.304c-3.989,1.664-6.571,5.547-6.571,9.856v117.333
H10.667C4.779,160.298,0,165.076,0,170.964v170.667c0,5.888,4.779,10.667,10.667,10.667h266.667v116.885
c0,4.309,2.603,8.192,6.592,9.856c1.323,0.555,2.709,0.811,4.075,0.811c2.773,0,5.504-1.088,7.552-3.115l213.504-213.419
c2.005-2.005,3.115-4.715,3.115-7.552C512.171,252.927,511.04,250.218,509.035,248.212z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 837 B

13
_ext/svg/arrows/top.svg Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512.171 512.171" style="enable-background:new 0 0 512.171 512.171;" xml:space="preserve">
<g>
<g>
<path d="M476.723,216.64L263.305,3.115C261.299,1.109,258.59,0,255.753,0c-2.837,0-5.547,1.131-7.552,3.136L35.422,216.64
c-3.051,3.051-3.947,7.637-2.304,11.627c1.664,3.989,5.547,6.571,9.856,6.571h117.333v266.667c0,5.888,4.779,10.667,10.667,10.667
h170.667c5.888,0,10.667-4.779,10.667-10.667V234.837h116.885c4.309,0,8.192-2.603,9.856-6.592
C480.713,224.256,479.774,219.691,476.723,216.64z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 796 B

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 426.672 426.672" style="enable-background:new 0 0 426.672 426.672;" xml:space="preserve" transform = "scale(1, -1)">
<g>
<g>
<path d="M423.553,131.12l-128-128c-4.16-4.16-10.923-4.16-15.083,0L96.001,187.589L18.22,109.787
c-3.051-3.051-7.616-3.947-11.627-2.304c-3.989,1.643-6.592,5.547-6.592,9.856v298.667c0,5.888,4.779,10.667,10.667,10.667
h298.667c4.309,0,8.213-2.603,9.856-6.592c1.664-3.989,0.747-8.576-2.304-11.627l-77.803-77.781l184.448-184.448
C427.713,142.043,427.713,135.301,423.553,131.12z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 809 B

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 426.672 426.672" style="enable-background:new 0 0 426.672 426.672;" xml:space="preserve" transform = "scale(1, -1)">
<g>
<g>
<path d="M420.069,107.483c-3.968-1.643-8.576-0.747-11.627,2.304l-77.781,77.803L146.213,3.12c-4.16-4.16-10.923-4.16-15.083,0
l-128,128c-4.16,4.16-4.16,10.923,0,15.083l184.448,184.448l-77.781,77.781c-3.051,3.051-3.968,7.637-2.304,11.627
s5.525,6.613,9.835,6.613h298.667c5.888,0,10.667-4.779,10.667-10.667V117.339C426.661,113.029,424.059,109.125,420.069,107.483z"
/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 798 B

View File

@ -305,11 +305,17 @@ svg {
background: transparent;
}
#tmp-canvas {
z-index: 3;
#pixel-grid {
z-index: 5000;
background: transparent;
}
#tmp-canvas {
z-index: 4;
background: transparent;
}
#vfx-canvas {
z-index: -5000;
background: transparent;
@ -855,9 +861,11 @@ svg {
color: #c0bfc1;
}
#settings-container {
.settings-entry {
display: flex;
align-items: baseline;
margin-top:10px;
label {
flex: 1;
}
@ -865,6 +873,7 @@ svg {
width: 90px !important;
display: block;
box-sizing: border-box;
float:right;
}
}
@ -947,6 +956,211 @@ svg {
display: none;
}
#resize-canvas, #resize-sprite {
display:flex;
position:relative;
flex-wrap:wrap;
}
#pivot-menu {
position: relative;
display:inline-flex;
flex-wrap:wrap;
vertical-align:middle;
text-align:center;
width:130px;
float:left;
button {
margin-right:10px;
margin-bottom:10px;
position:relative;
width:32px;
height:32px;
background:$basehover;
border:none;
path {
fill:$basehovericon;
}
transition: background 100ms ease-in-out,
transform 100ms ease;
-webkit-appearance: none;
-moz-appearance: none;
}
button:hover,
button:focus,
button.rc-selected-pivot {
cursor:pointer;
background-color: $baseicon;
path {
fill:$basehovericonhover;
}
border: 2px solid color(base, foreground);
}
button:active {
transform: scale(0.95);
}
}
#borders-menu, #rc-size-menu, #rs-size-menu, #rs-percentage-menu {
display:flex;
position:relative;
flex-wrap: wrap;
width:250px;
font-size:15px;
left:10px;
text-align:center;
button {
background:$basehover;
border:none;
color: $basehovericon;
transition: background 100ms ease-in-out,
transform 100ms ease;
-webkit-appearance: none;
-moz-appearance: none;
}
button:hover,
button:focus {
cursor:pointer;
background-color: $baseicon;
color:$basehovericonhover;
border: 2px solid color(base, foreground);
}
button:active {
transform: scale(0.95);
}
input[type=number] {
position:relative;
margin-left:10px;
height:15px;
width:40px;
padding:8px;
}
input[type=number]::-webkit-outer-spin-button,
input[type=number]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type=number] {
-moz-appearance: textfield;
}
span {
padding-right:10px;
float:left;
position:relative;
vertical-align:middle;
height:40px;
width:100px;
display: inline-flex;
align-items: center;
}
}
#rs-percentage-menu {
width: 400px;
span {
width:150px;
}
}
#rs-size-menu {
width: 400px;
span {
width:150px;
}
}
#rs-percentage-menu, #rs-size-menu {
justify-content: center;
div {
float: none;
}
}
#borders-menu {
width:400px;
justify-content: center;
div {
float: none;
width: 330px;
padding-left:50px;
span {
padding-right:20px;
}
}
}
#rs-ratio-div {
width:400px;
justify-content: center;
padding-left:20px;
span {
width:400px;
justify-content: center;
}
select {
height:30px;
background-color: $basehover;
color: $basehovericon;
border:none;
position:relative;
left:10px;
option {
background-color: $basehover;
color:$basehovericon;
padding:5px;
}
option:checked, option:hover {
box-shadow: 0 0 10px 100px $basehovericon inset;
color: $basehovericonhover;
}
}
}
#rs-keep-ratio {
background: color(button);
border: none;
font-size:14px;
color: color(button, foreground);
padding: 10px 20px;
margin: 0 auto;
position:relative;
display: block;
}
#resize-canvas-confirm, #resize-sprite-confirm {
background: color(button);
border: none;
font-size:18px;
border-radius: 4px;
color: color(button, foreground);
padding: 10px 20px;
cursor: pointer;
margin: 0 auto;
position:relative;
top:10px;
display: block;
&:hover {
background: color(button, background, hover);
}
}
#compatibility-warning {
display: flex;
justify-content: center;

View File

@ -4,7 +4,6 @@ let currentPalette = [];
//input hex color string
//returns list item element
function addColor (newColor) {
//add # at beginning if not present
if (newColor.charAt(0) != '#')
newColor = '#' + newColor;
@ -19,13 +18,6 @@ function addColor (newColor) {
button.addEventListener('mouseup', clickedColor);
listItem.appendChild(button);
/*
//create input to hold color value
var colorValue = document.createElement("input");
colorValue.classList.add("color-value");
listItem.appendChild(colorValue);
*/
//insert new listItem element at the end of the colors menu (right before add button)
colorsMenu.insertBefore(listItem, colorsMenu.children[colorsMenu.children.length-1]);

View File

@ -44,7 +44,7 @@ on('click', 'add-color-button', function(){
//add new color and make it selected
var addedColor = addColor(newColor);
addedColor.classList.add('selected');
context.fillStyle = '#' + newColor;
currentLayer.context.fillStyle = '#' + newColor;
//add history state
//saveHistoryState({type: 'addcolor', colorValue: addedColor.firstElementChild.jscolor.toString()});

View File

@ -11,7 +11,7 @@ function changeZoom (layer, direction, cursorLocation) {
newHeight = canvasSize[1] * zoom;
//adjust canvas position
setCanvasOffset(layer.canvas, layer.canvas.offsetLeft + (oldWidth - newWidth) *cursorLocation[0]/oldWidth, layer.canvas.offsetTop + (oldHeight - newHeight) *cursorLocation[1]/oldWidth)
layer.setCanvasOffset(layer.canvas.offsetLeft + (oldWidth - newWidth) *cursorLocation[0]/oldWidth, layer.canvas.offsetTop + (oldHeight - newHeight) *cursorLocation[1]/oldWidth);
}
//if you want to zoom in
else if (direction == 'in' && zoom + Math.ceil(zoom/10) < window.innerHeight/4){
@ -20,7 +20,7 @@ function changeZoom (layer, direction, cursorLocation) {
newHeight = canvasSize[1] * zoom;
//adjust canvas position
setCanvasOffset(layer.canvas, layer.canvas.offsetLeft - Math.round((newWidth - oldWidth)*cursorLocation[0]/oldWidth), layer.canvas.offsetTop - Math.round((newHeight - oldHeight)*cursorLocation[1]/oldHeight))
layer.setCanvasOffset(layer.canvas.offsetLeft - Math.round((newWidth - oldWidth)*cursorLocation[0]/oldWidth), layer.canvas.offsetTop - Math.round((newHeight - oldHeight)*cursorLocation[1]/oldHeight));
}
//resize canvas

View File

@ -1,10 +1,19 @@
// This script contains all the functions used to manage the checkboard
// Checkerboard color 1
var firstCheckerBoardColor = 'rgba(179, 173, 182, 1)';
// Checkerboard color 2
var secondCheckerBoardColor = 'rgba(204, 200, 206, 1)';
// Square size for the checkerboard
var checkerBoardSquareSize = 16;
// Checkerboard canvas
var checkerBoardCanvas = document.getElementById('checkerboard');
// Setting current colour (each square has a different colour
var currentColor = firstCheckerBoardColor;
// Saving number of squares filled until now
var nSquaresFilled = 0;
function fillCheckerboard() {
// Getting checkerboard context
var context = checkerBoard.context;

View File

@ -8,7 +8,7 @@ function clickedColor (e){
if (selectedColor) selectedColor.classList.remove('selected');
//set current color
for (let i=1; i<layers.length - 2; i++) {
for (let i=1; i<layers.length - nAppLayers; i++) {
layers[i].context.fillStyle = this.style.backgroundColor;
}

View File

@ -87,7 +87,7 @@ function colorChanged(e) {
//if this is the current color, update the drawing color
if (e.target.colorElement.parentElement.classList.contains('selected')) {
for (let i=1; i<layers.length - 2; i++) {
for (let i=1; i<layers.length - nAppLayers; i++) {
layers[i].context.fillStyle = '#'+ rgbToHex(newColor.r,newColor.g,newColor.b);
}

View File

@ -6,9 +6,6 @@ let copiedStartY;
let copiedEndX;
let copiedEndY;
// BUG: when merging tmp layer to currentLayer there are offset problems
// FIX: maybe copy the entire tmp layer and paste it so that the merging happens at 0,0
function copySelection() {
copiedEndX = endX;
copiedEndY = endY;
@ -65,9 +62,4 @@ function cutSelectionTool() {
clipboardData = currentLayer.context.getImageData(startX, startY, endX - startX + 1, endY - startY + 1);
currentLayer.context.clearRect(startX - 0.5, startY - 0.5, endX - startX + 1, endY - startY + 1);
}
// Moving those pixels from the current layer to the tmp layer
//TMPLayer.context.putImageData(imageDataToMove, startX + 1, startY);
//originalDataPosition = [currentPos[0], currentPos[1]];
}

View File

@ -30,7 +30,5 @@ function line(x0,y0,x1,y1, brushSize) {
err +=dx;
y0+=sy;
}
console.log(x0 + ", " + x1);
}
}

View File

@ -26,11 +26,8 @@ function switchMode(currentMode, mustConfirm = true) {
//switch to basic mode
else {
//if there is a current layer (a document is active)
if (currentLayer) {
//confirm with user before flattening image
if (mustConfirm ) {
if (!confirm('Switching to basic mode will flatten all the visible layers. Are you sure you want to continue?')) {

View File

@ -2,7 +2,7 @@
function getCursorPosition(e) {
var x;
var y;
if (e.pageX != undefined && e.pageY != undefined) {
x = e.pageX;
y = e.pageY;
@ -12,28 +12,8 @@ function getCursorPosition(e) {
y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop;
}
x -= canvas.offsetLeft;
y -= canvas.offsetTop;
x -= currentLayer.canvas.offsetLeft;
y -= currentLayer.canvas.offsetTop;
return [x,y];
}
//get cursor position relative to canvas
function getCursorPositionRelative(e, layer) {
var x;
var y;
if (e.pageX != undefined && e.pageY != undefined) {
x = e.pageX;
y = e.pageY;
}
else {
x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop;
}
x -= layer.canvas.offsetLeft;
y -= layer.canvas.offsetTop;
return [x,y];
}
return [Math.round(x), Math.round(y)];
}

View File

@ -3,6 +3,76 @@ var redoStates = [];
const undoLogStyle = 'background: #87ff1c; color: black; padding: 5px;';
function HistoryStateResizeSprite(xRatio, yRatio, algo, oldData) {
this.xRatio = xRatio;
this.yRatio = yRatio;
this.algo = algo;
this.oldData = oldData;
this.undo = function() {
let layerIndex = 0;
currentAlgo = algo;
resizeSprite(null, [1 / this.xRatio, 1 / this.yRatio]);
// Also putting the old data
for (let i=0; i<layers.length; i++) {
if (layers[i].menuEntry != null) {
layers[i].context.putImageData(this.oldData[layerIndex], 0, 0);
layerIndex++;
layers[i].updateLayerPreview();
}
}
redoStates.push(this);
};
this.redo = function() {
currentAlgo = algo;
resizeSprite(null, [this.xRatio, this.yRatio]);
undoStates.push(this);
};
saveHistoryState(this);
}
function HistoryStateResizeCanvas(newSize, oldSize, imageDatas, trim) {
this.oldSize = oldSize;
this.newSize = newSize;
this.imageDatas = imageDatas;
this.trim = trim;
this.undo = function() {
let dataIndex = 0;
console.log("breakpoint");
// Resizing the canvas
resizeCanvas(null, oldSize, null, false);
// Putting the image datas
for (let i=0; i<layers.length; i++) {
if (layers[i].menuEntry != null) {
layers[i].context.putImageData(this.imageDatas[dataIndex], 0, 0);
dataIndex++;
}
}
redoStates.push(this);
};
this.redo = function() {
console.log("trim: " + this.trim);
if (!this.trim) {
resizeCanvas(null, newSize, null, false);
}
else {
trimCanvas(null, false);
}
undoStates.push(this);
};
saveHistoryState(this);
}
function HistoryStateFlattenVisible(flattened) {
this.nFlattened = flattened;
@ -62,7 +132,7 @@ function HistoryStateFlattenAll(nFlattened) {
this.nFlattened = nFlattened;
this.undo = function() {
for (let i=0; i<this.nFlattened - 2; i++) {
for (let i=0; i<this.nFlattened - nAppLayers; i++) {
undo();
}
@ -70,7 +140,7 @@ function HistoryStateFlattenAll(nFlattened) {
};
this.redo = function() {
for (let i=0; i<this.nFlattened - 2; i++) {
for (let i=0; i<this.nFlattened - nAppLayers; i++) {
redo();
}
@ -129,6 +199,27 @@ function HistoryStateRenameLayer(oldName, newName, layer) {
saveHistoryState(this);
}
function HistoryStateDuplicateLayer(addedLayer, copiedLayer) {
this.addedLayer = addedLayer;
this.copiedLayer = copiedLayer;
this.undo = function() {
addedLayer.selectLayer();
deleteLayer(false);
redoStates.push(this);
};
this.redo = function() {
copiedLayer.selectLayer();
duplicateLayer(null, false);
undoStates.push(this);
};
saveHistoryState(this);
}
function HistoryStateDeleteLayer(layerData, before, index) {
this.deleted = layerData;
this.before = before;
@ -209,10 +300,20 @@ function HistoryStateAddLayer(layerData, index) {
this.index = index;
this.undo = function() {
console.log("uo");
redoStates.push(this);
if (layers.length - nAppLayers > this.index + 1) {
layers[this.index + 1].selectLayer();
}
else {
layers[this.index - 1].selectLayer();
}
this.added.canvas.remove();
this.added.menuEntry.remove();
layers.splice(index, 1);
};

View File

@ -57,6 +57,8 @@ class Layer {
if (menuEntry != null) {
this.name = menuEntry.getElementsByTagName("p")[0].innerHTML;
menuEntry.id = "layer" + id;
menuEntry.onmouseover = () => this.hover();
menuEntry.onmouseout = () => this.unhover();
menuEntry.onclick = () => this.selectLayer();
menuEntry.getElementsByTagName("button")[0].onclick = () => this.toggleLock();
menuEntry.getElementsByTagName("button")[1].onclick = () => this.toggleVisibility();
@ -76,12 +78,13 @@ class Layer {
// 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];
@ -99,6 +102,24 @@ class Layer {
this.context.mozImageSmoothingEnabled = false;
}
hover() {
// Hide all the layers but the current one
for (let i=1; i<layers.length - nAppLayers; i++) {
if (layers[i] !== this) {
layers[i].canvas.style.opacity = 0.3;
}
}
}
unhover() {
// Show all the layers again
for (let i=1; i<layers.length - nAppLayers; i++) {
if (layers[i] !== this) {
layers[i].canvas.style.opacity = 1;
}
}
}
setID(id) {
this.id = id;
if (this.menuEntry != null) {
@ -160,13 +181,38 @@ class Layer {
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;
setCanvasOffset (offsetLeft, offsetTop) {
//horizontal offset
var minXOffset = -this.canvasSize[0] * zoom;
var maxXOffset = window.innerWidth - 300;
if (offsetLeft < minXOffset)
this.canvas.style.left = minXOffset +'px';
else if (offsetLeft > maxXOffset)
this.canvas.style.left = maxXOffset +'px';
else
this.canvas.style.left = offsetLeft +'px';
//vertical offset
var minYOffset = -this.canvasSize[1] * zoom + 164;
var maxYOffset = window.innerHeight - 100;
if (offsetTop < minYOffset)
this.canvas.style.top = minYOffset +'px';
else if (offsetTop > maxYOffset)
this.canvas.style.top = maxYOffset +'px';
else
this.canvas.style.top = offsetTop +'px';
}
// Copies the otherLayer's position and size
copyData(otherLayer) {
this.canvas.style.width = otherLayer.canvas.style.width;
this.canvas.style.height = otherLayer.canvas.style.height;
this.canvas.style.left = otherLayer.canvas.style.left;
this.canvas.style.top = otherLayer.canvas.style.top;
}
openOptionsMenu(event) {
@ -219,9 +265,10 @@ class Layer {
layer.menuEntry.classList.add("selected-layer");
currentLayer = layer;
}
/*
canvas = currentLayer.canvas;
context = currentLayer.context;
*/
}
toggleLock() {
@ -433,6 +480,55 @@ function deleteLayer(saveHistory = true) {
currentLayer.closeOptionsMenu();
}
function duplicateLayer(event, saveHistory = true) {
let layerIndex = layers.indexOf(currentLayer);
let toDuplicate = currentLayer;
let menuEntries = layerList.children;
// Increasing z-indexes of the layers above
for (let i=getMenuEntryIndex(menuEntries, toDuplicate.menuEntry) - 1; i>=0; i--) {
getLayerByID(menuEntries[i].id).canvas.style.zIndex++;
}
maxZIndex++;
// Creating a new canvas
let newCanvas = document.createElement("canvas");
// Setting up the new canvas
canvasView.append(newCanvas);
newCanvas.style.zIndex = parseInt(currentLayer.canvas.style.zIndex) + 1;
newCanvas.classList.add("drawingCanvas");
if (!layerListEntry) return console.warn('skipping adding layer because no document');
// Clone the default layer
let toAppend = currentLayer.menuEntry.cloneNode(true);
// Setting the default name for the layer
toAppend.getElementsByTagName('p')[0].innerHTML += " copy";
// Removing the selected class
toAppend.classList.remove("selected-layer");
// Adding the layer to the list
layerCount++;
// Creating a layer object
let newLayer = new Layer(currentLayer.canvasSize[0], currentLayer.canvasSize[1], newCanvas, toAppend);
newLayer.context.fillStyle = currentLayer.context.fillStyle;
newLayer.copyData(currentLayer);
layers.splice(layerIndex, 0, newLayer);
// Insert it before the Add layer button
layerList.insertBefore(toAppend, currentLayer.menuEntry);
// Copy the layer content
newLayer.context.putImageData(currentLayer.context.getImageData(
0, 0, currentLayer.canvasSize[0], currentLayer.canvasSize[1]), 0, 0);
newLayer.updateLayerPreview();
// Basically "if I'm not adding a layer because redo() is telling meto do so", then I can save the history
if (saveHistory) {
new HistoryStateDuplicateLayer(newLayer, currentLayer);
}
}
function renameLayer(event) {
let layerIndex = layers.indexOf(currentLayer);
let toRename = currentLayer;
@ -521,6 +617,16 @@ function moveLayers(toDropLayer, staticLayer, saveHistory = true) {
}
}
function getMenuEntryIndex(list, entry) {
for (let i=0; i<list.length; i++) {
if (list[i] === entry) {
return i;
}
}
return -1;
}
// Finds a layer given its name
function getLayerByName(name) {
for (let i=0; i<layers.length; i++) {
@ -558,8 +664,6 @@ function addLayer(id, saveHistory = true) {
newCanvas.style.zIndex = maxZIndex;
newCanvas.classList.add("drawingCanvas");
console.log("Tela creata: " + newCanvas);
if (!layerListEntry) return console.warn('skipping adding layer because no document');
// Clone the default layer
@ -575,8 +679,9 @@ function addLayer(id, saveHistory = true) {
let newLayer = new Layer(currentLayer.canvasSize[0], currentLayer.canvasSize[1], newCanvas, toAppend);
newLayer.context.fillStyle = currentLayer.context.fillStyle;
newLayer.copyData(currentLayer);
layers.splice(index, 0, newLayer);
layers.splice(index, 0, newLayer);
// Insert it before the Add layer button
layerList.insertBefore(toAppend, layerList.childNodes[0]);

View File

@ -1,5 +1,5 @@
var currentMouseEvent;
var lastMousePos;
var lastMouseMovePos;
//mousedown - start drawing
window.addEventListener("mousedown", function (mouseEvent) {
@ -12,7 +12,7 @@ window.addEventListener("mousedown", function (mouseEvent) {
//prevent right mouse clicks and such, which will open unwanted menus
//mouseEvent.preventDefault();
lastPos = getCursorPosition(mouseEvent);
lastMouseClickPos = getCursorPosition(mouseEvent);
dragging = true;
//left or right click ?
@ -157,16 +157,23 @@ window.addEventListener("mouseup", function (mouseEvent) {
}, false);
// TODO: Make it snap to the pixel grid
function setPreviewPosition(preview, cursor, size){
function setPreviewPosition(preview, size){
let toAdd = 0;
// This prevents the brush to be placed in the middle of pixels
if (size % 2 == 0) {
toAdd = 0.5;
}
preview.style.left = (
currentLayer.canvas.offsetLeft
+ Math.floor(cursor[0]/zoom) * zoom
- Math.floor(size / 2) * zoom
- Math.floor(size / 2) * zoom + toAdd
) + 'px';
preview.style.top = (
currentLayer.canvas.offsetTop
+ Math.floor(cursor[1]/zoom) * zoom
- Math.floor(size / 2) * zoom
- Math.floor(size / 2) * zoom + toAdd
) + 'px';
}
@ -176,205 +183,187 @@ function setPreviewPosition(preview, cursor, size){
//mouse is moving on canvas
window.addEventListener("mousemove", draw, false);
function draw (mouseEvent) {
lastMousePos = getCursorPosition(mouseEvent);
// Saving the event in case something else needs it
currentMouseEvent = mouseEvent;
if (!dialogueOpen)
{
lastMouseMovePos = getCursorPosition(mouseEvent);
// Saving the event in case something else needs it
currentMouseEvent = mouseEvent;
var cursorLocation = lastMousePos;
var cursorLocation = lastMouseMovePos;
//if a document hasnt yet been created or the current layer is locked, exit this function
if (!documentCreated || dialogueOpen || !currentLayer.isVisible || currentLayer.isLocked) return;
//if a document hasnt yet been created or the current layer is locked, exit this function
if (!documentCreated || dialogueOpen || !currentLayer.isVisible || currentLayer.isLocked) return;
// Moving brush preview
currentTool.moveBrushPreview(cursorLocation);
// Hiding eyedropper, will be shown if it's needed
eyedropperPreview.style.display = 'none';
eyedropperPreview.style.display = 'none';
if (currentTool.name == 'pencil') {
//hide brush preview outside of canvas / canvas view
if (mouseEvent.target.className == 'drawingCanvas'|| mouseEvent.target.className == 'drawingCanvas')
brushPreview.style.visibility = 'visible';
else
brushPreview.style.visibility = 'hidden';
if (currentTool.name == 'pencil') {
//move the brush preview
brushPreview.style.left = cursorLocation[0] + currentLayer.canvas.offsetLeft - tool.pencil.brushSize * zoom / 2 + 'px';
brushPreview.style.top = cursorLocation[1] + currentLayer.canvas.offsetTop - tool.pencil.brushSize * zoom / 2 + 'px';
//draw line to current pixel
if (dragging) {
if (mouseEvent.target.className == 'drawingCanvas' || mouseEvent.target.className == 'drawingCanvas') {
line(Math.floor(lastMouseClickPos[0]/zoom),Math.floor(lastMouseClickPos[1]/zoom),Math.floor(cursorLocation[0]/zoom),Math.floor(cursorLocation[1]/zoom), tool.pencil.brushSize);
lastMouseClickPos = cursorLocation;
}
}
//hide brush preview outside of canvas / canvas view
if (mouseEvent.target.className == 'drawingCanvas'|| mouseEvent.target.className == 'drawingCanvas')
brushPreview.style.visibility = 'visible';
else
brushPreview.style.visibility = 'hidden';
//get lightness value of color
var selectedColor = currentLayer.context.getImageData(Math.floor(cursorLocation[0]/zoom),Math.floor(cursorLocation[1]/zoom),1,1).data;
var colorLightness = Math.max(selectedColor[0],selectedColor[1],selectedColor[2])
//draw line to current pixel
if (dragging) {
if (mouseEvent.target.className == 'drawingCanvas' || mouseEvent.target.className == 'drawingCanvas') {
line(Math.floor(lastPos[0]/zoom),Math.floor(lastPos[1]/zoom),Math.floor(cursorLocation[0]/zoom),Math.floor(cursorLocation[1]/zoom), tool.pencil.brushSize);
lastPos = cursorLocation;
//for the darkest 50% of colors, change the brush preview to dark mode
if (colorLightness>127) brushPreview.classList.remove('dark');
else brushPreview.classList.add('dark');
currentLayer.updateLayerPreview();
}
// Decided to write a different implementation in case of differences between the brush and the eraser tool
else if (currentTool.name == 'eraser') {
//hide brush preview outside of canvas / canvas view
if (mouseEvent.target.className == 'drawingCanvas' || mouseEvent.target.className == 'drawingCanvas')
brushPreview.style.visibility = 'visible';
else
brushPreview.style.visibility = 'hidden';
//draw line to current pixel
if (dragging) {
if (mouseEvent.target.className == 'drawingCanvas' || mouseEvent.target.className == 'drawingCanvas') {
line(Math.floor(lastMouseClickPos[0]/zoom),Math.floor(lastMouseClickPos[1]/zoom),Math.floor(cursorLocation[0]/zoom),Math.floor(cursorLocation[1]/zoom), currentTool.brushSize);
lastMouseClickPos = cursorLocation;
}
}
currentLayer.updateLayerPreview();
}
else if (currentTool.name == 'rectangle')
{
//hide brush preview outside of canvas / canvas view
if (mouseEvent.target.className == 'drawingCanvas'|| mouseEvent.target.className == 'drawingCanvas')
brushPreview.style.visibility = 'visible';
else
brushPreview.style.visibility = 'hidden';
if (!isDrawingRect && dragging) {
startRectDrawing(mouseEvent);
}
else if (dragging){
updateRectDrawing(mouseEvent);
}
}
//get lightness value of color
var selectedColor = context.getImageData(Math.floor(cursorLocation[0]/zoom),Math.floor(cursorLocation[1]/zoom),1,1).data;
var colorLightness = Math.max(selectedColor[0],selectedColor[1],selectedColor[2])
//for the darkest 50% of colors, change the brush preview to dark mode
if (colorLightness>127) brushPreview.classList.remove('dark');
else brushPreview.classList.add('dark');
currentLayer.updateLayerPreview();
}
// Decided to write a different implementation in case of differences between the brush and the eraser tool
else if (currentTool.name == 'eraser') {
// Uses the same preview as the brush
//move the brush preview
brushPreview.style.left = cursorLocation[0] + canvas.offsetLeft - currentTool.brushSize * zoom / 2 + 'px';
brushPreview.style.top = cursorLocation[1] + canvas.offsetTop - currentTool.brushSize * zoom / 2 + 'px';
//hide brush preview outside of canvas / canvas view
if (mouseEvent.target.className == 'drawingCanvas' || mouseEvent.target.className == 'drawingCanvas')
brushPreview.style.visibility = 'visible';
else
brushPreview.style.visibility = 'hidden';
//draw line to current pixel
if (dragging) {
if (mouseEvent.target.className == 'drawingCanvas' || mouseEvent.target.className == 'drawingCanvas') {
line(Math.floor(lastPos[0]/zoom),Math.floor(lastPos[1]/zoom),Math.floor(cursorLocation[0]/zoom),Math.floor(cursorLocation[1]/zoom), currentTool.brushSize);
lastPos = cursorLocation;
}
}
currentLayer.updateLayerPreview();
}
else if (currentTool.name == 'rectangle')
{
//move the brush preview
brushPreview.style.left = cursorLocation[0] + currentLayer.canvas.offsetLeft - currentTool.brushSize * zoom / 2 + 'px';
brushPreview.style.top = cursorLocation[1] + currentLayer.canvas.offsetTop - currentTool.brushSize * zoom / 2 + 'px';
//hide brush preview outside of canvas / canvas view
if (mouseEvent.target.className == 'drawingCanvas'|| mouseEvent.target.className == 'drawingCanvas')
brushPreview.style.visibility = 'visible';
else
brushPreview.style.visibility = 'hidden';
if (!isDrawingRect && dragging) {
startRectDrawing(mouseEvent);
else if (currentTool.name == 'pan' && dragging) {
// Setting first layer position
layers[0].setCanvasOffset(layers[0].canvas.offsetLeft + (cursorLocation[0] - lastMouseClickPos[0]), layers[0].canvas.offsetTop + (cursorLocation[1] - lastMouseClickPos[1]));
// Copying that position to the other layers
for (let i=1; i<layers.length; i++) {
layers[i].copyData(layers[0]);
}
}
else if (dragging){
updateRectDrawing(mouseEvent);
else if (currentTool.name == 'eyedropper' && dragging && mouseEvent.target.className == 'drawingCanvas') {
let selectedColor = getEyedropperColor(cursorLocation);
eyedropperPreview.style.borderColor = '#'+rgbToHex(selectedColor[0],selectedColor[1],selectedColor[2]);
eyedropperPreview.style.display = 'block';
eyedropperPreview.style.left = cursorLocation[0] + currentLayer.canvas.offsetLeft - 30 + 'px';
eyedropperPreview.style.top = cursorLocation[1] + currentLayer.canvas.offsetTop - 30 + 'px';
var colorLightness = Math.max(selectedColor[0],selectedColor[1],selectedColor[2]);
//for the darkest 50% of colors, change the eyedropper preview to dark mode
if (colorLightness>127) eyedropperPreview.classList.remove('dark');
else eyedropperPreview.classList.add('dark');
}
else if (currentTool.name == 'resizebrush' && dragging) {
//get new brush size based on x distance from original clicking location
var distanceFromClick = cursorLocation[0] - lastMouseClickPos[0];
//var roundingAmount = 20 - Math.round(distanceFromClick/10);
//this doesnt work in reverse... because... it's not basing it off of the brush size which it should be
var brushSizeChange = Math.round(distanceFromClick/10);
var newBrushSize = tool.pencil.previousBrushSize + brushSizeChange;
//set the brush to the new size as long as its bigger than 1
tool.pencil.brushSize = Math.max(1,newBrushSize);
//fix offset so the cursor stays centered
tool.pencil.moveBrushPreview(lastMouseClickPos);
currentTool.updateCursor();
}
else if (currentTool.name == 'resizeeraser' && dragging) {
//get new brush size based on x distance from original clicking location
var distanceFromClick = cursorLocation[0] - lastMouseClickPos[0];
//var roundingAmount = 20 - Math.round(distanceFromClick/10);
//this doesnt work in reverse... because... it's not basing it off of the brush size which it should be
var eraserSizeChange = Math.round(distanceFromClick/10);
var newEraserSizeChange = tool.eraser.previousBrushSize + eraserSizeChange;
//set the brush to the new size as long as its bigger than 1
tool.eraser.brushSize = Math.max(1,newEraserSizeChange);
//fix offset so the cursor stays centered
tool.eraser.moveBrushPreview(lastMouseClickPos);
currentTool.updateCursor();
}
else if (currentTool.name == 'resizerectangle' && dragging) {
//get new brush size based on x distance from original clicking location
var distanceFromClick = cursorLocation[0] - lastMouseClickPos[0];
//var roundingAmount = 20 - Math.round(distanceFromClick/10);
//this doesnt work in reverse... because... it's not basing it off of the brush size which it should be
var rectangleSizeChange = Math.round(distanceFromClick/10);
var newRectangleSize = tool.rectangle.previousBrushSize + rectangleSizeChange;
//set the brush to the new size as long as its bigger than 1
tool.rectangle.brushSize = Math.max(1,newRectangleSize);
//fix offset so the cursor stays centered
tool.rectangle.moveBrushPreview(lastMouseClickPos);
currentTool.updateCursor();
}
else if (currentTool.name == 'rectselect') {
if (dragging && !isRectSelecting && mouseEvent.target.className == 'drawingCanvas') {
isRectSelecting = true;
startRectSelection(mouseEvent);
}
else if (dragging && isRectSelecting) {
updateRectSelection(mouseEvent);
}
else if (isRectSelecting) {
endRectSelection();
}
}
else if (currentTool.name == 'moveselection') {
// Updating the cursor (move if inside rect, cross if not)
currentTool.updateCursor();
// If I'm dragging, I move the preview
if (dragging && cursorInSelectedArea()) {
updateMovePreview(getCursorPosition(mouseEvent));
}
}
}
else if (currentTool.name == 'pan' && dragging) {
// Setting first layer position
setCanvasOffset(layers[0].canvas, layers[0].canvas.offsetLeft + (cursorLocation[0] - lastPos[0]), layers[0].canvas.offsetTop + (cursorLocation[1] - lastPos[1]));
// Copying that position to the other layers
for (let i=1; i<layers.length; i++) {
layers[i].copyData(layers[0]);
}
}
else if (currentTool.name == 'eyedropper' && dragging && mouseEvent.target.className == 'drawingCanvas') {
let selectedColor = getEyedropperColor(cursorLocation);
eyedropperPreview.style.borderColor = '#'+rgbToHex(selectedColor[0],selectedColor[1],selectedColor[2]);
eyedropperPreview.style.display = 'block';
eyedropperPreview.style.left = cursorLocation[0] + currentLayer.canvas.offsetLeft - 30 + 'px';
eyedropperPreview.style.top = cursorLocation[1] + currentLayer.canvas.offsetTop - 30 + 'px';
var colorLightness = Math.max(selectedColor[0],selectedColor[1],selectedColor[2]);
//for the darkest 50% of colors, change the eyedropper preview to dark mode
if (colorLightness>127) eyedropperPreview.classList.remove('dark');
else eyedropperPreview.classList.add('dark');
}
else if (currentTool.name == 'resizebrush' && dragging) {
//get new brush size based on x distance from original clicking location
var distanceFromClick = cursorLocation[0] - lastPos[0];
//var roundingAmount = 20 - Math.round(distanceFromClick/10);
//this doesnt work in reverse... because... it's not basing it off of the brush size which it should be
var brushSizeChange = Math.round(distanceFromClick/10);
var newBrushSize = tool.pencil.previousBrushSize + brushSizeChange;
//set the brush to the new size as long as its bigger than 1
tool.pencil.brushSize = Math.max(1,newBrushSize);
//fix offset so the cursor stays centered
brushPreview.style.left = lastPos[0] + currentLayer.canvas.offsetLeft - tool.pencil.brushSize * zoom / 2 + 'px';
brushPreview.style.top = lastPos[1] + currentLayer.canvas.offsetTop - tool.pencil.brushSize * zoom / 2 + 'px';
currentTool.updateCursor();
}
else if (currentTool.name == 'resizeeraser' && dragging) {
//get new brush size based on x distance from original clicking location
var distanceFromClick = cursorLocation[0] - lastPos[0];
//var roundingAmount = 20 - Math.round(distanceFromClick/10);
//this doesnt work in reverse... because... it's not basing it off of the brush size which it should be
var eraserSizeChange = Math.round(distanceFromClick/10);
var newEraserSizeChange = tool.eraser.previousBrushSize + eraserSizeChange;
//set the brush to the new size as long as its bigger than 1
tool.eraser.brushSize = Math.max(1,newEraserSizeChange);
//fix offset so the cursor stays centered
brushPreview.style.left = lastPos[0] + currentLayer.canvas.offsetLeft - tool.eraser.brushSize * zoom / 2 + 'px';
brushPreview.style.top = lastPos[1] + currentLayer.canvas.offsetTop - tool.eraser.brushSize * zoom / 2 + 'px';
currentTool.updateCursor();
}
else if (currentTool.name == 'resizerectangle' && dragging) {
//get new brush size based on x distance from original clicking location
var distanceFromClick = cursorLocation[0] - lastPos[0];
//var roundingAmount = 20 - Math.round(distanceFromClick/10);
//this doesnt work in reverse... because... it's not basing it off of the brush size which it should be
var rectangleSizeChange = Math.round(distanceFromClick/10);
var newRectangleSize = tool.rectangle.previousBrushSize + rectangleSizeChange;
//set the brush to the new size as long as its bigger than 1
tool.rectangle.brushSize = Math.max(1,newRectangleSize);
//fix offset so the cursor stays centered
brushPreview.style.left = lastPos[0] + currentLayer.canvas.offsetLeft - tool.rectangle.brushSize * zoom / 2 + 'px';
brushPreview.style.top = lastPos[1] + currentLayer.canvas.offsetTop - tool.rectangle.brushSize * zoom / 2 + 'px';
currentTool.updateCursor();
}
else if (currentTool.name == 'rectselect') {
if (dragging && !isRectSelecting && mouseEvent.target.className == 'drawingCanvas') {
isRectSelecting = true;
startRectSelection(mouseEvent);
}
else if (dragging && isRectSelecting) {
updateRectSelection(mouseEvent);
}
else if (isRectSelecting) {
endRectSelection();
}
}
else if (currentTool.name == 'moveselection') {
// Updating the cursor (move if inside rect, cross if not)
currentTool.updateCursor();
// If I'm dragging, I move the preview
if (dragging && cursorInSelectedArea()) {
updateMovePreview(getCursorPosition(mouseEvent));
}
}
}
//mousewheel scrroll
//mousewheel scroll
canvasView.addEventListener("wheel", function(mouseEvent){
if (currentTool.name == 'zoom' || mouseEvent.altKey) {
let mode;
if (mouseEvent.deltaY < 0){
mode = 'in';
}
else if (mouseEvent.deltaY > 0) {
mode = 'out';
}
// Changing zoom and position of the first layer
changeZoom(layers[0], mode, getCursorPosition(mouseEvent));
for (let i=1; i<layers.length; i++) {
// Copying first layer's data into the other layers
layers[i].copyData(layers[0]);
}
let mode;
if (mouseEvent.deltaY < 0){
mode = 'in';
}
else if (mouseEvent.deltaY > 0) {
mode = 'out';
}
// Changing zoom and position of the first layer
changeZoom(layers[0], mode, getCursorPosition(mouseEvent));
for (let i=1; i<layers.length; i++) {
// Copying first layer's data into the other layers
layers[i].copyData(layers[0]);
}
});

View File

@ -7,13 +7,12 @@ function newPixel (width, height, editorMode, fileContent = null) {
if (firstPixel) {
layerListEntry = layerList.firstElementChild;
// Setting up the current layer
currentLayer = new Layer(width, height, canvas, layerListEntry);
canvas.style.zIndex = 2;
currentLayer.canvas.style.zIndex = 2;
}
else {
let nLayers = layers.length;
for (let i=2; i < layers.length - 2; i++) {
for (let i=2; i < layers.length - nAppLayers; i++) {
let currentEntry = layers[i].menuEntry;
let associatedLayer;
@ -32,7 +31,7 @@ function newPixel (width, height, editorMode, fileContent = null) {
}
// Removing the old layers from the list
for (let i=2; i<nLayers - 2; i++) {
for (let i=2; i<nLayers - nAppLayers; i++) {
layers.splice(2, 1);
}
@ -40,9 +39,12 @@ function newPixel (width, height, editorMode, fileContent = null) {
layers[1] = new Layer(width, height, layers[1].canvas, layers[1].menuEntry);
currentLayer = layers[1];
canvas = currentLayer.canvas;
context = currentLayer.context;
canvas.style.zIndex = 2;
currentLayer.canvas.style.zIndex = 2;
// Updating canvas size
for (let i=0; i<nLayers; i++) {
layers[i].canvasSize = [width, height];
}
}
// Adding the checkerboard behind it
@ -52,7 +54,10 @@ function newPixel (width, height, editorMode, fileContent = null) {
VFXLayer = new Layer(width, height, VFXCanvas);
// Tmp layer to draw previews on
TMPLayer = new Layer(width, height, TMPCanvas);
TMPLayer = new Layer(width, height, TMPCanvas);
// Pixel grid
pixelGrid = new Layer(width, height, pixelGridCanvas);
canvasSize = currentLayer.canvasSize;
@ -65,6 +70,7 @@ function newPixel (width, height, editorMode, fileContent = null) {
layers.push(currentLayer);
layers.push(VFXLayer);
layers.push(TMPLayer);
layers.push(pixelGrid);
}
//remove current palette
@ -108,6 +114,7 @@ function newPixel (width, height, editorMode, fileContent = null) {
//fill background of canvas with bg color
fillCheckerboard();
fillPixelGrid();
//reset undo and redo states
undoStates = [];

View File

@ -105,4 +105,89 @@ function getElementAbsolutePosition(element) {
}
return [curleft,curtop];
}
function nearestNeighbor (src, dst) {
let pos = 0
for (let y = 0; y < dst.height; y++) {
for (let x = 0; x < dst.width; x++) {
const srcX = Math.floor(x * src.width / dst.width)
const srcY = Math.floor(y * src.height / dst.height)
let srcPos = ((srcY * src.width) + srcX) * 4
dst.data[pos++] = src.data[srcPos++] // R
dst.data[pos++] = src.data[srcPos++] // G
dst.data[pos++] = src.data[srcPos++] // B
dst.data[pos++] = src.data[srcPos++] // A
}
}
}
function bilinearInterpolation (src, dst) {
function interpolate (k, kMin, kMax, vMin, vMax) {
return Math.round((k - kMin) * vMax + (kMax - k) * vMin)
}
function interpolateHorizontal (offset, x, y, xMin, xMax) {
const vMin = src.data[((y * src.width + xMin) * 4) + offset]
if (xMin === xMax) return vMin
const vMax = src.data[((y * src.width + xMax) * 4) + offset]
return interpolate(x, xMin, xMax, vMin, vMax)
}
function interpolateVertical (offset, x, xMin, xMax, y, yMin, yMax) {
const vMin = interpolateHorizontal(offset, x, yMin, xMin, xMax)
if (yMin === yMax) return vMin
const vMax = interpolateHorizontal(offset, x, yMax, xMin, xMax)
return interpolate(y, yMin, yMax, vMin, vMax)
}
let pos = 0
for (let y = 0; y < dst.height; y++) {
for (let x = 0; x < dst.width; x++) {
const srcX = x * src.width / dst.width
const srcY = y * src.height / dst.height
const xMin = Math.floor(srcX)
const yMin = Math.floor(srcY)
const xMax = Math.min(Math.ceil(srcX), src.width - 1)
const yMax = Math.min(Math.ceil(srcY), src.height - 1)
dst.data[pos++] = interpolateVertical(0, srcX, xMin, xMax, srcY, yMin, yMax) // R
dst.data[pos++] = interpolateVertical(1, srcX, xMin, xMax, srcY, yMin, yMax) // G
dst.data[pos++] = interpolateVertical(2, srcX, xMin, xMax, srcY, yMin, yMax) // B
dst.data[pos++] = interpolateVertical(3, srcX, xMin, xMax, srcY, yMin, yMax) // A
}
}
}
function resizeImageData (image, width, height, algorithm) {
algorithm = algorithm || 'bilinear-interpolation'
let resize
switch (algorithm) {
case 'nearest-neighbor': resize = nearestNeighbor; break
case 'bilinear-interpolation': resize = bilinearInterpolation; break
default: throw new Error(`Unknown algorithm: ${algorithm}`)
}
const result = new ImageData(width, height)
resize(image, result)
return result
}
function getPixelPosition(index) {
let linearIndex = index / 4;
let x = linearIndex % layers[0].canvasSize[0];
let y = Math.floor(linearIndex / layers[0].canvasSize[0]);
return [Math.ceil(x), Math.ceil(y)];
}

52
js/_pixelGrid.js Normal file
View File

@ -0,0 +1,52 @@
let pixelGridColor = "#0000FF";
let lineDistance = 12;
let pixelGridVisible = false;
pixelGridCanvas = document.getElementById("pixel-grid");
function togglePixelGrid(event) {
let button = document.getElementById("toggle-pixelgrid-button");
pixelGridVisible = !pixelGridVisible;
if (pixelGridVisible) {
button.innerHTML = "Hide pixel grid";
pixelGridCanvas.style.display = "inline-block";
}
else {
button.innerHTML = "Show pixel grid";
pixelGridCanvas.style.display = "none";
}
}
function fillPixelGrid() {
let context = pixelGridCanvas.getContext("2d");
let originalSize = layers[0].canvasSize;
pixelGridCanvas.width = originalSize[0] * lineDistance;
pixelGridCanvas.height = originalSize[1] * lineDistance;
// OPTIMIZABLE, could probably be a bit more elegant
// Draw horizontal lines
for (let i=0; i<pixelGridCanvas.width / lineDistance; i++) {
context.strokeStyle = settings.pixelGridColour;
context.beginPath();
context.moveTo(i * lineDistance + 0.5, 0);
context.lineTo(i * lineDistance + 0.5, originalSize[1] * lineDistance);
context.stroke();
context.closePath();
}
// Draw vertical lines
for (let i=0; i<pixelGridCanvas.height / lineDistance; i++) {
context.beginPath();
context.moveTo(0, i * lineDistance + 0.5);
context.lineTo(originalSize[0] * lineDistance, i * lineDistance + 0.5);
context.stroke();
context.closePath();
}
if (!pixelGridVisible) {
pixelGridCanvas.style.display = 'none';
}
}

View File

@ -53,8 +53,6 @@ function endRectDrawing(mouseEvent) {
startRectY = tmp;
}
let hexColor = hexToRgb(currentLayer.context.fillStyle);
// Resetting this
isDrawingRect = false;
// Drawing the rect

276
js/_resizeCanvas.js Normal file
View File

@ -0,0 +1,276 @@
let resizeCanvasContainer = document.getElementById("resize-canvas");
let rcPivot = "middle";
let currentPivotObject;
let borders = {left: 0, right: 0, top: 0, bottom: 0};
function openResizeCanvasWindow() {
initResizeCanvasInputs();
showDialogue('resize-canvas');
}
function initResizeCanvasInputs() {
let buttons = document.getElementsByClassName("pivot-button");
for (let i=0; i<buttons.length; i++) {
buttons[i].addEventListener("click", changePivot);
if (buttons[i].getAttribute("value").includes("middle")) {
currentPivotObject = buttons[i];
}
}
document.getElementById("rc-width").value = layers[0].canvasSize[0];
document.getElementById("rc-height").value = layers[0].canvasSize[1];
document.getElementById("rc-border-left").addEventListener("change", rcChangedBorder);
document.getElementById("rc-border-right").addEventListener("change", rcChangedBorder);
document.getElementById("rc-border-top").addEventListener("change", rcChangedBorder);
document.getElementById("rc-border-bottom").addEventListener("change", rcChangedBorder);
document.getElementById("rc-width").addEventListener("change", rcChangedSize);
document.getElementById("rc-height").addEventListener("change", rcChangedSize);
document.getElementById("resize-canvas-confirm").addEventListener("click", resizeCanvas);
console.log("Pivot selezionato: " + currentPivotObject);
}
function rcChangedBorder(event) {
rcUpdateBorders();
document.getElementById("rc-width").value = parseInt(layers[0].canvasSize[0]) + borders.left + borders.right;
document.getElementById("rc-height").value = parseInt(layers[0].canvasSize[1]) + borders.top + borders.bottom;
}
function rcChangedSize(event) {
let widthOffset = Math.abs(document.getElementById("rc-width").value) - layers[0].canvasSize[0];
let heightOffset = Math.abs(document.getElementById("rc-height").value) - layers[0].canvasSize[1];
let left = Math.round(widthOffset / 2);
let right = widthOffset - left;
let top = Math.round(heightOffset / 2);
let bottom = heightOffset - top;
document.getElementById("rc-border-left").value = left;
document.getElementById("rc-border-right").value = right;
document.getElementById("rc-border-top").value = top;
document.getElementById("rc-border-bottom").value = bottom;
borders.left = left;
borders.right = right;
borders.top = top;
borders.bottom = bottom;
}
function resizeCanvas(event, size, customData, saveHistory = true) {
let imageDatas = [];
let leftOffset = 0;
let topOffset = 0;
let copiedDataIndex = 0;
// If I'm undoing and I'm not trimming, I manually put the values in the window
if (size != null && customData == null) {
document.getElementById("rc-width").value = size.x;
document.getElementById("rc-height").value = size.y;
rcChangedSize();
}
rcUpdateBorders();
// Save all imageDatas
for (let i=0; i<layers.length; i++) {
if (layers[i].menuEntry != null) {
imageDatas.push(layers[i].context.getImageData(0, 0, layers[0].canvasSize[0], layers[0].canvasSize[1]));
}
}
// Saving the history only if I'm not already undoing or redoing
if (saveHistory) {
// Saving history
new HistoryStateResizeCanvas(
{x: parseInt(layers[0].canvasSize[0]) + borders.left + borders.right,
y: parseInt(layers[0].canvasSize[1]) + borders.top + borders.bottom},
{x: layers[0].canvasSize[0],
y: layers[0].canvasSize[1]},
imageDatas.slice(), customData != null && saveHistory
);
console.log("salvata");
}
// Resize the canvases
for (let i=0; i<layers.length; i++) {
layers[i].canvasSize[0] = parseInt(layers[i].canvasSize[0]) + borders.left + borders.right;
layers[i].canvasSize[1] = parseInt(layers[i].canvasSize[1]) + borders.top + borders.bottom;
layers[i].canvas.width = layers[i].canvasSize[0];
layers[i].canvas.height = layers[i].canvasSize[1];
layers[i].resize();
layers[i].context.fillStyle = currentGlobalColor;
}
// Regenerate the checkerboard
fillCheckerboard();
fillPixelGrid();
// Put the imageDatas in the right position
switch (rcPivot)
{
case 'topleft':
leftOffset = 0;
topOffset = 0;
break;
case 'top':
leftOffset = (borders.left + borders.right) / 2;
topOffset = 0;
break;
case 'topright':
leftOffset = borders.left + borders.right;
topOffset = 0;
break;
case 'left':
leftOffset = 0;
topOffset = (borders.top + borders.bottom) / 2;
break;
case 'middle':
leftOffset = (borders.left + borders.right) / 2;
topOffset = (borders.top + borders.bottom) / 2;
break;
case 'right':
leftOffset = borders.left + borders.right;
topOffset = (borders.top + borders.bottom) / 2;
break;
case 'bottomleft':
leftOffset = 0;
topOffset = borders.top + borders.bottom;
break;
case 'bottom':
leftOffset = (borders.left + borders.right) / 2;
topOffset = borders.top + borders.bottom;
break;
case 'bottomright':
leftOffset = borders.left + borders.right;
topOffset = borders.top + borders.bottom;
break;
default:
console.log('Pivot does not exist, please report an issue at https://github.com/lospec/pixel-editor');
break;
}
for (let i=0; i<layers.length; i++) {
if (layers[i].menuEntry != null) {
if (customData == undefined) {
layers[i].context.putImageData(imageDatas[copiedDataIndex], leftOffset, topOffset);
}
else {
console.log("sgancio " + layers[i].canvasSize + ", [" +
customData[copiedDataIndex].width + "," + customData[copiedDataIndex].height
+ "]");
layers[i].context.putImageData(customData[copiedDataIndex], 0, 0);
}
layers[i].updateLayerPreview();
copiedDataIndex++;
}
}
closeDialogue();
}
function trimCanvas(event, saveHistory) {
let minY = Infinity;
let minX = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
let tmp;
let imageDatas = [];
let historySave = saveHistory == null;
let prevPivot = rcPivot;
rcPivot = "topleft";
console.log("debug");
for (let i=1; i<layers.length - nAppLayers; i++) {
let imageData = layers[i].context.getImageData(0, 0, layers[0].canvasSize[0], layers[0].canvasSize[1]);
let pixelPosition;
for (let i=imageData.data.length - 1; i>= 0; i-=4) {
if (!isPixelEmpty(
[imageData.data[i - 3], imageData.data[i - 2],
-imageData.data[i - 1], imageData.data[i]])) {
pixelPosition = getPixelPosition(i);
// max x
if (pixelPosition[0] > maxX) {
maxX = pixelPosition[0];
}
// min x
if (pixelPosition[0] < minX) {
minX = pixelPosition[0];
}
// max y
if (pixelPosition[1] > maxY) {
maxY = pixelPosition[1];
}
// min y
if (pixelPosition[1] < minY) {
minY = pixelPosition[1];
}
}
}
}
tmp = minY;
minY = maxY;
maxY = tmp;
minY = layers[0].canvasSize[1] - minY;
maxY = layers[0].canvasSize[1] - maxY;
borders.right = (maxX - layers[0].canvasSize[0]) + 1;
borders.left = -minX;
borders.top = maxY - layers[0].canvasSize[1] + 1;
borders.bottom = -minY;
// Saving the data
for (let i=0; i<layers.length; i++) {
if (layers[i].menuEntry != null) {
imageDatas.push(layers[i].context.getImageData(minX - 1, layers[i].canvasSize[1] - maxY, maxX-minX + 1, maxY-minY + 1));
}
}
console.log(imageDatas);
//console.log("sx: " + borders.left + "dx: " + borders.right + "top: " + borders.top + "btm: " + borders.bottom);
document.getElementById("rc-border-left").value = borders.left;
document.getElementById("rc-border-right").value = borders.right;
document.getElementById("rc-border-top").value = borders.top;
document.getElementById("rc-border-bottom").value = borders.bottom;
resizeCanvas(null, null, imageDatas.slice(), historySave);
// Resetting the previous pivot
rcPivot = prevPivot;
}
function rcUpdateBorders() {
// Getting input
borders.left = document.getElementById("rc-border-left").value;
borders.right = document.getElementById("rc-border-right").value;
borders.top = document.getElementById("rc-border-top").value;
borders.bottom = document.getElementById("rc-border-bottom").value;
// Validating input
borders.left == "" ? borders.left = 0 : borders.left = Math.round(parseInt(borders.left));
borders.right == "" ? borders.right = 0 : borders.right = Math.round(parseInt(borders.right));
borders.top == "" ? borders.top = 0 : borders.top = Math.round(parseInt(borders.top));
borders.bottom == "" ? borders.bottom = 0 : borders.bottom = Math.round(parseInt(borders.bottom));
}
function changePivot(event) {
rcPivot = event.target.getAttribute("value");
// Setting the selected class
currentPivotObject.classList.remove("rc-selected-pivot");
currentPivotObject = event.target;
currentPivotObject.classList.add("rc-selected-pivot");
}

239
js/_resizeSprite.js Normal file
View File

@ -0,0 +1,239 @@
let keepRatio = true;
let currentRatio;
let currentAlgo = 'nearest-neighbor';
let data = {width: 0, height: 0, widthPercentage: 100, heightPercentage: 100};
let startData = {width: 0, height:0, widthPercentage: 100, heightPercentage: 100};
function openResizeSpriteWindow() {
initResizeSpriteInputs();
currentRatio = layers[0].canvasSize[0] / layers[0].canvasSize[1];
data.width = layers[0].canvasSize[0];
data.height = layers[1].canvasSize[1];
startData.width = parseInt(data.width);
startData.height = parseInt(data.height);
startData.heightPercentage = 100;
startData.widthPercentage = 100;
showDialogue('resize-sprite');
}
function initResizeSpriteInputs() {
document.getElementById("rs-width").value = layers[0].canvasSize[0];
document.getElementById("rs-height").value = layers[0].canvasSize[1];
document.getElementById("rs-width-percentage").value = 100;
document.getElementById("rs-height-percentage").value = 100;
document.getElementById("rs-keep-ratio").checked = true;
document.getElementById("rs-width").addEventListener("change", changedWidth);
document.getElementById("rs-height").addEventListener("change", changedHeight);
document.getElementById("rs-width-percentage").addEventListener("change", changedWidthPercentage);
document.getElementById("rs-height-percentage").addEventListener("change", changedHeightPercentage);
document.getElementById("resize-sprite-confirm").addEventListener("click", resizeSprite);
document.getElementById("rs-keep-ratio").addEventListener("click", toggleRatio);
document.getElementById("resize-algorithm-combobox").addEventListener("change", changedAlgorithm);
}
function resizeSprite(event, ratio) {
let oldWidth, oldHeight;
let newWidth, newHeight;
let rsImageDatas = [];
let layerIndex = 0;
let imageDatasCopy = [];
oldWidth = layers[0].canvasSize[0];
oldHeight = layers[1].canvasSize[1];
rcPivot = "middle";
// Updating values if the user didn't press enter
switch (document.activeElement.id) {
case "rs-width-percentage":
changedWidthPercentage();
break;
case "rs-width":
changedWidth();
break;
case "rs-height-percentage":
changedHeightPercentage();
break;
case "rs-height":
changedHeight();
break;
default:
// In this case everything has been updated correctly
break;
}
if (ratio == null) {
newWidth = data.width;
newHeight = data.height;
}
else {
newWidth = layers[0].canvasSize[0] * ratio[0];
newHeight = layers[1].canvasSize[1] * ratio[1];
}
// Get all the image datas
for (let i=0; i<layers.length; i++) {
if (layers[i].menuEntry != null) {
rsImageDatas.push(layers[i].context.getImageData(
0, 0, layers[0].canvasSize[0], layers[0].canvasSize[1])
);
}
}
if (ratio == null) {
imageDatasCopy = rsImageDatas.slice();
new HistoryStateResizeSprite(newWidth / oldWidth, newHeight / oldHeight, currentAlgo, imageDatasCopy);
}
// Resizing the canvas
resizeCanvas(null, {x: newWidth, y: newHeight});
// Put the image datas on the new canvases
for (let i=0; i<layers.length; i++) {
if (layers[i].menuEntry != null) {
layers[i].context.putImageData(
resizeImageData(rsImageDatas[layerIndex], newWidth, newHeight, currentAlgo), 0, 0
);
layers[i].updateLayerPreview();
layerIndex++;
}
}
// Updating start values when I finish scaling the sprite
// OPTIMIZABLE? Can't I just assign data to startData? Is js smart enough to understand?
if (ratio == null) {
startData.width = data.width;
startData.height = data.height;
}
else {
startData.width = layers[0].canvasSize[0];
startData.height = layers[0].canvasSize[1];
}
startData.widthPercentage = 100;
startData.heightPercentage = 100;
closeDialogue();
}
function changedWidth(event) {
let oldValue = data.width;
let ratio;
let percentageRatio;
let newHeight, newHeightPerc, newWidthPerc;
data.width = event.target.value;
delta = data.width - oldValue;
ratio = data.width / oldValue;
newHeight = data.width / currentRatio;
newHeightPerc = (newHeight * 100) / startData.height;
newWidthPerc = (data.width * 100) / startData.width;
if (keepRatio) {
document.getElementById("rs-height").value = newHeight;
data.height = newHeight;
document.getElementById("rs-height-percentage").value = newHeightPerc;
data.heightPercentage = newHeightPerc;
}
document.getElementById("rs-width-percentage").value = newWidthPerc;
}
function changedHeight(event) {
let oldValue = 100;
let ratio;
let newWidth, newWidthPerc, newHeightPerc;
data.height = event.target.value;
delta = data.height - oldValue;
ratio = data.height / oldValue;
newWidth = data.height * currentRatio;
newWidthPerc = (newWidth * 100) / startData.width;
newHeightPerc = (data.height * 100) / startData.height;
if (keepRatio) {
document.getElementById("rs-width").value = newWidth;
data.width = newWidth;
document.getElementById("rs-width-percentage").value = newWidthPerc;
data.widthPercentage = newWidthPerc;
}
document.getElementById("rs-height-percentage").value = newHeightPerc;
data.heightPercentage = newHeightPerc;
}
function changedWidthPercentage(event) {
let oldValue = 100;
let ratio;
let newWidth, newHeight, newHeightPerc;
data.widthPercentage = event.target.value;
delta = data.widthPercentage - oldValue;
ratio = data.widthPercentage / oldValue;
console.log("old value: " + oldValue + ", ratio: " + ratio);
newHeight = startData.height * ratio;
newHeightPerc = data.widthPercentage / currentRatio;
newWidth = startData.width * ratio;
if (keepRatio) {
document.getElementById("rs-height-percentage").value = newHeightPerc;
data.heightPercentage = newHeightPerc;
document.getElementById("rs-height").value = newHeight
data.height = newHeight;
}
document.getElementById("rs-width").value = newWidth;
data.width = newWidth;
}
function changedHeightPercentage(event) {
let oldValue = data.heightPercentage;
let ratio;
let newHeight, newWidth, newWidthPerc;
data.heightPercentage = event.target.value;
delta = data.heightPercentage - oldValue;
ratio = data.heightPercentage / oldValue;
newWidth = startData.width * ratio;
newWidthPerc = data.heightPercentage * currentRatio;
newHeight = startData.height * ratio;
if (keepRatio) {
document.getElementById("rs-width-percentage").value = data.heightPercentage * currentRatio;
data.widthPercentage = newWidthPerc;
document.getElementById("rs-width").value = newWidth;
data.width = newWidth;
}
document.getElementById("rs-height").value = newHeight;
data.height = newHeight;
}
function toggleRatio(event) {
keepRatio = !keepRatio;
}
function changedAlgorithm(event) {
currentAlgo = event.target.value;
}

View File

@ -1,23 +0,0 @@
function setCanvasOffset (canvas, offsetLeft, offsetTop) {
//horizontal offset
var minXOffset = -canvasSize[0]*zoom+ 164;
var maxXOffset = window.innerWidth - 300;
if (offsetLeft < minXOffset)
canvas.style.left = minXOffset +'px';
else if (offsetLeft > maxXOffset)
canvas.style.left = maxXOffset +'px';
else
canvas.style.left = offsetLeft +'px';
//vertical offset
var minYOffset = -canvasSize[1]*zoom + 164;
var maxYOffset = window.innerHeight-100;
if (offsetTop < minYOffset)
canvas.style.top = minYOffset +'px';
else if (offsetTop > maxYOffset)
canvas.style.top = maxYOffset +'px';
else
canvas.style.top = offsetTop +'px';
}

View File

@ -14,7 +14,8 @@ if(!settingsFromCookie) {
enableBrushPreview: true, //unused - performance
enableEyedropperPreview: true, //unused - performance
numberOfHistoryStates: 20,
maxColorsOnImportedImage: 128
maxColorsOnImportedImage: 128,
pixelGridColour: '#0000FF'
};
}
else{
@ -35,6 +36,9 @@ on('click', 'save-settings', function (){
//save new settings to settings object
settings.numberOfHistoryStates = getValue('setting-numberOfHistoryStates');
settings.pixelGridColour = getValue('setting-pixelGridColour');
// Filling pixel grid again if colour changed
fillPixelGrid();
//save settings object to cookie
var cookieValue = JSON.stringify(settings);

View File

@ -64,6 +64,46 @@ class Tool {
//change cursor
this.updateCursor();
}
updateCursor () {
//switch to that tools cursor
canvasView.style.cursor = this.cursor || 'default';
//if the tool uses a brush preview, make it visible and update the size
if (this.brushPreview) {
//console.log('brush size',this.currentBrushSize)
brushPreview.style.display = 'block';
brushPreview.style.width = this.currentBrushSize * zoom + 'px';
brushPreview.style.height = this.currentBrushSize * zoom + 'px';
}
//show / hide eyedropper color preview
if (this.eyedropperPreview) eyedropperPreview.style.display = 'block';
else eyedropperPreview.style.display = 'none';
//moveSelection
if (currentTool.name == 'moveselection') {
if (cursorInSelectedArea()) {
canMoveSelection = true;
canvasView.style.cursor = 'move';
brushPreview.style.display = 'none';
}
else {
canvasView.style.cursor = 'crosshair';
}
}
}
moveBrushPreview(cursorLocation) {
let toSub = 0;
// Prevents the brush to be put in the middle of pixels
if (this.currentBrushSize % 2 == 0) {
toSub = 0.5;
}
brushPreview.style.left = (Math.ceil(cursorLocation[0] / zoom) * zoom + currentLayer.canvas.offsetLeft - this.currentBrushSize * zoom / 2 - zoom / 2 - toSub * zoom) + 'px';
brushPreview.style.top = (Math.ceil(cursorLocation[1] / zoom) * zoom + currentLayer.canvas.offsetTop - this.currentBrushSize * zoom / 2 - zoom / 2 - toSub * zoom) + 'px';
}
}

View File

@ -1,35 +0,0 @@
//set the correct cursor for the current tool
Tool.prototype.updateCursor = function () {
//console.log('updateCursor()', currentTool)
//switch to that tools cursor
canvasView.style.cursor = this.cursor || 'default';
//if the tool uses a brush preview, make it visible and update the size
if (this.brushPreview) {
//console.log('brush size',this.currentBrushSize)
brushPreview.style.display = 'block';
brushPreview.style.width = this.currentBrushSize * zoom + 'px';
brushPreview.style.height = this.currentBrushSize * zoom + 'px';
}
//show / hide eyedropper color preview
if (this.eyedropperPreview) eyedropperPreview.style.display = 'block';
else eyedropperPreview.style.display = 'none';
//moveSelection
if (currentTool.name == 'moveselection') {
if (cursorInSelectedArea()) {
canMoveSelection = true;
canvasView.style.cursor = 'move';
brushPreview.style.display = 'none';
}
else {
canvasView.style.cursor = 'crosshair';
}
}
}
/*global Tool, dragging, canvasView, brushPreview, canMoveSelection, cursorInSelectedArea, eyedropperPreview, zoom, currentTool */

View File

@ -1,21 +1,12 @@
//init variables
var canvasSize,zoom;
var canvasSize;
var zoom = 7;
var dragging = false;
var lastPos = [0,0];
var lastMouseClickPos = [0,0];
var dialogueOpen = false;
var documentCreated = false;
var pixelEditorMode;
// Checkerboard management
// Checkerboard color 1
var firstCheckerBoardColor = 'rgba(179, 173, 182, 1)';
// Checkerboard color 2
var secondCheckerBoardColor = 'rgba(204, 200, 206, 1)';
// Square size for the checkerboard
var checkerBoardSquareSize = 16;
// Checkerboard canvas
var checkerBoardCanvas = document.getElementById('checkerboard');
//common elements
var brushPreview = document.getElementById("brush-preview");
var eyedropperPreview = document.getElementById("eyedropper-preview");
@ -26,7 +17,6 @@ var popUpContainer = document.getElementById("pop-up-container");
// main canvas
var canvas = document.getElementById('pixel-canvas');
var context = canvas.getContext('2d');
var currentGlobalColor;
// Layers
@ -43,3 +33,13 @@ var VFXCanvas = document.getElementById('vfx-canvas');
var TMPLayer;
// TMP canvas
var TMPCanvas = document.getElementById('tmp-canvas');
// Pixel grid layer
var pixelGrid;
// Pixel grid canvas
var pixelGridCanvas;
// Index of the first layer the user can use in the layers array
var firstUserLayerIndex = 2;
// Number of layers that are only used by the editor
var nAppLayers = 3;

View File

@ -42,8 +42,11 @@
//=include _deleteColor.js
//=include _replaceAllOfColor.js
//=include _checkerboard.js
//=include _pixelGrid.js
//=include _layer.js
//=include _copyPaste.js
//=include _resizeCanvas.js
//=include _resizeSprite.js
/**load file**/
//=include _loadImage.js

View File

@ -2,6 +2,7 @@
new Tool('rectselect', {
cursor: 'crosshair',
brushPreview: true,
});

View File

@ -52,14 +52,24 @@
<li>
<button>Edit</button>
<ul>
<li><button id="resize-canvas-button" onclick = "openResizeCanvasWindow()">Resize canvas</button></li>
<li><button id="resize-sprite-button" onclick = "openResizeSpriteWindow()">Scale sprite</button></li>
<li><button onclick = "trimCanvas()">Trim canvas</button></li>
<li><button id="undo-button" class="disabled">Undo</button></li>
<li><button id="redo-button" class="disabled">Redo</button></li>
</ul>
</li>
<li>
<button>View</button>
<ul>
<li><button id="toggle-pixelgrid-button" onclick="togglePixelGrid()">Show pixel grid</button></li>
</ul>
</li>
<li>
<button id = "layer-button">Layer</button>
<ul>
<li><button onclick = "addLayer()">New layer</button></li>
<li><button onclick = "duplicateLayer()">Duplicate</button></li>
<li><button onclick = "renameLayer()">Rename</button></li>
<li><button onclick = "deleteLayer()">Delete</button></li>
<li><button onclick = "merge()">Merge below</button></li>
@ -81,13 +91,13 @@
<button>Editor</button>
<ul>
<li><button id="switch-mode-button">Switch to basic mode</button></li>
<li><button>Settings</button></li>
</ul>
</li>
<li>
<button>Help</button>
<ul>
<li><button>Settings</button></li>
<li><button>Help</button></li>
<li><button>About</button></li>
<li><button>Changelog</button></li>
@ -171,10 +181,13 @@
<ul id = "layer-properties-menu">
<li>
<button onclick = "deleteLayer()">Delete</button>
<button onclick = "renameLayer()">Rename</button>
</li>
<li>
<button onclick = "renameLayer()">Rename</button>
<button onclick = "duplicateLayer()">Duplicate</button>
</li>
<li>
<button onclick = "deleteLayer()">Delete</button>
</li>
<li>
<button onclick = "merge()">Merge below</button>
@ -187,6 +200,7 @@
</li>
</ul>
<!-- TOOL PREVIEWS -->
<div id="eyedropper-preview"></div>
<div id="brush-preview"></div>
@ -196,6 +210,7 @@
<canvas id = "tmp-canvas" class = "drawingCanvas"></canvas>
<canvas id="pixel-canvas" class = "drawingCanvas"></canvas>
<canvas id="checkerboard" class = "drawingCanvas"></canvas>
<canvas id="pixel-grid" class = "drawingCanvas"></canvas>
</div>
<div id="canvas-view-shadow"></div>
@ -217,7 +232,8 @@
{{svg "adjust.svg" width="20" height="20" }}
</div>
<div id="pop-up-container">
<div id="pop-up-container" id = "new-pixel-container">
<!-- NEW PIXEL -->
<div id="new-pixel">
<button class="close-button">{{svg "x.svg" width="20" height="20"}}</button>
<h1>New Pixel</h1>
@ -245,6 +261,116 @@
<button id="create-button" class="default">Create</button>
</div>
</div>
<!--SPRITE RESIZE-->
<div id = "resize-sprite">
<button class="close-button">{{svg "x.svg" width="20" height="20"}}</button>
<h1>Scale sprite</h1>
<!-- SIZE-->
<h2>New size</h2>
<span id = "rs-size-menu">
<div>
<span>
Width: <input id="rs-width" type="number" default="0" step="1"
value="{{#if border}}{{border}}{{else}}0{{/if}}" autocomplete="off"/>
</span>
<span>
Height: <input id="rs-height" default="0" step="1" type="number"
value="{{#if border}}{{border}}{{else}}0{{/if}}" autocomplete="off"/>
</span>
</div>
</span>
<!--BORDERS-->
<h2>Resize percentages</h2>
<span id = "rs-percentage-menu">
<div>
<span>
Width <input id="rs-width-percentage" type="number" default="0" step="1"
value="{{#if border}}{{border}}{{else}}0{{/if}}" autocomplete="off"/> %
</span>
<span>
Height <input id="rs-height-percentage" type="number" default="0" step="1"
value="{{#if border}}{{border}}{{else}}0{{/if}}" autocomplete="off"/> %
</span>
</div>
<div id = "rs-ratio-div">
<span>
Keep current ratio <input type = "checkbox" id = "rs-keep-ratio"/>
</span>
<span>
Scaling algorithm:
<select name = "resize-algorithm" id = "resize-algorithm-combobox">
<option value = "nearest-neighbor">Nearest neighbour</option>
<option value = "bilinear-interpolation">Bilinear</option>
</select>
</span>
</br>
<button id = "resize-sprite-confirm">Scale sprite</button>
</div>
</span>
</div>
<!--CANVAS RESIZE-->
<div id = "resize-canvas">
<button class="close-button">{{svg "x.svg" width="20" height="20"}}</button>
<h1>Resize canvas</h1>
<!--PIVOTS-->
<span id = "pivot-menu">
<button class="pivot-button" value="topleft">{{svg "arrows/topleft.svg" width="20" height="20"}}</button>
<button class="pivot-button" value="top">{{svg "arrows/top.svg" width="20" height="20"}}</button>
<button class="pivot-button" value="topright">{{svg "arrows/topright.svg" width="20" height="20"}}</button>
<button class="pivot-button" value="left">{{svg "arrows/left.svg" width="20" height="20"}}</button>
<button class="pivot-button rc-selected-pivot" value="middle">{{svg "arrows/middle.svg" width="20" height="20"}}</button>
<button class="pivot-button" value="right">{{svg "arrows/right.svg" width="20" height="20"}}</button>
<button class="pivot-button" value="bottomleft">{{svg "arrows/bottomleft.svg" width="20" height="20"}}</button>
<button class="pivot-button" value="bottom">{{svg "arrows/bottom.svg" width="20" height="20"}}</button>
<button class="pivot-button" value="bottomright">{{svg "arrows/bottomright.svg" width="20" height="20"}}</button>
</span>
<!-- SIZE-->
<span id = "rc-size-menu">
<h2>Size</h2>
<div>
<span>
Width: <input id="rc-width" type="number" default="0" step="1"
value="{{#if border}}{{border}}{{else}}0{{/if}}" autocomplete="off"/>
</span>
<span>
Height: <input id="rc-height" default="0" step="1" type="number"
value="{{#if border}}{{border}}{{else}}0{{/if}}" autocomplete="off"/>
</span>
</div>
</span>
<!--BORDERS-->
<span id = "borders-menu">
<h2>Borders offsets</h2>
<div>
<span>
Left: <input id="rc-border-left" type="number" default="0" step="1"
value="{{#if border}}{{border}}{{else}}0{{/if}}" autocomplete="off"/>
</span>
<span>
Right: <input id="rc-border-right" type="number" default="0" step="1"
value="{{#if border}}{{border}}{{else}}0{{/if}}" autocomplete="off"/>
</span>
<span>
Top: <input id="rc-border-top" type="number" default="0" step="1"
value="{{#if border}}{{border}}{{else}}0{{/if}}" autocomplete="off"/>
</span>
<span>
Bottom: <input id="rc-border-bottom" default="0" step="1" type="number"
value="{{#if border}}{{border}}{{else}}0{{/if}}" autocomplete="off"/>
</span>
</div>
<button id = "resize-canvas-confirm">Resize canvas</button>
</span>
</div>
<div id="help">
<button class="close-button">{{svg "x.svg" width="20" height="20"}}</button>
<h1>Help</h1>
@ -306,7 +432,15 @@
<h1>Settings</h1>
<div id="settings-container">
<label for="setting-numberOfHistoryStates">Number of History States</label> <input id="setting-numberOfHistoryStates" value="20" autocomplete="off" />
<h2>History</h2>
<div class = "settings-entry">
<label for="setting-numberOfHistoryStates">Number of History States</label> <input id="setting-numberOfHistoryStates" value="20" autocomplete="off" />
</div>
<h2>Pixel grid</h2>
<div class = "settings-entry">
<label for="setting-pixelGridColour">Colour of the pixel grid</label><input id="setting-pixelGridColour" value = "#0000FF" autocomplete="off"/>
</div>
</div>
<p id="cookies-disabled-warning">Your browsers cookies are disabled, settings will be lost upon closing this page.</p>