Merge branch 'next-update' into new-feature

This commit is contained in:
Nicola 2022-02-05 17:40:03 +01:00 committed by GitHub
commit ceac61e8b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
65 changed files with 1745 additions and 579 deletions

View File

@ -6,7 +6,8 @@ The tool can be viewed online here: https://lospec.com/pixel-editor
## How to contribute
Please do not submit pull requests with new features or core changes. Instead, please file an issue first for discussion.
Before starting to work, please open an issue for discussion so that we can organize the work without creating too many conflicts. If your contribution is going to fix a bug, please
make a fork and use the bug-fixes branch. If you want to work on a new feature, please use the new-feature branch instead.
## What to Contribute

74
README.md.bak Normal file
View File

@ -0,0 +1,74 @@
# Lospec Pixel Editor
This is a browser based software for creating pixel art
The tool can be viewed online here: https://lospec.com/pixel-editor
## How to contribute
Please do not submit pull requests with new features or core changes. Instead, please file an issue first for discussion.
## What to Contribute
Any changes that fix bugs or add features are welcome. Check out the issues if you don't know where to start: if
you're new to the editor, we suggest you check out the Wiki first.
The next version is mostly focused on adding missing essential features and porting to mobile.
Suggestions / Planned features:
- Documentation
- Possibility to hide and resize menus (layers, palette)
- Tiled mode
- Load palette from LPE file
- Symmetry options (currently being worked on)
- Make a palette grid instead of having a huge stack on the right when colours are too many
- Possibly add collaborate function
- Mobile
- Touch equivalent for mouse clicks
- Hide or scale ui
- Maybe rearrange UI on portrait
- Fix popups
- Polish:
- CTRL+A to select everything / selection -> all, same for deselection
- Warning windows for wrong inputs
- Palette option remove unused colors
- Move selection with arrows
- Update borders by dragging the canvas' edges with the mouse when resizing canvas
- Move the canvases so they're centered after resizing the canvas (maybe a .center() method in layer class)
- Scale / rotate selection
## How to Contribute
### Requirements
You must have node.js and git installed.
You also need `npm` in version 7 (because of 2nd version of lockfile which was introduced there) which comes with Node.js 15 or newer. To simplify installation of proper versions you can make use of [nvm](https://github.com/nvm-sh/nvm#installing-and-updating) and run `nvm install` it will activate proper Node.js version in your current command prompt session.
### Contribution Workflow
1. Click **Fork** above. It will automatically create a copy of this repository and add it to your account.
2. Clone the repository to your computer.
3. Open the folder in command prompt and run **`npm install`**
4. Make any changes you would like to suggest.
5. In command prompt run **`npm run hot`** which will compile app to the `/build` folder, serve under [http://localhost:3000](http://localhost:3000), then open in your browser. Moreover, it restarts server every time you save your changes in a codebase. You can go even further by running `npm run hot:reload`, which will also trigger webpage reloads.
6. Add, Commit and Push your changes to your fork.
7. On the github page for your fork, click **New Pull Request** above the file list.
8. Change the **head repository** dropdown to your fork.
9. Add a title and description explaining your changes.
10. Click create pull request.
If you have any trouble, see this page: https://help.github.com/en/articles/creating-a-pull-request-from-a-fork
### Feature Toggles
Some feature might be hidden by default. Functions to enable/disable them are available inside global `featureToggles` and operate on a `window.localStorage`.
For example use `featureToggles.enableEllipseTool()` to make ellipse tool button visible. Then `featureToggles.disableEllipseTool()` to hide it.
## License
This software may not be resold, redistributed, rehosted or otherwise conveyed to a third party.

View File

@ -17,12 +17,16 @@ console.log('Building Pixel Editor');
function copy_images(){
// Icons
gulp.src('./images/*.png').pipe(gulp.dest(BUILDDIR));
//favicon
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() {
@ -52,7 +56,7 @@ function compile_page(){
.pipe(handlebars({encoding: 'utf8', debug: true, bustCache: true})
.partials('./views/[!index]*.hbs').partials('./views/popups/*.hbs')
//.helpers({ svg: hb_svg })
.partials('./views/components/*.hbs')
.helpers('./helpers/**/*.js')
.data({
projectSlug: 'pixel-editor',

View File

@ -1,7 +1,7 @@
.drawingCanvas {
cursor: url('/pixel-editor/pencil-tool-cursor.png');
cursor: url('pencil-tool-cursor.png');
border: solid 1px #fff;
image-rendering: optimizeSpeed;
@ -95,7 +95,7 @@
#canvas-view {
bottom: 0px;
left: 64px;
left: 48px;
right: 48px;
top: 48px;
cursor: default;
@ -107,7 +107,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;

View File

@ -112,7 +112,7 @@
button {
border: none;
width: 100%;
cursor: url('/pixel-editor/eyedropper.png'), auto;
cursor: url('eyedropper.png'), auto;
} //white outline
&.selected button::before {
content: "";

56
css/_components.scss Normal file
View File

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

View File

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

View File

@ -116,7 +116,7 @@
}
.dropdown-button {
background: $basehover url('/pixel-editor/dropdown-arrow.png') right center no-repeat;
background: $basehover url('dropdown-arrow.png') right center no-repeat;
border: none;
border-radius: 4px;
color: $basehovertext;
@ -126,7 +126,7 @@
width: 200px;
text-align: left;
&:hover {
background: $baseselected url('/pixel-editor/dropdown-arrow-hover.png') right center no-repeat;
background: $baseselected url('dropdown-arrow-hover.png') right center no-repeat;
color: $baseselectedtext;
}
&.selected {

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 972 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 580 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 676 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 757 KiB

BIN
images/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

View File

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

View File

@ -3,6 +3,7 @@ class File {
canvasSize = [];
zoom = 7;
canvasView = document.getElementById("canvas-view");
inited = false;
// Layers
layers = [];
@ -36,11 +37,13 @@ class File {
// Start resize data
startData = {width: 0, height:0, widthPercentage: 100, heightPercentage: 100};
// Sprite scaling attributes
openResizeCanvasWindow() {
if (!this.inited) {
this.initResizeCanvasInputs();
this.inited = true;
}
// Initializes the inputs
this.initResizeCanvasInputs();
Dialogue.showDialogue('resize-canvas');
}
@ -153,6 +156,9 @@ class File {
currFile.canvasSize[1] = parseInt(currFile.canvasSize[1]) +
this.rcBorders.top + this.rcBorders.bottom;
console.trace();
console.log(currFile.canvasSize);
// Resize the canvases
for (let i=0; i<currFile.layers.length; i++) {
currFile.layers[i].canvas.width = currFile.canvasSize[0];

View File

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

73
js/InputComponents.js Normal file
View File

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

View File

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

View File

@ -6,9 +6,10 @@ 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);
tools["eyedropper"] = new EyeDropperTool("eyedropper", {type: 'cursor', style: 'crosshair'}, switchTool);
tools["pan"] = new PanTool("pan", {type: 'custom'}, switchTool);
tools["zoom"] = new ZoomTool("zoom", {type:'custom'});
@ -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);
@ -36,7 +44,6 @@ const ToolManager = (() => {
}
function onMouseWheel(mouseEvent) {
console.log("MOUSE WHEEL");
if (!EditorState.documentCreated || Dialogue.isOpen())
return;
@ -109,7 +116,7 @@ const ToolManager = (() => {
}
function onMouseUp(mouseEvent) {
if (!EditorState.documentCreated())
if (!EditorState.documentCreated || Dialogue.isOpen())
return;
let mousePos = Input.getCursorPosition(mouseEvent);
@ -123,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);
@ -142,10 +149,13 @@ const ToolManager = (() => {
return currTool;
}
function switchTool(newTool) {
function switchTool(newTool, event) {
currTool.onDeselect();
currTool = newTool;
currTool.onSelect();
if (event != undefined)
event.stopPropagation();
}
return {

View File

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

View File

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

View File

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

View File

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

View File

@ -9,11 +9,16 @@
/** UTILITY AND INPUT **/
//=include Util.js
//=include Events.js
//=include InputComponents.js
//=include Dialogue.js
//=include History.js
//=include Settings.js
//=include EditorState.js
/** MENUS **/
//=include FileManager.js
//=include TopMenuModule.js
/** COLOR-RELATED **/
//=include Color.js
//=include ColorPicker.js
@ -39,12 +44,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/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
@ -55,8 +63,6 @@
/** STARTUP AND FILE MANAGEMENT **/
//=include Startup.js
//=include FileManager.js
//=include TopMenuModule.js
/** HTML INPUT EVENTS **/
//=include Input.js
@ -69,9 +75,10 @@ PresetModule.instrumentPresetMenu();
//when the page is done loading, you can get ready to start
window.onload = function () {
featureToggles.onLoad();
// First cursor update
ToolManager.currentTool().updateCursor();
// Apply checkboxes
//check if there are any url parameters
if (window.location.pathname.replace('/pixel-editor/','').length <= 1) {

View File

@ -5,11 +5,34 @@ 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) {
onStart(mousePos, cursorTarget) {
super.onStart(mousePos);
if (cursorTarget === undefined)
return;
new HistoryState().EditCanvas();
//draw line to current pixel
if (cursorTarget.className == 'drawingCanvas' || cursorTarget.className == 'drawingCanvas') {
currFile.currentLayer.drawLine(
Math.floor(mousePos[0]/currFile.zoom),
Math.floor(mousePos[1]/currFile.zoom),
Math.floor(mousePos[0]/currFile.zoom),
Math.floor(mousePos[1]/currFile.zoom),
this.currSize
);
}
currFile.currentLayer.updateLayerPreview();
}
onDrag(mousePos, cursorTarget) {
@ -23,7 +46,7 @@ class BrushTool extends ResizableTool {
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
);
@ -84,7 +107,7 @@ class BrushTool extends ResizableTool {
this.mirrorDraw(mirrorPrevX, mirrorPrevY, mirrorCurrentX, mirrorCurrentY, true, true);
}
}
currFile.currentLayer.updateLayerPreview();
}

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

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

View File

@ -5,6 +5,14 @@ class EraserTool extends ResizableTool {
Events.on('click', this.mainButton, switchFunction, this);
Events.on('click', this.biggerButton, this.increaseSize.bind(this));
Events.on('click', this.smallerButton, this.decreaseSize.bind(this));
this.resetTutorial();
this.addTutorialTitle("Eraser tool");
this.addTutorialKey("E", " to select the eraser");
this.addTutorialKey("Left drag", " to erase an area");
this.addTutorialKey("Right drag", " to resize the eraser");
this.addTutorialKey("+ or -", " to resize the eraser");
this.addTutorialImg("eraser-tutorial.gif");
}
onStart(mousePos) {
@ -23,7 +31,7 @@ class EraserTool extends ResizableTool {
Math.floor(this.prevMousePos[1]/currFile.zoom),
Math.floor(this.currMousePos[0]/currFile.zoom),
Math.floor(this.currMousePos[1]/currFile.zoom),
this.currSize
this.currSize, true
);
}

View File

@ -1,4 +1,4 @@
class EyedropperTool extends Tool {
class EyeDropperTool extends Tool {
eyedropperPreview = document.getElementById("eyedropper-preview");
selectedColor = {r:0, g:0, b:0};
@ -6,6 +6,15 @@ class EyedropperTool extends Tool {
super(name, options);
Events.on('click', this.mainButton, switchFunction, this);
this.resetTutorial();
this.addTutorialTitle("Eyedropper tool");
this.addTutorialKey("E", " to select the lasso selection tool");
this.addTutorialKey("Left drag", " to preview the picked colour");
this.addTutorialKey("Aòt + left drag", " to preview the picked colour");
this.addTutorialKey("Left click", " to select a colour");
this.addTutorialKey("Alt + click", " to select a colour");
this.addTutorialImg("eyedropper-tutorial.gif");
}
onStart(mousePos, target) {

View File

@ -3,6 +3,12 @@ class FillTool extends DrawingTool {
super(name, options);
Events.on('click', this.mainButton, switchFunction, this);
this.resetTutorial();
this.addTutorialTitle("Fill tool");
this.addTutorialKey("F", " to select the fill tool");
this.addTutorialKey("Left click", " to fill a contiguous area");
this.addTutorialImg("fill-tutorial.gif");
}
onStart(mousePos, target) {
@ -13,8 +19,9 @@ class FillTool extends DrawingTool {
if (target.className != 'drawingCanvas')
return;
this.fill(mousePos);
new HistoryState().EditCanvas();
FillTool.fill(mousePos);
let midX = (currFile.canvasSize[0] / 2);
let midY = (currFile.canvasSize[1] / 2);
@ -28,7 +35,7 @@ class FillTool extends DrawingTool {
mirrorY = Math.floor(midY - Math.abs(midY - y0));
}
let symmetryPos = [mousePos[0], mirrorY * currFile.zoom];
this.fill(symmetryPos);
FillTool.fill(symmetryPos);
}
if (currFile.vSymmetricLayer.isEnabled) {
@ -38,21 +45,19 @@ class FillTool extends DrawingTool {
mirrorX = Math.floor(midX - Math.abs(midX - x0));
}
let symmetryPos = [mirrorX * currFile.zoom, mousePos[1]];
this.fill(symmetryPos);
FillTool.fill(symmetryPos);
}
if (currFile.hSymmetricLayer.isEnabled && currFile.vSymmetricLayer.isEnabled) {
let symmetryPos = [mirrorX * currFile.zoom, mirrorY * currFile.zoom];
this.fill(symmetryPos);
FillTool.fill(symmetryPos);
}
currFile.currentLayer.updateLayerPreview();
new HistoryState().EditCanvas();
}
fill(cursorLocation) {
static fill(cursorLocation, context) {
//changes a pixels color
function colorPixel(tempImage, pixelPos, fillColor) {
//console.log('colorPixel:',pixelPos);
@ -74,8 +79,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)]];
@ -88,7 +96,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 &&
@ -153,7 +161,7 @@ class FillTool extends DrawingTool {
pixelPos += currFile.canvasSize[0] * 4;
}
}
currFile.currentLayer.context.putImageData(tempImage, 0, 0);
context.putImageData(tempImage, 0, 0);
}
onDrag(mousePos, cursorTarget) {

View File

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

View File

@ -5,6 +5,14 @@ class LineTool extends ResizableTool {
Events.on('click', this.mainButton, switchFunction, this);
Events.on('click', this.biggerButton, this.increaseSize.bind(this));
Events.on('click', this.smallerButton, this.decreaseSize.bind(this));
this.resetTutorial();
this.addTutorialTitle("Line tool");
this.addTutorialKey("L", " to select the line");
this.addTutorialKey("Left drag", " to draw a line");
this.addTutorialKey("Right drag", " to resize the brush");
this.addTutorialKey("+ or -", " to resize the brush");
this.addTutorialImg("line-tutorial.gif");
}
onStart(mousePos) {

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

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

View File

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

View File

@ -4,13 +4,20 @@ 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) {
super.onStart(mousePos);
if (target.className != 'drawingCanvas')
return;
currFile.canvasView.style.cursor = "url(\'/pixel-editor/pan-held.png\'), auto";
currFile.canvasView.style.cursor = "url(\'pan-held.png\'), auto";
}
onDrag(mousePos, target) {
@ -31,12 +38,12 @@ class PanTool extends Tool {
if (target.className != 'drawingCanvas')
return;
currFile.canvasView.style.cursor = "url(\'/pixel-editor/pan.png\'), auto";
currFile.canvasView.style.cursor = "url(\'pan.png\'), auto";
}
onSelect() {
super.onSelect();
currFile.canvasView.style.cursor = "url(\'/pixel-editor/pan.png\'), auto";
currFile.canvasView.style.cursor = "url(\'pan.png\'), auto";
}
onDeselect() {

View File

@ -1,5 +1,3 @@
// TODO: FIX SELECTION
class RectangleTool extends ResizableTool {
// Saving the empty rect svg
emptyRectangleSVG = document.getElementById("rectangle-empty-button-svg");
@ -22,6 +20,15 @@ class RectangleTool extends ResizableTool {
Events.on('click', this.mainButton, this.changeFillType.bind(this));
Events.on('click', this.biggerButton, this.increaseSize.bind(this));
Events.on('click', this.smallerButton, this.decreaseSize.bind(this));
this.resetTutorial();
this.addTutorialTitle("Rectangle tool");
this.addTutorialKey("U", " to select the rectangle");
this.addTutorialKey("U while selected", " to change fill mode (empty or fill)");
this.addTutorialKey("Left drag", " to draw a rectangle");
this.addTutorialKey("Right drag", " to resize the brush");
this.addTutorialKey("+ or -", " to resize the brush");
this.addTutorialImg("rectangle-tutorial.gif");
}
changeFillType() {

View File

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

View File

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

View File

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

View File

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

View File

@ -5,12 +5,46 @@ const app = express();
const BUILDDIR = process.argv[2] || './build';
const PORT = process.argv[3] || 3000;
const FULLBUILDPATH = path.join(__dirname, BUILDDIR)
//LOGGING
app.use((req, res, next)=> {
//console.log('REQUEST', req.method+' '+req.originalUrl, res.statusCode);
next();
});
//apply to every request
app.use((req, res, next) => {
//disable caching
res.set("Cache-Control", "no-store");
//enabled/disable reload module
if (process.env.RELOAD === "yes") res.locals.reload = true;
return next();
});
//ROUTE - other files
app.use('/pixel-editor', express.static(path.join(__dirname, BUILDDIR)));
app.use('/', express.static(FULLBUILDPATH, {
//custom function required for logging static files
setHeaders: (res, filePath, fileStats) => {
console.info('GET', '/'+path.relative(FULLBUILDPATH, filePath), res.statusCode);
}
}));
//ROUTE - match / or any route with just numbers letters and dashes, and return index.htm (all other routes should have been handled already)
app.get('/', (req, res, next) => {
console.log('root')
res.sendFile(path.join(__dirname, BUILDDIR, 'index.htm'), {}, function (err) {
console.log('sent file');
return next();
});
});
//HOT RELOADING
// "reload" module allows us to trigger webpage reload automatically on file changes, but inside pixel editor it also
// makes browser steal focus from any other window in order to ask user about unsaved changes. It might be quite
// intrusive so we decided to give option to choose preferred workflow.
@ -19,6 +53,7 @@ if (process.env.RELOAD === "yes") {
//start server
app.server = app.listen(PORT, () => {
console.log(`Web server listening on port ${PORT} (with reload module)`);
})
});
} else {
@ -27,25 +62,13 @@ if (process.env.RELOAD === "yes") {
})
}
// Better to show landing page rather than 404 on editor page reload
app.get('/', (req, res) => {
res.redirect('/pixel-editor');
})
//ROUTE - match / or any route with just numbers letters and dashes, and return index.htm (all other routes should have been handled already)
app.get(['/pixel-editor', /^\/pixel-editor\/[\/a-z0-9-]+$/gi ], (req, res) => {
res.sendFile(path.join(__dirname, BUILDDIR, 'index.htm'), {}, function (err) {
if (err) {
console.log('error sending file', err);
} else {
console.log("Server: Successfully served index.html", req.originalUrl);
}
});
app.use(function(req, res, next) {
res.status(404);
res.type('txt').send('The requested resource does not exist. Did you spell it right? Did you remember to build the app? It\'s probably your fault somehow.');
return next();
});
// Better to show landing page rather than 404 on editor page reload
app.get('/pixel-editor/app', (req, res) => {
res.redirect('/');
})
//LOGGING
app.use((req, res, next)=> {
console.log(req.method+' '+req.originalUrl, res.statusCode);
});

10
svg/check.svg Normal file
View File

@ -0,0 +1,10 @@
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="405.272px" height="405.272px" viewBox="0 0 405.272 405.272" style="enable-background:new 0 0 405.272 405.272;"
xml:space="preserve">
<g>
<path d="M393.401,124.425L179.603,338.208c-15.832,15.835-41.514,15.835-57.361,0L11.878,227.836
c-15.838-15.835-15.838-41.52,0-57.358c15.841-15.841,41.521-15.841,57.355-0.006l81.698,81.699L336.037,67.064
c15.841-15.841,41.523-15.829,57.358,0C409.23,82.902,409.23,108.578,393.401,124.425z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 581 B

View File

@ -1,6 +1,9 @@
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
<g>
<ellipse stroke="#000" stroke-width="32" fill="none" cx="255.50001" cy="255.5" id="svg_20" rx="239" ry="187.5"/>
</g>
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 18 18" style="enable-background:new 0 0 17 17;" xml:space="preserve">
<g>
<ellipse cx="9" cy="9" rx="7.5" ry="6.2" stroke="black" stroke-width="2" fill="none" fill-opacity="0"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 221 B

After

Width:  |  Height:  |  Size: 471 B

View File

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

Before

Width:  |  Height:  |  Size: 528 B

After

Width:  |  Height:  |  Size: 422 B

19
svg/lasso.svg Normal file
View File

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

After

Width:  |  Height:  |  Size: 1.7 KiB

8
svg/magicwand.svg Normal file
View File

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

After

Width:  |  Height:  |  Size: 719 B

View File

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

View File

@ -6,7 +6,7 @@
<title>{{title}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,400i,700,900" rel="stylesheet">
<link rel="stylesheet" href="/pixel-editor/pixel-editor.css" />
<link rel="stylesheet" href="/pixel-editor.css" />
<meta name="ROBOTS" content="NOINDEX, NOFOLLOW">
{{{google-analytics}}}
{{{favicons}}}
@ -40,7 +40,7 @@
{{> save-project}}
</div>
<script src="/pixel-editor/pixel-editor.js"></script>
<script src="/reload/reload.js"></script>
<script src="/pixel-editor.js"></script>
{{#reload}}<script src="/reload/reload.js"></script>{{/reload}}
</body>
</html>

View File

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

View File

@ -7,7 +7,7 @@ The editor now has a splash page! Besides a fancy cover image with beautiful art
left of the page you'll be able to create a new custom pixel. You can also use the quickstart
menu to quickly select a preset or load an existing file. It was designed by <a href="https://twitter.com/skeddles">Skeddles</a> himself!
<img src="/pixel-editor/splash.gif"/>
<img src="splash.gif"/>
<strong>Pro tip: </strong> once you've created a new project, you can go back to the splash page
by clicking on <strong>Editor -> Splash page</strong>
@ -19,7 +19,7 @@ You can now click on <strong>Edit -> Resize canvas</strong> to decrease the size
drew an ant on a 1024x1024 canvas, just go to <strong>Edit -> Resize canvas</strong> and decrease
the dimensions.
<img src="/pixel-editor/resize-canvas.gif"/>
<img src="resize-canvas.gif"/>
<h2>Sprite scaling</h2>
@ -29,14 +29,14 @@ to scale up or down your work. With the nearest-neighbour algorithm you'll be ab
in a pixel-perfect manner, while with bilinear interpolation it's possible to add (or remove, if you're scaling
down a sprite) antialiasing.
<img src="/pixel-editor/scale-sprite.gif"/>
<img src="scale-sprite.gif"/>
<h2>Line tool</h2>
Our contributor <a href="https://github.com/liamortiz">Liam</a> added a new line tool! Quality of
life improvement are planned for it, the rectangle and the rectangular selection tools.
<img src="/pixel-editor/line-tool.gif"/>
<img src="line-tool.gif"/>
<h2>Advanced mode: colour picker and palette block</h2>
@ -46,7 +46,7 @@ you'll find the new palette block, which lets you arrange your colours however y
remove multiple colours at once. Changes made in the palette block will update the palette list
you've always been familiar with.
<img src="/pixel-editor/palette-block.gif"/>
<img src="palette-block.gif"/>
<h2>Other changes:</h2>
<ul>

View File

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

View File

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

View File

@ -12,7 +12,7 @@
<div id="editor-logo">
<div id="black">
<div id="sp-coverdata">
<img src="https://lospec.com/brand/lospec_logo_3x.png"/> pixel editor
<img src="https://cdn.lospec.com/static/brand/lospec_logo_3x.png"/> pixel editor
<p>Version 1.4.0</p>
<a href="https://cdn.discordapp.com/attachments/506277390050131978/795660870221955082/final.png">Art by Unsettled</a>
</div>

View File

@ -1,14 +1,14 @@
<div class="preload">
<img src="/pixel-editor/dropdown-arrow.png" />
<img src="/pixel-editor/dropdown-arrow-hover.png" />
<img src="/pixel-editor/eyedropper.png" />
<img src="/pixel-editor/fill.png" />
<img src="/pixel-editor/pan.png" />
<img src="/pixel-editor/pan-held.png" />
<img src="/pixel-editor/pencil.png" />
<img src="/pixel-editor/zoom-in.png" />
<img src = "/pixel-editor/eraser.png"/>
<img src = "/pixel-editor/rectselect.png"/>
<img src="dropdown-arrow.png" />
<img src="dropdown-arrow-hover.png" />
<img src="eyedropper.png" />
<img src="fill.png" />
<img src="pan.png" />
<img src="pan-held.png" />
<img src="pencil.png" />
<img src="zoom-in.png" />
<img src = "eraser.png"/>
<img src = "rectselect.png"/>
<!-- TODO: [ELLIPSE] Where is this icon used? Do we need similar one for ellipsis? -->
<img src= "/pixel-editor/rectangle.png">
<img src= "rectangle.png">
</div>

View File

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