Merge pull request #38 from unsettledgames/master

Palette block, colour picker, splash page, line tool, minor improvements, bug fixes
This commit is contained in:
Nicola 2021-07-01 14:52:21 +02:00 committed by GitHub
commit 890aec519a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
99 changed files with 9297 additions and 678 deletions

38
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

10
.github/ISSUE_TEMPLATE/custom.md vendored Normal file
View File

@ -0,0 +1,10 @@
---
name: Custom issue template
about: Describe this issue template's purpose here.
title: ''
labels: ''
assignees: ''
---

View File

@ -0,0 +1,17 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Additional context**
Add any other context or screenshots about the feature request here.

3
.gitignore vendored
View File

@ -1,4 +1,5 @@
_ext
/_ext/*
!/_ext/svg/
routes
build
node_modules

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
v15.14.0

1
Model.drawio Normal file
View File

@ -0,0 +1 @@
<mxfile host="app.diagrams.net" modified="2021-04-25T14:57:23.836Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.85 Safari/537.36 Edg/90.0.818.46" etag="K3S2uQVQjw_jQCK8DyHB" version="14.6.6" type="device"><diagram id="C5RBs43oDa-KdzZeNtuy" name="Page-1">7V1bc+K4Ev41VOU8kPI98BjIZGb3MHuyk9nM7KOCBWhiWxxZBJhfv5ItG2zJxgSMsx5XpSq4Jd/0tVqfultyzxz7m48ELBefsQu9nqG5m5551zMMfWAN2D8u2QqJMxSSOUGukO0Ej+gnFEJNSFfIhWGmIsXYo2iZFU5xEMApzcgAIXidrTbDXvauSzCHkuBxCjxZ+g25dBFLB8bNTv4JovkiuTN7wbjEB0ll8SbhArh4vScyP/TMMcGYxr/8zRh6vPWSdvn22/abN3lxPv7+Z/h/8Nfov1//eOrHF7s/5pT0FQgM6Jsv3X+6tz5PHp5mqz/+fnn83+Oy/xX3LUe8G90mDQZd1n7iEBO6wHMcAO/DTjoieBW4kF9WY0e7OhOMl0yoM+EPSOlWKANYUcxEC+p7opS9Btl+Zwd97VpLBX/zC15rw1RwtxG3iI+2+0cPkCAfUkiEMH4N/uw52A+0magX4hWZilN/vszuPz1ZP74vH+7/XGsfb59/74u21Sggc0hLGnRPM1ifgpg9I9myEwn0AEWv2acDQrfnab0dfuyHgFANZ9lTvgJvJe70lfW18DMMVhLO4Rr5Hgg4oDMc0ARy9vAj4KF5wH5PWdvxFh69QkIR61O3ooByoEfTBfLcCdjiFW+RkILpS3I0WmCCfrLLggRzVkyo0AjDydR45GcKHAkMWZ2HBDY9FU1ASEWdKfY8sAzRc/TAvIrPYEHBCFOK/eRCGSVN+250QAl+Sa2BnmpPVWXhrQE3e0jKcItSJ7GCifU0xfF6Z4p0R8gW+2bI0opVRNzuCzOXIJizRkjvZ5i5+1kV72c42dsBjwEfAApHvBlDSTHTV327rpqSrvbF2BBKusqam+7ppQdntFArwyWYomA+iercWTvJF/G6XITZuTMv0ogFcl0YRBpDAQWxUnE1WWIU0Kg97BH7Y6021q7tns0eaMyO9d0x++PVCR3jgCkXQJEWQaaxa8i1tkwZhVnUlFpY2skPa+E2C+6xSnA2s2RJUEsIeyiyRHuDhNxLD8DvMyCj8Ung/TUajfq6pBOmrBOmAn8PPEPvAYeIIsyvT+K6Ob04BH1doNpGNVAHNWFqKzAdcUyhx+wSH3aukPufHtcpU4+1mxczLpir0LKuXhfeTkVLPigZOKoCrqaKg0apIm//faJ4gCSekQ8qW6MqH9SdC/FB5d1tu1HMctTesP8loJlmo6DdNAHae2l7Y9hk2zuNzIffS9s7dpNtL09eH9AGspZGFBMJlm76mp++SlyiqgYVTl9T11fJ9NVQziTMEtr5r5u+qgd2SVv59NXD83CP8/YjQkwpo6R58ZLpBaRhhiBnKsyQB/MngSnvhveIN1rZebHDJyOmO0dQ9jmAxx5QdYYHtpCoTnERYO+5go/bkEK/+Elipg9dRpUpV8O2Uf1jZvUHOmexQTxmVq/ui5pVk8E2pC4gQdzyaX0NqFY1sWeY16vZpwziLzzNsyoyJ91okjnJ/rV4iOgo08UpUz/lLMe6/NOIaXs5k+wz7Ee9lxN8HnrO8QcU3kZ8p5hhxBSluDzmSxNeS8lN2AigKnydbVTi6QJOXyB5xoC4orxjNEeNfdaxjEYdHDPrYjROx2jOjqpe2XNdE6g3ClB5KAIHkXlhFu9KjlPggLXNXnHLOnp9YA+qgW0aNaE9kND+8MreaLSSo8odH6qfDzk5OmQZFa2BrrWeDg2VdIgpmffM1OHXzoJ4i2kanIVdnCMLQj1HlB3cHbs4FdULpkGoQZX9wAmBuIKvkddzvNepZaIBfUTjmh3LqAp6VUZZlkR3Guqy61M9IewoxiVcLo59kGOk3pWMqW9VlqVaU9VplgHwJW+Le9j9EoufUKwWefkET19g/irTJPhT5KNxCVgzMzUGwStQBMleEVwnZS0zj3XzoSKndEkASdVL6nO36F1iaA24qhwuSmBr40RFuaEUz5khjc0HoluF2yWuERsSRfFq6TJr+bBvFXK1WmYialQS2VGjVJKbuhw1tiWhddHcxGtNy4Yar7Ubo9fs0qOSFNJ3H4TUZed5xIm7xUdNZW+92fV20/7sLTkooIwnciG3Hh886HcpTG8YhQrS34/0ydXHQOVwQcdAT4a16YifLnvWY/4I3Dhwr6CWBPpYpAqoFy6tlh6DlJZU8SEboEeQ9V5l8YyNwmwqfOt5ituLQjG1Vj4fn7OnT98yO1SfKlaMR+pl2TenZUmqF2V3lKgBSjSwsxSlqqHSh613FBpyIKPIURg+isTuNs+862ZGRRtFHMmMrNqSu+UQR8eMTob1pmFmZMjhALFsmw8HKo9bqCIjIeu3qsooJ5SSqmLLUZBvFaaFLbMl9alTZXZTlxfPkF34Xzg/4A3Z0ZyGaI6uDd/Ic8z28xx1Evp0RXiTR5rSNvNTO5U5T1p3bU4eQ/ZMd1TmZFibdvIYRXndKJgS1gCQ92UF03BhaXEIIyNwxY1/x0Sqa0NFJuLURkRkn9/D/tLmjoZcnIbktw/op5sT7WvJQMULjNbzkORR8zwEe7iLQJ2TnBTsJ1NCTlQaWZufxVQlEHfk5ERYVX4WFax1kZNkeZMqAjWOuvhV3NOLAlEllXL1uXIA8pEAF7Emi8/QRdY5/20oOA6DZLSdrHwUcARViVhxlTtAXgIY5pOsdhUeAV0RwHWgsMoY+0scxI/mLxW1fD40YtI5fo7RbwXdUum3btXFt0zZk3if7jzTka2Lk61+nmylC+gO6kj7V9qZqozUbpA9ZISK9kE8kjvVNsiq/Dp8SAngmtsiVZhhCYOCohC8wgeCf6jjE3DDm3ivvBupqilJ4tU7pCSOU5OW2LoE1q+8B6op3GYHs4mtgvHmMtnEpuzd+4RCisWlO3ZxYXZh5iJK1b9k0P5tjUw5iTTaiJEAH3ar1o4eT26OJh1KPajNYaNKLu245Imw2hW7d11c0pLdsSKnJXCxMh1XKeYkUoxTHUk8Av2K0aNkt4fzwy97YQWM9yTKgOw4x+U5h2O/kXO0P1s3ceup1i/dse6v2idRbMOcDy5No2W0XeLLW/ZCO893l+y6qIqlyvbsqMqJsKqC2BelKs181GWD6Pe937E7wxZHO2cGP8gsl/6+fxCfpVvmAS9IMwute5JnpPjbTU05RqxGvg5T9sVOXXPeI5pJqvvhL84UZDZdCM9ho3jqDE4ni6c2OOSjfC+985gNSi/07SatUTS16+FwkOud9rvc1MKuiGbRxPFCn4OSafZd7uMn3cQwObjYxNDOfVVXHepWOqPbn1foqDd8E+sbEu2V9Lab6pXPCWJLcLpXuq4op9NtpVYDrE3vLusUfmV3gddJX77ia7RlVzTrdfBAlamHQ3jreUm1sPNZV9eNqutfnJIhp0A52CHBmO4PDAQsF/wTKbzGPw==</diagram></mxfile>

View File

@ -12,16 +12,14 @@ The next version is mostly focused on adding missing essential features and port
Suggestions / Planned features:
- Documentation
- Possibility to hide and resize menus (layers, palette)
- Line tool
- Tiled mode
- Load palette from LPE file
- Move colours in (advanced) palette editor
- Symmetry options
- Custom color picker
- custom code without dependencies
- more features such as sliders / color modes
- Mobile
- Touch equivalent for mouse clicks
- Hide or scale ui
@ -32,25 +30,29 @@ Suggestions / Planned features:
- Possibly add collaborate function
- Polish:
- ctrl + a to select everything / selection -> all, same for deselection
- Show colors which would need to be added to palette
- 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 pivot buttons when resizing canvas
- Update borders by dragging the canvas' edges with the mouse when resizing canvas
- Move the canvases so they're centered after resizing the canvas (maybe a .center() method in layer class)
- Trim canvas
- Scale selection
## How to Contribute
Requirements: you must have node.js and git installed.
### 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**
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 **node build.js** which will compile it to the */build* folder, where you can make sure it works
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.
@ -59,6 +61,12 @@ Requirements: you must have node.js and git installed.
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

@ -7,4 +7,4 @@ function getText(elementId) {
function setText(elementId, text) {
var element = (typeof elementId == 'string' ? document.getElementById(elementId) : elementId);
element.textContent = text;
}
}

6
_ext/svg/ellipse.svg Normal file
View File

@ -0,0 +1,6 @@
<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>
</svg>

After

Width:  |  Height:  |  Size: 221 B

View File

@ -0,0 +1,22 @@
<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>
</svg>

After

Width:  |  Height:  |  Size: 528 B

41
_ext/svg/line.svg Normal file
View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512.001 512.001" style="enable-background:new 0 0 512.001 512.001;" xml:space="preserve">
<g>
<g>
<path d="M506.143,5.859c-7.811-7.811-20.475-7.811-28.285,0l-472,472c-7.811,7.811-7.811,20.474,0,28.284
c3.905,3.906,9.024,5.858,14.142,5.858s10.237-1.953,14.143-5.858l472-472C513.954,26.333,513.954,13.67,506.143,5.859z"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 799 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="332" height="80"><path fill="#eaeaea" d="M7 16H6V0H0v20h13v-7H7zm7 4h13V0H14v20zm6-16h1v12h-1V4zm15 4V4h1v3h5V0H28v12h7v4h-1v-3h-6v7h13V8zm7 12h6v-5h7V0H42v20zm6-16h1v7h-1V4zm8 16h13v-7h-6v3h-1v-4h7V8h-7V4h1v3h6V0H56zM83 8V0H70v20h13v-7h-6v3h-1V4h1v4z" class="a"/></svg>

After

Width:  |  Height:  |  Size: 317 B

17
_ext/svg/newfile.svg Normal file
View File

@ -0,0 +1,17 @@
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<g>
<g>
<path d="M456.606,154.394l-150-150C303.793,1.58,299.978,0,296,0H66c-8.284,0-15,6.716-15,15v482c0,8.284,6.716,15,15,15h380
c8.284,0,15-6.716,15-15V165C461,161.021,459.419,157.206,456.606,154.394z M431,482H81V30h208.787L431,171.213V482z"/>
</g>
</g>
<g>
<g>
<path d="M306,241h-35v-35c0-8.284-6.716-15-15-15s-15,6.716-15,15v35h-35c-8.284,0-15,6.716-15,15s6.716,15,15,15h35v35
c0,8.284,6.716,15,15,15s15-6.716,15-15v-35h35c8.284,0,15-6.716,15-15S314.284,241,306,241z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 815 B

16
_ext/svg/openfile.svg Normal file
View File

@ -0,0 +1,16 @@
<!-- Generator: Adobe Illustrator 17.1.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 276.157 276.157" style="enable-background:new 0 0 276.157 276.157;" xml:space="preserve">
<path style="fill:#000002;" d="M273.081,101.378c-3.3-4.651-8.86-7.319-15.255-7.319h-24.34v-26.47c0-10.201-8.299-18.5-18.5-18.5
h-85.322c-3.63,0-9.295-2.876-11.436-5.806l-6.386-8.735c-4.982-6.814-15.104-11.954-23.546-11.954H58.731
c-9.293,0-18.639,6.608-21.738,15.372l-2.033,5.752c-0.958,2.71-4.721,5.371-7.596,5.371H18.5c-10.201,0-18.5,8.299-18.5,18.5
v167.07c0,0.885,0.161,1.73,0.443,2.519c0.152,3.306,1.18,6.424,3.053,9.064c3.3,4.652,8.86,7.319,15.255,7.319h188.486
c11.395,0,23.27-8.424,27.035-19.179l40.677-116.188C277.061,112.159,276.381,106.03,273.081,101.378z M18.5,64.089h8.864
c9.295,0,18.64-6.608,21.738-15.372l2.032-5.75c0.959-2.711,4.722-5.372,7.597-5.372h29.564c3.63,0,9.295,2.876,11.437,5.806
l6.386,8.734c4.982,6.815,15.104,11.954,23.546,11.954h85.322c1.898,0,3.5,1.603,3.5,3.5v26.47H69.34
c-11.395,0-23.27,8.424-27.035,19.179L15,191.231V67.589C15,65.692,16.603,64.089,18.5,64.089z M260.791,113.238l-40.677,116.188
c-1.674,4.781-7.812,9.135-12.877,9.135H18.751c-1.448,0-2.577-0.373-3.02-0.998c-0.443-0.625-0.423-1.814,0.056-3.181
l40.677-116.188c1.674-4.781,7.812-9.135,12.877-9.135h188.486c1.448,0,2.577,0.373,3.021,0.998
C261.29,110.682,261.27,111.871,260.791,113.238z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -15,8 +15,16 @@ console.log('Building Pixel Editor');
function copy_images(){
gulp.src('./images/')
.pipe(gulp.dest(path.join(BUILDDIR, SLUG)));
// Icons
gulp.src('./images/*.png').pipe(gulp.dest(path.join(BUILDDIR, SLUG)));
// Splash images
gulp.src('./images/Splash images/*.png').pipe(gulp.dest(path.join(BUILDDIR, SLUG)));
// Logs images
gulp.src('./images/Logs/*.gif').pipe(gulp.dest(path.join(BUILDDIR, SLUG)));
}
function copy_logs() {
gulp.src('logs/*.html').pipe(gulp.dest(path.join(BUILDDIR, SLUG)));
}
function render_js(){
@ -56,4 +64,4 @@ function compile_page(){
// empty the build folder, or create it
fs.rmdirSync(BUILDDIR, { recursive: true });
fs.mkdirSync(BUILDDIR);
gulp.parallel(copy_images, render_js, render_css, compile_page)();
gulp.parallel(copy_images, copy_logs, render_js, render_css, compile_page)();

View File

@ -40,6 +40,33 @@ body {
/* Disable Android and iOS callouts*/
}
#help {
max-height: 500px;
overflow-y:scroll;
li {
margin-top:5px;
}
// Fancy scrollbar
&::-webkit-scrollbar {
background: #232125;
width: 0.5em;
}
&::-webkit-scrollbar-track {
margin-top: -0.125em;
width: 0.5em;
}
&::-webkit-scrollbar-thumb {
background: #332f35;
border-radius: 0.25em;
border: solid 0.125em #232125; //same color as scrollbar back to fake padding
}
&::-webkit-scrollbar-corner {
background: #232125;
}
}
#layer-properties-menu {
visibility: hidden;
margin: 0;
@ -125,7 +152,7 @@ body {
z-index: 1120;
list-style-type: none;
overflow-y:scroll;
overflow-x:hidden; // TODO: make the scroll bar a bit fancier
overflow-x:hidden;
#add-layer-button {
path {
fill: $baseicon;
@ -178,7 +205,7 @@ body {
.layers-menu-entry {
cursor: pointer;
margin-top: 2px;
margin-bottom: 2px;
font-size: 1em;
color: $basetext;
background-color: lighten($basecolor, 4%);
@ -301,7 +328,7 @@ svg {
}
#pixel-canvas {
z-index: 2;
z-index: 3;
background: transparent;
}
@ -311,7 +338,7 @@ svg {
}
#tmp-canvas {
z-index: 4;
z-index: 5;
background: transparent;
}
@ -354,7 +381,7 @@ svg {
left: 64px;
right: 48px;
top: 48px;
cursor: none;
cursor: default;
position: fixed;
display: block;
}
@ -687,14 +714,18 @@ svg {
#tools-menu li button#pencil-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#rectangle-bigger-button,
#tools-menu li button#ellipse-bigger-button,
#tools-menu li button#line-bigger-button {
left: 0;
}
#tools-menu li button#pencil-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#rectangle-smaller-button,
#tools-menu li button#ellipse-smaller-button,
#tools-menu li button#line-smaller-button {
right: 0;
}
@ -705,7 +736,11 @@ svg {
#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#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;
}
@ -775,8 +810,32 @@ svg {
}
}
div.update {
input {
background: $indent;
border: none;
border-radius: 4px;
color: $indenttext;
padding: 10px 20px;
margin: 0;
width: 60px;
text-align: center;
}
}
/*
input {
background: $indent;
border: none;
border-radius: 4px;
color: $indenttext;
padding: 10px 20px;
margin: 0;
width: 60px;
text-align: center;
}
*/
button.default {
float: right;
background: $basehover;
border: none;
border-radius: 4px;
@ -789,17 +848,6 @@ svg {
}
}
input {
background: $indent;
border: none;
border-radius: 4px;
color: $indenttext;
padding: 10px 20px;
margin: 0;
width: 60px;
text-align: center;
}
.dropdown-button {
background: $basehover url('/pixel-editor/dropdown-arrow.png') right center no-repeat;
border: none;
@ -1038,9 +1086,9 @@ svg {
input[type=number] {
position:relative;
margin-left:10px;
height:15px;
width:40px;
padding:8px;
height:15px !important;
width:40px !important;
padding:8px !important;
}
input[type=number]::-webkit-outer-spin-button,
@ -1212,4 +1260,602 @@ svg {
background: $baseselected;
}
}
}
/***********************COLOUR PICKER*****************************/
#colour-picker {
background-color:$basecolor;
width:250px;
height:350px;
position:absolute;
display:inline-block;
input[type=text] {
background-color:$basetext;
color:$basecolor;
box-shadow:none;
border:none;
}
input[type=range] {
width: 100%;
margin: 2.2px 0;
background-color: transparent;
-webkit-appearance: none;
}
input[type=range]::-webkit-slider-runnable-track {
background: #484d4d;
border: 0;
width: 100%;
height: 25.6px;
cursor: pointer;
}
input[type=range]::-webkit-slider-thumb {
margin-top: -2.2px;
width: 18px;
height: 30px;
background: $basetext;
border: 0;
cursor: pointer;
-webkit-appearance: none;
}
input[type=range]::-moz-range-track {
background: #484d4d;
border: 0;
width: 100%;
height: 25.6px;
cursor: pointer;
}
input[type=range]::-moz-range-thumb {
width: 18px;
height: 30px;
background: $basetextweak;
border: 0;
cursor: pointer;
}
/*TODO: Use one of the selectors from https://stackoverflow.com/a/20541859/7077589 and figure out
how to remove the vertical space around the range input in IE*/
@supports (-ms-ime-align:auto) {
/* Pre-Chromium Edge only styles, selector taken from hhttps://stackoverflow.com/a/32202953/7077589 */
input[type=range].slider {
margin: 0;
/*Edge starts the margin from the thumb, not the track as other browsers do*/
}
}
}
#cp-modes {
margin: 0 0 0 0;
font-size:0;
height:40px;
float:left;
display:flex;
font-family: 'Roboto', sans-serif;
background-color:$basetextweak;
width:100%;
button {
font-size:14px;
left:0;
right:0;
margin:0 0 0 0;
border: none;
border-radius: 0;
height:100%;
width:37x;
background-color:$basehover;
color:$basetext;
cursor:pointer;
}
button:hover {
background-color:$baseicon;
color:$basetext;
}
button.cp-selected-mode {
background-color:$baseicon;
color:$basetext;
}
input {
width:60px;
}
div {
background-color:yellow;
width:100%;
height:100%;
z-index:2;
position:relative;
}
}
#sliders-container {
padding:10px;
}
.cp-slider-entry {
width:100%;
height:30px;
display:flex;
align-items:center;
margin-top:2px;
position:relative;
label {
width: 20px;
font-size:15px;
font-style: bold;
}
input[type=text] {
text-align:center;
width: 30px;
overflow:visible;
margin-left:4px;
}
}
.colour-picker-slider {
width:90%;
}
#cp-minipicker {
width:100%;
height:100px;
position:relative;
margin: 0 0 0 0;
z-index: inherit 2000;
input {
width:100%;
margin: none;
padding: none;
}
.cp-colours-previews {
width:100%;
position:relative;
}
.cp-colour-preview {
width:100%;
position:relative;
background-color:blue;
color:$basecolor;
float:left;
height:30px;
justify-content: center;
display:flex;
align-items: center;
font-size:12px;
}
#cp-colour-picking-modes {
width:100%;
position:relative;
}
button {
font-size:14px;
left:0;
right:0;
margin:0 0 0 0;
border: none;
border-radius: 0;
height:30px;
width:16.66666%;
float:left;
overflow:hidden;
background-color:$basehover;
color:$basetext;
cursor:pointer;
}
button:hover {
background-color:$baseicon;
color:$basetext;
}
button.cp-selected-mode {
background-color:$baseicon;
color:$basetext;
}
}
#cp-canvas-container {
width:100%;
height:100%;
position:relative;
}
#cp-spectrum {
width:100%;
height:100px;
position:absolute;
background-color: transparent;
}
.cp-picker-icon{
width:16px;
height:16px;
border-radius:100%;
position:absolute;
background-color:white;
z-index:2;
border:2px solid black;
}
/***************PALETTE BLOCK****************/
div#palette-block {
z-index:1000;
position:relative;
resize: horizontal;
margin: 0 0 0 0;
width:600px;
height:400px;
}
div#palette-container {
display:inline-block;
background-color: #232125;
position:absolute;
scrollbar-color: #332f35 #232125;
scroll-behavior: smooth;
left:300px;
width:300px;
height:320px;
overflow-y: scroll;
&::-webkit-scrollbar {
background: #232125;
width: 0.5em;
}
&::-webkit-scrollbar-track {
margin-top: -0.125em;
width: 0.5em;
}
&::-webkit-scrollbar-thumb {
background: #332f35;
border-radius: 0.25em;
border: solid 0.125em #232125; //same color as scrollbar back to fake padding
}
&::-webkit-scrollbar-corner {
background: #232125;
}
}
ul#palette-list {
list-style:none;
margin: 0 0 0 0;
padding: 0 0 0 0;
position:relative;
display:inline-block;
li {
float:left;
width:50px;
height:50px;
border:none;
min-width:20px;
min-height:20px;
max-width:75px;
max-height:75px;
}
}
div#pb-options {
position:relative;
left:280px;
height:30px;
width:300px;
top:300px;
button {
border-radius:none;
position:relative;
float:left;
width:50%;
height:100%;
text-align:center;
cursor: pointer;
font-size:16px;
background-color:$baseicon;
border:none;
}
button:hover {
color: $basehovertext;
background-color: $basehover;
}
}
/********SPLASH PAGE*************/
#splash {
width:100% !important;
height:100%!important;
background-color: #232125 !important;
opacity: 1 !important;
#splash-input {
width:74%;
height:100% !important;
color:$baselink;
#splash-menu {
position:relative;
height:100%;
left:0;
top:0;
}
#editor-logo {
font-weight:bold;
text-transform:uppercase;
font-size:20px;
height:35vh;
width:100%;
position:relative;
background-image:url('https://cdn.discordapp.com/attachments/506277390050131978/795660870221955082/final.png');
background-size:cover;
background-position:center;
background-repeat:no-repeat;
}
#black {
width:100%;
height:100%;
position:relative;
background-color:rgba(0,0,0,0.2);
}
#sp-coverdata {
padding:20px;
p, a {
font-size:15px;
position:absolute;
text-transform:none;
right:20px;
}
p {
top:0px;
}
a {
font-size:17px;
bottom:20px;
text-decoration:underline;
}
}
}
#sp-quickstart-container {
height:100%;
max-height: 500px;
width:70%;
float:right;
padding:40px;
-webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */
-moz-box-sizing: border-box; /* Firefox, other Gecko */
box-sizing: border-box; /* Opera/IE 8+ */
overflow-y: scroll;
&::-webkit-scrollbar {
background: #232125;
width: 0.5em;
}
&::-webkit-scrollbar-track {
margin-top: -0.125em;
width: 0.5em;
}
&::-webkit-scrollbar-thumb {
background: #332f35;
border-radius: 0.25em;
border: solid 0.125em #232125; //same color as scrollbar back to fake padding
}
&::-webkit-scrollbar-corner {
background: #232125;
}
}
#sp-quickstart {
display:flex;
flex-direction: row;
flex-wrap: wrap;
height:100%;
// Fancy scrollbar
&::-webkit-scrollbar {
background: #232125;
width: 0.5em;
}
&::-webkit-scrollbar-track {
margin-top: -0.125em;
width: 0.5em;
}
&::-webkit-scrollbar-thumb {
background: #332f35;
border-radius: 0.25em;
border: solid 0.125em #232125; //same color as scrollbar back to fake padding
}
&::-webkit-scrollbar-corner {
background: #232125;
}
}
#sp-quickstart-title {
font-size:27px;
text-transform: uppercase;
font-weight: bold;
}
.sp-template {
display: flex;
align-items: center;
text-transform: uppercase;
width:16%;
border-radius:5%;
margin-right:4%;
margin-top:4%;
background-color:$basecolor;
font-size: 18px;
text-align:center;
font-weight: bold;
&:hover {
cursor:pointer;
background-color:$baseselected;
}
p {
span {
display:block;
font-size:14px !important;
margin: 0 0 0 0;
padding: 0 0 0 0;
}
width:100%;
float:left;
position:relative;
}
}
.sp-template:before {
content:'';
float:left;
padding-top:100%;
}
#sp-newpixel {
-webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */
-moz-box-sizing: border-box; /* Firefox, other Gecko */
box-sizing: border-box; /* Opera/IE 8+ */
display: inline-block;
width: 30% !important;
height:65vh;
padding:20px;
position:relative;
background-color:$basecolor;
#palette-button-splash {
left:5%;
width:90%;
}
.sp-np-entry {
width:100%;
text-align:center;
}
input {
border:none;
background-color: #232125;
color:$basetext;
font-size:14px;
width:40px;
padding:7px;
text-align:center;
}
#create-button {
font-size:18px;
width:150px;
margin-top:40px;
font-weight: bold;
}
#sp-mode-palette {
text-align: center;
position: relative;
float:bottom;
font-size:16px;
font-weight: bold;
div.button-menu {
border:2px solid $basetextweak;
border-radius:5px;
position:relative;
display:inline-block;
padding: 0 0 0 0;
text-align:left;
width:90%;
div {
border:none;
padding:none;
margin:none;
background-color:transparent;
width:50%;
float:left;
text-align: center;
height:25px;
cursor:pointer;
z-index:1;
p {
z-index:0;
-ms-transform: translateY(-60%);
transform: translateY(-60%);
}
}
.sp-interface-selected {
background-color: $basetextweak;
}
}
}
}
#splash-news {
box-sizing: border-box;
padding-left:20px;
width:26%;
height:100%;
background-color:#151516 !important;
float: right;
}
#latest-update {
width:100%;
font-size:15px;
height:90%;
line-height: 1.5;
position:relative;
top:20px;
overflow-y:scroll;
box-sizing: border-box;
img {
width:100%;
}
&::-webkit-scrollbar {
background: #232125;
width: 0.5em;
}
&::-webkit-scrollbar-track {
margin-top: -0.125em;
width: 0.5em;
}
&::-webkit-scrollbar-thumb {
background: #332f35;
border-radius: 0.25em;
border: solid 0.125em #232125; //same color as scrollbar back to fake padding
}
&::-webkit-scrollbar-corner {
background: #232125;
}
}
}

1
debug.log Normal file
View File

@ -0,0 +1 @@
[0114/110029.536:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3)

BIN
images/Logs/line-tool.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 561 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

BIN
images/Logs/splash.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

BIN
images/Logs/trim-canvas.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
images/sked.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

35
js/Util.js Normal file
View File

@ -0,0 +1,35 @@
// Acts as a public static class
class Util {
static getElement(elementOrElementId) {
return typeof elementOrElementId
? document.getElementById(elementOrElementId)
: elementOrElementId;
}
static getText(elementId) {
return this.getElement(elementId).textContent;
}
static setText(elementId, text) {
this.getElement(elementId).textContent = text;
}
static getValue(elementId) {
return this.getElement(elementId).value;
}
static setValue(elementId, value) {
this.getElement(elementId).value = value;
}
//add class .selected to specified element
static select(elementId) {
this.getElement(elementId).classList.add('selected');
}
//remove .selected class from specified element
static deselect(elementId) {
this.getElement(elementId).classList.remove('selected');
}
//toggle the status of the .selected class on the specified element
static toggle(elementId) {
this.getElement(elementId).classList.toggle('selected');
}
}

View File

@ -1,8 +1,10 @@
let currentPalette = [];
//adds the given color to the palette
//input hex color string
//returns list item element
/** Adds the given color to the palette
*
* @param {*} newColor the colour to add
* @return the list item containing the added colour
*/
function addColor (newColor) {
//add # at beginning if not present
if (newColor.charAt(0) != '#')
@ -17,6 +19,7 @@ function addColor (newColor) {
button.style.backgroundColor = newColor;
button.addEventListener('mouseup', clickedColor);
listItem.appendChild(button);
listItem.classList.add("draggable-colour")
//insert new listItem element at the end of the colors menu (right before add button)
colorsMenu.insertBefore(listItem, colorsMenu.children[colorsMenu.children.length-1]);
@ -35,11 +38,19 @@ function addColor (newColor) {
//hide edit button
button.parentElement.lastChild.classList.add('hidden');
//show jscolor picker
button.parentElement.firstChild.jscolor.show();
//show jscolor picker, if basic mode is enabled
if (pixelEditorMode == 'Basic')
button.parentElement.firstChild.jscolor.show();
else
showDialogue("palette-block", false);
});
console.log(currentPalette);
return listItem;
}
new Sortable(document.getElementById("colors-menu"), {
animation:100,
filter: ".noshrink",
draggable: ".draggable-colour",
onEnd: makeIsDraggingFalse
});

View File

@ -1,4 +1,4 @@
//add color button
// add-color-button management
on('click', 'add-color-button', function(){
if (!documentCreated) return;

185
js/_algorithms.js Normal file
View File

@ -0,0 +1,185 @@
// CONSTS
// Degrees to radiants
let degreesToRad = Math.PI / 180;
// I'm pretty sure that precision is necessary
let referenceWhite = {x: 95.05, y: 100, z: 108.89999999999999};
/**********************SECTION: COLOUR CONVERSIONS****************************** */
/**
* Converts an HSL color value to RGB. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes h, s, and l are contained in the set [0, 1] and
* returns r, g, and b in the set [0, 255].
*
* @param {number} h The hue
* @param {number} s The saturation
* @param {number} l The lightness
* @return {Array} The RGB representation
*/
function cpHslToRgb(h, s, l){
var r, g, b;
h /= 360;
s /= 100;
l /= 100;
if(s == 0){
r = g = b = l; // achromatic
}else{
var hue2rgb = function hue2rgb(p, q, t){
if(t < 0) t += 1;
if(t > 1) t -= 1;
if(t < 1/6) return p + (q - p) * 6 * t;
if(t < 1/2) return q;
if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
}
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}
function hsvToRgb(h, s, v) {
var r, g, b;
h /= 360;
s /= 100;
v /= 100;
var i = Math.floor(h * 6);
var f = h * 6 - i;
var p = v * (1 - s);
var q = v * (1 - f * s);
var t = v * (1 - (1 - f) * s);
switch (i % 6) {
case 0: r = v, g = t, b = p; break;
case 1: r = q, g = v, b = p; break;
case 2: r = p, g = v, b = t; break;
case 3: r = p, g = q, b = v; break;
case 4: r = t, g = p, b = v; break;
case 5: r = v, g = p, b = q; break;
}
return [ r * 255, g * 255, b * 255 ];
}
function hslToHex(h, s, l) {
h /= 360;
s /= 100;
l /= 100;
let r, g, b;
if (s === 0) {
r = g = b = l; // achromatic
} else {
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
const toHex = x => {
const hex = Math.round(x * 255).toString(16);
return hex.length === 1 ? '0' + hex : hex;
};
return `${toHex(r)}${toHex(g)}${toHex(b)}`;
}
function rgbToHsl(col) {
let r = col.r;
let g = col.g;
let b = col.b;
r /= 255, g /= 255, b /= 255;
let max = Math.max(r, g, b), min = Math.min(r, g, b);
let myH, myS, myL = (max + min) / 2;
if (max == min) {
myH = myS = 0; // achromatic
}
else {
let d = max - min;
myS = myL > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: myH = (g - b) / d + (g < b ? 6 : 0); break;
case g: myH = (b - r) / d + 2; break;
case b: myH = (r - g) / d + 4; break;
}
myH /= 6;
}
return {h: myH, s: myS, l: myL };
}
function rgbToHsv(col) {
let r = col.r;
let g = col.g;
let b = col.b;
r /= 255, g /= 255, b /= 255;
let max = Math.max(r, g, b), min = Math.min(r, g, b);
let myH, myS, myV = max;
let d = max - min;
myS = max == 0 ? 0 : d / max;
if (max == min) {
myH = 0; // achromatic
}
else {
switch (max) {
case r: myH = (g - b) / d + (g < b ? 6 : 0); break;
case g: myH = (b - r) / d + 2; break;
case b: myH = (r - g) / d + 4; break;
}
myH /= 6;
}
return {h: myH, s: myS, v: myV};
}
function RGBtoCIELAB(rgbColour) {
// Convert to XYZ first via matrix transformation
let x = 0.412453 * rgbColour.r + 0.357580 * rgbColour.g + 0.180423 * rgbColour.b;
let y = 0.212671 * rgbColour.r + 0.715160 * rgbColour.g + 0.072169 * rgbColour.b;
let z = 0.019334 * rgbColour.r + 0.119193 * rgbColour.g + 0.950227 * rgbColour.b;
let xFunc = CIELABconvF(x / referenceWhite.x);
let yFunc = CIELABconvF(y / referenceWhite.y);
let zFunc = CIELABconvF(z / referenceWhite.z);
let myL = 116 * yFunc - 16;
let myA = 500 * (xFunc - yFunc);
let myB = 200 * (yFunc - zFunc);
return {l: myL, a: myA, b: myB};
}
function CIELABconvF(value) {
if (value > Math.pow(6/29, 3)) {
return Math.cbrt(value);
}
return 1/3 * Math.pow(6/29, 2) * value + 4/29;
}

View File

@ -1,4 +1,9 @@
function changeZoom (layer, direction, cursorLocation) {
/** Changes the zoom level of the canvas
* @param {*} direction 'in' or 'out'
* @param {*} cursorLocation The position of the cursor when the user zoomed
*/
function changeZoom (direction, cursorLocation) {
// Computing current width and height
var oldWidth = canvasSize[0] * zoom;
var oldHeight = canvasSize[1] * zoom;
var newWidth, newHeight;
@ -11,7 +16,9 @@ function changeZoom (layer, direction, cursorLocation) {
newHeight = canvasSize[1] * zoom;
//adjust canvas position
layer.setCanvasOffset(layer.canvas.offsetLeft + (oldWidth - newWidth) *cursorLocation[0]/oldWidth, layer.canvas.offsetTop + (oldHeight - newHeight) *cursorLocation[1]/oldWidth);
layers[0].setCanvasOffset(
layers[0].canvas.offsetLeft + (oldWidth - newWidth) * cursorLocation[0]/oldWidth,
layers[0].canvas.offsetTop + (oldHeight - newHeight) * cursorLocation[1]/oldWidth);
}
//if you want to zoom in
else if (direction == 'in' && zoom + Math.ceil(zoom/10) < window.innerHeight/4){
@ -20,11 +27,13 @@ function changeZoom (layer, direction, cursorLocation) {
newHeight = canvasSize[1] * zoom;
//adjust canvas position
layer.setCanvasOffset(layer.canvas.offsetLeft - Math.round((newWidth - oldWidth)*cursorLocation[0]/oldWidth), layer.canvas.offsetTop - Math.round((newHeight - oldHeight)*cursorLocation[1]/oldHeight));
layers[0].setCanvasOffset(
layers[0].canvas.offsetLeft - Math.round((newWidth - oldWidth)*cursorLocation[0]/oldWidth),
layers[0].canvas.offsetTop - Math.round((newHeight - oldHeight)*cursorLocation[1]/oldHeight));
}
//resize canvas
layer.resize();
layers[0].resize();
// adjust brush size
currentTool.updateCursor();

View File

@ -13,7 +13,9 @@ var currentColor = firstCheckerBoardColor;
// Saving number of squares filled until now
var nSquaresFilled = 0;
/** Fills the checkerboard canvas with squares with alternating colours
*
*/
function fillCheckerboard() {
// Getting checkerboard context
var context = checkerBoard.context;

View File

@ -17,7 +17,7 @@ function clickedColor (e){
e.target.parentElement.classList.add('selected');
} else if (e.which == 3) { //right clicked color
console.log('right clicked color button');
//console.log('right clicked color button');
//hide edit color button (to prevent it from showing)
e.target.parentElement.lastChild.classList.add('hidden');

View File

@ -23,7 +23,7 @@ on('input', 'jscolor-hex-input', function (e) {
//changes all of one color to another after being changed from color picker
function colorChanged(e) {
console.log('colorChanged() to ' + e.target.value);
//console.log('colorChanged() to ' + e.target.value);
//get colors
var newColor = hexToRgb(e.target.value);
var oldColor = e.target.oldColor;
@ -41,7 +41,7 @@ function colorChanged(e) {
//check if selected color already matches another color
colors = document.getElementsByClassName('color-button');
console.log(colors);
//console.log(colors);
var colorCheckingStyle = 'background: #bc60c4; color: white';
var newColorHex = e.target.value.toLowerCase();
@ -54,11 +54,11 @@ function colorChanged(e) {
//if generated color matches this color
if (newColorHex == colors[i].jscolor.toString()) {
console.log('%ccolor already exists'+(colors[i].parentElement.classList.contains('jscolor-active')?' (but is the current color)':''), colorCheckingStyle);
//console.log('%ccolor already exists'+(colors[i].parentElement.classList.contains('jscolor-active')?' (but is the current color)':''), colorCheckingStyle);
//if the color isnt the one that has the picker currently open
if (!colors[i].parentElement.classList.contains('jscolor-active')) {
console.log('%cColor is duplicate', colorCheckingStyle);
//console.log('%cColor is duplicate', colorCheckingStyle);
//show the duplicate color warning
duplicateColorWarning.style.visibility = 'visible';

806
js/_colorPicker.js Normal file
View File

@ -0,0 +1,806 @@
let sliders = document.getElementsByClassName("cp-slider-entry");
let colourPreview = document.getElementById("cp-colour-preview");
let colourValue = document.getElementById("cp-hex");
let currentPickerMode = "rgb";
let currentPickingMode = "mono";
let styleElement = document.createElement("style");
let miniPickerCanvas = document.getElementById("cp-spectrum");
let miniPickerSlider = document.getElementById("cp-minipicker-slider");
let activePickerIcon = document.getElementById("cp-active-icon");
let pickerIcons = [activePickerIcon];
let hexContainers = [document.getElementById("cp-colours-previews").children[0],null,null,null];
let startPickerIconPos = [[0,0],[0,0],[0,0],[0,0]];
let currPickerIconPos = [[0,0], [0,0],[0,0],[0,0]];
let styles = ["",""];
let draggingCursor = false;
cpInit();
function cpInit() {
// Appending the palette styles
document.getElementsByTagName("head")[0].appendChild(styleElement);
// Saving first icon position
startPickerIconPos[0] = [miniPickerCanvas.getBoundingClientRect().left, miniPickerCanvas.getBoundingClientRect().top];
// Set the correct size of the canvas
miniPickerCanvas.height = miniPickerCanvas.getBoundingClientRect().height;
miniPickerCanvas.width = miniPickerCanvas.getBoundingClientRect().width;
// Update picker position
updatePickerByHex(colourValue.value);
// Startup updating
updateAllSliders();
// Fill minislider
updateMiniSlider(colourValue.value);
// Fill minipicker
updatePickerByHex(colourValue.value);
updateMiniPickerSpectrum();
}
function hexUpdated() {
updatePickerByHex(colourValue.value);
updateSlidersByHex(colourValue.value);
}
// Applies the styles saved in the style array to the style element in the head of the document
function updateStyles() {
styleElement.innerHTML = styles[0] + styles[1];
}
/** Updates the background gradients of the sliders given their value
* Updates the hex colour and its preview
* Updates the minipicker according to the computed hex colour
*
*/
function updateSliderValue (sliderIndex, updateMini = true) {
let toUpdate;
let slider;
let input;
let hexColour;
let sliderValues;
toUpdate = sliders[sliderIndex - 1];
slider = toUpdate.getElementsByTagName("input")[0];
input = toUpdate.getElementsByTagName("input")[1];
// Update label value
input.value = slider.value;
// Update preview colour
// get slider values
sliderValues = getSlidersValues();
// Generate preview colour
switch (currentPickerMode) {
case 'rgb':
hexColour = rgbToHex(sliderValues[0], sliderValues[1], sliderValues[2]);
break;
case 'hsv':
let tmpRgb = hsvToRgb(sliderValues[0], sliderValues[1], sliderValues[2]);
hexColour = rgbToHex(parseInt(tmpRgb[0]), parseInt(tmpRgb[1]), parseInt(tmpRgb[2]));
break;
case 'hsl':
hexColour = hslToHex(sliderValues[0], sliderValues[1], sliderValues[2]);
break;
default:
console.log("wtf select a decent picker mode");
return;
}
// Update preview colour div
colourPreview.style.backgroundColor = '#' + hexColour;
colourValue.value = '#' + hexColour;
// Update sliders background
// there's no other way than creating a custom css file, appending it to the head and
// specify the sliders' backgrounds here
styles[0] = '';
for (let i=0; i<sliders.length; i++) {
styles[0] += getSliderCSS(i + 1, sliderValues);
}
updateStyles();
if (updateMini) {
updatePickerByHex(colourValue.value);
updateMiniPickerSpectrum();
}
}
// Calculates the css gradient for a slider
function getSliderCSS(index, sliderValues) {
let ret = 'input[type=range]#';
let sliderId;
let gradientMin;
let gradientMax;
let hueGradient;
let rgbColour;
switch (index) {
case 1:
sliderId = 'first-slider';
switch (currentPickerMode) {
case 'rgb':
gradientMin = 'rgba(0,' + sliderValues[1] + ',' + sliderValues[2] + ',1)';
gradientMax = 'rgba(255,' + sliderValues[1] + ',' + sliderValues[2] + ',1)';
break;
case 'hsv':
hueGradient = getHueGradientHSV(sliderValues);
break;
case 'hsl':
// Hue gradient
hueGradient = getHueGradientHSL(sliderValues);
break;
}
break;
case 2:
sliderId = 'second-slider';
switch (currentPickerMode) {
case 'rgb':
gradientMin = 'rgba(' + sliderValues[0] + ',0,' + sliderValues[2] + ',1)';
gradientMax = 'rgba(' + sliderValues[0] + ',255,' + sliderValues[2] + ',1)';
break;
case 'hsv':
rgbColour = hsvToRgb(sliderValues[0], 0, sliderValues[2]);
gradientMin = 'rgba(' + rgbColour[0] + ',' + rgbColour[1] + ',' + rgbColour[2] + ',1)';
rgbColour = hsvToRgb(sliderValues[0], 100, sliderValues[2]);
gradientMax = 'rgba(' + rgbColour[0] + ',' + rgbColour[1] + ',' + rgbColour[2] + ',1)';
break;
case 'hsl':
rgbColour = cpHslToRgb(sliderValues[0], 0, sliderValues[2]);
gradientMin = 'rgba(' + rgbColour[0] + ',' + rgbColour[1] + ',' + rgbColour[2] + ',1)';
rgbColour = cpHslToRgb(sliderValues[0], 100, sliderValues[2]);
gradientMax = 'rgba(' + rgbColour[0] + ',' + rgbColour[1] + ',' + rgbColour[2] + ',1)';
break;
}
break;
case 3:
sliderId = 'third-slider';
switch (currentPickerMode) {
case 'rgb':
gradientMin = 'rgba(' + sliderValues[0] + ',' + sliderValues[1] + ',0,1)';
gradientMax = 'rgba(' + sliderValues[0] + ',' + sliderValues[1] + ',255,1)';
break;
case 'hsv':
rgbColour = hsvToRgb(sliderValues[0], sliderValues[1], 0);
gradientMin = 'rgba(' + rgbColour[0] + ',' + rgbColour[1] + ',' + rgbColour[2] + ',1)';
rgbColour = hsvToRgb(sliderValues[0], sliderValues[1], 100);
gradientMax = 'rgba(' + rgbColour[0] + ',' + rgbColour[1] + ',' + rgbColour[2] + ',1)';
break;
case 'hsl':
gradientMin = 'rgba(0,0,0,1)';
gradientMax = 'rgba(255,255,255,1)';
break;
}
break;
default:
return '';
}
ret += sliderId;
ret += '::-webkit-slider-runnable-track {';
switch (currentPickerMode) {
case 'rgb':
ret += 'background: linear-gradient(90deg, rgba(2,0,36,1) 0%, ' +
gradientMin + ' 0%, ' + gradientMax + '100%)';
break;
case 'hsv':
case 'hsl':
ret += 'background: ';
if (index == 1) {
ret += hueGradient;
}
else {
ret += 'linear-gradient(90deg, rgba(2,0,36,1) 0%, ' + gradientMin + ' 0%, ';
// For hsl I also have to add a middle point
if (currentPickerMode == 'hsl' && index == 3) {
let rgb = cpHslToRgb(sliderValues[0], sliderValues[1], 50);
ret += 'rgba(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ',1) 50%,';
}
ret += gradientMax + '100%);';
}
break;
}
ret += '}'
ret += ret.replace('::-webkit-slider-runnable-track', '::-moz-range-track');
return ret;
}
// Computes the hue gradient used for hsl
function getHueGradientHSL(sliderValues) {
return 'linear-gradient(90deg, rgba(2,0,36,1) 0%, \
hsl(0,' + sliderValues[1] + '%,' + sliderValues[2]+ '%) 0%, \
hsl(60,' + sliderValues[1] + '%,' + sliderValues[2]+ '%) 16.6666%, \
hsl(120,' + sliderValues[1] + '%,' + sliderValues[2]+ '%) 33.3333333333%, \
hsl(180,'+ sliderValues[1] + '%,' + sliderValues[2]+ '%) 50%, \
hsl(240,' + sliderValues[1] + '%,' + sliderValues[2]+ '%) 66.66666%, \
hsl(300,'+ sliderValues[1] + '%,' + sliderValues[2]+ '%) 83.333333%, \
hsl(360,'+ sliderValues[1] + '%,' + sliderValues[2]+ '%) 100%);';
}
// Computes the hue gradient used for hsv
function getHueGradientHSV(sliderValues) {
let col = hsvToRgb(0, sliderValues[1], sliderValues[2]);
let ret = 'linear-gradient(90deg, rgba(2,0,36,1) 0%, ';
ret += 'rgba(' + col[0] + ',' + col[1] + ',' + col[2] + ',1) 0%,'
col = hsvToRgb(60, sliderValues[1], sliderValues[2]);
ret += 'rgba(' + col[0] + ',' + col[1] + ',' + col[2] + ',1) 16.6666%,';
col = hsvToRgb(120, sliderValues[1], sliderValues[2]);
ret += 'rgba(' + col[0] + ',' + col[1] + ',' + col[2] + ',1) 33.3333333333%,';
col = hsvToRgb(180, sliderValues[1], sliderValues[2]);
ret += 'rgba(' + col[0] + ',' + col[1] + ',' + col[2] + ',1) 50%,';
col = hsvToRgb(240, sliderValues[1], sliderValues[2]);
ret += 'rgba(' + col[0] + ',' + col[1] + ',' + col[2] + ',1) 66.66666%,';
col = hsvToRgb(300, sliderValues[1], sliderValues[2]);
ret += 'rgba(' + col[0] + ',' + col[1] + ',' + col[2] + ',1) 83.333333%,';
col = hsvToRgb(360, sliderValues[1], sliderValues[2]);
ret += 'rgba(' + col[0] + ',' + col[1] + ',' + col[2] + ',1) 100%);';
return ret;
}
// Fired when the values in the labels are changed
function inputChanged(target, index) {
let sliderIndex = index - 1;
sliders[sliderIndex].getElementsByTagName("input")[0].value = target.value;
updateSliderValue(index);
}
// Updates the colour model used to pick colours
function changePickerMode(target, newMode) {
let maxRange;
let colArray;
let rgbTmp;
let hexColour = colourValue.value.replace('#', '');
currentPickerMode = newMode;
document.getElementsByClassName("cp-selected-mode")[0].classList.remove("cp-selected-mode");
target.classList.add("cp-selected-mode");
switch (newMode)
{
case 'rgb':
maxRange = [255,255,255];
sliders[0].getElementsByTagName("label")[0].innerHTML = 'R';
sliders[1].getElementsByTagName("label")[0].innerHTML = 'G';
sliders[2].getElementsByTagName("label")[0].innerHTML = 'B';
break;
case 'hsv':
maxRange = [360, 100, 100];
sliders[0].getElementsByTagName("label")[0].innerHTML = 'H';
sliders[1].getElementsByTagName("label")[0].innerHTML = 'S';
sliders[2].getElementsByTagName("label")[0].innerHTML = 'V';
break;
case 'hsl':
maxRange = [360, 100, 100];
sliders[0].getElementsByTagName("label")[0].innerHTML = 'H';
sliders[1].getElementsByTagName("label")[0].innerHTML = 'S';
sliders[2].getElementsByTagName("label")[0].innerHTML = 'L';
break;
default:
console.log("wtf select a decent picker mode");
break;
}
for (let i=0; i<sliders.length; i++) {
let slider = sliders[i].getElementsByTagName("input")[0];
slider.setAttribute("max", maxRange[i]);
}
// Putting the current colour in the new slider
switch(currentPickerMode) {
case 'rgb':
colArray = hexToRgb(hexColour);
colArray = [colArray.r, colArray.g, colArray.b];
break;
case 'hsv':
rgbTmp = hexToRgb(hexColour);
colArray = rgbToHsv(rgbTmp);
colArray.h *= 360;
colArray.s *= 100;
colArray.v *= 100;
colArray = [colArray.h, colArray.s, colArray.v];
break;
case 'hsl':
rgbTmp = hexToRgb(hexColour);
colArray = rgbToHsl(rgbTmp);
colArray.h *= 360;
colArray.s *= 100;
colArray.l *= 100;
colArray = [colArray.h, colArray.s, colArray.l];
break;
default:
break;
}
for (let i=0; i<3; i++) {
sliders[i].getElementsByTagName("input")[0].value = colArray[i];
}
updateAllSliders();
}
// Returns an array containing the values of the sliders
function getSlidersValues() {
return [parseInt(sliders[0].getElementsByTagName("input")[0].value),
parseInt(sliders[1].getElementsByTagName("input")[0].value),
parseInt(sliders[2].getElementsByTagName("input")[0].value)];
}
// Updates every slider
function updateAllSliders(updateMini=true) {
for (let i=1; i<=3; i++) {
updateSliderValue(i, updateMini);
}
}
/******************SECTION: MINIPICKER******************/
// Moves the picker icon according to the mouse position on the canvas
function movePickerIcon(event) {
event.preventDefault();
if (event.which == 1 || draggingCursor) {
let cursorPos = getCursorPosMinipicker(event);
let canvasRect = miniPickerCanvas.getBoundingClientRect();
let left = (cursorPos[0] - startPickerIconPos[0][0] - 8);
let top = (cursorPos[1] - startPickerIconPos[0][1] - 8);
if (left > -8 && top > -8 && left < canvasRect.width-8 && top < canvasRect.height-8){
activePickerIcon.style["left"] = "" + left + "px";
activePickerIcon.style["top"]= "" + top + "px";
currPickerIconPos[0] = [left, top];
}
updateMiniPickerColour();
updateOtherIcons();
}
}
// Updates the main sliders given a hex value computed with the minipicker
function updateSlidersByHex(hex, updateMini = true) {
let colour;
let mySliders = [sliders[0].getElementsByTagName("input")[0],
sliders[1].getElementsByTagName("input")[0],
sliders[2].getElementsByTagName("input")[0]];
switch (currentPickerMode) {
case 'rgb':
colour = hexToRgb(hex);
mySliders[0].value = colour.r;
mySliders[1].value = colour.g;
mySliders[2].value = colour.b;
break;
case 'hsv':
colour = rgbToHsv(hexToRgb(hex));
mySliders[0].value = colour.h * 360;
mySliders[1].value = colour.s * 100;
mySliders[2].value = colour.v * 100;
break;
case 'hsl':
colour = rgbToHsl(hexToRgb(hex));
mySliders[0].value = colour.h * 360;
mySliders[1].value = colour.s * 100;
mySliders[2].value = colour.l * 100;
break;
default:
break;
}
updateAllSliders(false);
}
// Gets the position of the picker cursor relative to the canvas
function getCursorPosMinipicker(e) {
var x;
var y;
if (e.pageX != undefined && e.pageY != undefined) {
x = e.pageX;
y = e.pageY;
}
else {
x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop;
}
x -= miniPickerCanvas.offsetLeft;
y -= miniPickerCanvas.offsetTop;
return [Math.round(x), Math.round(y)];
}
// Updates the minipicker given a hex computed by the main sliders
// Moves the cursor
function updatePickerByHex(hex) {
let hsv = rgbToHsv(hexToRgb(hex));
let xPos = miniPickerCanvas.width * hsv.h - 8;
let yPos = miniPickerCanvas.height * hsv.s + 8;
miniPickerSlider.value = hsv.v * 100;
currPickerIconPos[0][0] = xPos;
currPickerIconPos[0][1] = miniPickerCanvas.height - yPos;
if (currPickerIconPos[0][1] >= 92)
{
currPickerIconPos[0][1] = 91.999;
}
activePickerIcon.style.left = '' + xPos + 'px';
activePickerIcon.style.top = '' + (miniPickerCanvas.height - yPos) + 'px';
activePickerIcon.style.backgroundColor = '#' + getMiniPickerColour();
colourPreview.style.backgroundColor = hex;
updateOtherIcons();
updateMiniSlider(hex);
}
// Fired when the value of the minislider changes: updates the spectrum gradient and the hex colour
function miniSliderInput(event) {
let newHex;
let newHsv = rgbToHsv(hexToRgb(getMiniPickerColour()));
let rgb;
// Adding slider value to value
newHsv.v = parseInt(event.target.value);
// Updating hex
rgb = hsvToRgb(newHsv.h * 360, newHsv.s * 100, newHsv.v);
newHex = rgbToHex(Math.round(rgb[0]), Math.round(rgb[1]), Math.round(rgb[2]));
colourValue.value = newHex;
updateMiniPickerSpectrum();
updateMiniPickerColour();
}
// Updates the hex colour after having changed the minislider (MERGE)
function updateMiniPickerColour() {
let hex = getMiniPickerColour();
activePickerIcon.style.backgroundColor = '#' + hex;
// Update hex and sliders based on hex
colourValue.value = '#' + hex;
colourPreview.style.backgroundColor = '#' + hex;
updateSlidersByHex(hex);
updateMiniSlider(hex);
updateOtherIcons();
}
// Returns the current colour of the minipicker
function getMiniPickerColour() {
let hex;
let pickedColour;
pickedColour = miniPickerCanvas.getContext('2d').getImageData(currPickerIconPos[0][0] + 8,
currPickerIconPos[0][1] + 8, 1, 1).data;
hex = rgbToHex(pickedColour[0], pickedColour[1], pickedColour[2]);
return hex;
}
// Update the background gradient of the slider in the minipicker
function updateMiniSlider(hex) {
let rgb = hexToRgb(hex);
styles[1] = "input[type=range]#cp-minipicker-slider::-webkit-slider-runnable-track { background: rgb(2,0,36);";
styles[1] += "background: linear-gradient(90deg, rgba(2,0,36,1) 0%, rgba(0,0,0,1) 0%, " +
"rgba(" + rgb.r + "," + rgb.g + "," + rgb.b + ",1) 100%);}";
updateMiniPickerSpectrum();
updateStyles();
}
// Updates the gradient of the spectrum canvas in the minipicker
function updateMiniPickerSpectrum() {
let ctx = miniPickerCanvas.getContext('2d');
let hsv = rgbToHsv(hexToRgb(colourValue.value));
let tmp;
let white = {h:hsv.h * 360, s:0, v: parseInt(miniPickerSlider.value)};
white = hsvToRgb(white.h, white.s, white.v);
ctx.clearRect(0, 0, miniPickerCanvas.width, miniPickerCanvas.height);
// Drawing hues
var hGrad = ctx.createLinearGradient(0, 0, miniPickerCanvas.width, 0);
for (let i=0; i<7; i++) {
tmp = hsvToRgb(60 * i, 100, hsv.v * 100);
hGrad.addColorStop(i / 6, '#' + rgbToHex(Math.round(tmp[0]), Math.round(tmp[1]), Math.round(tmp[2])));
}
ctx.fillStyle = hGrad;
ctx.fillRect(0, 0, miniPickerCanvas.width, miniPickerCanvas.height);
// Drawing sat / lum
var vGrad = ctx.createLinearGradient(0, 0, 0, miniPickerCanvas.height);
vGrad.addColorStop(0, 'rgba(' + white[0] +',' + white[1] + ',' + white[2] + ',0)');
/*
vGrad.addColorStop(0.1, 'rgba(255,255,255,0)');
vGrad.addColorStop(0.9, 'rgba(255,255,255,1)');
*/
vGrad.addColorStop(1, 'rgba(' + white[0] +',' + white[1] + ',' + white[2] + ',1)');
ctx.fillStyle = vGrad;
ctx.fillRect(0, 0, miniPickerCanvas.width, miniPickerCanvas.height);
}
function toggleDraggingCursor() {
draggingCursor = !draggingCursor;
}
function changePickingMode(event, newMode) {
let nIcons = pickerIcons.length;
let canvasContainer = document.getElementById("cp-canvas-container");
// Number of hex containers to add
let nHexContainers;
// Remove selected class from previous mode
document.getElementById("cp-colour-picking-modes").getElementsByClassName("cp-selected-mode")[0].classList.remove("cp-selected-mode");
// Updating mode
currentPickingMode = newMode;
// Adding selected class to new mode
event.target.classList.add("cp-selected-mode");
for (let i=1; i<nIcons; i++) {
// Deleting extra icons
pickerIcons.pop();
canvasContainer.removeChild(canvasContainer.children[2]);
// Deleting extra hex containers
hexContainers[0].parentElement.removeChild(hexContainers[0].parentElement.children[1]);
hexContainers[i] = null;
}
// Resetting first hex container size
hexContainers[0].style.width = '100%';
switch (currentPickingMode)
{
case 'analog':
createIcon();
createIcon();
nHexContainers = 2;
break;
case 'cmpt':
// Easiest one, add 180 to the H value and move the icon
createIcon();
nHexContainers = 1;
break;
case 'tri':
createIcon();
createIcon();
nHexContainers = 2;
break
case 'scmpt':
createIcon();
createIcon();
nHexContainers = 2;
break;
case 'tetra':
for (let i=0; i<3; i++) {
createIcon();
}
nHexContainers = 3;
break;
default:
console.log("How did you select the " + currentPickingMode + ", hackerman?");
break;
}
// Editing the size of the first container
hexContainers[0].style.width = '' + 100 / (nHexContainers + 1) + '%';
// Adding hex preview containers
for (let i=0; i<nHexContainers; i++) {
let newContainer = document.createElement("div");
newContainer.classList.add("cp-colour-preview");
newContainer.style.width = "" + (100 / (nHexContainers + 1)) + "%";
hexContainers[0].parentElement.appendChild(newContainer);
hexContainers[i + 1] = newContainer;
}
function createIcon() {
let newIcon = document.createElement("div");
newIcon.classList.add("cp-picker-icon");
pickerIcons.push(newIcon);
canvasContainer.appendChild(newIcon);
}
updateOtherIcons();
}
function updateOtherIcons() {
let currentColorHex = colourValue.value;
let currentColourHsv = rgbToHsv(hexToRgb(currentColorHex));
let newColourHsv = {h:currentColourHsv.h, s:currentColourHsv.s, v:currentColourHsv.v};
let newColourHexes = ['', '', ''];
let tmpRgb;
// Salvo tutti i
switch (currentPickingMode)
{
case 'mono':
break;
case 'analog':
// First colour
newColourHsv.h = (((currentColourHsv.h*360 + 40) % 360) / 360);
currPickerIconPos[1][0] = miniPickerCanvas.width * newColourHsv.h - 8;
currPickerIconPos[1][1] = miniPickerCanvas.height - (miniPickerCanvas.height * newColourHsv.s + 8);
tmpRgb = hsvToRgb(newColourHsv.h*360, newColourHsv.s*100, newColourHsv.v*100);
newColourHexes[0] = rgbToHex(Math.round(tmpRgb[0]), Math.round(tmpRgb[1]), Math.round(tmpRgb[2]));
// Second colour
newColourHsv.h = (((currentColourHsv.h*360 - 40) % 360) / 360);
if (newColourHsv.h < 0) {
newColourHsv.h += 1;
}
currPickerIconPos[2][0] = miniPickerCanvas.width * newColourHsv.h - 8;
currPickerIconPos[2][1] = miniPickerCanvas.height - (miniPickerCanvas.height * newColourHsv.s + 8);
tmpRgb = hsvToRgb(newColourHsv.h*360, newColourHsv.s*100, newColourHsv.v*100);
newColourHexes[1] = rgbToHex(Math.round(tmpRgb[0]), Math.round(tmpRgb[1]), Math.round(tmpRgb[2]));
break;
case 'cmpt':
newColourHsv.h = (((currentColourHsv.h*360 + 180) % 360) / 360);
currPickerIconPos[1][0] = miniPickerCanvas.width * newColourHsv.h - 8;
currPickerIconPos[1][1] = miniPickerCanvas.height - (miniPickerCanvas.height * newColourHsv.s + 8);
tmpRgb = hsvToRgb(newColourHsv.h*360, newColourHsv.s*100, newColourHsv.v*100);
newColourHexes[0] = rgbToHex(Math.round(tmpRgb[0]), Math.round(tmpRgb[1]), Math.round(tmpRgb[2]));
break;
case 'tri':
for (let i=1; i< 3; i++) {
newColourHsv.h = (((currentColourHsv.h*360 + 120*i) % 360) / 360);
currPickerIconPos[i][0] = miniPickerCanvas.width * newColourHsv.h - 8;
currPickerIconPos[i][1] = miniPickerCanvas.height - (miniPickerCanvas.height * newColourHsv.s + 8);
tmpRgb = hsvToRgb(newColourHsv.h*360, newColourHsv.s*100, newColourHsv.v*100);
newColourHexes[i - 1] = rgbToHex(Math.round(tmpRgb[0]), Math.round(tmpRgb[1]), Math.round(tmpRgb[2]));
}
break
case 'scmpt':
// First colour
newColourHsv.h = (((currentColourHsv.h*360 + 210) % 360) / 360);
currPickerIconPos[1][0] = miniPickerCanvas.width * newColourHsv.h - 8;
currPickerIconPos[1][1] = miniPickerCanvas.height - (miniPickerCanvas.height * newColourHsv.s + 8);
tmpRgb = hsvToRgb(newColourHsv.h*360, newColourHsv.s*100, newColourHsv.v*100);
newColourHexes[0] = rgbToHex(Math.round(tmpRgb[0]), Math.round(tmpRgb[1]), Math.round(tmpRgb[2]));
// Second colour
newColourHsv.h = (((currentColourHsv.h*360 + 150) % 360) / 360);
currPickerIconPos[2][0] = miniPickerCanvas.width * newColourHsv.h - 8;
currPickerIconPos[2][1] = miniPickerCanvas.height - (miniPickerCanvas.height * newColourHsv.s + 8);
tmpRgb = hsvToRgb(newColourHsv.h*360, newColourHsv.s*100, newColourHsv.v*100);
newColourHexes[1] = rgbToHex(Math.round(tmpRgb[0]), Math.round(tmpRgb[1]), Math.round(tmpRgb[2]));
break;
case 'tetra':
for (let i=1; i< 4; i++) {
newColourHsv.h = (((currentColourHsv.h*360 + 90*i) % 360) / 360);
currPickerIconPos[i][0] = miniPickerCanvas.width * newColourHsv.h - 8;
currPickerIconPos[i][1] = miniPickerCanvas.height - (miniPickerCanvas.height * newColourHsv.s + 8);
tmpRgb = hsvToRgb(newColourHsv.h*360, newColourHsv.s*100, newColourHsv.v*100);
newColourHexes[i - 1] = rgbToHex(Math.round(tmpRgb[0]), Math.round(tmpRgb[1]), Math.round(tmpRgb[2]));
}
break;
default:
console.log("How did you select the " + currentPickingMode + ", hackerman?");
break;
}
hexContainers[0].style.color = getHexPreviewColour(colourValue.value);
for (let i=1; i<pickerIcons.length; i++) {
pickerIcons[i].style.left = '' + currPickerIconPos[i][0] + 'px';
pickerIcons[i].style.top = '' + currPickerIconPos[i][1] + 'px';
pickerIcons[i].style.backgroundColor = '#' + newColourHexes[i - 1];
}
if (currentPickingMode != "analog") {
hexContainers[0].style.backgroundColor = colourValue.value;
hexContainers[0].innerHTML = colourValue.value;
for (let i=0; i<pickerIcons.length - 1; i++) {
hexContainers[i + 1].style.backgroundColor = '#' + newColourHexes[i];
hexContainers[i + 1].innerHTML = '#' + newColourHexes[i];
hexContainers[i + 1].style.color = getHexPreviewColour(newColourHexes[i]);
}
}
// If I'm using analogous mode, I place the current colour in the middle
else {
hexContainers[1].style.backgroundColor = colourValue.value;
hexContainers[1].innerHTML = colourValue.value;
hexContainers[2].style.backgroundColor = '#' + newColourHexes[0];
hexContainers[2].innerHTML = '#' + newColourHexes[0];
hexContainers[0].style.backgroundColor = '#' + newColourHexes[1];
hexContainers[0].innerHTML = '#' + newColourHexes[1];
for (let i=1; i<3; i++) {
hexContainers[i].style.color = getHexPreviewColour(newColourHexes[i - 1]);
}
}
}
function getSelectedColours() {
let ret = [];
for (let i=0; i<hexContainers.length; i++) {
if (hexContainers[i] != null) {
ret.push(hexContainers[i].innerHTML);
}
}
return ret;
}
function getHexPreviewColour(hex) {
//if brightness is over threshold, make the text dark
if (colorBrightness(hex) > 110) {
return '#332f35'
}
else {
return '#c2bbc7';
}
//take in a color and return its brightness
function colorBrightness (color) {
var r = parseInt(color.slice(1, 3), 16);
var g = parseInt(color.slice(3, 5), 16);
var b = parseInt(color.slice(5, 7), 16);
return Math.round(((parseInt(r) * 299) + (parseInt(g) * 587) + (parseInt(b) * 114)) / 1000);
}
}

View File

@ -1,11 +1,17 @@
// Data saved when copying or cutting
let clipboardData;
// Tells if the user is pasting something or not
let isPasting = false;
// Coordinates of the copied (or cut) selection
let copiedStartX;
let copiedStartY;
let copiedEndX;
let copiedEndY;
/** Copies the current selection to the clipboard
*
*/
function copySelection() {
copiedEndX = endX;
copiedEndY = endY;
@ -16,13 +22,19 @@ function copySelection() {
clipboardData = currentLayer.context.getImageData(startX, startY, endX - startX + 1, endY - startY + 1);
}
/** Pastes the clipboard data onto the current layer
*
*/
function pasteSelection() {
// Can't paste if the layer is locked
if (currentLayer.isLocked) {
return;
}
// Cancel the current selection
endSelection();
// I'm pasting
isPasting = true;
// Putting the image data on the tmp layer
TMPLayer.context.putImageData(clipboardData, copiedStartX, copiedStartY);
@ -43,9 +55,11 @@ function pasteSelection() {
//drawRect(copiedStartX, copiedEndX, copiedStartY, copiedEndY);
}
/** Cuts the current selection and copies it to the clipboard
*
*/
function cutSelectionTool() {
console.log("Taglio");
// Saving the coordinates
copiedEndX = endX;
copiedEndY = endY;
@ -53,13 +67,19 @@ function cutSelectionTool() {
copiedStartY = startY;
// Getting the selected pixels
// If I'm already moving a selection
if (imageDataToMove !== undefined) {
// I just save that selection in the clipboard
clipboardData = imageDataToMove;
// And clear the underlying space
TMPLayer.context.clearRect(0, 0, TMPLayer.canvas.width, TMPLayer.canvas.height);
// The image has been cleared, so I don't have anything to move anymore
imageDataToMove = undefined;
}
else {
clipboardData = currentLayer.context.getImageData(startX, startY, endX - startX + 1, endY - startY + 1);
// Otherwise, I copy the current selection into the clipboard
copySelection();
// And clear the selection
currentLayer.context.clearRect(startX - 0.5, startY - 0.5, endX - startX + 1, endY - startY + 1);
}
}

View File

@ -1,7 +1,51 @@
function create(isSplash) {
var splashPostfix = '';
// If I'm creating from the splash menu, I append '-splash' so I get the corresponding values
if (isSplash) {
splashPostfix = '-splash';
}
var width = getValue('size-width' + splashPostfix);
var height = getValue('size-height' + splashPostfix);
// If I'm creating from the splash screen, I use the splashMode variable
var mode = isSplash ? splashMode : getValue('editor-mode');
newPixel(width, height, mode);
// If I'm not creating from the splash page, then this is not the first project I've created
if (!isSplash)
document.getElementById('new-pixel-warning').style.display = 'block';
//get selected palette name
var selectedPalette = getText('palette-button' + splashPostfix);
if (selectedPalette == 'Choose a palette...')
selectedPalette = 'none';
//track google event
ga('send', 'event', 'Pixel Editor New', selectedPalette, width+'/'+height); /*global ga*/
//reset new form
setValue('size-width', 64);
setValue('size-height', 64);
setValue("editor-mode", 'Advanced')
setText('editor-mode-button', 'Choose a mode...');
setText('palette-button', 'Choose a palette...');
setText('preset-button', 'Choose a preset...');
}
/** Triggered when the "Create" button in the new pixel dialogue is pressed
*
*/
on('click', 'create-button', function (){
// Getting the values of the form
var width = getValue('size-width');
var height = getValue('size-height');
var mode = getValue("editor-mode");
// Creating a new pixel with those properties
newPixel(width, height, mode);
document.getElementById('new-pixel-warning').style.display = 'block';
@ -23,3 +67,37 @@ on('click', 'create-button', function (){
setText('palette-button', 'Choose a palette...');
setText('preset-button', 'Choose a preset...');
});
/** Triggered when the "Create" button in the new pixel dialogue is pressed
*
*/
on('click', 'create-button-splash', function (){
// Getting the values of the form
var width = getValue('size-width-splash');
var height = getValue('size-height-splash');
var mode = pixelEditorMode;
if (mode == 'Advanced')
mode = "Basic";
else
mode = "Advanced";
// Creating a new pixel with those properties
newPixel(width, height, mode);
//track google event
ga('send', 'event', 'Pixel Editor New', selectedPalette, width+'/'+height); /*global ga*/
document.getElementById('new-pixel-warning').style.display = 'block';
// Resetting the new pixel values
selectedPalette = 'none';
//reset new pixel form
setValue('size-width-splash', 64);
setValue('size-height-splash', 64);
setValue("editor-mode", 'Advanced')
setText('editor-mode-button', 'Choose a mode...');
setText('palette-button', 'Choose a palette...');
setText('preset-button', 'Choose a preset...');
});

View File

@ -1,5 +1,11 @@
function createColorPalette(paletteColors, fillBackground, deletePreviousPalette = true) {
/** Creates the colour palette
*
* @param {*} paletteColors The colours of the palette
* @param {*} deletePreviousPalette Tells if the app should delete the previous palette or not
* (used when opening a file, for example)
*/
function createColorPalette(paletteColors, deletePreviousPalette = true) {
//remove current palette
if (deletePreviousPalette) {
colors = document.getElementsByClassName('color-button');
@ -11,6 +17,7 @@ function createColorPalette(paletteColors, fillBackground, deletePreviousPalette
var lightestColor = '#000000';
var darkestColor = '#ffffff';
// Adding all the colours in the array
for (var i = 0; i < paletteColors.length; i++) {
var newColor = paletteColors[i];
var newColorElement = addColor(newColor);
@ -42,6 +49,9 @@ function createColorPalette(paletteColors, fillBackground, deletePreviousPalette
currentLayer.context.fillStyle = darkestColor;
}
/** Creates the palette with the colours used in all the layers
*
*/
function createPaletteFromLayers() {
let colors = {};
@ -68,8 +78,6 @@ function createPaletteFromLayers() {
}
}
console.log(colors);
//create array out of colors object
let colorPaletteArray = [];
for (let color in colors) {
@ -77,8 +85,7 @@ function createPaletteFromLayers() {
colorPaletteArray.push('#'+rgbToHex(colors[color]));
}
}
console.log('COLOR PALETTE ARRAY', colorPaletteArray);
//create palette form colors array
createColorPalette(colorPaletteArray, false);
//create palette from colors array
createColorPalette(colorPaletteArray, true);
}

View File

@ -9,19 +9,19 @@ function deleteColor (color) {
//if color is a string, then find the corresponding button
if (typeof color === 'string') {
console.log('trying to find ',color);
//console.log('trying to find ',color);
//get all colors in palette
colors = document.getElementsByClassName('color-button');
//loop through colors
for (var i = 0; i < colors.length; i++) {
console.log(color,'=',colors[i].jscolor.toString());
//console.log(color,'=',colors[i].jscolor.toString());
if (color == colors[i].jscolor.toString()) {
console.log('match');
//console.log('match');
//set color to the color button
color = colors[i];
console.log('found color', color);
//console.log('found color', color);
//exit loop
break;
@ -30,7 +30,7 @@ function deleteColor (color) {
//if the color wasn't found
if (typeof color === 'string') {
console.log('color not found');
//console.log('color not found');
//exit function
return;
}

View File

@ -1,27 +1,55 @@
let currentOpenDialogue = "";
/** Shows the dialogue window called dialogueName, which is a child of pop-up-container in pixel-editor.hbs
*
* @param {*} dialogueName The name of the window to show
* @param {*} trackEvent Should I track the GA event?
*/
function showDialogue (dialogueName, trackEvent) {
if (typeof trackEvent === 'undefined') trackEvent = true;
// Updating currently open dialogue
currentOpenDialogue = dialogueName;
// The pop up window is open
dialogueOpen = true;
// Showing the pop up container
popUpContainer.style.display = 'block';
// Showing the window
document.getElementById(dialogueName).style.display = 'block';
// If I'm opening the palette window, I initialize the colour picker
if (dialogueName == 'palette-block' && documentCreated) {
cpInit();
pbInit();
}
//track google event
if (trackEvent)
ga('send', 'event', 'Palette Editor Dialogue', dialogueName); /*global ga*/
}
/** Closes the current dialogue by hiding the window and the pop-up-container
*
*/
function closeDialogue () {
popUpContainer.style.display = 'none';
var popups = popUpContainer.children;
for (var i = 0; i < popups.length; i++) {
popups[i].style.display = 'none';
}
dialogueOpen = false;
if (currentOpenDialogue == "palette-block") {
pbAddToSimplePalette();
}
}
/** Closes a dialogue window if the user clicks everywhere but in the current window
*
*/
popUpContainer.addEventListener('click', function (e) {
if (e.target == popUpContainer)
closeDialogue();
@ -31,6 +59,6 @@ popUpContainer.addEventListener('click', function (e) {
var cancelButtons = popUpContainer.getElementsByClassName('close-button');
for (var i = 0; i < cancelButtons.length; i++) {
cancelButtons[i].addEventListener('click', function () {
closeDialogue();
closeDialogue();
});
}

View File

@ -1,4 +1,4 @@
//draw a line between two points on canvas
//draws a line between two points on canvas
function line(x0,y0,x1,y1, brushSize) {
var dx = Math.abs(x1-x0);
var dy = Math.abs(y1-y0);
@ -9,7 +9,7 @@ function line(x0,y0,x1,y1, brushSize) {
while (true) {
//set pixel
// If the current tool is the brush
if (currentTool.name == 'pencil' || currentTool.name == 'rectangle') {
if (currentTool.name == 'pencil' || currentTool.name == 'rectangle' || currentTool.name == 'ellipse') {
// I fill the rect
currentLayer.context.fillRect(x0-Math.floor(brushSize/2), y0-Math.floor(brushSize/2), brushSize, brushSize);
} else if (currentTool.name == 'eraser') {

View File

@ -3,27 +3,46 @@ let modes = {
description: 'Basic mode is perfect if you want to create simple sprites or try out palettes.'
},
'Advanced' : {
description: 'Choose advanced mode to gain access to features such as layers.'
description: 'Choose advanced mode to gain access to more advanced features such as layers.'
}
}
let infoBox = document.getElementById('editor-mode-info');
let currentSplashButton = document.getElementById("sp-mode-palette").children[0].children[1];
function switchMode(currentMode, mustConfirm = true) {
function splashMode(mouseEvent, mode) {
if (currentSplashButton == undefined) {
currentSplashButton = mouseEvent.target.parentElement;
}
if (mode !== pixelEditorMode) {
// Remove selected class to old button
currentSplashButton.classList.remove("sp-interface-selected");
// Add selected class to new button
mouseEvent.target.parentElement.classList.add("sp-interface-selected");
// Setting the new mode
pixelEditorMode = mode;
}
// Setting the new selected button
currentSplashButton = mouseEvent.target.parentElement;
}
function switchMode(mustConfirm = true) {
//switch to advanced mode
if (currentMode == 'Basic') {
if (pixelEditorMode == 'Basic') {
// Switch to advanced ez pez lemon squez
document.getElementById('switch-mode-button').innerHTML = 'Switch to basic mode';
// Show the layer menus
layerList.style.display = "inline-block";
document.getElementById('layer-button').style.display = 'inline-block';
// Move the palette menu
document.getElementById('colors-menu').style.right = '200px';
// Hide the palette menu
document.getElementById('colors-menu').style.right = '200px'
pixelEditorMode = 'Advanced';
}
//switch to basic mode
else {
//if there is a current layer (a document is active)
@ -47,15 +66,17 @@ function switchMode(currentMode, mustConfirm = true) {
// Hide the layer menus
layerList.style.display = 'none';
document.getElementById('layer-button').style.display = 'none';
// Move the palette menu
document.getElementById('colors-menu').style.right = '0px';
// Show the palette menu
document.getElementById('colors-menu').style.display = 'flex';
// Move the palette menu
document.getElementById('colors-menu').style.right = '0px';
pixelEditorMode = 'Basic';
}
}
on('click', 'switch-mode-button', function (e) {
switchMode(pixelEditorMode);
switchMode();
});
// Makes the menu open

146
js/_ellipse.js Normal file
View File

@ -0,0 +1,146 @@
// 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
VFXCanvas.style.zIndex = parseInt(currentLayer.canvas.style.zIndex, 10) + 1;;
// Updating flag
isDrawingEllipse = true;
// Saving the start coords of the ellipse
let cursorPos = 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 = 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 = getCursorPosition(mouseEvent);
let vfxContext = VFXCanvas.getContext("2d");
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 and colour
currentLayer.context.lineWidth = tool.ellipse.brushSize;
currentLayer.context.fillStyle = currentGlobalColor;
// Drawing the ellipse using 4 lines
line(startEllipseX, startEllipseY, endEllipseX, startEllipseY, tool.ellipse.brushSize);
line(endEllipseX, startEllipseY, endEllipseX, endEllipseY, tool.ellipse.brushSize);
line(endEllipseX, endEllipseY, startEllipseX, endEllipseY, tool.ellipse.brushSize);
line(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, VFXCanvas.width, VFXCanvas.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 = VFXCanvas.getContext("2d");
// Clearing the vfx canvas
vfxContext.clearRect(0, 0, VFXCanvas.width, VFXCanvas.height);
// Drawing the ellipse
vfxContext.lineWidth = tool.ellipse.brushSize;
vfxContext.strokeStyle = currentGlobalColor;
// 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');
}
}

33
js/_featureToggles.js Normal file
View File

@ -0,0 +1,33 @@
const featureToggles = (function featureTogglesModule() {
const ellipseToolLocalStorageKey = 'feature_ellipseTool';
return {
onLoad: () => {
updateEllipseToolVisibility()
},
enableEllipseTool,
disableEllipseTool
}
////////
function updateEllipseToolVisibility() {
// TODO: [ELLIPSE] Once ellipse is ready for release make it enabled by default
const isEllipseToolEnabled = (window.localStorage.getItem(ellipseToolLocalStorageKey) === "yes") || false;
const ellipseToolElement = document.getElementById("tools-menu--ellipse");
ellipseToolElement.style.display = isEllipseToolEnabled ? 'block' : 'none';
}
function enableEllipseTool() {
window.localStorage.setItem(ellipseToolLocalStorageKey, "yes");
updateEllipseToolVisibility();
}
function disableEllipseTool() {
window.localStorage.setItem(ellipseToolLocalStorageKey, "no");
updateEllipseToolVisibility();
}
})();

1
js/_featuresLog.js Normal file
View File

@ -0,0 +1 @@
showDialogue("splash", false);

View File

@ -7,19 +7,14 @@ for (var i = 1; i < mainMenuItems.length; i++) {
var menuItem = mainMenuItems[i];
var menuButton = menuItem.children[0];
console.log(mainMenuItems);
//when you click a main menu items button
on('click', menuButton, function (e, button) {
console.log('parent ', button.parentElement);
select(button.parentElement);
});
var subMenu = menuItem.children[1];
var subMenuItems = subMenu.children;
//when you click an item within a menu button
for (var j = 0; j < subMenuItems.length; j++) {

View File

@ -7,11 +7,6 @@ function fill(cursorLocation) {
tempImage.data[pixelPos + 1] = fillColor.g;
tempImage.data[pixelPos + 2] = fillColor.b;
tempImage.data[pixelPos + 3] = 255;
/*
tempImage.data[pixelPos] = fillColor.r;
tempImage.data[pixelPos + 1] = fillColor.g;
tempImage.data[pixelPos + 2] = fillColor.b;
*/
}
//change x y to color value passed from the function and use that as the original color
@ -21,8 +16,9 @@ function fill(cursorLocation) {
var r = tempImage.data[pixelPos];
var g = tempImage.data[pixelPos + 1];
var b = tempImage.data[pixelPos + 2];
var a = tempImage.data[pixelPos + 3];
//console.log(r == color[0] && g == color[1] && b == color[2]);
return (r == color[0] && g == color[1] && b == color[2]);
return (r == color[0] && g == color[1] && b == color[2] && a == color[3]);
}
//save history state
@ -41,16 +37,21 @@ function fill(cursorLocation) {
var startingPosition = (topmostPixelsArray[0][1] * canvasSize[0] + topmostPixelsArray[0][0]) * 4;
//the color of the cluster that is being filled
var clusterColor = [tempImage.data[startingPosition],tempImage.data[startingPosition+1],tempImage.data[startingPosition+2]];
var clusterColor = [tempImage.data[startingPosition],tempImage.data[startingPosition+1],tempImage.data[startingPosition+2], tempImage.data[startingPosition+3]];
//the new color to fill with
var fillColor = hexToRgb(currentLayer.context.fillStyle);
console.log("here");
//if you try to fill with the same color that's already there, exit the function
if (clusterColor[0] == fillColor.r &&
clusterColor[1] == fillColor.g &&
clusterColor[2] == fillColor.b )
return;
clusterColor[2] == fillColor.b &&
clusterColor[3] != 0) {
console.log("Returned");
return;
}
//loop until there are no more values left in this array
while (topmostPixelsArray.length) {

View File

@ -1,4 +1,4 @@
//get cursor position relative to canvas
//gets cursor position relative to canvas
function getCursorPosition(e) {
var x;
var y;

View File

@ -1,3 +1,17 @@
/** How the history works
* - undoStates stores the states that can be undone
* - redoStates stores the states that can be redone
* - undo() undoes an action and adds it to the redoStates
* - redo() redoes an action and adds it to the undoStates
* - Each HistoryState must implement an undo() and redo() function
* Those functions actually implement the undo and redo mechanism for that action,
* so you'll need to save the data you need as attributes in the constructor. For example,
* for the HistoryStateAddColour, the added colour is saved so that it can be removed in
* undo() or added back in redo().
* - Each HistoryState must call saveHistoryState(this) so that it gets added to the stack
*
*/
var undoStates = [];
var redoStates = [];
@ -28,6 +42,8 @@ function HistoryStateResizeSprite(xRatio, yRatio, algo, oldData) {
};
this.redo = function() {
console.log("REDOOOO");
console.log("Ratio: " + this.xRatio + "," + this.yRatio);
currentAlgo = algo;
resizeSprite(null, [this.xRatio, this.yRatio]);
undoStates.push(this);
@ -44,7 +60,6 @@ function HistoryStateResizeCanvas(newSize, oldSize, imageDatas, trim) {
this.undo = function() {
let dataIndex = 0;
console.log("breakpoint");
// Resizing the canvas
resizeCanvas(null, oldSize, null, false);
// Putting the image datas
@ -59,7 +74,6 @@ function HistoryStateResizeCanvas(newSize, oldSize, imageDatas, trim) {
};
this.redo = function() {
console.log("trim: " + this.trim);
if (!this.trim) {
resizeCanvas(null, newSize, null, false);
}
@ -101,7 +115,6 @@ function HistoryStateFlattenTwoVisibles(belowImageData, afterAbove, layerIndex,
this.belowImageData = belowImageData;
this.undo = function() {
console.log(afterAbove.menuEntry);
canvasView.append(aboveLayer.canvas);
layerList.insertBefore(aboveLayer.menuEntry, afterAbove);
@ -300,7 +313,6 @@ function HistoryStateAddLayer(layerData, index) {
this.index = index;
this.undo = function() {
console.log("uo");
redoStates.push(this);
if (layers.length - nAppLayers > this.index + 1) {
@ -473,8 +485,6 @@ function saveHistoryState (state) {
}
function undo () {
//console.log('%cundo', undoLogStyle);
//if there are any states saved to undo
if (undoStates.length > 0) {
document.getElementById('redo-button').classList.remove('disabled');
@ -496,8 +506,6 @@ function undo () {
}
function redo () {
//console.log('%credo', undoLogStyle);
if (redoStates.length > 0) {
//enable undo button

View File

@ -1,5 +1,9 @@
var spacePressed = false;
/** Just listens to hotkeys and calls the linked functions
*
* @param {*} e
*/
function KeyPress(e) {
var keyboardEvent = window.event? event : e;
@ -46,6 +50,9 @@ function KeyPress(e) {
case 52: case 80:
tool.pan.switchTo();
break;
case 76:
tool.line.switchTo();
break;
//zoom - 5
case 53:
tool.zoom.switchTo();
@ -58,6 +65,15 @@ function KeyPress(e) {
case 77: case 109:
tool.rectselect.switchTo()
break;
// TODO: [ELLIPSE] Decide on a shortcut to use. "s" was chosen without any in-team consultation.
// ellipse tool, s
case 83:
tool.ellipse.switchTo()
break;
// rectangle tool, u
case 85:
tool.rectangle.switchTo()
break;
// Paste tool
case 86: case 118:
if (keyboardEvent.ctrlKey && !dragging) {

View File

@ -1,5 +1,7 @@
// NEXTPULL: to remove when the new palette system is added
//format a color button
//formats a color button
function initColor (colorElement) {
//console.log('initColor()');
//console.log(document.getElementById('jscolor-hex-input'))

View File

@ -1,3 +1,5 @@
// NEXTPULL: to remove when the new palette system is added
/**
* jscolor - JavaScript Color Picker
*

View File

@ -1,31 +1,29 @@
/** TODO LIST FOR LAYERS
GENERAL REQUIREMENTS:
- Saving the state of an artwork to a .lospec file so that people can work on it later keeping
the layers they created? That'd be cool, even for the app users, that could just double click on a lospec
file for it to be opened right in the pixel editor
OPTIONAL:
1 - Fix issues
*/
// HTML element that contains the layer entries
let layerList;
// A single layer entry (used as a prototype to create the new ones)
let layerListEntry;
// NEXTPULL: remove the drag n drop system and use Sortable.js instead
let layerDragSource = null;
// Number of layers at the beginning
let layerCount = 1;
// Current max z index (so that I know which z-index to assign to new layers)
let maxZIndex = 3;
// When a layer is deleted, its id is added to this array and can be reused
let unusedIDs = [];
// Id for the next added layer
let currentID = layerCount;
let idToDelete;
// Layer menu
let layerOptions = document.getElementById("layer-properties-menu");
// Is the user currently renaming a layer?
let isRenamingLayer = false;
// I need to save this, trust me
let oldLayerName = null;
let dragStartLayer;
// Binding the add layer button to the function
on('click',"add-layer-button", addLayer, false);
/** Handler class for a single canvas (a single layer)
@ -54,6 +52,7 @@ class Layer {
this.id = "layer" + id;
// Binding the events
if (menuEntry != null) {
this.name = menuEntry.getElementsByTagName("p")[0].innerHTML;
menuEntry.id = "layer" + id;
@ -78,20 +77,13 @@ class Layer {
// Initializes the canvas
initialize() {
/*
var maxHorizontalZoom = Math.floor(window.innerWidth/this.canvasSize[0]*0.75);
var maxVerticalZoom = Math.floor(window.innerHeight/this.canvasSize[1]*0.75);
zoom = Math.min(maxHorizontalZoom,maxVerticalZoom);
if (zoom < 1) zoom = 1;
*/
//resize canvas
this.canvas.width = this.canvasSize[0];
this.canvas.height = this.canvasSize[1];
this.canvas.style.width = (this.canvas.width*zoom)+'px';
this.canvas.style.height = (this.canvas.height*zoom)+'px';
//unhide canvas
//show canvas
this.canvas.style.display = 'block';
//center canvas in window
@ -103,7 +95,7 @@ class Layer {
}
hover() {
// Hide all the layers but the current one
// Hides all the layers but the current one
for (let i=1; i<layers.length - nAppLayers; i++) {
if (layers[i] !== this) {
layers[i].canvas.style.opacity = 0.3;
@ -112,7 +104,7 @@ class Layer {
}
unhover() {
// Show all the layers again
// Shows all the layers again
for (let i=1; i<layers.length - nAppLayers; i++) {
if (layers[i] !== this) {
layers[i].canvas.style.opacity = 1;
@ -127,52 +119,6 @@ class Layer {
}
}
layerDragStart(element) {
layerDragSource = this;
element.dataTransfer.effectAllowed = 'move';
element.dataTransfer.setData('text/html', this.id);
this.classList.add('dragElem');
}
layerDragOver(element) {
if (element.preventDefault) {
element.preventDefault(); // Necessary. Allows us to drop.
}
this.classList.add('layerdragover');
element.dataTransfer.dropEffect = 'move';
return false;
}
layerDragLeave(element) {
this.classList.remove('layerdragover');
}
layerDragDrop(element) {
if (element.stopPropagation) {
element.stopPropagation(); // Stops some browsers from redirecting.
}
// Don't do anything if dropping the same column we're dragging.
if (layerDragSource != this) {
let toDropID = element.dataTransfer.getData('text/html');
let thisID = this.id;
moveLayers(toDropID, thisID);
}
this.classList.remove('layerdragover');
dragging = false;
return false;
}
layerDragEnd(element) {
this.classList.remove('layerdragover');
}
// Resizes canvas
resize() {
let newWidth = (this.canvas.width * zoom) + 'px';
@ -217,20 +163,20 @@ class Layer {
openOptionsMenu(event) {
if (event.which == 3) {
let selectedId;
let target = event.target;
let offsets = getElementAbsolutePosition(this);
while (target != null && target.classList != null && !target.classList.contains("layers-menu-entry")) {
target = target.parentElement;
}
idToDelete = target.id;
selectedId = target.id;
layerOptions.style.visibility = "visible";
layerOptions.style.top = "0";
layerOptions.style.marginTop = "" + (event.clientY - 25) + "px";
getLayerByID(idToDelete).selectLayer();
getLayerByID(selectedId).selectLayer();
}
}
@ -265,10 +211,6 @@ class Layer {
layer.menuEntry.classList.add("selected-layer");
currentLayer = layer;
}
/*
canvas = currentLayer.canvas;
context = currentLayer.context;
*/
}
toggleLock() {
@ -442,7 +384,6 @@ function merge(saveHistory = true) {
// Updating the layer preview
currentLayer.updateLayerPreview();
}
}
function deleteLayer(saveHistory = true) {
@ -456,7 +397,7 @@ function deleteLayer(saveHistory = true) {
unusedIDs.push(toDelete.id);
// Selecting the next layer
if (layerIndex != (layers.length - 3)) {
if (layerIndex != (layers.length - 4)) {
layers[layerIndex + 1].selectLayer();
}
// or the previous one if the next one doesn't exist
@ -489,13 +430,13 @@ function duplicateLayer(event, saveHistory = true) {
for (let i=getMenuEntryIndex(menuEntries, toDuplicate.menuEntry) - 1; i>=0; i--) {
getLayerByID(menuEntries[i].id).canvas.style.zIndex++;
}
maxZIndex++;
maxZIndex+=2;
// Creating a new canvas
let newCanvas = document.createElement("canvas");
// Setting up the new canvas
canvasView.append(newCanvas);
newCanvas.style.zIndex = parseInt(currentLayer.canvas.style.zIndex) + 1;
newCanvas.style.zIndex = parseInt(currentLayer.canvas.style.zIndex) + 2;
newCanvas.classList.add("drawingCanvas");
if (!layerListEntry) return console.warn('skipping adding layer because no document');
@ -545,78 +486,6 @@ function renameLayer(event) {
isRenamingLayer = true;
}
// Swaps two layer entries in the layer menu
function moveLayers(toDropLayer, staticLayer, saveHistory = true) {
let toDrop = getLayerByID(toDropLayer);
let staticc = getLayerByID(staticLayer);
let layerCopy = layers.slice();
let beforeToDrop = toDrop.menuEntry.nextElementSibling;
let nMoved = 0;
layerCopy.sort((a, b) => (a.canvas.style.zIndex > b.canvas.style.zIndex) ? 1 : -1);
let toDropIndex = layerCopy.indexOf(toDrop);
let staticIndex = layerCopy.indexOf(staticc);
layerList.insertBefore(toDrop.menuEntry, staticc.menuEntry);
if (toDropIndex < staticIndex) {
let tmp = toDrop.canvas.style.zIndex;
let tmp2;
for (let i=toDropIndex+1; i<=staticIndex; i++) {
tmp2 = layerCopy[i].canvas.style.zIndex;
if (saveHistory) {
new HistoryStateMoveTwoLayers(layerCopy[i], tmp2, tmp);
}
layerCopy[i].canvas.style.zIndex = tmp;
tmp = tmp2;
nMoved++;
}
if (saveHistory) {
new HistoryStateMoveTwoLayers(toDrop, toDrop.canvas.style.zIndex, tmp);
}
toDrop.canvas.style.zIndex = tmp;
if (saveHistory) {
new HistoryStateMoveLayer(beforeToDrop, toDrop, staticc, nMoved);
}
}
else {
// BUG QUI
let tmp = toDrop.canvas.style.zIndex;
let tmp2;
for (let i=toDropIndex-1; i>staticIndex; i--) {
tmp2 = layerCopy[i].canvas.style.zIndex;
if (saveHistory) {
new HistoryStateMoveTwoLayers(layerCopy[i], tmp2, tmp);
}
layerCopy[i].canvas.style.zIndex = tmp;
tmp = tmp2;
nMoved++;
}
if (saveHistory) {
new HistoryStateMoveTwoLayers(toDrop, toDrop.canvas.style.zIndex, tmp);
}
toDrop.canvas.style.zIndex = tmp;
if (saveHistory) {
new HistoryStateMoveLayer(beforeToDrop, toDrop, staticc, nMoved);
}
}
}
function getMenuEntryIndex(list, entry) {
for (let i=0; i<list.length; i++) {
if (list[i] === entry) {
@ -660,7 +529,7 @@ function addLayer(id, saveHistory = true) {
let newCanvas = document.createElement("canvas");
// Setting up the new canvas
canvasView.append(newCanvas);
maxZIndex++;
maxZIndex+=2;
newCanvas.style.zIndex = maxZIndex;
newCanvas.classList.add("drawingCanvas");
@ -696,4 +565,49 @@ function addLayer(id, saveHistory = true) {
return newLayer;
}
layerList = document.getElementById("layers-menu");
/** Saves the layer that is being moved when the dragging starts
*
* @param {*} event
*/
function layerDragStart(event) {
dragStartLayer = getLayerByID(layerList.children[event.oldIndex].id);
}
/** Sets the z indexes of the layers when the user drops the layer in the menu
*
* @param {*} event
*/
function layerDragDrop(event) {
let oldIndex = event.oldDraggableIndex;
let newIndex = event.newDraggableIndex;
let movedZIndex = dragStartLayer.canvas.style.zIndex;
if (oldIndex > newIndex)
{
for (let i=newIndex; i<oldIndex; i++) {
getLayerByID(layerList.children[i].id).canvas.style.zIndex = getLayerByID(layerList.children[i + 1].id).canvas.style.zIndex;
}
}
else
{
for (let i=newIndex; i>oldIndex; i--) {
getLayerByID(layerList.children[i].id).canvas.style.zIndex = getLayerByID(layerList.children[i - 1].id).canvas.style.zIndex;
}
}
getLayerByID(layerList.children[oldIndex].id).canvas.style.zIndex = movedZIndex;
dragging = false;
}
layerList = document.getElementById("layers-menu");
// Making the layers list sortable
new Sortable(document.getElementById("layers-menu"), {
animation: 100,
filter: ".layer-button",
draggable: ".layers-menu-entry",
onStart: layerDragStart,
onEnd: layerDragDrop
});

45
js/_line.js Normal file
View File

@ -0,0 +1,45 @@
function diagLine(lastMouseClickPos, zoom, cursorLocation) {
let x0 = Math.floor(lastMouseClickPos[0]/zoom);
let y0 = Math.floor(lastMouseClickPos[1]/zoom);
let x1 = Math.floor(cursorLocation[0]/zoom);
let y1 = Math.floor(cursorLocation[1]/zoom);
let dx = Math.abs(x1-x0);
let dy = Math.abs(y1-y0);
let sx = (x0 < x1 ? 1 : -1);
let sy = (y0 < y1 ? 1 : -1);
let err = dx-dy;
const brushSize = tool.line.brushSize;
const canvas = document.getElementById('tmp-canvas');
const context = canvas.getContext('2d');
context.fillStyle=currentGlobalColor;
context.clearRect(0, 0, canvas.width, canvas.height);
canvas.style.zIndex = parseInt(currentLayer.canvas.style.zIndex, 10) + 1;
//console.log(canvas.style.zIndex, currentLayer.canvas.style.zIndex);
while (true) {
if (currentTool.name !== 'line') return;
context.fillRect(x0-Math.floor(brushSize/2), y0-Math.floor(brushSize/2), brushSize, brushSize);
//if we've reached the end goal, exit the loop
if ((x0==x1) && (y0==y1)) break;
var e2 = 2*err;
if (e2 >-dy) {
err -=dy;
x0+=sx;
}
if (e2 < dx) {
err +=dx;
y0+=sy;
}
}
}

View File

@ -1,18 +1,27 @@
/** Loads a file (.png or .lpe)
*
*/
document.getElementById('open-image-browse-holder').addEventListener('change', function () {
let fileName = document.getElementById("open-image-browse-holder").value;
// Getting the extension
let extension = fileName.substring(fileName.lastIndexOf('.')+1, fileName.length) || fileName;
// I didn't write this check and I have no idea what it does
if (this.files && this.files[0]) {
// Btw, checking if the extension is supported
if (extension == 'png' || extension == 'gif' || extension == 'lpe') {
// If it's a Lospec Pixel Editor tm file, I load the project
if (extension == 'lpe') {
let file = this.files[0];
let reader = new FileReader();
// Getting all the data
reader.readAsText(file, "UTF-8");
// Converting the data to a json object and creating a new pixel (see _newPixel.js for more)
reader.onload = function (e) {
let dictionary = JSON.parse(e.target.result);
newPixel(dictionary['canvasWidth'], dictionary['canvasHeight'], dictionary['editorMode'], dictionary);
let mode = dictionary['editorMode'] == 'Advanced' ? 'Basic' : 'Advanced';
newPixel(dictionary['canvasWidth'], dictionary['canvasHeight'], mode, dictionary);
}
}
else {

View File

@ -1,8 +1,8 @@
//this is called when a user picks a file after selecting "load palette" from the new pixel dialogue
// TODO: load palette from .lpe file
document.getElementById('load-palette-browse-holder').addEventListener('change', function () {
if (this.files && this.files[0]) {
//make sure file is allowed filetype
var fileContentType = this.files[0].type;
if (fileContentType == 'image/png' || fileContentType == 'image/gif') {
@ -26,8 +26,6 @@ document.getElementById('load-palette-browse-holder').addEventListener('change',
var colorPalette = [];
var imagePixelData = loadPaletteContext.getImageData(0,0,this.width, this.height).data;
console.log(imagePixelData);
//loop through pixels looking for colors to add to palette
for (var i = 0; i < imagePixelData.length; i += 4) {
var color = '#'+rgbToHex(imagePixelData[i],imagePixelData[i + 1],imagePixelData[i + 2]);
@ -40,6 +38,8 @@ document.getElementById('load-palette-browse-holder').addEventListener('change',
palettes['Loaded palette'] = {};
palettes['Loaded palette'].colors = colorPalette;
setText('palette-button', 'Loaded palette');
setText('palette-button-splash', 'Loaded palette');
toggle('palette-menu-splash');
};
img.src = e.target.result;
};

15
js/_logs.js Normal file
View File

@ -0,0 +1,15 @@
var rawFile = new XMLHttpRequest();
rawFile.open("GET", '/pixel-editor/latestLog.html', false);
rawFile.onreadystatechange = function ()
{
if(rawFile.readyState === 4)
{
if(rawFile.status === 200 || rawFile.status == 0)
{
var allText = rawFile.responseText;
document.getElementById("latest-update").innerHTML = allText;
}
}
}
rawFile.send(null);

View File

@ -22,7 +22,7 @@ window.addEventListener("mousedown", function (mouseEvent) {
else if (mouseEvent.altKey)
currentTool = tool.eyedropper;
else if (mouseEvent.target.className == 'drawingCanvas' &&
(currentTool.name == 'pencil' || currentTool.name == 'eraser' || currentTool.name == 'rectangle'))
(currentTool.name == 'pencil' || currentTool.name == 'eraser' || currentTool.name == 'rectangle' || currentTool.name == 'ellipse' || currentTool.name === 'line'))
new HistoryStateEditCanvas();
else if (currentTool.name == 'moveselection') {
if (!cursorInSelectedArea() &&
@ -30,14 +30,15 @@ window.addEventListener("mousedown", function (mouseEvent) {
tool.pencil.switchTo();
canDraw = false;
}
}
currentTool.updateCursor();
}
if (!currentLayer.isLocked && !currentLayer.isVisible && canDraw) {
draw(mouseEvent);
}
}
else if (mouseEvent.which == 2) {
currentTool = tool.pan;
}
else if (currentTool.name == 'pencil' && mouseEvent.which == 3) {
currentTool = tool.resizebrush;
tool.pencil.previousBrushSize = tool.pencil.brushSize;
@ -46,10 +47,15 @@ window.addEventListener("mousedown", function (mouseEvent) {
currentTool = tool.resizeeraser;
tool.eraser.previousBrushSize = tool.eraser.brushSize;
}
// TODO: [ELLIPSE] Do we need similar logic related to ellipse?
else if (currentTool.name == 'rectangle' && mouseEvent.which == 3) {
currentTool = tool.resizerectangle;
tool.rectangle.previousBrushSize = tool.rectangle.brushSize;
}
else if (currentTool.name == 'line' && mouseEvent.which == 3) {
currentTool = tool.resizeline;
tool.line.previousBrushSize = tool.line.brushSize;
}
if (currentTool.name == 'eyedropper' && mouseEvent.target.className == 'drawingCanvas')
eyedropperPreview.style.display = 'block';
@ -70,6 +76,15 @@ window.addEventListener("mouseup", function (mouseEvent) {
currentLayer.closeOptionsMenu();
}
// If the user finished placing down a line, clear the tmp canvas and copy the data to the current layer
if (currentTool.name === "line") {
const tmpCanvas = document.getElementById('tmp-canvas');
currentLayer.context.drawImage(tmpCanvas, 0, 0);
const tmpContext = tmpCanvas.getContext('2d');
tmpContext.clearRect(0, 0, tmpCanvas.width, tmpCanvas.height);
}
if (!documentCreated || dialogueOpen || !currentLayer.isVisible || currentLayer.isLocked) return;
if (currentTool.name == 'eyedropper' && mouseEvent.target.className == 'drawingCanvas') {
@ -134,7 +149,7 @@ window.addEventListener("mouseup", function (mouseEvent) {
mode = 'out';
}
changeZoom(layers[0], mode, getCursorPosition(mouseEvent));
changeZoom(mode, getCursorPosition(mouseEvent));
for (let i=1; i<layers.length; i++) {
layers[i].copyData(layers[0]);
@ -147,6 +162,10 @@ window.addEventListener("mouseup", function (mouseEvent) {
endRectDrawing(mouseEvent);
currentLayer.updateLayerPreview();
}
else if (currentTool.name == 'ellipse' && isDrawingEllipse) {
endEllipseDrawing(mouseEvent);
currentLayer.updateLayerPreview();
}
dragging = false;
currentTool = currentToolTemp;
@ -156,7 +175,6 @@ window.addEventListener("mouseup", function (mouseEvent) {
}, false);
// TODO: Make it snap to the pixel grid
function setPreviewPosition(preview, size){
let toAdd = 0;
@ -182,6 +200,8 @@ function setPreviewPosition(preview, size){
//mouse is moving on canvas
window.addEventListener("mousemove", draw, false);
window.addEventListener("mousedown", draw, false);
function draw (mouseEvent) {
if (!dialogueOpen)
{
@ -209,7 +229,12 @@ function draw (mouseEvent) {
//draw line to current pixel
if (dragging) {
if (mouseEvent.target.className == 'drawingCanvas' || mouseEvent.target.className == 'drawingCanvas') {
line(Math.floor(lastMouseClickPos[0]/zoom),Math.floor(lastMouseClickPos[1]/zoom),Math.floor(cursorLocation[0]/zoom),Math.floor(cursorLocation[1]/zoom), tool.pencil.brushSize);
line(Math.floor(lastMouseClickPos[0]/zoom),
Math.floor(lastMouseClickPos[1]/zoom),
Math.floor(cursorLocation[0]/zoom),
Math.floor(cursorLocation[1]/zoom),
tool.pencil.brushSize
);
lastMouseClickPos = cursorLocation;
}
}
@ -257,6 +282,21 @@ function draw (mouseEvent) {
updateRectDrawing(mouseEvent);
}
}
else if (currentTool.name == 'ellipse')
{
//hide brush preview outside of canvas / canvas view
if (mouseEvent.target.className == 'drawingCanvas'|| mouseEvent.target.className == 'drawingCanvas')
brushPreview.style.visibility = 'visible';
else
brushPreview.style.visibility = 'hidden';
if (!isDrawingEllipse && dragging) {
startEllipseDrawing(mouseEvent);
}
else if (dragging){
updateEllipseDrawing(mouseEvent);
}
}
else if (currentTool.name == 'pan' && dragging) {
// Setting first layer position
layers[0].setCanvasOffset(layers[0].canvas.offsetLeft + (cursorLocation[0] - lastMouseClickPos[0]), layers[0].canvas.offsetTop + (cursorLocation[1] - lastMouseClickPos[1]));
@ -316,15 +356,33 @@ function draw (mouseEvent) {
//var roundingAmount = 20 - Math.round(distanceFromClick/10);
//this doesnt work in reverse... because... it's not basing it off of the brush size which it should be
var rectangleSizeChange = Math.round(distanceFromClick/10);
// TODO: [ELLIPSE] Do we need similar logic related to ellipse?
var newRectangleSize = tool.rectangle.previousBrushSize + rectangleSizeChange;
//set the brush to the new size as long as its bigger than 1
// TODO: [ELLIPSE] Do we need similar logic related to ellipse?
tool.rectangle.brushSize = Math.max(1,newRectangleSize);
//fix offset so the cursor stays centered
// TODO: [ELLIPSE] Do we need similar logic related to ellipse?
tool.rectangle.moveBrushPreview(lastMouseClickPos);
currentTool.updateCursor();
}
else if (currentTool.name == 'resizeline' && dragging) {
//get new brush size based on x distance from original clicking location
var distanceFromClick = cursorLocation[0] - lastMouseClickPos[0];
//var roundingAmount = 20 - Math.round(distanceFromClick/10);
//this doesnt work in reverse... because... it's not basing it off of the brush size which it should be
var lineSizeChange = Math.round(distanceFromClick/10);
var newLineSize = tool.line.previousBrushSize + lineSizeChange;
//set the brush to the new size as long as its bigger than 1
tool.line.brushSize = Math.max(1, newLineSize);
//fix offset so the cursor stays centered
tool.line.moveBrushPreview(lastMouseClickPos);
currentTool.updateCursor();
}
else if (currentTool.name == 'rectselect') {
if (dragging && !isRectSelecting && mouseEvent.target.className == 'drawingCanvas') {
isRectSelecting = true;
@ -346,7 +404,27 @@ function draw (mouseEvent) {
updateMovePreview(getCursorPosition(mouseEvent));
}
}
else if (currentTool.name === "line") {
if (mouseEvent.target.className == 'drawingCanvas'|| mouseEvent.target.className == 'drawingCanvas') {
brushPreview.style.visibility = 'visible';
} else {
brushPreview.style.visibility = 'hidden';
}
if (dragging) {
if (mouseEvent.target.className == 'drawingCanvas' || mouseEvent.target.className == 'drawingCanvas') {
diagLine(lastMouseClickPos, zoom, cursorLocation);
}
}
currentLayer.updateLayerPreview();
}
}
if (mouseEvent.target.className == 'drawingCanvas')
currentTool.updateCursor();
else
canvasView.style.cursor = 'default';
console.log("Cursor: " + canvasView.style.cursor);
}
//mousewheel scroll
@ -360,7 +438,7 @@ canvasView.addEventListener("wheel", function(mouseEvent){
}
// Changing zoom and position of the first layer
changeZoom(layers[0], mode, getCursorPosition(mouseEvent));
changeZoom(mode, getCursorPosition(mouseEvent));
for (let i=1; i<layers.length; i++) {
// Copying first layer's data into the other layers

View File

@ -6,9 +6,15 @@ var selectionCanceled = true;
var firstTimeMove = true;
// TODO: move with arrows
/** Updates the move preview so that is placed in the right position
*
* @param {*} mousePosition The position of the cursor
*/
function updateMovePreview(mousePosition) {
// I haven't canceled the selection
selectionCanceled = false;
// If it's the first time that I move the selection, I cut it from its original position
if (firstTimeMove) {
cutSelection(mousePosition);
}
@ -18,26 +24,34 @@ function updateMovePreview(mousePosition) {
lastMousePos = mousePosition;
// clear the entire tmp layer
TMPLayer.context.clearRect(0, 0, TMPLayer.canvas.width, TMPLayer.canvas.height);
// put the image data with offset
// put the image data on the tmp layer with offset
TMPLayer.context.putImageData(
imageDataToMove,
Math.round(lastMousePos[0] / zoom) - imageDataToMove.width / 2,
Math.round(lastMousePos[1] / zoom) - imageDataToMove.height / 2);
lastMovePos = lastMousePos;
// Moving the the rectangular ants
moveSelection(lastMousePos[0] / zoom, lastMousePos[1] / zoom, imageDataToMove.width, imageDataToMove.height);
}
/** Ends a selection, meaning that it makes the changes definitive and creates the history states
*
*/
function endSelection() {
// Clearing the tmp (move preview) and vfx (ants) layers
TMPLayer.context.clearRect(0, 0, TMPLayer.canvas.width, TMPLayer.canvas.height);
VFXLayer.context.clearRect(0, 0, VFXLayer.canvas.width, VFXLayer.canvas.height);
// Preparing an empty imageData with the size of the canvas
let cleanImageData = new ImageData(endX - startX, endY - startY);
// If I was moving something
if (imageDataToMove !== undefined) {
console.log("definito");
// Saving the current clipboard before editing it in order to merge it with the current layer
cleanImageData.data.set(imageDataToMove.data);
// 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 = currentLayer.context.getImageData(startX, startY, endX - startX, endY - startY);
for (let i=0; i<underlyingImageData.data.length; i+=4) {
@ -51,6 +65,7 @@ function endSelection() {
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 (isPixelEmpty(currentMovePixel)) {
if (!isPixelEmpty(underlyingImageData)) {
imageDataToMove.data[i] = currentUnderlyingPixel[0];
@ -61,13 +76,16 @@ function endSelection() {
}
}
// If I moved the selection before confirming it
if (lastMovePos !== undefined) {
// I put it in the new position
currentLayer.context.putImageData(
imageDataToMove,
Math.round(lastMovePos[0] / zoom) - imageDataToMove.width / 2,
Math.round(lastMovePos[1] / zoom) - imageDataToMove.height / 2);
}
else {
// I put it in the same position
currentLayer.context.putImageData(
imageDataToMove,
copiedStartX,
@ -77,6 +95,7 @@ function endSelection() {
imageDataToMove.data.set(cleanImageData.data);
}
// Resetting all the flags
selectionCanceled = true;
isRectSelecting = false;
firstTimeMove = true;
@ -86,5 +105,6 @@ function endSelection() {
lastMovePos = undefined;
currentLayer.updateLayerPreview();
// Saving the history
new HistoryStateEditCanvas();
}

View File

@ -1,16 +1,32 @@
let firstPixel = true;
/** Creates a new, empty file
*
* @param {*} width Start width of the canvas
* @param {*} height Start height of the canvas
* @param {*} editorMode The editor mode chosen by the user (advanced or basic)
* @param {*} fileContent If fileContent != null, then the newPixel is being called from the open menu
*/
function newPixel (width, height, editorMode, fileContent = null) {
// Saving the editor mode
pixelEditorMode = editorMode;
// The palette is empty, at the beginning
currentPalette = [];
// If this is the first pixel I'm creating since the app has started
if (firstPixel) {
// I configure the layers elements
layerListEntry = layerList.firstElementChild;
// Creating the first layer
currentLayer = new Layer(width, height, canvas, layerListEntry);
currentLayer.canvas.style.zIndex = 2;
}
else {
// If it's not the first Pixel, I have to reset the app
// Deleting all the extra layers and canvases, leaving only one
let nLayers = layers.length;
for (let i=2; i < layers.length - nAppLayers; i++) {
let currentEntry = layers[i].menuEntry;
@ -38,10 +54,9 @@ function newPixel (width, height, editorMode, fileContent = null) {
// Setting up the current layer
layers[1] = new Layer(width, height, layers[1].canvas, layers[1].menuEntry);
currentLayer = layers[1];
currentLayer.canvas.style.zIndex = 2;
// Updating canvas size
// Updating canvas size to the new size
for (let i=0; i<nLayers; i++) {
layers[i].canvasSize = [width, height];
}
@ -58,7 +73,7 @@ function newPixel (width, height, editorMode, fileContent = null) {
// Pixel grid
pixelGrid = new Layer(width, height, pixelGridCanvas);
// Setting the general canvasSize
canvasSize = currentLayer.canvasSize;
if (firstPixel) {
@ -80,16 +95,23 @@ function newPixel (width, height, editorMode, fileContent = null) {
}
//add colors from selected palette
var selectedPalette = getText('palette-button');
var selectedPalette;
if (!firstPixel)
var selectedPalette = getText('palette-button');
else
var selectedPalette = getText('palette-button-splash');
// If the user selected a palette and isn't opening a file, I load the selected palette
if (selectedPalette != 'Choose a palette...' && fileContent == null) {
//if this palette isnt the one specified in the url, then reset the url
if (!palettes[selectedPalette].specified)
history.pushState(null, null, '/pixel-editor/app');
//fill the palette with specified palette
//fill the palette with specified colours
createColorPalette(palettes[selectedPalette].colors,true);
}
// Otherwise, I just generate 2 semirandom colours
else if (fileContent == null) {
//this wasn't a specified palette, so reset the url
history.pushState(null, null, '/pixel-editor/app');
@ -120,15 +142,21 @@ function newPixel (width, height, editorMode, fileContent = null) {
undoStates = [];
redoStates = [];
// Closing the "New Pixel dialogue"
closeDialogue();
// Updating the cursor of the current tool
currentTool.updateCursor();
// The user is now able to export the Pixel
document.getElementById('export-button').classList.remove('disabled');
documentCreated = true;
// This is not the first Pixel anymore
firstPixel = false;
// Now, if I opened an LPE file
if (fileContent != null) {
// I add every layer the file had in it
for (let i=0; i<fileContent['nLayers']; i++) {
let layerData = fileContent['layer' + i];
let layerImage = fileContent['layer' + i + 'ImageData'];
@ -166,10 +194,27 @@ function newPixel (width, height, editorMode, fileContent = null) {
deleteLayer(false);
}
// Applying the correct editor mode
if (pixelEditorMode == 'Basic') {
switchMode('Advanced', false);
switchMode(false);
}
else {
switchMode('Basic', false);
switchMode(false);
}
// Resetting history
undoStates = [];
redoStates = [];
}
function newFromTemplate(preset, x, y) {
if (preset != '') {
const presetProperties = PresetModule.propertiesOf(preset);
Util.setText('palette-button-splash', presetProperties.palette);
Util.setText('palette-button', presetProperties.palette);
x = presetProperties.width;
y = presetProperties.height;
}
newPixel(x, y, pixelEditorMode == 'Advanced' ? 'Basic' : 'Advanced');
}

View File

@ -1,12 +1,17 @@
//when the page is donw loading, you can get ready to start
window.onload = function(){
currentTool.updateCursor();
//when the page is done loading, you can get ready to start
window.onload = function () {
//if the user specified dimentions
if (specifiedDimentions)
//create a new pixel
newPixel(getValue('size-width'),getValue('size-height'), getValue('editor-mode'));
else
//otherwise show the new pixel dialog
showDialogue('new-pixel', false);
};
featureToggles.onLoad();
currentTool.updateCursor();
//if the user specified dimensions
if (specifiedDimentions) {
//create a new pixel
newPixel(getValue('size-width'), getValue('size-height'), getValue('editor-mode'));
} else {
//otherwise show the new pixel dialog
showDialogue('splash', false);
}
};

333
js/_paletteBlock.js Normal file
View File

@ -0,0 +1,333 @@
/** INIT is called when it shouldn't **/
let coloursList = document.getElementById("palette-list");
let rampMenu = document.getElementById("pb-ramp-options");
let pbRampDialogue = document.getElementById("pb-ramp-dialogue");
let currentSquareSize = coloursList.children[0].clientWidth;
let blockData = {blockWidth: 300, blockHeight: 320, squareSize: 40};
let isRampSelecting = false;
let ramps = [];
let currentSelection = {startIndex:0, endIndex:0, startCoords:[], endCoords: [], name: "", colour: "", label: null};
// Making the palette list sortable
new Sortable(document.getElementById("palette-list"), {
animation: 100,
onEnd: updateRampSelection
});
// Listening for the palette block resize
new ResizeObserver(updateSizeData).observe(coloursList.parentElement);
// Initializes the palette block
function pbInit() {
let simplePalette = document.getElementById("colors-menu");
let childCount = coloursList.childElementCount;
currentSquareSize = coloursList.children[0].clientWidth;
coloursList = document.getElementById("palette-list");
// Remove all the colours
for (let i=0; i<childCount; i++) {
coloursList.children[0].remove();
}
// Add all the colours from the simplepalette
for (let i=0; i<simplePalette.childElementCount-1; i++) {
addSingleColour(cssToHex(simplePalette.children[i].children[0].style.backgroundColor));
}
}
/** Listens for the mouse wheel, used to change the size of the squares in the palette list
*
*/
coloursList.parentElement.addEventListener("wheel", function (mouseEvent) {
// Only resize when pressing alt, used to distinguish between scrolling through the palette and
// resizing it
if (mouseEvent.altKey) {
resizeSquares(mouseEvent);
}
});
/** Tells whether a colour is in the palette or not
*
* @param {*} colour The colour to add
*/
function hasColour(colour) {
for (let i=0; i<coloursList.childElementCount; i++) {
let currentCol = coloursList.children[i].style.backgroundColor;
let currentHex = cssToHex(currentCol);
if (currentHex == colour) {
return true;
}
}
return false;
}
/** Adds a single colour to the palette
*
* @param {*} colour The colour to add
*/
function addSingleColour(colour) {
if (!hasColour(colour)) {
let li = document.createElement("li");
li.style.width = currentSquareSize + "px";
li.style.height = currentSquareSize + "px";
li.style.backgroundColor = colour;
li.addEventListener("mousedown", startRampSelection);
li.addEventListener("mouseup", endRampSelection);
li.addEventListener("mousemove", updateRampSelection);
li.addEventListener("onclick", endRampSelection);
coloursList.appendChild(li);
}
}
/** Adds all the colours currently selected in the colour picker
*
*/
function pbAddColours() {
let colours = getSelectedColours();
for (let i=0; i<colours.length; i++) {
addSingleColour(colours[i]);
}
}
/** Removes all the currently selected colours from the palette
*
*/
function pbRemoveColours() {
let startIndex = currentSelection.startIndex;
let endIndex = currentSelection.endIndex;
if (startIndex > endIndex) {
let tmp = startIndex;
startIndex = endIndex;
endIndex = tmp;
}
for (let i=startIndex; i<=endIndex; i++) {
coloursList.removeChild(coloursList.children[startIndex]);
}
clearBorders();
}
/** Starts selecting a ramp. Saves the data needed to draw the outline.
*
* @param {*} mouseEvent
*/
function startRampSelection(mouseEvent) {
if (mouseEvent.which == 3) {
let index = getElementIndex(mouseEvent.target);
isRampSelecting = true;
currentSelection.startIndex = index;
currentSelection.endIndex = index;
currentSelection.startCoords = getColourCoordinates(index);
currentSelection.endCoords = getColourCoordinates(index);
}
}
/** Updates the outline for the current selection.
*
* @param {*} mouseEvent
*/
function updateRampSelection(mouseEvent) {
if (mouseEvent != null && mouseEvent.which == 3) {
currentSelection.endIndex = getElementIndex(mouseEvent.target);
}
if (mouseEvent == null || mouseEvent.which == 3) {
let startCoords = getColourCoordinates(currentSelection.startIndex);
let endCoords = getColourCoordinates(currentSelection.endIndex);
let startIndex = currentSelection.startIndex;
let endIndex = currentSelection.endIndex;
if (currentSelection.startIndex > endIndex) {
let tmp = startIndex;
startIndex = endIndex;
endIndex = tmp;
tmp = startCoords;
startCoords = endCoords;
endCoords = tmp;
}
clearBorders();
for (let i=startIndex; i<=endIndex; i++) {
let currentSquare = coloursList.children[i];
let currentCoords = getColourCoordinates(i);
let borderStyle = "3px solid white";
let bordersToSet = [];
// Deciding which borders to use to make the outline
if (i == 0 || i == startIndex) {
bordersToSet.push("border-left");
}
if (currentCoords[1] == startCoords[1] || ((currentCoords[1] == startCoords[1] + 1)) && currentCoords[0] < startCoords[0]) {
bordersToSet.push("border-top");
}
if (currentCoords[1] == endCoords[1] || ((currentCoords[1] == endCoords[1] - 1)) && currentCoords[0] > endCoords[0]) {
bordersToSet.push("border-bottom");
}
if ((i == coloursList.childElementCount - 1) || (currentCoords[0] == Math.floor(blockData.blockWidth / blockData.squareSize) - 1)
|| i == endIndex) {
bordersToSet.push("border-right");
}
if (bordersToSet != []) {
currentSquare.style["box-sizing"] = "border-box";
for (let i=0; i<bordersToSet.length; i++) {
currentSquare.style[bordersToSet[i]] = borderStyle;
}
}
}
}
}
/** Removes all the borders from all the squares. The borders are cleared only for the
* current selection, so every border that is not white is kept.
*
*/
function clearBorders() {
for (let i=0; i<coloursList.childElementCount; i++) {
coloursList.children[i].style["border-top"] = "none";
coloursList.children[i].style["border-left"] = "none";
coloursList.children[i].style["border-right"] = "none";
coloursList.children[i].style["border-bottom"] = "none";
}
}
/** Ends the current selection, opens the ramp menu
*
* @param {*} mouseEvent
*/
function endRampSelection(mouseEvent) {
let col;
if (currentSelection.startCoords.length == 0) {
currentSelection.endIndex = getElementIndex(mouseEvent.target);
currentSelection.startIndex = currentSelection.endIndex;
currentSelection.startCoords = getColourCoordinates(currentSelection.startIndex);
}
// I'm not selecting a ramp anymore
isRampSelecting = false;
// Setting the end coordinates
currentSelection.endCoords = getColourCoordinates(getElementIndex(mouseEvent.target));
// Setting the colour in the colour picker
col = cssToHex(coloursList.children[currentSelection.startIndex].style.backgroundColor);
updatePickerByHex(col);
updateSlidersByHex(col);
updateMiniPickerColour();
updateRampSelection();
currentSelection.startCoords = [];
}
function closeAllSubmenus() {
let menus = document.getElementsByClassName("pb-submenu");
for (let i=0; i<menus.length; i++) {
menus[i].style.display = "none";
}
}
/** Updates the current data about the size of the palette list (height, width and square size).
* It also updates the outline after doing so.
*
*/
function updateSizeData() {
blockData.blockHeight = coloursList.parentElement.clientHeight;
blockData.blockWidth = coloursList.parentElement.clientWidth;
blockData.squareSize = coloursList.children[0].clientWidth;
updateRampSelection();
}
/** Gets the colour coordinates relative to the colour list seen as a matrix. Coordinates
* start from the top left angle.
*
* @param {*} index The index of the colour in the list seen as a linear array
*/
function getColourCoordinates(index) {
let yIndex = Math.floor(index / Math.floor(blockData.blockWidth / blockData.squareSize));
let xIndex = Math.floor(index % Math.floor(blockData.blockWidth / blockData.squareSize));
return [xIndex, yIndex];
}
/** Returns the index of the element in the colour list
*
* @param {*} element The element of which we need to get the index
*/
function getElementIndex(element) {
for (let i=0; i<coloursList.childElementCount; i++) {
if (element == coloursList.children[i]) {
return i;
}
}
}
/** Resizes the squares depending on the scroll amount (only resizes if the user is
* also holding alt)
*
* @param {*} mouseEvent
*/
function resizeSquares(mouseEvent) {
let amount = mouseEvent.deltaY > 0 ? -5 : 5;
currentSquareSize += amount;
for (let i=0; i<coloursList.childElementCount; i++) {
let currLi = coloursList.children[i];
currLi.style["box-sizing"] = "content-box";
currLi.style.width = currLi.clientWidth + amount + "px";
currLi.style.height = currLi.clientHeight + amount + "px";
}
updateSizeData();
}
/** Converts a CSS colour eg rgb(x,y,z) to a hex string
*
* @param {*} rgb
*/
function cssToHex(rgb) {
rgb = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
function hex(x) {
return ("0" + parseInt(x).toString(16)).slice(-2);
}
return "#" + hex(rgb[1]) + hex(rgb[2]) + hex(rgb[3]);
}
function pbAddToSimplePalette() {
let simplePalette = document.getElementById("colors-menu");
let childCount = simplePalette.childElementCount;
// Removing all the colours
for (let i=0; i<childCount-1; i++) {
simplePalette.removeChild(simplePalette.children[0]);
}
// Adding the new ones
for (let i=0; i<coloursList.childElementCount; i++) {
let col = coloursList.children[i].style.backgroundColor;
if (col.includes("rgb")) {
addColor(cssToHex(col));
}
else {
addColor(col);
}
}
}

View File

@ -1,62 +1,91 @@
//populate palettes list in new pixel menu
Object.keys(palettes).forEach(function(paletteName,index) {
(() => {
const palettesMenu = document.getElementById('palette-menu');
const splashPalettes = document.getElementById('palette-menu-splash');
const noPaletteButton = document.getElementById('no-palette-button');
const newPixelElement = document.getElementById('new-pixel');
const paletteButton = document.getElementById('palette-button');
const paletteButtonSplash = document.getElementById('palette-button-splash');
const loadPaletteButton = document.getElementById('load-palette-button');
const loadPaletteButtonSplash = document.getElementById('load-palette-button-splash');
var palettesMenu = document.getElementById('palette-menu');
Object.keys(palettes).forEach((paletteName,) => {
//create button
var button = document.createElement('button');
button.appendChild(document.createTextNode(paletteName));
const button = document.createElement('button');
button.appendChild(document.createTextNode(paletteName));
//insert new element
palettesMenu.appendChild(button);
//if the palette was specified by the user, change the dropdown to it
if (palettes[paletteName].specified) {
Util.setText('palette-button', paletteName);
Util.setText('palette-button-splash', paletteName)
//Show empty palette option
noPaletteButton.style.display = 'block';
}
//if the palette was specified by the user, change the dropdown to it
if (palettes[paletteName].specified == true) {
setText('palette-button', paletteName);
//Show empty palette option
document.getElementById('no-palette-button').style.display = 'block';
}
const buttonEvent = () => {
//hide the dropdown menu
Util.deselect('palette-menu');
Util.deselect('palette-button');
Util.deselect('palette-menu-splash');
Util.deselect('palette-button-splash');
on('click', button, function() {
//show empty palette option
noPaletteButton.style.display = 'block';
//hide the dropdown menu
deselect('palette-menu');
deselect('palette-button');
//set the text of the dropdown to the newly selected preset
Util.setText('palette-button', paletteName);
Util.setText('palette-button-splash', paletteName);
}
//show empty palette option
document.getElementById('no-palette-button').style.display = 'block';
// Making a copy for the splash page too
const copyButton = button.cloneNode(true);
copyButton.addEventListener('click', buttonEvent);
button.addEventListener('click', buttonEvent);
//set the text of the dropdown to the newly selected preset
setText('palette-button', paletteName);
// Appending it to the splash palette menu
splashPalettes.appendChild(copyButton);
palettesMenu.appendChild(button);
});
});
//select no palette
on('click', 'no-palette-button', function () {
document.getElementById('no-palette-button').style.display = 'none';
setText('palette-button', 'Choose a palette...');
});
//select load palette
on('click', 'load-palette-button', function () {
document.getElementById('load-palette-browse-holder').click();
});
const loadPaletteButtonEvent = () => {
document.getElementById('load-palette-browse-holder').click();
}
const clickPaletteButtonEvent = (e) => {
Util.toggle('palette-button');
Util.toggle('palette-menu');
on('click', 'palette-button', function (e){
toggle('palette-button');
toggle('palette-menu');
Util.deselect('preset-button');
Util.deselect('preset-menu');
deselect('preset-button');
deselect('preset-menu');
e.stopPropagation();
});
// Splash version
Util.toggle('palette-button-splash');
Util.toggle('palette-menu-splash');
on('click', 'new-pixel', function (){
deselect('editor-mode-menu');
deselect('preset-button');
deselect('preset-menu');
deselect('palette-button');
deselect('palette-menu');
});
e.stopPropagation();
}
// Load Palettes
loadPaletteButton.addEventListener('click', loadPaletteButtonEvent);
loadPaletteButtonSplash.addEventListener('click', loadPaletteButtonEvent);
// Palette menu click
paletteButtonSplash.addEventListener('click', clickPaletteButtonEvent);
paletteButton.addEventListener('click', clickPaletteButtonEvent);
noPaletteButton.addEventListener('click', () => {
noPaletteButton.style.display = 'none';
Util.setText('palette-button', 'Choose a palette...');
})
newPixelElement.addEventListener('click', () => {
Util.deselect('editor-mode-menu');
Util.deselect('preset-button');
Util.deselect('preset-menu');
Util.deselect('palette-button');
Util.deselect('palette-menu');
// Splash version
Util.deselect('palette-button-splash');
Util.deselect('palette-menu-splash');
})
})();

View File

@ -1,3 +1,10 @@
/***********MISCELLANEOUS UTILITY FUNCTIONS**************/
/** Merges topLayer onto belowLayer
*
* @param {*} belowLayer The layer on the bottom of the layer stack
* @param {*} topLayer The layer on the top of the layer stack
*/
function mergeLayers(belowLayer, topLayer) {
// Copying the above content on the layerBelow
let belowImageData = belowLayer.getImageData(0, 0, canvas.width, canvas.height);
@ -24,10 +31,19 @@ function mergeLayers(belowLayer, topLayer) {
}
}
// Putting the top data into the belowdata
belowLayer.putImageData(toMergeImageData, 0, 0);
}
/** Used to programmatically create an input event
*
* @param {*} keyCode KeyCode of the key to press
* @param {*} ctrl Is ctrl pressed?
* @param {*} alt Is alt pressed?
* @param {*} shift Is shift pressed?
*/
function simulateInput(keyCode, ctrl, alt, shift) {
// I just copy pasted this from stack overflow lol please have mercy
let keyboardEvent = document.createEvent("KeyboardEvent");
let initMethod = typeof keyboardEvent.initKeyboardEvent !== 'undefined' ? "initKeyboardEvent" : "initKeyEvent";
@ -46,38 +62,62 @@ function simulateInput(keyCode, ctrl, alt, shift) {
document.dispatchEvent(keyboardEvent);
}
/** Tells if a pixel is empty (has alpha = 0)
*
* @param {*} pixel
*/
function isPixelEmpty(pixel) {
if (pixel == null || pixel === undefined) {
return false;
}
if ((pixel[0] == 0 && pixel[1] == 0 && pixel[2] == 0) || pixel[3] == 0) {
// If the alpha channel is 0, the current pixel is empty
if (pixel[3] == 0) {
return true;
}
return false;
}
/** Tells if element is a child of an element with class className
*
* @param {*} element
* @param {*} className
*/
function isChildOfByClass(element, className) {
// Getting the element with class className
while (element != null && element.classList != null && !element.classList.contains(className)) {
element = element.parentElement;
}
// If that element exists and its class is the correct one
if (element != null && element.classList != null && element.classList.contains(className)) {
// Then element is a chld of an element with class className
return true;
}
return false;
}
/** Gets the eyedropped colour (the colour of the pixel pointed by the cursor when the user is using the eyedropper).
* It takes the colour of the canvas with the biggest z-index, basically the one the user can see, since it doesn't
* make much sense to sample a colour which is hidden behind a different layer
*
* @param {*} cursorLocation The position of the cursor
*/
function getEyedropperColor(cursorLocation) {
// Making sure max will take some kind of value
let max = -1;
// Using tmpColour to sample the sprite
let tmpColour;
// Returned colour
let selectedColor;
for (let i=1; i<layers.length; i++) {
// Getting the colour of the pixel in the cursorLocation
tmpColour = layers[i].context.getImageData(Math.floor(cursorLocation[0]/zoom),Math.floor(cursorLocation[1]/zoom),1,1).data;
// If it's not empty, I check if it's on the top of the previous colour
if (layers[i].canvas.style.zIndex > max || isPixelEmpty(selectedColor) || selectedColor === undefined) {
max = layers[i].canvas.style.zIndex;
@ -87,6 +127,7 @@ function getEyedropperColor(cursorLocation) {
}
}
// If the final colour was empty, I return black
if (isPixelEmpty(tmpColour) && selectedColor === undefined) {
selectedColor = [0, 0, 0];
}
@ -94,7 +135,12 @@ function getEyedropperColor(cursorLocation) {
return selectedColor;
}
/** Gets the absolute position of the element (position on the screen)
*
* @param {*} element The element of which we have to get the position
*/
function getElementAbsolutePosition(element) {
// Probably copy pasted this from stack overflow too, if not I don't recall how it works
let curleft = curtop = 0;
if (element.offsetParent) {
@ -107,9 +153,15 @@ function getElementAbsolutePosition(element) {
return [curleft,curtop];
}
/** Nearest neighbor algorithm to scale a sprite
*
* @param {*} src The source imageData
* @param {*} dst The destination imageData
*/
function nearestNeighbor (src, dst) {
let pos = 0
// Just applying the nearest neighbor algorithm
for (let y = 0; y < dst.height; y++) {
for (let x = 0; x < dst.width; x++) {
const srcX = Math.floor(x * src.width / dst.width)
@ -125,7 +177,14 @@ function nearestNeighbor (src, dst) {
}
}
/** Bilinear interpolation used to scale a sprite
*
* @param {*} src The source imageData
* @param {*} dst The destination imageData
*/
function bilinearInterpolation (src, dst) {
// Applying the bilinear interpolation algorithm
function interpolate (k, kMin, kMax, vMin, vMax) {
return Math.round((k - kMin) * vMax + (kMax - k) * vMin)
}
@ -167,14 +226,21 @@ function bilinearInterpolation (src, dst) {
}
}
/** Resizes an imageData depending on the algorithm and on the new width and height
*
* @param {*} image The imageData to scale
* @param {*} width The new width of the imageData
* @param {*} height The new height of the imageData
* @param {*} algorithm Scaling algorithm chosen by the user in the dialogue
*/
function resizeImageData (image, width, height, algorithm) {
algorithm = algorithm || 'bilinear-interpolation'
let resize
let resize;
switch (algorithm) {
case 'nearest-neighbor': resize = nearestNeighbor; break
case 'bilinear-interpolation': resize = bilinearInterpolation; break
default: throw new Error(`Unknown algorithm: ${algorithm}`)
default: return image;
}
const result = new ImageData(width, height)
@ -184,10 +250,21 @@ function resizeImageData (image, width, height, algorithm) {
return result
}
/** Gets the position in (x, y) format of the pixel with index "index"
*
* @param {*} index The index of the pixel of which we need the (x, y) position
*/
function getPixelPosition(index) {
let linearIndex = index / 4;
let x = linearIndex % layers[0].canvasSize[0];
let y = Math.floor(linearIndex / layers[0].canvasSize[0]);
return [Math.ceil(x), Math.ceil(y)];
}
/** Sets isDragging to false, used when the user interacts with sortable lists
*
*/
function makeIsDraggingFalse(event) {
dragging = false;
}

View File

@ -1,27 +1,44 @@
// Start colour of the pixel grid (can be changed in the preferences)
let pixelGridColor = "#0000FF";
// Distance between one line and another in HTML pixels
let lineDistance = 12;
// The grid is not visible at the beginning
let pixelGridVisible = false;
// Saving the canvas containing the pixel grid
pixelGridCanvas = document.getElementById("pixel-grid");
/** Shows or hides the pixel grid depening on its current visibility
* (triggered by the show pixel grid button in the top menu)
*
*/
function togglePixelGrid(event) {
// Getting the button because I have to change its text
let button = document.getElementById("toggle-pixelgrid-button");
// Toggling the state
pixelGridVisible = !pixelGridVisible;
// If it was visible, I hide it
if (pixelGridVisible) {
button.innerHTML = "Hide pixel grid";
pixelGridCanvas.style.display = "inline-block";
}
// Otherwise, I show it
else {
button.innerHTML = "Show pixel grid";
pixelGridCanvas.style.display = "none";
}
}
/** Fills the pixelGridCanvas with the pixelgrid
*
*/
function fillPixelGrid() {
let context = pixelGridCanvas.getContext("2d");
let originalSize = layers[0].canvasSize;
// The pixelGridCanvas is lineDistance times bigger so that the lines don't take 1 canvas pixel
// (which would cover the whole canvas with the pixelGridColour), but they take 1/lineDistance canvas pixels
pixelGridCanvas.width = originalSize[0] * lineDistance;
pixelGridCanvas.height = originalSize[1] * lineDistance;

View File

@ -1,63 +1,60 @@
//prests
var presets = {
'Gameboy Color': {
width: 240,
height: 203,
palette: 'Gameboy Color'
},
'PICO-8': {
width: 128,
height: 128,
palette: 'PICO-8'
},
'Commodore 64': {
width: 40,
height: 80,
palette: 'Commodore 64'
const PresetModule = (() => {
const presets = {
'Gameboy Color': {width: 240, height: 203, palette: 'Gameboy Color'},
'PICO-8': {width: 128, height: 128, palette: 'PICO-8'},
'Commodore 64': {width: 40, height: 80, palette: 'Commodore 64'}
};
function instrumentPresetMenu() {
console.info("Initializing presets..");
// Add a button for all the presets available
const presetsMenu = document.getElementById('preset-menu');
Object.keys(presets).forEach((presetName,) => {
const button = document.createElement('button');
button.appendChild(document.createTextNode(presetName));
presetsMenu.appendChild(button);
button.addEventListener('click', () => {
//change dimentions on new pixel form
Util.setValue('size-width', presets[presetName].width);
Util.setValue('size-height', presets[presetName].height);
//set the text of the dropdown to the newly selected preset
Util.setText('palette-button', presets[presetName].palette);
//hide the dropdown menu
Util.deselect('preset-menu');
Util.deselect('preset-button');
//set the text of the dropdown to the newly selected preset
Util.setText('preset-button', presetName);
});
});
const presetButton = document.getElementById('preset-button');
presetButton.addEventListener('click', (e) => {
//open or close the preset menu
Util.toggle('preset-button');
Util.toggle('preset-menu');
//close the palette menu
Util.deselect('palette-button');
Util.deselect('palette-menu');
e.stopPropagation();
});
}
};
//populate preset list in new pixel menu
Object.keys(presets).forEach(function(presetName,index) {
function propertiesOf(presetId) {
return presets[presetId];
}
var presetsMenu = document.getElementById('preset-menu');
//create button
var button = document.createElement('button');
button.appendChild(document.createTextNode(presetName));
//insert new element
presetsMenu.appendChild(button);
//add click event listener
on('click', button, function() {
//change dimentions on new pixel form
setValue('size-width', presets[presetName].width);
setValue('size-height', presets[presetName].height);
//set the text of the dropdown to the newly selected preset
setText('palette-button', presets[presetName].palette);
//hide the dropdown menu
deselect('preset-menu');
deselect('preset-button');
//set the text of the dropdown to the newly selected preset
setText('preset-button', presetName);
});
});
on('click', 'preset-button', function (e){
//open or close the preset menu
toggle('preset-button');
toggle('preset-menu');
//close the palette menu
deselect('palette-button');
deselect('palette-menu');
//stop the click from propogating to the parent element
e.stopPropagation();
});
return {
propertiesOf,
instrumentPresetMenu
};
})();

View File

@ -4,6 +4,10 @@ let startY;
let endX;
let endY;
/** Starts the selection: saves the canvas, sets the start coordinates
*
* @param {*} mouseEvent
*/
function startRectSelection(mouseEvent) {
// Saving the canvas
new HistoryStateEditCanvas();
@ -35,6 +39,10 @@ function startRectSelection(mouseEvent) {
selectionCanceled = false;
}
/** Updates the selection
*
* @param {*} mouseEvent
*/
function updateRectSelection(mouseEvent) {
let pos = getCursorPosition(mouseEvent);
@ -42,6 +50,10 @@ function updateRectSelection(mouseEvent) {
drawRect(Math.round(pos[0] / zoom) + 0.5, Math.round(pos[1] / zoom) + 0.5);
}
/** Ends the selection: sets the end coordiantes
*
* @param {*} mouseEvent
*/
function endRectSelection(mouseEvent) {
// Getting the end position
let currentPos = getCursorPosition(mouseEvent);
@ -72,6 +84,10 @@ function endRectSelection(mouseEvent) {
currentTool.updateCursor();
}
/** Cuts the selection from its canvas and puts it in the tmp layer so it can be moved
*
* @param {*} mousePosition
*/
function cutSelection(mousePosition) {
// Getting the selected pixels
imageDataToMove = currentLayer.context.getImageData(startX, startY, endX - startX + 1, endY - startY + 1);
@ -79,10 +95,13 @@ function cutSelection(mousePosition) {
currentLayer.context.clearRect(startX - 0.5, startY - 0.5, endX - startX + 1, endY - startY + 1);
// Moving those pixels from the current layer to the tmp layer
TMPLayer.context.putImageData(imageDataToMove, startX + 1, startY);
//originalDataPosition = [currentPos[0], currentPos[1]];
}
/** Draws a dashed rectangle representing the selection
*
* @param {*} x Current end x coordinate of the selection
* @param {*} y Current end y coordinate of the selection
*/
function drawRect(x, y) {
// Getting the vfx context
let vfxContext = VFXCanvas.getContext('2d');
@ -106,10 +125,13 @@ function applyChanges() {
// Checks whether the pointer is inside the selected area or not
function cursorInSelectedArea() {
// Getting the cursor position
let cursorPos = getCursorPosition(currentMouseEvent);
// Getting the coordinates relatively to the canvas
let x = cursorPos[0] / zoom;
let y = cursorPos[1] / zoom;
// This is to avoid rightX or topY being less than leftX or bottomY
let leftX = Math.min(startX, endX);
let rightX = Math.max(startX, endX);
let topY = Math.max(startY, endY);
@ -126,6 +148,13 @@ function cursorInSelectedArea() {
return false;
}
/** 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 selectin
*/
function moveSelection(x, y, width, height) {
// Getting the vfx context
let vfxContext = VFXCanvas.getContext('2d');
@ -135,6 +164,7 @@ function moveSelection(x, y, width, height) {
vfxContext.lineWidth = 1;
vfxContext.setLineDash([4]);
// Fixing the coordinates
startX = Math.round(Math.round(x) - 0.5 - Math.round(width / 2)) + 0.5;
startY = Math.round(Math.round(y) - 0.5 - Math.round(height / 2)) + 0.5;
endX = startX + Math.round(width);

View File

@ -1,19 +1,26 @@
// Saving the empty rect svg
var emptyRectangleSVG = document.getElementById("rectangle-empty-button-svg");
// and the full rect svg so that I can change them when the user changes rect modes
var fullRectangleSVG = document.getElementById("rectangle-full-button-svg");
var emptySVG = document.getElementById("empty-button-svg");
var fullSVG = document.getElementById("full-button-svg");
var drawMode = 'empty';
// The start mode is empty rectangle
var rectangleDrawMode = 'empty';
// I'm not drawing a rectangle at the beginning
var isDrawingRect = false;
// Rect coordinates
let startRectX;
let startRectY;
let endRectX;
let endRectY;
/** Starts drawing the rect, saves the start coordinates
*
* @param {*} mouseEvent
*/
function startRectDrawing(mouseEvent) {
// Putting the vfx layer on top of everything
VFXCanvas.style.zIndex = MAX_Z_INDEX;
// Putting the vfx layer on top of everything
VFXCanvas.style.zIndex = parseInt(currentLayer.canvas.style.zIndex, 10) + 1;;
// Updating flag
isDrawingRect = true;
@ -25,20 +32,29 @@ function startRectDrawing(mouseEvent) {
drawRectangle(startRectX, startRectY);
}
/** Updates the rectangle preview depending on the position of the mouse
*
* @param {*} mouseEvent The mouseEvent from which we'll get the mouse position
*/
function updateRectDrawing(mouseEvent) {
let pos = getCursorPosition(mouseEvent);
// Drawing the rect
drawRectangle(Math.round(pos[0] / zoom) + 0.5, Math.round(pos[1] / zoom) + 0.5);
// Drawing the rect at the right position
drawRectangle(Math.floor(pos[0] / zoom) + 0.5, Math.floor(pos[1] / zoom) + 0.5);
}
/** Finishes drawing the rect, decides the end coordinates and moves the preview rectangle to the
* current layer
*
* @param {*} mouseEvent event from which we'll get the mouse position
*/
function endRectDrawing(mouseEvent) {
// Getting the end position
let currentPos = getCursorPosition(mouseEvent);
let vfxContext = VFXCanvas.getContext("2d");
endRectX = Math.round(currentPos[0] / zoom) + 0.5;
endRectY = Math.round(currentPos[1] / zoom) + 0.5;
endRectX = Math.floor(currentPos[0] / zoom) + 0.5;
endRectY = Math.floor(currentPos[1] / zoom) + 0.5;
// Inverting end and start (start must always be the top left corner)
if (endRectX < startRectX) {
@ -61,15 +77,18 @@ function endRectDrawing(mouseEvent) {
endRectX -= 0.5;
startRectX -= 0.5;
// Setting the correct linewidth and colour
currentLayer.context.lineWidth = tool.rectangle.brushSize;
currentLayer.context.fillStyle = currentGlobalColor;
// Drawing the rect using 4 lines
line(startRectX, startRectY, endRectX, startRectY, tool.rectangle.brushSize);
line(endRectX, startRectY, endRectX, endRectY, tool.rectangle.brushSize);
line(endRectX, endRectY, startRectX, endRectY, tool.rectangle.brushSize);
line(startRectX, endRectY, startRectX, startRectY, tool.rectangle.brushSize);
if (drawMode == 'fill') {
// If I have to fill it, I do so
if (rectangleDrawMode == 'fill') {
currentLayer.context.fillRect(startRectX, startRectY, endRectX - startRectX, endRectY - startRectY);
}
@ -77,6 +96,12 @@ function endRectDrawing(mouseEvent) {
vfxContext.clearRect(0, 0, VFXCanvas.width, VFXCanvas.height);
}
/** Draws a rectangle with end coordinates given by x and y on the VFX layer (draws
* the preview for the rectangle tool)
*
* @param {*} x The current end x of the rectangle
* @param {*} y The current end y of the rectangle
*/
function drawRectangle(x, y) {
// Getting the vfx context
let vfxContext = VFXCanvas.getContext("2d");
@ -98,21 +123,23 @@ function drawRectangle(x, y) {
}
vfxContext.setLineDash([]);
vfxContext.stroke();
}
/** Sets the correct tool icon depending on its mode
*
*/
function setRectToolSvg() {
if (drawMode == 'empty') {
emptySVG.setAttribute('display', 'visible');
fullSVG.setAttribute('display', 'none');
if (rectangleDrawMode == 'empty') {
emptyRectangleSVG.setAttribute('display', 'visible');
fullRectangleSVG.setAttribute('display', 'none');
}
else {
emptySVG.setAttribute('display', 'none');
fullSVG.setAttribute('display', 'visible');
emptyRectangleSVG.setAttribute('display', 'none');
fullRectangleSVG.setAttribute('display', 'visible');
}
}
function applyChanges() {
VFXCanvas.style.zIndex = MIN_Z_INDEX;
//VFXCanvas.style.zIndex = MIN_Z_INDEX;
}

View File

@ -1,16 +1,31 @@
let resizeCanvasContainer = document.getElementById("resize-canvas");
let rcPivot = "middle";
let currentPivotObject;
let borders = {left: 0, right: 0, top: 0, bottom: 0};
/* This scripts contains all the code used to handle the canvas resizing */
// Resize canvas pop up window
let resizeCanvasContainer = document.getElementById("resize-canvas");
// Start pivot
let rcPivot = "middle";
// Selected pivot button
let currentPivotObject;
// Border offsets
let rcBorders = {left: 0, right: 0, top: 0, bottom: 0};
/** Opens the canvas resize window
*
*/
function openResizeCanvasWindow() {
// Initializes the inputs
initResizeCanvasInputs();
showDialogue('resize-canvas');
}
/** Initializes the canvas resizing input
*
*/
function initResizeCanvasInputs() {
// Getting the pivot buttons
let buttons = document.getElementsByClassName("pivot-button");
// Adding the event handlers for them
for (let i=0; i<buttons.length; i++) {
buttons[i].addEventListener("click", changePivot);
if (buttons[i].getAttribute("value").includes("middle")) {
@ -33,13 +48,21 @@ function initResizeCanvasInputs() {
console.log("Pivot selezionato: " + currentPivotObject);
}
/** Fired when a border offset is changed: it updates the width and height
*
* @param {*} event
*/
function rcChangedBorder(event) {
rcUpdateBorders();
document.getElementById("rc-width").value = parseInt(layers[0].canvasSize[0]) + borders.left + borders.right;
document.getElementById("rc-height").value = parseInt(layers[0].canvasSize[1]) + borders.top + borders.bottom;
document.getElementById("rc-width").value = parseInt(layers[0].canvasSize[0]) + rcBorders.left + rcBorders.right;
document.getElementById("rc-height").value = parseInt(layers[0].canvasSize[1]) + rcBorders.top + rcBorders.bottom;
}
/** Fired when width or height are changed: updates the border offsets
*
* @param {*} event
*/
function rcChangedSize(event) {
let widthOffset = Math.abs(document.getElementById("rc-width").value) - layers[0].canvasSize[0];
let heightOffset = Math.abs(document.getElementById("rc-height").value) - layers[0].canvasSize[1];
@ -54,12 +77,19 @@ function rcChangedSize(event) {
document.getElementById("rc-border-top").value = top;
document.getElementById("rc-border-bottom").value = bottom;
borders.left = left;
borders.right = right;
borders.top = top;
borders.bottom = bottom;
rcBorders.left = left;
rcBorders.right = right;
rcBorders.top = top;
rcBorders.bottom = bottom;
}
/** Resizes the canvas
*
* @param {*} event The event that triggered the canvas resizing
* @param {*} size The new size of the picture
* @param {*} customData Used when ctrl+z ing
* @param {*} saveHistory Should I save the history? You shouldn't if you're undoing
*/
function resizeCanvas(event, size, customData, saveHistory = true) {
let imageDatas = [];
let leftOffset = 0;
@ -84,11 +114,11 @@ function resizeCanvas(event, size, customData, saveHistory = true) {
}
// Saving the history only if I'm not already undoing or redoing
if (saveHistory) {
if (saveHistory && event != null) {
// Saving history
new HistoryStateResizeCanvas(
{x: parseInt(layers[0].canvasSize[0]) + borders.left + borders.right,
y: parseInt(layers[0].canvasSize[1]) + borders.top + borders.bottom},
{x: parseInt(layers[0].canvasSize[0]) + rcBorders.left + rcBorders.right,
y: parseInt(layers[0].canvasSize[1]) + rcBorders.top + rcBorders.bottom},
{x: layers[0].canvasSize[0],
y: layers[0].canvasSize[1]},
@ -100,8 +130,8 @@ function resizeCanvas(event, size, customData, saveHistory = true) {
// Resize the canvases
for (let i=0; i<layers.length; i++) {
layers[i].canvasSize[0] = parseInt(layers[i].canvasSize[0]) + borders.left + borders.right;
layers[i].canvasSize[1] = parseInt(layers[i].canvasSize[1]) + borders.top + borders.bottom;
layers[i].canvasSize[0] = parseInt(layers[i].canvasSize[0]) + rcBorders.left + rcBorders.right;
layers[i].canvasSize[1] = parseInt(layers[i].canvasSize[1]) + rcBorders.top + rcBorders.bottom;
layers[i].canvas.width = layers[i].canvasSize[0];
layers[i].canvas.height = layers[i].canvasSize[1];
@ -121,51 +151,49 @@ function resizeCanvas(event, size, customData, saveHistory = true) {
topOffset = 0;
break;
case 'top':
leftOffset = (borders.left + borders.right) / 2;
leftOffset = (rcBorders.left + rcBorders.right) / 2;
topOffset = 0;
break;
case 'topright':
leftOffset = borders.left + borders.right;
leftOffset = rcBorders.left + rcBorders.right;
topOffset = 0;
break;
case 'left':
leftOffset = 0;
topOffset = (borders.top + borders.bottom) / 2;
topOffset = (rcBorders.top + rcBorders.bottom) / 2;
break;
case 'middle':
leftOffset = (borders.left + borders.right) / 2;
topOffset = (borders.top + borders.bottom) / 2;
leftOffset = (rcBorders.left + rcBorders.right) / 2;
topOffset = (rcBorders.top + rcBorders.bottom) / 2;
break;
case 'right':
leftOffset = borders.left + borders.right;
topOffset = (borders.top + borders.bottom) / 2;
leftOffset = rcBorders.left + rcBorders.right;
topOffset = (rcBorders.top + rcBorders.bottom) / 2;
break;
case 'bottomleft':
leftOffset = 0;
topOffset = borders.top + borders.bottom;
topOffset = rcBorders.top + rcBorders.bottom;
break;
case 'bottom':
leftOffset = (borders.left + borders.right) / 2;
topOffset = borders.top + borders.bottom;
leftOffset = (rcBorders.left + rcBorders.right) / 2;
topOffset = rcBorders.top + rcBorders.bottom;
break;
case 'bottomright':
leftOffset = borders.left + borders.right;
topOffset = borders.top + borders.bottom;
leftOffset = rcBorders.left + rcBorders.right;
topOffset = rcBorders.top + rcBorders.bottom;
break;
default:
console.log('Pivot does not exist, please report an issue at https://github.com/lospec/pixel-editor');
break;
}
// Putting all the data for each layer with the right offsets (decided by the pivot)
for (let i=0; i<layers.length; i++) {
if (layers[i].menuEntry != null) {
if (customData == undefined) {
layers[i].context.putImageData(imageDatas[copiedDataIndex], leftOffset, topOffset);
}
else {
console.log("sgancio " + layers[i].canvasSize + ", [" +
customData[copiedDataIndex].width + "," + customData[copiedDataIndex].height
+ "]");
layers[i].context.putImageData(customData[copiedDataIndex], 0, 0);
}
layers[i].updateLayerPreview();
@ -176,6 +204,11 @@ function resizeCanvas(event, size, customData, saveHistory = true) {
closeDialogue();
}
/** Trims the canvas so tat the sprite is perfectly contained in it
*
* @param {*} event
* @param {*} saveHistory Should I save the history? You shouldn't if you're undoing
*/
function trimCanvas(event, saveHistory) {
let minY = Infinity;
let minX = Infinity;
@ -189,6 +222,7 @@ function trimCanvas(event, saveHistory) {
rcPivot = "topleft";
console.log("debug");
// Computing the min and max coordinates in which there's a non empty pixel
for (let i=1; i<layers.length - nAppLayers; i++) {
let imageData = layers[i].context.getImageData(0, 0, layers[0].canvasSize[0], layers[0].canvasSize[1]);
let pixelPosition;
@ -226,10 +260,11 @@ function trimCanvas(event, saveHistory) {
minY = layers[0].canvasSize[1] - minY;
maxY = layers[0].canvasSize[1] - maxY;
borders.right = (maxX - layers[0].canvasSize[0]) + 1;
borders.left = -minX;
borders.top = maxY - layers[0].canvasSize[1] + 1;
borders.bottom = -minY;
// Setting the borders coherently with the values I've just computed
rcBorders.right = (maxX - layers[0].canvasSize[0]) + 1;
rcBorders.left = -minX;
rcBorders.top = maxY - layers[0].canvasSize[1] + 1;
rcBorders.bottom = -minY;
// Saving the data
for (let i=0; i<layers.length; i++) {
@ -241,11 +276,12 @@ function trimCanvas(event, saveHistory) {
console.log(imageDatas);
//console.log("sx: " + borders.left + "dx: " + borders.right + "top: " + borders.top + "btm: " + borders.bottom);
document.getElementById("rc-border-left").value = borders.left;
document.getElementById("rc-border-right").value = borders.right;
document.getElementById("rc-border-top").value = borders.top;
document.getElementById("rc-border-bottom").value = borders.bottom;
document.getElementById("rc-border-left").value = rcBorders.left;
document.getElementById("rc-border-right").value = rcBorders.right;
document.getElementById("rc-border-top").value = rcBorders.top;
document.getElementById("rc-border-bottom").value = rcBorders.bottom;
// Resizing the canvas with the decided border offsets
resizeCanvas(null, null, imageDatas.slice(), historySave);
// Resetting the previous pivot
rcPivot = prevPivot;
@ -253,16 +289,16 @@ function trimCanvas(event, saveHistory) {
function rcUpdateBorders() {
// Getting input
borders.left = document.getElementById("rc-border-left").value;
borders.right = document.getElementById("rc-border-right").value;
borders.top = document.getElementById("rc-border-top").value;
borders.bottom = document.getElementById("rc-border-bottom").value;
rcBorders.left = document.getElementById("rc-border-left").value;
rcBorders.right = document.getElementById("rc-border-right").value;
rcBorders.top = document.getElementById("rc-border-top").value;
rcBorders.bottom = document.getElementById("rc-border-bottom").value;
// Validating input
borders.left == "" ? borders.left = 0 : borders.left = Math.round(parseInt(borders.left));
borders.right == "" ? borders.right = 0 : borders.right = Math.round(parseInt(borders.right));
borders.top == "" ? borders.top = 0 : borders.top = Math.round(parseInt(borders.top));
borders.bottom == "" ? borders.bottom = 0 : borders.bottom = Math.round(parseInt(borders.bottom));
rcBorders.left == "" ? rcBorders.left = 0 : rcBorders.left = Math.round(parseInt(rcBorders.left));
rcBorders.right == "" ? rcBorders.right = 0 : rcBorders.right = Math.round(parseInt(rcBorders.right));
rcBorders.top == "" ? rcBorders.top = 0 : rcBorders.top = Math.round(parseInt(rcBorders.top));
rcBorders.bottom == "" ? rcBorders.bottom = 0 : rcBorders.bottom = Math.round(parseInt(rcBorders.bottom));
}
function changePivot(event) {

View File

@ -1,15 +1,28 @@
/* This scripts contains all the code used to handle the sprite scaling */
// Should I keep the sprite ratio?
let keepRatio = true;
// Used to store the current ratio
let currentRatio;
// The currenty selected resizing algorithm (nearest-neighbor or bilinear-interpolation)
let currentAlgo = 'nearest-neighbor';
// Current resize data
let data = {width: 0, height: 0, widthPercentage: 100, heightPercentage: 100};
// Start resize data
let startData = {width: 0, height:0, widthPercentage: 100, heightPercentage: 100};
/** Opens the sprite resizing window
*
*/
function openResizeSpriteWindow() {
// Inits the sprie resize inputs
initResizeSpriteInputs();
// Computing the current ratio
currentRatio = layers[0].canvasSize[0] / layers[0].canvasSize[1];
console.log("Current ratio: " + currentRatio);
// Initializing the input fields
data.width = layers[0].canvasSize[0];
data.height = layers[1].canvasSize[1];
@ -18,9 +31,13 @@ function openResizeSpriteWindow() {
startData.heightPercentage = 100;
startData.widthPercentage = 100;
// Opening the pop up now that it's ready
showDialogue('resize-sprite');
}
/** Initalizes the input values and binds the elements to their events
*
*/
function initResizeSpriteInputs() {
document.getElementById("rs-width").value = layers[0].canvasSize[0];
document.getElementById("rs-height").value = layers[0].canvasSize[1];
@ -40,11 +57,21 @@ function initResizeSpriteInputs() {
document.getElementById("resize-algorithm-combobox").addEventListener("change", changedAlgorithm);
}
/** Resizes (scales) the sprite
*
* @param {*} event
* @param {*} ratio Keeps infos about the x ratio and y ratio
*/
function resizeSprite(event, ratio) {
// Old data
let oldWidth, oldHeight;
// New data
let newWidth, newHeight;
// Current imageDatas
let rsImageDatas = [];
// Index that will be used a few lines below
let layerIndex = 0;
// Copy of the imageDatas that will be stored in the history
let imageDatasCopy = [];
oldWidth = layers[0].canvasSize[0];
@ -70,6 +97,7 @@ function resizeSprite(event, ratio) {
break;
}
// Computing newWidth and newHeight
if (ratio == null) {
newWidth = data.width;
newHeight = data.height;
@ -88,8 +116,11 @@ function resizeSprite(event, ratio) {
}
}
if (ratio == null) {
// event is null when the user is undoing
if (event != null) {
// Copying the image data
imageDatasCopy = rsImageDatas.slice();
// Saving the history
new HistoryStateResizeSprite(newWidth / oldWidth, newHeight / oldHeight, currentAlgo, imageDatasCopy);
}
@ -124,6 +155,12 @@ function resizeSprite(event, ratio) {
closeDialogue();
}
/* Trust me, the math for the functions below works. If you want to optimize them feel free to have a look, though */
/** Fired when the input field for width is changed. Updates th othe input fields consequently
*
* @param {*} event
*/
function changedWidth(event) {
let oldValue = data.width;
let ratio;
@ -150,6 +187,10 @@ function changedWidth(event) {
document.getElementById("rs-width-percentage").value = newWidthPerc;
}
/**Fired when the input field for width is changed. Updates the other input fields consequently
*
* @param {*} event
*/
function changedHeight(event) {
let oldValue = 100;
let ratio;
@ -176,6 +217,10 @@ function changedHeight(event) {
data.heightPercentage = newHeightPerc;
}
/**Fired when the input field for width percentage is changed. Updates the other input fields consequently
*
* @param {*} event
*/
function changedWidthPercentage(event) {
let oldValue = 100;
let ratio;
@ -189,7 +234,7 @@ function changedWidthPercentage(event) {
console.log("old value: " + oldValue + ", ratio: " + ratio);
newHeight = startData.height * ratio;
newHeightPerc = data.widthPercentage / currentRatio;
newHeightPerc = data.widthPercentage;
newWidth = startData.width * ratio;
if (keepRatio) {
@ -204,6 +249,10 @@ function changedWidthPercentage(event) {
data.width = newWidth;
}
/**Fired when the input field for height percentage is changed. Updates the other input fields consequently
*
* @param {*} event
*/
function changedHeightPercentage(event) {
let oldValue = data.heightPercentage;
let ratio;
@ -215,7 +264,7 @@ function changedHeightPercentage(event) {
ratio = data.heightPercentage / oldValue;
newWidth = startData.width * ratio;
newWidthPerc = data.heightPercentage * currentRatio;
newWidthPerc = data.heightPercentage;
newHeight = startData.height * ratio;
if (keepRatio) {
@ -230,10 +279,18 @@ function changedHeightPercentage(event) {
data.height = newHeight;
}
/** Toggles the keepRatio value (fired by the checkbox in the pop up window)
*
* @param {*} event
*/
function toggleRatio(event) {
keepRatio = !keepRatio;
}
/** Changes the scaling algorithm (fired by the combobox in the pop up window)
*
* @param {*} event
*/
function changedAlgorithm(event) {
currentAlgo = event.target.value;
}

View File

@ -26,8 +26,9 @@ else{
console.log(settings);
//on clicking the save button in the settings dialog
on('click', 'save-settings', function (){
on('click', 'save-settings', saveSettings);
function saveSettings() {
//check if values are valid
if (isNaN(getValue('setting-numberOfHistoryStates'))) {
alert('Invalid value for numberOfHistoryStates');
@ -46,4 +47,4 @@ on('click', 'save-settings', function (){
//close window
closeDialogue();
});
}

23
js/_splashPage.js Normal file
View File

@ -0,0 +1,23 @@
function SplashCoverImage(path, author, link) {
this.path = path;
this.author = author;
this.link = link;
}
let images = [
new SplashCoverImage('Rayquaza', 'Unsettled', 'https://lospec.com/unsettled'),
new SplashCoverImage('Mountains', 'Skeddles', 'https://lospec.com/skeddles'),
new SplashCoverImage('Sweetie', 'GrafxKid', 'https://twitter.com/GrafxKid'),
new SplashCoverImage('Glacier', 'WindfallApples', 'https://lospec.com/windfallapples'),
new SplashCoverImage('Polyphorge1', 'Polyphorge', 'https://lospec.com/poly-phorge'),
new SplashCoverImage('Fusionnist', 'Fusionnist', 'https://lospec.com/fusionnist')
];
let coverImage = document.getElementById('editor-logo');
let authorLink = coverImage.getElementsByTagName('a')[0];
let chosenImage = images[Math.round(Math.random() * (images.length - 1))];
coverImage.style.backgroundImage = 'url("/pixel-editor/' + chosenImage.path + '.png")';
authorLink.setAttribute('href', chosenImage.link);
authorLink.innerHTML = 'Art by ' + chosenImage.author;

View File

@ -35,12 +35,12 @@ on('click',"eraser-smaller-button", function(e){
on('click','rectangle-button', function(e){
// If the user clicks twice on the button, they change the draw mode
if (currentTool.name == 'rectangle') {
if (drawMode == 'empty') {
drawMode = 'fill';
if (rectangleDrawMode == 'empty') {
rectangleDrawMode = 'fill';
setRectToolSvg();
}
else {
drawMode = 'empty';
rectangleDrawMode = 'empty';
setRectToolSvg();
}
}
@ -49,6 +49,24 @@ on('click','rectangle-button', function(e){
}
}, false);
// ellipse
on('click','ellipse-button', function(e){
// If the user clicks twice on the button, they change the draw mode
if (currentTool.name == 'ellipse') {
if (ellipseDrawMode == 'empty') {
ellipseDrawMode = 'fill';
setEllipseToolSvg();
}
else {
ellipseDrawMode = 'empty';
setEllipseToolSvg();
}
}
else {
tool.ellipse.switchTo();
}
}, false);
// rectangle bigger
on('click',"rectangle-bigger-button", function(){
tool.rectangle.brushSize++;
@ -60,6 +78,17 @@ on('click',"rectangle-smaller-button", function(e){
tool.rectangle.brushSize--;
}, false);
// ellipse bigger
on('click',"ellipse-bigger-button", function(){
tool.ellipse.brushSize++;
}, false);
// ellipse smaller
on('click',"ellipse-smaller-button", function(e){
if(tool.ellipse.brushSize > 1)
tool.ellipse.brushSize--;
}, false);
//fill
on('click',"fill-button", function(){
tool.fill.switchTo();
@ -75,33 +104,24 @@ on('click',"eyedropper-button", function(){
tool.eyedropper.switchTo();
}, false);
//zoom tool button
on('click',"zoom-button", function(){
tool.zoom.switchTo();
}, false);
//zoom in button
on('click','zoom-in-button', function(){
//changeZoom('in',[window.innerWidth/2-canvas.offsetLeft,window.innerHeight/2-canvas.offsetTop]);
changeZoom(layers[0],'in', [canvasSize[0] * zoom / 2, canvasSize[1] * zoom / 2]);
for (let i=1; i<layers.length; i++) {
layers[i].copyData(layers[0]);
}
}, false);
//zoom out button
on('click','zoom-out-button', function(){
changeZoom(layers[0],'out',[canvasSize[0]*zoom/2,canvasSize[1]*zoom/2]);
for (let i=1; i<layers.length; i++) {
layers[i].copyData(layers[0]);
}
}, false);
//rectangular selection button
on('click', "rectselect-button", function(){
tool.rectselect.switchTo();
}, false);
//line
on('click',"line-button", function(){
tool.line.switchTo();
}, false);
on('click',"line-bigger-button", function(){
tool.line.brushSize++;
}, false);
on('click',"line-smaller-button", function(){
if(tool.line.brushSize > 1)
tool.line.brushSize--;
}, false);
/*global on */

View File

@ -95,15 +95,14 @@ class Tool {
}
moveBrushPreview(cursorLocation) {
let toSub = 0;
// Prevents the brush to be put in the middle of pixels
if (this.currentBrushSize % 2 == 0) {
toSub = 0.5;
}
brushPreview.style.left = (Math.ceil(cursorLocation[0] / zoom) * zoom + currentLayer.canvas.offsetLeft - this.currentBrushSize * zoom / 2 - zoom / 2 - toSub * zoom) + 'px';
brushPreview.style.top = (Math.ceil(cursorLocation[1] / zoom) * zoom + currentLayer.canvas.offsetTop - this.currentBrushSize * zoom / 2 - zoom / 2 - toSub * zoom) + 'px';
}
let toSub = 1;
// Prevents the brush to be put in the middle of pixels
if (this.currentBrushSize % 2 == 0) {
toSub = 0.5;
}
brushPreview.style.left = (Math.floor(cursorLocation[0] / zoom) * zoom + currentLayer.canvas.offsetLeft - this.currentBrushSize * zoom / 2 - zoom / 2 + toSub * zoom) + 'px';
brushPreview.style.top = (Math.floor(cursorLocation[1] / zoom) * zoom + currentLayer.canvas.offsetTop - this.currentBrushSize * zoom / 2 - zoom / 2 + toSub * zoom) + 'px';
}
}

View File

@ -5,7 +5,7 @@ var dragging = false;
var lastMouseClickPos = [0,0];
var dialogueOpen = false;
var documentCreated = false;
var pixelEditorMode;
var pixelEditorMode = "Advanced";
//common elements
var brushPreview = document.getElementById("brush-preview");

View File

@ -1,8 +1,6 @@
ajax('https://api.github.com/repos/lospec/pixel-editor/contributors', response => {
console.log(response)
if (Array.isArray(response)) {
var html = '';

View File

@ -12,6 +12,9 @@
//=include utilities/hslToRgb.js
//=include libraries/cookies.js
//=include _pixelEditorUtility.js
//=include sortable.js
//=include _algorithms.js
//=include Util.js
/**init**/
//=include _consts.js
@ -28,16 +31,16 @@
//=include tools/*.js
//=include _newPixel.js
//=include _createColorPalette.js
//=include _setCanvasOffset.js
//=include _changeZoom.js
//=include _addColor.js
//=include _colorChanged.js
//=include _initColor.js
//=include _dialogue.js
//=include _updateCursor.js
//=include _featuresLog.js
//=include _drawLine.js
//=include _getCursorPosition.js
//=include _fill.js
//=include _line.js
//=include _history.js
//=include _deleteColor.js
//=include _replaceAllOfColor.js
@ -47,6 +50,10 @@
//=include _copyPaste.js
//=include _resizeCanvas.js
//=include _resizeSprite.js
//=include _colorPicker.js
//=include _paletteBlock.js
//=include _splashPage.js
//=include _logs.js
/**load file**/
//=include _loadImage.js
@ -65,6 +72,7 @@
//=include _rectSelect.js
//=include _move.js
//=include _rectangle.js
//=include _ellipse.js
/**onload**/
//=include _onLoad.js
@ -72,3 +80,9 @@
/**libraries**/
//=include _jscolor.js
/**feature toggles**/
//=include _featureToggles.js
// Controls execution of this preset module
PresetModule.instrumentPresetMenu();

3709
js/sortable.js Normal file

File diff suppressed because it is too large Load Diff

68
js/tools/_all.js Normal file
View File

@ -0,0 +1,68 @@
new Tool('eraser', {
cursor: 'none',
brushPreview: true,
});
new Tool('resizeeraser', {
cursor: 'default',
});
new Tool('eyedropper', {
imageCursor: 'eyedropper',
});
new Tool('fill', {
imageCursor: 'fill',
});
new Tool('line', {
cursor: 'none',
brushPreview: true,
});
new Tool('resizeline', {
cursor: 'default',
});
new Tool('pan', {
cursor: function () {
if (dragging) return 'url(\'/pixel-editor/pan-held.png\'), auto';
else return 'url(\'/pixel-editor/pan.png\'), auto';
},
});
new Tool('pencil', {
cursor: 'none',
brushPreview: true,
});
new Tool('resizebrush', {
cursor: 'default',
});
new Tool('rectangle', {
cursor: 'none',
brushPreview: true,
});
new Tool('ellipse', {
cursor: 'none',
brushPreview: true,
});
new Tool('resizerectangle', {
cursor: 'default',
});
new Tool('rectselect', {
cursor: 'crosshair',
brushPreview: true,
});
new Tool('moveselection', {
cursor: 'crosshair',
});
new Tool('zoom', {
imageCursor: 'zoom-in',
});
//set a default tool
var currentTool = tool.pencil;
var currentToolTemp = tool.pencil;

View File

@ -1,13 +0,0 @@
new Tool('eraser', {
cursor: 'crosshair',
brushPreview: true,
});
new Tool('resizeeraser', {
cursor: 'default',
});
/*global Tool, tool*/

View File

@ -1,7 +0,0 @@
new Tool('eyedropper', {
imageCursor: 'eyedropper',
});
/*global Tool, tool*/

View File

@ -1,7 +0,0 @@
new Tool('fill', {
imageCursor: 'fill',
});
/*global Tool, tool*/

View File

@ -1,10 +0,0 @@
new Tool('pan', {
cursor: function () {
if (dragging) return 'url(\'/pixel-editor/pan-held.png\'), auto';
else return 'url(\'/pixel-editor/pan.png\'), auto';
},
});
/*global Tool, tool*/

View File

@ -1,17 +0,0 @@
new Tool('pencil', {
cursor: 'crosshair',
brushPreview: true,
});
new Tool('resizebrush', {
cursor: 'default',
});
//set as default tool
var currentTool = tool.pencil;
var currentToolTemp = tool.pencil;
/*global Tool, tool*/

View File

@ -1,14 +0,0 @@
new Tool('rectangle', {
cursor: 'crosshair',
brushPreview: true,
});
new Tool('resizerectangle', {
cursor: 'default',
});
/*global Tool, tool*/

View File

@ -1,14 +0,0 @@
new Tool('rectselect', {
cursor: 'crosshair',
brushPreview: true,
});
new Tool('moveselection', {
cursor: 'crosshair',
});
/*global Tool, tool*/

View File

@ -1,6 +0,0 @@
new Tool('zoom', {
imageCursor: 'zoom-in',
});
/*global Tool, tool*/

66
logs/latestLog.html Normal file
View File

@ -0,0 +1,66 @@
<h1>Latest update</h1>
Hello there, welcome to the latest version of the Lospec Pixel Editor. As you can see, we changed
quite a lot of things. Let's go through all them, starting from this page.
<h2>Splash page</h2>
The editor now has a splash page! Besides a fancy cover image with beautiful art, on the bottom
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"/>
<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>
<h2>Canvas resizing</h2>
We implemented canvas resizing! If you wanted to draw a t-rex but created a 4x4 canvas, we've got you covered!
You can now click on <strong>Edit -> Resize canvas</strong> to decrease the size of the project. Same goes if you
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"/>
<h2>Sprite scaling</h2>
In addition to the <a href="https://www.lospec.com/pixel-art-scaler">Lospec Pixel Art Scaler</a>,
you can now take advantage of the editor's built-in scaling function. Just click on <strong>Edit -> Scale sprite</strong>
to scale up or down your work. With the nearest-neighbour algorithm you'll be able to scale sprites
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"/>
<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"/>
<h2>Advanced mode: colour picker and palette block</h2>
If you're a proud user of the advanced mode, you'll be able to try out the new colour picker: it
supports 3 colour models, 6 colour harmonies and multiple ways to input data. Next to it,
you'll find the new palette block, which lets you arrange your colours however you want, add and
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"/>
<h2>Other changes:</h2>
<ul>
<li>You can now move colours in the palette menu</li>
<li>Use <strong>View -> Pixel grid</strong> to show the pixel grid</li>
<li>Fixed a bunch of bugs, made the brush preview pixel perfect</li>
<li>Added <strong>Layer -> Duplicate</strong> to duplicate a layer</li>
<li>Quality of life development improvements by <a href="https://github.com/nkoder">Nkoder</a> and <a href="https://github.com/JulianWebb">Pongles</a></a></li>
<li>Canvas trimming to get rid of all the extra space you have in your sprite</li>
</ul>
<h2>That's all folks!</h2>
That's all for this update! Hope you have fun with this new release :) </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!

1673
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,11 +6,18 @@
"scripts": {
"build": "node ./build.js ./build",
"serve": "node ./server.js ./build 3000",
"test": "npm run build && npm run serve"
"test": "npm run build && npm run serve",
"hot": "concurrently \"nodemon --exec npm test\" \"await tcp localhost:3000 && open-cli http://localhost:3000\"",
"hot:reload": "cross-env RELOAD=yes npm run hot"
},
"author": "Lospec",
"license": "ISC",
"nodemonConfig": {
"ext": "js,hbs,scss",
"ignore": "build/"
},
"dependencies": {
"concurrently": "^6.0.2",
"express": "^4.16.4",
"fs-extra": "^7.0.1",
"gulp": "^4.0.2",
@ -19,7 +26,14 @@
"gulp-rename": "^2.0.0",
"gulp-sass": "^4.0.2",
"handlebars-helper-svg": "git+https://bitbucket.org/skeddles/npm-handlebars-helper-svg-lospec-open-source.git",
"open": "^6.0.0",
"nodemon": "^2.0.7",
"open": "^8.0.6",
"open-cli": "^6.0.1",
"sass": "^1.17.3"
},
"devDependencies": {
"cross-env": "7.0.3",
"reload": "^3.1.1",
"wait-cli": "^1.0.0"
}
}

View File

@ -1,37 +1,48 @@
const path = require('path');
const express = require('express');
const reload = require('reload');
const app = express();
const BUILDDIR = process.argv[2] || './build';
const PORT = process.argv[3] || 3000;
//ROUTE - index.htm
app.get('/', (req, res) => {
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, BUILDDIR, 'index.htm'), {}, function (err) {
if(err){
console.log('error sending file',err);
if (err) {
console.log('error sending file', err);
} else {
setTimeout(()=>{
console.log("Server: Successfully served index.html");
/*setTimeout(()=>{
console.log('closing server');
server.close();
res.app.server.close();
process.exit();
},1000*10);
},1000*10); */
}
});
});
// Better to show landing page rather than 404 on editor page reload
app.get('/pixel-editor/app', (req, res) => {
res.redirect('/');
})
//ROUTE - other files
app.use(express.static(path.join(__dirname, BUILDDIR)));
//start server
var server = app.listen(PORT, () => {
console.log(`\nTemp server started at http://localhost:${PORT}!`);
//console.log('press ctrl+c to stop ');
var opn = require('open');
// opens the url in the default browser
opn(`http://localhost:${PORT}`);
});
// "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.
if (process.env.RELOAD === "yes") {
reload(app).then(() => {
//start server
app.server = app.listen(PORT, () => {
console.log(`Web server listening on port ${PORT} (with reload module)`);
})
});
} else {
app.listen(PORT, () => {
console.log(`Web server listening on port ${PORT}`);
})
}

View File

@ -13,7 +13,6 @@
</head>
<body oncontextmenu="return false;">
<div id="compatibility-warning">
<div><div>
<p><strong>Warning: a modern, desktop, web browser is required to use this tool.</strong></p>
@ -34,6 +33,7 @@
<img src="/pixel-editor/zoom-in.png" />
<img src = "/pixel-editor/eraser.png"/>
<img src = "/pixel-editor/rectselect.png"/>
<!-- TODO: [ELLIPSE] Where is this icon used? Do we need similar one for ellipsis? -->
<img src= "/pixel-editor/rectangle.png">
</div>
@ -91,6 +91,7 @@
<button>Editor</button>
<ul>
<li><button id="switch-mode-button">Switch to basic mode</button></li>
<li><button onclick="showDialogue('splash', false)">Splash page</button></li>
<li><button>Settings</button></li>
</ul>
</li>
@ -119,40 +120,46 @@
</li>
<li class="expanded">
<button title="Rectangle Tool (U)" id="rectangle-button">{{svg "rectangle.svg" width="32" height="32" id = "empty-button-svg"}}
{{svg "fullrect.svg" width="32" height="32" id = "full-button-svg" display = "none"}}</button>
<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>
</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"}}
</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>
</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>
</li>
<li><button title="Fill Tool (F)" id="fill-button">{{svg "fill.svg" width="32" height="32"}}</button></li>
<li><button title="Eyedropper Tool (E)" id="eyedropper-button">{{svg "eyedropper.svg" width="32" height="32"}}</button></li>
<li><button title="Pan Tool (P)" id="pan-button">{{svg "pan.svg" width="32" height="32"}}</button></li>
<li class="expanded">
<button title="Zoom Tool (Z)" id="zoom-button">{{svg "zoom.svg" width="32" height="32"}}</button>
<button title="Zoom In" id="zoom-in-button" class="tools-menu-sub-button">{{svg "plus.svg" width="12" height="12"}}</button>
<button title="Zoom Out" id="zoom-out-button" class="tools-menu-sub-button">{{svg "minus.svg" width="12" height="12"}}</button>
</li>
<li><button title = "Rectangular Selection Tool (M)" id = "rectselect-button">{{svg "rectselect.svg" width = "32" height = "32"}}</button><li>
</ul>
<!-- PALETTE -->
<ul id="colors-menu">
{{!
<li class="noshrink"><button id="current-color" class="jscolor {valueElement: 'current-color-value', styleElement: 'current-color-preview', onFineChange:'setColor(this)', width:151, position: 'left', padding:0,
borderWidth:14, borderColor: '#332f35',backgroundColor: '#332f35', insetColor:'transparent'}"><div id="current-color-preview"></div></button><input id="current-color-value" class="color-value" value="#000000" autocomplete="off" /></li>
}}
<li class="noshrink"><button title="Add Current Color To Palette" id="add-color-button">{{svg "./plus.svg" width="30" height="30"}}</button></li>
</ul>
<!-- LAYER MENU -->
<ul id = "layers-menu">
<li class = "layers-menu-entry selected-layer" draggable = "true">
<li class = "layers-menu-entry selected-layer">
<canvas class = "preview-canvas"></canvas>
<ul class="layer-buttons">
<li class = "layer-button">
@ -232,9 +239,9 @@
{{svg "adjust.svg" width="20" height="20" }}
</div>
<div id="pop-up-container" id = "new-pixel-container">
<div id="pop-up-container">
<!-- NEW PIXEL -->
<div id="new-pixel">
<div id="new-pixel" class="update">
<button class="close-button">{{svg "x.svg" width="20" height="20"}}</button>
<h1>New Pixel</h1>
@ -262,8 +269,76 @@
</div>
</div>
<!-- Splash page -->
<div id = "splash">
<div id="splash-news">
<div id="latest-update">
<h1>Latest updates</h1>
</div>
</div>
<div id="splash-input">
<div id="editor-logo">
<div id="black">
<div id="sp-coverdata">
<img src="https://lospec.com/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>
</div>
</div>
<div class="splash-menu">
<div id="sp-newpixel">
<h1>New Custom Pixel</h1>
<!-- Editor mode-->
<h2>Editor mode</h2>
<div class="sp-np-entry" id="sp-mode-palette">
<div class="button-menu">
<div class="bm-left" onclick="splashMode(event,'Basic')"><p>Basic</p></div>
<div class="sp-interface-selected bm-right" onclick="splashMode(event,'Advanced')"><p>Advanced</p></div>
</div>
</div>
<h2>Size</h2>
<div class="sp-np-entry">
<input id="size-width-splash" value="{{#if width}}{{width}}{{else}}64{{/if}}" autocomplete="off" />{{svg "x.svg" width="16" height="16" class="dimentions-x"}}<input id="size-height-splash" value="{{#if height}}{{height}}{{else}}64{{/if}}" autocomplete="off" />
</div>
<h2>Palette</h2>
<button id="palette-button-splash" class="dropdown-button">Choose a palette...</button>
<div id="palette-menu-splash" class="dropdown-menu"><button id="load-palette-button-splash">Load palette...</button></div>
<div id="new-pixel-warning">Creating a new pixel will discard your current one.</div>
<div class="sp-np-entry">
<button id="create-button-splash" class="default">Create</button>
</div>
</div>
<div id = "sp-quickstart-container">
<div id="sp-quickstart-title">
Quickstart
</div>
<div id="sp-quickstart">
<div class="sp-template" onclick="document.getElementById('open-image-browse-holder').click()"><p>Load</p></div>
<div class="sp-template" onclick="newFromTemplate('Gameboy Color')"><p><span>New</span> Gameboy</p></div>
<div class="sp-template" onclick="newFromTemplate('Commodore 64')"><p><span>New</span> C64</p></div>
<div class="sp-template" onclick="newFromTemplate('PICO-8')"><p><span>New</span> Pico8</p></div>
<div class="sp-template" onclick="newFromTemplate('',16,16)"><p><span>New</span> 16x16</p></div>
<div class="sp-template" onclick="newFromTemplate('',32,32)"><p><span>New</span> 32x32</p></div>
<div class="sp-template" onclick="newFromTemplate('',64,64)"><p><span>New</span> 64x64</p></div>
<div class="sp-template" onclick="newFromTemplate('',128,128)"><p><span>New</span> 128x128</p></div>
<div class="sp-template" onclick="newFromTemplate('',256,256)"><p><span>New</span> 256x256</p></div>
<div class="sp-template" onclick="newFromTemplate('',512,512)"><p><span>New</span> 512x512</p></div>
</div>
</div>
</div>
</div>
</div>
<!--SPRITE RESIZE-->
<div id = "resize-sprite">
<div class="update" id = "resize-sprite">
<button class="close-button">{{svg "x.svg" width="20" height="20"}}</button>
<h1>Scale sprite</h1>
<!-- SIZE-->
@ -313,7 +388,7 @@
</div>
<!--CANVAS RESIZE-->
<div id = "resize-canvas">
<div class="update" id = "resize-canvas">
<button class="close-button">{{svg "x.svg" width="20" height="20"}}</button>
<h1>Resize canvas</h1>
@ -371,6 +446,82 @@
<button id = "resize-canvas-confirm">Resize canvas</button>
</span>
</div>
<!-- PALETTE -->
<div id = "palette-block">
<button class="close-button">{{svg "x.svg" width="20" height="20"}}</button>
<h1>Edit palette</h1>
<div id = "colour-picker">
<div id = "cp-modes">
<button id="cp-rgb" class="cp-selected-mode" onclick="changePickerMode(this, 'rgb')">RGB</button>
<button id="cp-hsv" onclick="changePickerMode(this, 'hsv')">HSV</button>
<button id="cp-hsl" onclick="changePickerMode(this, 'hsl')">HSL</button>
<div id="cp-colour-preview" class="cp-colour-preview"></div>
<input id="cp-hex" type="text" value="#123456" onchange="hexUpdated()"/>
</div>
<div id = "sliders-container">
<div class = "cp-slider-entry">
<label for = "first-slider">R</label>
<input type="range" min="0" max="255" class="colour-picker-slider" id="first-slider" onmousemove="updateSliderValue(1)" onclick="updateSliderValue(1)"/>
<input type = "text" value = "128" onchange="inputChanged(this,1)"/>
</div>
<div class = "cp-slider-entry">
<label for = "second-slider">G</label>
<input type="range" min="0" max ="255" class="colour-picker-slider" id="second-slider" onmousemove="updateSliderValue(2)" onclick="updateSliderValue(2)"/>
<input type = "text" value = "128" onchange="inputChanged(this,2)"/>
</div>
<div class = "cp-slider-entry">
<label for = "third-slider">B</label>
<input type="range" min = "0" max = "255" class = "colour-picker-slider" id = "third-slider" onmousemove="updateSliderValue(3)" onclick="updateSliderValue(3)"/>
<input type = "text" value = "128" onchange="inputChanged(this,3)"/>
</div>
</div>
<div id = "cp-minipicker">
<input type = "range" min = "0" max = "100" id = "cp-minipicker-slider" onmousemove="miniSliderInput(event)" onclick="miniSliderInput(event)"/>
<div id="cp-canvas-container" onmousemove="movePickerIcon(event)">
<canvas id = "cp-spectrum"></canvas>
<div id="cp-active-icon" class="cp-picker-icon"></div>
</div>
<div id = "cp-colours-previews">
<div class = "cp-colour-preview">
#123456
</div>
</div>
<div id = "cp-colour-picking-modes">
<button class="cp-selected-mode" onclick="changePickingMode(event,'mono')">Mono</button>
<button onclick="changePickingMode(event,'analog')">Nlgs</button>
<button onclick="changePickingMode(event,'cmpt')">Cmpt</button>
<button onclick="changePickingMode(event,'tri')">Tri</button>
<button onclick="changePickingMode(event,'scmpt')">Scm</button>
<button onclick="changePickingMode(event,'tetra')">Tetra</button>
</div>
</div>
</div>
<div id = "palette-container">
<ul id = "palette-list">
<li style = "background-color:rgb(255,0,0);width:40px;height:40px;" onmousedown="startRampSelection(event)"
onmousemove="updateRampSelection(event)" onmouseup="endRampSelection(event)"></li>
<li style = "background-color:rgb(0,255,0);width:40px;height:40px;"onmousedown="startRampSelection(event)"
onmousemove="updateRampSelection(event)" onmouseup="endRampSelection(event)"></li>
</ul>
</div>
<div id="pb-options">
<button title="Add colours to palette" onclick="pbAddColours()">Add colours</button>
<button title="Remove colours from palette" onclick="pbRemoveColours()">Remove colours</button>
</div>
</div>
<div id="help">
<button class="close-button">{{svg "x.svg" width="20" height="20"}}</button>
<h1>Help</h1>
@ -381,19 +532,38 @@
</ul>
<h2>Hotkeys</h2>
<ul>
<li>Pencil: <span class="keyboard-key">B</span> or <span class="keyboard-key">1</span></li>
<li>Fill: <span class="keyboard-key">F</span> or <span class="keyboard-key">2</span></li>
<li>Eyedropper: <span class="keyboard-key">E</span> or <span class="keyboard-key">3</span></li>
<li>Pan: <span class="keyboard-key">P</span> or <span class="keyboard-key">M</span> or <span class="keyboard-key">4</span></li>
<li>Zoom: <span class="keyboard-key">Z</span> or <span class="keyboard-key">5</span></li>
<li>Undo: Ctrl + <span class="keyboard-key">Z</span></li>
<li>Redo: Ctrl + <span class="keyboard-key">Y</span> or Ctrl + Alt + <span class="keyboard-key">Z</span></li>
<li><strong>Pencil:</strong> <span class="keyboard-key">B</span> or <span class="keyboard-key">1</span></li>
<li><strong>Eraser:</strong> <span class="keyboard-key">R</span></li>
<li><strong>Rectangle:</strong> <span class="keyboard-key">U</span></li>
<li><strong>Line:</strong> <span class="keyboard-key">L</span></li>
<li><strong>Fill:</strong> <span class="keyboard-key">F</span> or <span class="keyboard-key">2</span></li>
<li><strong>Eyedropper:</strong> <span class="keyboard-key">E</span> or <span class="keyboard-key">3</span></li>
<li><strong>Pan:</strong> <span class="keyboard-key">P</span> or <span class="keyboard-key">M</span> or <span class="keyboard-key">4</span></li>
<li><strong>Zoom:</strong> <span class="keyboard-key">Z</span> or <span class="keyboard-key">5</span></li>
<li><strong>Undo:</strong> Ctrl + <span class="keyboard-key">Z</span></li>
<li><strong>Redo:</strong> Ctrl + <span class="keyboard-key">Y</span> or Ctrl + Alt + <span class="keyboard-key">Z</span></li>
<li><strong>Rectangular selection:</strong> <span class="keyboard-key">M</span></li>
</ul>
<h2>Mouse Shortcuts</h2>
<ul>
<li>Alt + Click - Eyedropper</li>
<li>Space + Click - Pan</li>
<li>Alt + Scroll Wheel - Zoom</li>
<li><strong>Eyedropper: </strong>Alt + Click</li>
<li><strong>Pan: </strong>Space + Click</li>
<li><strong>Zoom: </strong>Alt + Scroll Wheel</li>
</ul>
<h2>Layers</h2>
<ul>
<li>{{svg "visible.svg" width="15" height="15" class = "default-icon"}}: show / hide layer</li>
<li>{{svg "lockedpadlock.svg" width="15" height="15" class = "default-icon"}}: lock / unlock layer, when a layer is locked it's not possible to draw on it</li>
<li>Right click on a layer to open the <strong>menu</strong>:
<ul>
<li><strong>Rename:</strong> change the name of the layer</li>
<li><strong>Duplicate:</strong> duplicate the layer</li>
<li><strong>Delete:</strong> delete the layer (doesn't work if there's only one layer)</li>
<li><strong>Merge below:</strong> merges the selected the layer with the one below it</li>
<li><strong>Flatten visible:</strong> merges all the visible layers</li>
<li></strong>Flatten all:</strong> merges all the layers</li>
</ul>
</li>
</ul>
</div>
<div id="about">
@ -481,5 +651,6 @@
</script>
<script src="/pixel-editor/pixel-editor.js"></script>
<script src="/reload/reload.js"></script>
</body>
</html>