Merge pull request #75 from lospec/next-update

Lasso, Magic Wand, Ellipse
This commit is contained in:
Nicola 2022-02-04 22:18:32 +01:00 committed by GitHub
commit 7b6a67ac32
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
55 changed files with 1564 additions and 518 deletions

View File

@ -21,10 +21,12 @@ function copy_images(){
gulp.src('./images/*.ico').pipe(gulp.dest(BUILDDIR));
// Splash images
gulp.src('./images/Splash images/*.png').pipe(gulp.dest(BUILDDIR));
// Logs images
// Logs gifs
gulp.src('./images/Logs/*.gif').pipe(gulp.dest(BUILDDIR));
// Logs images
// Logs pngs
gulp.src('./images/Logs/*.png').pipe(gulp.dest(BUILDDIR));
// Tool tutorials
gulp.src('./images/ToolTutorials/*.gif').pipe(gulp.dest(BUILDDIR));
}
function copy_logs() {
@ -54,7 +56,7 @@ function compile_page(){
.pipe(handlebars({encoding: 'utf8', debug: true, bustCache: true})
.partials('./views/[!index]*.hbs').partials('./views/popups/*.hbs')
//.helpers({ svg: hb_svg })
.partials('./views/components/*.hbs')
.helpers('./helpers/**/*.js')
.data({
projectSlug: 'pixel-editor',

View File

@ -85,7 +85,7 @@
#canvas-view {
bottom: 0px;
left: 64px;
left: 48px;
right: 48px;
top: 48px;
cursor: default;
@ -97,7 +97,7 @@
box-shadow: inset 0px 0px 4px 0px rgba(0, 0, 0, 0.4);
position: fixed;
bottom: 0px;
left: 64px;
left: 48px;
right: 48px;
top: 48px;
display: block;

56
css/_components.scss Normal file
View File

@ -0,0 +1,56 @@
.checkbox-holder {
position:relative;
width:15px;
height:15px;
display:block;
.checkbox {
display: inline-block;
cursor: pointer;
padding-right: 1em;
.box {
color: $basetext;
background: darken($basecolor, 5%);
border: none;
padding: 0.5rem;
border-radius: 0.5rem;
box-sizing: border-box;
margin-right: 0.5rem;
display: inline-block;
vertical-align: middle;
border: solid 2px $basecolor;
pointer-events: none;
svg {
visibility: hidden;
width: 1em;
height: 1em;
display: block;
path {
fill: $basetext;
}
}
}
//checked
&.checked .box svg {
visibility: visible;
}
label {
display: inline-block;
pointer-events: none;
} //hover label or box
&:hover:not(.disabled){
border-color: $basecolor;
}
&.disabled {
cursor: not-allowed;
.box svg path {fill: $basetext;}
}
}
}

View File

@ -69,7 +69,7 @@
/*app title*/
/* app title */
.logo {
color: lighten($basecolor, 20%);
@ -83,3 +83,57 @@
#main-menu li.open, #main-menu li button:hover {
background: $basehover;
}
/* Editor info */
li#editor-info {
float:right;
height:100%;
display:flex;
align-items: center;
background-color: $basecolor;
color:$basetext;
ul {
background-color: $basecolor;
display:block;
position:relative;
top:0px;
padding-top:0px;
box-shadow: none;
padding-bottom: 0px;
li {
top:0px;
padding-top:0px;
display:inline;
padding-right:20px;
}
.checkbox-holder {
display: inline;
}
input {
margin-left:10px;
background-color:darken($basecolor, 6%);
box-shadow:none;
border:none;
vertical-align: middle;
border-radius:5px;
padding: 5px;
color:$basetext;
}
input[type=number] {
appearance:none;
-moz-appearance:textfield;
-webkit-appearance:text;
width:25px;
height:15px;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
}
}

View File

@ -3,6 +3,8 @@
#splash {
width:100% !important;
height:100%!important;
position:fixed;
margin-top:-20px;
background-color: #232125 !important;
opacity: 1 !important;
@ -155,11 +157,9 @@
.sp-template {
display: flex;
align-items: center;
height: 5em;
min-width: 5em;
text-transform: uppercase;
width:16%;
width:5.5em;
border-radius:5%;
margin-right:4%;
margin-top:4%;
@ -188,12 +188,11 @@
}
}
/*
.sp-template:before {
content:'';
float:left;
padding-top:100%;
}*/
}
#sp-newpixel {
-webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */

View File

@ -1,7 +1,7 @@
#tools-menu {
left: 0;
width: 64px;
width: 48px;
list-style-type: none;
top: 48px;
bottom: 0;
@ -10,17 +10,13 @@
background-color: $basecolor;
box-sizing: border-box;
position: fixed;
z-index: 1120;
z-index: 1108;
}
#tools-menu li {
position: relative;
}
#tools-menu li button:first-child {
text-align: center;
border: none;
@ -28,15 +24,18 @@
width: 100%;
padding: 0;
cursor: pointer;
height: 64px;
height: 48px;
z-index:0;
}
#tools-menu li button path {
#tools-menu li button path, #tools-menu li button ellipse {
fill: $baseicon;
stroke: $baseicon;
}
#tools-menu li:hover button:first-child path {
#tools-menu li:hover button:first-child path, #tools-menu li:hover button:first-child ellipse {
fill: $basehovericon;
stroke: $basehovericon;
}
@ -44,27 +43,21 @@
background: $baseselected !important;
}
#tools-menu li.selected button:first-child path {
#tools-menu li.selected button:first-child path, #tools-menu li.selected button:first-child ellipse {
fill: $baseselectedicon;
}
#tools-menu li.selected.expanded {
padding-bottom: 10px;
}
.tools-menu-sub-button {
text-align: center;
border: none;
background: none;
cursor: pointer;
width: 50%;
height: 22px;
display: none;
width: 22px !important;
height:24px !important;
display: inline-block;
line-height: 0;
overflow: hidden;
position: absolute;
bottom: 0;
position: relative;
path {
fill: $baseselectedicon !important;
@ -77,40 +70,96 @@
}
}
#tools-menu li button#brush-bigger-button,
#tools-menu li button#zoom-in-button,
#tools-menu li button#eraser-bigger-button,
#tools-menu li button#rectangle-bigger-button,
#tools-menu li button#ellipse-bigger-button,
#tools-menu li button#line-bigger-button {
left: 0;
#tools-menu .size-buttons {
position:absolute;
display: none;
height:48px;
left:8px;
z-index:-1;
background-color: $basecolor !important;
}
#tools-menu li button#brush-smaller-button,
#tools-menu li button#zoom-out-button,
#tools-menu li button#eraser-smaller-button,
#tools-menu li button#rectangle-smaller-button,
#tools-menu li button#ellipse-smaller-button,
#tools-menu li button#line-smaller-button {
right: 0;
#tools-menu .size-buttons button {
text-align: center;
border: none;
padding: 0;
position: relative;
height:24px;
background-color: $baseselected !important;
cursor: pointer;
display:inline-block;
line-height: 0;
overflow: hidden;
width:22px;
}
#tools-menu li.selected button#brush-bigger-button,
#tools-menu li.selected button#brush-smaller-button,
#tools-menu li.selected button#zoom-in-button,
#tools-menu li.selected button#zoom-out-button,
#tools-menu li.selected button#eraser-bigger-button,
#tools-menu li.selected button#eraser-smaller-button,
#tools-menu li.selected button#rectangle-bigger-button,
#tools-menu li.selected button#rectangle-smaller-button,
#tools-menu li.selected button#ellipse-bigger-button,
#tools-menu li.selected button#ellipse-smaller-button,
#tools-menu li.selected button#line-bigger-button,
#tools-menu li.selected button#line-smaller-button {
display: block;
#tools-menu .size-buttons button:hover {
background:$baseselected !important;
}
#tools-menu .size-buttons button:active {
background:$basehovericon !important;
}
#tools-menu li.selected .size-buttons {
display:inline-block;
}
#tools-menu li:hover {
background: $basehover;
}
@keyframes fadeIn {
to {
opacity: 1;
}
}
@keyframes fadeOut {
to {
opacity: 0;
}
}
.fade-in {
opacity: 0;
animation: fadeIn .1s forwards;
}
.fade-out {
animation: fadeOut .1s forwards;
}
.is-paused {
animation-play-state: paused;
}
/* Tool tutorial */
#tool-tutorial {
display:inline-block;
position:absolute;
margin-left:48px;
margin-top:48px;
background-color: $basehover;
color:$basetext;
font-size:14px;
width:22%;
border-radius:0 5px 5px 5px;
z-index:1000;
img {
width:100%;
}
h3 {
margin-left:20px;
margin-bottom:-5px;
}
}
#tool-tutorial:after {
border-left: 11px solid #222;
border-top: 8px solid transparent;
border-bottom: 8px solid transparent;
position: relative;
}

View File

@ -2,6 +2,7 @@
@import 'general';
@import 'zindex';
@import 'shake';
@import 'components';
@import 'help';
@import 'layers';
@import 'canvas';

Binary file not shown.

After

Width:  |  Height:  |  Size: 972 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 580 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 676 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 757 KiB

View File

@ -1,4 +1,4 @@
const featureToggles = (function featureTogglesModule() {
/*const featureToggles = (function featureTogglesModule() {
const ellipseToolLocalStorageKey = 'feature_ellipseTool';
@ -30,4 +30,4 @@ const featureToggles = (function featureTogglesModule() {
}
})();
*/

View File

@ -116,15 +116,22 @@ const Input = (() => {
case 77: case 109:
Events.emit("tool-shortcut", "rectselect");
break;
// TODO: [ELLIPSE] Decide on a shortcut to use. "s" was chosen without any in-team consultation.
// Lasso tool, q
case 81: case 113:
Events.emit("tool-shortcut", "lassoselect");
break;
// ellipse tool, s
case 83:
//Events.emit("tool-shortcut", "ellipse");
Events.emit("tool-shortcut", "ellipse");
break;
// rectangle tool, u
case 85:
Events.emit("tool-shortcut", "rectangle");
break;
// magic wand tool
case 87: case 119:
Events.emit("tool-shortcut", "magicwand");
break;
// Paste tool
case 86: case 118:
if (keyboardEvent.ctrlKey) {

73
js/InputComponents.js Normal file
View File

@ -0,0 +1,73 @@
const InputComponents = (() => {
setInputEvents();
function setInputEvents() {
// Make the checkboxes toggleable
let checkboxes = document.getElementsByClassName("checkbox");
for (let i=0; i<checkboxes.length; i++) {
Events.on("click", checkboxes[i], toggleBox);
}
}
function toggleBox(event) {
if (event.target.classList.contains("checked"))
event.target.classList.remove("checked");
else
event.target.classList.add("checked");
}
function createNumber(id, label) {
let element = document.createElement("label");
let inputEl = document.createElement("input");
inputEl.id = id;
inputEl.type = "number";
element.innerHTML = label;
element.appendChild(inputEl);
return element;
}
function createCheckbox(id, label) {
let element = document.createElement("div");
let inner = document.createElement("div");
let hiddenInput = document.createElement("input");
let box = document.createElement("div");
let labelEl = document.createElement("label");
labelEl.innerHTML = label;
box.innerHTML = '\
<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"\
width="405.272px" height="405.272px" viewBox="0 0 405.272 405.272" style="enable-background:new 0 0 405.272 405.272;"\
xml:space="preserve">\
<g>\
<path d="M393.401,124.425L179.603,338.208c-15.832,15.835-41.514,15.835-57.361,0L11.878,227.836\
c-15.838-15.835-15.838-41.52,0-57.358c15.841-15.841,41.521-15.841,57.355-0.006l81.698,81.699L336.037,67.064\
c15.841-15.841,41.523-15.829,57.358,0C409.23,82.902,409.23,108.578,393.401,124.425z"/>\
</g>\
</svg>';
element.className = "checkbox-holder";
inner.className = "checkbox";
hiddenInput.type = "hidden";
box.className = "box";
box.id = id;
inner.appendChild(labelEl);
inner.appendChild(hiddenInput);
inner.appendChild(box);
element.appendChild(inner);
}
function updated() {
setInputEvents();
}
return {
updated,
createCheckbox,
createNumber
}
})();

View File

@ -6,6 +6,7 @@ var tool = {};
class Tool {
name = "AbstractTool";
isSelected = false;
switchFunction = undefined;
// Cursor and brush size
cursorType = {};
@ -20,17 +21,70 @@ class Tool {
// HTML elements
mainButton = undefined;
biggerButton = undefined;
smallerButton = undefined;
brushPreview = document.getElementById("brush-preview");
// Tool tutorial
toolTutorial = document.getElementById("tool-tutorial");
tutorialTimer = undefined;
tutorialString = "";
constructor (name, options) {
this.name = name;
this.cursorType = options;
this.mainButton = document.getElementById(name + "-button");
this.biggerButton = document.getElementById(name + "-bigger-button");
this.smallerButton = document.getElementById(name + "-smaller-button");
if (this.mainButton != undefined) {
// Timer to show the tutorial
Events.on("mouseenter", this.mainButton, function(){
this.setTutorial();
this.tutorialTimer = setTimeout(this.showTutorial.bind(this), 750)
}.bind(this));
// Clear the callback if the user cancels the hovering
Events.on("mouseleave", this.mainButton, function() {
if (this.tutorialTimer != undefined)
clearTimeout(this.tutorialTimer);
this.tutorialTimer = undefined;
this.hideTutorial();
}.bind(this))
this.hideTutorial();
}
}
showTutorial() {
let tutorialRect = this.toolTutorial.getBoundingClientRect();
if ((this.mainButton.getBoundingClientRect().top - 48 + (tutorialRect.bottom - tutorialRect.top)) > window.innerHeight) {
this.toolTutorial.style.top = window.innerHeight - 48 - (tutorialRect.bottom - tutorialRect.top) + "px";
}
else {
this.toolTutorial.style.top = this.mainButton.getBoundingClientRect().top - 48 + "px";
}
this.toolTutorial.className = "fade-in";
}
hideTutorial() {
this.toolTutorial.className = "fade-out";
}
resetTutorial() {
this.tutorialString = "";
}
setTutorial() {
this.toolTutorial.innerHTML = this.tutorialString;
}
addTutorialKey(key, text) {
this.tutorialString += '<li><span class="keyboard-key">' + key + '</span> ' + text + '</li>';
}
addTutorialText(key, text) {
this.tutorialString += '<li>' + key + ': ' + text + '</li>';
}
addTutorialImg(imgPath) {
this.tutorialString += '</ul><img src="' + imgPath + '"/>';
}
addTutorialTitle(text) {
this.tutorialString += "<h3>" + text + "</h3><ul>";
}
onSelect() {
@ -38,6 +92,7 @@ class Tool {
this.mainButton.parentElement.classList.add("selected");
this.isSelected = true;
// Update the cursor
switch (this.cursorType.type) {
case 'html':
currFile.canvasView.style.cursor = 'none';
@ -49,6 +104,9 @@ class Tool {
default:
break;
}
// Reset the topbar
TopMenuModule.resetInfos();
}
updateCursor() {
@ -95,32 +153,14 @@ class Tool {
currFile.canvasView.style.cursor = 'default';
}
onStart(mousePos) {
onStart(mousePos, mouseTarget) {
this.startMousePos = mousePos;
}
onDrag(mousePos) {
onDrag(mousePos, mouseTarget) {
}
onEnd(mousePos) {
onEnd(mousePos, mouseTarget) {
this.endMousePos = mousePos;
}
increaseSize() {
if (this.currSize < 128) {
this.currSize++;
this.updateCursor();
}
}
decreaseSize() {
if (this.currSize > 1) {
this.currSize--;
this.updateCursor();
}
}
get size() {
return this.currSize;
}
}

View File

@ -6,6 +6,7 @@ const ToolManager = (() => {
tools["eraser"] = new EraserTool("eraser", {type: 'html'}, switchTool);
tools["rectangle"] = new RectangleTool("rectangle", {type: 'html'}, switchTool);
tools["line"] = new LineTool("line", {type: 'html'}, switchTool);
tools["ellipse"] = new EllipseTool("ellipse", {type: 'cursor', style: 'crosshair'}, switchTool);
tools["fill"] = new FillTool("fill", {type: 'cursor', style: 'crosshair'}, switchTool);
tools["eyedropper"] = new EyeDropperTool("eyedropper", {type: 'cursor', style: 'crosshair'}, switchTool);
@ -16,6 +17,10 @@ const ToolManager = (() => {
{type:'cursor', style:'crosshair'}, switchTool, tools["brush"]);
tools["rectselect"] = new RectangularSelectionTool("rectselect",
{type: 'cursor', style:'crosshair'}, switchTool, tools["moveselection"]);
tools["lassoselect"] = new LassoSelectionTool("lassoselect",
{type: 'cursor', style:'crosshair'}, switchTool, tools["moveselection"]);
tools["magicwand"] = new MagicWandTool("magicwand",
{type: 'cursor', style:'crosshair'}, switchTool, tools["moveselection"]);
currTool = tools["brush"];
currTool.onSelect();
@ -26,6 +31,9 @@ const ToolManager = (() => {
Events.on("mousedown", window, onMouseDown);
Events.on("wheel", window, onMouseWheel);
// Assign a selection tool to the move tool
tools["moveselection"].selectionTool = tools["lassoselect"];
// Bind tool shortcuts
Events.onCustom("tool-shortcut", onShortcut);
@ -108,7 +116,7 @@ const ToolManager = (() => {
}
function onMouseUp(mouseEvent) {
if (!EditorState.documentCreated())
if (!EditorState.documentCreated || Dialogue.isOpen())
return;
let mousePos = Input.getCursorPosition(mouseEvent);
@ -122,11 +130,11 @@ const ToolManager = (() => {
tools["eyedropper"].onEnd(mousePos, mouseEvent.target);
}
else if (!currFile.currentLayer.isLocked || !((Object.getPrototypeOf(currTool) instanceof DrawingTool))) {
currTool.onEnd(mousePos);
currTool.onEnd(mousePos, mouseEvent.target);
}
break;
case 2:
tools["pan"].onEnd(mousePos);
tools["pan"].onEnd(mousePos, mouseEvent.target);
break;
case 3:
currTool.onRightEnd(mousePos, mouseEvent.target);
@ -141,10 +149,13 @@ const ToolManager = (() => {
return currTool;
}
function switchTool(newTool) {
function switchTool(newTool, event) {
currTool.onDeselect();
currTool = newTool;
currTool.onSelect();
if (event != undefined)
event.stopPropagation();
}
return {

View File

@ -1,12 +1,15 @@
const TopMenuModule = (() => {
const mainMenuItems = document.getElementById('main-menu').children;
let infoList = document.getElementById('editor-info');
let infoElements = {};
initMenu();
function initMenu() {
//for each button in main menu (starting at 1 to avoid logo)
for (let i = 1; i < mainMenuItems.length; i++) {
// for each button in main menu (starting at 1 to avoid logo), ending at length-1 to avoid
// editor info
for (let i = 1; i < mainMenuItems.length-1; i++) {
//get the button that's in the list item
const menuItem = mainMenuItems[i];
@ -18,6 +21,7 @@ const TopMenuModule = (() => {
closeMenu();
// Select the item
Util.select(e.target.parentElement);
e.stopPropagation();
});
const subMenu = menuItem.children[1];
@ -53,18 +57,29 @@ const TopMenuModule = (() => {
e.preventDefault();
}
break;
// REFACTOR: move the binding to the Selection IIFE or something like that once it's done
case 'Paste':
Events.on('click', currSubmenuButton, function(){Events.emit("ctrl+v");});
Events.on('click', currSubmenuButton, function(e){
Events.emit("ctrl+v");
e.stopPropagation();
});
break;
case 'Copy':
Events.on('click', currSubmenuButton, function(){Events.emit("ctrl+c");});
Events.on('click', currSubmenuButton, function(e){
Events.emit("ctrl+c");
e.stopPropagation();
});
break;
case 'Cut':
Events.on('click', currSubmenuButton, function(){Events.emit("ctrl+x");});
Events.on('click', currSubmenuButton, function(e){
Events.emit("ctrl+x");
e.stopPropagation();
});
break;
case 'Cancel':
Events.on('click', currSubmenuButton, function(){Events.emit("esc-pressed")});
Events.on('click', currSubmenuButton, function(e){
Events.emit("esc-pressed");
e.stopPropagation();
});
break;
//Help Menu
case 'Settings':
@ -93,7 +108,27 @@ const TopMenuModule = (() => {
}
}
function resetInfos() {
infoList.innerHTML = "<ul></ul>";
}
function updateField(fieldId, value) {
document.getElementById(fieldId).value = value;
}
function addInfoElement(fieldId, field) {
let liEl = document.createElement("li");
infoElements[fieldId] = field;
liEl.appendChild(field);
infoList.firstChild.appendChild(liEl);
}
return {
closeMenu
closeMenu,
updateField,
addInfoElement,
resetInfos
}
})();

View File

@ -1,6 +1,37 @@
// Acts as a public static class
class Util {
/** Pastes the source image data on the destination image data, keeping the pixels where the
* source image data is transparent
*
* @param {*} source
* @param {*} destination
*/
static pasteData(underlyingImageData, pasteData, finalContext) {
for (let i=0; i<underlyingImageData.data.length; i+=4) {
let currentMovePixel = [
pasteData.data[i], pasteData.data[i+1], pasteData.data[i+2], pasteData.data[i+3]
];
let currentUnderlyingPixel = [
underlyingImageData.data[i], underlyingImageData.data[i+1],
underlyingImageData.data[i+2], underlyingImageData.data[i+3]
];
// If the pixel of the clipboard is empty, but the one below it isn't, I use the pixel below
if (Util.isPixelEmpty(currentMovePixel)) {
if (!Util.isPixelEmpty(currentUnderlyingPixel)) {
pasteData.data[i] = currentUnderlyingPixel[0];
pasteData.data[i+1] = currentUnderlyingPixel[1];
pasteData.data[i+2] = currentUnderlyingPixel[2];
pasteData.data[i+3] = currentUnderlyingPixel[3];
}
}
}
finalContext.putImageData(pasteData, 0, 0);
}
/** Tells if a pixel is empty (has alpha = 0)
*
* @param {*} pixel
@ -86,4 +117,17 @@ class Util {
static toggle(elementId) {
Util.getElement(elementId).classList.toggle('selected');
}
static getPixelColor(data, x, y, dataWidth) {
let pos = (y * dataWidth + x) * 4;
return [data[pos], data[pos+1], data[pos+2], data[pos + 3]];
}
static isPixelTransparent(data, x, y, dataWidth) {
return this.getPixelColor(data, x, y, dataWidth)[3] == 255;
}
static cursorInCanvas(canvasSize, mousePos) {
return mousePos[0] >= 0 && mousePos[1] >= 0 && canvasSize[0] > mousePos[0] && canvasSize[1] > mousePos[1];
}
}

View File

@ -283,7 +283,7 @@ class Layer {
previewWidth, previewHeight);
}
drawLine(x0,y0,x1,y1, brushSize) {
drawLine(x0,y0,x1,y1, brushSize, clear=false) {
var dx = Math.abs(x1-x0);
var dy = Math.abs(y1-y0);
var sx = (x0 < x1 ? 1 : -1);
@ -293,12 +293,13 @@ class Layer {
while (true) {
//set pixel
// If the current tool is the brush
if (ToolManager.currentTool().name == 'brush' || ToolManager.currentTool().name == 'rectangle' || ToolManager.currentTool().name == 'ellipse') {
// REFACTOR: this is terrible
if (!clear) {
// I fill the rect
currFile.currentLayer.context.fillRect(x0-Math.floor(brushSize/2), y0-Math.floor(brushSize/2), brushSize, brushSize);
} else if (ToolManager.currentTool().name == 'eraser') {
this.context.fillRect(x0-Math.floor(brushSize/2), y0-Math.floor(brushSize/2), brushSize, brushSize);
} else {
// In case I'm using the eraser I must clear the rect
currFile.currentLayer.context.clearRect(x0-Math.floor(brushSize/2), y0-Math.floor(brushSize/2), brushSize, brushSize);
this.context.clearRect(x0-Math.floor(brushSize/2), y0-Math.floor(brushSize/2), brushSize, brushSize);
}
//if we've reached the end goal, exit the loop

View File

@ -82,8 +82,6 @@ class PixelGrid extends Layer {
this.context.strokeStyle = Settings.getCurrSettings().pixelGridColour;
console.log("Line ditance: " + this.lineDistance)
// OPTIMIZABLE, could probably be a bit more elegant
// Draw horizontal lines
for (let i=0; i<this.canvas.width / Math.round(this.lineDistance); i++) {

View File

@ -9,11 +9,16 @@
/** UTILITY AND INPUT **/
//=include Util.js
//=include Events.js
//=include InputComponents.js
//=include Dialogue.js
//=include History.js
//=include Settings.js
//=include EditorState.js
/** MENUS **/
//=include FileManager.js
//=include TopMenuModule.js
/** COLOR-RELATED **/
//=include Color.js
//=include ColorPicker.js
@ -37,12 +42,15 @@
//=include tools/EraserTool.js
//=include tools/LineTool.js
//=include tools/RectangleTool.js
//=include tools/EllipseTool.js
//=include tools/FillTool.js
//=include tools/EyeDropperTool.js
//=include tools/PanTool.js
//=include tools/ZoomTool.js
//=include tools/RectangularSelectionTool.js
//=include tools/MoveSelectionTool.js
//=include tools/RectangularSelectionTool.js
//=include tools/LassoSelectionTool.js
//=include tools/MagicWandTool.js
/** MODULES AND MENUS **/
//=include SplashPage.js
@ -53,8 +61,6 @@
/** STARTUP AND FILE MANAGEMENT **/
//=include Startup.js
//=include FileManager.js
//=include TopMenuModule.js
/** HTML INPUT EVENTS **/
//=include Input.js
@ -67,9 +73,10 @@ PresetModule.instrumentPresetMenu();
//when the page is done loading, you can get ready to start
window.onload = function () {
featureToggles.onLoad();
// First cursor update
ToolManager.currentTool().updateCursor();
// Apply checkboxes
//check if there are any url parameters
if (window.location.pathname.replace('/pixel-editor/','').length <= 1) {

View File

@ -5,6 +5,14 @@ class BrushTool extends ResizableTool {
Events.on('click', this.mainButton, switchFunction, this);
Events.on('click', this.biggerButton, this.increaseSize.bind(this));
Events.on('click', this.smallerButton, this.decreaseSize.bind(this));
this.resetTutorial();
this.addTutorialTitle("Pencil tool");
this.addTutorialKey("B", " to select the brush");
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");
}
onStart(mousePos, cursorTarget) {
@ -37,11 +45,11 @@ class BrushTool extends ResizableTool {
currFile.currentLayer.drawLine(Math.floor(this.prevMousePos[0]/currFile.zoom),
Math.floor(this.prevMousePos[1]/currFile.zoom),
Math.floor(this.currMousePos[0]/currFile.zoom),
Math.floor(this.currMousePos[1]/currFile.zoom),
Math.floor(this.currMousePos[1]/currFile.zoom),
this.currSize
);
}
currFile.currentLayer.updateLayerPreview();
}

188
js/tools/EllipseTool.js Normal file
View File

@ -0,0 +1,188 @@
class EllipseTool extends ResizableTool {
// Saving the empty rect svg
emptyEllipseSVG = document.getElementById("ellipse-empty-button-svg");
// and the full rect svg so that I can change them when the user changes rect modes
fullEllipseSVG = document.getElementById("ellipse-full-button-svg");
// Current fill mode
currFillMode = 'empty';
filledPixels = {};
switchFunction = null;
constructor(name, options, switchFunction) {
super(name, options);
this.switchFunction = switchFunction;
Events.on('click', this.mainButton, this.changeFillType.bind(this));
Events.on('click', this.biggerButton, this.increaseSize.bind(this));
Events.on('click', this.smallerButton, this.decreaseSize.bind(this));
this.resetTutorial();
this.addTutorialTitle("Ellipse tool");
this.addTutorialKey("S", " to select the ellipse");
this.addTutorialKey("S while selected", " to change fill mode (empty or fill)");
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");
}
changeFillType() {
if (this.isSelected)
if (this.currFillMode == 'empty') {
this.currFillMode = 'fill';
this.emptyEllipseSVG.setAttribute('display', 'none');
this.fullEllipseSVG.setAttribute('display', 'visible');
}
else {
this.currFillMode = 'empty'
this.emptyEllipseSVG.setAttribute('display', 'visible');
this.fullEllipseSVG.setAttribute('display', 'none');
}
else
this.switchFunction(this);
}
onStart(mousePos, mouseTarget) {
super.onStart(mousePos);
if (mouseTarget.className != "drawingCanvas")
return;
// Putting the tmp layer on top of everything
currFile.TMPLayer.canvas.style.zIndex = parseInt(currFile.currentLayer.canvas.style.zIndex, 10) + 1;
this.startMousePos[0] = Math.floor(mousePos[0] / currFile.zoom) + 0.5;
this.startMousePos[1] = Math.floor(mousePos[1] / currFile.zoom) + 0.5;
new HistoryState().EditCanvas();
}
onDrag(mousePos) {
// Drawing the rect at the right position
this.drawEllipse(Math.floor(mousePos[0] / currFile.zoom) + 0.5, Math.floor(mousePos[1] / currFile.zoom) + 0.5,
currFile.TMPLayer.context);
}
/** Finishes drawing the rect, decides the end coordinates and moves the preview rectangle to the
* current layer
*
* @param {*} mousePos The position of the mouse when the user stopped dragging
*/
onEnd(mousePos) {
super.onEnd(mousePos);
if (this.startMousePos == undefined)
return;
let tmpContext = currFile.TMPLayer.context;
this.endMousePos[0] = Math.floor(mousePos[0] / currFile.zoom) + 0.5;
this.endMousePos[1] = Math.floor(mousePos[1] / currFile.zoom) + 0.5;
// If I have to fill it, I do so
if (this.currFillMode == 'fill') {
// Use the fill tool on the tmp canvas
FillTool.fill([this.startMousePos[0] * currFile.zoom, this.startMousePos[1] * currFile.zoom],
currFile.TMPLayer.context);
}
Util.pasteData(currFile.currentLayer.context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]),
currFile.TMPLayer.context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]),
currFile.currentLayer.context);
// Update the layer preview
currFile.currentLayer.updateLayerPreview();
// Clearing the tmp canvas
tmpContext.clearRect(0, 0, currFile.TMPLayer.canvas.width, currFile.TMPLayer.canvas.height);
this.startMousePos = undefined;
}
onSelect() {
super.onSelect();
}
onDeselect() {
super.onDeselect();
}
/** Draws an ellipse with end coordinates given by x and y on the tmp layer (draws
* the preview for the ellipse tool)
*
* @param {*} x The current end x of the ellipse
* @param {*} y The current end y of the ellipse
*/
drawEllipse(x, y, context) {
// Width and height of the ellipse
let width = undefined;
let height = undefined;
// Clearing the tmp canvas
currFile.TMPLayer.context.clearRect(0, 0, currFile.TMPLayer.canvas.width, currFile.TMPLayer.canvas.height);
// Compute width and height
width = Math.abs(x - this.startMousePos[0]);
height = Math.abs(y - this.startMousePos[1]);
// Drawing the ellipse
this.previewEllipse(context, this.startMousePos[0], this.startMousePos[1], width, height);
}
previewEllipse(context, xc, yc, a, b) {
let x, y1, y2;
let toFill = {};
let removed = {};
x = xc - a;
while (x < (xc + a)) {
let root = Math.sqrt((1 - (((x - xc)*(x - xc)) / (a*a))) * b*b);
let flooredX = Math.floor(x);
let flooredY1, flooredY2;
y1 = root + yc;
y2 = -root + yc;
flooredY1 = Math.floor(y1);
flooredY2 = Math.floor(y2);
toFill[[flooredX, flooredY1]] = true;
toFill[[flooredX, flooredY2]] = true;
x += 0.005;
}
for (const coord in toFill) {
let arrayCoord = JSON.parse("[" + coord + "]");
if (arrayCoord[0]-xc < 0 || arrayCoord[1]-yc < 0) {
continue;
}
if (!(
// Top and left
(toFill[[arrayCoord[0], arrayCoord[1] - 1]] && toFill[[arrayCoord[0] - 1, arrayCoord[1]]] &&
!removed[[arrayCoord[0], arrayCoord[1] - 1]] && !removed[arrayCoord[0] - 1, arrayCoord[1]]) ||
// Top and right
(toFill[[arrayCoord[0], arrayCoord[1] - 1]] && toFill[[arrayCoord[0] + 1, arrayCoord[1]]] &&
!removed[[arrayCoord[0], arrayCoord[1] - 1]] && !removed[arrayCoord[0] + 1, arrayCoord[1]]) ||
// Bottom and left
(toFill[[arrayCoord[0], arrayCoord[1] + 1]] && toFill[[arrayCoord[0] - 1, arrayCoord[1]]] &&
!removed[[arrayCoord[0], arrayCoord[1] + 1]] && !removed[arrayCoord[0] - 1, arrayCoord[1]]) ||
// Bottom and right
(toFill[[arrayCoord[0], arrayCoord[1] + 1]] && toFill[[arrayCoord[0] + 1, arrayCoord[1]]] &&
!removed[[arrayCoord[0], arrayCoord[1] + 1]] && !removed[arrayCoord[0] + 1, arrayCoord[1]])) ||
removed[arrayCoord]) {
context.fillRect(arrayCoord[0], arrayCoord[1], this.currSize, this.currSize);
context.fillRect(xc - Math.abs(xc - arrayCoord[0]), arrayCoord[1], this.currSize, this.currSize);
context.fillRect(arrayCoord[0], yc - Math.abs(yc - arrayCoord[1]), this.currSize, this.currSize);
context.fillRect(xc - Math.abs(xc - arrayCoord[0]), yc - Math.abs(yc - arrayCoord[1]), this.currSize, this.currSize);
}
removed[arrayCoord] = true;
}
}
}

View File

@ -5,6 +5,14 @@ class EraserTool extends ResizableTool {
Events.on('click', this.mainButton, switchFunction, this);
Events.on('click', this.biggerButton, this.increaseSize.bind(this));
Events.on('click', this.smallerButton, this.decreaseSize.bind(this));
this.resetTutorial();
this.addTutorialTitle("Eraser tool");
this.addTutorialKey("E", " to select the eraser");
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");
}
onStart(mousePos) {
@ -23,7 +31,7 @@ class EraserTool extends ResizableTool {
Math.floor(this.prevMousePos[1]/currFile.zoom),
Math.floor(this.currMousePos[0]/currFile.zoom),
Math.floor(this.currMousePos[1]/currFile.zoom),
this.currSize
this.currSize, true
);
}

View File

@ -6,6 +6,15 @@ class EyeDropperTool extends Tool {
super(name, options);
Events.on('click', this.mainButton, switchFunction, this);
this.resetTutorial();
this.addTutorialTitle("Eyedropper tool");
this.addTutorialKey("E", " to select the lasso selection tool");
this.addTutorialKey("Left drag", " to preview the picked colour");
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");
}
onStart(mousePos, target) {

View File

@ -3,6 +3,12 @@ class FillTool extends DrawingTool {
super(name, options);
Events.on('click', this.mainButton, switchFunction, this);
this.resetTutorial();
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");
}
onStart(mousePos, target) {
@ -10,14 +16,13 @@ class FillTool extends DrawingTool {
if (target.className != 'drawingCanvas')
return;
this.fill(mousePos);
currFile.currentLayer.updateLayerPreview();
new HistoryState().EditCanvas();
FillTool.fill(mousePos);
currFile.currentLayer.updateLayerPreview();
}
fill(cursorLocation) {
static fill(cursorLocation, context) {
//changes a pixels color
function colorPixel(tempImage, pixelPos, fillColor) {
//console.log('colorPixel:',pixelPos);
@ -39,8 +44,11 @@ class FillTool extends DrawingTool {
return (r == color[0] && g == color[1] && b == color[2] && a == color[3]);
}
if (context == undefined)
context = currFile.currentLayer.context;
//temporary image holds the data while we change it
let tempImage = currFile.currentLayer.context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
let tempImage = context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
//this is an array that holds all of the pixels at the top of the cluster
let topmostPixelsArray = [[Math.floor(cursorLocation[0]/currFile.zoom), Math.floor(cursorLocation[1]/currFile.zoom)]];
@ -53,7 +61,7 @@ class FillTool extends DrawingTool {
let clusterColor = [tempImage.data[startingPosition],tempImage.data[startingPosition+1],tempImage.data[startingPosition+2], tempImage.data[startingPosition+3]];
//the color to fill with
let fillColor = Color.hexToRgb(currFile.currentLayer.context.fillStyle);
let fillColor = Color.hexToRgb(context.fillStyle);
//if you try to fill with the same color that's already there, exit the function
if (clusterColor[0] == fillColor.r &&
@ -118,7 +126,7 @@ class FillTool extends DrawingTool {
pixelPos += currFile.canvasSize[0] * 4;
}
}
currFile.currentLayer.context.putImageData(tempImage, 0, 0);
context.putImageData(tempImage, 0, 0);
}
onDrag(mousePos, cursorTarget) {

View File

@ -0,0 +1,95 @@
class LassoSelectionTool extends SelectionTool {
currentPixels = [];
constructor (name, options, switchFunc, moveTool) {
super(name, options, switchFunc, moveTool);
Events.on('click', this.mainButton, switchFunc, this);
this.resetTutorial();
this.addTutorialTitle("Lasso selection tool");
this.addTutorialKey("Q", " to select the lasso selection tool");
this.addTutorialKey("Left drag", " to select an area");
this.addTutorialKey("Left drag", " to move a selection");
this.addTutorialKey("Esc", " to cancel a selection")
this.addTutorialKey("Click", " outside the selection to cancel it")
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");
}
onStart(mousePos, mouseTarget) {
super.onStart(mousePos, mouseTarget);
if (Util.isChildOfByClass(mouseTarget, "editor-top-menu") ||
!Util.cursorInCanvas(currFile.canvasSize, [mousePos[0]/currFile.zoom, mousePos[1]/currFile.zoom]))
return;
this.currentPixels = [];
this.drawSelection();
this.currentPixels.push([mousePos[0] / currFile.zoom, mousePos[1] / currFile.zoom]);
}
onDrag(mousePos, mouseTarget) {
super.onDrag(mousePos, mouseTarget);
if (!Util.cursorInCanvas(currFile.canvasSize, [mousePos[0]/currFile.zoom, mousePos[1]/currFile.zoom]))
return;
if (this.currentPixels[this.currentPixels.length - 1] != mousePos)
this.currentPixels.push([mousePos[0] / currFile.zoom, mousePos[1] / currFile.zoom]);
this.drawSelection();
}
onEnd(mousePos, mouseTarget) {
super.onEnd(mousePos, mouseTarget);
new HistoryState().EditCanvas();
if (Util.isChildOfByClass(mouseTarget, "editor-top-menu"))
return;
this.currentPixels.push([this.startMousePos[0] / currFile.zoom, this.startMousePos[1] / currFile.zoom]);
// Include extreme borders
this.boundingBox.maxX++;
this.boundingBox.maxY++;
// Switch to the move tool so that the user can move the selection
this.switchFunc(this.moveTool);
this.moveTool.setSelectionData(this.getSelection(), this);
}
onSelect() {
super.onSelect();
}
onDeselect() {
super.onDeselect();
}
drawSelection() {
if (this.currentPixels.length <= 1)
return;
let point = [];
let prevPoint = [];
currFile.VFXLayer.context.clearRect(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
currFile.VFXLayer.context.fillStyle = 'rgba(0,0,0,1)';
for (var index = 0; index < this.currentPixels.length; index ++) {
point = this.currentPixels[index];
if (index == 0)
currFile.VFXLayer.context.moveTo(point[0], point[1]);
else {
prevPoint = this.currentPixels[index- 1];
currFile.VFXLayer.drawLine(Math.floor(prevPoint[0]), Math.floor(prevPoint[1]),
Math.floor(point[0]), Math.floor(point[1]), 1);
}
}
currFile.VFXLayer.drawLine(Math.floor(point[0]), Math.floor(point[1]),
Math.floor(this.currentPixels[0][0]), Math.floor(this.currentPixels[0][1]), 1);
}
}

View File

@ -5,6 +5,14 @@ class LineTool extends ResizableTool {
Events.on('click', this.mainButton, switchFunction, this);
Events.on('click', this.biggerButton, this.increaseSize.bind(this));
Events.on('click', this.smallerButton, this.decreaseSize.bind(this));
this.resetTutorial();
this.addTutorialTitle("Line tool");
this.addTutorialKey("L", " to select the line");
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");
}
onStart(mousePos) {

116
js/tools/MagicWandTool.js Normal file
View File

@ -0,0 +1,116 @@
class MagicWandTool extends SelectionTool {
constructor (name, options, switchFunc, moveTool) {
super(name, options, switchFunc, moveTool);
Events.on('click', this.mainButton, switchFunc, this);
this.resetTutorial();
this.addTutorialTitle("Magic wand tool");
this.addTutorialKey("W", " to select the magic wand tool");
this.addTutorialKey("Left click", " to select a contiguous area");
this.addTutorialKey("Esc", " to cancel a selection");
this.addTutorialKey("Click", " outside the selection to cancel it");
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");
}
onEnd(mousePos, mouseTarget) {
super.onStart(mousePos, mouseTarget);
if (Util.isChildOfByClass(mouseTarget, "editor-top-menu") ||
!Util.cursorInCanvas(currFile.canvasSize, [mousePos[0]/currFile.zoom, mousePos[1]/currFile.zoom]))
return;
this.switchFunc(this.moveTool);
this.moveTool.setSelectionData(this.getSelection(), this);
}
getSelection() {
let coords = [Math.floor(this.endMousePos[0]), Math.floor(this.endMousePos[1])];
let data = currFile.currentLayer.context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]).data;
let index = (coords[1] * currFile.canvasSize[0] + coords[0]) * 4;
let color = [data[index], data[index+1], data[index+2], data[index+3]];
let selectedData = new ImageData(currFile.canvasSize[0], currFile.canvasSize[1]);
this.visit([Math.floor(this.endMousePos[0]), Math.floor(this.endMousePos[1])],
this.currSelection, data, color);
for (const pixel in this.currSelection) {
let coords = [parseInt(pixel.split(",")[0]), parseInt(pixel.split(",")[1])];
let index = (currFile.canvasSize[0] * coords[1] + coords[0]) * 4;
selectedData[index] = color[0];
selectedData[index+1] = color[1];
selectedData[index+2] = color[2];
selectedData[index+3] = color[3];
this.updateBoundingBox(coords[0], coords[1]);
}
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];
// Cut the selection
this.cutSelection();
// Put it on the TMP layer
currFile.TMPLayer.context.putImageData(this.previewData, 0, 0);
// Draw the bounding box
this.drawBoundingBox();
return selectedData;
}
visit(pixel, selected, data, color) {
let toVisit = [pixel];
let visited = [];
while (toVisit.length > 0) {
pixel = toVisit.pop();
visited[pixel] = true;
let col = Util.getPixelColor(data, pixel[0], pixel[1], currFile.canvasSize[0]);
if (col[0] == color[0] && col[1] == color[1] && col[2] == color[2] && col[3] == color[3])
selected[pixel] = true;
else
continue;
let top, bottom, left, right;
if (pixel[1] > 0)
top = [pixel[0], pixel[1] - 1];
else
top = undefined;
if (pixel[0] > 0)
left = [pixel[0] - 1, pixel[1]];
else
left = undefined;
if (pixel[1] < currFile.canvasSize[1])
bottom = [pixel[0], pixel[1] + 1];
else
bottom = undefined;
if (pixel[0] < currFile.canvasSize[0])
right = [pixel[0] + 1, pixel[1]];
else
right = undefined;
if (right != undefined && visited[right] == undefined)
toVisit.push(right);
if (left != undefined && visited[left] == undefined)
toVisit.push(left);
if (top != undefined && visited[top] == undefined)
toVisit.push(top);
if (bottom != undefined && visited[bottom] == undefined)
toVisit.push(bottom);
}
return selected;
}
}

View File

@ -14,17 +14,20 @@ class MoveSelectionTool extends DrawingTool {
Events.onCustom("esc-pressed", this.endSelection.bind(this));
Events.onCustom("ctrl+c", this.copySelection.bind(this));
Events.onCustom("ctrl+x", this.cutSelection.bind(this));
Events.onCustom("ctrl+c", this.copySelection.bind(this), true);
Events.onCustom("ctrl+x", this.cutSelection.bind(this), true);
Events.onCustom("ctrl+v", this.pasteSelection.bind(this));
}
copySelection() {
copySelection(event) {
this.lastCopiedSelection = this.currSelection;
this.cutting = false;
if (event)
this.switchFunc(this.selectionTool);
}
cutSelection() {
cutSelection(event) {
if (currFile.currentLayer.isLocked)
return;
this.cutting = true;
@ -32,8 +35,12 @@ class MoveSelectionTool extends DrawingTool {
this.endSelection();
this.currSelection = this.lastCopiedSelection;
// Cut the data
currFile.currentLayer.context.clearRect(this.currSelection.left-0.5, this.currSelection.top-0.5,
this.currSelection.width, this.currSelection.height);
this.selectionTool.cutSelection();
if (event)
this.switchFunc(this.selectionTool);
new HistoryState().EditCanvas();
}
pasteSelection() {
@ -41,6 +48,10 @@ class MoveSelectionTool extends DrawingTool {
return;
if (this.lastCopiedSelection === undefined)
return;
if (!(this.currMousePos[0]/currFile.zoom >= 0 && this.currMousePos[1]/currFile.zoom >= 0 &&
this.currMousePos[0]/currFile.zoom < currFile.canvasSize[0] && this.currMousePos[1]/currFile.zoom < currFile.canvasSize[1]))
this.currMousePos = [currFile.canvasSize[0]*currFile.zoom / 2, currFile.canvasSize[1]*currFile.zoom /2];
// Finish the current selection and start a new one with the same data
if (!this.cutting) {
this.endSelection();
@ -49,6 +60,7 @@ class MoveSelectionTool extends DrawingTool {
this.switchFunc(this);
this.currSelection = this.lastCopiedSelection;
this.selectionTool.drawSelectedArea();
// Putting the vfx layer on top of everything
currFile.VFXLayer.canvas.style.zIndex = MAX_Z_INDEX;
@ -59,30 +71,35 @@ class MoveSelectionTool extends DrawingTool {
onStart(mousePos, mouseTarget) {
super.onStart(mousePos, mouseTarget);
if (!this.cursorInSelectedArea(mousePos) &&
!Util.isChildOfByClass(mouseTarget, "editor-top-menu")) {
this.endSelection();
}
}
onDrag(mousePos) {
super.onDrag(mousePos);
this.currSelection = this.selectionTool.moveAnts(mousePos[0]/currFile.zoom,
mousePos[1]/currFile.zoom, this.currSelection.width, this.currSelection.height);
this.selectionTool.moveOffset =
[Math.floor(mousePos[0] / currFile.zoom - currFile.canvasSize[0] / 2 - (this.selectionTool.boundingBoxCenter[0] - currFile.canvasSize[0]/2)),
Math.floor(mousePos[1] / currFile.zoom - currFile.canvasSize[1] / 2- (this.selectionTool.boundingBoxCenter[1] - currFile.canvasSize[1]/2))];
// clear the entire tmp layer
currFile.TMPLayer.context.clearRect(0, 0, currFile.TMPLayer.canvas.width, currFile.TMPLayer.canvas.height);
currFile.TMPLayer.context.clearRect(0, 0, currFile.TMPLayer.canvas.width, currFile.TMPLayer.canvas.height);
// put the image data on the tmp layer with offset
currFile.TMPLayer.context.putImageData(
this.currSelection.data,
Math.round(mousePos[0] / currFile.zoom) - this.currSelection.width / 2,
Math.round(mousePos[1] / currFile.zoom) - this.currSelection.height / 2);
currFile.TMPLayer.context.putImageData(this.currSelection,
this.selectionTool.moveOffset[0], this.selectionTool.moveOffset[1]);
// Draw the selection area and outline
this.selectionTool.drawOutline();
this.selectionTool.drawSelectedArea();
this.selectionTool.drawBoundingBox();
}
onEnd(mousePos) {
super.onEnd(mousePos);
onEnd(mousePos, mouseTarget) {
super.onEnd(mousePos, mouseTarget);
if (!this.selectionTool.cursorInSelectedArea(mousePos) &&
!Util.isChildOfByClass(mouseTarget, "editor-top-menu")) {
this.endSelection();
// Switch to selection tool
this.switchFunc(this.selectionTool);
}
}
onSelect() {
@ -91,6 +108,7 @@ class MoveSelectionTool extends DrawingTool {
onDeselect() {
super.onDeselect();
this.endSelection();
}
setSelectionData(data, tool) {
@ -102,7 +120,7 @@ class MoveSelectionTool extends DrawingTool {
onHover(mousePos) {
super.onHover(mousePos);
if (this.cursorInSelectedArea(mousePos)) {
if (this.selectionTool.cursorInSelectedArea(mousePos)) {
currFile.canvasView.style.cursor = 'move';
}
else {
@ -110,65 +128,13 @@ class MoveSelectionTool extends DrawingTool {
}
}
cursorInSelectedArea(cursorPos) {
// Getting the coordinates relatively to the canvas
let x = cursorPos[0] / currFile.zoom;
let y = cursorPos[1] / currFile.zoom;
if (this.currSelection.left <= x && x <= this.currSelection.right) {
if (y <= this.currSelection.bottom && y >= this.currSelection.top) {
return true;
}
return false;
}
return false;
}
endSelection() {
endSelection(event) {
if (this.currSelection == undefined)
return;
// Clearing the tmp (move preview) and vfx (ants) layers
currFile.TMPLayer.context.clearRect(0, 0, currFile.TMPLayer.canvas.width, currFile.TMPLayer.canvas.height);
currFile.VFXLayer.context.clearRect(0, 0, currFile.VFXLayer.canvas.width, currFile.VFXLayer.canvas.height);
// I have to save the underlying data, so that the transparent pixels in the clipboard
// don't override the coloured pixels in the canvas
let underlyingImageData = currFile.currentLayer.context.getImageData(
this.currSelection.left, this.currSelection.top,
this.currSelection.width+1, this.currSelection.height+1
);
let pasteData = this.currSelection.data.data.slice();
for (let i=0; i<underlyingImageData.data.length; i+=4) {
let currentMovePixel = [
pasteData[i], pasteData[i+1], pasteData[i+2], pasteData[i+3]
];
let currentUnderlyingPixel = [
underlyingImageData.data[i], underlyingImageData.data[i+1],
underlyingImageData.data[i+2], underlyingImageData.data[i+3]
];
// If the pixel of the clipboard is empty, but the one below it isn't, I use the pixel below
if (Util.isPixelEmpty(currentMovePixel)) {
if (!Util.isPixelEmpty(currentUnderlyingPixel)) {
pasteData[i] = currentUnderlyingPixel[0];
pasteData[i+1] = currentUnderlyingPixel[1];
pasteData[i+2] = currentUnderlyingPixel[2];
pasteData[i+3] = currentUnderlyingPixel[3];
}
}
}
currFile.currentLayer.context.putImageData(new ImageData(pasteData, this.currSelection.width+1),
this.currSelection.left, this.currSelection.top
);
this.currSelection = undefined;
currFile.currentLayer.updateLayerPreview();
currFile.VFXLayer.canvas.style.zIndex = MIN_Z_INDEX;
// Switch to brush
this.switchFunc(this.endTool);
this.selectionTool.pasteSelection();
if (event)
this.switchFunc(this.selectionTool);
}
}

View File

@ -4,6 +4,13 @@ class PanTool extends Tool {
super(name, options);
Events.on('click', this.mainButton, switchFunction, this);
this.resetTutorial();
this.addTutorialTitle("Pan 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");
}
onStart(mousePos, target) {

View File

@ -1,5 +1,3 @@
// TODO: FIX SELECTION
class RectangleTool extends ResizableTool {
// Saving the empty rect svg
emptyRectangleSVG = document.getElementById("rectangle-empty-button-svg");
@ -17,6 +15,15 @@ class RectangleTool extends ResizableTool {
Events.on('click', this.mainButton, this.changeFillType.bind(this));
Events.on('click', this.biggerButton, this.increaseSize.bind(this));
Events.on('click', this.smallerButton, this.decreaseSize.bind(this));
this.resetTutorial();
this.addTutorialTitle("Rectangle tool");
this.addTutorialKey("U", " to select the rectangle");
this.addTutorialKey("U while selected", " to change fill mode (empty or fill)");
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");
}
changeFillType() {

View File

@ -1,25 +1,28 @@
class RectangularSelectionTool extends SelectionTool {
switchFunc = undefined;
moveTool = undefined;
currSelection = {};
constructor (name, options, switchFunc, moveTool) {
super(name, options, switchFunc);
this.switchFunc = switchFunc;
this.moveTool = moveTool;
super(name, options, switchFunc, moveTool);
Events.on('click', this.mainButton, switchFunc, this);
this.resetTutorial();
this.addTutorialTitle("Rectangular selection tool");
this.addTutorialKey("M", " to select the rectangular selection tool");
this.addTutorialKey("Left drag", " to select a rectangular area");
this.addTutorialKey("Left drag", " to move a selection");
this.addTutorialKey("Esc", " to cancel a selection");
this.addTutorialKey("Click", " outside the selection to cancel it");
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");
}
onStart(mousePos) {
super.onStart(mousePos);
onStart(mousePos, mouseTarget) {
super.onStart(mousePos, mouseTarget);
// Putting the vfx layer on top of everything
currFile.VFXLayer.canvas.style.zIndex = MAX_Z_INDEX;
// Saving the start coords of the rect
this.startMousePos[0] = Math.round(this.startMousePos[0] / currFile.zoom) - 0.5;
this.startMousePos[1] = Math.round(this.startMousePos[1] / currFile.zoom) - 0.5;
if (Util.isChildOfByClass(mouseTarget, "editor-top-menu") ||
!Util.cursorInCanvas(currFile.canvasSize, [mousePos[0]/currFile.zoom, mousePos[1]/currFile.zoom]))
return;
// Avoiding external selections
if (this.startMousePos[0] < 0) {
@ -40,20 +43,28 @@ class RectangularSelectionTool extends SelectionTool {
this.drawSelection(this.startMousePos[0], this.startMousePos[1]);
}
onDrag(mousePos) {
super.onDrag(mousePos);
onDrag(mousePos, mouseTarget) {
super.onDrag(mousePos, mouseTarget);
if (Util.isChildOfByClass(mouseTarget, "editor-top-menu") ||
!Util.cursorInCanvas(currFile.canvasSize, [mousePos[0]/currFile.zoom, mousePos[1]/currFile.zoom]))
return;
// Drawing the rect
this.drawSelection(Math.round(mousePos[0] / currFile.zoom) + 0.5, Math.round(mousePos[1] / currFile.zoom) + 0.5);
this.endMousePos = [Math.floor(mousePos[0] / currFile.zoom), Math.floor(mousePos[1] / currFile.zoom)];
this.drawSelection(Math.floor(mousePos[0] / currFile.zoom), Math.floor(mousePos[1] / currFile.zoom));
}
onEnd(mousePos) {
super.onEnd(mousePos);
onEnd(mousePos, mouseTarget) {
super.onEnd(mousePos, mouseTarget);
if (Util.isChildOfByClass(mouseTarget, "editor-top-menu"))
return;
new HistoryState().EditCanvas();
// Getting the end position
this.endMousePos[0] = Math.round(this.endMousePos[0] / currFile.zoom) + 0.5;
this.endMousePos[1] = Math.round(this.endMousePos[1] / currFile.zoom) + 0.5;
this.endMousePos = [Math.floor(mousePos[0] / currFile.zoom), Math.floor(mousePos[1] / currFile.zoom)];
// Inverting end and start (start must always be the top left corner)
if (this.endMousePos[0] < this.startMousePos[0]) {
@ -68,33 +79,23 @@ class RectangularSelectionTool extends SelectionTool {
this.startMousePos[1] = tmp;
}
if (Util.cursorInCanvas(currFile.canvasSize, [mousePos[0]/currFile.zoom, mousePos[1]/currFile.zoom])) {
this.boundingBox.minX = this.startMousePos[0] - 1;
this.boundingBox.maxX = this.endMousePos[0] + 1;
this.boundingBox.minY = this.startMousePos[1] - 1;
this.boundingBox.maxY = this.endMousePos[1] + 1;
}
// Switch to the move tool so that the user can move the selection
this.switchFunc(this.moveTool);
// Preparing data for the move tool
let dataWidth = this.endMousePos[0] - this.startMousePos[0];
let dataHeight = this.endMousePos[1] - this.startMousePos[1];
// Obtain the selected pixels
this.moveTool.setSelectionData(this.getSelection(), this);
}
this.currSelection = {
left: this.startMousePos[0], right: this.endMousePos[0],
top: this.startMousePos[1], bottom: this.endMousePos[1],
width: dataWidth,
height: dataHeight,
data: currFile.currentLayer.context.getImageData(
this.startMousePos[0], this.startMousePos[1],
dataWidth + 1, dataHeight + 1)
};
// Moving the selection to the TMP layer. It will be moved back to the original
// layer if the user will cancel or end the selection
currFile.currentLayer.context.clearRect(this.startMousePos[0] - 0.5, this.startMousePos[1] - 0.5,
dataWidth + 1, dataHeight + 1);
// Moving those pixels from the current layer to the tmp layer
currFile.TMPLayer.context.putImageData(this.currSelection.data, this.startMousePos[0], this.startMousePos[1]);
this.moveTool.setSelectionData(this.currSelection, this);
console.log("data set");
cutSelection() {
super.cutSelection();
currFile.currentLayer.context.clearRect(this.currSelection.left-0.5, this.currSelection.top-0.5,
this.currSelection.width, this.currSelection.height);
}
onSelect() {
@ -105,53 +106,16 @@ class RectangularSelectionTool extends SelectionTool {
super.onDeselect();
}
drawSelection(x, y) {
drawSelection() {
// Getting the vfx context
let vfxContext = currFile.VFXLayer.context;
// Clearing the vfx canvas
vfxContext.clearRect(0, 0, currFile.VFXLayer.canvas.width, currFile.VFXLayer.canvas.height);
vfxContext.lineWidth = 1;
vfxContext.strokeStyle = 'black';
vfxContext.setLineDash([4]);
// Drawing the rect
vfxContext.beginPath();
vfxContext.rect(this.startMousePos[0], this.startMousePos[1], x - this.startMousePos[0], y - this.startMousePos[1]);
vfxContext.stroke();
}
/** Moves the rect ants to the specified position
*
* @param {*} x X coordinate of the rect ants
* @param {*} y Y coordinat of the rect ants
* @param {*} width Width of the selection
* @param {*} height Height of the selectione
*
* @return The data regarding the current position and size of the selection
*/
moveAnts(x, y, width, height) {
// Getting the vfx context
let vfxContext = currFile.VFXLayer.context;
let ret = this.currSelection;
// Clearing the vfx canvas
vfxContext.clearRect(0, 0, currFile.VFXLayer.canvas.width, currFile.VFXLayer.canvas.height);
vfxContext.lineWidth = 1;
vfxContext.setLineDash([4]);
// Fixing the coordinates
this.currSelection.left = Math.round(Math.round(x) - 0.5 - Math.round(width / 2)) + 0.5;
this.currSelection.top = Math.round(Math.round(y) - 0.5 - Math.round(height / 2)) + 0.5;
this.currSelection.right = this.currSelection.left + Math.round(width);
this.currSelection.bottom = this.currSelection.top + Math.round(height);
// Drawing the rect
vfxContext.beginPath();
vfxContext.rect(this.currSelection.left, this.currSelection.top, width, height);
vfxContext.stroke();
return ret;
currFile.VFXLayer.drawLine(this.startMousePos[0], this.startMousePos[1], this.endMousePos[0], this.startMousePos[1], 1);
currFile.VFXLayer.drawLine(this.endMousePos[0], this.startMousePos[1], this.endMousePos[0], this.endMousePos[1], 1);
currFile.VFXLayer.drawLine(this.endMousePos[0], this.endMousePos[1], this.startMousePos[0], this.endMousePos[1], 1);
currFile.VFXLayer.drawLine(this.startMousePos[0], this.endMousePos[1], this.startMousePos[0], this.startMousePos[1], 1);
}
}

View File

@ -2,9 +2,32 @@ class ResizableTool extends DrawingTool {
startResizePos = undefined;
currSize = 1;
prevSize = 1;
toolSizeInput = undefined;
biggerButton = undefined;
smallerButton = undefined;
constructor(name, options, switchFunc) {
super(name, options, switchFunc);
this.biggerButton = document.getElementById(name + "-bigger-button");
this.smallerButton = document.getElementById(name + "-smaller-button");
}
onSelect(mousePos) {
super.onSelect(mousePos);
if (this.toolSizeInput == undefined) {
this.toolSizeInput = InputComponents.createNumber(this.name + "-input", "Tool size");
Events.on("change", this.toolSizeInput.getElementsByTagName("input")[0], this.updateSize.bind(this));
}
TopMenuModule.addInfoElement(this.name + "-input", this.toolSizeInput);
TopMenuModule.updateField(this.name + "-input", this.currSize);
}
updateSize(event) {
let value = event.target.value;
this.currSize = value;
}
onRightStart(mousePos, mouseEvent) {
@ -24,9 +47,30 @@ class ResizableTool extends DrawingTool {
//fix offset so the cursor stays centered
this.updateCursor();
this.onHover(this.startResizePos, mouseEvent);
TopMenuModule.updateField(this.name + "-input", this.currSize);
}
onRightEnd(mousePos, mouseEvent) {
}
increaseSize() {
if (this.currSize < 128) {
this.currSize++;
this.updateCursor();
TopMenuModule.updateField(this.name + "-input", this.currSize);
}
}
decreaseSize() {
if (this.currSize > 1) {
this.currSize--;
this.updateCursor();
TopMenuModule.updateField(this.name + "-input", this.currSize);
}
}
get size() {
return this.currSize;
}
}

View File

@ -1,5 +1,276 @@
class SelectionTool extends Tool {
constructor(name, options, switchFunc) {
super(name, options, switchFunc);
switchFunc = undefined;
moveTool = undefined;
boundingBox = {minX: 9999999, maxX: -1, minY: 9999999, maxY: -1};
currSelection = {};
outlineData = undefined;
previewData = undefined;
selectedPixel = undefined;
moveOffset = [0, 0];
boundingBoxCenter = [0,0];
reconstruct = {left:false, right:false, top:false, bottom:false};
constructor(name, options, switchFunc, moveTool) {
super(name, options);
this.moveTool = moveTool;
this.switchFunc = switchFunc;
}
onStart(mousePos, mouseTarget) {
super.onStart(mousePos);
if (mouseTarget == undefined || Util.isChildOfByClass(mouseTarget, "editor-top-menu") ||
!Util.cursorInCanvas(currFile.canvasSize, [mousePos[0]/currFile.zoom, mousePos[1]/currFile.zoom]))
return;
// Putting the vfx layer on top of everything
currFile.VFXLayer.canvas.style.zIndex = MAX_Z_INDEX;
currFile.VFXLayer.context.fillStyle = "rgba(0,0,0,1)";
this.startMousePos = [Math.floor(mousePos[0] / currFile.zoom),
Math.floor(mousePos[1] / currFile.zoom)];
this.endMousePos = [this.startMousePos[0], this.startMousePos[1]];
let mouseX = mousePos[0] / currFile.zoom;
let mouseY = mousePos[1] / currFile.zoom;
this.boundingBox = {minX: 9999999, maxX: -1, minY: 9999999, maxY: -1};
this.reconstruct = {left:false, right:false, top:false, bottom:false};
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));
}
onDrag(mousePos) {
super.onDrag(mousePos);
let mouseX = mousePos[0] / currFile.zoom;
let mouseY = mousePos[1] / currFile.zoom;
if (mouseX > currFile.canvasSize[0])
this.reconstruct.right = true;
else if (mouseX < 0)
this.reconstruct.left = true;
if (mouseY > currFile.canvasSize[1])
this.reconstruct.bottom = true;
else if (mouseY < 0)
this.reconstruct.top = true;
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));
}
}
onEnd(mousePos, mouseTarget) {
super.onEnd(mousePos);
if (mouseTarget == undefined || Util.isChildOfByClass(mouseTarget, "editor-top-menu"))
return;
let mouseX = mousePos[0] / currFile.zoom;
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.boundingBoxCenter = [this.boundingBox.minX + (this.boundingBox.maxX - this.boundingBox.minX) / 2,
this.boundingBox.minY + (this.boundingBox.maxY - this.boundingBox.minY) / 2];
}
cutSelection() {
let currLayerData = currFile.currentLayer.context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]).data;
// Save the selected pixels so that they can be moved and pasted back in the right place
for (const key in this.currSelection) {
let x = parseInt(key.split(",")[0]);
let y = parseInt(key.split(",")[1]);
let index = (y * currFile.canvasSize[0] + x) * 4;
for (let i=0; i<4; i++) {
// Save the pixel
this.previewData.data[index + i] = currLayerData[index + i];
// Delete the data below
currLayerData[index + i] = 0;
}
}
currFile.currentLayer.context.putImageData(new ImageData(currLayerData, currFile.canvasSize[0]), 0, 0);
}
pasteSelection(){
if (this.currSelection == undefined)
return;
// I have to save the underlying data, so that the transparent pixels in the clipboard
// don't override the coloured pixels in the canvas
let underlyingImageData = currFile.currentLayer.context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
let pasteData = currFile.TMPLayer.context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
// Clearing the tmp (move preview) and vfx (ants) layers
currFile.TMPLayer.context.clearRect(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
currFile.VFXLayer.context.clearRect(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
Util.pasteData(underlyingImageData, pasteData, currFile.currentLayer.context);
currFile.currentLayer.updateLayerPreview();
currFile.VFXLayer.canvas.style.zIndex = MIN_Z_INDEX;
}
cursorInSelectedArea(mousePos) {
let floored = [Math.floor(mousePos[0] / currFile.zoom) - this.moveOffset[0],
Math.floor(mousePos[1] / currFile.zoom) - this.moveOffset[1]];
if (this.currSelection[floored] != undefined)
return true;
return false;
}
visit(pixel, visited, data) {
let toVisit = [pixel];
let selected = [];
let currVisited = {};
currFile.TMPLayer.context.clearRect(0, 0, 512, 512);
while (toVisit.length > 0) {
pixel = toVisit.pop();
selected.push(pixel);
visited[pixel] = true;
currVisited[pixel] = true;
let col = Util.getPixelColor(data, pixel[0], pixel[1], currFile.canvasSize[0]);
if (col[3] == 255)
continue;
if (this.isBorderOfBox(pixel))
return [];
let top, bottom, left, right;
if (pixel[1] > 0)
top = [pixel[0], pixel[1] - 1];
else
top = undefined;
if (pixel[0] > 0)
left = [pixel[0] - 1, pixel[1]];
else
left = undefined;
if (pixel[1] < currFile.canvasSize[1])
bottom = [pixel[0], pixel[1] + 1];
else
bottom = undefined;
if (pixel[0] < currFile.canvasSize[0])
right = [pixel[0] + 1, pixel[1]];
else
right = undefined;
if (right != undefined && currVisited[right] == undefined)
toVisit.push(right);
if (left != undefined && currVisited[left] == undefined)
toVisit.push(left);
if (top != undefined && currVisited[top] == undefined)
toVisit.push(top);
if (bottom != undefined && currVisited[bottom] == undefined)
toVisit.push(bottom);
}
return selected;
}
getSelection() {
let selected = [];
let visited = {};
let data = currFile.VFXLayer.context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]).data;
// BFS: a pixel that causes the algorithm to visit a pixel of the bounding box is outside the
// selection. Otherwise, since the algorithm stops visiting when it reaches the outline,
// the pixel is inside the selection (and so are all the ones that have been visited)
for (let x=this.boundingBox.minX-1; x<=this.boundingBox.maxX+1; x++) {
for (let y=this.boundingBox.minY-1; y<=this.boundingBox.maxY+1; y++) {
if (visited[[x, y]] == undefined) {
let insidePixels = this.visit([x,y], visited, data);
for (let i=0; i<insidePixels.length; i++) {
selected.push(insidePixels[i]);
this.currSelection[insidePixels[i]] = true;
}
}
}
}
// Save the selection outline
this.outlineData = currFile.VFXLayer.context.getImageData(this.boundingBox.minX,
this.boundingBox.minY, this.boundingBox.maxX - this.boundingBox.minX,
this.boundingBox.maxY - this.boundingBox.minY);
// Create the image data containing the selected pixels
this.previewData = new ImageData(currFile.canvasSize[0], currFile.canvasSize[1]);
// Cut the selection
this.cutSelection();
// Put it on the TMP layer
currFile.TMPLayer.context.putImageData(this.previewData, 0, 0);
// Draw the selected area and the bounding box
this.drawSelectedArea();
this.drawBoundingBox();
return this.previewData;
}
drawSelectedArea() {
for (const key in this.currSelection) {
let x = parseInt(key.split(",")[0]);
let y = parseInt(key.split(",")[1]);
currFile.VFXLayer.context.fillStyle = "rgba(10, 0, 40, 0.3)";
currFile.VFXLayer.context.fillRect(x + this.moveOffset[0], y + this.moveOffset[1], 1, 1);
}
}
drawOutline() {
currFile.VFXLayer.context.clearRect(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
currFile.VFXLayer.context.putImageData(this.outlineData, this.boundingBox.minX + this.moveOffset[0],
this.boundingBox.minY + this.moveOffset[1]);
}
drawBoundingBox() {
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);
}
isBorderOfBox(pixel) {
return pixel[0] == this.boundingBox.minX || pixel[0] == this.boundingBox.maxX ||
pixel[1] == this.boundingBox.minY || pixel[1] == this.boundingBox.maxY;
}
updateBoundingBox(mouseX, mouseY) {
if (mouseX > this.boundingBox.maxX)
this.boundingBox.maxX = Math.floor(mouseX);
if (mouseX < this.boundingBox.minX)
this.boundingBox.minX = Math.floor(mouseX);
if (mouseY < this.boundingBox.minY)
this.boundingBox.minY = Math.floor(mouseY);
if (mouseY > this.boundingBox.maxY)
this.boundingBox.maxY = Math.floor(mouseY);
}
}

View File

@ -1,144 +0,0 @@
// Saving the empty rect svg
var emptyEllipseSVG = document.getElementById("ellipse-empty-button-svg");
// and the full rect svg so that I can change them when the user changes rect modes
var fullEllipseSVG = document.getElementById("ellipse-full-button-svg");
// The start mode is empty ellipse
var ellipseDrawMode = 'empty';
// I'm not drawing a ellipse at the beginning
var isDrawingEllipse = false;
// Ellipse coordinates
let startEllipseX;
let startEllipseY;
let endEllipseX;
let endEllipseY;
// TODO: [ELLIPSE] Make it draw ellipse instead of copy-pasted rectangle
/** Starts drawing the ellipse, saves the start coordinates
*
* @param {*} mouseEvent
*/
function startEllipseDrawing(mouseEvent) {
// Putting the vfx layer on top of everything
VFXLayer.canvas.style.zIndex = parseInt(currentLayer.canvas.style.zIndex, 10) + 1;;
// Updating flag
isDrawingEllipse = true;
// Saving the start coords of the ellipse
let cursorPos = Input.getCursorPosition(mouseEvent);
startEllipseX = Math.floor(cursorPos[0] / zoom) + 0.5;
startEllipseY = Math.floor(cursorPos[1] / zoom) + 0.5;
drawEllipse(startEllipseX, startEllipseY);
}
// TODO: [ELLIPSE] Make it draw ellipse instead of copy-pasted rectangle
/** Updates the ellipse preview depending on the position of the mouse
*
* @param {*} mouseEvent The mouseEvent from which we'll get the mouse position
*/
function updateEllipseDrawing(mouseEvent) {
let pos = Input.getCursorPosition(mouseEvent);
// Drawing the ellipse at the right position
drawEllipse(Math.round(pos[0] / zoom) + 0.5, Math.round(pos[1] / zoom) + 0.5);
}
// TODO: [ELLIPSE] Make it draw ellipse instead of copy-pasted rectangle
/** Finishes drawing the ellipse, decides the end coordinates and moves the preview ellipse to the
* current layer
*
* @param {*} mouseEvent event from which we'll get the mouse position
*/
function endEllipseDrawing(mouseEvent) {
// Getting the end position
let currentPos = Input.getCursorPosition(mouseEvent);
let vfxContext = VFXLayer.context;
endEllipseX = Math.round(currentPos[0] / zoom) + 0.5;
endEllipseY = Math.round(currentPos[1] / zoom) + 0.5;
// Inverting end and start (start must always be the top left corner)
if (endEllipseX < startEllipseX) {
let tmp = endEllipseX;
endEllipseX = startEllipseX;
startEllipseX = tmp;
}
// Same for the y
if (endEllipseY < startEllipseY) {
let tmp = endEllipseY;
endEllipseY = startEllipseY;
startEllipseY = tmp;
}
// Resetting this
isDrawingEllipse = false;
// Drawing the ellipse
startEllipseY -= 0.5;
endEllipseY -= 0.5;
endEllipseX -= 0.5;
startEllipseX -= 0.5;
// Setting the correct linewidth
currentLayer.context.lineWidth = tool.ellipse.brushSize;
// Drawing the ellipse using 4 lines
currentLayer.drawLine(startEllipseX, startEllipseY, endEllipseX, startEllipseY, tool.ellipse.brushSize);
currentLayer.drawLine(endEllipseX, startEllipseY, endEllipseX, endEllipseY, tool.ellipse.brushSize);
currentLayer.drawLine(endEllipseX, endEllipseY, startEllipseX, endEllipseY, tool.ellipse.brushSize);
currentLayer.drawLine(startEllipseX, endEllipseY, startEllipseX, startEllipseY, tool.ellipse.brushSize);
// If I have to fill it, I do so
if (ellipseDrawMode == 'fill') {
currentLayer.context.fillRect(startEllipseX, startEllipseY, endEllipseX - startEllipseX, endEllipseY - startEllipseY);
}
// Clearing the vfx canvas
vfxContext.clearRect(0, 0, VFXLayer.canvas.width, VFXLayer.canvas.height);
}
// TODO: [ELLIPSE] Make it draw ellipse instead of copy-pasted rectangle
/** Draws a ellipse with end coordinates given by x and y on the VFX layer (draws
* the preview for the ellipse tool)
*
* @param {*} x The current end x of the ellipse
* @param {*} y The current end y of the ellipse
*/
function drawEllipse(x, y) {
// Getting the vfx context
let vfxContext = VFXLayer.context;
// Clearing the vfx canvas
vfxContext.clearRect(0, 0, VFXLayer.canvas.width, VFXLayer.canvas.height);
// Drawing the ellipse
vfxContext.lineWidth = tool.ellipse.brushSize;
// Drawing the ellipse
vfxContext.beginPath();
if ((tool.ellipse.brushSize % 2 ) == 0) {
vfxContext.rect(startEllipseX - 0.5, startEllipseY - 0.5, x - startEllipseX, y - startEllipseY);
}
else {
vfxContext.rect(startEllipseX, startEllipseY, x - startEllipseX, y - startEllipseY);
}
vfxContext.setLineDash([]);
vfxContext.stroke();
}
/** Sets the correct tool icon depending on its mode
*
*/
function setEllipseToolSvg() {
console.log("set eilipse svg");
if (ellipseDrawMode == 'empty') {
emptyEllipseSVG.setAttribute('display', 'visible');
fullEllipseSVG.setAttribute('display', 'none');
}
else {
emptyEllipseSVG.setAttribute('display', 'none');
fullEllipseSVG.setAttribute('display', 'visible');
}
}

10
svg/check.svg Normal file
View File

@ -0,0 +1,10 @@
<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"
width="405.272px" height="405.272px" viewBox="0 0 405.272 405.272" style="enable-background:new 0 0 405.272 405.272;"
xml:space="preserve">
<g>
<path d="M393.401,124.425L179.603,338.208c-15.832,15.835-41.514,15.835-57.361,0L11.878,227.836
c-15.838-15.835-15.838-41.52,0-57.358c15.841-15.841,41.521-15.841,57.355-0.006l81.698,81.699L336.037,67.064
c15.841-15.841,41.523-15.829,57.358,0C409.23,82.902,409.23,108.578,393.401,124.425z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 581 B

View File

@ -1,6 +1,9 @@
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
<g>
<ellipse stroke="#000" stroke-width="32" fill="none" cx="255.50001" cy="255.5" id="svg_20" rx="239" ry="187.5"/>
</g>
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 18.1.1, 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 18 18" style="enable-background:new 0 0 17 17;" xml:space="preserve">
<g>
<ellipse cx="9" cy="9" rx="7.5" ry="6.2" stroke="black" stroke-width="2" fill="none" fill-opacity="0"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 221 B

After

Width:  |  Height:  |  Size: 471 B

View File

@ -1,22 +1,8 @@
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
<g>
<title>Layer 1</title>
<g id="svg_4"/>
<g id="svg_5"/>
<g id="svg_6"/>
<g id="svg_7"/>
<g id="svg_8"/>
<g id="svg_9"/>
<g id="svg_10"/>
<g id="svg_11"/>
<g id="svg_12"/>
<g id="svg_13"/>
<g id="svg_14"/>
<g id="svg_15"/>
<g id="svg_16"/>
<g id="svg_17"/>
<g id="svg_18"/>
<ellipse stroke="#000" stroke-width="32" fill="#000000" cx="255.50001" cy="255.5" id="svg_20" rx="239" ry="187.5"/>
</g>
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 18.1.1, 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 18 18" style="enable-background:new 0 0 17 17;" xml:space="preserve">
<g>
<ellipse stroke="none" cx="9" cy="9" rx="8.5" ry="7.2"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 528 B

After

Width:  |  Height:  |  Size: 422 B

19
svg/lasso.svg Normal file
View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 18.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 496.149 496.149" style="enable-background:new 0 0 496.149 496.149;" xml:space="preserve">
<g>
<path d="M250.201,81.608c97.43,0,179.746,43.434,179.746,94.834c0,12.449-4.934,24.449-13.645,35.465l35.402,10.404
c8.613-14.227,13.533-29.629,13.533-45.869c0-72.965-94.463-130.123-215.037-130.123S35.164,103.477,35.164,176.442
c0,26.918,12.936,51.643,35.189,72.172c-6.951,4.502-13.756,10.502-18.836,18.984c-10.453,17.449-10.66,39.094-0.645,64.35
c9.433,23.791,7.125,32.582,5.693,35.242c-3.354,6.322-18.127,9.514-32.385,12.596c-3.486,0.758-7.035,1.531-10.582,2.371
c-9.484,2.24-15.353,11.725-13.129,21.225c1.902,8.111,9.164,13.596,17.16,13.596c1.34,0,2.709-0.16,4.068-0.467
c3.32-0.791,6.656-1.518,9.932-2.227c21.111-4.564,45.016-9.742,56.076-30.482c8.486-15.902,7.195-36.516-4.031-64.85
c-5.725-14.435-6.385-25.564-1.947-33.098c4.965-8.451,16.141-12.207,22.19-13.467c35.705,19.902,82.85,32.354,135.592,33.869
l-10.504-35.74c-87.867-5.758-158.555-46.447-158.555-94.074C70.451,125.041,152.772,81.608,250.201,81.608z"/>
<path d="M487.573,269.629l-222.049-65.271c-1.115-0.338-2.244-0.482-3.373-0.482c-3.113,0-6.158,1.227-8.434,3.5
c-3.096,3.08-4.258,7.613-3.018,11.789l65.271,222.102c1.34,4.613,5.352,7.982,10.143,8.5c0.453,0.047,0.891,0.064,1.322,0.064
c4.309,0,8.307-2.338,10.439-6.162l54.076-98.043l98.025-54.094c4.225-2.322,6.629-6.951,6.1-11.727
C495.561,275.018,492.201,271,487.573,269.629z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

8
svg/magicwand.svg Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<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 221.559 221.559" style="enable-background:new 0 0 221.559 221.559;" xml:space="preserve">
<path d="M143.869,102.927l-26.016-26.016l28.652-28.652l26.014,26.018L143.869,102.927z M26.016,220.779l107.247-107.246
l-26.017-26.016L0,194.762L26.016,220.779z M84.795,24.35v11.838H72.959v10h11.836v11.837h10V46.188h11.838v-10H94.795V24.35H84.795
z M182.887,0.779v11.836h-11.838v10h11.838v11.837h10V22.616h11.836v-10h-11.836V0.779H182.887z M221.559,73.776h-11.836V61.938h-10
v11.838h-11.838v10h11.838v11.835h10V83.776h11.836V73.776z"/>
</svg>

After

Width:  |  Height:  |  Size: 719 B

View File

@ -0,0 +1,7 @@
<div class="checkbox-holder">
<div class="checkbox checked">
<label>Snap to grid</label>
<input type="hidden"/>
<div class="box">{{svg "check"}}</div>
</div>
</div>

View File

@ -1,39 +1,52 @@
Hi! Welcome to the latest version of the Pixel Editor. It's been quite a while! Most of the changes happened behind
the scenes: we worked very hard to refactor the code, make it a bit more modern and scalable so that contributions will,
hopefully, be easier to make.</br></br>
We have some good news for users as well!
Heyo! New pixel editor update, with some very requested changes. After the code refactoring, adding features
is way easier: I introduced some more selection tools, the ellipse tool and an info bar in the top menu :)
<h2>Pixel Grid</h2>
<h2>Lasso tool</h2>
Finally! With the lasso tool you're not forced to select rectangular areas anymore. Have fun selecting, cutting,
copying and pasting any kind of selection with pixel-perfect precision.
I've worked a bit on the pixel grid to make it look a bit better and less intrusive when zooming in. You can see the
difference in behaviour between the new grid (left) and the old grid (right) in the image below.
<img src="grid.png"/>
In addition, the pixel grid will now automatically be hidden when the zoom level becomes too low: in that way looking at big
sprites becomes a lot less performance-heavy and it doesn't cause lag.
</br><img src="lassoselect-tutorial.gif"/>
<h2>Quality of life</h2>
<h2>Magic wand</h2>
In addition to the lasso tool, we added a new selection tool: the magic wand. You can use it to select
contiguous areas of the same colour! If you need to exactly select the pixels of a certain colour, you're
probably going to find the magic wand useful.
I've added some quality of life improvements, sometimes subtle (maybe you won't notice but you'd have definitely noticed
the absence of them), sometimes a bit more noticeable. For example:
</br><img src="magicwand-tutorial.gif"/>
<h2>Ellipse tool</h2>
I added a cute friend for the rectangle tool: with the ellipse tool you'll be able to draw circles and
ellipses of all sizes. The tool works similarly to the rectangle tool: select it to draw empty ellipses,
click on the ellipse button again to draw filled ellipses.
</br><img src="ellipse-tutorial.gif"/>
<h2>Tool tutorials</h2>
I know what you're thinking, "wow those gifs are so cute, that guy must have put a lot of love in them".
Well, I'm glad you like them, and I have good news for you: there are more! Move the cursor on a tool
button: after a little a small tutorial explaining how to use the tool will appear. Hope it's useful for
everyone who's new to the editor!
</br><img src="tool-tutorials.gif"/>
<h2>Top bar info</h2>
Depending on the tool you're using, you'll notice that the top right part of the editor will slightly change.
When using a resizable tool (eraser, brush, rectangle, ellipse, line), it's now possible to select a precise
size by typing it in the input field that appears when you select it. More features that make use of the
top bar are planned.
<h2>Bug fixes and minor details</h2>
<ul>
<li>It's now possible to delete a layer by selecting it and hitting "DEL"</li>
<li>When adding a colour to the palette, it is automatically selected</li>
<li>Thanks to <a href="https://github.com/NSSure">NSSure</a>, you can now select a name for your exported / saved project</li>
<li>The brush preview should now be less intrusive while still being visible</li>
</ul>
<h2> Important bug fixes</h2>
A lot of nasty bugs have been fixed! Please, if you know more we aren't aware of, <a href="https://github.com/lospec/pixel-editor">
feel free to open an issue or make a pull request.</a>
<ul>
<li>Fixed the bug that caused file corruptions</li>
<li>Fixed fill tool not working and creating hundreds of similar colours in certain browsers</li>
<li>Fixed the build procedure for Windows</li>
<li>Fixed the editor ignoring the scale factor when exporting</li>
<li>Squares in the splash page are now...well...actual squares</li>
<li>Using the mouse when a dialogue popup is open will no longer edit the canvas</li>
<li>For coders: the selection system has been uniformed</li>
<li>Tool buttons have been shrinked to make room for more</li>
</ul>
<h2>End of log</h2>
That's all for this update! We have some cool things planned for the near future, so stay tuned :)</br>
You've reached the end of this log, congrats. Special thanks to Jaman on Discord, who's helping us and
who found a quite nasty bug in the selectiont tools. Hope to see you soon in a new log!</br>
- <a href="https://github.com/unsettledgames">Unsettled</a>
</br></br>
P.S.: we're always looking for contributors! Join the <a href="https://discord.com/invite/QjsgTQM">Lospec discord</a> to get in touch!
P.S.: we're always looking for contributors! Join the <a href="https://discord.com/invite/QjsgTQM">Lospec discord</a> to get in touch
or have a look at the <a href="https://github.com/lospec/pixel-edior">editor repository</a>!

View File

@ -0,0 +1,39 @@
Hi! Welcome to the latest version of the Pixel Editor. It's been quite a while! Most of the changes happened behind
the scenes: we worked very hard to refactor the code, make it a bit more modern and scalable so that contributions will,
hopefully, be easier to make.</br></br>
We have some good news for users as well!
<h2>Pixel Grid</h2>
I've worked a bit on the pixel grid to make it look a bit better and less intrusive when zooming in. You can see the
difference in behaviour between the new grid (left) and the old grid (right) in the image below.
<img src="grid.png"/>
In addition, the pixel grid will now automatically be hidden when the zoom level becomes too low: in that way looking at big
sprites becomes a lot less performance-heavy and it doesn't cause lag.
<h2>Quality of life</h2>
I've added some quality of life improvements, sometimes subtle (maybe you won't notice but you'd have definitely noticed
the absence of them), sometimes a bit more noticeable. For example:
<ul>
<li>It's now possible to delete a layer by selecting it and hitting "DEL"</li>
<li>When adding a colour to the palette, it is automatically selected</li>
<li>Thanks to <a href="https://github.com/NSSure">NSSure</a>, you can now select a name for your exported / saved project</li>
<li>The brush preview should now be less intrusive while still being visible</li>
</ul>
<h2> Important bug fixes</h2>
A lot of nasty bugs have been fixed! Please, if you know more we aren't aware of, <a href="https://github.com/lospec/pixel-editor">
feel free to open an issue or make a pull request.</a>
<ul>
<li>Fixed the bug that caused file corruptions</li>
<li>Fixed fill tool not working and creating hundreds of similar colours in certain browsers</li>
<li>Fixed the build procedure for Windows</li>
<li>Fixed the editor ignoring the scale factor when exporting</li>
</ul>
<h2>End of log</h2>
That's all for this update! We have some cool things planned for the near future, so stay tuned :)</br>
- <a href="https://github.com/unsettledgames">Unsettled</a>
</br></br>
P.S.: we're always looking for contributors! Join the <a href="https://discord.com/invite/QjsgTQM">Lospec discord</a> to get in touch!

View File

@ -65,4 +65,11 @@
<li><button>Changelog</button></li>
</ul>
</li>
<li id="editor-info">
<ul>
<li><label>Tool size: <input type="number"/></label></li>
<li>{{> checkbox}}</li>
</ul>
</li>
</ul>

View File

@ -1,46 +1,68 @@
<ul id="tools-menu">
<li class="selected expanded">
<button title="Brush Tool (B)" id="brush-button">{{svg "pencil.svg" width="32" height="32"}}</button>
<button title="Increase Brush Size" id="brush-bigger-button" class="tools-menu-sub-button">{{svg "plus.svg" width="12" height="12"}}</button>
<button title="Decrease Brush Size" id="brush-smaller-button" class="tools-menu-sub-button">{{svg "minus.svg" width="12" height="12"}}</button>
<button id="brush-button">{{svg "pencil.svg" width="24" height="24"}}</button>
<ul class="size-buttons">
<button title="Increase Brush Size" id="brush-bigger-button" class="tools-menu-sub-button">{{svg "plus.svg" width="12" height="12"}}</button>
<button title="Decrease Brush Size" id="brush-smaller-button" class="tools-menu-sub-button">{{svg "minus.svg" width="12" height="12"}}</button>
</ul>
</li>
<li class = "expanded">
<button title="Eraser tool (R)" id="eraser-button">{{svg "eraser.svg" width="32" height="32"}}</button>
<button title="Increase Eraser Size" id="eraser-bigger-button" class="tools-menu-sub-button">{{svg "plus.svg" width="12" height="12"}}</button>
<button title="Decrease Eraser Size" id="eraser-smaller-button" class="tools-menu-sub-button">{{svg "minus.svg" width="12" height="12"}}</button>
<button id="eraser-button">{{svg "eraser.svg" width="24" height="24"}}</button>
<ul class="size-buttons">
<button title="Increase Eraser Size" id="eraser-bigger-button" class="tools-menu-sub-button">{{svg "plus.svg" width="12" height="12"}}</button>
<button title="Decrease Eraser Size" id="eraser-smaller-button" class="tools-menu-sub-button">{{svg "minus.svg" width="12" height="12"}}</button>
</ul>
</li>
<li class="expanded">
<button title="Rectangle Tool (U)" id="rectangle-button">{{svg "rectangle.svg" width="32" height="32" id = "rectangle-empty-button-svg"}}
{{svg "fullrect.svg" width="32" height="32" id = "rectangle-full-button-svg" display = "none"}}</button>
<button title="Increase Rectangle Size" id="rectangle-bigger-button" class="tools-menu-sub-button">{{svg "plus.svg" width="12" height="12"}}</button>
<button title="Decrease Rectangle Size" id="rectangle-smaller-button" class="tools-menu-sub-button">{{svg "minus.svg" width="12" height="12"}}</button>
<button id="rectangle-button">{{svg "rectangle.svg" width="24" height="24" id = "rectangle-empty-button-svg"}}
{{svg "fullrect.svg" width="24" height="24" id = "rectangle-full-button-svg" display = "none"}}</button>
<ul class="size-buttons">
<button title="Increase Rectangle Size" id="rectangle-bigger-button" class="tools-menu-sub-button">{{svg "plus.svg" width="12" height="12"}}</button>
<button title="Decrease Rectangle Size" id="rectangle-smaller-button" class="tools-menu-sub-button">{{svg "minus.svg" width="12" height="12"}}</button>
</ul>
</li>
<!-- TODO: [ELLIPSE] Once ellipse is ready for release make it visible by default -->
<li class="expanded" id="tools-menu--ellipse" style="display: none">
<!-- TODO: [ELLIPSE] Decide on a shortcut to use. "S" was chosen without any in-team consultation. -->
<!-- TODO: [ELLIPSE] Decide on icons to use. Current ones are quickly prepared drafts and display with incorrect color. -->
<button title="Ellipse Tool (S)" id="ellipse-button">
{{svg "ellipse.svg" width="32" height="32" id = "ellipse-empty-button-svg"}}
{{svg "filledellipse.svg" width="32" height="32" id = "ellipse-full-button-svg" display = "none"}}
<li class="expanded">
<button id="ellipse-button">
{{svg "ellipse.svg" width="24" height="24" id = "ellipse-empty-button-svg"}}
{{svg "filledellipse.svg" width="24" height="24" id = "ellipse-full-button-svg" display = "none"}}
</button>
<button title="Increase Ellipse Size" id="ellipse-bigger-button" class="tools-menu-sub-button">{{svg "plus.svg" width="12" height="12"}}</button>
<button title="Decrease Ellipse Size" id="ellipse-smaller-button" class="tools-menu-sub-button">{{svg "minus.svg" width="12" height="12"}}</button>
<ul class="size-buttons">
<button title="Increase Ellipse Size" id="ellipse-bigger-button" class="tools-menu-sub-button">{{svg "plus.svg" width="12" height="12"}}</button>
<button title="Decrease Ellipse Size" id="ellipse-smaller-button" class="tools-menu-sub-button">{{svg "minus.svg" width="12" height="12"}}</button>
</ul>
</li>
<li class="expanded">
<button title="Line Tool (L)" id="line-button">{{svg "line.svg" width="32" height="32"}}</button>
<button title="Increase Line Size" id="line-bigger-button" class="tools-menu-sub-button">{{svg "plus.svg" width="12" height="12"}}</button>
<button title="Decrease Line Size" id="line-smaller-button" class="tools-menu-sub-button">{{svg "minus.svg" width="12" height="12"}}</button>
<button id="line-button">{{svg "line.svg" width="24" height="24"}}</button>
<ul class="size-buttons">
<button title="Increase Line Size" id="line-bigger-button" class="tools-menu-sub-button">{{svg "plus.svg" width="12" height="12"}}</button>
<button title="Decrease Line Size" id="line-smaller-button" class="tools-menu-sub-button">{{svg "minus.svg" width="12" height="12"}}</button>
</ul>
</li>
<li><button title="Fill Tool (F)" id="fill-button">{{svg "fill.svg" width="32" height="32"}}</button></li>
<li><button id="fill-button">{{svg "fill.svg" width="24" height="24"}}</button></li>
<li><button title="Eyedropper Tool (E)" id="eyedropper-button">{{svg "eyedropper.svg" width="32" height="32"}}</button></li>
<li><button id = "rectselect-button">{{svg "rectselect.svg" width = "24" height = "24"}}</button><li>
<li><button title="Pan Tool (P)" id="pan-button">{{svg "pan.svg" width="32" height="32"}}</button></li>
<li><button id = "lassoselect-button">{{svg "lasso.svg" width = "26" height = "26"}}</button></li>
<li><button title = "Rectangular Selection Tool (M)" id = "rectselect-button">{{svg "rectselect.svg" width = "32" height = "32"}}</button><li>
</ul>
<li><button id = "magicwand-button">{{svg "magicwand.svg" width = "26" height = "26"}}</button></li>
<li><button id="eyedropper-button">{{svg "eyedropper.svg" width="24" height="24"}}</button></li>
<li><button id="pan-button">{{svg "pan.svg" width="24" height="24"}}</button></li>
</ul>
<div id="tool-tutorial" class="fade-in">
<h3>Brush tool</h3>
<ul>
<li><span class="keyboard-key">B</span> to select the tool</li>
<li><span class="keyboard-key">Left drag</span> to use the tool</li>
<li><span class="keyboard-key">Right drag</span> to change tool size</li>
<li><span class="keyboard-key">+</span> or <span class="keyboard-key">-</span> to change tool size</li>
</ul>
<img src="brush-tutorial.gif"/>
</div>"