Merge pull request #75 from lospec/next-update
Lasso, Magic Wand, Ellipse
8
build.js
@ -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',
|
||||
|
@ -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
@ -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;}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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 */
|
||||
|
@ -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;
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
@import 'general';
|
||||
@import 'zindex';
|
||||
@import 'shake';
|
||||
@import 'components';
|
||||
@import 'help';
|
||||
@import 'layers';
|
||||
@import 'canvas';
|
||||
|
BIN
images/Logs/tool-tutorials.gif
Normal file
After Width: | Height: | Size: 972 KiB |
BIN
images/ToolTutorials/brush-tutorial.gif
Normal file
After Width: | Height: | Size: 580 KiB |
BIN
images/ToolTutorials/ellipse-tutorial.gif
Normal file
After Width: | Height: | Size: 391 KiB |
BIN
images/ToolTutorials/eraser-tutorial.gif
Normal file
After Width: | Height: | Size: 334 KiB |
BIN
images/ToolTutorials/eyedropper-tutorial.gif
Normal file
After Width: | Height: | Size: 1.3 MiB |
BIN
images/ToolTutorials/fill-tutorial.gif
Normal file
After Width: | Height: | Size: 676 KiB |
BIN
images/ToolTutorials/lassoselect-tutorial.gif
Normal file
After Width: | Height: | Size: 3.0 MiB |
BIN
images/ToolTutorials/line-tutorial.gif
Normal file
After Width: | Height: | Size: 1.4 MiB |
BIN
images/ToolTutorials/magicwand-tutorial.gif
Normal file
After Width: | Height: | Size: 300 KiB |
BIN
images/ToolTutorials/pan-tutorial.gif
Normal file
After Width: | Height: | Size: 11 MiB |
BIN
images/ToolTutorials/rectangle-tutorial.gif
Normal file
After Width: | Height: | Size: 320 KiB |
BIN
images/ToolTutorials/rectselect-tutorial.gif
Normal file
After Width: | Height: | Size: 757 KiB |
@ -1,4 +1,4 @@
|
||||
const featureToggles = (function featureTogglesModule() {
|
||||
/*const featureToggles = (function featureTogglesModule() {
|
||||
|
||||
const ellipseToolLocalStorageKey = 'feature_ellipseTool';
|
||||
|
||||
@ -30,4 +30,4 @@ const featureToggles = (function featureTogglesModule() {
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
*/
|
||||
|
11
js/Input.js
@ -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
@ -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
|
||||
}
|
||||
})();
|
90
js/Tool.js
@ -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;
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
})();
|
44
js/Util.js
@ -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];
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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++) {
|
||||
|
@ -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) {
|
||||
|
@ -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
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
95
js/tools/LassoSelectionTool.js
Normal 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);
|
||||
}
|
||||
}
|
@ -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
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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() {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
@ -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 |
@ -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 |
@ -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
@ -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
@ -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 |
7
views/components/checkbox.hbs
Normal 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>
|
@ -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>!
|
39
views/logs/00002refactor.hbs
Normal 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!
|
@ -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>
|
@ -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>"
|