commit
8ee0214fad
20
README.md
20
README.md
|
@ -4,39 +4,41 @@ This is a browser based software for creating pixel art
|
|||
|
||||
The tool can be viewed online here: https://lospec.com/pixel-editor
|
||||
|
||||
## How to contribute
|
||||
|
||||
Please do not submit pull requests with new features or core changes. Instead, please file an issue first for discussion.
|
||||
|
||||
## What to Contribute
|
||||
|
||||
Any changes that fix bugs or add features are welcome.
|
||||
Any changes that fix bugs or add features are welcome. Check out the issues if you don't know where to start: if
|
||||
you're new to the editor, we suggest you check out the Wiki first.
|
||||
|
||||
The next version is mostly focused on adding missing essential features and porting to mobile.
|
||||
|
||||
Suggestions / Planned features:
|
||||
|
||||
- Documentation
|
||||
|
||||
- Possibility to hide and resize menus (layers, palette)
|
||||
- Line tool
|
||||
- Tiled mode
|
||||
- Load palette from LPE file
|
||||
- Symmetry options
|
||||
- Symmetry options (currently being worked on)
|
||||
- Make a palette grid instead of having a huge stack on the right when colours are too many
|
||||
- Possibly add collaborate function
|
||||
|
||||
- Mobile
|
||||
- Touch equivalent for mouse clicks
|
||||
- Hide or scale ui
|
||||
- Maybe rearrange UI on portrait
|
||||
- Stack colors when too many
|
||||
- Fix popups
|
||||
|
||||
- Possibly add collaborate function
|
||||
|
||||
- Polish:
|
||||
- ctrl a to select everything / selection -> all, same for deselection
|
||||
- CTRL+A to select everything / selection -> all, same for deselection
|
||||
- Warning windows for wrong inputs
|
||||
- Palette option remove unused colors
|
||||
- Move selection with arrows
|
||||
- Update borders by dragging the canvas' edges with the mouse when resizing canvas
|
||||
- Move the canvases so they're centered after resizing the canvas (maybe a .center() method in layer class)
|
||||
- Scale selection
|
||||
- Scale / rotate selection
|
||||
|
||||
## How to Contribute
|
||||
|
||||
|
|
4
build.js
4
build.js
|
@ -21,6 +21,8 @@ function copy_images(){
|
|||
gulp.src('./images/Splash images/*.png').pipe(gulp.dest(BUILDDIR));
|
||||
// Logs images
|
||||
gulp.src('./images/Logs/*.gif').pipe(gulp.dest(BUILDDIR));
|
||||
// Logs images
|
||||
gulp.src('./images/Logs/*.png').pipe(gulp.dest(BUILDDIR));
|
||||
}
|
||||
|
||||
function copy_logs() {
|
||||
|
@ -49,7 +51,7 @@ function compile_page(){
|
|||
.pipe(include({includePaths: ['/svg']}))
|
||||
|
||||
.pipe(handlebars({encoding: 'utf8', debug: true, bustCache: true})
|
||||
.partials('./views/[!index]*.hbs')
|
||||
.partials('./views/[!index]*.hbs').partials('./views/popups/*.hbs')
|
||||
//.helpers({ svg: hb_svg })
|
||||
.helpers('./helpers/**/*.js')
|
||||
.data({
|
||||
|
|
|
@ -69,6 +69,8 @@
|
|||
}
|
||||
|
||||
#brush-preview {
|
||||
background-color:black;
|
||||
opacity:0.3;
|
||||
position: absolute;
|
||||
border: solid 1px #fff;
|
||||
z-index: 1200;
|
||||
|
|
|
@ -77,7 +77,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
#tools-menu li button#pencil-bigger-button,
|
||||
#tools-menu li button#brush-bigger-button,
|
||||
#tools-menu li button#zoom-in-button,
|
||||
#tools-menu li button#eraser-bigger-button,
|
||||
#tools-menu li button#rectangle-bigger-button,
|
||||
|
@ -86,7 +86,7 @@
|
|||
left: 0;
|
||||
}
|
||||
|
||||
#tools-menu li button#pencil-smaller-button,
|
||||
#tools-menu li button#brush-smaller-button,
|
||||
#tools-menu li button#zoom-out-button,
|
||||
#tools-menu li button#eraser-smaller-button,
|
||||
#tools-menu li button#rectangle-smaller-button,
|
||||
|
@ -95,8 +95,8 @@
|
|||
right: 0;
|
||||
}
|
||||
|
||||
#tools-menu li.selected button#pencil-bigger-button,
|
||||
#tools-menu li.selected button#pencil-smaller-button,
|
||||
#tools-menu li.selected button#brush-bigger-button,
|
||||
#tools-menu li.selected button#brush-smaller-button,
|
||||
#tools-menu li.selected button#zoom-in-button,
|
||||
#tools-menu li.selected button#zoom-out-button,
|
||||
#tools-menu li.selected button#eraser-bigger-button,
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
|
@ -0,0 +1,180 @@
|
|||
// OPTIMIZABLE: add a normalize function that returns the normalized colour in the current
|
||||
// format.
|
||||
class Color {
|
||||
constructor(fmt, v1, v2, v3, v4) {
|
||||
this.fmt = fmt;
|
||||
|
||||
switch (fmt) {
|
||||
case 'hsv':
|
||||
this.hsv = {h: v1, s: v2, v: v3};
|
||||
this.rgb = Color.hsvToRgb(this.hsv);
|
||||
this.hsl = Color.rgbToHsl(this.rgb);
|
||||
this.hex = Color.rgbToHex(this.rgb);
|
||||
break;
|
||||
case 'hsl':
|
||||
this.hsl = {h: v1, s: v2, l: v3};
|
||||
this.rgb = Color.hslToRgb(this.hsl);
|
||||
this.hsv = Color.rgbToHsv(this.rgb);
|
||||
this.hex = Color.rgbToHex(this.rgb);
|
||||
break;
|
||||
case 'rgb':
|
||||
this.rgb = {r: v1, g: v2, b: v3};
|
||||
this.hsl = Color.rgbToHsl(this.rgb);
|
||||
this.hsv = Color.rgbToHsv(this.rgb);
|
||||
this.hex = Color.rgbToHex(this.rgb);
|
||||
break;
|
||||
case 'hex':
|
||||
this.hex = v1;
|
||||
this.rgb = Color.hexToRgb(this.hex);
|
||||
this.hsl = Color.rgbToHsl(this.rgb);
|
||||
this.hsv = Color.rgbToHsv(this.rgb);
|
||||
break;
|
||||
default:
|
||||
console.error("Unsupported color mode " + fmt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static hexToRgb(hex, divisor) {
|
||||
//if divisor isn't set, set it to one (so it has no effect)
|
||||
divisor = divisor || 1;
|
||||
//split given hex code into array of 3 values
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex.trim());
|
||||
|
||||
return result ? {
|
||||
r: parseInt(result[1], 16)/divisor,
|
||||
g: parseInt(result[2], 16)/divisor,
|
||||
b: parseInt(result[3], 16)/divisor
|
||||
} : null;
|
||||
}
|
||||
static rgbToHex(rgb) {
|
||||
function componentToHex (c) {
|
||||
var hex = Math.round(c).toString(16);
|
||||
return hex.length == 1 ? "0" + hex : hex.substring(0, 2);
|
||||
}
|
||||
|
||||
return componentToHex(rgb.r) + componentToHex(rgb.g) + componentToHex(rgb.b);
|
||||
}
|
||||
|
||||
static hslToRgb(hsl) {
|
||||
let r, g, b;
|
||||
let h = hsl.h, s = hsl.s, l = hsl.l;
|
||||
|
||||
h /= 360;
|
||||
s /= 100;
|
||||
l /= 100;
|
||||
|
||||
if(s == 0){
|
||||
r = g = b = l; // achromatic
|
||||
}else{
|
||||
const 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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
return {
|
||||
r:Math.round(r * 255),
|
||||
g:Math.round(g * 255),
|
||||
b:Math.round(b * 255)
|
||||
};
|
||||
}
|
||||
static rgbToHsl(rgb) {
|
||||
let r, g, b;
|
||||
r = rgb.r; g = rgb.g; b = rgb.b;
|
||||
|
||||
r /= 255, g /= 255, b /= 255;
|
||||
|
||||
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
||||
let hue, saturation, luminosity = (max + min) / 2;
|
||||
|
||||
if(max == min){
|
||||
hue = saturation = 0; // achromatic
|
||||
}else{
|
||||
const d = max - min;
|
||||
saturation = luminosity > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
switch(max){
|
||||
case r: hue = (g - b) / d + (g < b ? 6 : 0); break;
|
||||
case g: hue = (b - r) / d + 2; break;
|
||||
case b: hue = (r - g) / d + 4; break;
|
||||
}
|
||||
hue /= 6;
|
||||
}
|
||||
|
||||
return {h:hue*360, s:saturation*100, l:luminosity*100};
|
||||
}
|
||||
|
||||
static hsvToRgb(hsv) {
|
||||
let r, g, b, h, s, v;
|
||||
h = hsv.h; s = hsv.s; v = hsv.v;
|
||||
|
||||
h /= 360;
|
||||
s /= 100;
|
||||
v /= 100;
|
||||
|
||||
const i = Math.floor(h * 6);
|
||||
const f = h * 6 - i;
|
||||
const p = v * (1 - s);
|
||||
const q = v * (1 - f * s);
|
||||
const 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: r * 255, g: g * 255, b: b * 255 };
|
||||
}
|
||||
static rgbToHsv(rgb) {
|
||||
let r = rgb.r, g = rgb.g, b = rgb.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 * 360, s: myS * 100, v: myV * 100};
|
||||
}
|
||||
|
||||
/** Converts a CSS colour eg rgb(x,y,z) to a hex string
|
||||
*
|
||||
* @param {*} rgb
|
||||
*/
|
||||
static 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]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,451 @@
|
|||
/** ColorModule holds the functions used to implement the basic-mode palette.
|
||||
*
|
||||
*/
|
||||
const ColorModule = (() => {
|
||||
// Array containing the colours of the current palette
|
||||
let currentPalette = [];
|
||||
// Reference to the HTML palette
|
||||
const coloursList = document.getElementById("palette-list");
|
||||
// Reference to the colours menu
|
||||
const colorsMenu = document.getElementById("colors-menu");
|
||||
|
||||
// Binding events to callbacks
|
||||
document.getElementById('jscolor-hex-input').addEventListener('change',colorChanged, false);
|
||||
document.getElementById('jscolor-hex-input').addEventListener('input', colorChanged, false);
|
||||
document.getElementById('add-color-button').addEventListener('click', addColorButtonEvent, false);
|
||||
|
||||
// Making the colours in the HTML menu sortable
|
||||
new Sortable(document.getElementById("colors-menu"), {
|
||||
animation:100,
|
||||
filter: ".noshrink",
|
||||
draggable: ".draggable-colour",
|
||||
onEnd: function() {Events.simulateMouseEvent(window, "mouseup");}
|
||||
});
|
||||
|
||||
/** Changes all of one color to another after being changed from the color picker
|
||||
*
|
||||
* @param {*} colorHexElement The element that has been changed
|
||||
* @returns
|
||||
*/
|
||||
function colorChanged(colorHexElement) {
|
||||
// Get old and new colors from the element
|
||||
const hexElement = colorHexElement.target;
|
||||
const hexElementValue = hexElement.value;
|
||||
const newColor = Color.hexToRgb(hexElementValue);
|
||||
const oldColor = hexElement.oldColor;
|
||||
|
||||
//if the color is not a valid hex color, exit this function and do nothing
|
||||
const newColorHex = hexElementValue.toLowerCase();
|
||||
if (/^[0-9a-f]{6}$/i.test(newColorHex) == false) return;
|
||||
|
||||
currentPalette.splice(currentPalette.indexOf("#" + newColor), 1);
|
||||
newColor.a = 255;
|
||||
|
||||
//save undo state
|
||||
new HistoryState().EditColor(hexElementValue.toLowerCase(), Color.rgbToHex(oldColor));
|
||||
|
||||
//get the currently selected color
|
||||
const currentlyEditedColor = document.getElementsByClassName('jscolor-active')[0];
|
||||
const duplicateColorWarning = document.getElementById('duplicate-color-warning');
|
||||
|
||||
//check if selected color already matches another color
|
||||
colors = document.getElementsByClassName('color-button');
|
||||
|
||||
//loop through all colors in palette
|
||||
for (let i = 0; i < colors.length; i++) {
|
||||
//if generated color matches this color
|
||||
if (newColorHex == colors[i].jscolor.toString()) {
|
||||
//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);
|
||||
|
||||
//show the duplicate color warning
|
||||
duplicateColorWarning.style.visibility = 'visible';
|
||||
|
||||
//shake warning icon
|
||||
duplicateColorWarning.classList.remove('shake');
|
||||
void duplicateColorWarning.offsetWidth;
|
||||
duplicateColorWarning.classList.add('shake');
|
||||
|
||||
//exit function without updating color
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//if the color being edited has a duplicate color warning, remove it
|
||||
duplicateColorWarning.style.visibility = 'hidden';
|
||||
|
||||
currentlyEditedColor.firstChild.jscolor.fromString(newColorHex);
|
||||
|
||||
ColorModule.replaceAllOfColor(oldColor, newColor);
|
||||
|
||||
//set new old color to changed color
|
||||
hexElement.oldColor = newColor;
|
||||
currentPalette.push('#' + newColorHex);
|
||||
|
||||
//if this is the current color, update the drawing color
|
||||
if (hexElement.colorElement.parentElement.classList.contains('selected')) {
|
||||
updateCurrentColor('#' + Color.rgbToHex(newColor));
|
||||
}
|
||||
}
|
||||
|
||||
/** Callback triggered when the user clicks on a colour in the palette menu on the right
|
||||
*
|
||||
* @param {*} e The event that triggered the callback
|
||||
*/
|
||||
function clickedColor (e){
|
||||
//left clicked color
|
||||
if (e.which == 1) {
|
||||
// remove current color selection
|
||||
document.querySelector('#colors-menu li.selected')?.classList.remove('selected');
|
||||
|
||||
//set current color
|
||||
updateCurrentColor(Color.cssToHex(e.target.style.backgroundColor));
|
||||
|
||||
//make color selected
|
||||
e.target.parentElement.classList.add('selected');
|
||||
|
||||
}
|
||||
//right clicked color
|
||||
else if (e.which == 3) {
|
||||
//hide edit color button (to prevent it from showing)
|
||||
e.target.parentElement.lastChild.classList.add('hidden');
|
||||
//show color picker
|
||||
e.target.jscolor.show();
|
||||
}
|
||||
}
|
||||
|
||||
/** Called whenever the user presses the button used to add a new colour to the palette
|
||||
*
|
||||
*/
|
||||
function addColorButtonEvent() {
|
||||
//generate random color
|
||||
const newColor = new Color("hsv", Math.floor(Math.random()*360), Math.floor(Math.random()*100), Math.floor(Math.random()*100)).hex;
|
||||
|
||||
//remove current color selection
|
||||
document.querySelector('#colors-menu li.selected')?.classList.remove('selected');
|
||||
|
||||
//add new color and make it selected
|
||||
let addedColor = addColor(newColor);
|
||||
addedColor.classList.add('selected');
|
||||
updateCurrentColor(newColor);
|
||||
|
||||
//add history state
|
||||
new HistoryState().AddColor(addedColor.firstElementChild.jscolor.toString());
|
||||
|
||||
//show color picker
|
||||
addedColor.firstElementChild.jscolor.show();
|
||||
//hide edit button
|
||||
addedColor.lastChild.classList.add('hidden');
|
||||
}
|
||||
|
||||
/** Adds the colors that have been added through the advanced-mode color picker to the
|
||||
* basic-mode palette.
|
||||
*
|
||||
*/
|
||||
function addToSimplePalette() {
|
||||
const simplePalette = document.getElementById("colors-menu");
|
||||
const 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++) {
|
||||
const col = coloursList.children[i].style.backgroundColor;
|
||||
|
||||
if (col.includes("rgb")) {
|
||||
addColor(Color.cssToHex(col));
|
||||
}
|
||||
else {
|
||||
addColor(col);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Initializes jscolor for the element passed as a parameter
|
||||
*
|
||||
* @param {*} colorElement The element of which we need to setup jscolor
|
||||
*/
|
||||
function initColor (colorElement) {
|
||||
//add jscolor picker for this color
|
||||
colorElement.jscolor = new jscolor(colorElement.parentElement, {
|
||||
valueElement: null,
|
||||
styleElement: colorElement,
|
||||
width:151,
|
||||
position: 'left',
|
||||
padding:0,
|
||||
borderWidth:14,
|
||||
borderColor: '#332f35',
|
||||
backgroundColor: '#332f35',
|
||||
insetColor: 'transparent',
|
||||
value: colorElement.style.backgroundColor,
|
||||
deleteButton: true,
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/** Adds a color to the palette
|
||||
*
|
||||
* @param {*} newColor The color to add in hex format
|
||||
* @returns The HTML palette item that has been created
|
||||
*/
|
||||
function addColor (newColor) {
|
||||
//add # at beginning if not present
|
||||
if (newColor.charAt(0) != '#')
|
||||
newColor = '#' + newColor;
|
||||
currentPalette.push(newColor);
|
||||
//create list item
|
||||
const listItem = document.createElement('li');
|
||||
|
||||
//create button
|
||||
const button = document.createElement('button');
|
||||
button.classList.add('color-button');
|
||||
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]);
|
||||
|
||||
//add jscolor functionality
|
||||
initColor(button);
|
||||
|
||||
//add edit button
|
||||
const editButtonTemplate = document.getElementsByClassName('color-edit-button')[0];
|
||||
newEditButton = editButtonTemplate.cloneNode(true);
|
||||
listItem.appendChild(newEditButton);
|
||||
|
||||
newEditButton.addEventListener('click', (event) => {
|
||||
//hide edit button
|
||||
event.target.parentElement.lastChild.classList.add('hidden');
|
||||
|
||||
//show jscolor picker, if basic mode is enabled
|
||||
if (EditorState.getCurrentMode() == 'Basic')
|
||||
event.target.parentElement.firstChild.jscolor.show();
|
||||
else
|
||||
Dialogue.showDialogue("palette-block", false);
|
||||
});
|
||||
|
||||
return listItem;
|
||||
}
|
||||
|
||||
/** Deletes a color from the palette
|
||||
*
|
||||
* @param {*} color A string in hex format or the HTML element corresponding to the color
|
||||
* that should be removed.
|
||||
*/
|
||||
function deleteColor (color) {
|
||||
const logStyle = 'background: #913939; color: white; padding: 5px;';
|
||||
|
||||
//if color is a string, then find the corresponding button
|
||||
if (typeof color === 'string') {
|
||||
if (color[0] === '#')
|
||||
color = color.substr(1, color.length - 1);
|
||||
//get all colors in palette
|
||||
let colors = document.getElementsByClassName('color-button');
|
||||
|
||||
//loop through colors
|
||||
for (var i = 0; i < colors.length; i++) {
|
||||
//console.log(color,'=',colors[i].jscolor.toString());
|
||||
|
||||
if (color == colors[i].jscolor.toString()) {
|
||||
//set color to the color button
|
||||
currentPalette.splice(i, 1);
|
||||
color = colors[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//if the color wasn't found
|
||||
if (typeof color === 'string') {
|
||||
//exit function
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//hide color picker
|
||||
color.jscolor.hide();
|
||||
|
||||
//find lightest color in palette
|
||||
let colors = document.getElementsByClassName('color-button');
|
||||
let lightestColor = [0,null];
|
||||
for (let i = 0; i < colors.length; i++) {
|
||||
|
||||
//get colors lightness
|
||||
let lightness = Color.rgbToHsl(colors[i].jscolor.toRgb()).l;
|
||||
|
||||
//if not the color we're deleting
|
||||
if (colors[i] != color) {
|
||||
|
||||
//if lighter than the current lightest, set as the new lightest
|
||||
if (lightness > lightestColor[0]) {
|
||||
lightestColor[0] = lightness;
|
||||
lightestColor[1] = colors[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//replace deleted color with lightest color
|
||||
ColorModule.replaceAllOfColor(color.jscolor.toString(),lightestColor[1].jscolor.toString());
|
||||
|
||||
//if the color you are deleting is the currently selected color
|
||||
if (color.parentElement.classList.contains('selected')) {
|
||||
//set current color TO LIGHTEST COLOR
|
||||
lightestColor[1].parentElement.classList.add('selected');
|
||||
updateCurrentColor('#'+lightestColor[1].jscolor.toString());
|
||||
}
|
||||
|
||||
//delete the element
|
||||
colorsMenu.removeChild(color.parentElement);
|
||||
}
|
||||
|
||||
/** Replaces all of a single color on the canvas with a different color
|
||||
*
|
||||
* @param {*} oldColor Old colour in {r,g,b} object format
|
||||
* @param {*} newColor New colour in {r,g,b} object format
|
||||
*/
|
||||
function replaceAllOfColor (oldColor, newColor) {
|
||||
|
||||
//convert strings to objects if nessesary
|
||||
if (typeof oldColor === 'string') oldColor = Color.hexToRgb(oldColor);
|
||||
if (typeof newColor === 'string') newColor = Color.hexToRgb(newColor);
|
||||
|
||||
//create temporary image from canvas to search through
|
||||
var tempImage = currFile.currentLayer.context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
|
||||
|
||||
//loop through all pixels
|
||||
for (var i=0;i<tempImage.data.length;i+=4) {
|
||||
//check if pixel matches old color
|
||||
if(tempImage.data[i]==oldColor.r && tempImage.data[i+1]==oldColor.g && tempImage.data[i+2]==oldColor.b){
|
||||
//change to new color
|
||||
tempImage.data[i]=newColor.r;
|
||||
tempImage.data[i+1]=newColor.g;
|
||||
tempImage.data[i+2]=newColor.b;
|
||||
}
|
||||
}
|
||||
|
||||
//put temp image back onto canvas
|
||||
currFile.currentLayer.context.putImageData(tempImage,0,0);
|
||||
}
|
||||
|
||||
function getCurrentPalette() {
|
||||
return currentPalette;
|
||||
}
|
||||
|
||||
function resetPalette() {
|
||||
currentPalette = [];
|
||||
}
|
||||
|
||||
/** Creates the colour palette when starting up the editor from _newPixel.js
|
||||
*
|
||||
* @param {*} paletteColors The colours of the palette
|
||||
*/
|
||||
function createColorPalette(paletteColors) {
|
||||
console.log("creating palette");
|
||||
//remove current palette
|
||||
while (colorsMenu.childElementCount > 1)
|
||||
colorsMenu.children[0].remove();
|
||||
|
||||
var lightestColor = new Color("hex", '#000000');
|
||||
var darkestColor = new Color("hex", '#ffffff');
|
||||
|
||||
// Adding all the colours in the array
|
||||
for (var i = 0; i < paletteColors.length; i++) {
|
||||
var newColor = new Color("hex", paletteColors[i]);
|
||||
var newColorElement = ColorModule.addColor(newColor.hex);
|
||||
|
||||
var newColRgb = newColor.rgb;
|
||||
|
||||
var lightestColorRgb = lightestColor.rgb;
|
||||
if (newColRgb.r + newColRgb.g + newColRgb.b > lightestColorRgb.r + lightestColorRgb.g + lightestColorRgb.b)
|
||||
lightestColor = newColor;
|
||||
|
||||
var darkestColorRgb = darkestColor.rgb;
|
||||
if (newColRgb.r + newColRgb.g + newColRgb.b < darkestColorRgb.r + darkestColorRgb.g + darkestColorRgb.b) {
|
||||
|
||||
//remove current color selection
|
||||
document.querySelector('#colors-menu li.selected')?.classList.remove('selected');
|
||||
|
||||
//set as current color
|
||||
newColorElement.classList.add('selected');
|
||||
darkestColor = newColor;
|
||||
}
|
||||
}
|
||||
|
||||
//prepend # if not present
|
||||
if (!darkestColor.hex.includes('#')) darkestColor.hex = '#' + darkestColor.hex;
|
||||
|
||||
//set as current color
|
||||
updateCurrentColor(darkestColor.hex);
|
||||
}
|
||||
|
||||
/** Creates the palette with the colours used in all the layers
|
||||
*
|
||||
*/
|
||||
function createPaletteFromLayers() {
|
||||
let colors = {};
|
||||
let nColors = 0;
|
||||
//create array out of colors object
|
||||
let colorPaletteArray = [];
|
||||
|
||||
for (let i=0; i<currFile.layers.length; i++) {
|
||||
if (currFile.layers[i].hasCanvas()) {
|
||||
let imageData = currFile.layers[i].context.getImageData(
|
||||
0, 0, currFile.canvasSize[0], currFile.canvasSize[1]).data;
|
||||
let dataLength = imageData.length;
|
||||
|
||||
for (let j=0; j<dataLength; j += 4) {
|
||||
if (!Util.isPixelEmpty(imageData[j])) {
|
||||
let color = imageData[j]+','+imageData[j + 1]+','+imageData[j + 2];
|
||||
|
||||
if (!colors[color]) {
|
||||
colorPaletteArray.push('#' + new Color("rgb", imageData[j], imageData[j + 1], imageData[j + 2]).hex);
|
||||
colors[color] = new Color("rgb", imageData[j], imageData[j + 1], imageData[j + 2]).rgb;
|
||||
nColors++;
|
||||
//don't allow more than 256 colors to be added
|
||||
if (nColors >= Settings.getCurrSettings().maxColorsOnImportedImage) {
|
||||
alert('The image loaded seems to have more than '+Settings.getCurrSettings().maxColorsOnImportedImage+' colors.');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//create palette from colors array
|
||||
createColorPalette(colorPaletteArray);
|
||||
|
||||
console.log("Done 2");
|
||||
}
|
||||
|
||||
function updateCurrentColor(color, refLayer) {
|
||||
if (color[0] != '#')
|
||||
color = '#' + color;
|
||||
|
||||
if (refLayer)
|
||||
color = refLayer.context.fillStyle;
|
||||
|
||||
for (let i=0; i<currFile.layers.length - 1; i++) {
|
||||
currFile.layers[i].context.fillStyle = color;
|
||||
currFile.layers[i].context.strokeStyle = color;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
getCurrentPalette,
|
||||
addColor,
|
||||
deleteColor,
|
||||
replaceAllOfColor,
|
||||
addToSimplePalette,
|
||||
resetPalette,
|
||||
createColorPalette,
|
||||
createPaletteFromLayers,
|
||||
updateCurrentColor,
|
||||
}
|
||||
})();
|
|
@ -0,0 +1,777 @@
|
|||
const ColorPicker = (() => {
|
||||
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;
|
||||
|
||||
// Picker mode events
|
||||
Events.on("click", "cp-rgb", changePickerMode, 'rgb');
|
||||
Events.on("click", "cp-hsv", changePickerMode, 'hsv');
|
||||
Events.on("click", "cp-hsl", changePickerMode, 'hsl');
|
||||
|
||||
// Hex-related events
|
||||
Events.on("change", "cp-hex", hexUpdated);
|
||||
|
||||
// Slider events
|
||||
Events.on("mousemove", "first-slider", updateSliderValue, 1);
|
||||
Events.on("mousemove", "second-slider", updateSliderValue, 2);
|
||||
Events.on("mousemove", "third-slider", updateSliderValue, 3);
|
||||
Events.on("click", "first-slider", updateSliderValue, 1);
|
||||
Events.on("click", "second-slider", updateSliderValue, 2);
|
||||
Events.on("click", "third-slider", updateSliderValue, 3);
|
||||
// Slider textbox events
|
||||
Events.on("change", "cp-sliderText1", inputChanged, 1);
|
||||
Events.on("change", "cp-sliderText2", inputChanged, 2);
|
||||
Events.on("change", "cp-sliderText3", inputChanged, 3);
|
||||
|
||||
// Minipicker events
|
||||
Events.on("mousemove", "cp-minipicker-slider", miniSliderInput);
|
||||
Events.on("click", "cp-minipicker-slider", miniSliderInput);
|
||||
Events.on("mousemove", "cp-canvas-container", movePickerIcon);
|
||||
|
||||
Events.on("click", "cp-mono", changePickingMode, "mono");
|
||||
Events.on("click", "cp-analog", changePickingMode, "analog");
|
||||
Events.on("click", "cp-cmpt", changePickingMode, "cmpt");
|
||||
Events.on("click", "cp-tri", changePickingMode, "tri");
|
||||
Events.on("click", "cp-scmpt", changePickingMode, "scmpt");
|
||||
Events.on("click", "cp-tetra", changePickingMode, "tetra");
|
||||
|
||||
init();
|
||||
|
||||
function init() {
|
||||
// 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
|
||||
hexColour = new Color(currentPickerMode, sliderValues[0], sliderValues[1], sliderValues[2]);
|
||||
|
||||
// Update preview colour div
|
||||
colourPreview.style.backgroundColor = '#' + hexColour.hex;
|
||||
colourValue.value = '#' + hexColour.hex;
|
||||
|
||||
// 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 = Color.hsvToRgb({h:sliderValues[0], s:0, v:sliderValues[2]});
|
||||
gradientMin = 'rgba(' + rgbColour.r + ',' + rgbColour.g + ',' + rgbColour.b + ',1)';
|
||||
|
||||
rgbColour = Color.hsvToRgb({h:sliderValues[0], s:100, v:sliderValues[2]});
|
||||
gradientMax = 'rgba(' + rgbColour.r + ',' + rgbColour.g + ',' + rgbColour.b + ',1)';
|
||||
break;
|
||||
case 'hsl':
|
||||
rgbColour = Color.hslToRgb({h:sliderValues[0], s:0, l:sliderValues[2]});
|
||||
gradientMin = 'rgba(' + rgbColour.r + ',' + rgbColour.g + ',' + rgbColour.b + ',1)';
|
||||
|
||||
rgbColour = Color.hslToRgb({h:sliderValues[0], s:100, l:sliderValues[2]});
|
||||
gradientMax = 'rgba(' + rgbColour.r + ',' + rgbColour.g + ',' + rgbColour.b + ',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 = Color.hsvToRgb({h:sliderValues[0], s:sliderValues[1], v:0});
|
||||
gradientMin = 'rgba(' + rgbColour.r + ',' + rgbColour.g + ',' + rgbColour.b + ',1)';
|
||||
|
||||
rgbColour = Color.hsvToRgb({h:sliderValues[0], s:sliderValues[1], v:100});
|
||||
gradientMax = 'rgba(' + rgbColour.r + ',' + rgbColour.g + ',' + rgbColour.b + ',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 = Color.hslToRgb({h:sliderValues[0], s:sliderValues[1], l:50});
|
||||
ret += 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',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 ret = 'linear-gradient(90deg, rgba(2,0,36,1) 0%, ';
|
||||
let consts = ['0%,', '16.6666%,', '33.3333%,', '50%,', '66.6666%,', '83.3333%,', '100%);'];
|
||||
|
||||
for (let i=0; i<consts.length; i++) {
|
||||
let col = Color.hsvToRgb({h:i * 60, s:sliderValues[1], v:sliderValues[2]});
|
||||
ret += 'rgba(' + col.r + ',' + col.g + ',' + col.b + ',1) ' + consts[i];
|
||||
}
|
||||
|
||||
return ret.substr(0, ret.length - 1);
|
||||
}
|
||||
|
||||
// Fired when the values in the labels are changed
|
||||
function inputChanged(index) {
|
||||
let sliderIndex = index - 1;
|
||||
let target = document.getElementById("cp-sliderText" + index);
|
||||
|
||||
sliders[sliderIndex].getElementsByTagName("input")[0].value = target.value;
|
||||
updateSliderValue(index);
|
||||
}
|
||||
|
||||
// Updates the colour model used to pick colours
|
||||
function changePickerMode(newMode) {
|
||||
let maxRange;
|
||||
let colArray;
|
||||
let hexColour = colourValue.value.replace('#', '');
|
||||
let currColor = new Color("hex", hexColour);
|
||||
|
||||
currentPickerMode = newMode;
|
||||
document.getElementsByClassName("cp-selected-mode")[0].classList.remove("cp-selected-mode");
|
||||
document.getElementById("cp-" + newMode).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:
|
||||
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 = currColor.rgb;
|
||||
colArray = [colArray.r, colArray.g, colArray.b];
|
||||
break;
|
||||
case 'hsv':
|
||||
colArray = currColor.hsv;
|
||||
colArray = [colArray.h, colArray.s, colArray.v];
|
||||
break;
|
||||
case 'hsl':
|
||||
colArray = currColor.hsl;
|
||||
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.buttons == 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 = new Color("hex", hex);
|
||||
let mySliders = [sliders[0].getElementsByTagName("input")[0],
|
||||
sliders[1].getElementsByTagName("input")[0],
|
||||
sliders[2].getElementsByTagName("input")[0]];
|
||||
|
||||
switch (currentPickerMode) {
|
||||
case 'rgb':
|
||||
colour = colour.rgb;
|
||||
|
||||
mySliders[0].value = colour.r;
|
||||
mySliders[1].value = colour.g;
|
||||
mySliders[2].value = colour.b;
|
||||
|
||||
break;
|
||||
case 'hsv':
|
||||
colour = colour.hsv;
|
||||
|
||||
mySliders[0].value = colour.h;
|
||||
mySliders[1].value = colour.s;
|
||||
mySliders[2].value = colour.v;
|
||||
|
||||
break;
|
||||
case 'hsl':
|
||||
colour = colour.hsl;
|
||||
|
||||
mySliders[0].value = colour.h;
|
||||
mySliders[1].value = colour.s;
|
||||
mySliders[2].value = colour.l;
|
||||
|
||||
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 = new Color("hex", hex).hsv;
|
||||
let xPos = miniPickerCanvas.width * hsv.h/360 - 8;
|
||||
let yPos = miniPickerCanvas.height * hsv.s/100 + 8;
|
||||
|
||||
miniPickerSlider.value = hsv.v;
|
||||
|
||||
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 currColor = new Color("hex", getMiniPickerColour());
|
||||
let newHsv = currColor.hsv;
|
||||
let newHex;
|
||||
|
||||
// Adding slider value to value
|
||||
newHsv = new Color("hsv", newHsv.h, newHsv.s, parseInt(event.target.value));
|
||||
// Updating hex
|
||||
newHex = newHsv.hex;
|
||||
|
||||
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 pickedColour;
|
||||
|
||||
pickedColour = miniPickerCanvas.getContext('2d').getImageData(currPickerIconPos[0][0] + 8,
|
||||
currPickerIconPos[0][1] + 8, 1, 1).data;
|
||||
|
||||
return new Color("rgb", Math.round(pickedColour[0]), Math.round(pickedColour[1]), Math.round(pickedColour[2])).hex;
|
||||
}
|
||||
|
||||
// Update the background gradient of the slider in the minipicker
|
||||
function updateMiniSlider(hex) {
|
||||
let rgb = Color.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%);}";
|
||||
|
||||
styles[1] += "input[type=range]#cp-minipicker-slider::-moz-range-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 = new Color("hex", colourValue.value).hsv;
|
||||
let white = new Color("hsv", hsv.h, 0, parseInt(miniPickerSlider.value)).rgb;
|
||||
|
||||
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++) {
|
||||
let stopHex = new Color("hsv", 60*i, 100, hsv.v);
|
||||
hGrad.addColorStop(i / 6, '#' + stopHex.hex);
|
||||
}
|
||||
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.r +',' + white.g + ',' + white.b + ',0)');
|
||||
vGrad.addColorStop(1, 'rgba(' + white.r +',' + white.g + ',' + white.b + ',1)');
|
||||
|
||||
ctx.fillStyle = vGrad;
|
||||
ctx.fillRect(0, 0, miniPickerCanvas.width, miniPickerCanvas.height);
|
||||
}
|
||||
|
||||
function changePickingMode(newMode, event) {
|
||||
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:
|
||||
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 = new Color("hex", colourValue.value).hex;
|
||||
let currentColourHsv = new Color("hex", currentColorHex).hsv;
|
||||
let newColourHsv;
|
||||
let newColourHexes = ['', '', ''];
|
||||
|
||||
switch (currentPickingMode)
|
||||
{
|
||||
case 'mono':
|
||||
break;
|
||||
case 'analog':
|
||||
// First colour
|
||||
newColourHsv = new Color("hsv", ((currentColourHsv.h + 40) % 360), currentColourHsv.s, currentColourHsv.v);
|
||||
|
||||
currPickerIconPos[1][0] = miniPickerCanvas.width * newColourHsv.hsv.h/360 - 8;
|
||||
currPickerIconPos[1][1] = miniPickerCanvas.height - (miniPickerCanvas.height * newColourHsv.hsv.s/100 + 8);
|
||||
|
||||
newColourHexes[0] = newColourHsv.hex;
|
||||
|
||||
// Second colour
|
||||
newColourHsv = new Color("hsv", ((currentColourHsv.h + 320) % 360), currentColourHsv.s, currentColourHsv.v);
|
||||
|
||||
currPickerIconPos[2][0] = miniPickerCanvas.width * newColourHsv.hsv.h/360 - 8;
|
||||
currPickerIconPos[2][1] = miniPickerCanvas.height - (miniPickerCanvas.height * newColourHsv.hsv.s/100 + 8);
|
||||
|
||||
newColourHexes[1] = newColourHsv.hex;
|
||||
break;
|
||||
case 'cmpt':
|
||||
newColourHsv = new Color("hsv", ((currentColourHsv.h + 180) % 360), currentColourHsv.s, currentColourHsv.v);
|
||||
|
||||
currPickerIconPos[1][0] = miniPickerCanvas.width * newColourHsv.hsv.h/360 - 8;
|
||||
currPickerIconPos[1][1] = miniPickerCanvas.height - (miniPickerCanvas.height * newColourHsv.hsv.s/100 + 8);
|
||||
|
||||
newColourHexes[0] = newColourHsv.hex;
|
||||
break;
|
||||
case 'tri':
|
||||
for (let i=1; i< 3; i++) {
|
||||
newColourHsv = new Color("hsv", (currentColourHsv.h + 120*i) % 360, currentColourHsv.s, currentColourHsv.v);
|
||||
|
||||
currPickerIconPos[i][0] = miniPickerCanvas.width * newColourHsv.hsv.h/360 - 8;
|
||||
currPickerIconPos[i][1] = miniPickerCanvas.height - (miniPickerCanvas.height * newColourHsv.hsv.s/100 + 8);
|
||||
|
||||
newColourHexes[i - 1] = newColourHsv.hex;
|
||||
}
|
||||
|
||||
break
|
||||
case 'scmpt':
|
||||
// First colour
|
||||
newColourHsv = new Color("hsv", (currentColourHsv.h + 210) % 360, currentColourHsv.s, currentColourHsv.v);
|
||||
|
||||
currPickerIconPos[1][0] = miniPickerCanvas.width * newColourHsv.hsv.h/360 - 8;
|
||||
currPickerIconPos[1][1] = miniPickerCanvas.height - (miniPickerCanvas.height * newColourHsv.hsv.s/100 + 8);
|
||||
|
||||
newColourHexes[0] = newColourHsv.hex;
|
||||
|
||||
// Second colour
|
||||
newColourHsv = new Color("hsv", (currentColourHsv.h + 150) % 360, currentColourHsv.s, currentColourHsv.v);
|
||||
|
||||
currPickerIconPos[2][0] = miniPickerCanvas.width * newColourHsv.hsv.h/360 - 8;
|
||||
currPickerIconPos[2][1] = miniPickerCanvas.height - (miniPickerCanvas.height * newColourHsv.hsv.s/100 + 8);
|
||||
|
||||
newColourHexes[1] = newColourHsv.hex;
|
||||
break;
|
||||
case 'tetra':
|
||||
for (let i=1; i< 4; i++) {
|
||||
newColourHsv = new Color("hsv", (currentColourHsv.h + 90*i) % 360, currentColourHsv.s, currentColourHsv.v);
|
||||
|
||||
currPickerIconPos[i][0] = miniPickerCanvas.width * newColourHsv.hsv.h/360 - 8;
|
||||
currPickerIconPos[i][1] = miniPickerCanvas.height - (miniPickerCanvas.height * newColourHsv.hsv.s/100 + 8);
|
||||
|
||||
newColourHexes[i - 1] = newColourHsv.hex;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
init,
|
||||
getSelectedColours,
|
||||
updatePickerByHex,
|
||||
updateSlidersByHex,
|
||||
updateMiniPickerColour
|
||||
}
|
||||
})();
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
/** Handles the pop up windows (NewPixel, ResizeCanvas ecc)
|
||||
*
|
||||
*/
|
||||
const Dialogue = (() => {
|
||||
let currentOpenDialogue = "";
|
||||
let dialogueOpen = true;
|
||||
|
||||
const popUpContainer = document.getElementById("pop-up-container");
|
||||
const cancelButtons = popUpContainer.getElementsByClassName('close-button');
|
||||
|
||||
Events.onCustom("esc-pressed", closeDialogue);
|
||||
|
||||
// Add click handlers for all cancel buttons
|
||||
for (var i = 0; i < cancelButtons.length; i++) {
|
||||
cancelButtons[i].addEventListener('click', function () {
|
||||
closeDialogue();
|
||||
});
|
||||
}
|
||||
|
||||
/** Closes a dialogue window if the user clicks everywhere but in the current window
|
||||
*
|
||||
*/
|
||||
popUpContainer.addEventListener('click', function (e) {
|
||||
if (e.target == popUpContainer)
|
||||
closeDialogue();
|
||||
});
|
||||
|
||||
/** 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' && EditorState.documentCreated()) {
|
||||
ColorPicker.init();
|
||||
PaletteBlock.init();
|
||||
}
|
||||
|
||||
//track google event
|
||||
if (trackEvent && typeof ga !== 'undefined')
|
||||
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") {
|
||||
ColorModule.addToSimplePalette();
|
||||
}
|
||||
}
|
||||
|
||||
function isOpen() {
|
||||
return dialogueOpen;
|
||||
}
|
||||
|
||||
return {
|
||||
showDialogue,
|
||||
closeDialogue,
|
||||
isOpen
|
||||
}
|
||||
})();
|
||||
|
||||
console.log("Dialog: " + Dialogue);
|
|
@ -0,0 +1,79 @@
|
|||
const EditorState = (() => {
|
||||
let pixelEditorMode = "Basic";
|
||||
let firstFile = true;
|
||||
|
||||
Events.on('click', 'switch-editor-mode-splash', chooseMode);
|
||||
Events.on('click', 'switch-mode-button', toggleMode);
|
||||
|
||||
function getCurrentMode() {
|
||||
return pixelEditorMode;
|
||||
}
|
||||
|
||||
function switchMode(newMode) {
|
||||
if (!firstFile && newMode == "Basic" && !confirm('Switching to basic mode will flatten all the visible layers. Are you sure you want to continue?')) {
|
||||
return;
|
||||
}
|
||||
//switch to advanced mode
|
||||
if (newMode == 'Advanced') {
|
||||
Events.emit("switchedToAdvanced");
|
||||
// Hide the palette menu
|
||||
document.getElementById('colors-menu').style.right = '200px'
|
||||
|
||||
pixelEditorMode = 'Advanced';
|
||||
document.getElementById("switch-mode-button").innerHTML = 'Switch to basic mode';
|
||||
}
|
||||
//switch to basic mode
|
||||
else {
|
||||
Events.emit("switchedToBasic");
|
||||
// Show the palette menu
|
||||
document.getElementById('colors-menu').style.display = 'flex';
|
||||
// Move the palette menu
|
||||
document.getElementById('colors-menu').style.right = '0px';
|
||||
|
||||
pixelEditorMode = 'Basic';
|
||||
document.getElementById("switch-mode-button").innerHTML = 'Switch to advanced mode';
|
||||
}
|
||||
}
|
||||
|
||||
function chooseMode() {
|
||||
let prevMode = pixelEditorMode.toLowerCase();
|
||||
|
||||
if (pixelEditorMode === "Basic") {
|
||||
pixelEditorMode = "Advanced";
|
||||
}
|
||||
else {
|
||||
pixelEditorMode = "Basic";
|
||||
}
|
||||
|
||||
//change splash text
|
||||
document.querySelector('#sp-quickstart-container .mode-switcher').classList.remove(prevMode + '-mode');
|
||||
document.querySelector('#sp-quickstart-container .mode-switcher').classList.add(pixelEditorMode.toLowerCase() + '-mode');
|
||||
}
|
||||
|
||||
function toggleMode() {
|
||||
if (pixelEditorMode == 'Advanced')
|
||||
switchMode('Basic');
|
||||
else
|
||||
switchMode('Advanced');
|
||||
}
|
||||
|
||||
function documentCreated() {
|
||||
return !firstFile;
|
||||
}
|
||||
|
||||
function firstPixel() {
|
||||
return firstFile;
|
||||
}
|
||||
|
||||
function created() {
|
||||
firstFile = false;
|
||||
}
|
||||
|
||||
return {
|
||||
getCurrentMode,
|
||||
switchMode,
|
||||
documentCreated,
|
||||
created,
|
||||
firstPixel
|
||||
}
|
||||
})();
|
|
@ -0,0 +1,160 @@
|
|||
const Events = (() => {
|
||||
let customCallback = {};
|
||||
|
||||
/** 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";
|
||||
|
||||
keyboardEvent[initMethod](
|
||||
"keydown", // event type: keydown, keyup, keypress
|
||||
true, // bubbles
|
||||
true, // cancelable
|
||||
window, // view: should be window
|
||||
ctrl, // ctrlKey
|
||||
alt, // altKey
|
||||
shift, // shiftKey
|
||||
false, // metaKey
|
||||
keyCode, // keyCode: unsigned long - the virtual key code, else 0
|
||||
keyCode // charCode: unsigned long - the Unicode character associated with the depressed key, else 0
|
||||
);
|
||||
document.dispatchEvent(keyboardEvent);
|
||||
}
|
||||
|
||||
/** Simulates a mouse event (https://stackoverflow.com/questions/6157929/how-to-simulate-a-mouse-click-using-javascript)
|
||||
*
|
||||
* @param {*} element The element that triggered the event
|
||||
* @param {*} eventName The name of the event
|
||||
* @returns
|
||||
*/
|
||||
function simulateMouseEvent (element, eventName)
|
||||
{
|
||||
function extend(destination, source) {
|
||||
for (let property in source)
|
||||
destination[property] = source[property];
|
||||
return destination;
|
||||
}
|
||||
|
||||
let eventMatchers = {
|
||||
'HTMLEvents': /^(?:load|unload|abort|error|select|change|submit|reset|focus|blur|resize|scroll)$/,
|
||||
'MouseEvents': /^(?:click|dblclick|mouse(?:down|up|over|move|out))$/
|
||||
}
|
||||
let defaultOptions = {
|
||||
pointerX: 0,
|
||||
pointerY: 0,
|
||||
button: 0,
|
||||
ctrlKey: false,
|
||||
altKey: false,
|
||||
shiftKey: false,
|
||||
metaKey: false,
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
}
|
||||
|
||||
let options = extend(defaultOptions, arguments[2] || {});
|
||||
let oEvent, eventType = null;
|
||||
|
||||
for (let name in eventMatchers)
|
||||
if (eventMatchers[name].test(eventName)) { eventType = name; break; }
|
||||
|
||||
if (!eventType)
|
||||
throw new SyntaxError('Only HTMLEvents and MouseEvents interfaces are supported');
|
||||
|
||||
if (document.createEvent) {
|
||||
oEvent = document.createEvent(eventType);
|
||||
if (eventType == 'HTMLEvents')
|
||||
{
|
||||
oEvent.initEvent(eventName, options.bubbles, options.cancelable);
|
||||
}
|
||||
else
|
||||
{
|
||||
oEvent.initMouseEvent(eventName, options.bubbles, options.cancelable, document.defaultView,
|
||||
options.button, options.pointerX, options.pointerY, options.pointerX, options.pointerY,
|
||||
options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, element);
|
||||
}
|
||||
element.dispatchEvent(oEvent);
|
||||
}
|
||||
else {
|
||||
options.clientX = options.pointerX;
|
||||
options.clientY = options.pointerY;
|
||||
var evt = document.createEventObject();
|
||||
oEvent = extend(evt, options);
|
||||
element.fireEvent('on' + eventName, oEvent);
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
/** Register a callback for a certain window event
|
||||
*
|
||||
* @param {*} event The event to register to
|
||||
* @param {*} elementId The id of the element that will listen to the event
|
||||
* @param {*} functionCallback The function to callback when the event is shoot
|
||||
* @param {...any} args Arguments for the callback
|
||||
*/
|
||||
function on(event, elementId, functionCallback, ...args) {
|
||||
//if element provided is string, get the actual element
|
||||
const element = Util.getElement(elementId);
|
||||
|
||||
element.addEventListener(event,
|
||||
function (e) {
|
||||
functionCallback(...args, e);
|
||||
});
|
||||
}
|
||||
|
||||
/** Register a callback for a certain window event that is shot by the children of
|
||||
* an element passed as an argument
|
||||
*
|
||||
* @param {*} event The event to register to
|
||||
* @param {*} elementId The id of the element whose children will listen to the event
|
||||
* @param {*} functionCallback The function to callback when the event is shoot
|
||||
* @param {...any} args Arguments for the callback
|
||||
*/
|
||||
function onChildren(event, parentElement, functionCallback, ...args) {
|
||||
parentElement = Util.getElement(parentElement);
|
||||
const children = parentElement.children;
|
||||
|
||||
//loop through children and add onClick listener
|
||||
for (var i = 0; i < children.length; i++) {
|
||||
on(event, children[i], functionCallback, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
/** Registers a callback for a custom (non HTML) event
|
||||
*
|
||||
* @param {*} event The event to register to
|
||||
* @param {*} functionCallback The function to call
|
||||
*/
|
||||
function onCustom(event, functionCallback) {
|
||||
if (customCallback[event] === undefined)
|
||||
customCallback[event] = [];
|
||||
|
||||
customCallback[event].push(functionCallback);
|
||||
}
|
||||
|
||||
/** Emits a custom (non HTML) event
|
||||
*
|
||||
* @param {*} event The event to emit
|
||||
* @param {...any} args The arguments for that event
|
||||
*/
|
||||
function emit(event, ...args) {
|
||||
if (customCallback[event] != undefined)
|
||||
for (let i=0; i<customCallback[event].length; i++)
|
||||
customCallback[event][i](args);
|
||||
}
|
||||
|
||||
return {
|
||||
simulateInput,
|
||||
simulateMouseEvent,
|
||||
on,
|
||||
onChildren,
|
||||
onCustom,
|
||||
emit
|
||||
}
|
||||
})();
|
|
@ -0,0 +1,692 @@
|
|||
class File {
|
||||
// Canvas, canvas state
|
||||
canvasSize = [];
|
||||
zoom = 7;
|
||||
canvasView = document.getElementById("canvas-view");
|
||||
|
||||
// Layers
|
||||
layers = [];
|
||||
currentLayer = undefined;
|
||||
VFXLayer = undefined;
|
||||
TMPLayer = undefined;
|
||||
pixelGrid = undefined;
|
||||
checkerBoard = undefined
|
||||
|
||||
// Canvas resize attributes
|
||||
// Resize canvas pop up window
|
||||
resizeCanvasContainer = document.getElementById("resize-canvas");
|
||||
// Start pivot
|
||||
rcPivot = "middle";
|
||||
// Selected pivot button
|
||||
currentPivotObject = undefined;
|
||||
// Border offsets
|
||||
rcBorders = {left: 0, right: 0, top: 0, bottom: 0};
|
||||
|
||||
// Sprite scaling attributes
|
||||
// Should I keep the sprite ratio?
|
||||
keepRatio = true;
|
||||
// Used to store the current ratio
|
||||
currentRatio = undefined;
|
||||
// The currenty selected resizing algorithm (nearest-neighbor or bilinear-interpolation)
|
||||
currentAlgo = 'nearest-neighbor';
|
||||
// Current resize data
|
||||
data = {width: 0, height: 0, widthPercentage: 100, heightPercentage: 100};
|
||||
// Start resize data
|
||||
startData = {width: 0, height:0, widthPercentage: 100, heightPercentage: 100};
|
||||
|
||||
// Sprite scaling attributes
|
||||
|
||||
openResizeCanvasWindow() {
|
||||
// Initializes the inputs
|
||||
this.initResizeCanvasInputs();
|
||||
Dialogue.showDialogue('resize-canvas');
|
||||
}
|
||||
|
||||
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++) {
|
||||
Events.on("click", buttons[i], this.changePivot.bind(this));
|
||||
if (buttons[i].getAttribute("value").includes("middle")) {
|
||||
this.currentPivotObject = buttons[i];
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("rc-width").value = currFile.canvasSize[0];
|
||||
document.getElementById("rc-height").value = currFile.canvasSize[1];
|
||||
|
||||
Events.on("change", "rc-border-left", this.rcChangedBorder.bind(this));
|
||||
Events.on("change", "rc-border-right", this.rcChangedBorder.bind(this));
|
||||
Events.on("change", "rc-border-top", this.rcChangedBorder.bind(this));
|
||||
Events.on("change", "rc-border-bottom", this.rcChangedBorder.bind(this));
|
||||
|
||||
Events.on("change", "rc-width", this.rcChangedSize.bind(this));
|
||||
Events.on("change", "rc-height", this.rcChangedSize.bind(this));
|
||||
|
||||
Events.on("click", "resize-canvas-confirm", this.resizeCanvas.bind(this));
|
||||
}
|
||||
|
||||
/** Fired when a border offset is changed: it updates the width and height
|
||||
*/
|
||||
rcChangedBorder() {
|
||||
this.rcUpdateBorders();
|
||||
|
||||
document.getElementById("rc-width").value = parseInt(currFile.canvasSize[0]) +
|
||||
this.rcBorders.left + this.rcBorders.right;
|
||||
document.getElementById("rc-height").value = parseInt(currFile.canvasSize[1]) +
|
||||
this.rcBorders.top + this.rcBorders.bottom;
|
||||
}
|
||||
|
||||
/** Fired when width or height are changed: updates the border offsets
|
||||
*/
|
||||
rcChangedSize() {
|
||||
let widthOffset = Math.abs(document.getElementById("rc-width").value) - currFile.canvasSize[0];
|
||||
let heightOffset = Math.abs(document.getElementById("rc-height").value) - currFile.canvasSize[1];
|
||||
|
||||
let left = Math.round(widthOffset / 2);
|
||||
let right = widthOffset - left;
|
||||
let top = Math.round(heightOffset / 2);
|
||||
let bottom = heightOffset - top;
|
||||
|
||||
document.getElementById("rc-border-left").value = left;
|
||||
document.getElementById("rc-border-right").value = right;
|
||||
document.getElementById("rc-border-top").value = top;
|
||||
document.getElementById("rc-border-bottom").value = bottom;
|
||||
|
||||
this.rcBorders.left = left;
|
||||
this.rcBorders.right = right;
|
||||
this.rcBorders.top = top;
|
||||
this.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
|
||||
*/
|
||||
resizeCanvas(event, size, customData, saveHistory = true) {
|
||||
let imageDatas = [];
|
||||
let leftOffset = 0;
|
||||
let topOffset = 0;
|
||||
let copiedDataIndex = 0;
|
||||
|
||||
// If I'm undoing and I'm not trimming, I manually put the values in the window
|
||||
if (size != null && customData == null) {
|
||||
document.getElementById("rc-width").value = size.x;
|
||||
document.getElementById("rc-height").value = size.y;
|
||||
|
||||
this.rcChangedSize();
|
||||
}
|
||||
|
||||
this.rcUpdateBorders();
|
||||
|
||||
// Save all imageDatas
|
||||
for (let i=0; i<currFile.layers.length; i++) {
|
||||
if (currFile.layers[i].hasCanvas()) {
|
||||
imageDatas.push(currFile.layers[i].context.getImageData(
|
||||
0, 0, currFile.canvasSize[0], currFile.canvasSize[1])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Saving the history only if I'm not already undoing or redoing
|
||||
if (saveHistory && event != null) {
|
||||
// Saving history
|
||||
new HistoryState().ResizeCanvas(
|
||||
{x: parseInt(currFile.canvasSize[0]) + this.rcBorders.left + this.rcBorders.right,
|
||||
y: parseInt(currFile.canvasSize[1]) + this.rcBorders.top + this.rcBorders.bottom},
|
||||
|
||||
{x: currFile.canvasSize[0],
|
||||
y: currFile.canvasSize[1]},
|
||||
imageDatas.slice(), customData != null && saveHistory
|
||||
);
|
||||
}
|
||||
|
||||
currFile.canvasSize[0] = parseInt(currFile.canvasSize[0]) +
|
||||
this.rcBorders.left + this.rcBorders.right;
|
||||
currFile.canvasSize[1] = parseInt(currFile.canvasSize[1]) +
|
||||
this.rcBorders.top + this.rcBorders.bottom;
|
||||
|
||||
// Resize the canvases
|
||||
for (let i=0; i<currFile.layers.length; i++) {
|
||||
currFile.layers[i].canvas.width = currFile.canvasSize[0];
|
||||
currFile.layers[i].canvas.height = currFile.canvasSize[1];
|
||||
|
||||
currFile.layers[i].resize();
|
||||
}
|
||||
|
||||
// Regenerate the checkerboard
|
||||
currFile.checkerBoard.fillCheckerboard();
|
||||
currFile.pixelGrid.fillPixelGrid();
|
||||
// Put the imageDatas in the right position
|
||||
switch (this.rcPivot)
|
||||
{
|
||||
case 'topleft':
|
||||
leftOffset = 0;
|
||||
topOffset = 0;
|
||||
break;
|
||||
case 'top':
|
||||
leftOffset = (this.rcBorders.left + this.rcBorders.right) / 2;
|
||||
topOffset = 0;
|
||||
break;
|
||||
case 'topright':
|
||||
leftOffset = this.rcBorders.left + this.rcBorders.right;
|
||||
topOffset = 0;
|
||||
break;
|
||||
case 'left':
|
||||
leftOffset = 0;
|
||||
topOffset = (this.rcBorders.top + this.rcBorders.bottom) / 2;
|
||||
break;
|
||||
case 'middle':
|
||||
leftOffset = (this.rcBorders.left + this.rcBorders.right) / 2;
|
||||
topOffset = (this.rcBorders.top + this.rcBorders.bottom) / 2;
|
||||
break;
|
||||
case 'right':
|
||||
leftOffset = this.rcBorders.left + this.rcBorders.right;
|
||||
topOffset = (this.rcBorders.top + this.rcBorders.bottom) / 2;
|
||||
break;
|
||||
case 'bottomleft':
|
||||
leftOffset = 0;
|
||||
topOffset = this.rcBorders.top + this.rcBorders.bottom;
|
||||
break;
|
||||
case 'bottom':
|
||||
leftOffset = (this.rcBorders.left + this.rcBorders.right) / 2;
|
||||
topOffset = this.rcBorders.top + this.rcBorders.bottom;
|
||||
break;
|
||||
case 'bottomright':
|
||||
leftOffset = this.rcBorders.left + this.rcBorders.right;
|
||||
topOffset = this.rcBorders.top + this.rcBorders.bottom;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Putting all the data for each layer with the right offsets (decided by the pivot)
|
||||
for (let i=0; i<currFile.layers.length; i++) {
|
||||
if (currFile.layers[i].hasCanvas()) {
|
||||
if (customData == undefined) {
|
||||
currFile.layers[i].context.putImageData(imageDatas[copiedDataIndex], leftOffset, topOffset);
|
||||
}
|
||||
else {
|
||||
currFile.layers[i].context.putImageData(customData[copiedDataIndex], 0, 0);
|
||||
}
|
||||
currFile.layers[i].updateLayerPreview();
|
||||
copiedDataIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
Dialogue.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
|
||||
*/
|
||||
trimCanvas(event, saveHistory) {
|
||||
let minY = Infinity;
|
||||
let minX = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let maxY = -Infinity;
|
||||
let tmp;
|
||||
let imageDatas = [];
|
||||
let historySave = saveHistory == null;
|
||||
let prevPivot = rcPivot;
|
||||
|
||||
this.rcPivot = "topleft";
|
||||
|
||||
// Computing the min and max coordinates in which there's a non empty pixel
|
||||
for (let i=1; i<currFile.layers.length - nAppLayers; i++) {
|
||||
let imageData = currFile.layers[i].context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
|
||||
let pixelPosition;
|
||||
|
||||
for (let i=imageData.data.length - 1; i>= 0; i-=4) {
|
||||
if (!Util.isPixelEmpty(
|
||||
[imageData.data[i - 3], imageData.data[i - 2],
|
||||
-imageData.data[i - 1], imageData.data[i]])) {
|
||||
pixelPosition = getPixelPosition(i);
|
||||
|
||||
// max x
|
||||
if (pixelPosition[0] > maxX) {
|
||||
maxX = pixelPosition[0];
|
||||
}
|
||||
// min x
|
||||
if (pixelPosition[0] < minX) {
|
||||
minX = pixelPosition[0];
|
||||
}
|
||||
// max y
|
||||
if (pixelPosition[1] > maxY) {
|
||||
maxY = pixelPosition[1];
|
||||
}
|
||||
// min y
|
||||
if (pixelPosition[1] < minY) {
|
||||
minY = pixelPosition[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tmp = minY;
|
||||
minY = maxY;
|
||||
maxY = tmp;
|
||||
|
||||
minY = currFile.canvasSize[1] - minY;
|
||||
maxY = currFile.canvasSize[1] - maxY;
|
||||
|
||||
// Setting the borders coherently with the values I've just computed
|
||||
this.rcBorders.right = (maxX - currFile.canvasSize[0]) + 1;
|
||||
this.rcBorders.left = -minX;
|
||||
this.rcBorders.top = maxY - currFile.canvasSize[1] + 1;
|
||||
this.rcBorders.bottom = -minY;
|
||||
|
||||
// Saving the data
|
||||
for (let i=0; i<currFile.layers.length; i++) {
|
||||
if (currFile.layers[i].hasCanvas()) {
|
||||
imageDatas.push(currFile.layers[i].context.getImageData(minX - 1, currFile.layers[i].canvasSize[1] - maxY, maxX-minX + 1, maxY-minY + 1));
|
||||
}
|
||||
}
|
||||
|
||||
//console.log("sx: " + borders.left + "dx: " + borders.right + "top: " + borders.top + "btm: " + borders.bottom);
|
||||
|
||||
document.getElementById("rc-border-left").value = this.rcBorders.left;
|
||||
document.getElementById("rc-border-right").value = this.rcBorders.right;
|
||||
document.getElementById("rc-border-top").value = this.rcBorders.top;
|
||||
document.getElementById("rc-border-bottom").value = this.rcBorders.bottom;
|
||||
|
||||
// Resizing the canvas with the decided border offsets
|
||||
this.resizeCanvas(null, null, imageDatas.slice(), historySave);
|
||||
// Resetting the previous pivot
|
||||
this.rcPivot = prevPivot;
|
||||
}
|
||||
|
||||
rcUpdateBorders() {
|
||||
// Getting input
|
||||
this.rcBorders.left = document.getElementById("rc-border-left").value;
|
||||
this.rcBorders.right = document.getElementById("rc-border-right").value;
|
||||
this.rcBorders.top = document.getElementById("rc-border-top").value;
|
||||
this.rcBorders.bottom = document.getElementById("rc-border-bottom").value;
|
||||
|
||||
// Validating input
|
||||
this.rcBorders.left == "" ? this.rcBorders.left = 0 : this.rcBorders.left = Math.round(parseInt(this.rcBorders.left));
|
||||
this.rcBorders.right == "" ? this.rcBorders.right = 0 : this.rcBorders.right = Math.round(parseInt(this.rcBorders.right));
|
||||
this.rcBorders.top == "" ? this.rcBorders.top = 0 : this.rcBorders.top = Math.round(parseInt(this.rcBorders.top));
|
||||
this.rcBorders.bottom == "" ? this.rcBorders.bottom = 0 : this.rcBorders.bottom = Math.round(parseInt(this.rcBorders.bottom));
|
||||
}
|
||||
|
||||
changePivot(event) {
|
||||
this.rcPivot = event.target.getAttribute("value");
|
||||
|
||||
// Setting the selected class
|
||||
this.currentPivotObject.classList.remove("rc-selected-pivot");
|
||||
this.currentPivotObject = event.target;
|
||||
this.currentPivotObject.classList.add("rc-selected-pivot");
|
||||
}
|
||||
|
||||
/** Opens the sprite resizing window
|
||||
*
|
||||
*/
|
||||
openResizeSpriteWindow() {
|
||||
// Inits the sprie resize inputs
|
||||
this.initResizeSpriteInputs();
|
||||
|
||||
// Computing the current ratio
|
||||
this.currentRatio = currFile.canvasSize[0] / currFile.canvasSize[1];
|
||||
|
||||
// Initializing the input fields
|
||||
this.data.width = currFile.canvasSize[0];
|
||||
this.data.height = currFile.canvasSize[1];
|
||||
|
||||
this.startData.width = parseInt(this.data.width);
|
||||
this.startData.height = parseInt(this.data.height);
|
||||
this.startData.heightPercentage = 100;
|
||||
this.startData.widthPercentage = 100;
|
||||
|
||||
// Opening the pop up now that it's ready
|
||||
Dialogue.showDialogue('resize-sprite');
|
||||
}
|
||||
|
||||
/** Initalizes the input values and binds the elements to their events
|
||||
*
|
||||
*/
|
||||
initResizeSpriteInputs() {
|
||||
document.getElementById("rs-width").value = currFile.canvasSize[0];
|
||||
document.getElementById("rs-height").value = currFile.canvasSize[1];
|
||||
|
||||
document.getElementById("rs-width-percentage").value = 100;
|
||||
document.getElementById("rs-height-percentage").value = 100;
|
||||
|
||||
document.getElementById("rs-keep-ratio").checked = true;
|
||||
|
||||
Events.on("change", "rs-width", this.changedWidth.bind(this));
|
||||
Events.on("change", "rs-height", this.changedHeight.bind(this));
|
||||
|
||||
Events.on("change", "rs-width-percentage", this.changedWidthPercentage.bind(this));
|
||||
Events.on("change", "rs-height-percentage", this.changedHeightPercentage.bind(this));
|
||||
|
||||
Events.on("click", "resize-sprite-confirm", this.resizeSprite.bind(this));
|
||||
Events.on("click", "rs-keep-ratio", this.toggleRatio.bind(this));
|
||||
Events.on("change", "resize-algorithm-combobox", this.changedAlgorithm.bind(this));
|
||||
}
|
||||
|
||||
/** Resizes (scales) the sprite
|
||||
*
|
||||
* @param {*} event
|
||||
* @param {*} ratio Keeps infos about the x ratio and y ratio
|
||||
*/
|
||||
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 = currFile.canvasSize[0];
|
||||
oldHeight = currFile.canvasSize[1];
|
||||
this.rcPivot = "middle";
|
||||
|
||||
// Updating values if the user didn't press enter
|
||||
switch (document.activeElement.id) {
|
||||
case "rs-width-percentage":
|
||||
this.changedWidthPercentage();
|
||||
break;
|
||||
case "rs-width":
|
||||
this.changedWidth();
|
||||
break;
|
||||
case "rs-height-percentage":
|
||||
this.changedHeightPercentage();
|
||||
break;
|
||||
case "rs-height":
|
||||
this.changedHeight();
|
||||
break;
|
||||
default:
|
||||
// In this case everything has been updated correctly
|
||||
break;
|
||||
}
|
||||
|
||||
// Computing newWidth and newHeight
|
||||
if (ratio == null) {
|
||||
newWidth = this.data.width;
|
||||
newHeight = this.data.height;
|
||||
}
|
||||
else {
|
||||
newWidth = currFile.canvasSize[0] * ratio[0];
|
||||
newHeight = currFile.canvasSize[1] * ratio[1];
|
||||
}
|
||||
|
||||
// Get all the image datas
|
||||
for (let i=0; i<currFile.layers.length; i++) {
|
||||
if (currFile.layers[i].hasCanvas()) {
|
||||
rsImageDatas.push(currFile.layers[i].context.getImageData(
|
||||
0, 0, currFile.canvasSize[0], currFile.canvasSize[1])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// event is null when the user is undoing
|
||||
if (event != null) {
|
||||
// Copying the image data
|
||||
imageDatasCopy = rsImageDatas.slice();
|
||||
// Saving the history
|
||||
new HistoryState().ResizeSprite(newWidth / oldWidth, newHeight / oldHeight, this.currentAlgo, imageDatasCopy);
|
||||
}
|
||||
|
||||
// Resizing the canvas
|
||||
currFile.resizeCanvas(null, {x: newWidth, y: newHeight});
|
||||
|
||||
// Put the image datas on the new canvases
|
||||
for (let i=0; i<currFile.layers.length; i++) {
|
||||
if (currFile.layers[i].hasCanvas()) {
|
||||
currFile.layers[i].context.putImageData(
|
||||
this.resizeImageData(rsImageDatas[layerIndex], newWidth, newHeight, this.currentAlgo), 0, 0
|
||||
);
|
||||
currFile.layers[i].updateLayerPreview();
|
||||
layerIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
// Updating start values when I finish scaling the sprite
|
||||
// OPTIMIZABLE? Can't I just assign data to startData? Is js smart enough to understand?
|
||||
if (ratio == null) {
|
||||
this.startData.width = this.data.width;
|
||||
this.startData.height = this.data.height;
|
||||
}
|
||||
else {
|
||||
this.startData.width = currFile.canvasSize[0];
|
||||
this.startData.height = currFile.canvasSize[1];
|
||||
}
|
||||
|
||||
this.startData.widthPercentage = 100;
|
||||
this.startData.heightPercentage = 100;
|
||||
|
||||
Dialogue.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
|
||||
*/
|
||||
changedWidth(event) {
|
||||
let newHeight, newHeightPerc, newWidthPerc;
|
||||
this.data.width = event.target.value;
|
||||
|
||||
newHeight = this.data.width / this.currentRatio;
|
||||
newHeightPerc = (newHeight * 100) / this.startData.height;
|
||||
newWidthPerc = (this.data.width * 100) / this.startData.width;
|
||||
|
||||
if (this.keepRatio) {
|
||||
document.getElementById("rs-height").value = newHeight;
|
||||
this.data.height = newHeight;
|
||||
|
||||
document.getElementById("rs-height-percentage").value = newHeightPerc;
|
||||
this.data.heightPercentage = newHeightPerc;
|
||||
}
|
||||
|
||||
document.getElementById("rs-width-percentage").value = newWidthPerc;
|
||||
}
|
||||
|
||||
/**Fired when the input field for width is changed. Updates the other input fields consequently
|
||||
*
|
||||
* @param {*} event
|
||||
*/
|
||||
changedHeight(event) {
|
||||
let newWidth, newWidthPerc, newHeightPerc;
|
||||
this.data.height = event.target.value;
|
||||
|
||||
newWidth = this.data.height * this.currentRatio;
|
||||
newWidthPerc = (newWidth * 100) / this.startData.width;
|
||||
newHeightPerc = (this.data.height * 100) / this.startData.height;
|
||||
|
||||
if (this.keepRatio) {
|
||||
document.getElementById("rs-width").value = newWidth;
|
||||
this.data.width = newWidth;
|
||||
|
||||
document.getElementById("rs-width-percentage").value = newWidthPerc;
|
||||
this.data.widthPercentage = newWidthPerc;
|
||||
}
|
||||
|
||||
document.getElementById("rs-height-percentage").value = newHeightPerc;
|
||||
this.data.heightPercentage = newHeightPerc;
|
||||
}
|
||||
|
||||
/**Fired when the input field for width percentage is changed. Updates the other input fields consequently
|
||||
*
|
||||
* @param {*} event
|
||||
*/
|
||||
changedWidthPercentage(event) {
|
||||
let oldValue = 100;
|
||||
let ratio;
|
||||
let newWidth, newHeight, newHeightPerc;
|
||||
|
||||
this.data.widthPercentage = event.target.value;
|
||||
ratio = this.data.widthPercentage / oldValue;
|
||||
|
||||
newHeight = this.startData.height * ratio;
|
||||
newHeightPerc = this.data.widthPercentage;
|
||||
newWidth = this.startData.width * ratio;
|
||||
|
||||
if (this.keepRatio) {
|
||||
document.getElementById("rs-height-percentage").value = newHeightPerc;
|
||||
this.data.heightPercentage = newHeightPerc;
|
||||
|
||||
document.getElementById("rs-height").value = newHeight
|
||||
this.data.height = newHeight;
|
||||
}
|
||||
|
||||
document.getElementById("rs-width").value = newWidth;
|
||||
this.data.width = newWidth;
|
||||
}
|
||||
|
||||
/**Fired when the input field for height percentage is changed. Updates the other input fields consequently
|
||||
*
|
||||
* @param {*} event
|
||||
*/
|
||||
changedHeightPercentage(event) {
|
||||
let oldValue = this.data.heightPercentage;
|
||||
let ratio;
|
||||
let newHeight, newWidth, newWidthPerc;
|
||||
|
||||
this.data.heightPercentage = event.target.value;
|
||||
|
||||
ratio = this.data.heightPercentage / oldValue;
|
||||
|
||||
newWidth = this.startData.width * ratio;
|
||||
newWidthPerc = this.data.heightPercentage;
|
||||
newHeight = this.startData.height * ratio;
|
||||
|
||||
if (this.keepRatio) {
|
||||
document.getElementById("rs-width-percentage").value = this.data.heightPercentage * currentRatio;
|
||||
this.data.widthPercentage = newWidthPerc;
|
||||
|
||||
document.getElementById("rs-width").value = newWidth;
|
||||
this.data.width = newWidth;
|
||||
}
|
||||
|
||||
document.getElementById("rs-height").value = newHeight;
|
||||
this.data.height = newHeight;
|
||||
}
|
||||
|
||||
/** Toggles the keepRatio value (fired by the checkbox in the pop up window)
|
||||
*/
|
||||
toggleRatio() {
|
||||
this.keepRatio = !this.keepRatio;
|
||||
}
|
||||
|
||||
/** Changes the scaling algorithm (fired by the combobox in the pop up window)
|
||||
*
|
||||
* @param {*} event
|
||||
*/
|
||||
changedAlgorithm(event) {
|
||||
this.currentAlgo = event.target.value;
|
||||
}
|
||||
|
||||
/** 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
|
||||
*/
|
||||
resizeImageData (image, width, height, algorithm) {
|
||||
algorithm = algorithm || 'bilinear-interpolation'
|
||||
|
||||
let resize;
|
||||
switch (algorithm) {
|
||||
case 'nearest-neighbor': resize = this.nearestNeighbor; break
|
||||
case 'bilinear-interpolation': resize = this.bilinearInterpolation; break
|
||||
default: return image;
|
||||
}
|
||||
|
||||
const result = new ImageData(width, height)
|
||||
|
||||
resize(image, result)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
/** Nearest neighbor algorithm to scale a sprite
|
||||
*
|
||||
* @param {*} src The source imageData
|
||||
* @param {*} dst The destination imageData
|
||||
*/
|
||||
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)
|
||||
const srcY = Math.floor(y * src.height / dst.height)
|
||||
|
||||
let srcPos = ((srcY * src.width) + srcX) * 4
|
||||
|
||||
dst.data[pos++] = src.data[srcPos++] // R
|
||||
dst.data[pos++] = src.data[srcPos++] // G
|
||||
dst.data[pos++] = src.data[srcPos++] // B
|
||||
dst.data[pos++] = src.data[srcPos++] // A
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Bilinear interpolation used to scale a sprite
|
||||
*
|
||||
* @param {*} src The source imageData
|
||||
* @param {*} dst The destination imageData
|
||||
*/
|
||||
bilinearInterpolation (src, dst) {
|
||||
// Applying the bilinear interpolation algorithm
|
||||
|
||||
function interpolate (k, kMin, kMax, vMin, vMax) {
|
||||
return Math.round((k - kMin) * vMax + (kMax - k) * vMin)
|
||||
}
|
||||
|
||||
function interpolateHorizontal (offset, x, y, xMin, xMax) {
|
||||
const vMin = src.data[((y * src.width + xMin) * 4) + offset]
|
||||
if (xMin === xMax) return vMin
|
||||
|
||||
const vMax = src.data[((y * src.width + xMax) * 4) + offset]
|
||||
return interpolate(x, xMin, xMax, vMin, vMax)
|
||||
}
|
||||
|
||||
function interpolateVertical (offset, x, xMin, xMax, y, yMin, yMax) {
|
||||
const vMin = interpolateHorizontal(offset, x, yMin, xMin, xMax)
|
||||
if (yMin === yMax) return vMin
|
||||
|
||||
const vMax = interpolateHorizontal(offset, x, yMax, xMin, xMax)
|
||||
return interpolate(y, yMin, yMax, vMin, vMax)
|
||||
}
|
||||
|
||||
let pos = 0
|
||||
|
||||
for (let y = 0; y < dst.height; y++) {
|
||||
for (let x = 0; x < dst.width; x++) {
|
||||
const srcX = x * src.width / dst.width
|
||||
const srcY = y * src.height / dst.height
|
||||
|
||||
const xMin = Math.floor(srcX)
|
||||
const yMin = Math.floor(srcY)
|
||||
|
||||
const xMax = Math.min(Math.ceil(srcX), src.width - 1)
|
||||
const yMax = Math.min(Math.ceil(srcY), src.height - 1)
|
||||
|
||||
dst.data[pos++] = interpolateVertical(0, srcX, xMin, xMax, srcY, yMin, yMax) // R
|
||||
dst.data[pos++] = interpolateVertical(1, srcX, xMin, xMax, srcY, yMin, yMax) // G
|
||||
dst.data[pos++] = interpolateVertical(2, srcX, xMin, xMax, srcY, yMin, yMax) // B
|
||||
dst.data[pos++] = interpolateVertical(3, srcX, xMin, xMax, srcY, yMin, yMax) // A
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let currFile = new File();
|
|
@ -0,0 +1,279 @@
|
|||
const FileManager = (() => {
|
||||
|
||||
// Binding the browse holder change event to file loading
|
||||
const browseHolder = document.getElementById('open-image-browse-holder');
|
||||
const browsePaletteHolder = document.getElementById('load-palette-browse-holder');
|
||||
|
||||
Events.on('change', browseHolder, loadFile);
|
||||
Events.on('change', browsePaletteHolder, loadPalette);
|
||||
|
||||
function openSaveProjectWindow() {
|
||||
//create name
|
||||
let selectedPalette = Util.getText('palette-button');
|
||||
|
||||
if (selectedPalette != 'Choose a palette...'){
|
||||
var paletteAbbreviation = palettes[selectedPalette].abbreviation;
|
||||
var fileName = 'pixel-'+paletteAbbreviation+'-'+currFile.canvasSize[0]+'x'+currFile.canvasSize[1];
|
||||
} else {
|
||||
var fileName = 'pixel-'+currFile.canvasSize[0]+'x'+currFile.canvasSize[1];
|
||||
selectedPalette = 'none';
|
||||
}
|
||||
|
||||
Util.setValue('lpe-file-name', fileName);
|
||||
Events.on("click", "save-project-confirm", saveProject);
|
||||
Dialogue.showDialogue('save-project', false);
|
||||
}
|
||||
|
||||
function openPixelExportWindow() {
|
||||
let selectedPalette = Util.getText('palette-button');
|
||||
|
||||
if (selectedPalette != 'Choose a palette...'){
|
||||
var paletteAbbreviation = palettes[selectedPalette].name;
|
||||
var fileName = 'pixel-'+paletteAbbreviation+'-'+canvasSize[0]+'x'+canvasSize[1]+'.png';
|
||||
} else {
|
||||
var fileName = 'pixel-'+currFile.canvasSize[0]+'x'+currFile.canvasSize[1]+'.png';
|
||||
selectedPalette = 'none';
|
||||
}
|
||||
|
||||
Util.setValue('export-file-name', fileName);
|
||||
Events.on("click", "export-confirm", exportProject);
|
||||
Dialogue.showDialogue('export', false);
|
||||
}
|
||||
|
||||
function saveProject() {
|
||||
// Get name
|
||||
let fileName = Util.getValue("lpe-file-name") + ".lpe";
|
||||
let selectedPalette = Util.getText('palette-button');
|
||||
//set download link
|
||||
const linkHolder = document.getElementById('save-project-link-holder');
|
||||
// create file content
|
||||
const content = getProjectData();
|
||||
|
||||
linkHolder.href = 'data:text/plain;charset=utf-8,' + encodeURIComponent(content);
|
||||
linkHolder.download = fileName;
|
||||
linkHolder.click();
|
||||
|
||||
if (typeof ga !== 'undefined')
|
||||
ga('send', 'event', 'Pixel Editor Save', selectedPalette, currFile.canvasSize[0]+'/'+currFile.canvasSize[1]); /*global ga*/
|
||||
}
|
||||
|
||||
function exportProject() {
|
||||
if (EditorState.documentCreated()) {
|
||||
//create name
|
||||
let fileName = Util.getValue("export-file-name");
|
||||
//set download link
|
||||
let linkHolder = document.getElementById('save-image-link-holder');
|
||||
// Creating a tmp canvas to flatten everything
|
||||
let exportCanvas = document.createElement("canvas");
|
||||
let emptyCanvas = document.createElement("canvas");
|
||||
let layersCopy = currFile.layers.slice();
|
||||
|
||||
exportCanvas.width = currFile.canvasSize[0];
|
||||
exportCanvas.height = currFile.canvasSize[1];
|
||||
|
||||
emptyCanvas.width = currFile.canvasSize[0];
|
||||
emptyCanvas.height = currFile.canvasSize[1];
|
||||
|
||||
// Sorting the layers by z index
|
||||
layersCopy.sort((a, b) => (a.canvas.style.zIndex > b.canvas.style.zIndex) ? 1 : -1);
|
||||
|
||||
// Merging every layer on the export canvas
|
||||
for (let i=0; i<layersCopy.length; i++) {
|
||||
if (layersCopy[i].hasCanvas() && layersCopy[i].isVisible) {
|
||||
LayerList.mergeLayers(exportCanvas.getContext('2d'), layersCopy[i].context);
|
||||
}
|
||||
// I'm not going to find out why the layer ordering screws up if you don't copy
|
||||
// a blank canvas when layers[i] is not set as visible, but if you have time to
|
||||
// spend, feel free to investigate (comment the else, create 3 layers: hide the
|
||||
// middle one and export, the other 2 will be swapped in their order)
|
||||
else {
|
||||
LayerList.mergeLayers(exportCanvas.getContext('2d'), emptyCanvas.getContext('2d'));
|
||||
}
|
||||
}
|
||||
|
||||
linkHolder.href = exportCanvas.toDataURL();
|
||||
linkHolder.download = fileName;
|
||||
|
||||
linkHolder.click();
|
||||
|
||||
emptyCanvas.remove();
|
||||
exportCanvas.remove();
|
||||
|
||||
//track google event
|
||||
if (typeof ga !== 'undefined')
|
||||
ga('send', 'event', 'Pixel Editor Export', selectedPalette, currFile.canvasSize[0]+'/'+currFile.canvasSize[1]); /*global ga*/
|
||||
}
|
||||
}
|
||||
|
||||
function open() {
|
||||
//if a document exists
|
||||
if (EditorState.documentCreated()) {
|
||||
//check if the user wants to overwrite
|
||||
if (confirm('Opening a pixel will discard your current one. Are you sure you want to do that?'))
|
||||
//open file selection dialog
|
||||
document.getElementById('open-image-browse-holder').click();
|
||||
}
|
||||
else
|
||||
//open file selection dialog
|
||||
document.getElementById('open-image-browse-holder').click();
|
||||
}
|
||||
|
||||
function loadFile() {
|
||||
let fileName = document.getElementById("open-image-browse-holder").value;
|
||||
// Getting the extension
|
||||
let extension = (fileName.substring(fileName.lastIndexOf('.')+1, fileName.length) || fileName).toLowerCase();
|
||||
|
||||
if (browseHolder.files && browseHolder.files[0]) {
|
||||
// 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') {
|
||||
openProject();
|
||||
}
|
||||
else {
|
||||
openFile();
|
||||
}
|
||||
}
|
||||
else alert('Only .LPE project files, PNG and GIF files are allowed at this time.');
|
||||
}
|
||||
|
||||
browseHolder.value = null;
|
||||
}
|
||||
|
||||
function openFile() {
|
||||
//load file
|
||||
var fileReader = new FileReader();
|
||||
|
||||
fileReader.onload = function(e) {
|
||||
var img = new Image();
|
||||
|
||||
img.onload = function() {
|
||||
//create a new pixel with the images dimentions
|
||||
Startup.newPixel(this.width, this.height);
|
||||
EditorState.switchMode('Advanced');
|
||||
|
||||
//draw the image onto the canvas
|
||||
currFile.currentLayer.context.drawImage(img, 0, 0);
|
||||
ColorModule.createPaletteFromLayers();
|
||||
|
||||
//track google event
|
||||
if (typeof ga !== 'undefined')
|
||||
ga('send', 'event', 'Pixel Editor Load', colorPalette.length, this.width+'/'+this.height); /*global ga*/
|
||||
|
||||
};
|
||||
img.src = e.target.result;
|
||||
};
|
||||
fileReader.readAsDataURL(browseHolder.files[0]);
|
||||
}
|
||||
|
||||
function openProject() {
|
||||
let file = browseHolder.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);
|
||||
Startup.newPixel(dictionary['canvasWidth'], dictionary['canvasHeight'], dictionary);
|
||||
|
||||
for (let i=0; dictionary['color' + i] != null; i++) {
|
||||
ColorModule.addColor(dictionary['color'+i]);
|
||||
}
|
||||
|
||||
// Removing the default colours
|
||||
ColorModule.deleteColor(ColorModule.getCurrentPalette()[0]);
|
||||
ColorModule.deleteColor(ColorModule.getCurrentPalette()[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function getProjectData() {
|
||||
// use a dictionary
|
||||
let dictionary = {};
|
||||
// sorting layers by increasing z-index
|
||||
let layersCopy = currFile.layers.slice();
|
||||
layersCopy.sort((a, b) => (a.canvas.style.zIndex > b.canvas.style.zIndex) ? 1 : -1);
|
||||
// save canvas size
|
||||
dictionary['canvasWidth'] = currFile.canvasSize[0];
|
||||
dictionary['canvasHeight'] = currFile.canvasSize[1];
|
||||
// save editor mode
|
||||
dictionary['editorMode'] = EditorState.getCurrentMode();
|
||||
// save palette
|
||||
for (let i=0; i<ColorModule.getCurrentPalette().length; i++) {
|
||||
dictionary["color" + i] = ColorModule.getCurrentPalette()[i];
|
||||
}
|
||||
|
||||
// save number of layers
|
||||
dictionary["nLayers"] = layersCopy.length;
|
||||
|
||||
// save layers
|
||||
for (let i=0; i<layersCopy.length; i++) {
|
||||
// Only saving the layers the user has access to (no vfx, tmp or checkerboard layers)
|
||||
if (layersCopy[i].menuEntry != null) {
|
||||
dictionary["layer" + i] = layersCopy[i];
|
||||
dictionary["layer" + i + "ImageData"] = layersCopy[i].canvas.toDataURL();
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify(dictionary);
|
||||
}
|
||||
|
||||
function loadPalette() {
|
||||
if (browsePaletteHolder.files && browsePaletteHolder.files[0]) {
|
||||
//make sure file is allowed filetype
|
||||
var fileContentType = browsePaletteHolder.files[0].type;
|
||||
if (fileContentType == 'image/png' || fileContentType == 'image/gif') {
|
||||
|
||||
//load file
|
||||
var fileReader = new FileReader();
|
||||
fileReader.onload = function(e) {
|
||||
var img = new Image();
|
||||
img.onload = function() {
|
||||
|
||||
//draw image onto the temporary canvas
|
||||
var loadPaletteCanvas = document.getElementById('load-palette-canvas-holder');
|
||||
var loadPaletteContext = loadPaletteCanvas.getContext('2d');
|
||||
|
||||
loadPaletteCanvas.width = img.width;
|
||||
loadPaletteCanvas.height = img.height;
|
||||
|
||||
loadPaletteContext.drawImage(img, 0, 0);
|
||||
|
||||
//create array to hold found colors
|
||||
var colorPalette = [];
|
||||
var imagePixelData = loadPaletteContext.getImageData(0,0,this.width, this.height).data;
|
||||
|
||||
//loop through pixels looking for colors to add to palette
|
||||
for (var i = 0; i < imagePixelData.length; i += 4) {
|
||||
const newColor = {r:imagePixelData[i],g:imagePixelData[i + 1],b:imagePixelData[i + 2]};
|
||||
var color = '#' + Color.rgbToHex(newColor);
|
||||
if (colorPalette.indexOf(color) == -1) {
|
||||
colorPalette.push(color);
|
||||
}
|
||||
}
|
||||
|
||||
//add to palettes so that it can be loaded when they click okay
|
||||
palettes['Loaded palette'] = {};
|
||||
palettes['Loaded palette'].colors = colorPalette;
|
||||
Util.setText('palette-button', 'Loaded palette');
|
||||
Util.setText('palette-button-splash', 'Loaded palette');
|
||||
Util.toggle('palette-menu-splash');
|
||||
};
|
||||
img.src = e.target.result;
|
||||
};
|
||||
fileReader.readAsDataURL(browsePaletteHolder.files[0]);
|
||||
}
|
||||
else alert('Only PNG and GIF files are supported at this time.');
|
||||
}
|
||||
|
||||
browsePaletteHolder.value = null;
|
||||
}
|
||||
|
||||
return {
|
||||
saveProject,
|
||||
exportProject,
|
||||
openPixelExportWindow,
|
||||
openSaveProjectWindow,
|
||||
open
|
||||
}
|
||||
})();
|
|
@ -0,0 +1,442 @@
|
|||
/** 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
|
||||
*
|
||||
*/
|
||||
const History = (() => {
|
||||
|
||||
const undoLogStyle = 'background: #87ff1c; color: black; padding: 5px;';
|
||||
let undoStates = [];
|
||||
let redoStates = [];
|
||||
|
||||
Events.on('click', 'undo-button', undo);
|
||||
Events.on('click', 'redo-button', redo);
|
||||
|
||||
//rename to add undo state
|
||||
function saveHistoryState (state) {
|
||||
//get current canvas data and save to undoStates array
|
||||
undoStates.push(state);
|
||||
|
||||
//limit the number of states to settings.numberOfHistoryStates
|
||||
if (undoStates.length > Settings.getCurrSettings().numberOfHistoryStates) {
|
||||
undoStates = undoStates.splice(-Settings.getCurrSettings().numberOfHistoryStates, Settings.getCurrSettings().numberOfHistoryStates);
|
||||
}
|
||||
|
||||
//there is now definitely at least 1 undo state, so the button shouldnt be disabled
|
||||
document.getElementById('undo-button').classList.remove('disabled');
|
||||
|
||||
//there should be no redoStates after an undoState is saved
|
||||
redoStates = [];
|
||||
}
|
||||
|
||||
function undo () {
|
||||
console.log("undoing");
|
||||
undoOrRedo('undo');
|
||||
}
|
||||
|
||||
function redo () {
|
||||
console.log("redoing");
|
||||
undoOrRedo('redo');
|
||||
}
|
||||
|
||||
function undoOrRedo(mode) {
|
||||
if (redoStates.length <= 0 && mode == 'redo') return;
|
||||
if (undoStates.length <= 0 && mode == 'undo') return;
|
||||
|
||||
// Enable button
|
||||
document.getElementById(mode + '-button').classList.remove('disabled');
|
||||
|
||||
if (mode == 'undo') {
|
||||
const undoState = undoStates.pop();
|
||||
redoStates.push(undoState);
|
||||
undoState.undo();
|
||||
}
|
||||
else {
|
||||
const redoState = redoStates.pop();
|
||||
undoStates.push(redoState);
|
||||
redoState.redo();
|
||||
}
|
||||
|
||||
|
||||
// if theres none left, disable the option
|
||||
if (redoStates.length == 0) document.getElementById('redo-button').classList.add('disabled');
|
||||
if (undoStates.length == 0) document.getElementById('undo-button').classList.add('disabled');
|
||||
}
|
||||
|
||||
return {
|
||||
redo,
|
||||
undo,
|
||||
saveHistoryState
|
||||
}
|
||||
})();
|
||||
|
||||
class HistoryState {
|
||||
constructor() {
|
||||
History.saveHistoryState(this);
|
||||
}
|
||||
|
||||
ResizeSprite (xRatio, yRatio, algo, oldData) {
|
||||
this.xRatio = xRatio;
|
||||
this.yRatio = yRatio;
|
||||
this.algo = algo;
|
||||
this.oldData = oldData;
|
||||
|
||||
this.undo = function() {
|
||||
let layerIndex = 0;
|
||||
|
||||
currFile.currentAlgo = algo;
|
||||
currFile.resizeSprite(null, [1 / this.xRatio, 1 / this.yRatio]);
|
||||
|
||||
// Also putting the old data
|
||||
for (let i=0; i<currFile.layers.length; i++) {
|
||||
if (currFile.layers[i].hasCanvas()) {
|
||||
currFile.layers[i].context.putImageData(this.oldData[layerIndex], 0, 0);
|
||||
layerIndex++;
|
||||
currFile.layers[i].updateLayerPreview();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.redo = function() {
|
||||
currFile.currentAlgo = algo;
|
||||
currFile.resizeSprite(null, [this.xRatio, this.yRatio]);
|
||||
};
|
||||
}
|
||||
|
||||
ResizeCanvas (newSize, oldSize, imageDatas, trim) {
|
||||
this.oldSize = oldSize;
|
||||
this.newSize = newSize;
|
||||
this.imageDatas = imageDatas;
|
||||
this.trim = trim;
|
||||
|
||||
this.undo = function() {
|
||||
let dataIndex = 0;
|
||||
// Resizing the canvas
|
||||
currFile.resizeCanvas(null, oldSize, null, false);
|
||||
// Putting the image datas
|
||||
for (let i=0; i<currFile.layers.length; i++) {
|
||||
if (currFile.layers[i].hasCanvas()) {
|
||||
currFile.layers[i].context.putImageData(this.imageDatas[dataIndex], 0, 0);
|
||||
dataIndex++;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.redo = function() {
|
||||
if (!this.trim) {
|
||||
currFile.resizeCanvas(null, newSize, null, false);
|
||||
}
|
||||
else {
|
||||
currFile.trimCanvas(null, false);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
FlattenVisible(flattened) {
|
||||
this.nFlattened = flattened;
|
||||
|
||||
this.undo = function() {
|
||||
for (let i=0; i<this.nFlattened; i++) {
|
||||
undo();
|
||||
}
|
||||
};
|
||||
|
||||
this.redo = function() {
|
||||
for (let i=0; i<this.nFlattened; i++) {
|
||||
redo();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
FlattenTwoVisibles(belowImageData, afterAbove, layerIndex, aboveLayer, belowLayer) {
|
||||
this.aboveLayer = aboveLayer;
|
||||
this.belowLayer = belowLayer;
|
||||
this.belowImageData = belowImageData;
|
||||
|
||||
this.undo = function() {
|
||||
currFile.canvasView.append(aboveLayer.canvas);
|
||||
LayerList.getLayerListEntries().insertBefore(aboveLayer.menuEntry, afterAbove);
|
||||
|
||||
belowLayer.context.clearRect(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
|
||||
belowLayer.context.putImageData(this.belowImageData, 0, 0);
|
||||
belowLayer.updateLayerPreview();
|
||||
|
||||
currFile.layers.splice(layerIndex, 0, aboveLayer);
|
||||
};
|
||||
|
||||
this.redo = function() {
|
||||
LayerList.mergeLayers(belowLayer.context, aboveLayer.context);
|
||||
|
||||
// Deleting the above layer
|
||||
aboveLayer.canvas.remove();
|
||||
aboveLayer.menuEntry.remove();
|
||||
currFile.layers.splice(currFile.layers.indexOf(aboveLayer), 1);
|
||||
};
|
||||
}
|
||||
|
||||
FlattenAll(nFlattened) {
|
||||
this.nFlattened = nFlattened;
|
||||
|
||||
this.undo = function() {
|
||||
for (let i=0; i<this.nFlattened - nAppLayers; i++) {
|
||||
undo();
|
||||
}
|
||||
};
|
||||
|
||||
this.redo = function() {
|
||||
for (let i=0; i<this.nFlattened - nAppLayers; i++) {
|
||||
redo();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
MergeLayer(aboveIndex, aboveLayer, belowData, belowLayer) {
|
||||
this.aboveIndex = aboveIndex;
|
||||
this.belowData = belowData;
|
||||
this.aboveLayer = aboveLayer;
|
||||
this.belowLayer = belowLayer;
|
||||
|
||||
this.undo = function() {
|
||||
LayerList.getLayerListEntries().insertBefore(this.aboveLayer.menuEntry, this.belowLayer.menuEntry);
|
||||
currFile.canvasView.append(this.aboveLayer.canvas);
|
||||
|
||||
belowLayer.context.clearRect(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
|
||||
belowLayer.context.putImageData(this.belowData, 0, 0);
|
||||
belowLayer.updateLayerPreview();
|
||||
|
||||
currFile.layers.splice(this.aboveIndex, 0, this.aboveLayer);
|
||||
};
|
||||
|
||||
this.redo = function() {
|
||||
aboveLayer.selectLayer();
|
||||
LayerList.merge(false);
|
||||
};
|
||||
}
|
||||
|
||||
RenameLayer(oldName, newName, layer) {
|
||||
this.edited = layer;
|
||||
this.oldName = oldName;
|
||||
this.newName = newName;
|
||||
|
||||
this.undo = function() {
|
||||
layer.menuEntry.getElementsByTagName("p")[0].innerHTML = oldName;
|
||||
};
|
||||
|
||||
this.redo = function() {
|
||||
layer.menuEntry.getElementsByTagName("p")[0].innerHTML = newName;
|
||||
};
|
||||
}
|
||||
|
||||
DuplicateLayer(addedLayer, copiedLayer) {
|
||||
this.addedLayer = addedLayer;
|
||||
this.copiedLayer = copiedLayer;
|
||||
|
||||
this.undo = function() {
|
||||
addedLayer.selectLayer();
|
||||
LayerList.deleteLayer(false);
|
||||
};
|
||||
|
||||
this.redo = function() {
|
||||
copiedLayer.selectLayer();
|
||||
LayerList.duplicateLayer(null, false);
|
||||
};
|
||||
}
|
||||
|
||||
DeleteLayer(layerData, before, index) {
|
||||
this.deleted = layerData;
|
||||
this.before = before;
|
||||
this.index = index;
|
||||
|
||||
this.undo = function() {
|
||||
currFile.canvasView.append(this.deleted.canvas);
|
||||
if (this.before != null) {
|
||||
LayerList.getLayerListEntries().insertBefore(this.deleted.menuEntry, this.before);
|
||||
}
|
||||
else {
|
||||
LayerList.getLayerListEntries().prepend(this.deleted.menuEntry);
|
||||
}
|
||||
currFile.layers.splice(this.index, 0, this.deleted);
|
||||
};
|
||||
|
||||
this.redo = function() {
|
||||
this.deleted.selectLayer();
|
||||
LayerList.deleteLayer(false);
|
||||
};
|
||||
}
|
||||
|
||||
MoveTwoLayers(layer, oldIndex, newIndex) {
|
||||
this.layer = layer;
|
||||
this.oldIndex = oldIndex;
|
||||
this.newIndex = newIndex;
|
||||
|
||||
this.undo = function() {
|
||||
layer.canvas.style.zIndex = oldIndex;
|
||||
};
|
||||
|
||||
this.redo = function() {
|
||||
layer.canvas.style.zIndex = newIndex;
|
||||
};
|
||||
}
|
||||
|
||||
MoveLayer(afterToDrop, toDrop, staticc, nMoved) {
|
||||
this.beforeToDrop = afterToDrop;
|
||||
this.toDrop = toDrop;
|
||||
|
||||
this.undo = function() {
|
||||
toDrop.menuEntry.remove();
|
||||
|
||||
if (afterToDrop != null) {
|
||||
LayerList.getLayerListEntries().insertBefore(toDrop.menuEntry, afterToDrop)
|
||||
}
|
||||
else {
|
||||
LayerList.getLayerListEntries().append(toDrop.menuEntry);
|
||||
}
|
||||
|
||||
for (let i=0; i<nMoved; i++) {
|
||||
undo();
|
||||
}
|
||||
};
|
||||
|
||||
this.redo = function() {
|
||||
moveLayers(toDrop.menuEntry.id, staticc.menuEntry.id, true);
|
||||
};
|
||||
}
|
||||
|
||||
AddLayer(layerData, index) {
|
||||
this.added = layerData;
|
||||
this.index = index;
|
||||
|
||||
this.undo = function() {
|
||||
if (currFile.layers.length - nAppLayers > this.index + 1) {
|
||||
currFile.layers[this.index + 1].selectLayer();
|
||||
}
|
||||
else {
|
||||
currFile.layers[this.index - 1].selectLayer();
|
||||
}
|
||||
|
||||
this.added.canvas.remove();
|
||||
this.added.menuEntry.remove();
|
||||
|
||||
currFile.layers.splice(index, 1);
|
||||
};
|
||||
|
||||
this.redo = function() {
|
||||
currFile.canvasView.append(this.added.canvas);
|
||||
LayerList.getLayerListEntries().prepend(this.added.menuEntry);
|
||||
layers.splice(this.index, 0, this.added);
|
||||
};
|
||||
}
|
||||
|
||||
//prototype for undoing canvas changes
|
||||
EditCanvas() {
|
||||
this.canvasState = currFile.currentLayer.context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
|
||||
this.layerID = currFile.currentLayer.id;
|
||||
|
||||
this.undo = function () {
|
||||
var stateLayer = LayerList.getLayerByID(this.layerID);
|
||||
var currentCanvas = stateLayer.context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
|
||||
stateLayer.context.putImageData(this.canvasState, 0, 0);
|
||||
|
||||
this.canvasState = currentCanvas;
|
||||
|
||||
stateLayer.updateLayerPreview();
|
||||
};
|
||||
|
||||
this.redo = function () {
|
||||
var stateLayer = LayerList.getLayerByID(this.layerID);
|
||||
var currentCanvas = stateLayer.context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
|
||||
|
||||
stateLayer.context.putImageData(this.canvasState, 0, 0);
|
||||
|
||||
this.canvasState = currentCanvas;
|
||||
|
||||
stateLayer.updateLayerPreview();
|
||||
};
|
||||
}
|
||||
|
||||
//prototype for undoing added colors
|
||||
AddColor(colorValue) {
|
||||
this.colorValue = colorValue;
|
||||
|
||||
this.undo = function () {
|
||||
ColorModule.deleteColor(this.colorValue);
|
||||
};
|
||||
|
||||
this.redo = function () {
|
||||
ColorModule.addColor(this.colorValue);
|
||||
};
|
||||
}
|
||||
|
||||
//prototype for undoing deleted colors
|
||||
DeleteColor(colorValue) {
|
||||
this.colorValue = colorValue;
|
||||
this.canvas = currFile.currentLayer.context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
|
||||
|
||||
this.undo = function () {
|
||||
var currentCanvas = currFile.currentLayer.context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
|
||||
currFile.currentLayer.context.putImageData(this.canvas, 0, 0);
|
||||
|
||||
ColorModule.addColor(this.colorValue);
|
||||
|
||||
this.canvas = currentCanvas;
|
||||
};
|
||||
|
||||
this.redo = function () {
|
||||
var currentCanvas = currFile.currentLayer.context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
|
||||
currFile.currentLayer.context.putImageData(this.canvas, 0, 0);
|
||||
|
||||
ColorModule.deleteColor(this.colorValue);
|
||||
|
||||
this.canvas = currentCanvas;
|
||||
};
|
||||
}
|
||||
|
||||
//prototype for undoing colors edits
|
||||
EditColor(newColorValue, oldColorValue) {
|
||||
this.newColorValue = newColorValue;
|
||||
this.oldColorValue = oldColorValue;
|
||||
this.canvas = currFile.currentLayer.context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
|
||||
|
||||
this.undo = function () {
|
||||
let currentCanvas = currFile.currentLayer.context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
|
||||
currFile.currentLayer.context.putImageData(this.canvas, 0, 0);
|
||||
|
||||
//find new color in palette and change it back to old color
|
||||
let colors = document.getElementsByClassName('color-button');
|
||||
for (let i = 0; i < colors.length; i++) {
|
||||
//console.log(newColorValue, '==', colors[i].jscolor.toString());
|
||||
if (newColorValue == colors[i].jscolor.toString()) {
|
||||
colors[i].jscolor.fromString(oldColorValue);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.canvas = currentCanvas;
|
||||
};
|
||||
|
||||
this.redo = function () {
|
||||
let currentCanvas = currFile.currentLayer.context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
|
||||
currFile.currentLayer.context.putImageData(this.canvas, 0, 0);
|
||||
|
||||
//find old color in palette and change it back to new color
|
||||
let colors = document.getElementsByClassName('color-button');
|
||||
for (let i = 0; i < colors.length; i++) {
|
||||
//console.log(oldColorValue, '==', colors[i].jscolor.toString());
|
||||
if (oldColorValue == colors[i].jscolor.toString()) {
|
||||
colors[i].jscolor.fromString(newColorValue);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.canvas = currentCanvas;
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,199 @@
|
|||
const Input = (() => {
|
||||
let dragging = false;
|
||||
let currentMouseEvent = undefined;
|
||||
let lastMouseTarget = undefined;
|
||||
let spacePressed = false;
|
||||
let altPressed = false;
|
||||
let ctrlPressed = false;
|
||||
|
||||
// Hotkeys when pressing a key
|
||||
Events.on("keydown", document, KeyPress);
|
||||
// Update held keys when releasing a key
|
||||
Events.on("keyup", window, function (e) {
|
||||
if (e.keyCode == 32) spacePressed = false;
|
||||
if (!e.altKey) altPressed = false;
|
||||
if (!e.ctrlKey) ctrlPressed = false;
|
||||
});
|
||||
|
||||
// Update variables on mouse clicks
|
||||
Events.on("mousedown", window, onMouseDown);
|
||||
Events.on("mouseup", window, onMouseUp);
|
||||
|
||||
function onMouseDown(event) {
|
||||
lastMouseTarget = event.target;
|
||||
currentMouseEvent = event;
|
||||
dragging = true;
|
||||
|
||||
if (!Util.isChildOfByClass(event.target, "editor-top-menu")) {
|
||||
TopMenuModule.closeMenu();
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseUp(event) {
|
||||
currentMouseEvent = event;
|
||||
dragging = false;
|
||||
|
||||
if (currFile.currentLayer != null && !Util.isChildOfByClass(event.target, "layers-menu-entry")) {
|
||||
LayerList.closeOptionsMenu();
|
||||
}
|
||||
}
|
||||
|
||||
function getCursorPosition(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 -= currFile.currentLayer.canvas.offsetLeft;
|
||||
y -= currFile.currentLayer.canvas.offsetTop;
|
||||
|
||||
return [Math.round(x), Math.round(y)];
|
||||
}
|
||||
|
||||
/** Just listens to hotkeys and calls the linked functions
|
||||
*
|
||||
* @param {*} e
|
||||
*/
|
||||
function KeyPress(e) {
|
||||
var keyboardEvent = window.event? event : e;
|
||||
altPressed = e.altKey;
|
||||
ctrlPressed = e.ctrlKey;
|
||||
|
||||
//if the user is typing in an input field or renaming a layer, ignore these hotkeys, unless it's an enter key
|
||||
if (document.activeElement.tagName == 'INPUT' || LayerList.isRenamingLayer()) {
|
||||
if (e.keyCode == 13) {
|
||||
LayerList.closeOptionsMenu();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
//if no document has been created yet or there is a dialog box open ignore hotkeys
|
||||
if (!EditorState.documentCreated()) return;
|
||||
|
||||
if (e.key === "Escape") {
|
||||
Events.emit("esc-pressed");
|
||||
}
|
||||
else if (!Dialogue.isOpen()){
|
||||
switch (keyboardEvent.keyCode) {
|
||||
//pencil tool - 1, b
|
||||
case 49: case 66:
|
||||
Events.emit("tool-shortcut", "brush");
|
||||
break;
|
||||
// copy tool c
|
||||
case 67: case 99:
|
||||
if (keyboardEvent.ctrlKey) {
|
||||
Events.emit("ctrl+c");
|
||||
}
|
||||
break;
|
||||
//fill tool - 2, f
|
||||
case 50: case 70:
|
||||
Events.emit("tool-shortcut", "fill");
|
||||
break;
|
||||
//eyedropper - 3, e
|
||||
case 51: case 69:
|
||||
Events.emit("tool-shortcut", "eyedropper");
|
||||
break;
|
||||
//pan - 4, p,
|
||||
case 52: case 80:
|
||||
Events.emit("tool-shortcut", "pan");
|
||||
break;
|
||||
// line - l
|
||||
case 76:
|
||||
Events.emit("tool-shortcut", "line");
|
||||
break;
|
||||
// eraser -6, r
|
||||
case 54: case 82:
|
||||
Events.emit("tool-shortcut", "eraser");
|
||||
break;
|
||||
// Rectangular selection m
|
||||
case 77: case 109:
|
||||
Events.emit("tool-shortcut", "rectselect");
|
||||
break;
|
||||
// TODO: [ELLIPSE] Decide on a shortcut to use. "s" was chosen without any in-team consultation.
|
||||
// ellipse tool, s
|
||||
case 83:
|
||||
//Events.emit("tool-shortcut", "ellipse");
|
||||
break;
|
||||
// rectangle tool, u
|
||||
case 85:
|
||||
Events.emit("tool-shortcut", "rectangle");
|
||||
break;
|
||||
// Paste tool
|
||||
case 86: case 118:
|
||||
if (keyboardEvent.ctrlKey) {
|
||||
Events.emit("ctrl+v");
|
||||
}
|
||||
break;
|
||||
case 88: case 120:
|
||||
if (keyboardEvent.ctrlKey) {
|
||||
Events.emit("ctrl+x");
|
||||
}
|
||||
break;
|
||||
//Z
|
||||
case 90: case 122:
|
||||
//CTRL+ALT+Z redo
|
||||
if (keyboardEvent.altKey && keyboardEvent.ctrlKey) {
|
||||
History.redo();
|
||||
}
|
||||
//CTRL+Z undo
|
||||
else if (keyboardEvent.ctrlKey) {
|
||||
History.undo();
|
||||
}
|
||||
break;
|
||||
//redo - ctrl y
|
||||
case 89:
|
||||
if (keyboardEvent.ctrlKey)
|
||||
History.redo();
|
||||
break;
|
||||
case 32:
|
||||
spacePressed = true;
|
||||
break;
|
||||
case 46:
|
||||
console.log("Pressed del");
|
||||
Events.emit("del");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isDragging() {
|
||||
return dragging;
|
||||
}
|
||||
|
||||
function getCurrMouseEvent() {
|
||||
return currentMouseEvent;
|
||||
}
|
||||
|
||||
function isAltPressed() {
|
||||
return altPressed;
|
||||
}
|
||||
|
||||
function isCtrlPressed() {
|
||||
return ctrlPressed;
|
||||
}
|
||||
|
||||
function isSpacePressed() {
|
||||
return spacePressed;
|
||||
}
|
||||
|
||||
function getLastTarget() {
|
||||
return lastMouseTarget;
|
||||
}
|
||||
|
||||
return {
|
||||
isDragging,
|
||||
getCurrMouseEvent,
|
||||
getCursorPosition,
|
||||
isAltPressed,
|
||||
isCtrlPressed,
|
||||
isSpacePressed,
|
||||
getLastTarget
|
||||
}
|
||||
})();
|
|
@ -0,0 +1,417 @@
|
|||
const LayerList = (() => {
|
||||
|
||||
let layerList = document.getElementById("layers-menu");
|
||||
let layerListEntry = layerList.firstElementChild;
|
||||
let renamingLayer = false;
|
||||
let dragStartLayer;
|
||||
|
||||
// Binding the right click menu
|
||||
Events.on("mousedown", layerList, openOptionsMenu);
|
||||
// Binding the add layer button to the right function
|
||||
Events.on('click',"add-layer-button", addLayer, false);
|
||||
// Listening to the switch mode event so I can change the layout
|
||||
Events.onCustom("switchedToAdvanced", showMenu);
|
||||
Events.onCustom("switchedToBasic", hideMenu);
|
||||
|
||||
// Making the layers list sortable
|
||||
new Sortable(layerList, {
|
||||
animation: 100,
|
||||
filter: ".layer-button",
|
||||
draggable: ".layers-menu-entry",
|
||||
onStart: layerDragStart,
|
||||
onEnd: layerDragDrop
|
||||
});
|
||||
|
||||
function showMenu() {
|
||||
layerList.style.display = "inline-block";
|
||||
document.getElementById('layer-button').style.display = 'inline-block';
|
||||
}
|
||||
function hideMenu() {
|
||||
if (EditorState.documentCreated()) {
|
||||
// Selecting the current layer
|
||||
currFile.currentLayer.selectLayer();
|
||||
// Flatten the layers
|
||||
flatten(true);
|
||||
}
|
||||
|
||||
layerList.style.display = "none";
|
||||
document.getElementById('layer-button').style.display = 'none';
|
||||
}
|
||||
|
||||
function addLayer(id, saveHistory = true) {
|
||||
// layers.length - 3
|
||||
let index = currFile.layers.length - 3;
|
||||
// Creating a new canvas
|
||||
let newCanvas = document.createElement("canvas");
|
||||
// Setting up the new canvas
|
||||
currFile.canvasView.append(newCanvas);
|
||||
Layer.maxZIndex+=2;
|
||||
newCanvas.style.zIndex = Layer.maxZIndex;
|
||||
newCanvas.classList.add("drawingCanvas");
|
||||
|
||||
if (!layerListEntry) return console.warn('skipping adding layer because no document');
|
||||
|
||||
// Clone the default layer
|
||||
let toAppend = layerListEntry.cloneNode(true);
|
||||
// Setting the default name for the layer
|
||||
toAppend.getElementsByTagName('p')[0].innerHTML = "Layer " + Layer.layerCount;
|
||||
// Removing the selected class
|
||||
toAppend.classList.remove("selected-layer");
|
||||
// Adding the layer to the list
|
||||
Layer.layerCount++;
|
||||
|
||||
// Creating a layer object
|
||||
let newLayer = new Layer(currFile.canvasSize[0], currFile.canvasSize[1], newCanvas, toAppend);
|
||||
newLayer.context.fillStyle = currFile.currentLayer.context.fillStyle;
|
||||
newLayer.copyData(currFile.currentLayer);
|
||||
|
||||
currFile.layers.splice(index, 0, newLayer);
|
||||
|
||||
// Insert it before the Add layer button
|
||||
layerList.insertBefore(toAppend, layerList.childNodes[0]);
|
||||
|
||||
if (id != null && typeof(id) == "string") {
|
||||
newLayer.setID(id);
|
||||
}
|
||||
// Basically "if I'm not adding a layer because redo() is telling meto do so", then I can save the history
|
||||
if (saveHistory) {
|
||||
new HistoryState().AddLayer(newLayer, index);
|
||||
}
|
||||
|
||||
return newLayer;
|
||||
}
|
||||
|
||||
/** 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, belowLayer.canvas.width, belowLayer.canvas.height);
|
||||
let toMergeImageData = topLayer.getImageData(0, 0, topLayer.canvas.width, topLayer.canvas.height);
|
||||
|
||||
for (let i=0; i<belowImageData.data.length; i+=4) {
|
||||
let currentMovePixel = [
|
||||
toMergeImageData.data[i], toMergeImageData.data[i+1],
|
||||
toMergeImageData.data[i+2], toMergeImageData.data[i+3]
|
||||
];
|
||||
|
||||
let currentUnderlyingPixel = [
|
||||
belowImageData.data[i], belowImageData.data[i+1],
|
||||
belowImageData.data[i+2], belowImageData.data[i+3]
|
||||
];
|
||||
|
||||
if (Util.isPixelEmpty(currentMovePixel)) {
|
||||
if (!Util.isPixelEmpty(belowImageData)) {
|
||||
toMergeImageData.data[i] = currentUnderlyingPixel[0];
|
||||
toMergeImageData.data[i+1] = currentUnderlyingPixel[1];
|
||||
toMergeImageData.data[i+2] = currentUnderlyingPixel[2];
|
||||
toMergeImageData.data[i+3] = currentUnderlyingPixel[3];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Putting the top data into the belowdata
|
||||
belowLayer.putImageData(toMergeImageData, 0, 0);
|
||||
}
|
||||
|
||||
/** 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;
|
||||
Events.simulateMouseEvent(window, "mouseup");
|
||||
}
|
||||
|
||||
/** Saves the layer that is being moved when the dragging starts
|
||||
*
|
||||
* @param {*} event
|
||||
*/
|
||||
function layerDragStart(event) {
|
||||
dragStartLayer = getLayerByID(layerList.children[event.oldIndex].id);
|
||||
}
|
||||
|
||||
// Finds a layer given its id
|
||||
function getLayerByID(id) {
|
||||
for (let i=0; i<currFile.layers.length; i++) {
|
||||
if (currFile.layers[i].hasCanvas()) {
|
||||
if (currFile.layers[i].menuEntry.id == id) {
|
||||
return currFile.layers[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Finds a layer given its name
|
||||
function getLayerByName(name) {
|
||||
for (let i=0; i<currFile.layers.length; i++) {
|
||||
if (currFile.layers[i].hasCanvas()) {
|
||||
if (currFile.layers[i].menuEntry.getElementsByTagName("p")[0].innerHTML == name) {
|
||||
return currFile.layers[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function startRenamingLayer(event) {
|
||||
let p = currFile.currentLayer.menuEntry.getElementsByTagName("p")[0];
|
||||
|
||||
currFile.currentLayer.oldLayerName = p.innerHTML;
|
||||
|
||||
p.setAttribute("contenteditable", true);
|
||||
p.classList.add("layer-name-editable");
|
||||
p.focus();
|
||||
Events.simulateInput(65, true, false, false);
|
||||
|
||||
renamingLayer = true;
|
||||
}
|
||||
|
||||
function duplicateLayer(event, saveHistory = true) {
|
||||
function getMenuEntryIndex(list, entry) {
|
||||
for (let i=0; i<list.length; i++) {
|
||||
if (list[i] === entry) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
let layerIndex = currFile.layers.indexOf(currFile.currentLayer);
|
||||
let toDuplicate = currFile.currentLayer;
|
||||
let menuEntries = layerList.children;
|
||||
|
||||
// Increasing z-indexes of the layers above
|
||||
for (let i=getMenuEntryIndex(menuEntries, toDuplicate.menuEntry) - 1; i>=0; i--) {
|
||||
LayerList.getLayerByID(menuEntries[i].id).canvas.style.zIndex++;
|
||||
}
|
||||
Layer.maxZIndex+=2;
|
||||
|
||||
// Creating a new canvas
|
||||
let newCanvas = document.createElement("canvas");
|
||||
// Setting up the new canvas
|
||||
currFile.canvasView.append(newCanvas);
|
||||
newCanvas.style.zIndex = parseInt(currFile.currentLayer.canvas.style.zIndex) + 2;
|
||||
newCanvas.classList.add("drawingCanvas");
|
||||
|
||||
if (!layerListEntry) return console.warn('skipping adding layer because no document');
|
||||
|
||||
// Clone the default layer
|
||||
let toAppend = currFile.currentLayer.menuEntry.cloneNode(true);
|
||||
// Setting the default name for the layer
|
||||
toAppend.getElementsByTagName('p')[0].innerHTML += " copy";
|
||||
// Removing the selected class
|
||||
toAppend.classList.remove("selected-layer");
|
||||
// Adding the layer to the list
|
||||
Layer.layerCount++;
|
||||
|
||||
// Creating a layer object
|
||||
let newLayer = new Layer(currFile.canvasSize[0], currFile.canvasSize[1], newCanvas, toAppend);
|
||||
newLayer.context.fillStyle = currFile.currentLayer.context.fillStyle;
|
||||
newLayer.copyData(currFile.currentLayer);
|
||||
|
||||
currFile.layers.splice(layerIndex, 0, newLayer);
|
||||
|
||||
// Insert it before the Add layer button
|
||||
layerList.insertBefore(toAppend, currFile.currentLayer.menuEntry);
|
||||
|
||||
// Copy the layer content
|
||||
newLayer.context.putImageData(currFile.currentLayer.context.getImageData(
|
||||
0, 0, currFile.canvasSize[0], currFile.canvasSize[1]), 0, 0);
|
||||
newLayer.updateLayerPreview();
|
||||
// Basically "if I'm not adding a layer because redo() is telling meto do so", then I can save the history
|
||||
if (saveHistory) {
|
||||
new HistoryState().DuplicateLayer(newLayer, currFile.currentLayer);
|
||||
}
|
||||
}
|
||||
|
||||
function deleteLayer(saveHistory = true) {
|
||||
// Cannot delete all the layers
|
||||
if (currFile.layers.length != 4) {
|
||||
let layerIndex = currFile.layers.indexOf(currFile.currentLayer);
|
||||
let toDelete = currFile.layers[layerIndex];
|
||||
let previousSibling = toDelete.menuEntry.previousElementSibling;
|
||||
// Adding the ids to the unused ones
|
||||
Layer.unusedIDs.push(toDelete.id);
|
||||
|
||||
// Selecting the next layer
|
||||
if (layerIndex != (currFile.layers.length - 4)) {
|
||||
currFile.layers[layerIndex + 1].selectLayer();
|
||||
}
|
||||
// or the previous one if the next one doesn't exist
|
||||
else {
|
||||
currFile.layers[layerIndex - 1].selectLayer();
|
||||
}
|
||||
|
||||
// Deleting canvas and entry
|
||||
toDelete.canvas.remove();
|
||||
toDelete.menuEntry.remove();
|
||||
|
||||
// Removing the layer from the list
|
||||
currFile.layers.splice(layerIndex, 1);
|
||||
|
||||
if (saveHistory) {
|
||||
new HistoryState().DeleteLayer(toDelete, previousSibling, layerIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// Closing the menu
|
||||
closeOptionsMenu();
|
||||
}
|
||||
|
||||
function merge(saveHistory = true) {
|
||||
// Saving the layer that should be merged
|
||||
let toMerge = currFile.currentLayer;
|
||||
let toMergeIndex = currFile.layers.indexOf(toMerge);
|
||||
// Getting layer below
|
||||
let layerBelow = LayerList.getLayerByID(currFile.currentLayer.menuEntry.nextElementSibling.id);
|
||||
|
||||
// If I have something to merge with
|
||||
if (layerBelow != null) {
|
||||
// Selecting that layer
|
||||
layerBelow.selectLayer();
|
||||
|
||||
if (saveHistory) {
|
||||
new HistoryState().MergeLayer(toMergeIndex, toMerge,
|
||||
layerBelow.context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]),
|
||||
layerBelow);
|
||||
}
|
||||
|
||||
LayerList.mergeLayers(currFile.currentLayer.context, toMerge.context);
|
||||
|
||||
// Deleting the above layer
|
||||
toMerge.canvas.remove();
|
||||
toMerge.menuEntry.remove();
|
||||
currFile.layers.splice(toMergeIndex, 1);
|
||||
|
||||
// Updating the layer preview
|
||||
currFile.currentLayer.updateLayerPreview();
|
||||
}
|
||||
}
|
||||
|
||||
function flatten(onlyVisible) {
|
||||
if (!onlyVisible) {
|
||||
// Selecting the first layer
|
||||
let firstLayer = layerList.firstElementChild;
|
||||
let nToFlatten = layerList.childElementCount - 1;
|
||||
LayerList.getLayerByID(firstLayer.id).selectLayer();
|
||||
|
||||
for (let i = 0; i < nToFlatten; i++) {
|
||||
merge();
|
||||
}
|
||||
|
||||
new HistoryState().FlattenAll(nToFlatten);
|
||||
}
|
||||
else {
|
||||
// Getting all the visible layers
|
||||
let visibleLayers = [];
|
||||
let nToFlatten = 0;
|
||||
|
||||
for (let i=0; i<currFile.layers.length; i++) {
|
||||
if (currFile.layers[i].hasCanvas() && currFile.layers[i].isVisible) {
|
||||
visibleLayers.push(currFile.layers[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Sorting them by z-index
|
||||
visibleLayers.sort((a, b) => (a.canvas.style.zIndex > b.canvas.style.zIndex) ? -1 : 1);
|
||||
// Selecting the last visible layer (the only one that won't get deleted)
|
||||
visibleLayers[visibleLayers.length - 1].selectLayer();
|
||||
|
||||
// Merging all the layer but the last one
|
||||
for (let i=0; i<visibleLayers.length - 1; i++) {
|
||||
nToFlatten++;
|
||||
|
||||
new HistoryState().FlattenTwoVisibles(
|
||||
visibleLayers[i + 1].context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]),
|
||||
visibleLayers[i].menuEntry.nextElementSibling,
|
||||
currFile.layers.indexOf(visibleLayers[i]),
|
||||
visibleLayers[i], visibleLayers[i + 1]
|
||||
);
|
||||
|
||||
LayerList.mergeLayers(visibleLayers[i + 1].context, visibleLayers[i].context);
|
||||
|
||||
// Deleting the above layer
|
||||
visibleLayers[i].canvas.remove();
|
||||
visibleLayers[i].menuEntry.remove();
|
||||
currFile.layers.splice(currFile.layers.indexOf(visibleLayers[i]), 1);
|
||||
}
|
||||
|
||||
new HistoryState().FlattenVisible(nToFlatten);
|
||||
// Updating the layer preview
|
||||
currFile.currentLayer.updateLayerPreview();
|
||||
}
|
||||
}
|
||||
|
||||
function openOptionsMenu(event) {
|
||||
if (event.which == 3) {
|
||||
let selectedId;
|
||||
let target = event.target;
|
||||
|
||||
while (target != null && target.classList != null && !target.classList.contains("layers-menu-entry")) {
|
||||
target = target.parentElement;
|
||||
}
|
||||
|
||||
selectedId = target.id;
|
||||
|
||||
Layer.layerOptions.style.visibility = "visible";
|
||||
Layer.layerOptions.style.top = "0";
|
||||
Layer.layerOptions.style.marginTop = "" + (event.clientY - 25) + "px";
|
||||
|
||||
getLayerByID(selectedId).selectLayer(false);
|
||||
}
|
||||
}
|
||||
|
||||
function closeOptionsMenu(event) {
|
||||
Layer.layerOptions.style.visibility = "hidden";
|
||||
currFile.currentLayer.rename();
|
||||
renamingLayer = false;
|
||||
}
|
||||
|
||||
function getLayerListEntries() {
|
||||
return layerList;
|
||||
}
|
||||
|
||||
function isRenamingLayer() {
|
||||
return renamingLayer;
|
||||
}
|
||||
|
||||
return {
|
||||
addLayer,
|
||||
mergeLayers,
|
||||
getLayerByID,
|
||||
getLayerByName,
|
||||
renameLayer: startRenamingLayer,
|
||||
duplicateLayer,
|
||||
deleteLayer,
|
||||
merge,
|
||||
flatten,
|
||||
closeOptionsMenu,
|
||||
getLayerListEntries,
|
||||
isRenamingLayer
|
||||
}
|
||||
})();
|
|
@ -0,0 +1,299 @@
|
|||
const PaletteBlock = (() => {
|
||||
// HTML elements
|
||||
let coloursList = document.getElementById("palette-list");
|
||||
|
||||
// PaletteBlock-specific data
|
||||
let currentSquareSize = coloursList.children[0].clientWidth;
|
||||
let blockData = {blockWidth: 300, blockHeight: 320, squareSize: 40};
|
||||
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);
|
||||
|
||||
Events.on("click", "pb-addcolours", addColours);
|
||||
Events.on("click", "pb-removecolours", removeColours);
|
||||
|
||||
/** 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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Initializes the palette block
|
||||
function init() {
|
||||
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(Color.cssToHex(simplePalette.children[i].children[0].style.backgroundColor));
|
||||
}
|
||||
}
|
||||
|
||||
/** 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 = Color.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.bind(this));
|
||||
li.addEventListener("mouseup", endRampSelection.bind(this));
|
||||
li.addEventListener("mousemove", updateRampSelection.bind(this));
|
||||
li.addEventListener("onclick", endRampSelection.bind(this));
|
||||
|
||||
coloursList.appendChild(li);
|
||||
}
|
||||
}
|
||||
/** Adds all the colours currently selected in the colour picker
|
||||
*
|
||||
*/
|
||||
function addColours() {
|
||||
let colours = ColorPicker.getSelectedColours();
|
||||
|
||||
for (let i=0; i<colours.length; i++) {
|
||||
addSingleColour(colours[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/** Removes all the currently selected colours from the palette
|
||||
*
|
||||
*/
|
||||
function removeColours() {
|
||||
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);
|
||||
}
|
||||
else if (mouseEvent.which == 1) {
|
||||
endRampSelection(mouseEvent);
|
||||
}
|
||||
}
|
||||
|
||||
/** Updates the outline for the current selection.
|
||||
*
|
||||
* @param {*} mouseEvent
|
||||
*/
|
||||
function updateRampSelection(mouseEvent) {
|
||||
if (mouseEvent != null && mouseEvent.buttons == 2) {
|
||||
currentSelection.endIndex = getElementIndex(mouseEvent.target);
|
||||
}
|
||||
|
||||
if (mouseEvent == null || mouseEvent.buttons == 2) {
|
||||
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 = Color.cssToHex(coloursList.children[currentSelection.startIndex].style.backgroundColor);
|
||||
ColorPicker.updatePickerByHex(col);
|
||||
ColorPicker.updateSlidersByHex(col);
|
||||
ColorPicker.updateMiniPickerColour();
|
||||
|
||||
updateRampSelection();
|
||||
|
||||
currentSelection.startCoords = [];
|
||||
}
|
||||
|
||||
/** 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();
|
||||
}
|
||||
|
||||
return {
|
||||
init
|
||||
}
|
||||
})();
|
|
@ -17,6 +17,7 @@ const PresetModule = (() => {
|
|||
presetsMenu.appendChild(button);
|
||||
|
||||
button.addEventListener('click', () => {
|
||||
console.log("Preset: " + presetName);
|
||||
//change dimentions on new pixel form
|
||||
Util.setValue('size-width', presets[presetName].width);
|
||||
Util.setValue('size-height', presets[presetName].height);
|
||||
|
@ -30,7 +31,6 @@ const PresetModule = (() => {
|
|||
|
||||
//set the text of the dropdown to the newly selected preset
|
||||
Util.setText('preset-button', presetName);
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
const Settings = (() => {
|
||||
let settings;
|
||||
let settingsFromCookie;
|
||||
|
||||
//on clicking the save button in the settings dialog
|
||||
Events.on('click', 'save-settings', saveSettings);
|
||||
|
||||
init();
|
||||
|
||||
function init() {
|
||||
if (!Cookies.enabled) {
|
||||
document.getElementById('cookies-disabled-warning').style.display = 'block';
|
||||
}
|
||||
settingsFromCookie = Cookies.get('pixelEditorSettings');
|
||||
|
||||
if(!settingsFromCookie) {
|
||||
console.log('settings cookie not found');
|
||||
|
||||
settings = {
|
||||
switchToChangedColor: true,
|
||||
enableDynamicCursorOutline: true, //unused - performance
|
||||
enableBrushPreview: true, //unused - performance
|
||||
enableEyedropperPreview: true, //unused - performance
|
||||
numberOfHistoryStates: 256,
|
||||
maxColorsOnImportedImage: 128,
|
||||
pixelGridColour: '#000000'
|
||||
};
|
||||
}
|
||||
else{
|
||||
console.log('settings cookie found');
|
||||
console.log(settingsFromCookie);
|
||||
|
||||
settings = JSON.parse(settingsFromCookie);
|
||||
}
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
//check if values are valid
|
||||
if (isNaN(Util.getValue('setting-numberOfHistoryStates'))) {
|
||||
alert('Invalid value for numberOfHistoryStates');
|
||||
return;
|
||||
}
|
||||
|
||||
//save new settings to settings object
|
||||
settings.numberOfHistoryStates = Util.getValue('setting-numberOfHistoryStates');
|
||||
settings.pixelGridColour = Util.getValue('setting-pixelGridColour');
|
||||
// Filling pixel grid again if colour changed
|
||||
Events.emit("refreshPixelGrid");
|
||||
|
||||
//save settings object to cookie
|
||||
let cookieValue = JSON.stringify(settings);
|
||||
Cookies.set('pixelEditorSettings', cookieValue, { expires: Infinity });
|
||||
|
||||
//close window
|
||||
Dialogue.closeDialogue();
|
||||
}
|
||||
|
||||
function getCurrSettings() {
|
||||
return settings;
|
||||
}
|
||||
|
||||
return {
|
||||
getCurrSettings
|
||||
}
|
||||
|
||||
})();
|
|
@ -0,0 +1,39 @@
|
|||
const SplashPage = (() => {
|
||||
const 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')
|
||||
];
|
||||
const coverImage = document.getElementById('editor-logo');
|
||||
const authorLink = coverImage.getElementsByTagName('a')[0];
|
||||
const chosenImage = images[Math.round(Math.random() * (images.length - 1))];
|
||||
|
||||
initSplashPage();
|
||||
|
||||
function initSplashPage() {
|
||||
coverImage.style.backgroundImage = 'url("' + chosenImage.path + '.png")';
|
||||
authorLink.setAttribute('href', chosenImage.link);
|
||||
authorLink.innerHTML = 'Art by ' + chosenImage.author;
|
||||
|
||||
Dialogue.showDialogue("splash", false);
|
||||
}
|
||||
|
||||
function SplashCoverImage(path, author, link) {
|
||||
this.path = path;
|
||||
this.author = author;
|
||||
this.link = link;
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,238 @@
|
|||
const Startup = (() => {
|
||||
let splashPostfix = '';
|
||||
|
||||
Events.on('click', 'create-button', create, false);
|
||||
Events.on('click', 'create-button-splash', create, true);
|
||||
|
||||
function create(isSplash) {
|
||||
// If I'm creating from the splash menu, I append '-splash' so I get the corresponding values
|
||||
if (isSplash)
|
||||
splashPostfix = '-splash';
|
||||
else
|
||||
splashPostfix = '';
|
||||
|
||||
var width = Util.getValue('size-width' + splashPostfix);
|
||||
var height = Util.getValue('size-height' + splashPostfix);
|
||||
var selectedPalette = Util.getText('palette-button' + splashPostfix);
|
||||
|
||||
newPixel(width, height);
|
||||
resetInput();
|
||||
|
||||
//track google event
|
||||
if (typeof ga !== 'undefined')
|
||||
ga('send', 'event', 'Pixel Editor New', selectedPalette, width+'/'+height); /*global ga*/
|
||||
}
|
||||
|
||||
/** Creates a new, empty file
|
||||
*
|
||||
* @param {*} width Start width of the canvas
|
||||
* @param {*} height Start height of the canvas
|
||||
* @param {*} fileContent If fileContent != null, then the newPixel is being called from the open menu
|
||||
*/
|
||||
function newPixel (width, height, fileContent = null) {
|
||||
// The palette is empty, at the beginning
|
||||
ColorModule.resetPalette();
|
||||
|
||||
initLayers(width, height);
|
||||
initPalette();
|
||||
|
||||
// Closing the "New Pixel dialogue"
|
||||
Dialogue.closeDialogue();
|
||||
// Updating the cursor of the current tool
|
||||
ToolManager.currentTool().updateCursor();
|
||||
|
||||
// The user is now able to export the Pixel
|
||||
document.getElementById('export-button').classList.remove('disabled');
|
||||
|
||||
// Now, if I opened an LPE file
|
||||
if (fileContent != null) {
|
||||
loadFromLPE(fileContent);
|
||||
// Deleting the default layer
|
||||
LayerList.deleteLayer(false);
|
||||
// Selecting the new one
|
||||
currFile.layers[1].selectLayer();
|
||||
}
|
||||
|
||||
EditorState.switchMode(EditorState.getCurrentMode());
|
||||
// This is not the first Pixel anymore
|
||||
EditorState.created();
|
||||
}
|
||||
|
||||
function initLayers(width, height) {
|
||||
// Setting the general canvasSize
|
||||
currFile.canvasSize = [width, height];
|
||||
|
||||
// If this is the first pixel I'm creating since the app has started
|
||||
if (EditorState.firstPixel()) {
|
||||
// Creating the first layer
|
||||
currFile.currentLayer = new Layer(width, height, 'pixel-canvas', "");
|
||||
currFile.currentLayer.canvas.style.zIndex = 2;
|
||||
}
|
||||
else {
|
||||
// Deleting all the extra layers and canvases, leaving only one
|
||||
let nLayers = currFile.layers.length;
|
||||
for (let i=2; i < currFile.layers.length - nAppLayers; i++) {
|
||||
let currentEntry = currFile.layers[i].menuEntry;
|
||||
let associatedLayer;
|
||||
|
||||
if (currentEntry != null) {
|
||||
// Getting the associated layer
|
||||
associatedLayer = LayerList.getLayerByID(currentEntry.id);
|
||||
|
||||
// Deleting its canvas
|
||||
associatedLayer.canvas.remove();
|
||||
|
||||
// Adding the id to the unused ones
|
||||
Layer.unusedIDs.push(currentEntry.id);
|
||||
// Removing the entry from the menu
|
||||
currentEntry.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Removing the old layers from the list
|
||||
for (let i=2; i<nLayers - nAppLayers; i++) {
|
||||
currFile.layers.splice(2, 1);
|
||||
}
|
||||
|
||||
// Setting up the current layer
|
||||
currFile.layers[1] = new Layer(width, height, currFile.layers[1].canvas, currFile.layers[1].menuEntry);
|
||||
currFile.currentLayer = currFile.layers[1];
|
||||
currFile.currentLayer.canvas.style.zIndex = 2;
|
||||
}
|
||||
|
||||
// Adding the checkerboard behind it
|
||||
currFile.checkerBoard = new Checkerboard(width, height, null);
|
||||
// Pixel grid
|
||||
console.log("CREATED GRID");
|
||||
currFile.pixelGrid = new PixelGrid(width, height, "pixel-grid");
|
||||
|
||||
// Creating the vfx layer on top of everything
|
||||
currFile.VFXLayer = new Layer(width, height, 'vfx-canvas');
|
||||
// Tmp layer to draw previews on
|
||||
currFile.TMPLayer = new Layer(width, height, 'tmp-canvas');
|
||||
|
||||
if (EditorState.firstPixel()) {
|
||||
// Adding the first layer and the checkerboard to the list of layers
|
||||
currFile.layers.push(currFile.checkerBoard);
|
||||
currFile.layers.push(currFile.currentLayer);
|
||||
currFile.layers.push(currFile.TMPLayer);
|
||||
currFile.layers.push(currFile.pixelGrid);
|
||||
currFile.layers.push(currFile.VFXLayer);
|
||||
}
|
||||
}
|
||||
|
||||
function initPalette() {
|
||||
// Get selected palette
|
||||
let selectedPalette = Util.getText('palette-button' + splashPostfix);
|
||||
|
||||
//remove current palette
|
||||
let colors = document.getElementsByClassName('color-button');
|
||||
while (colors.length > 0) {
|
||||
colors[0].parentElement.remove();
|
||||
}
|
||||
|
||||
// If the user selected a palette and isn't opening a file, I load the selected palette
|
||||
if (selectedPalette != 'Choose a palette...') {
|
||||
if (selectedPalette === 'Loaded palette') {
|
||||
ColorModule.createColorPalette(palettes['Loaded palette'].colors);
|
||||
}
|
||||
else {
|
||||
//if this palette isnt the one specified in the url, then reset the url
|
||||
if (!palettes[selectedPalette].specified)
|
||||
history.pushState(null, null, '/pixel-editor');
|
||||
|
||||
//fill the palette with specified colours
|
||||
ColorModule.createColorPalette(palettes[selectedPalette].colors);
|
||||
}
|
||||
}
|
||||
// Otherwise, I just generate 2 semirandom colours
|
||||
else {
|
||||
//this wasn't a specified palette, so reset the url
|
||||
history.pushState(null, null, '/pixel-editor');
|
||||
|
||||
//generate default colors
|
||||
var fg = new Color("hsv", Math.floor(Math.random()*360), 50, 50).rgb;
|
||||
var bg = new Color("hsv", Math.floor(Math.random()*360), 80, 100).rgb;
|
||||
|
||||
//convert colors to hex
|
||||
var defaultForegroundColor = Color.rgbToHex(fg);
|
||||
var defaultBackgroundColor = Color.rgbToHex(bg);
|
||||
|
||||
//add colors to palette
|
||||
ColorModule.addColor(defaultForegroundColor).classList.add('selected');
|
||||
ColorModule.addColor(defaultBackgroundColor);
|
||||
|
||||
//set current drawing color as foreground color
|
||||
ColorModule.updateCurrentColor('#'+defaultForegroundColor);
|
||||
selectedPalette = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function loadFromLPE(fileContent) {
|
||||
// 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'];
|
||||
|
||||
if (layerData != null) {
|
||||
// Setting id
|
||||
let createdLayer = LayerList.addLayer(layerData.id, false);
|
||||
// Setting name
|
||||
createdLayer.menuEntry.getElementsByTagName("p")[0].innerHTML = layerData.name;
|
||||
|
||||
// Adding the image (I can do that because they're sorted by increasing z-index)
|
||||
let img = new Image();
|
||||
img.onload = function() {
|
||||
createdLayer.context.drawImage(img, 0, 0);
|
||||
createdLayer.updateLayerPreview();
|
||||
|
||||
if (i == (fileContent['nLayers'] - 1)) {
|
||||
ColorModule.createPaletteFromLayers();
|
||||
}
|
||||
};
|
||||
|
||||
img.src = layerImage;
|
||||
|
||||
// Setting visibility and lock options
|
||||
if (!layerData.isVisible) {
|
||||
createdLayer.hide();
|
||||
}
|
||||
if (layerData.isLocked) {
|
||||
createdLayer.lock();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resetInput() {
|
||||
//reset new form
|
||||
Util.setValue('size-width', 64);
|
||||
Util.setValue('size-height', 64);
|
||||
|
||||
Util.setText('palette-button', 'Choose a palette...');
|
||||
Util.setText('preset-button', 'Choose a preset...');
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
function splashEditorMode(mode) {
|
||||
editorMode = mode;
|
||||
}
|
||||
|
||||
return {
|
||||
create,
|
||||
newPixel,
|
||||
newFromTemplate,
|
||||
splashEditorMode
|
||||
}
|
||||
})();
|
|
@ -0,0 +1,126 @@
|
|||
// REFACTOR: this is a nice base for the Tool class
|
||||
//tools container / list, automatically managed when you create a new Tool();
|
||||
var tool = {};
|
||||
|
||||
//class for tools
|
||||
class Tool {
|
||||
name = "AbstractTool";
|
||||
isSelected = false;
|
||||
|
||||
// Cursor and brush size
|
||||
cursorType = {};
|
||||
cursor = undefined;
|
||||
cursorHTMLElement = undefined;
|
||||
|
||||
// Useful coordinates
|
||||
startMousePos = {};
|
||||
currMousePos = {};
|
||||
prevMousePos = {};
|
||||
endMousePos = {};
|
||||
|
||||
// HTML elements
|
||||
mainButton = undefined;
|
||||
biggerButton = undefined;
|
||||
smallerButton = undefined;
|
||||
brushPreview = document.getElementById("brush-preview");
|
||||
|
||||
constructor (name, options) {
|
||||
this.name = name;
|
||||
this.cursorType = options;
|
||||
|
||||
this.mainButton = document.getElementById(name + "-button");
|
||||
this.biggerButton = document.getElementById(name + "-bigger-button");
|
||||
this.smallerButton = document.getElementById(name + "-smaller-button");
|
||||
}
|
||||
|
||||
onSelect() {
|
||||
if (this.mainButton != undefined)
|
||||
this.mainButton.parentElement.classList.add("selected");
|
||||
this.isSelected = true;
|
||||
|
||||
switch (this.cursorType.type) {
|
||||
case 'html':
|
||||
currFile.canvasView.style.cursor = 'none';
|
||||
break;
|
||||
case 'cursor':
|
||||
this.cursor = this.cursorType.style;
|
||||
currFile.canvasView.style.cursor = this.cursor || 'default';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
updateCursor() {
|
||||
this.brushPreview.style.display = 'block';
|
||||
this.brushPreview.style.width = this.currSize * currFile.zoom + 'px';
|
||||
this.brushPreview.style.height = this.currSize * currFile.zoom + 'px';
|
||||
}
|
||||
|
||||
onMouseWheel(mousePos, mode) {}
|
||||
|
||||
onHover(cursorLocation, cursorTarget) {
|
||||
this.prevMousePos = this.currMousePos;
|
||||
this.currMousePos = cursorLocation;
|
||||
|
||||
this.updateCursor();
|
||||
|
||||
let toSub = 1;
|
||||
// Prevents the brush to be put in the middle of pixels
|
||||
if (this.currSize % 2 == 0) {
|
||||
toSub = 0.5;
|
||||
}
|
||||
|
||||
this.brushPreview.style.left = (Math.floor(cursorLocation[0] / currFile.zoom) * currFile.zoom + currFile.currentLayer.canvas.offsetLeft - this.currSize * currFile.zoom / 2 - currFile.zoom / 2 + toSub * currFile.zoom) + 'px';
|
||||
this.brushPreview.style.top = (Math.floor(cursorLocation[1] / currFile.zoom) * currFile.zoom + currFile.currentLayer.canvas.offsetTop - this.currSize * currFile.zoom / 2 - currFile.zoom / 2 + toSub * currFile.zoom) + 'px';
|
||||
|
||||
if (this.cursorType.type == 'html') {
|
||||
if (cursorTarget.className == 'drawingCanvas'|| cursorTarget.className == 'drawingCanvas') {
|
||||
this.brushPreview.style.visibility = 'visible';
|
||||
currFile.canvasView.style.cursor = 'none';
|
||||
}
|
||||
else {
|
||||
this.brushPreview.style.visibility = 'hidden';
|
||||
currFile.canvasView.style.cursor = 'default';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onDeselect() {
|
||||
if (this.mainButton != undefined)
|
||||
this.mainButton.parentElement.classList.remove("selected");
|
||||
this.isSelected = false;
|
||||
|
||||
this.brushPreview.style.visibility = 'hidden';
|
||||
currFile.canvasView.style.cursor = 'default';
|
||||
}
|
||||
|
||||
onStart(mousePos) {
|
||||
this.startMousePos = mousePos;
|
||||
}
|
||||
|
||||
onDrag(mousePos) {
|
||||
}
|
||||
|
||||
onEnd(mousePos) {
|
||||
this.endMousePos = mousePos;
|
||||
}
|
||||
|
||||
increaseSize() {
|
||||
if (this.currSize < 128) {
|
||||
this.currSize++;
|
||||
this.updateCursor();
|
||||
}
|
||||
}
|
||||
|
||||
decreaseSize() {
|
||||
if (this.currSize > 1) {
|
||||
this.currSize--;
|
||||
this.updateCursor();
|
||||
}
|
||||
}
|
||||
|
||||
get size() {
|
||||
return this.currSize;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
const ToolManager = (() => {
|
||||
tools = {};
|
||||
isPanning = false;
|
||||
|
||||
tools["brush"] = new BrushTool("brush", {type: 'html'}, switchTool);
|
||||
tools["eraser"] = new EraserTool("eraser", {type: 'html'}, switchTool);
|
||||
tools["rectangle"] = new RectangleTool("rectangle", {type: 'html'}, switchTool);
|
||||
tools["line"] = new LineTool("line", {type: 'html'}, switchTool);
|
||||
tools["fill"] = new FillTool("fill", {type: 'cursor', style: 'crosshair'}, switchTool);
|
||||
|
||||
tools["eyedropper"] = new EyedropperTool("eyedropper", {type: 'cursor', style: 'crosshair'}, switchTool);
|
||||
tools["pan"] = new PanTool("pan", {type: 'custom'}, switchTool);
|
||||
tools["zoom"] = new ZoomTool("zoom", {type:'custom'});
|
||||
|
||||
tools["moveselection"] = new MoveSelectionTool("moveselection",
|
||||
{type:'cursor', style:'crosshair'}, switchTool, tools["brush"]);
|
||||
tools["rectselect"] = new RectangularSelectionTool("rectselect",
|
||||
{type: 'cursor', style:'crosshair'}, switchTool, tools["moveselection"]);
|
||||
|
||||
currTool = tools["brush"];
|
||||
currTool.onSelect();
|
||||
currFile.canvasView.style.cursor = 'default';
|
||||
|
||||
Events.on("mouseup", window, onMouseUp);
|
||||
Events.on("mousemove", window, onMouseMove);
|
||||
Events.on("mousedown", window, onMouseDown);
|
||||
Events.on("wheel", window, onMouseWheel);
|
||||
|
||||
// Bind tool shortcuts
|
||||
Events.onCustom("tool-shortcut", onShortcut);
|
||||
|
||||
function onShortcut(tool) {
|
||||
if (!EditorState.documentCreated || Dialogue.isOpen())
|
||||
return;
|
||||
switchTool(tools[tool]);
|
||||
}
|
||||
|
||||
function onMouseWheel(mouseEvent) {
|
||||
console.log("MOUSE WHEEL");
|
||||
if (!EditorState.documentCreated || Dialogue.isOpen())
|
||||
return;
|
||||
|
||||
let mousePos = Input.getCursorPosition(mouseEvent);
|
||||
tools["zoom"].onMouseWheel(mousePos, mouseEvent.deltaY < 0 ? 'in' : 'out');
|
||||
}
|
||||
|
||||
function onMouseDown(mouseEvent) {
|
||||
if (!EditorState.documentCreated() || Dialogue.isOpen())
|
||||
return;
|
||||
|
||||
let mousePos = Input.getCursorPosition(mouseEvent);
|
||||
|
||||
if (!Input.isDragging()) {
|
||||
switch(mouseEvent.which) {
|
||||
case 1:
|
||||
if (Input.isSpacePressed()) {
|
||||
tools["pan"].onStart(mousePos, mouseEvent.target);
|
||||
}
|
||||
else if (Input.isAltPressed()) {
|
||||
tools["eyedropper"].onStart(mousePos, mouseEvent.target);
|
||||
}
|
||||
else if (!currFile.currentLayer.isLocked || !((Object.getPrototypeOf(currTool) instanceof DrawingTool))) {
|
||||
currTool.onStart(mousePos, mouseEvent.target);
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
tools["pan"].onStart(mousePos, mouseEvent.target);
|
||||
break;
|
||||
case 3:
|
||||
currTool.onRightStart(mousePos, mouseEvent.target);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseMove(mouseEvent) {
|
||||
if (!EditorState.documentCreated() || Dialogue.isOpen())
|
||||
return;
|
||||
let mousePos = Input.getCursorPosition(mouseEvent);
|
||||
// Call the hover event
|
||||
currTool.onHover(mousePos, mouseEvent.target);
|
||||
|
||||
if (Input.isDragging()) {
|
||||
switch (mouseEvent.buttons) {
|
||||
case 1:
|
||||
if (Input.isSpacePressed()) {
|
||||
tools["pan"].onDrag(mousePos, mouseEvent.target);
|
||||
}
|
||||
else if (Input.isAltPressed()) {
|
||||
tools["eyedropper"].onDrag(mousePos, mouseEvent.target);
|
||||
}
|
||||
else if (!currFile.currentLayer.isLocked || !((Object.getPrototypeOf(currTool) instanceof DrawingTool))){
|
||||
currTool.onDrag(mousePos, mouseEvent.target);
|
||||
}
|
||||
break;
|
||||
case 4:
|
||||
tools["pan"].onDrag(mousePos, mouseEvent.target);
|
||||
break;
|
||||
case 2:
|
||||
currTool.onRightDrag(mousePos, mouseEvent.target);
|
||||
break;
|
||||
default:
|
||||
console.log("wtf");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseUp(mouseEvent) {
|
||||
if (!EditorState.documentCreated())
|
||||
return;
|
||||
let mousePos = Input.getCursorPosition(mouseEvent);
|
||||
|
||||
if (Input.isDragging()) {
|
||||
switch(mouseEvent.which) {
|
||||
case 1:
|
||||
if (Input.isSpacePressed()) {
|
||||
tools["pan"].onEnd(mousePos, mouseEvent.target);
|
||||
}
|
||||
else if (Input.isAltPressed()) {
|
||||
tools["eyedropper"].onEnd(mousePos, mouseEvent.target);
|
||||
}
|
||||
else if (!currFile.currentLayer.isLocked || !((Object.getPrototypeOf(currTool) instanceof DrawingTool))) {
|
||||
currTool.onEnd(mousePos);
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
tools["pan"].onEnd(mousePos);
|
||||
break;
|
||||
case 3:
|
||||
currTool.onRightEnd(mousePos, mouseEvent.target);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function currentTool() {
|
||||
return currTool;
|
||||
}
|
||||
|
||||
function switchTool(newTool) {
|
||||
currTool.onDeselect();
|
||||
currTool = newTool;
|
||||
currTool.onSelect();
|
||||
}
|
||||
|
||||
return {
|
||||
currentTool
|
||||
}
|
||||
})();
|
|
@ -0,0 +1,99 @@
|
|||
const TopMenuModule = (() => {
|
||||
|
||||
const mainMenuItems = document.getElementById('main-menu').children;
|
||||
|
||||
initMenu();
|
||||
|
||||
function initMenu() {
|
||||
//for each button in main menu (starting at 1 to avoid logo)
|
||||
for (let i = 1; i < mainMenuItems.length; i++) {
|
||||
|
||||
//get the button that's in the list item
|
||||
const menuItem = mainMenuItems[i];
|
||||
const menuButton = menuItem.children[0];
|
||||
|
||||
//when you click a main menu items button
|
||||
Events.on('click', menuButton, function (e) {
|
||||
// Close the already open menus
|
||||
closeMenu();
|
||||
// Select the item
|
||||
Util.select(e.target.parentElement);
|
||||
});
|
||||
|
||||
const subMenu = menuItem.children[1];
|
||||
const subMenuItems = subMenu.children;
|
||||
|
||||
//when you click an item within a menu button
|
||||
for (var j = 0; j < subMenuItems.length; j++) {
|
||||
|
||||
const currSubmenuItem = subMenuItems[j];
|
||||
const currSubmenuButton = currSubmenuItem.children[0];
|
||||
|
||||
switch (currSubmenuButton.textContent) {
|
||||
case 'New':
|
||||
Events.on('click', currSubmenuButton, Dialogue.showDialogue, 'new-pixel');
|
||||
break;
|
||||
case 'Save project':
|
||||
Events.on('click', currSubmenuButton, FileManager.openSaveProjectWindow);
|
||||
break;
|
||||
case 'Open':
|
||||
Events.on('click', currSubmenuButton, FileManager.open);
|
||||
break;
|
||||
case 'Export':
|
||||
Events.on('click', currSubmenuButton, FileManager.openPixelExportWindow);
|
||||
break;
|
||||
case 'Exit':
|
||||
//if a document exists, make sure they want to delete it
|
||||
if (EditorState.documentCreated()) {
|
||||
//ask user if they want to leave
|
||||
if (confirm('Exiting will discard your current pixel. Are you sure you want to do that?'))
|
||||
//skip onbeforeunload prompt
|
||||
window.onbeforeunload = null;
|
||||
else
|
||||
e.preventDefault();
|
||||
}
|
||||
break;
|
||||
// REFACTOR: move the binding to the Selection IIFE or something like that once it's done
|
||||
case 'Paste':
|
||||
Events.on('click', currSubmenuButton, function(){Events.emit("ctrl+v");});
|
||||
break;
|
||||
case 'Copy':
|
||||
Events.on('click', currSubmenuButton, function(){Events.emit("ctrl+c");});
|
||||
break;
|
||||
case 'Cut':
|
||||
Events.on('click', currSubmenuButton, function(){Events.emit("ctrl+x");});
|
||||
break;
|
||||
case 'Cancel':
|
||||
Events.on('click', currSubmenuButton, function(){Events.emit("esc-pressed")});
|
||||
break;
|
||||
//Help Menu
|
||||
case 'Settings':
|
||||
Events.on('click', currSubmenuButton, Dialogue.showDialogue, 'settings');
|
||||
break;
|
||||
case 'Help':
|
||||
Events.on('click', currSubmenuButton, Dialogue.showDialogue, 'help');
|
||||
break;
|
||||
case 'About':
|
||||
Events.on('click', currSubmenuButton, Dialogue.showDialogue, 'about');
|
||||
break;
|
||||
case 'Changelog':
|
||||
Events.on('click', currSubmenuButton, Dialogue.showDialogue, 'changelog');
|
||||
break;
|
||||
}
|
||||
|
||||
Events.on('click', currSubmenuButton, function() {TopMenuModule.closeMenu();});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function closeMenu () {
|
||||
//remove .selected class from all menu buttons
|
||||
for (var i = 0; i < mainMenuItems.length; i++) {
|
||||
Util.deselect(mainMenuItems[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
closeMenu
|
||||
}
|
||||
})();
|
84
js/Util.js
84
js/Util.js
|
@ -1,35 +1,89 @@
|
|||
// 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;
|
||||
|
||||
/** Tells if a pixel is empty (has alpha = 0)
|
||||
*
|
||||
* @param {*} pixel
|
||||
*/
|
||||
static isPixelEmpty(pixel) {
|
||||
if (pixel == null || pixel === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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
|
||||
*/
|
||||
static 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;
|
||||
}
|
||||
|
||||
/** Returns elementOrElementId if the argument is already an element, otherwise it finds
|
||||
* the element by its ID (given by the argument) and returns it
|
||||
*
|
||||
* @param {*} elementOrElementId The element to return, or the ID of the element to return
|
||||
* @returns The desired element
|
||||
*/
|
||||
static getElement(elementOrElementId) {
|
||||
if (typeof(elementOrElementId) == "object") {
|
||||
return elementOrElementId;
|
||||
}
|
||||
else if (typeof(elementOrElementId) == "string") {
|
||||
return document.getElementById(elementOrElementId);
|
||||
}
|
||||
else {
|
||||
console.log("Type not supported: " + typeof(elementOrElementId));
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the text content of the element with ID elementId
|
||||
static getText(elementId) {
|
||||
return Util.getElement(elementId).textContent;
|
||||
}
|
||||
// Sets the text content of the element with ID elementId
|
||||
static setText(elementId, text) {
|
||||
this.getElement(elementId).textContent = text;
|
||||
Util.getElement(elementId).textContent = text;
|
||||
}
|
||||
|
||||
// Gets the value of the element with ID elementId
|
||||
static getValue(elementId) {
|
||||
return this.getElement(elementId).value;
|
||||
return Util.getElement(elementId).value;
|
||||
}
|
||||
|
||||
// Sets the value of the element with ID elementId
|
||||
static setValue(elementId, value) {
|
||||
this.getElement(elementId).value = value;
|
||||
Util.getElement(elementId).value = value;
|
||||
}
|
||||
|
||||
//add class .selected to specified element
|
||||
static select(elementId) {
|
||||
this.getElement(elementId).classList.add('selected');
|
||||
Util.getElement(elementId).classList.add('selected');
|
||||
}
|
||||
|
||||
//remove .selected class from specified element
|
||||
static deselect(elementId) {
|
||||
this.getElement(elementId).classList.remove('selected');
|
||||
Util.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');
|
||||
Util.getElement(elementId).classList.toggle('selected');
|
||||
}
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
let currentPalette = [];
|
||||
|
||||
/** 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) != '#')
|
||||
newColor = '#' + newColor;
|
||||
currentPalette.push(newColor);
|
||||
//create list item
|
||||
var listItem = document.createElement('li');
|
||||
|
||||
//create button
|
||||
var button = document.createElement('button');
|
||||
button.classList.add('color-button');
|
||||
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]);
|
||||
|
||||
//add jscolor functionality
|
||||
initColor(button);
|
||||
|
||||
//add edit button
|
||||
var editButtonTemplate = document.getElementsByClassName('color-edit-button')[0];
|
||||
newEditButton = editButtonTemplate.cloneNode(true);
|
||||
listItem.appendChild(newEditButton);
|
||||
|
||||
//when you click the edit button
|
||||
on('click', newEditButton, function (event, button) {
|
||||
|
||||
//hide edit button
|
||||
button.parentElement.lastChild.classList.add('hidden');
|
||||
|
||||
//show jscolor picker, if basic mode is enabled
|
||||
if (pixelEditorMode == 'Basic')
|
||||
button.parentElement.firstChild.jscolor.show();
|
||||
else
|
||||
showDialogue("palette-block", false);
|
||||
});
|
||||
|
||||
return listItem;
|
||||
}
|
||||
|
||||
new Sortable(document.getElementById("colors-menu"), {
|
||||
animation:100,
|
||||
filter: ".noshrink",
|
||||
draggable: ".draggable-colour",
|
||||
onEnd: makeIsDraggingFalse
|
||||
});
|
|
@ -1,59 +0,0 @@
|
|||
// add-color-button management
|
||||
on('click', 'add-color-button', function(){
|
||||
if (!documentCreated) return;
|
||||
|
||||
var colorCheckingStyle = `
|
||||
color: white;
|
||||
background: #3c4cc2;
|
||||
`;
|
||||
|
||||
var colorIsUnique = true;
|
||||
do {
|
||||
//console.log('%cchecking for unique colors', colorCheckingStyle)
|
||||
//generate random color
|
||||
var hue = Math.floor(Math.random()*255);
|
||||
var sat = 130+Math.floor(Math.random()*100);
|
||||
var lit = 70+Math.floor(Math.random()*100);
|
||||
var newColorRgb = hslToRgb(hue,sat,lit);
|
||||
var newColor = rgbToHex(newColorRgb.r,newColorRgb.g,newColorRgb.b);
|
||||
|
||||
var newColorHex = newColor;
|
||||
|
||||
//check if color has been used before
|
||||
colors = document.getElementsByClassName('color-button');
|
||||
colorCheckingLoop: for (var i = 0; i < colors.length; i++) {
|
||||
//console.log('%c'+newColorHex +' '+ colors[i].jscolor.toString(), colorCheckingStyle)
|
||||
|
||||
//if generated color matches this color
|
||||
if (newColorHex == colors[i].jscolor.toString()) {
|
||||
//console.log('%ccolor already exists', colorCheckingStyle)
|
||||
|
||||
//start loop again
|
||||
colorIsUnique = false;
|
||||
|
||||
//exit
|
||||
break colorCheckingLoop;
|
||||
}
|
||||
}
|
||||
}
|
||||
while (colorIsUnique == false);
|
||||
|
||||
//remove current color selection
|
||||
document.querySelector('#colors-menu li.selected')?.classList.remove('selected');
|
||||
|
||||
//add new color and make it selected
|
||||
var addedColor = addColor(newColor);
|
||||
addedColor.classList.add('selected');
|
||||
currentLayer.context.fillStyle = '#' + newColor;
|
||||
|
||||
//add history state
|
||||
//saveHistoryState({type: 'addcolor', colorValue: addedColor.firstElementChild.jscolor.toString()});
|
||||
new HistoryStateAddColor(addedColor.firstElementChild.jscolor.toString());
|
||||
|
||||
//show color picker
|
||||
addedColor.firstElementChild.jscolor.show();
|
||||
console.log('showing picker');
|
||||
|
||||
//hide edit button
|
||||
addedColor.lastChild.classList.add('hidden');
|
||||
}, false);
|
|
@ -1,185 +0,0 @@
|
|||
// 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;
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
/** 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;
|
||||
|
||||
//change zoom level
|
||||
//if you want to zoom out, and the zoom isnt already at the smallest level
|
||||
if (direction == 'out' && zoom > 1) {
|
||||
zoom -= Math.ceil(zoom / 10);
|
||||
newWidth = canvasSize[0] * zoom;
|
||||
newHeight = canvasSize[1] * zoom;
|
||||
|
||||
//adjust canvas position
|
||||
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){
|
||||
zoom += Math.ceil(zoom/10);
|
||||
newWidth = canvasSize[0] * zoom;
|
||||
newHeight = canvasSize[1] * zoom;
|
||||
|
||||
//adjust canvas position
|
||||
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
|
||||
layers[0].resize();
|
||||
|
||||
// adjust brush size
|
||||
currentTool.updateCursor();
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
/////=include libraries/bowser.js
|
||||
|
||||
function closeCompatibilityWarning () {
|
||||
document.getElementById('compatibility-warning').style.visibility = 'hidden';
|
||||
}
|
||||
|
||||
console.log('checking compatibility');
|
||||
|
||||
//check browser/version
|
||||
if ((bowser.msie && bowser.version < 11) ||
|
||||
(bowser.firefox && bowser.version < 28) ||
|
||||
(bowser.chrome && bowser.version < 29) ||
|
||||
(bowser.msedge && bowser.version < 12) ||
|
||||
(bowser.safari && bowser.version < 9) ||
|
||||
(bowser.opera && bowser.version < 17) )
|
||||
//show warning
|
||||
document.getElementById('compatibility-warning').style.visibility = 'visible';
|
||||
|
||||
else alert(bowser.name+' '+bowser.version+' is fine!');
|
|
@ -1,71 +0,0 @@
|
|||
// This script contains all the functions used to manage the checkboard
|
||||
// Checkerboard color 1
|
||||
var firstCheckerBoardColor = 'rgba(179, 173, 182, 1)';
|
||||
// Checkerboard color 2
|
||||
var secondCheckerBoardColor = 'rgba(204, 200, 206, 1)';
|
||||
// Square size for the checkerboard
|
||||
var checkerBoardSquareSize = 16;
|
||||
// Checkerboard canvas
|
||||
var checkerBoardCanvas = document.getElementById('checkerboard');
|
||||
|
||||
// Setting current colour (each square has a different colour
|
||||
var currentColor = firstCheckerBoardColor;
|
||||
// Saving number of squares filled until now
|
||||
var nSquaresFilled = 0;
|
||||
|
||||
/** Fills the checkerboard canvas with squares with alternating colours
|
||||
*
|
||||
*/
|
||||
function fillCheckerboard() {
|
||||
// Getting checkerboard context
|
||||
var context = checkerBoard.context;
|
||||
context.clearRect(0, 0, canvasSize[0], canvasSize[1]);
|
||||
|
||||
// Cycling through the canvas (using it as a matrix)
|
||||
for (var i=0; i<canvasSize[0] / checkerBoardSquareSize; i++) {
|
||||
nSquaresFilled = 0;
|
||||
|
||||
for (var j=0; j<canvasSize[1] / checkerBoardSquareSize; j++) {
|
||||
var rectX;
|
||||
var rectY;
|
||||
|
||||
// Managing the not perfect squares (the ones at the sides if the canvas' sizes are not powers of checkerBoardSquareSize
|
||||
if (i * checkerBoardSquareSize < canvasSize[0]) {
|
||||
rectX = i * checkerBoardSquareSize;
|
||||
}
|
||||
else {
|
||||
rectX = canvasSize[0];
|
||||
}
|
||||
|
||||
if (j * checkerBoardSquareSize < canvasSize[1]) {
|
||||
rectY = j * checkerBoardSquareSize;
|
||||
}
|
||||
else {
|
||||
rectY = canvasSize[1];
|
||||
}
|
||||
|
||||
// Selecting the colour
|
||||
context.fillStyle = currentColor;
|
||||
context.fillRect(rectX, rectY, checkerBoardSquareSize, checkerBoardSquareSize);
|
||||
|
||||
// Changing colour
|
||||
changeCheckerboardColor();
|
||||
|
||||
nSquaresFilled++;
|
||||
}
|
||||
|
||||
// If the number of filled squares was even, I change colour for the next column
|
||||
if ((nSquaresFilled % 2) == 0) {
|
||||
changeCheckerboardColor();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Simply switches the checkerboard colour
|
||||
function changeCheckerboardColor(isVertical) {
|
||||
if (currentColor == firstCheckerBoardColor) {
|
||||
currentColor = secondCheckerBoardColor;
|
||||
} else if (currentColor == secondCheckerBoardColor) {
|
||||
currentColor = firstCheckerBoardColor;
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
//color in palette has been clicked
|
||||
function clickedColor (e){
|
||||
|
||||
//left clicked color
|
||||
if (e.which == 1) {
|
||||
// remove current color selection
|
||||
document.querySelector('#colors-menu li.selected')?.classList.remove('selected');
|
||||
|
||||
//set current color
|
||||
for (let i=1; i<layers.length - nAppLayers; i++) {
|
||||
layers[i].context.fillStyle = this.style.backgroundColor;
|
||||
}
|
||||
|
||||
currentGlobalColor = this.style.backgroundColor;
|
||||
//make color selected
|
||||
e.target.parentElement.classList.add('selected');
|
||||
|
||||
} else if (e.which == 3) { //right clicked color
|
||||
//console.log('right clicked color button');
|
||||
|
||||
//hide edit color button (to prevent it from showing)
|
||||
e.target.parentElement.lastChild.classList.add('hidden');
|
||||
|
||||
//show color picker
|
||||
e.target.jscolor.show();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
|
||||
|
||||
document.getElementById('jscolor-hex-input').addEventListener('change', colorChanged, false);
|
||||
|
||||
on('input', 'jscolor-hex-input', function (e) {
|
||||
|
||||
//get hex value
|
||||
var newColorHex = e.target.value.toLowerCase();
|
||||
|
||||
//if the color is not (yet) a valid hex color, exit this function and do nothing
|
||||
if (/^[0-9a-f]{6}$/i.test(newColorHex) == false)
|
||||
return;
|
||||
|
||||
//get currently editing color
|
||||
var currentlyEditedColor = document.getElementsByClassName('jscolor-active')[0];
|
||||
|
||||
//update the actual color picker to the inputted color
|
||||
currentlyEditedColor.firstChild.jscolor.fromString(newColorHex);
|
||||
|
||||
colorChanged(e);
|
||||
});
|
||||
|
||||
|
||||
//changes all of one color to another after being changed from color picker
|
||||
function colorChanged(e) {
|
||||
//console.log('colorChanged() to ' + e.target.value);
|
||||
//get colors
|
||||
var newColor = hexToRgb(e.target.value);
|
||||
var oldColor = e.target.oldColor;
|
||||
|
||||
currentPalette.splice(currentPalette.indexOf("#" + newColor), 1);
|
||||
|
||||
newColor.a = 255;
|
||||
|
||||
//save undo state
|
||||
new HistoryStateEditColor(e.target.value.toLowerCase(), rgbToHex(oldColor));
|
||||
|
||||
//get the currently selected color
|
||||
var currentlyEditedColor = document.getElementsByClassName('jscolor-active')[0];
|
||||
var duplicateColorWarning = document.getElementById('duplicate-color-warning');
|
||||
|
||||
//check if selected color already matches another color
|
||||
colors = document.getElementsByClassName('color-button');
|
||||
//console.log(colors);
|
||||
var colorCheckingStyle = 'background: #bc60c4; color: white';
|
||||
var newColorHex = e.target.value.toLowerCase();
|
||||
|
||||
//if the color is not a valid hex color, exit this function and do nothing
|
||||
if (/^[0-9a-f]{6}$/i.test(newColorHex) == false)
|
||||
return;
|
||||
|
||||
//loop through all colors in palette
|
||||
for (var i = 0; i < colors.length; i++) {
|
||||
|
||||
//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);
|
||||
|
||||
//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);
|
||||
|
||||
//show the duplicate color warning
|
||||
duplicateColorWarning.style.visibility = 'visible';
|
||||
|
||||
//shake warning icon
|
||||
duplicateColorWarning.classList.remove('shake');
|
||||
void duplicateColorWarning.offsetWidth;
|
||||
duplicateColorWarning.classList.add('shake');
|
||||
|
||||
//exit function without updating color
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//if the color being edited has a duplicate color warning, remove it
|
||||
duplicateColorWarning.style.visibility = 'hidden';
|
||||
|
||||
currentlyEditedColor.firstChild.jscolor.fromString(newColorHex);
|
||||
|
||||
replaceAllOfColor(oldColor, newColor);
|
||||
|
||||
//set new old color to changed color
|
||||
e.target.oldColor = newColor;
|
||||
currentPalette.push('#' + newColorHex);
|
||||
|
||||
//if this is the current color, update the drawing color
|
||||
if (e.target.colorElement.parentElement.classList.contains('selected')) {
|
||||
for (let i=1; i<layers.length - nAppLayers; i++) {
|
||||
layers[i].context.fillStyle = '#'+ rgbToHex(newColor.r,newColor.g,newColor.b);
|
||||
}
|
||||
|
||||
currentGlobalColor = newColor;
|
||||
}
|
||||
/* this is wrong and bad
|
||||
if (settings.switchToChangedColor) {
|
||||
|
||||
}*/
|
||||
}
|
||||
|
|
@ -1,806 +0,0 @@
|
|||
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);
|
||||
}
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
const MIN_Z_INDEX = -5000;
|
||||
const MAX_Z_INDEX = 5000;
|
|
@ -1,85 +0,0 @@
|
|||
// 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;
|
||||
|
||||
copiedStartX = startX;
|
||||
copiedStartY = startY;
|
||||
// Getting the selected pixels
|
||||
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);
|
||||
|
||||
// Setting up the move tool to move the pasted value
|
||||
selectionCanceled = false;
|
||||
imageDataToMove = clipboardData;
|
||||
firstTimeMove = false;
|
||||
isRectSelecting = false;
|
||||
|
||||
// Switching to the move tool
|
||||
tool.moveselection.switchTo();
|
||||
// Updating the rectangle preview
|
||||
moveSelection(
|
||||
copiedStartX + (copiedEndX - copiedStartX) / 2,
|
||||
copiedStartY + (copiedEndY - copiedStartY) / 2,
|
||||
clipboardData.width, clipboardData.height);
|
||||
//drawRect(copiedStartX, copiedEndX, copiedStartY, copiedEndY);
|
||||
}
|
||||
|
||||
/** Cuts the current selection and copies it to the clipboard
|
||||
*
|
||||
*/
|
||||
function cutSelectionTool() {
|
||||
// Saving the coordinates
|
||||
copiedEndX = endX;
|
||||
copiedEndY = endY;
|
||||
|
||||
copiedStartX = startX;
|
||||
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 {
|
||||
// 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);
|
||||
}
|
||||
}
|
|
@ -1,96 +0,0 @@
|
|||
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 : pixelEditorMode;
|
||||
|
||||
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);
|
||||
|
||||
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');
|
||||
|
||||
// Creating a new pixel with those properties
|
||||
newPixel(width, height);
|
||||
document.getElementById('new-pixel-warning').style.display = 'block';
|
||||
|
||||
//get selected palette name
|
||||
var selectedPalette = getText('palette-button');
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
setText('palette-button', 'Choose a palette...');
|
||||
setText('preset-button', 'Choose a preset...');
|
||||
});
|
|
@ -1,90 +0,0 @@
|
|||
|
||||
/** 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');
|
||||
while (colors.length > 0) {
|
||||
colors[0].parentElement.remove();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
var newColorHex = hexToRgb(newColor);
|
||||
|
||||
var lightestColorHex = hexToRgb(lightestColor);
|
||||
if (newColorHex.r + newColorHex.g + newColorHex.b > lightestColorHex.r + lightestColorHex.g + lightestColorHex.b)
|
||||
lightestColor = newColor;
|
||||
|
||||
var darkestColorHex = hexToRgb(darkestColor);
|
||||
if (newColorHex.r + newColorHex.g + newColorHex.b < darkestColorHex.r + darkestColorHex.g + darkestColorHex.b) {
|
||||
|
||||
//remove current color selection
|
||||
document.querySelector('#colors-menu li.selected')?.classList.remove('selected');
|
||||
|
||||
//set as current color
|
||||
newColorElement.classList.add('selected');
|
||||
|
||||
darkestColor = newColor;
|
||||
}
|
||||
}
|
||||
|
||||
//prepend # if not present
|
||||
if (!darkestColor.includes('#')) darkestColor = '#' + darkestColor;
|
||||
|
||||
//set as current color
|
||||
currentLayer.context.fillStyle = darkestColor;
|
||||
}
|
||||
|
||||
/** Creates the palette with the colours used in all the layers
|
||||
*
|
||||
*/
|
||||
function createPaletteFromLayers() {
|
||||
let colors = {};
|
||||
|
||||
for (let i=0; i<layers.length; i++) {
|
||||
if (layers[i].menuEntry != null) {
|
||||
let imageData = layers[i].context.getImageData(0, 0, layers[i].canvasSize[0], layers[i].canvasSize[1]).data;
|
||||
let dataLength = imageData.length;
|
||||
|
||||
for (let j=0; j<dataLength; j += 4) {
|
||||
if (!isPixelEmpty(imageData[j])) {
|
||||
let color = imageData[j]+','+imageData[j + 1]+','+imageData[j + 2];
|
||||
|
||||
if (!colors[color]) {
|
||||
colors[color] = {r:imageData[j],g:imageData[j + 1],b:imageData[j + 2]};
|
||||
|
||||
//don't allow more than 256 colors to be added
|
||||
if (Object.keys(colors).length >= settings.maxColorsOnImportedImage) {
|
||||
alert('The image loaded seems to have more than '+settings.maxColorsOnImportedImage+' colors.');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//create array out of colors object
|
||||
let colorPaletteArray = [];
|
||||
for (let color in colors) {
|
||||
if (colors.hasOwnProperty(color)) {
|
||||
colorPaletteArray.push('#'+rgbToHex(colors[color]));
|
||||
}
|
||||
}
|
||||
|
||||
//create palette from colors array
|
||||
createColorPalette(colorPaletteArray, true);
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
|
||||
|
||||
//called when the delete button is pressed on color picker
|
||||
//input color button or hex string
|
||||
function deleteColor (color) {
|
||||
const logStyle = 'background: #913939; color: white; padding: 5px;';
|
||||
|
||||
//console.log('%c'+'deleting color', logStyle);
|
||||
|
||||
//if color is a string, then find the corresponding button
|
||||
if (typeof color === 'string') {
|
||||
//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());
|
||||
|
||||
if (color == colors[i].jscolor.toString()) {
|
||||
//console.log('match');
|
||||
//set color to the color button
|
||||
color = colors[i];
|
||||
//console.log('found color', color);
|
||||
|
||||
//exit loop
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//if the color wasn't found
|
||||
if (typeof color === 'string') {
|
||||
//console.log('color not found');
|
||||
//exit function
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//hide color picker
|
||||
color.jscolor.hide();
|
||||
|
||||
|
||||
//find lightest color in palette
|
||||
var colors = document.getElementsByClassName('color-button');
|
||||
var lightestColor = [0,null];
|
||||
for (var i = 0; i < colors.length; i++) {
|
||||
|
||||
//get colors lightness
|
||||
var lightness = rgbToHsl(colors[i].jscolor.toRgb()).l;
|
||||
//console.log('%c'+lightness, logStyle)
|
||||
|
||||
//if not the color we're deleting
|
||||
if (colors[i] != color) {
|
||||
|
||||
//if lighter than the current lightest, set as the new lightest
|
||||
if (lightness > lightestColor[0]) {
|
||||
lightestColor[0] = lightness;
|
||||
lightestColor[1] = colors[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//console.log('%c'+'replacing with lightest color: '+lightestColor[1].jscolor.toString(), logStyle)
|
||||
|
||||
//replace deleted color with lightest color
|
||||
replaceAllOfColor(color.jscolor.toString(),lightestColor[1].jscolor.toString());
|
||||
|
||||
|
||||
//if the color you are deleting is the currently selected color
|
||||
if (color.parentElement.classList.contains('selected')) {
|
||||
//console.log('%c'+'deleted color is currently selected', logStyle);
|
||||
|
||||
//set current color TO LIGHTEST COLOR
|
||||
lightestColor[1].parentElement.classList.add('selected');
|
||||
currentLayer.context.fillStyle = '#'+lightestColor[1].jscolor.toString();
|
||||
}
|
||||
|
||||
//delete the element
|
||||
colorsMenu.removeChild(color.parentElement);
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
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();
|
||||
});
|
||||
|
||||
//add click handlers for all cancel buttons
|
||||
var cancelButtons = popUpContainer.getElementsByClassName('close-button');
|
||||
for (var i = 0; i < cancelButtons.length; i++) {
|
||||
cancelButtons[i].addEventListener('click', function () {
|
||||
closeDialogue();
|
||||
});
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
//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);
|
||||
var sx = (x0 < x1 ? 1 : -1);
|
||||
var sy = (y0 < y1 ? 1 : -1);
|
||||
var err = dx-dy;
|
||||
|
||||
while (true) {
|
||||
//set pixel
|
||||
// If the current tool is the brush
|
||||
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') {
|
||||
// In case I'm using the eraser I must clear the rect
|
||||
currentLayer.context.clearRect(x0-Math.floor(tool.eraser.brushSize/2), y0-Math.floor(tool.eraser.brushSize/2), tool.eraser.brushSize, tool.eraser.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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
let modes = {
|
||||
'Basic' : {
|
||||
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 more advanced features such as layers.'
|
||||
}
|
||||
}
|
||||
|
||||
on('click', 'switch-editor-mode-splash', function (e) {
|
||||
console.log('switching mode')
|
||||
switchMode();
|
||||
});
|
||||
|
||||
function switchMode(mustConfirm = true) {
|
||||
console.log('switching mode', 'current:',pixelEditorMode)
|
||||
//switch to advanced mode
|
||||
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';
|
||||
// Hide the palette menu
|
||||
document.getElementById('colors-menu').style.right = '200px'
|
||||
|
||||
//change splash text
|
||||
document.querySelector('#sp-quickstart-container .mode-switcher').classList.add('advanced-mode');
|
||||
|
||||
pixelEditorMode = 'Advanced';
|
||||
|
||||
//turn pixel grid off
|
||||
togglePixelGrid('off');
|
||||
}
|
||||
//switch to basic mode
|
||||
else {
|
||||
//if there is a current layer (a document is active)
|
||||
if (currentLayer) {
|
||||
//confirm with user before flattening image
|
||||
if (mustConfirm ) {
|
||||
if (!confirm('Switching to basic mode will flatten all the visible layers. Are you sure you want to continue?')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Selecting the current layer
|
||||
currentLayer.selectLayer();
|
||||
// Flatten the layers
|
||||
flatten(true);
|
||||
}
|
||||
|
||||
//change menu text
|
||||
document.getElementById('switch-mode-button').innerHTML = 'Switch to advanced mode';
|
||||
|
||||
// Hide the layer menus
|
||||
layerList.style.display = 'none';
|
||||
document.getElementById('layer-button').style.display = 'none';
|
||||
// Show the palette menu
|
||||
document.getElementById('colors-menu').style.display = 'flex';
|
||||
// Move the palette menu
|
||||
document.getElementById('colors-menu').style.right = '0px';
|
||||
|
||||
|
||||
//change splash text
|
||||
document.querySelector('#sp-quickstart-container .mode-switcher').classList.remove('advanced-mode');
|
||||
|
||||
pixelEditorMode = 'Basic';
|
||||
togglePixelGrid('on');
|
||||
}
|
||||
}
|
||||
|
||||
on('click', 'switch-mode-button', function (e) {
|
||||
switchMode();
|
||||
});
|
|
@ -1 +0,0 @@
|
|||
showDialogue("splash", false);
|
155
js/_fileMenu.js
155
js/_fileMenu.js
|
@ -1,155 +0,0 @@
|
|||
var mainMenuItems = document.getElementById('main-menu').children;
|
||||
|
||||
//for each button in main menu (starting at 1 to avoid logo)
|
||||
for (var i = 1; i < mainMenuItems.length; i++) {
|
||||
|
||||
//get the button that's in the list item
|
||||
var menuItem = mainMenuItems[i];
|
||||
var menuButton = menuItem.children[0];
|
||||
|
||||
//when you click a main menu items button
|
||||
on('click', menuButton, function (e, button) {
|
||||
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++) {
|
||||
|
||||
var subMenuItem = subMenuItems[j];
|
||||
var subMenuButton = subMenuItem.children[0];
|
||||
|
||||
subMenuButton.addEventListener('click', function (e) {
|
||||
|
||||
switch(this.textContent) {
|
||||
|
||||
//File Menu
|
||||
case 'New':
|
||||
showDialogue('new-pixel');
|
||||
break;
|
||||
case 'Save project':
|
||||
openSaveProjectWindow();
|
||||
break;
|
||||
case 'Open':
|
||||
//if a document exists
|
||||
if (documentCreated) {
|
||||
//check if the user wants to overwrite
|
||||
if (confirm('Opening a pixel will discard your current one. Are you sure you want to do that?'))
|
||||
//open file selection dialog
|
||||
document.getElementById('open-image-browse-holder').click();
|
||||
}
|
||||
else
|
||||
//open file selection dialog
|
||||
document.getElementById('open-image-browse-holder').click();
|
||||
|
||||
break;
|
||||
|
||||
case 'Export':
|
||||
openPixelExportWindow();
|
||||
break;
|
||||
|
||||
case 'Exit':
|
||||
|
||||
console.log('exit');
|
||||
//if a document exists, make sure they want to delete it
|
||||
if (documentCreated) {
|
||||
|
||||
//ask user if they want to leave
|
||||
if (confirm('Exiting will discard your current pixel. Are you sure you want to do that?'))
|
||||
//skip onbeforeunload prompt
|
||||
window.onbeforeunload = null;
|
||||
else
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
break;
|
||||
//Edit Menu
|
||||
case 'Undo':
|
||||
undo();
|
||||
break;
|
||||
case 'Redo':
|
||||
redo();
|
||||
break;
|
||||
|
||||
//Palette Menu
|
||||
case 'Add color':
|
||||
addColor('#eeeeee');
|
||||
break;
|
||||
// SELECTION MENU
|
||||
case 'Paste':
|
||||
pasteSelection();
|
||||
break;
|
||||
case 'Copy':
|
||||
copySelection();
|
||||
tool.pencil.switchTo();
|
||||
break;
|
||||
case 'Cut':
|
||||
cutSelectionTool();
|
||||
tool.pencil.switchTo();
|
||||
break;
|
||||
case 'Cancel':
|
||||
tool.pencil.switchTo();
|
||||
break;
|
||||
//Help Menu
|
||||
case 'Settings':
|
||||
//fill form with current settings values
|
||||
setValue('setting-numberOfHistoryStates', settings.numberOfHistoryStates);
|
||||
|
||||
showDialogue('settings');
|
||||
break;
|
||||
//Help Menu
|
||||
case 'Help':
|
||||
showDialogue('help');
|
||||
break;
|
||||
case 'About':
|
||||
showDialogue('about');
|
||||
break;
|
||||
case 'Changelog':
|
||||
showDialogue('changelog');
|
||||
break;
|
||||
}
|
||||
|
||||
closeMenu();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function closeMenu () {
|
||||
//remove .selected class from all menu buttons
|
||||
for (var i = 0; i < mainMenuItems.length; i++) {
|
||||
deselect(mainMenuItems[i]);
|
||||
}
|
||||
}
|
||||
|
||||
function getProjectData() {
|
||||
// use a dictionary
|
||||
let dictionary = {};
|
||||
// sorting layers by increasing z-index
|
||||
let layersCopy = layers.slice();
|
||||
layersCopy.sort((a, b) => (a.canvas.style.zIndex > b.canvas.style.zIndex) ? 1 : -1);
|
||||
// save canvas size
|
||||
dictionary['canvasWidth'] = currentLayer.canvasSize[0];
|
||||
dictionary['canvasHeight'] = currentLayer.canvasSize[1];
|
||||
// save editor mode
|
||||
dictionary['editorMode'] = pixelEditorMode;
|
||||
// save palette
|
||||
for (let i=0; i<currentPalette.length; i++) {
|
||||
dictionary["color" + i] = currentPalette[i];
|
||||
}
|
||||
|
||||
// save number of layers
|
||||
dictionary["nLayers"] = layersCopy.length;
|
||||
|
||||
// save layers
|
||||
for (let i=0; i<layersCopy.length; i++) {
|
||||
// Only saving the layers the user has access to (no vfx, tmp or checkerboard layers)
|
||||
if (layersCopy[i].menuEntry != null) {
|
||||
dictionary["layer" + i] = layersCopy[i];
|
||||
dictionary["layer" + i + "ImageData"] = layersCopy[i].canvas.toDataURL();
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify(dictionary);
|
||||
}
|
111
js/_fill.js
111
js/_fill.js
|
@ -1,111 +0,0 @@
|
|||
function fill(cursorLocation) {
|
||||
|
||||
//changes a pixels color
|
||||
function colorPixel(tempImage, pixelPos, fillColor) {
|
||||
//console.log('colorPixel:',pixelPos);
|
||||
tempImage.data[pixelPos] = fillColor.r;
|
||||
tempImage.data[pixelPos + 1] = fillColor.g;
|
||||
tempImage.data[pixelPos + 2] = fillColor.b;
|
||||
tempImage.data[pixelPos + 3] = 255;
|
||||
}
|
||||
|
||||
//change x y to color value passed from the function and use that as the original color
|
||||
function matchStartColor(tempImage, pixelPos, color) {
|
||||
//console.log('matchPixel:',x,y)
|
||||
|
||||
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] && a == color[3]);
|
||||
}
|
||||
|
||||
//save history state
|
||||
new HistoryStateEditCanvas();
|
||||
//saveHistoryState({type: 'canvas', canvas: context.getImageData(0, 0, canvasSize[0], canvasSize[1])});
|
||||
//console.log('filling at '+ Math.floor(cursorLocation[0]/zoom) + ','+ Math.floor(cursorLocation[1]/zoom));
|
||||
|
||||
//temporary image holds the data while we change it
|
||||
var tempImage = currentLayer.context.getImageData(0, 0, canvasSize[0], canvasSize[1]);
|
||||
|
||||
//this is an array that holds all of the pixels at the top of the cluster
|
||||
var topmostPixelsArray = [[Math.floor(cursorLocation[0]/zoom), Math.floor(cursorLocation[1]/zoom)]];
|
||||
//console.log('topmostPixelsArray:',topmostPixelsArray)
|
||||
|
||||
//the offset of the pixel in the temp image data to start with
|
||||
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], 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 &&
|
||||
clusterColor[3] != 0) {
|
||||
console.log("Returned");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
//loop until there are no more values left in this array
|
||||
while (topmostPixelsArray.length) {
|
||||
var reachLeft, reachRight;
|
||||
|
||||
//move the most recent pixel from the array and set it as our current working pixels
|
||||
var currentPixel = topmostPixelsArray.pop();
|
||||
|
||||
//set the values of this pixel to x/y variables just for readability
|
||||
var x = currentPixel[0];
|
||||
var y = currentPixel[1];
|
||||
|
||||
//this variable holds the index of where the starting values for the current pixel are in the data array
|
||||
//we multiply the number of rows down (y) times the width of each row, then add x. at the end we multiply by 4 because
|
||||
//each pixel has 4 values, rgba
|
||||
var pixelPos = (y * canvasSize[0] + x) * 4;
|
||||
|
||||
//move up in the image until you reach the top or the pixel you hit was not the right color
|
||||
while (y-- >= 0 && matchStartColor(tempImage, pixelPos, clusterColor)) {
|
||||
pixelPos -= canvasSize[0] * 4;
|
||||
}
|
||||
pixelPos += canvasSize[0] * 4;
|
||||
++y;
|
||||
reachLeft = false;
|
||||
reachRight = false;
|
||||
while (y++ < canvasSize[1] - 1 && matchStartColor(tempImage, pixelPos, clusterColor)) {
|
||||
colorPixel(tempImage, pixelPos, fillColor);
|
||||
if (x > 0) {
|
||||
if (matchStartColor(tempImage, pixelPos - 4, clusterColor)) {
|
||||
if (!reachLeft) {
|
||||
topmostPixelsArray.push([x - 1, y]);
|
||||
reachLeft = true;
|
||||
}
|
||||
}
|
||||
else if (reachLeft) {
|
||||
reachLeft = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (x < canvasSize[0] - 1) {
|
||||
if (matchStartColor(tempImage, pixelPos + 4, clusterColor)) {
|
||||
if (!reachRight) {
|
||||
topmostPixelsArray.push([x + 1, y]);
|
||||
reachRight = true;
|
||||
}
|
||||
}
|
||||
else if (reachRight) {
|
||||
reachRight = false;
|
||||
}
|
||||
}
|
||||
|
||||
pixelPos += canvasSize[0] * 4;
|
||||
}
|
||||
}
|
||||
currentLayer.context.putImageData(tempImage, 0, 0);
|
||||
//console.log('done filling')
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
//gets cursor position relative to canvas
|
||||
function getCursorPosition(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 -= currentLayer.canvas.offsetLeft;
|
||||
y -= currentLayer.canvas.offsetTop;
|
||||
|
||||
return [Math.round(x), Math.round(y)];
|
||||
}
|
529
js/_history.js
529
js/_history.js
|
@ -1,529 +0,0 @@
|
|||
/** 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 = [];
|
||||
|
||||
const undoLogStyle = 'background: #87ff1c; color: black; padding: 5px;';
|
||||
|
||||
function HistoryStateResizeSprite(xRatio, yRatio, algo, oldData) {
|
||||
this.xRatio = xRatio;
|
||||
this.yRatio = yRatio;
|
||||
this.algo = algo;
|
||||
this.oldData = oldData;
|
||||
|
||||
this.undo = function() {
|
||||
let layerIndex = 0;
|
||||
|
||||
currentAlgo = algo;
|
||||
resizeSprite(null, [1 / this.xRatio, 1 / this.yRatio]);
|
||||
|
||||
// Also putting the old data
|
||||
for (let i=0; i<layers.length; i++) {
|
||||
if (layers[i].menuEntry != null) {
|
||||
layers[i].context.putImageData(this.oldData[layerIndex], 0, 0);
|
||||
layerIndex++;
|
||||
layers[i].updateLayerPreview();
|
||||
}
|
||||
}
|
||||
|
||||
redoStates.push(this);
|
||||
};
|
||||
|
||||
this.redo = function() {
|
||||
console.log("REDOOOO");
|
||||
console.log("Ratio: " + this.xRatio + "," + this.yRatio);
|
||||
currentAlgo = algo;
|
||||
resizeSprite(null, [this.xRatio, this.yRatio]);
|
||||
undoStates.push(this);
|
||||
};
|
||||
|
||||
saveHistoryState(this);
|
||||
}
|
||||
|
||||
function HistoryStateResizeCanvas(newSize, oldSize, imageDatas, trim) {
|
||||
this.oldSize = oldSize;
|
||||
this.newSize = newSize;
|
||||
this.imageDatas = imageDatas;
|
||||
this.trim = trim;
|
||||
|
||||
this.undo = function() {
|
||||
let dataIndex = 0;
|
||||
// Resizing the canvas
|
||||
resizeCanvas(null, oldSize, null, false);
|
||||
// Putting the image datas
|
||||
for (let i=0; i<layers.length; i++) {
|
||||
if (layers[i].menuEntry != null) {
|
||||
layers[i].context.putImageData(this.imageDatas[dataIndex], 0, 0);
|
||||
dataIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
redoStates.push(this);
|
||||
};
|
||||
|
||||
this.redo = function() {
|
||||
if (!this.trim) {
|
||||
resizeCanvas(null, newSize, null, false);
|
||||
}
|
||||
else {
|
||||
trimCanvas(null, false);
|
||||
}
|
||||
|
||||
undoStates.push(this);
|
||||
};
|
||||
|
||||
saveHistoryState(this);
|
||||
}
|
||||
|
||||
function HistoryStateFlattenVisible(flattened) {
|
||||
this.nFlattened = flattened;
|
||||
|
||||
this.undo = function() {
|
||||
for (let i=0; i<this.nFlattened; i++) {
|
||||
undo();
|
||||
}
|
||||
|
||||
redoStates.push(this);
|
||||
};
|
||||
|
||||
this.redo = function() {
|
||||
for (let i=0; i<this.nFlattened; i++) {
|
||||
redo();
|
||||
}
|
||||
|
||||
undoStates.push(this);
|
||||
};
|
||||
|
||||
saveHistoryState(this);
|
||||
}
|
||||
|
||||
function HistoryStateFlattenTwoVisibles(belowImageData, afterAbove, layerIndex, aboveLayer, belowLayer) {
|
||||
this.aboveLayer = aboveLayer;
|
||||
this.belowLayer = belowLayer;
|
||||
this.belowImageData = belowImageData;
|
||||
|
||||
this.undo = function() {
|
||||
canvasView.append(aboveLayer.canvas);
|
||||
layerList.insertBefore(aboveLayer.menuEntry, afterAbove);
|
||||
|
||||
belowLayer.context.clearRect(0, 0, belowLayer.canvasSize[0], belowLayer.canvasSize[1]);
|
||||
belowLayer.context.putImageData(this.belowImageData, 0, 0);
|
||||
belowLayer.updateLayerPreview();
|
||||
|
||||
layers.splice(layerIndex, 0, aboveLayer);
|
||||
|
||||
redoStates.push(this);
|
||||
};
|
||||
|
||||
this.redo = function() {
|
||||
mergeLayers(belowLayer.context, aboveLayer.context);
|
||||
|
||||
// Deleting the above layer
|
||||
aboveLayer.canvas.remove();
|
||||
aboveLayer.menuEntry.remove();
|
||||
layers.splice(layers.indexOf(aboveLayer), 1);
|
||||
|
||||
undoStates.push(this);
|
||||
};
|
||||
|
||||
saveHistoryState(this);
|
||||
}
|
||||
|
||||
function HistoryStateFlattenAll(nFlattened) {
|
||||
this.nFlattened = nFlattened;
|
||||
|
||||
this.undo = function() {
|
||||
for (let i=0; i<this.nFlattened - nAppLayers; i++) {
|
||||
undo();
|
||||
}
|
||||
|
||||
redoStates.push(this);
|
||||
};
|
||||
|
||||
this.redo = function() {
|
||||
for (let i=0; i<this.nFlattened - nAppLayers; i++) {
|
||||
redo();
|
||||
}
|
||||
|
||||
undoStates.push(this);
|
||||
};
|
||||
|
||||
saveHistoryState(this);
|
||||
}
|
||||
|
||||
function HistoryStateMergeLayer(aboveIndex, aboveLayer, belowData, belowLayer) {
|
||||
this.aboveIndex = aboveIndex;
|
||||
this.belowData = belowData;
|
||||
this.aboveLayer = aboveLayer;
|
||||
this.belowLayer = belowLayer;
|
||||
|
||||
this.undo = function() {
|
||||
layerList.insertBefore(this.aboveLayer.menuEntry, this.belowLayer.menuEntry);
|
||||
canvasView.append(this.aboveLayer.canvas);
|
||||
|
||||
belowLayer.context.clearRect(0, 0, this.belowLayer.canvasSize[0], this.belowLayer.canvasSize[1]);
|
||||
belowLayer.context.putImageData(this.belowData, 0, 0);
|
||||
belowLayer.updateLayerPreview();
|
||||
|
||||
layers.splice(this.aboveIndex, 0, this.aboveLayer);
|
||||
|
||||
redoStates.push(this);
|
||||
};
|
||||
|
||||
this.redo = function() {
|
||||
aboveLayer.selectLayer();
|
||||
merge(false);
|
||||
|
||||
undoStates.push(this);
|
||||
};
|
||||
|
||||
saveHistoryState(this);
|
||||
}
|
||||
|
||||
function HistoryStateRenameLayer(oldName, newName, layer) {
|
||||
this.edited = layer;
|
||||
this.oldName = oldName;
|
||||
this.newName = newName;
|
||||
|
||||
this.undo = function() {
|
||||
layer.menuEntry.getElementsByTagName("p")[0].innerHTML = oldName;
|
||||
|
||||
redoStates.push(this);
|
||||
};
|
||||
|
||||
this.redo = function() {
|
||||
layer.menuEntry.getElementsByTagName("p")[0].innerHTML = newName;
|
||||
|
||||
undoStates.push(this);
|
||||
};
|
||||
|
||||
saveHistoryState(this);
|
||||
}
|
||||
|
||||
function HistoryStateDuplicateLayer(addedLayer, copiedLayer) {
|
||||
this.addedLayer = addedLayer;
|
||||
this.copiedLayer = copiedLayer;
|
||||
|
||||
this.undo = function() {
|
||||
addedLayer.selectLayer();
|
||||
deleteLayer(false);
|
||||
|
||||
redoStates.push(this);
|
||||
};
|
||||
|
||||
this.redo = function() {
|
||||
copiedLayer.selectLayer();
|
||||
duplicateLayer(null, false);
|
||||
|
||||
undoStates.push(this);
|
||||
};
|
||||
|
||||
saveHistoryState(this);
|
||||
}
|
||||
|
||||
function HistoryStateDeleteLayer(layerData, before, index) {
|
||||
this.deleted = layerData;
|
||||
this.before = before;
|
||||
this.index = index;
|
||||
|
||||
this.undo = function() {
|
||||
canvasView.append(this.deleted.canvas);
|
||||
if (this.before != null) {
|
||||
layerList.insertBefore(this.deleted.menuEntry, this.before);
|
||||
}
|
||||
else {
|
||||
layerList.prepend(this.deleted.menuEntry);
|
||||
}
|
||||
layers.splice(this.index, 0, this.deleted);
|
||||
|
||||
redoStates.push(this);
|
||||
};
|
||||
|
||||
this.redo = function() {
|
||||
this.deleted.selectLayer();
|
||||
deleteLayer(false);
|
||||
|
||||
undoStates.push(this);
|
||||
};
|
||||
|
||||
saveHistoryState(this);
|
||||
}
|
||||
|
||||
function HistoryStateMoveTwoLayers(layer, oldIndex, newIndex) {
|
||||
this.layer = layer;
|
||||
this.oldIndex = oldIndex;
|
||||
this.newIndex = newIndex;
|
||||
|
||||
this.undo = function() {
|
||||
layer.canvas.style.zIndex = oldIndex;
|
||||
redoStates.push(this);
|
||||
};
|
||||
|
||||
this.redo = function() {
|
||||
layer.canvas.style.zIndex = newIndex;
|
||||
undoStates.push(this);
|
||||
};
|
||||
|
||||
saveHistoryState(this);
|
||||
}
|
||||
|
||||
function HistoryStateMoveLayer(afterToDrop, toDrop, staticc, nMoved) {
|
||||
this.beforeToDrop = afterToDrop;
|
||||
this.toDrop = toDrop;
|
||||
|
||||
this.undo = function() {
|
||||
toDrop.menuEntry.remove();
|
||||
|
||||
if (afterToDrop != null) {
|
||||
layerList.insertBefore(toDrop.menuEntry, afterToDrop)
|
||||
}
|
||||
else {
|
||||
layerList.append(toDrop.menuEntry);
|
||||
}
|
||||
|
||||
for (let i=0; i<nMoved; i++) {
|
||||
undo();
|
||||
}
|
||||
|
||||
redoStates.push(this);
|
||||
};
|
||||
|
||||
this.redo = function() {
|
||||
moveLayers(toDrop.menuEntry.id, staticc.menuEntry.id, true);
|
||||
undoStates.push(this);
|
||||
};
|
||||
|
||||
saveHistoryState(this);
|
||||
}
|
||||
|
||||
function HistoryStateAddLayer(layerData, index) {
|
||||
this.added = layerData;
|
||||
this.index = index;
|
||||
|
||||
this.undo = function() {
|
||||
|
||||
redoStates.push(this);
|
||||
if (layers.length - nAppLayers > this.index + 1) {
|
||||
layers[this.index + 1].selectLayer();
|
||||
}
|
||||
else {
|
||||
layers[this.index - 1].selectLayer();
|
||||
}
|
||||
|
||||
|
||||
this.added.canvas.remove();
|
||||
this.added.menuEntry.remove();
|
||||
|
||||
layers.splice(index, 1);
|
||||
};
|
||||
|
||||
this.redo = function() {
|
||||
undoStates.push(this);
|
||||
|
||||
canvasView.append(this.added.canvas);
|
||||
layerList.prepend(this.added.menuEntry);
|
||||
layers.splice(this.index, 0, this.added);
|
||||
};
|
||||
|
||||
saveHistoryState(this);
|
||||
}
|
||||
|
||||
//prototype for undoing canvas changes
|
||||
function HistoryStateEditCanvas () {
|
||||
this.canvasState = currentLayer.context.getImageData(0, 0, canvasSize[0], canvasSize[1]);
|
||||
this.layerID = currentLayer.id;
|
||||
|
||||
this.undo = function () {
|
||||
var stateLayer = getLayerByID(this.layerID);
|
||||
var currentCanvas = stateLayer.context.getImageData(0, 0, canvasSize[0], canvasSize[1]);
|
||||
stateLayer.context.putImageData(this.canvasState, 0, 0);
|
||||
|
||||
this.canvasState = currentCanvas;
|
||||
redoStates.push(this);
|
||||
|
||||
stateLayer.updateLayerPreview();
|
||||
};
|
||||
|
||||
this.redo = function () {
|
||||
console.log("YEET");
|
||||
var stateLayer = getLayerByID(this.layerID);
|
||||
var currentCanvas = stateLayer.context.getImageData(0, 0, canvasSize[0], canvasSize[1]);
|
||||
|
||||
stateLayer.context.putImageData(this.canvasState, 0, 0);
|
||||
|
||||
this.canvasState = currentCanvas;
|
||||
undoStates.push(this);
|
||||
|
||||
stateLayer.updateLayerPreview();
|
||||
};
|
||||
|
||||
//add self to undo array
|
||||
saveHistoryState(this);
|
||||
}
|
||||
|
||||
//prototype for undoing added colors
|
||||
function HistoryStateAddColor (colorValue) {
|
||||
this.colorValue = colorValue;
|
||||
|
||||
this.undo = function () {
|
||||
redoStates.push(this);
|
||||
deleteColor(this.colorValue);
|
||||
};
|
||||
|
||||
this.redo = function () {
|
||||
addColor(this.colorValue);
|
||||
undoStates.push(this);
|
||||
};
|
||||
|
||||
//add self to undo array
|
||||
saveHistoryState(this);
|
||||
}
|
||||
|
||||
//prototype for undoing deleted colors
|
||||
function HistoryStateDeleteColor (colorValue) {
|
||||
this.colorValue = colorValue;
|
||||
this.canvas = currentLayer.context.getImageData(0, 0, canvasSize[0], canvasSize[1]);
|
||||
|
||||
this.undo = function () {
|
||||
var currentCanvas = currentLayer.context.getImageData(0, 0, canvasSize[0], canvasSize[1]);
|
||||
currentLayer.context.putImageData(this.canvas, 0, 0);
|
||||
|
||||
addColor(this.colorValue);
|
||||
|
||||
this.canvas = currentCanvas;
|
||||
redoStates.push(this);
|
||||
};
|
||||
|
||||
this.redo = function () {
|
||||
var currentCanvas = currentLayer.context.getImageData(0, 0, canvasSize[0], canvasSize[1]);
|
||||
currentLayer.context.putImageData(this.canvas, 0, 0);
|
||||
|
||||
deleteColor(this.colorValue);
|
||||
|
||||
this.canvas = currentCanvas;
|
||||
undoStates.push(this);
|
||||
};
|
||||
|
||||
//add self to undo array
|
||||
saveHistoryState(this);
|
||||
}
|
||||
|
||||
//prototype for undoing colors edits
|
||||
function HistoryStateEditColor (newColorValue, oldColorValue) {
|
||||
this.newColorValue = newColorValue;
|
||||
this.oldColorValue = oldColorValue;
|
||||
this.canvas = currentLayer.context.getImageData(0, 0, canvasSize[0], canvasSize[1]);
|
||||
|
||||
this.undo = function () {
|
||||
var currentCanvas = currentLayer.context.getImageData(0, 0, canvasSize[0], canvasSize[1]);
|
||||
currentLayer.context.putImageData(this.canvas, 0, 0);
|
||||
|
||||
//find new color in palette and change it back to old color
|
||||
var colors = document.getElementsByClassName('color-button');
|
||||
for (var i = 0; i < colors.length; i++) {
|
||||
//console.log(newColorValue, '==', colors[i].jscolor.toString());
|
||||
if (newColorValue == colors[i].jscolor.toString()) {
|
||||
colors[i].jscolor.fromString(oldColorValue);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.canvas = currentCanvas;
|
||||
redoStates.push(this);
|
||||
};
|
||||
|
||||
this.redo = function () {
|
||||
var currentCanvas = currentLayer.context.getImageData(0, 0, canvasSize[0], canvasSize[1]);
|
||||
currentLayer.context.putImageData(this.canvas, 0, 0);
|
||||
|
||||
//find old color in palette and change it back to new color
|
||||
var colors = document.getElementsByClassName('color-button');
|
||||
for (var i = 0; i < colors.length; i++) {
|
||||
//console.log(oldColorValue, '==', colors[i].jscolor.toString());
|
||||
if (oldColorValue == colors[i].jscolor.toString()) {
|
||||
colors[i].jscolor.fromString(newColorValue);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.canvas = currentCanvas;
|
||||
undoStates.push(this);
|
||||
};
|
||||
|
||||
//add self to undo array
|
||||
saveHistoryState(this);
|
||||
}
|
||||
|
||||
|
||||
//rename to add undo state
|
||||
function saveHistoryState (state) {
|
||||
//get current canvas data and save to undoStates array
|
||||
undoStates.push(state);
|
||||
|
||||
//limit the number of states to settings.numberOfHistoryStates
|
||||
if (undoStates.length > settings.numberOfHistoryStates) {
|
||||
undoStates = undoStates.splice(-settings.numberOfHistoryStates, settings.numberOfHistoryStates);
|
||||
}
|
||||
|
||||
//there is now definitely at least 1 undo state, so the button shouldnt be disabled
|
||||
document.getElementById('undo-button').classList.remove('disabled');
|
||||
|
||||
//there should be no redoStates after an undoState is saved
|
||||
redoStates = [];
|
||||
}
|
||||
|
||||
function undo () {
|
||||
//if there are any states saved to undo
|
||||
if (undoStates.length > 0) {
|
||||
document.getElementById('redo-button').classList.remove('disabled');
|
||||
|
||||
//get state
|
||||
var undoState = undoStates[undoStates.length-1];
|
||||
//console.log(undoState);
|
||||
|
||||
//remove from the undo list
|
||||
undoStates.splice(undoStates.length-1,1);
|
||||
|
||||
//restore the state
|
||||
undoState.undo();
|
||||
|
||||
//if theres none left to undo, disable the option
|
||||
if (undoStates.length == 0)
|
||||
document.getElementById('undo-button').classList.add('disabled');
|
||||
}
|
||||
}
|
||||
|
||||
function redo () {
|
||||
if (redoStates.length > 0) {
|
||||
|
||||
//enable undo button
|
||||
document.getElementById('undo-button').classList.remove('disabled');
|
||||
|
||||
//get state
|
||||
var redoState = redoStates[redoStates.length-1];
|
||||
|
||||
//remove from redo array (do this before restoring the state, else the flatten state will break)
|
||||
redoStates.splice(redoStates.length-1,1);
|
||||
|
||||
//restore the state
|
||||
redoState.redo();
|
||||
|
||||
//if theres none left to redo, disable the option
|
||||
if (redoStates.length == 0)
|
||||
document.getElementById('redo-button').classList.add('disabled');
|
||||
}
|
||||
//console.log(undoStates);
|
||||
//console.log(redoStates);
|
||||
}
|
|
@ -1,126 +0,0 @@
|
|||
var spacePressed = false;
|
||||
|
||||
/** Just listens to hotkeys and calls the linked functions
|
||||
*
|
||||
* @param {*} e
|
||||
*/
|
||||
function KeyPress(e) {
|
||||
var keyboardEvent = window.event? event : e;
|
||||
|
||||
//if the user is typing in an input field or renaming a layer, ignore these hotkeys, unless it's an enter key
|
||||
if (document.activeElement.tagName == 'INPUT' || isRenamingLayer) {
|
||||
if (e.keyCode == 13) {
|
||||
currentLayer.closeOptionsMenu();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
//if no document has been created yet,
|
||||
//orthere is a dialog box open
|
||||
//ignore hotkeys
|
||||
if (!documentCreated || dialogueOpen) return;
|
||||
|
||||
//
|
||||
if (e.key === "Escape") {
|
||||
if (!selectionCanceled) {
|
||||
tool.pencil.switchTo();
|
||||
}
|
||||
}
|
||||
else {
|
||||
switch (keyboardEvent.keyCode) {
|
||||
//pencil tool - 1, b
|
||||
case 49: case 66:
|
||||
tool.pencil.switchTo();
|
||||
break;
|
||||
// copy tool c
|
||||
case 67: case 99:
|
||||
if (keyboardEvent.ctrlKey && !dragging && currentTool.name == 'moveselection') {
|
||||
copySelection();
|
||||
}
|
||||
break;
|
||||
//fill tool - 2, f
|
||||
case 50: case 70:
|
||||
tool.fill.switchTo();
|
||||
break;
|
||||
//eyedropper - 3, e
|
||||
case 51: case 69:
|
||||
tool.eyedropper.switchTo();
|
||||
break;
|
||||
//pan - 4, p,
|
||||
case 52: case 80:
|
||||
tool.pan.switchTo();
|
||||
break;
|
||||
case 76:
|
||||
tool.line.switchTo();
|
||||
break;
|
||||
//zoom - 5
|
||||
case 53:
|
||||
tool.zoom.switchTo();
|
||||
break;
|
||||
// eraser -6, r
|
||||
case 54: case 82:
|
||||
tool.eraser.switchTo()
|
||||
break;
|
||||
// Rectangular selection
|
||||
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) {
|
||||
pasteSelection();
|
||||
}
|
||||
break;
|
||||
case 88: case 120:
|
||||
if (keyboardEvent.ctrlKey && !dragging && currentTool.name == 'moveselection') {
|
||||
cutSelectionTool();
|
||||
tool.pencil.switchTo();
|
||||
}
|
||||
break;
|
||||
//Z
|
||||
case 90:
|
||||
//CTRL+ALT+Z redo
|
||||
if (keyboardEvent.altKey && keyboardEvent.ctrlKey)
|
||||
redo();
|
||||
if (!selectionCanceled) {
|
||||
tool.pencil.switchTo()
|
||||
}
|
||||
//CTRL+Z undo
|
||||
else if (keyboardEvent.ctrlKey) {
|
||||
undo();
|
||||
if (!selectionCanceled) {
|
||||
tool.pencil.switchTo()
|
||||
}
|
||||
}
|
||||
//Z switch to zoom tool
|
||||
else
|
||||
tool.zoom.switchTo()
|
||||
break;
|
||||
//redo - ctrl y
|
||||
case 89:
|
||||
if (keyboardEvent.ctrlKey)
|
||||
redo();
|
||||
break;
|
||||
case 32:
|
||||
spacePressed=true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.onkeydown = KeyPress;
|
||||
|
||||
window.addEventListener("keyup", function (e) {
|
||||
|
||||
if (e.keyCode == 32) spacePressed = false;
|
||||
|
||||
});
|
|
@ -1,25 +0,0 @@
|
|||
// NEXTPULL: to remove when the new palette system is added
|
||||
|
||||
|
||||
//formats a color button
|
||||
function initColor (colorElement) {
|
||||
//console.log('initColor()');
|
||||
//console.log(document.getElementById('jscolor-hex-input'))
|
||||
|
||||
|
||||
//add jscolor picker for this color
|
||||
colorElement.jscolor = new jscolor(colorElement.parentElement, {
|
||||
valueElement: null, //if you dont set this to null, it turns the button (colorElement) into text, we set it when you open the picker
|
||||
styleElement: colorElement,
|
||||
width:151,
|
||||
position: 'left',
|
||||
padding:0,
|
||||
borderWidth:14,
|
||||
borderColor: '#332f35',
|
||||
backgroundColor: '#332f35',
|
||||
insetColor: 'transparent',
|
||||
value: colorElement.style.backgroundColor,
|
||||
deleteButton: true,
|
||||
});
|
||||
|
||||
}
|
613
js/_layer.js
613
js/_layer.js
|
@ -1,613 +0,0 @@
|
|||
// 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;
|
||||
// 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)
|
||||
*
|
||||
* @param width Canvas width
|
||||
* @param height Canvas height
|
||||
* @param canvas HTML canvas element
|
||||
*/
|
||||
class Layer {
|
||||
constructor(width, height, canvas, menuEntry) {
|
||||
this.canvasSize = [width, height];
|
||||
this.canvas = canvas;
|
||||
this.context = this.canvas.getContext('2d');
|
||||
this.isSelected = false;
|
||||
this.isVisible = true;
|
||||
this.isLocked = false;
|
||||
this.menuEntry = menuEntry;
|
||||
|
||||
let id = unusedIDs.pop();
|
||||
console.log("id creato: " + id);
|
||||
|
||||
if (id == null) {
|
||||
id = currentID;
|
||||
currentID++;
|
||||
}
|
||||
|
||||
this.id = "layer" + id;
|
||||
|
||||
// Binding the events
|
||||
if (menuEntry != null) {
|
||||
this.name = menuEntry.getElementsByTagName("p")[0].innerHTML;
|
||||
menuEntry.id = "layer" + id;
|
||||
menuEntry.onmouseover = () => this.hover();
|
||||
menuEntry.onmouseout = () => this.unhover();
|
||||
menuEntry.onclick = () => this.selectLayer();
|
||||
menuEntry.getElementsByTagName("button")[0].onclick = () => this.toggleLock();
|
||||
menuEntry.getElementsByTagName("button")[1].onclick = () => this.toggleVisibility();
|
||||
|
||||
menuEntry.addEventListener("mouseup", this.openOptionsMenu, false);
|
||||
menuEntry.addEventListener("dragstart", this.layerDragStart, false);
|
||||
menuEntry.addEventListener("drop", this.layerDragDrop, false);
|
||||
menuEntry.addEventListener("dragover", this.layerDragOver, false);
|
||||
menuEntry.addEventListener("dragleave", this.layerDragLeave, false);
|
||||
menuEntry.addEventListener("dragend", this.layerDragEnd, false);
|
||||
|
||||
menuEntry.getElementsByTagName("canvas")[0].getContext('2d').imageSmoothingEnabled = false;
|
||||
}
|
||||
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
// Initializes the canvas
|
||||
initialize() {
|
||||
//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';
|
||||
|
||||
//show canvas
|
||||
this.canvas.style.display = 'block';
|
||||
|
||||
//center canvas in window
|
||||
this.canvas.style.left = 64+canvasView.clientWidth/2-(this.canvasSize[0]*zoom/2)+'px';
|
||||
this.canvas.style.top = 48+canvasView.clientHeight/2-(this.canvasSize[1]*zoom/2)+'px';
|
||||
|
||||
this.context.imageSmoothingEnabled = false;
|
||||
this.context.mozImageSmoothingEnabled = false;
|
||||
}
|
||||
|
||||
hover() {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unhover() {
|
||||
// Shows all the layers again
|
||||
for (let i=1; i<layers.length - nAppLayers; i++) {
|
||||
if (layers[i] !== this) {
|
||||
layers[i].canvas.style.opacity = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setID(id) {
|
||||
this.id = id;
|
||||
if (this.menuEntry != null) {
|
||||
this.menuEntry.id = id;
|
||||
}
|
||||
}
|
||||
|
||||
// Resizes canvas
|
||||
resize() {
|
||||
let newWidth = (this.canvas.width * zoom) + 'px';
|
||||
let newHeight = (this.canvas.height *zoom)+ 'px';
|
||||
|
||||
this.canvas.style.width = newWidth;
|
||||
this.canvas.style.height = newHeight;
|
||||
}
|
||||
|
||||
setCanvasOffset (offsetLeft, offsetTop) {
|
||||
//horizontal offset
|
||||
var minXOffset = -this.canvasSize[0] * zoom;
|
||||
var maxXOffset = window.innerWidth - 300;
|
||||
|
||||
if (offsetLeft < minXOffset)
|
||||
this.canvas.style.left = minXOffset +'px';
|
||||
else if (offsetLeft > maxXOffset)
|
||||
this.canvas.style.left = maxXOffset +'px';
|
||||
else
|
||||
this.canvas.style.left = offsetLeft +'px';
|
||||
|
||||
//vertical offset
|
||||
var minYOffset = -this.canvasSize[1] * zoom + 164;
|
||||
var maxYOffset = window.innerHeight - 100;
|
||||
|
||||
if (offsetTop < minYOffset)
|
||||
this.canvas.style.top = minYOffset +'px';
|
||||
else if (offsetTop > maxYOffset)
|
||||
this.canvas.style.top = maxYOffset +'px';
|
||||
else
|
||||
this.canvas.style.top = offsetTop +'px';
|
||||
}
|
||||
|
||||
// Copies the otherLayer's position and size
|
||||
copyData(otherLayer) {
|
||||
this.canvas.style.width = otherLayer.canvas.style.width;
|
||||
this.canvas.style.height = otherLayer.canvas.style.height;
|
||||
|
||||
this.canvas.style.left = otherLayer.canvas.style.left;
|
||||
this.canvas.style.top = otherLayer.canvas.style.top;
|
||||
}
|
||||
|
||||
openOptionsMenu(event) {
|
||||
if (event.which == 3) {
|
||||
let selectedId;
|
||||
let target = event.target;
|
||||
|
||||
while (target != null && target.classList != null && !target.classList.contains("layers-menu-entry")) {
|
||||
target = target.parentElement;
|
||||
}
|
||||
|
||||
selectedId = target.id;
|
||||
|
||||
layerOptions.style.visibility = "visible";
|
||||
layerOptions.style.top = "0";
|
||||
layerOptions.style.marginTop = "" + (event.clientY - 25) + "px";
|
||||
|
||||
getLayerByID(selectedId).selectLayer();
|
||||
}
|
||||
}
|
||||
|
||||
closeOptionsMenu(event) {
|
||||
layerOptions.style.visibility = "hidden";
|
||||
currentLayer.menuEntry.getElementsByTagName("p")[0].setAttribute("contenteditable", false);
|
||||
isRenamingLayer = false;
|
||||
|
||||
if (oldLayerName != null) {
|
||||
let name = this.menuEntry.getElementsByTagName("p")[0].innerHTML;
|
||||
this.name = name;
|
||||
|
||||
new HistoryStateRenameLayer(oldLayerName, name, currentLayer);
|
||||
oldLayerName = null;
|
||||
}
|
||||
}
|
||||
|
||||
selectLayer(layer) {
|
||||
if (layer == null) {
|
||||
// Deselecting the old layer
|
||||
currentLayer.deselectLayer();
|
||||
|
||||
// Selecting the current layer
|
||||
this.isSelected = true;
|
||||
this.menuEntry.classList.add("selected-layer");
|
||||
currentLayer = getLayerByName(this.menuEntry.getElementsByTagName("p")[0].innerHTML);
|
||||
}
|
||||
else {
|
||||
currentLayer.deselectLayer();
|
||||
|
||||
layer.isSelected = true;
|
||||
layer.menuEntry.classList.add("selected-layer");
|
||||
currentLayer = layer;
|
||||
}
|
||||
}
|
||||
|
||||
toggleLock() {
|
||||
if (this.isLocked) {
|
||||
this.unlock();
|
||||
}
|
||||
else {
|
||||
this.lock();
|
||||
}
|
||||
}
|
||||
|
||||
toggleVisibility() {
|
||||
if (this.isVisible) {
|
||||
this.hide();
|
||||
}
|
||||
else {
|
||||
this.show();
|
||||
}
|
||||
}
|
||||
|
||||
deselectLayer() {
|
||||
this.isSelected = false;
|
||||
this.menuEntry.classList.remove("selected-layer");
|
||||
}
|
||||
|
||||
lock() {
|
||||
this.isLocked = true;
|
||||
this.menuEntry.getElementsByClassName("layer-button")[0].style.visibility = "visible";
|
||||
|
||||
this.menuEntry.getElementsByClassName("default-icon")[0].style.display = "none";
|
||||
this.menuEntry.getElementsByClassName("edited-icon")[0].style.display = "inline-block";
|
||||
}
|
||||
|
||||
unlock() {
|
||||
this.isLocked = false;
|
||||
this.menuEntry.getElementsByClassName("layer-button")[0].style.visibility = "hidden";
|
||||
|
||||
this.menuEntry.getElementsByClassName("default-icon")[0].style.display = "inline-block";
|
||||
this.menuEntry.getElementsByClassName("edited-icon")[0].style.display = "none";
|
||||
}
|
||||
|
||||
show() {
|
||||
this.isVisible = true;
|
||||
this.canvas.style.visibility = "visible";
|
||||
this.menuEntry.getElementsByClassName("layer-button")[1].style.visibility = "hidden";
|
||||
|
||||
// Changing icon
|
||||
this.menuEntry.getElementsByClassName("default-icon")[1].style.display = "inline-block";
|
||||
this.menuEntry.getElementsByClassName("edited-icon")[1].style.display = "none";
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.isVisible = false;
|
||||
this.canvas.style.visibility = "hidden";
|
||||
this.menuEntry.getElementsByClassName("layer-button")[1].style.visibility = "visible";
|
||||
|
||||
// Changing icon
|
||||
this.menuEntry.getElementsByClassName("default-icon")[1].style.display = "none";
|
||||
this.menuEntry.getElementsByClassName("edited-icon")[1].style.display = "inline-block";
|
||||
}
|
||||
|
||||
updateLayerPreview() {
|
||||
// Getting the canvas
|
||||
let destination = this.menuEntry.getElementsByTagName("canvas")[0];
|
||||
let widthRatio = this.canvasSize[0] / this.canvasSize[1];
|
||||
let heightRatio = this.canvasSize[1] / this.canvasSize[0];
|
||||
|
||||
// Computing width and height for the preview image
|
||||
let previewWidth = destination.width;
|
||||
let previewHeight = destination.height;
|
||||
|
||||
// If the sprite is rectangular, I apply the ratio to the preview as well
|
||||
if (widthRatio < 1) {
|
||||
previewWidth = destination.width * widthRatio;
|
||||
}
|
||||
else if (widthRatio > 1) {
|
||||
previewHeight = destination.height * heightRatio;
|
||||
}
|
||||
|
||||
// La appiccico sulla preview
|
||||
destination.getContext('2d').clearRect(0, 0, destination.width, destination.height);
|
||||
destination.getContext('2d').drawImage(this.canvas,
|
||||
// This is necessary to center the preview in the canvas
|
||||
(destination.width - previewWidth) / 2, (destination.height - previewHeight) / 2,
|
||||
previewWidth, previewHeight);
|
||||
}
|
||||
}
|
||||
|
||||
function flatten(onlyVisible) {
|
||||
if (!onlyVisible) {
|
||||
// Selecting the first layer
|
||||
let firstLayer = layerList.firstElementChild;
|
||||
let nToFlatten = layerList.childElementCount - 1;
|
||||
getLayerByID(firstLayer.id).selectLayer();
|
||||
|
||||
for (let i = 0; i < nToFlatten; i++) {
|
||||
merge();
|
||||
}
|
||||
|
||||
new HistoryStateFlattenAll(nToFlatten);
|
||||
}
|
||||
else {
|
||||
// Getting all the visible layers
|
||||
let visibleLayers = [];
|
||||
let nToFlatten = 0;
|
||||
|
||||
for (let i=0; i<layers.length; i++) {
|
||||
console.log(layers[i].name);
|
||||
if (layers[i].menuEntry != null && layers[i].isVisible) {
|
||||
visibleLayers.push(layers[i]);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("da piallare: " + visibleLayers.length);
|
||||
|
||||
// Sorting them by z-index
|
||||
visibleLayers.sort((a, b) => (a.canvas.style.zIndex > b.canvas.style.zIndex) ? -1 : 1);
|
||||
// Selecting the last visible layer (the only one that won't get deleted)
|
||||
visibleLayers[visibleLayers.length - 1].selectLayer();
|
||||
|
||||
// Merging all the layer but the last one
|
||||
for (let i=0; i<visibleLayers.length - 1; i++) {
|
||||
nToFlatten++;
|
||||
console.log(visibleLayers[i].menuEntry.nextElementSibling);
|
||||
new HistoryStateFlattenTwoVisibles(
|
||||
visibleLayers[i + 1].context.getImageData(0, 0, visibleLayers[i].canvasSize[0], visibleLayers[i].canvasSize[1]),
|
||||
visibleLayers[i].menuEntry.nextElementSibling,
|
||||
layers.indexOf(visibleLayers[i]),
|
||||
visibleLayers[i], visibleLayers[i + 1]
|
||||
);
|
||||
|
||||
mergeLayers(visibleLayers[i + 1].context, visibleLayers[i].context);
|
||||
|
||||
// Deleting the above layer
|
||||
visibleLayers[i].canvas.remove();
|
||||
visibleLayers[i].menuEntry.remove();
|
||||
layers.splice(layers.indexOf(visibleLayers[i]), 1);
|
||||
}
|
||||
|
||||
new HistoryStateFlattenVisible(nToFlatten);
|
||||
// Updating the layer preview
|
||||
currentLayer.updateLayerPreview();
|
||||
}
|
||||
}
|
||||
|
||||
function merge(saveHistory = true) {
|
||||
// Saving the layer that should be merged
|
||||
let toMerge = currentLayer;
|
||||
let toMergeIndex = layers.indexOf(toMerge);
|
||||
// Getting layer below
|
||||
let layerBelow = getLayerByID(currentLayer.menuEntry.nextElementSibling.id);
|
||||
|
||||
// If I have something to merge with
|
||||
if (layerBelow != null) {
|
||||
// Selecting that layer
|
||||
layerBelow.selectLayer();
|
||||
|
||||
if (saveHistory) {
|
||||
new HistoryStateMergeLayer(toMergeIndex, toMerge,
|
||||
layerBelow.context.getImageData(0, 0, layerBelow.canvasSize[0], layerBelow.canvasSize[1]),
|
||||
layerBelow);
|
||||
}
|
||||
|
||||
mergeLayers(currentLayer.context, toMerge.context);
|
||||
|
||||
// Deleting the above layer
|
||||
toMerge.canvas.remove();
|
||||
toMerge.menuEntry.remove();
|
||||
layers.splice(toMergeIndex, 1);
|
||||
|
||||
// Updating the layer preview
|
||||
currentLayer.updateLayerPreview();
|
||||
}
|
||||
}
|
||||
|
||||
function deleteLayer(saveHistory = true) {
|
||||
// Cannot delete all the layers
|
||||
if (layers.length != 4) {
|
||||
let layerIndex = layers.indexOf(currentLayer);
|
||||
let toDelete = layers[layerIndex];
|
||||
let previousSibling = toDelete.menuEntry.previousElementSibling;
|
||||
// Adding the ids to the unused ones
|
||||
console.log("id cancellato: " + toDelete.id);
|
||||
unusedIDs.push(toDelete.id);
|
||||
|
||||
// Selecting the next layer
|
||||
if (layerIndex != (layers.length - 4)) {
|
||||
layers[layerIndex + 1].selectLayer();
|
||||
}
|
||||
// or the previous one if the next one doesn't exist
|
||||
else {
|
||||
layers[layerIndex - 1].selectLayer();
|
||||
}
|
||||
|
||||
// Deleting canvas and entry
|
||||
toDelete.canvas.remove();
|
||||
toDelete.menuEntry.remove();
|
||||
|
||||
// Removing the layer from the list
|
||||
layers.splice(layerIndex, 1);
|
||||
|
||||
if (saveHistory) {
|
||||
new HistoryStateDeleteLayer(toDelete, previousSibling, layerIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// Closing the menu
|
||||
currentLayer.closeOptionsMenu();
|
||||
}
|
||||
|
||||
function duplicateLayer(event, saveHistory = true) {
|
||||
let layerIndex = layers.indexOf(currentLayer);
|
||||
let toDuplicate = currentLayer;
|
||||
let menuEntries = layerList.children;
|
||||
|
||||
// Increasing z-indexes of the layers above
|
||||
for (let i=getMenuEntryIndex(menuEntries, toDuplicate.menuEntry) - 1; i>=0; i--) {
|
||||
getLayerByID(menuEntries[i].id).canvas.style.zIndex++;
|
||||
}
|
||||
maxZIndex+=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) + 2;
|
||||
newCanvas.classList.add("drawingCanvas");
|
||||
|
||||
if (!layerListEntry) return console.warn('skipping adding layer because no document');
|
||||
|
||||
// Clone the default layer
|
||||
let toAppend = currentLayer.menuEntry.cloneNode(true);
|
||||
// Setting the default name for the layer
|
||||
toAppend.getElementsByTagName('p')[0].innerHTML += " copy";
|
||||
// Removing the selected class
|
||||
toAppend.classList.remove("selected-layer");
|
||||
// Adding the layer to the list
|
||||
layerCount++;
|
||||
|
||||
// Creating a layer object
|
||||
let newLayer = new Layer(currentLayer.canvasSize[0], currentLayer.canvasSize[1], newCanvas, toAppend);
|
||||
newLayer.context.fillStyle = currentLayer.context.fillStyle;
|
||||
newLayer.copyData(currentLayer);
|
||||
|
||||
layers.splice(layerIndex, 0, newLayer);
|
||||
|
||||
// Insert it before the Add layer button
|
||||
layerList.insertBefore(toAppend, currentLayer.menuEntry);
|
||||
|
||||
// Copy the layer content
|
||||
newLayer.context.putImageData(currentLayer.context.getImageData(
|
||||
0, 0, currentLayer.canvasSize[0], currentLayer.canvasSize[1]), 0, 0);
|
||||
newLayer.updateLayerPreview();
|
||||
// Basically "if I'm not adding a layer because redo() is telling meto do so", then I can save the history
|
||||
if (saveHistory) {
|
||||
new HistoryStateDuplicateLayer(newLayer, currentLayer);
|
||||
}
|
||||
}
|
||||
|
||||
function renameLayer(event) {
|
||||
let layerIndex = layers.indexOf(currentLayer);
|
||||
let toRename = currentLayer;
|
||||
let p = currentLayer.menuEntry.getElementsByTagName("p")[0];
|
||||
|
||||
oldLayerName = p.innerHTML;
|
||||
|
||||
p.setAttribute("contenteditable", true);
|
||||
p.classList.add("layer-name-editable");
|
||||
p.focus();
|
||||
|
||||
simulateInput(65, true, false, false);
|
||||
|
||||
isRenamingLayer = true;
|
||||
}
|
||||
|
||||
function getMenuEntryIndex(list, entry) {
|
||||
for (let i=0; i<list.length; i++) {
|
||||
if (list[i] === entry) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Finds a layer given its name
|
||||
function getLayerByName(name) {
|
||||
for (let i=0; i<layers.length; i++) {
|
||||
if (layers[i].menuEntry != null) {
|
||||
if (layers[i].menuEntry.getElementsByTagName("p")[0].innerHTML == name) {
|
||||
return layers[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Finds a layer given its id
|
||||
function getLayerByID(id) {
|
||||
for (let i=0; i<layers.length; i++) {
|
||||
if (layers[i].menuEntry != null) {
|
||||
if (layers[i].menuEntry.id == id) {
|
||||
return layers[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function addLayer(id, saveHistory = true) {
|
||||
// layers.length - 3
|
||||
let index = layers.length - 3;
|
||||
// Creating a new canvas
|
||||
let newCanvas = document.createElement("canvas");
|
||||
// Setting up the new canvas
|
||||
canvasView.append(newCanvas);
|
||||
maxZIndex+=2;
|
||||
newCanvas.style.zIndex = maxZIndex;
|
||||
newCanvas.classList.add("drawingCanvas");
|
||||
|
||||
if (!layerListEntry) return console.warn('skipping adding layer because no document');
|
||||
|
||||
// Clone the default layer
|
||||
let toAppend = layerListEntry.cloneNode(true);
|
||||
// Setting the default name for the layer
|
||||
toAppend.getElementsByTagName('p')[0].innerHTML = "Layer " + layerCount;
|
||||
// Removing the selected class
|
||||
toAppend.classList.remove("selected-layer");
|
||||
// Adding the layer to the list
|
||||
layerCount++;
|
||||
|
||||
// Creating a layer object
|
||||
let newLayer = new Layer(currentLayer.canvasSize[0], currentLayer.canvasSize[1], newCanvas, toAppend);
|
||||
newLayer.context.fillStyle = currentLayer.context.fillStyle;
|
||||
newLayer.copyData(currentLayer);
|
||||
|
||||
layers.splice(index, 0, newLayer);
|
||||
|
||||
// Insert it before the Add layer button
|
||||
layerList.insertBefore(toAppend, layerList.childNodes[0]);
|
||||
|
||||
if (id != null && typeof(id) == "string") {
|
||||
newLayer.setID(id);
|
||||
}
|
||||
// Basically "if I'm not adding a layer because redo() is telling meto do so", then I can save the history
|
||||
if (saveHistory) {
|
||||
new HistoryStateAddLayer(newLayer, index);
|
||||
}
|
||||
|
||||
return newLayer;
|
||||
}
|
||||
|
||||
/** 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
45
js/_line.js
|
@ -1,45 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
/** 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);
|
||||
let mode = dictionary['editorMode'] == 'Advanced' ? 'Basic' : 'Advanced';
|
||||
newPixel(dictionary['canvasWidth'], dictionary['canvasHeight'], mode, dictionary);
|
||||
|
||||
for (let i=0; i<dictionary['color' + i] != null; i++) {
|
||||
addColor(dictionary['color'+i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
//load file
|
||||
var fileReader = new FileReader();
|
||||
fileReader.onload = function(e) {
|
||||
var img = new Image();
|
||||
img.onload = function() {
|
||||
//create a new pixel with the images dimentions
|
||||
newPixel(this.width, this.height, 'Advanced');
|
||||
|
||||
//draw the image onto the canvas
|
||||
currentLayer.context.drawImage(img, 0, 0);
|
||||
createPaletteFromLayers();
|
||||
|
||||
//track google event
|
||||
ga('send', 'event', 'Pixel Editor Load', colorPalette.length, this.width+'/'+this.height); /*global ga*/
|
||||
|
||||
};
|
||||
img.src = e.target.result;
|
||||
};
|
||||
fileReader.readAsDataURL(this.files[0]);
|
||||
}
|
||||
}
|
||||
else alert('Only .lpe project files, PNG and GIF files are allowed at this time.');
|
||||
}
|
||||
});
|
|
@ -1,50 +0,0 @@
|
|||
//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') {
|
||||
|
||||
//load file
|
||||
var fileReader = new FileReader();
|
||||
fileReader.onload = function(e) {
|
||||
var img = new Image();
|
||||
img.onload = function() {
|
||||
|
||||
//draw image onto the temporary canvas
|
||||
var loadPaletteCanvas = document.getElementById('load-palette-canvas-holder');
|
||||
var loadPaletteContext = loadPaletteCanvas.getContext('2d');
|
||||
|
||||
loadPaletteCanvas.width = img.width;
|
||||
loadPaletteCanvas.height = img.height;
|
||||
|
||||
loadPaletteContext.drawImage(img, 0, 0);
|
||||
|
||||
//create array to hold found colors
|
||||
var colorPalette = [];
|
||||
var imagePixelData = loadPaletteContext.getImageData(0,0,this.width, this.height).data;
|
||||
|
||||
//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]);
|
||||
if (colorPalette.indexOf(color) == -1) {
|
||||
colorPalette.push(color);
|
||||
}
|
||||
}
|
||||
|
||||
//add to palettes so that it can be loaded when they click okay
|
||||
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;
|
||||
};
|
||||
fileReader.readAsDataURL(this.files[0]);
|
||||
}
|
||||
else alert('Only PNG and GIF files are supported at this time.');
|
||||
}
|
||||
});
|
|
@ -1,453 +0,0 @@
|
|||
var currentMouseEvent;
|
||||
var lastMouseMovePos;
|
||||
|
||||
//mousedown - start drawing
|
||||
window.addEventListener("mousedown", function (mouseEvent) {
|
||||
// Saving the event in case something else needs it
|
||||
currentMouseEvent = mouseEvent;
|
||||
canDraw = true;
|
||||
|
||||
//if no document has been created yet, or this is a dialog open, or the currentLayer is locked
|
||||
if (!documentCreated || dialogueOpen || currentLayer.isLocked || !currentLayer.isVisible) return;
|
||||
//prevent right mouse clicks and such, which will open unwanted menus
|
||||
//mouseEvent.preventDefault();
|
||||
|
||||
lastMouseClickPos = getCursorPosition(mouseEvent);
|
||||
|
||||
dragging = true;
|
||||
//left or right click ?
|
||||
if (mouseEvent.which == 1) {
|
||||
if (spacePressed)
|
||||
currentTool = tool.pan;
|
||||
else if (mouseEvent.altKey)
|
||||
currentTool = tool.eyedropper;
|
||||
else if (mouseEvent.target.className == 'drawingCanvas' &&
|
||||
(currentTool.name == 'pencil' || currentTool.name == 'eraser' || currentTool.name == 'rectangle' || currentTool.name == 'ellipse' || currentTool.name === 'line'))
|
||||
new HistoryStateEditCanvas();
|
||||
else if (currentTool.name == 'moveselection') {
|
||||
if (!cursorInSelectedArea() &&
|
||||
((mouseEvent.target.id == 'canvas-view') || mouseEvent.target.className == 'drawingCanvas')) {
|
||||
tool.pencil.switchTo();
|
||||
canDraw = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!currentLayer.isLocked && !currentLayer.isVisible && canDraw) {
|
||||
draw(mouseEvent);
|
||||
}
|
||||
}
|
||||
else if (mouseEvent.which == 2) {
|
||||
tool.pan.brushSize = currentTool.brushSize;
|
||||
currentTool = tool.pan;
|
||||
}
|
||||
else if (currentTool.name == 'pencil' && mouseEvent.which == 3) {
|
||||
currentTool = tool.resizebrush;
|
||||
tool.pencil.previousBrushSize = tool.pencil.brushSize;
|
||||
}
|
||||
else if (currentTool.name == 'eraser' && mouseEvent.which == 3) {
|
||||
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';
|
||||
|
||||
return false;
|
||||
}, false);
|
||||
|
||||
|
||||
|
||||
//mouseup - end drawing
|
||||
window.addEventListener("mouseup", function (mouseEvent) {
|
||||
// Saving the event in case something else needs it
|
||||
currentMouseEvent = mouseEvent;
|
||||
|
||||
closeMenu();
|
||||
|
||||
if (currentLayer != null && !isChildOfByClass(mouseEvent.target, "layers-menu-entry")) {
|
||||
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') {
|
||||
var cursorLocation = getCursorPosition(mouseEvent);
|
||||
var selectedColor = getEyedropperColor(cursorLocation);
|
||||
var newColor = rgbToHex(selectedColor[0],selectedColor[1],selectedColor[2]);
|
||||
|
||||
currentGlobalColor = "#" + newColor;
|
||||
|
||||
for (let i=1; i<layers.length - 1; i++) {
|
||||
layers[i].context.fillStyle = currentGlobalColor;
|
||||
}
|
||||
|
||||
var colors = document.getElementsByClassName('color-button');
|
||||
for (var i = 0; i < colors.length; i++) {
|
||||
|
||||
//if picked color matches this color
|
||||
if (newColor == colors[i].jscolor.toString()) {
|
||||
//remove current color selection
|
||||
document.querySelector("#colors-menu li.selected")?.classList.remove("selected");
|
||||
|
||||
//set current color
|
||||
|
||||
for (let i=2; i<layers.length; i++) {
|
||||
layers[i].context.fillStyle = '#' + newColor;
|
||||
}
|
||||
|
||||
//make color selected
|
||||
colors[i].parentElement.classList.add('selected');
|
||||
|
||||
//hide eyedropper
|
||||
eyedropperPreview.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (currentTool.name == 'fill' && mouseEvent.target.className == 'drawingCanvas') {
|
||||
|
||||
//get cursor postion
|
||||
var cursorLocation = getCursorPosition(mouseEvent);
|
||||
|
||||
//offset to match cursor point
|
||||
cursorLocation[0] += 2;
|
||||
cursorLocation[1] += 12;
|
||||
|
||||
//fill starting at the location
|
||||
fill(cursorLocation);
|
||||
currentLayer.updateLayerPreview();
|
||||
}
|
||||
else if (currentTool.name == 'zoom' && mouseEvent.target.className == 'drawingCanvas') {
|
||||
let mode;
|
||||
if (mouseEvent.which == 1){
|
||||
mode = "in";
|
||||
}
|
||||
}
|
||||
else if (currentTool == 'zoom' && mouseEvent.target.className == 'drawingCanvas') {
|
||||
let mode;
|
||||
if (mouseEvent.which == 1){
|
||||
mode = 'in';
|
||||
}
|
||||
else if (mouseEvent.which == 3){
|
||||
mode = 'out';
|
||||
}
|
||||
|
||||
changeZoom(mode, getCursorPosition(mouseEvent));
|
||||
|
||||
for (let i=1; i<layers.length; i++) {
|
||||
layers[i].copyData(layers[0]);
|
||||
}
|
||||
}
|
||||
else if (currentTool.name == 'rectselect' && isRectSelecting) {
|
||||
endRectSelection(mouseEvent);
|
||||
}
|
||||
else if (currentTool.name == 'rectangle' && isDrawingRect) {
|
||||
endRectDrawing(mouseEvent);
|
||||
currentLayer.updateLayerPreview();
|
||||
}
|
||||
else if (currentTool.name == 'ellipse' && isDrawingEllipse) {
|
||||
endEllipseDrawing(mouseEvent);
|
||||
currentLayer.updateLayerPreview();
|
||||
}
|
||||
|
||||
dragging = false;
|
||||
currentTool = currentToolTemp;
|
||||
|
||||
currentTool.updateCursor();
|
||||
var cursorLocation = getCursorPosition(mouseEvent);
|
||||
currentTool.moveBrushPreview(cursorLocation);
|
||||
|
||||
|
||||
}, false);
|
||||
|
||||
function setPreviewPosition(preview, size){
|
||||
let toAdd = 0;
|
||||
|
||||
// This prevents the brush to be placed in the middle of pixels
|
||||
if (size % 2 == 0) {
|
||||
toAdd = 0.5;
|
||||
}
|
||||
|
||||
preview.style.left = (
|
||||
currentLayer.canvas.offsetLeft
|
||||
+ Math.floor(cursor[0]/zoom) * zoom
|
||||
- Math.floor(size / 2) * zoom + toAdd
|
||||
) + 'px';
|
||||
preview.style.top = (
|
||||
currentLayer.canvas.offsetTop
|
||||
+ Math.floor(cursor[1]/zoom) * zoom
|
||||
- Math.floor(size / 2) * zoom + toAdd
|
||||
) + 'px';
|
||||
}
|
||||
|
||||
|
||||
// OPTIMIZABLE: redundant || mouseEvent.target.className in currentTool ifs
|
||||
|
||||
//mouse is moving on canvas
|
||||
window.addEventListener("mousemove", draw, false);
|
||||
window.addEventListener("mousedown", draw, false);
|
||||
|
||||
function draw (mouseEvent) {
|
||||
if (!dialogueOpen)
|
||||
{
|
||||
lastMouseMovePos = getCursorPosition(mouseEvent);
|
||||
// Saving the event in case something else needs it
|
||||
currentMouseEvent = mouseEvent;
|
||||
|
||||
var cursorLocation = lastMouseMovePos;
|
||||
|
||||
//if a document hasnt yet been created or the current layer is locked, exit this function
|
||||
if (!documentCreated || dialogueOpen || !currentLayer.isVisible || currentLayer.isLocked) return;
|
||||
|
||||
// Hiding eyedropper, will be shown if it's needed
|
||||
eyedropperPreview.style.display = 'none';
|
||||
|
||||
if (currentTool.name == 'pencil') {
|
||||
//hide brush preview outside of canvas / canvas view
|
||||
if (mouseEvent.target.className == 'drawingCanvas'|| mouseEvent.target.className == 'drawingCanvas')
|
||||
brushPreview.style.visibility = 'visible';
|
||||
else
|
||||
brushPreview.style.visibility = 'hidden';
|
||||
|
||||
//draw line to current pixel
|
||||
if (dragging) {
|
||||
if (mouseEvent.target.className == 'drawingCanvas' || mouseEvent.target.className == 'drawingCanvas') {
|
||||
line(Math.floor(lastMouseClickPos[0]/zoom),
|
||||
Math.floor(lastMouseClickPos[1]/zoom),
|
||||
Math.floor(cursorLocation[0]/zoom),
|
||||
Math.floor(cursorLocation[1]/zoom),
|
||||
tool.pencil.brushSize
|
||||
);
|
||||
lastMouseClickPos = cursorLocation;
|
||||
}
|
||||
}
|
||||
|
||||
//get lightness value of color
|
||||
var selectedColor = currentLayer.context.getImageData(Math.floor(cursorLocation[0]/zoom),Math.floor(cursorLocation[1]/zoom),1,1).data;
|
||||
var colorLightness = Math.max(selectedColor[0],selectedColor[1],selectedColor[2])
|
||||
|
||||
//for the darkest 50% of colors, change the brush preview to dark mode
|
||||
if (colorLightness>127) brushPreview.classList.remove('dark');
|
||||
else brushPreview.classList.add('dark');
|
||||
|
||||
currentLayer.updateLayerPreview();
|
||||
}
|
||||
// Decided to write a different implementation in case of differences between the brush and the eraser tool
|
||||
else if (currentTool.name == 'eraser') {
|
||||
//hide brush preview outside of canvas / canvas view
|
||||
if (mouseEvent.target.className == 'drawingCanvas' || mouseEvent.target.className == 'drawingCanvas')
|
||||
brushPreview.style.visibility = 'visible';
|
||||
else
|
||||
brushPreview.style.visibility = 'hidden';
|
||||
|
||||
//draw line to current pixel
|
||||
if (dragging) {
|
||||
if (mouseEvent.target.className == 'drawingCanvas' || mouseEvent.target.className == 'drawingCanvas') {
|
||||
line(Math.floor(lastMouseClickPos[0]/zoom),Math.floor(lastMouseClickPos[1]/zoom),Math.floor(cursorLocation[0]/zoom),Math.floor(cursorLocation[1]/zoom), currentTool.brushSize);
|
||||
lastMouseClickPos = cursorLocation;
|
||||
}
|
||||
}
|
||||
|
||||
currentLayer.updateLayerPreview();
|
||||
}
|
||||
else if (currentTool.name == 'rectangle')
|
||||
{
|
||||
//hide brush preview outside of canvas / canvas view
|
||||
if (mouseEvent.target.className == 'drawingCanvas'|| mouseEvent.target.className == 'drawingCanvas')
|
||||
brushPreview.style.visibility = 'visible';
|
||||
else
|
||||
brushPreview.style.visibility = 'hidden';
|
||||
|
||||
if (!isDrawingRect && dragging) {
|
||||
startRectDrawing(mouseEvent);
|
||||
}
|
||||
else if (dragging){
|
||||
updateRectDrawing(mouseEvent);
|
||||
}
|
||||
}
|
||||
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]));
|
||||
// Copying that position to the other layers
|
||||
for (let i=1; i<layers.length; i++) {
|
||||
layers[i].copyData(layers[0]);
|
||||
}
|
||||
// Updating cursorLocation with new layer position
|
||||
lastMouseMovePos = getCursorPosition(mouseEvent);
|
||||
cursorLocation = lastMouseMovePos;
|
||||
}
|
||||
else if (currentTool.name == 'eyedropper' && dragging && mouseEvent.target.className == 'drawingCanvas') {
|
||||
let selectedColor = getEyedropperColor(cursorLocation);
|
||||
|
||||
eyedropperPreview.style.borderColor = '#'+rgbToHex(selectedColor[0],selectedColor[1],selectedColor[2]);
|
||||
eyedropperPreview.style.display = 'block';
|
||||
|
||||
eyedropperPreview.style.left = cursorLocation[0] + currentLayer.canvas.offsetLeft - 30 + 'px';
|
||||
eyedropperPreview.style.top = cursorLocation[1] + currentLayer.canvas.offsetTop - 30 + 'px';
|
||||
|
||||
var colorLightness = Math.max(selectedColor[0],selectedColor[1],selectedColor[2]);
|
||||
|
||||
//for the darkest 50% of colors, change the eyedropper preview to dark mode
|
||||
if (colorLightness>127) eyedropperPreview.classList.remove('dark');
|
||||
else eyedropperPreview.classList.add('dark');
|
||||
}
|
||||
else if (currentTool.name == 'resizebrush' && dragging) {
|
||||
//get new brush size based on x distance from original clicking location
|
||||
var distanceFromClick = cursorLocation[0] - lastMouseClickPos[0];
|
||||
//var roundingAmount = 20 - Math.round(distanceFromClick/10);
|
||||
//this doesnt work in reverse... because... it's not basing it off of the brush size which it should be
|
||||
var brushSizeChange = Math.round(distanceFromClick/10);
|
||||
var newBrushSize = tool.pencil.previousBrushSize + brushSizeChange;
|
||||
|
||||
//set the brush to the new size as long as its bigger than 1
|
||||
tool.pencil.brushSize = Math.max(1,newBrushSize);
|
||||
|
||||
//fix offset so the cursor stays centered
|
||||
tool.pencil.moveBrushPreview(lastMouseClickPos);
|
||||
currentTool.updateCursor();
|
||||
}
|
||||
else if (currentTool.name == 'resizeeraser' && dragging) {
|
||||
//get new brush size based on x distance from original clicking location
|
||||
var distanceFromClick = cursorLocation[0] - lastMouseClickPos[0];
|
||||
//var roundingAmount = 20 - Math.round(distanceFromClick/10);
|
||||
//this doesnt work in reverse... because... it's not basing it off of the brush size which it should be
|
||||
var eraserSizeChange = Math.round(distanceFromClick/10);
|
||||
var newEraserSizeChange = tool.eraser.previousBrushSize + eraserSizeChange;
|
||||
|
||||
//set the brush to the new size as long as its bigger than 1
|
||||
tool.eraser.brushSize = Math.max(1,newEraserSizeChange);
|
||||
|
||||
//fix offset so the cursor stays centered
|
||||
tool.eraser.moveBrushPreview(lastMouseClickPos);
|
||||
currentTool.updateCursor();
|
||||
}
|
||||
else if (currentTool.name == 'resizerectangle' && dragging) {
|
||||
//get new brush size based on x distance from original clicking location
|
||||
var distanceFromClick = cursorLocation[0] - lastMouseClickPos[0];
|
||||
//var roundingAmount = 20 - Math.round(distanceFromClick/10);
|
||||
//this doesnt work in reverse... because... it's not basing it off of the brush size which it should be
|
||||
var rectangleSizeChange = Math.round(distanceFromClick/10);
|
||||
// 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;
|
||||
startRectSelection(mouseEvent);
|
||||
}
|
||||
else if (dragging && isRectSelecting) {
|
||||
updateRectSelection(mouseEvent);
|
||||
}
|
||||
else if (isRectSelecting) {
|
||||
endRectSelection();
|
||||
}
|
||||
}
|
||||
else if (currentTool.name == 'moveselection') {
|
||||
// Updating the cursor (move if inside rect, cross if not)
|
||||
currentTool.updateCursor();
|
||||
|
||||
// If I'm dragging, I move the preview
|
||||
if (dragging && cursorInSelectedArea()) {
|
||||
updateMovePreview(getCursorPosition(mouseEvent));
|
||||
}
|
||||
}
|
||||
else if (currentTool.name === "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();
|
||||
}
|
||||
|
||||
// Moving brush preview
|
||||
currentTool.moveBrushPreview(cursorLocation);
|
||||
}
|
||||
|
||||
if (mouseEvent.target.className == 'drawingCanvas')
|
||||
currentTool.updateCursor();
|
||||
else
|
||||
canvasView.style.cursor = 'default';
|
||||
|
||||
console.log("Cursor: " + canvasView.style.cursor);
|
||||
}
|
||||
|
||||
//mousewheel scroll
|
||||
canvasView.addEventListener("wheel", function(mouseEvent){
|
||||
let mode;
|
||||
if (mouseEvent.deltaY < 0){
|
||||
mode = 'in';
|
||||
}
|
||||
else if (mouseEvent.deltaY > 0) {
|
||||
mode = 'out';
|
||||
}
|
||||
|
||||
// Changing zoom and position of the first layer
|
||||
changeZoom(mode, getCursorPosition(mouseEvent));
|
||||
|
||||
for (let i=1; i<layers.length; i++) {
|
||||
// Copying first layer's data into the other layers
|
||||
layers[i].copyData(layers[0]);
|
||||
}
|
||||
});
|
110
js/_move.js
110
js/_move.js
|
@ -1,110 +0,0 @@
|
|||
var imageDataToMove;
|
||||
var originalDataPosition;
|
||||
var canMoveSelection = false;
|
||||
var lastMovePos;
|
||||
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);
|
||||
}
|
||||
|
||||
firstTimeMove = false;
|
||||
|
||||
lastMousePos = mousePosition;
|
||||
// clear the entire tmp layer
|
||||
TMPLayer.context.clearRect(0, 0, TMPLayer.canvas.width, TMPLayer.canvas.height);
|
||||
// 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) {
|
||||
// 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) {
|
||||
let currentMovePixel = [
|
||||
imageDataToMove.data[i], imageDataToMove.data[i+1],
|
||||
imageDataToMove.data[i+2], imageDataToMove.data[i+3]
|
||||
];
|
||||
|
||||
let currentUnderlyingPixel = [
|
||||
underlyingImageData.data[i], underlyingImageData.data[i+1],
|
||||
underlyingImageData.data[i+2], underlyingImageData.data[i+3]
|
||||
];
|
||||
|
||||
// If the pixel of the clipboard is empty, but the one below it isn't, I use the pixel below
|
||||
if (isPixelEmpty(currentMovePixel)) {
|
||||
if (!isPixelEmpty(underlyingImageData)) {
|
||||
imageDataToMove.data[i] = currentUnderlyingPixel[0];
|
||||
imageDataToMove.data[i+1] = currentUnderlyingPixel[1];
|
||||
imageDataToMove.data[i+2] = currentUnderlyingPixel[2];
|
||||
imageDataToMove.data[i+3] = currentUnderlyingPixel[3];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
copiedStartY);
|
||||
}
|
||||
|
||||
imageDataToMove.data.set(cleanImageData.data);
|
||||
}
|
||||
|
||||
// Resetting all the flags
|
||||
selectionCanceled = true;
|
||||
isRectSelecting = false;
|
||||
firstTimeMove = true;
|
||||
imageDataToMove = undefined;
|
||||
isPasting = false;
|
||||
isCutting = false;
|
||||
lastMovePos = undefined;
|
||||
currentLayer.updateLayerPreview();
|
||||
|
||||
// Saving the history
|
||||
new HistoryStateEditCanvas();
|
||||
}
|
220
js/_newPixel.js
220
js/_newPixel.js
|
@ -1,220 +0,0 @@
|
|||
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;
|
||||
let associatedLayer;
|
||||
|
||||
if (currentEntry != null) {
|
||||
// Getting the associated layer
|
||||
associatedLayer = getLayerByID(currentEntry.id);
|
||||
|
||||
// Deleting its canvas
|
||||
associatedLayer.canvas.remove();
|
||||
|
||||
// Adding the id to the unused ones
|
||||
unusedIDs.push(currentEntry.id);
|
||||
// Removing the entry from the menu
|
||||
currentEntry.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Removing the old layers from the list
|
||||
for (let i=2; i<nLayers - nAppLayers; i++) {
|
||||
layers.splice(2, 1);
|
||||
}
|
||||
|
||||
// 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 to the new size
|
||||
for (let i=0; i<nLayers; i++) {
|
||||
layers[i].canvasSize = [width, height];
|
||||
}
|
||||
}
|
||||
|
||||
// Adding the checkerboard behind it
|
||||
checkerBoard = new Layer(width, height, checkerBoardCanvas);
|
||||
|
||||
// Creating the vfx layer on top of everything
|
||||
VFXLayer = new Layer(width, height, VFXCanvas);
|
||||
|
||||
// Tmp layer to draw previews on
|
||||
TMPLayer = new Layer(width, height, TMPCanvas);
|
||||
|
||||
// Pixel grid
|
||||
pixelGrid = new Layer(width, height, pixelGridCanvas);
|
||||
// Setting the general canvasSize
|
||||
canvasSize = currentLayer.canvasSize;
|
||||
|
||||
if (firstPixel) {
|
||||
// Cloning the entry so that when I change something on the first layer, those changes aren't
|
||||
// propagated to the other ones
|
||||
layerListEntry = layerListEntry.cloneNode(true);
|
||||
// Adding the first layer and the checkerboard to the list of layers
|
||||
layers.push(checkerBoard);
|
||||
layers.push(currentLayer);
|
||||
layers.push(VFXLayer);
|
||||
layers.push(TMPLayer);
|
||||
layers.push(pixelGrid);
|
||||
}
|
||||
|
||||
//remove current palette
|
||||
colors = document.getElementsByClassName('color-button');
|
||||
while (colors.length > 0) {
|
||||
colors[0].parentElement.remove();
|
||||
}
|
||||
|
||||
//add colors from selected palette
|
||||
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) {
|
||||
console.log('HELO', selectedPalette, palettes[selectedPalette])
|
||||
//if this palette isnt the one specified in the url, then reset the url
|
||||
if (!palettes[selectedPalette].specified)
|
||||
history.pushState(null, null, '/pixel-editor');
|
||||
|
||||
//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');
|
||||
|
||||
//generate default colors
|
||||
var fg = hslToRgb(Math.floor(Math.random()*255), 230,70);
|
||||
var bg = hslToRgb(Math.floor(Math.random()*255), 230,170);
|
||||
|
||||
//convert colors to hex
|
||||
var defaultForegroundColor = rgbToHex(fg.r,fg.g,fg.b);
|
||||
var defaultBackgroundColor = rgbToHex(bg.r,bg.g,bg.b);
|
||||
|
||||
//add colors to palette
|
||||
addColor(defaultForegroundColor).classList.add('selected');
|
||||
addColor(defaultBackgroundColor);
|
||||
|
||||
//set current drawing color as foreground color
|
||||
currentLayer.context.fillStyle = '#'+defaultForegroundColor;
|
||||
currentGlobalColor = '#' + defaultForegroundColor;
|
||||
selectedPalette = 'none';
|
||||
}
|
||||
|
||||
//fill background of canvas with bg color
|
||||
fillCheckerboard();
|
||||
fillPixelGrid();
|
||||
|
||||
//reset undo and redo states
|
||||
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'];
|
||||
|
||||
if (layerData != null) {
|
||||
// Setting id
|
||||
let createdLayer = addLayer(layerData.id, false);
|
||||
// Setting name
|
||||
createdLayer.menuEntry.getElementsByTagName("p")[0].innerHTML = layerData.name;
|
||||
|
||||
// Adding the image (I can do that because they're sorted by increasing z-index)
|
||||
let img = new Image();
|
||||
img.onload = function() {
|
||||
createdLayer.context.drawImage(img, 0, 0);
|
||||
createdLayer.updateLayerPreview();
|
||||
|
||||
if (i == (fileContent['nLayers'] - 1)) {
|
||||
createPaletteFromLayers();
|
||||
}
|
||||
};
|
||||
|
||||
img.src = layerImage;
|
||||
|
||||
// Setting visibility and lock options
|
||||
if (!layerData.isVisible) {
|
||||
createdLayer.hide();
|
||||
}
|
||||
if (layerData.isLocked) {
|
||||
createdLayer.lock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deleting the default layer
|
||||
deleteLayer(false);
|
||||
}
|
||||
|
||||
// Applying the correct editor mode
|
||||
if (pixelEditorMode == 'Basic') {
|
||||
switchMode(false);
|
||||
}
|
||||
else {
|
||||
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');
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
|
||||
|
||||
|
||||
|
||||
//when the page is done loading, you can get ready to start
|
||||
window.onload = function () {
|
||||
|
||||
featureToggles.onLoad();
|
||||
|
||||
currentTool.updateCursor();
|
||||
|
||||
//check if there are any url parameters
|
||||
if (window.location.pathname.replace('/pixel-editor/','').length <= 1) {
|
||||
console.log('no url parameters were found');
|
||||
|
||||
//show splash screen
|
||||
showDialogue('splash', false);
|
||||
}
|
||||
|
||||
//url parameters were specified
|
||||
else {
|
||||
console.log('loading preset from url parameters', window.location.pathname);
|
||||
|
||||
let args = window.location.pathname.split('/');
|
||||
let paletteSlug = args[2];
|
||||
let dimentions = args[3];
|
||||
|
||||
//fetch palette via lospec palette API
|
||||
fetch('https://lospec.com/palette-list/'+paletteSlug+'.json')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
//palette loaded successfully
|
||||
console.log('loaded palette', data);
|
||||
palettes[paletteSlug] = data;
|
||||
palettes[paletteSlug].specified = true;
|
||||
|
||||
//refresh list of palettes
|
||||
document.getElementById('palette-menu-splash').refresh();
|
||||
|
||||
//if the dimentions were specified
|
||||
if (dimentions && dimentions.length >= 3 && dimentions.includes('x')) {
|
||||
let width = dimentions.split('x')[0];
|
||||
let height = dimentions.split('x')[1];
|
||||
|
||||
console.log('dimentions were specified',width,'x',height)
|
||||
|
||||
//firstPixel = false;
|
||||
|
||||
//create new document
|
||||
newPixel(width, height);
|
||||
}
|
||||
|
||||
//dimentions were not specified -- show splash screen with palette preselected
|
||||
else {
|
||||
//show splash
|
||||
showDialogue('new-pixel', false);
|
||||
}
|
||||
|
||||
})
|
||||
//error fetching url (either palette doesn't exist, or lospec is down)
|
||||
.catch((error) => {
|
||||
console.warn('failed to load palette "'+paletteSlug+'"', error);
|
||||
|
||||
//proceed to splash screen
|
||||
showDialogue('splash', false);
|
||||
});
|
||||
}
|
||||
};
|
|
@ -1,7 +0,0 @@
|
|||
//prevent user from leaving page with unsaved data
|
||||
window.onbeforeunload = function() {
|
||||
if (documentCreated)
|
||||
return 'You will lose your pixel if it\'s not saved!';
|
||||
|
||||
else return;
|
||||
};
|
|
@ -1,333 +0,0 @@
|
|||
/** 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,270 +0,0 @@
|
|||
/***********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, belowLayer.canvas.width, belowLayer.canvas.height);
|
||||
let toMergeImageData = topLayer.getImageData(0, 0, topLayer.canvas.width, topLayer.canvas.height);
|
||||
|
||||
for (let i=0; i<belowImageData.data.length; i+=4) {
|
||||
let currentMovePixel = [
|
||||
toMergeImageData.data[i], toMergeImageData.data[i+1],
|
||||
toMergeImageData.data[i+2], toMergeImageData.data[i+3]
|
||||
];
|
||||
|
||||
let currentUnderlyingPixel = [
|
||||
belowImageData.data[i], belowImageData.data[i+1],
|
||||
belowImageData.data[i+2], belowImageData.data[i+3]
|
||||
];
|
||||
|
||||
if (isPixelEmpty(currentMovePixel)) {
|
||||
if (!isPixelEmpty(belowImageData)) {
|
||||
toMergeImageData.data[i] = currentUnderlyingPixel[0];
|
||||
toMergeImageData.data[i+1] = currentUnderlyingPixel[1];
|
||||
toMergeImageData.data[i+2] = currentUnderlyingPixel[2];
|
||||
toMergeImageData.data[i+3] = currentUnderlyingPixel[3];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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";
|
||||
|
||||
keyboardEvent[initMethod](
|
||||
"keydown", // event type: keydown, keyup, keypress
|
||||
true, // bubbles
|
||||
true, // cancelable
|
||||
window, // view: should be window
|
||||
ctrl, // ctrlKey
|
||||
alt, // altKey
|
||||
shift, // shiftKey
|
||||
false, // metaKey
|
||||
keyCode, // keyCode: unsigned long - the virtual key code, else 0
|
||||
keyCode // charCode: unsigned long - the Unicode character associated with the depressed key, else 0
|
||||
);
|
||||
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 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;
|
||||
|
||||
if (!isPixelEmpty(tmpColour)) {
|
||||
selectedColor = tmpColour;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the final colour was empty, I return black
|
||||
if (isPixelEmpty(tmpColour) && selectedColor === undefined) {
|
||||
selectedColor = [0, 0, 0];
|
||||
}
|
||||
|
||||
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) {
|
||||
do {
|
||||
curleft += element.offsetLeft;
|
||||
curtop += element.offsetTop;
|
||||
} while (element = element.offsetParent);
|
||||
}
|
||||
|
||||
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)
|
||||
const srcY = Math.floor(y * src.height / dst.height)
|
||||
|
||||
let srcPos = ((srcY * src.width) + srcX) * 4
|
||||
|
||||
dst.data[pos++] = src.data[srcPos++] // R
|
||||
dst.data[pos++] = src.data[srcPos++] // G
|
||||
dst.data[pos++] = src.data[srcPos++] // B
|
||||
dst.data[pos++] = src.data[srcPos++] // A
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 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)
|
||||
}
|
||||
|
||||
function interpolateHorizontal (offset, x, y, xMin, xMax) {
|
||||
const vMin = src.data[((y * src.width + xMin) * 4) + offset]
|
||||
if (xMin === xMax) return vMin
|
||||
|
||||
const vMax = src.data[((y * src.width + xMax) * 4) + offset]
|
||||
return interpolate(x, xMin, xMax, vMin, vMax)
|
||||
}
|
||||
|
||||
function interpolateVertical (offset, x, xMin, xMax, y, yMin, yMax) {
|
||||
const vMin = interpolateHorizontal(offset, x, yMin, xMin, xMax)
|
||||
if (yMin === yMax) return vMin
|
||||
|
||||
const vMax = interpolateHorizontal(offset, x, yMax, xMin, xMax)
|
||||
return interpolate(y, yMin, yMax, vMin, vMax)
|
||||
}
|
||||
|
||||
let pos = 0
|
||||
|
||||
for (let y = 0; y < dst.height; y++) {
|
||||
for (let x = 0; x < dst.width; x++) {
|
||||
const srcX = x * src.width / dst.width
|
||||
const srcY = y * src.height / dst.height
|
||||
|
||||
const xMin = Math.floor(srcX)
|
||||
const yMin = Math.floor(srcY)
|
||||
|
||||
const xMax = Math.min(Math.ceil(srcX), src.width - 1)
|
||||
const yMax = Math.min(Math.ceil(srcY), src.height - 1)
|
||||
|
||||
dst.data[pos++] = interpolateVertical(0, srcX, xMin, xMax, srcY, yMin, yMax) // R
|
||||
dst.data[pos++] = interpolateVertical(1, srcX, xMin, xMax, srcY, yMin, yMax) // G
|
||||
dst.data[pos++] = interpolateVertical(2, srcX, xMin, xMax, srcY, yMin, yMax) // B
|
||||
dst.data[pos++] = interpolateVertical(3, srcX, xMin, xMax, srcY, yMin, yMax) // A
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 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;
|
||||
switch (algorithm) {
|
||||
case 'nearest-neighbor': resize = nearestNeighbor; break
|
||||
case 'bilinear-interpolation': resize = bilinearInterpolation; break
|
||||
default: return image;
|
||||
}
|
||||
|
||||
const result = new ImageData(width, height)
|
||||
|
||||
resize(image, result)
|
||||
|
||||
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;
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
/**
|
||||
* Opens the export window and initializes events for export customization.
|
||||
*/
|
||||
function openPixelExportWindow() {
|
||||
var selectedPalette = getText('palette-button');
|
||||
|
||||
if (selectedPalette != 'Choose a palette...'){
|
||||
var paletteAbbreviation = palettes[selectedPalette].name;
|
||||
var fileName = 'pixel-'+paletteAbbreviation+'-'+layers[0].canvasSize[0]+'x'+layers[0].canvasSize[1]+'.png';
|
||||
} else {
|
||||
var fileName = 'pixel-'+layers[0].canvasSize[0]+'x'+layers[0].canvasSize[1]+'.png';
|
||||
selectedPalette = 'none';
|
||||
}
|
||||
|
||||
setValue('file-name', fileName);
|
||||
|
||||
document.getElementById("export-confirm").addEventListener("click", exportPixel);
|
||||
|
||||
showDialogue('export', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports the pixel based on the export dialogue.
|
||||
*/
|
||||
function exportPixel() {
|
||||
if (documentCreated) {
|
||||
var fileName = getValue('file-name');
|
||||
|
||||
//set download link
|
||||
var linkHolder = document.getElementById('save-image-link-holder');
|
||||
// Creating a tmp canvas to flatten everything
|
||||
var exportCanvas = document.createElement("canvas");
|
||||
var emptyCanvas = document.createElement("canvas");
|
||||
var layersCopy = layers.slice();
|
||||
|
||||
exportCanvas.width = layers[0].canvasSize[0];
|
||||
exportCanvas.height = layers[0].canvasSize[1];
|
||||
|
||||
emptyCanvas.width = layers[0].canvasSize[0];
|
||||
emptyCanvas.height = layers[0].canvasSize[1];
|
||||
|
||||
// Sorting the layers by z index
|
||||
layersCopy.sort((a, b) => (a.canvas.style.zIndex > b.canvas.style.zIndex) ? 1 : -1);
|
||||
|
||||
// Merging every layer on the export canvas
|
||||
for (let i=0; i<layersCopy.length; i++) {
|
||||
if (layersCopy[i].menuEntry != null && layersCopy[i].isVisible) {
|
||||
mergeLayers(exportCanvas.getContext('2d'), layersCopy[i].context);
|
||||
}
|
||||
// I'm not going to find out why the layer ordering screws up if you don't copy
|
||||
// a blank canvas when layers[i] is not set as visible, but if you have time to
|
||||
// spend, feel free to investigate (comment the else, create 3 layers: hide the
|
||||
// middle one and export, the other 2 will be swapped in their order)
|
||||
else {
|
||||
mergeLayers(exportCanvas.getContext('2d'), emptyCanvas.getContext('2d'));
|
||||
}
|
||||
}
|
||||
|
||||
linkHolder.href = exportCanvas.toDataURL();
|
||||
linkHolder.download = fileName;
|
||||
|
||||
linkHolder.click();
|
||||
|
||||
emptyCanvas.remove();
|
||||
exportCanvas.remove();
|
||||
|
||||
//track google event
|
||||
ga('send', 'event', 'Pixel Editor Export', selectedPalette, canvasSize[0]+'/'+canvasSize[1]); /*global ga*/
|
||||
}
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
// Start colour of the pixel grid (can be changed in the preferences)
|
||||
let pixelGridColor = "#000000";
|
||||
// Distance between one line and another in HTML pixels
|
||||
let lineDistance = 12;
|
||||
// The grid is visible by default
|
||||
let pixelGridVisible = true;
|
||||
// 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(newState) {
|
||||
console.log('toggling pixel grid', newState)
|
||||
// Getting the button because I have to change its text
|
||||
let button = document.getElementById("toggle-pixelgrid-button");
|
||||
|
||||
//Set the state based on the passed newState variable, otherwise just toggle it
|
||||
if (newState == 'on') pixelGridVisible = true;
|
||||
else if (newState == 'off') pixelGridVisible = false;
|
||||
else 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;
|
||||
|
||||
// OPTIMIZABLE, could probably be a bit more elegant
|
||||
// Draw horizontal lines
|
||||
for (let i=0; i<pixelGridCanvas.width / lineDistance; i++) {
|
||||
context.strokeStyle = settings.pixelGridColour;
|
||||
|
||||
context.beginPath();
|
||||
context.moveTo(i * lineDistance + 0.5, 0);
|
||||
context.lineTo(i * lineDistance + 0.5, originalSize[1] * lineDistance);
|
||||
context.stroke();
|
||||
context.closePath();
|
||||
}
|
||||
|
||||
// Draw vertical lines
|
||||
for (let i=0; i<pixelGridCanvas.height / lineDistance; i++) {
|
||||
context.beginPath();
|
||||
context.moveTo(0, i * lineDistance + 0.5);
|
||||
context.lineTo(originalSize[0] * lineDistance, i * lineDistance + 0.5);
|
||||
context.stroke();
|
||||
context.closePath();
|
||||
}
|
||||
|
||||
if (!pixelGridVisible) {
|
||||
pixelGridCanvas.style.display = 'none';
|
||||
}
|
||||
}
|
|
@ -1,178 +0,0 @@
|
|||
var isRectSelecting = false;
|
||||
let startX;
|
||||
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();
|
||||
// Putting the vfx layer on top of everything
|
||||
VFXCanvas.style.zIndex = MAX_Z_INDEX;
|
||||
|
||||
// Saving the start coords of the rect
|
||||
let cursorPos = getCursorPosition(mouseEvent);
|
||||
startX = Math.round(cursorPos[0] / zoom) - 0.5;
|
||||
startY = Math.round(cursorPos[1] / zoom) - 0.5;
|
||||
|
||||
// Avoiding external selections
|
||||
if (startX < 0) {
|
||||
startX = 0;
|
||||
}
|
||||
else if (startX > currentLayer.canvas.width) {
|
||||
startX = currentLayer.canvas.width;
|
||||
}
|
||||
|
||||
if (startY < 0) {
|
||||
startY = 0;
|
||||
}
|
||||
else if (startY > currentLayer.canvas.height) {
|
||||
startY = currentLayer.canvas.height;
|
||||
}
|
||||
|
||||
// Drawing the rect
|
||||
drawRect(startX, startY);
|
||||
selectionCanceled = false;
|
||||
}
|
||||
|
||||
/** Updates the selection
|
||||
*
|
||||
* @param {*} mouseEvent
|
||||
*/
|
||||
function updateRectSelection(mouseEvent) {
|
||||
let pos = getCursorPosition(mouseEvent);
|
||||
|
||||
// Drawing the rect
|
||||
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);
|
||||
endX = Math.round(currentPos[0] / zoom) + 0.5;
|
||||
endY = Math.round(currentPos[1] / zoom) + 0.5;
|
||||
|
||||
// Inverting end and start (start must always be the top left corner)
|
||||
if (endX < startX) {
|
||||
let tmp = endX;
|
||||
endX = startX;
|
||||
startX = tmp;
|
||||
}
|
||||
// Same for the y
|
||||
if (endY < startY) {
|
||||
let tmp = endY;
|
||||
endY = startY;
|
||||
startY = tmp;
|
||||
}
|
||||
|
||||
// Selecting the move tool
|
||||
currentTool = tool.moveselection;
|
||||
currentToolTemp = currentTool;
|
||||
|
||||
// Resetting this
|
||||
isRectSelecting = false;
|
||||
|
||||
// Updating the cursor
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/** 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');
|
||||
|
||||
// Clearing the vfx canvas
|
||||
vfxContext.clearRect(0, 0, VFXCanvas.width, VFXCanvas.height);
|
||||
vfxContext.lineWidth = 1;
|
||||
vfxContext.strokeStyle = 'black';
|
||||
vfxContext.setLineDash([4]);
|
||||
|
||||
// Drawing the rect
|
||||
vfxContext.beginPath();
|
||||
vfxContext.rect(startX, startY, x - startX, y - startY);
|
||||
|
||||
vfxContext.stroke();
|
||||
}
|
||||
|
||||
function applyChanges() {
|
||||
VFXCanvas.style.zIndex = MIN_Z_INDEX;
|
||||
}
|
||||
|
||||
// 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);
|
||||
let bottomY = Math.min(startY, endY);
|
||||
|
||||
if (leftX <= x && x <= rightX) {
|
||||
if (bottomY <= y && y <= topY) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
// Clearing the vfx canvas
|
||||
vfxContext.clearRect(0, 0, VFXCanvas.width, VFXCanvas.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);
|
||||
endY = startY + Math.round(height);
|
||||
|
||||
// Drawing the rect
|
||||
vfxContext.beginPath();
|
||||
vfxContext.rect(startX, startY, width, height);
|
||||
|
||||
vfxContext.stroke();
|
||||
}
|
145
js/_rectangle.js
145
js/_rectangle.js
|
@ -1,145 +0,0 @@
|
|||
// 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");
|
||||
|
||||
// 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 = parseInt(currentLayer.canvas.style.zIndex, 10) + 1;;
|
||||
// Updating flag
|
||||
isDrawingRect = true;
|
||||
|
||||
// Saving the start coords of the rect
|
||||
let cursorPos = getCursorPosition(mouseEvent);
|
||||
startRectX = Math.floor(cursorPos[0] / zoom) + 0.5;
|
||||
startRectY = Math.floor(cursorPos[1] / zoom) + 0.5;
|
||||
|
||||
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 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.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) {
|
||||
let tmp = endRectX;
|
||||
endRectX = startRectX;
|
||||
startRectX = tmp;
|
||||
}
|
||||
// Same for the y
|
||||
if (endRectY < startRectY) {
|
||||
let tmp = endRectY;
|
||||
endRectY = startRectY;
|
||||
startRectY = tmp;
|
||||
}
|
||||
|
||||
// Resetting this
|
||||
isDrawingRect = false;
|
||||
// Drawing the rect
|
||||
startRectY -= 0.5;
|
||||
endRectY -= 0.5;
|
||||
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 I have to fill it, I do so
|
||||
if (rectangleDrawMode == 'fill') {
|
||||
currentLayer.context.fillRect(startRectX, startRectY, endRectX - startRectX, endRectY - startRectY);
|
||||
}
|
||||
|
||||
// Clearing the vfx canvas
|
||||
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");
|
||||
|
||||
// Clearing the vfx canvas
|
||||
vfxContext.clearRect(0, 0, VFXCanvas.width, VFXCanvas.height);
|
||||
|
||||
// Drawing the rect
|
||||
vfxContext.lineWidth = tool.rectangle.brushSize;
|
||||
vfxContext.strokeStyle = currentGlobalColor;
|
||||
|
||||
// Drawing the rect
|
||||
vfxContext.beginPath();
|
||||
if ((tool.rectangle.brushSize % 2 ) == 0) {
|
||||
vfxContext.rect(startRectX - 0.5, startRectY - 0.5, x - startRectX, y - startRectY);
|
||||
}
|
||||
else {
|
||||
vfxContext.rect(startRectX, startRectY, x - startRectX, y - startRectY);
|
||||
}
|
||||
|
||||
vfxContext.setLineDash([]);
|
||||
vfxContext.stroke();
|
||||
}
|
||||
|
||||
/** Sets the correct tool icon depending on its mode
|
||||
*
|
||||
*/
|
||||
function setRectToolSvg() {
|
||||
if (rectangleDrawMode == 'empty') {
|
||||
emptyRectangleSVG.setAttribute('display', 'visible');
|
||||
fullRectangleSVG.setAttribute('display', 'none');
|
||||
}
|
||||
else {
|
||||
emptyRectangleSVG.setAttribute('display', 'none');
|
||||
fullRectangleSVG.setAttribute('display', 'visible');
|
||||
}
|
||||
}
|
||||
|
||||
function applyChanges() {
|
||||
//VFXCanvas.style.zIndex = MIN_Z_INDEX;
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
//replaces all of a single color on the canvas with a different color
|
||||
//input two rgb color objects {r:0,g:0,b:0}
|
||||
function replaceAllOfColor (oldColor, newColor) {
|
||||
|
||||
//convert strings to objects if nessesary
|
||||
if (typeof oldColor === 'string') oldColor = hexToRgb(oldColor);
|
||||
if (typeof newColor === 'string') newColor = hexToRgb(newColor);
|
||||
|
||||
//create temporary image from canvas to search through
|
||||
var tempImage = currentLayer.context.getImageData(0, 0, canvasSize[0], canvasSize[1]);
|
||||
|
||||
//loop through all pixels
|
||||
for (var i=0;i<tempImage.data.length;i+=4) {
|
||||
//check if pixel matches old color
|
||||
if(tempImage.data[i]==oldColor.r && tempImage.data[i+1]==oldColor.g && tempImage.data[i+2]==oldColor.b){
|
||||
//change to new color
|
||||
tempImage.data[i]=newColor.r;
|
||||
tempImage.data[i+1]=newColor.g;
|
||||
tempImage.data[i+2]=newColor.b;
|
||||
}
|
||||
}
|
||||
|
||||
//put temp image back onto canvas
|
||||
currentLayer.context.putImageData(tempImage,0,0);
|
||||
}
|
|
@ -1,312 +0,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")) {
|
||||
currentPivotObject = buttons[i];
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("rc-width").value = layers[0].canvasSize[0];
|
||||
document.getElementById("rc-height").value = layers[0].canvasSize[1];
|
||||
|
||||
document.getElementById("rc-border-left").addEventListener("change", rcChangedBorder);
|
||||
document.getElementById("rc-border-right").addEventListener("change", rcChangedBorder);
|
||||
document.getElementById("rc-border-top").addEventListener("change", rcChangedBorder);
|
||||
document.getElementById("rc-border-bottom").addEventListener("change", rcChangedBorder);
|
||||
|
||||
document.getElementById("rc-width").addEventListener("change", rcChangedSize);
|
||||
document.getElementById("rc-height").addEventListener("change", rcChangedSize);
|
||||
|
||||
document.getElementById("resize-canvas-confirm").addEventListener("click", resizeCanvas);
|
||||
console.log("Pivot selezionato: " + currentPivotObject);
|
||||
}
|
||||
|
||||
/** 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]) + 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];
|
||||
|
||||
let left = Math.round(widthOffset / 2);
|
||||
let right = widthOffset - left;
|
||||
let top = Math.round(heightOffset / 2);
|
||||
let bottom = heightOffset - top;
|
||||
|
||||
document.getElementById("rc-border-left").value = left;
|
||||
document.getElementById("rc-border-right").value = right;
|
||||
document.getElementById("rc-border-top").value = top;
|
||||
document.getElementById("rc-border-bottom").value = bottom;
|
||||
|
||||
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;
|
||||
let topOffset = 0;
|
||||
let copiedDataIndex = 0;
|
||||
|
||||
// If I'm undoing and I'm not trimming, I manually put the values in the window
|
||||
if (size != null && customData == null) {
|
||||
document.getElementById("rc-width").value = size.x;
|
||||
document.getElementById("rc-height").value = size.y;
|
||||
|
||||
rcChangedSize();
|
||||
}
|
||||
|
||||
rcUpdateBorders();
|
||||
|
||||
// Save all imageDatas
|
||||
for (let i=0; i<layers.length; i++) {
|
||||
if (layers[i].menuEntry != null) {
|
||||
imageDatas.push(layers[i].context.getImageData(0, 0, layers[0].canvasSize[0], layers[0].canvasSize[1]));
|
||||
}
|
||||
}
|
||||
|
||||
// Saving the history only if I'm not already undoing or redoing
|
||||
if (saveHistory && event != null) {
|
||||
// Saving history
|
||||
new HistoryStateResizeCanvas(
|
||||
{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]},
|
||||
imageDatas.slice(), customData != null && saveHistory
|
||||
);
|
||||
|
||||
console.log("salvata");
|
||||
}
|
||||
|
||||
// Resize the canvases
|
||||
for (let i=0; i<layers.length; i++) {
|
||||
layers[i].canvasSize[0] = parseInt(layers[i].canvasSize[0]) + 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];
|
||||
|
||||
layers[i].resize();
|
||||
layers[i].context.fillStyle = currentGlobalColor;
|
||||
}
|
||||
|
||||
// Regenerate the checkerboard
|
||||
fillCheckerboard();
|
||||
fillPixelGrid();
|
||||
// Put the imageDatas in the right position
|
||||
switch (rcPivot)
|
||||
{
|
||||
case 'topleft':
|
||||
leftOffset = 0;
|
||||
topOffset = 0;
|
||||
break;
|
||||
case 'top':
|
||||
leftOffset = (rcBorders.left + rcBorders.right) / 2;
|
||||
topOffset = 0;
|
||||
break;
|
||||
case 'topright':
|
||||
leftOffset = rcBorders.left + rcBorders.right;
|
||||
topOffset = 0;
|
||||
break;
|
||||
case 'left':
|
||||
leftOffset = 0;
|
||||
topOffset = (rcBorders.top + rcBorders.bottom) / 2;
|
||||
break;
|
||||
case 'middle':
|
||||
leftOffset = (rcBorders.left + rcBorders.right) / 2;
|
||||
topOffset = (rcBorders.top + rcBorders.bottom) / 2;
|
||||
break;
|
||||
case 'right':
|
||||
leftOffset = rcBorders.left + rcBorders.right;
|
||||
topOffset = (rcBorders.top + rcBorders.bottom) / 2;
|
||||
break;
|
||||
case 'bottomleft':
|
||||
leftOffset = 0;
|
||||
topOffset = rcBorders.top + rcBorders.bottom;
|
||||
break;
|
||||
case 'bottom':
|
||||
leftOffset = (rcBorders.left + rcBorders.right) / 2;
|
||||
topOffset = rcBorders.top + rcBorders.bottom;
|
||||
break;
|
||||
case 'bottomright':
|
||||
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 {
|
||||
layers[i].context.putImageData(customData[copiedDataIndex], 0, 0);
|
||||
}
|
||||
layers[i].updateLayerPreview();
|
||||
copiedDataIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
let maxX = -Infinity;
|
||||
let maxY = -Infinity;
|
||||
let tmp;
|
||||
let imageDatas = [];
|
||||
let historySave = saveHistory == null;
|
||||
let prevPivot = rcPivot;
|
||||
|
||||
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;
|
||||
|
||||
for (let i=imageData.data.length - 1; i>= 0; i-=4) {
|
||||
if (!isPixelEmpty(
|
||||
[imageData.data[i - 3], imageData.data[i - 2],
|
||||
-imageData.data[i - 1], imageData.data[i]])) {
|
||||
pixelPosition = getPixelPosition(i);
|
||||
|
||||
// max x
|
||||
if (pixelPosition[0] > maxX) {
|
||||
maxX = pixelPosition[0];
|
||||
}
|
||||
// min x
|
||||
if (pixelPosition[0] < minX) {
|
||||
minX = pixelPosition[0];
|
||||
}
|
||||
// max y
|
||||
if (pixelPosition[1] > maxY) {
|
||||
maxY = pixelPosition[1];
|
||||
}
|
||||
// min y
|
||||
if (pixelPosition[1] < minY) {
|
||||
minY = pixelPosition[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tmp = minY;
|
||||
minY = maxY;
|
||||
maxY = tmp;
|
||||
|
||||
minY = layers[0].canvasSize[1] - minY;
|
||||
maxY = layers[0].canvasSize[1] - maxY;
|
||||
|
||||
// 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++) {
|
||||
if (layers[i].menuEntry != null) {
|
||||
imageDatas.push(layers[i].context.getImageData(minX - 1, layers[i].canvasSize[1] - maxY, maxX-minX + 1, maxY-minY + 1));
|
||||
}
|
||||
}
|
||||
|
||||
console.log(imageDatas);
|
||||
//console.log("sx: " + borders.left + "dx: " + borders.right + "top: " + borders.top + "btm: " + borders.bottom);
|
||||
|
||||
document.getElementById("rc-border-left").value = 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;
|
||||
}
|
||||
|
||||
function rcUpdateBorders() {
|
||||
// Getting input
|
||||
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
|
||||
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) {
|
||||
rcPivot = event.target.getAttribute("value");
|
||||
|
||||
// Setting the selected class
|
||||
currentPivotObject.classList.remove("rc-selected-pivot");
|
||||
currentPivotObject = event.target;
|
||||
currentPivotObject.classList.add("rc-selected-pivot");
|
||||
}
|
||||
|
|
@ -1,296 +0,0 @@
|
|||
/* 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];
|
||||
|
||||
startData.width = parseInt(data.width);
|
||||
startData.height = parseInt(data.height);
|
||||
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];
|
||||
|
||||
document.getElementById("rs-width-percentage").value = 100;
|
||||
document.getElementById("rs-height-percentage").value = 100;
|
||||
|
||||
document.getElementById("rs-keep-ratio").checked = true;
|
||||
|
||||
document.getElementById("rs-width").addEventListener("change", changedWidth);
|
||||
document.getElementById("rs-height").addEventListener("change", changedHeight);
|
||||
document.getElementById("rs-width-percentage").addEventListener("change", changedWidthPercentage);
|
||||
document.getElementById("rs-height-percentage").addEventListener("change", changedHeightPercentage);
|
||||
|
||||
document.getElementById("resize-sprite-confirm").addEventListener("click", resizeSprite);
|
||||
document.getElementById("rs-keep-ratio").addEventListener("click", toggleRatio);
|
||||
document.getElementById("resize-algorithm-combobox").addEventListener("change", changedAlgorithm);
|
||||
}
|
||||
|
||||
/** 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];
|
||||
oldHeight = layers[1].canvasSize[1];
|
||||
rcPivot = "middle";
|
||||
|
||||
// Updating values if the user didn't press enter
|
||||
switch (document.activeElement.id) {
|
||||
case "rs-width-percentage":
|
||||
changedWidthPercentage();
|
||||
break;
|
||||
case "rs-width":
|
||||
changedWidth();
|
||||
break;
|
||||
case "rs-height-percentage":
|
||||
changedHeightPercentage();
|
||||
break;
|
||||
case "rs-height":
|
||||
changedHeight();
|
||||
break;
|
||||
default:
|
||||
// In this case everything has been updated correctly
|
||||
break;
|
||||
}
|
||||
|
||||
// Computing newWidth and newHeight
|
||||
if (ratio == null) {
|
||||
newWidth = data.width;
|
||||
newHeight = data.height;
|
||||
}
|
||||
else {
|
||||
newWidth = layers[0].canvasSize[0] * ratio[0];
|
||||
newHeight = layers[1].canvasSize[1] * ratio[1];
|
||||
}
|
||||
|
||||
// Get all the image datas
|
||||
for (let i=0; i<layers.length; i++) {
|
||||
if (layers[i].menuEntry != null) {
|
||||
rsImageDatas.push(layers[i].context.getImageData(
|
||||
0, 0, layers[0].canvasSize[0], layers[0].canvasSize[1])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Resizing the canvas
|
||||
resizeCanvas(null, {x: newWidth, y: newHeight});
|
||||
|
||||
// Put the image datas on the new canvases
|
||||
for (let i=0; i<layers.length; i++) {
|
||||
if (layers[i].menuEntry != null) {
|
||||
layers[i].context.putImageData(
|
||||
resizeImageData(rsImageDatas[layerIndex], newWidth, newHeight, currentAlgo), 0, 0
|
||||
);
|
||||
layers[i].updateLayerPreview();
|
||||
layerIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
// Updating start values when I finish scaling the sprite
|
||||
// OPTIMIZABLE? Can't I just assign data to startData? Is js smart enough to understand?
|
||||
if (ratio == null) {
|
||||
startData.width = data.width;
|
||||
startData.height = data.height;
|
||||
}
|
||||
else {
|
||||
startData.width = layers[0].canvasSize[0];
|
||||
startData.height = layers[0].canvasSize[1];
|
||||
}
|
||||
|
||||
startData.widthPercentage = 100;
|
||||
startData.heightPercentage = 100;
|
||||
|
||||
closeDialogue();
|
||||
}
|
||||
|
||||
/* 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;
|
||||
let percentageRatio;
|
||||
let newHeight, newHeightPerc, newWidthPerc;
|
||||
|
||||
data.width = event.target.value;
|
||||
delta = data.width - oldValue;
|
||||
|
||||
ratio = data.width / oldValue;
|
||||
|
||||
newHeight = data.width / currentRatio;
|
||||
newHeightPerc = (newHeight * 100) / startData.height;
|
||||
newWidthPerc = (data.width * 100) / startData.width;
|
||||
|
||||
if (keepRatio) {
|
||||
document.getElementById("rs-height").value = newHeight;
|
||||
data.height = newHeight;
|
||||
|
||||
document.getElementById("rs-height-percentage").value = newHeightPerc;
|
||||
data.heightPercentage = newHeightPerc;
|
||||
}
|
||||
|
||||
document.getElementById("rs-width-percentage").value = newWidthPerc;
|
||||
}
|
||||
|
||||
/**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;
|
||||
let newWidth, newWidthPerc, newHeightPerc;
|
||||
|
||||
data.height = event.target.value;
|
||||
delta = data.height - oldValue;
|
||||
|
||||
ratio = data.height / oldValue;
|
||||
|
||||
newWidth = data.height * currentRatio;
|
||||
newWidthPerc = (newWidth * 100) / startData.width;
|
||||
newHeightPerc = (data.height * 100) / startData.height;
|
||||
|
||||
if (keepRatio) {
|
||||
document.getElementById("rs-width").value = newWidth;
|
||||
data.width = newWidth;
|
||||
|
||||
document.getElementById("rs-width-percentage").value = newWidthPerc;
|
||||
data.widthPercentage = newWidthPerc;
|
||||
}
|
||||
|
||||
document.getElementById("rs-height-percentage").value = newHeightPerc;
|
||||
data.heightPercentage = newHeightPerc;
|
||||
}
|
||||
|
||||
/**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;
|
||||
let newWidth, newHeight, newHeightPerc;
|
||||
|
||||
data.widthPercentage = event.target.value;
|
||||
delta = data.widthPercentage - oldValue;
|
||||
|
||||
ratio = data.widthPercentage / oldValue;
|
||||
|
||||
console.log("old value: " + oldValue + ", ratio: " + ratio);
|
||||
|
||||
newHeight = startData.height * ratio;
|
||||
newHeightPerc = data.widthPercentage;
|
||||
newWidth = startData.width * ratio;
|
||||
|
||||
if (keepRatio) {
|
||||
document.getElementById("rs-height-percentage").value = newHeightPerc;
|
||||
data.heightPercentage = newHeightPerc;
|
||||
|
||||
document.getElementById("rs-height").value = newHeight
|
||||
data.height = newHeight;
|
||||
}
|
||||
|
||||
document.getElementById("rs-width").value = newWidth;
|
||||
data.width = newWidth;
|
||||
}
|
||||
|
||||
/**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;
|
||||
let newHeight, newWidth, newWidthPerc;
|
||||
|
||||
data.heightPercentage = event.target.value;
|
||||
delta = data.heightPercentage - oldValue;
|
||||
|
||||
ratio = data.heightPercentage / oldValue;
|
||||
|
||||
newWidth = startData.width * ratio;
|
||||
newWidthPerc = data.heightPercentage;
|
||||
newHeight = startData.height * ratio;
|
||||
|
||||
if (keepRatio) {
|
||||
document.getElementById("rs-width-percentage").value = data.heightPercentage * currentRatio;
|
||||
data.widthPercentage = newWidthPerc;
|
||||
|
||||
document.getElementById("rs-width").value = newWidth;
|
||||
data.width = newWidth;
|
||||
}
|
||||
|
||||
document.getElementById("rs-height").value = newHeight;
|
||||
data.height = newHeight;
|
||||
}
|
||||
|
||||
/** 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;
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
/**
|
||||
* Opens the save project window and initializes events for save project customization.
|
||||
*/
|
||||
function openSaveProjectWindow() {
|
||||
//create name
|
||||
var selectedPalette = getText('palette-button');
|
||||
if (selectedPalette != 'Choose a palette...'){
|
||||
var paletteAbbreviation = palettes[selectedPalette].abbreviation;
|
||||
var fileName = 'pixel-'+paletteAbbreviation+'-'+canvasSize[0]+'x'+canvasSize[1];
|
||||
} else {
|
||||
var fileName = 'pixel-'+canvasSize[0]+'x'+canvasSize[1];
|
||||
selectedPalette = 'none';
|
||||
}
|
||||
|
||||
setValue('lpe-file-name', fileName);
|
||||
|
||||
document.getElementById("save-project-confirm").addEventListener("click", saveProject);
|
||||
|
||||
showDialogue('save-project', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads the .lpe file for the current project.
|
||||
*/
|
||||
function saveProject() {
|
||||
var fileName = `${getValue('lpe-file-name')}.lpe`;
|
||||
|
||||
// create file content
|
||||
var content = getProjectData();
|
||||
|
||||
//set download link
|
||||
var linkHolder = document.getElementById('save-project-link-holder');
|
||||
|
||||
linkHolder.href = 'data:text/plain;charset=utf-8,' + encodeURIComponent(content);
|
||||
linkHolder.download = fileName;
|
||||
linkHolder.click();
|
||||
|
||||
ga('send', 'event', 'Pixel Editor Save', selectedPalette, canvasSize[0]+'/'+canvasSize[1]); /*global ga*/
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
var settings;
|
||||
|
||||
if (!Cookies.enabled) {
|
||||
document.getElementById('cookies-disabled-warning').style.display = 'block';
|
||||
}
|
||||
|
||||
//try to load settings from cookie
|
||||
var settingsFromCookie = Cookies.get('pixelEditorSettings');
|
||||
if(!settingsFromCookie) {
|
||||
console.log('settings cookie not found');
|
||||
settings = {
|
||||
switchToChangedColor: true,
|
||||
enableDynamicCursorOutline: true, //unused - performance
|
||||
enableBrushPreview: true, //unused - performance
|
||||
enableEyedropperPreview: true, //unused - performance
|
||||
numberOfHistoryStates: 20,
|
||||
maxColorsOnImportedImage: 128,
|
||||
pixelGridColour: '#000000'
|
||||
};
|
||||
}
|
||||
else{
|
||||
console.log('settings cookie found');
|
||||
console.log(settingsFromCookie);
|
||||
var settings = JSON.parse(settingsFromCookie);
|
||||
}
|
||||
console.log(settings);
|
||||
|
||||
//on clicking the save button in the settings dialog
|
||||
on('click', 'save-settings', saveSettings);
|
||||
|
||||
function saveSettings() {
|
||||
//check if values are valid
|
||||
if (isNaN(getValue('setting-numberOfHistoryStates'))) {
|
||||
alert('Invalid value for numberOfHistoryStates');
|
||||
return;
|
||||
}
|
||||
|
||||
//save new settings to settings object
|
||||
settings.numberOfHistoryStates = getValue('setting-numberOfHistoryStates');
|
||||
settings.pixelGridColour = getValue('setting-pixelGridColour');
|
||||
// Filling pixel grid again if colour changed
|
||||
fillPixelGrid();
|
||||
|
||||
//save settings object to cookie
|
||||
var cookieValue = JSON.stringify(settings);
|
||||
Cookies.set('pixelEditorSettings', cookieValue, { expires: Infinity });
|
||||
|
||||
//close window
|
||||
closeDialogue();
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
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("' + chosenImage.path + '.png")';
|
||||
authorLink.setAttribute('href', chosenImage.link);
|
||||
authorLink.innerHTML = 'Art by ' + chosenImage.author;
|
|
@ -1,127 +0,0 @@
|
|||
//pencil
|
||||
on('click',"pencil-button", function(){
|
||||
tool.pencil.switchTo();
|
||||
}, false);
|
||||
|
||||
//pencil bigger
|
||||
on('click',"pencil-bigger-button", function(){
|
||||
tool.pencil.brushSize++;
|
||||
}, false);
|
||||
|
||||
//pencil smaller
|
||||
on('click',"pencil-smaller-button", function(){
|
||||
if(tool.pencil.brushSize > 1)
|
||||
tool.pencil.brushSize--;
|
||||
}, false);
|
||||
|
||||
//eraser
|
||||
on('click',"eraser-button", function(){
|
||||
console.log("selecting eraser");
|
||||
tool.eraser.switchTo();
|
||||
}, false);
|
||||
|
||||
//eraser bigger
|
||||
on('click',"eraser-bigger-button", function(){
|
||||
tool.eraser.brushSize++;
|
||||
}, false);
|
||||
|
||||
//eraser smaller
|
||||
on('click',"eraser-smaller-button", function(e){
|
||||
if(tool.eraser.brushSize > 1)
|
||||
tool.eraser.brushSize--;
|
||||
}, false);
|
||||
|
||||
// rectangle
|
||||
on('click','rectangle-button', function(e){
|
||||
// If the user clicks twice on the button, they change the draw mode
|
||||
if (currentTool.name == 'rectangle') {
|
||||
if (rectangleDrawMode == 'empty') {
|
||||
rectangleDrawMode = 'fill';
|
||||
setRectToolSvg();
|
||||
}
|
||||
else {
|
||||
rectangleDrawMode = 'empty';
|
||||
setRectToolSvg();
|
||||
}
|
||||
}
|
||||
else {
|
||||
tool.rectangle.switchTo();
|
||||
}
|
||||
}, 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++;
|
||||
}, false);
|
||||
|
||||
// rectangle smaller
|
||||
on('click',"rectangle-smaller-button", function(e){
|
||||
if(tool.rectangle.brushSize > 1)
|
||||
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();
|
||||
}, false);
|
||||
|
||||
//pan
|
||||
on('click',"pan-button", function(){
|
||||
tool.pan.switchTo();
|
||||
}, false);
|
||||
|
||||
//eyedropper
|
||||
on('click',"eyedropper-button", function(){
|
||||
tool.eyedropper.switchTo();
|
||||
}, 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 */
|
109
js/_tools.js
109
js/_tools.js
|
@ -1,109 +0,0 @@
|
|||
//tools container / list, automatically managed when you create a new Tool();
|
||||
var tool = {};
|
||||
|
||||
//class for tools
|
||||
class Tool {
|
||||
constructor (name, options) {
|
||||
|
||||
//stores the name in object, only needed for legacy functions from when currentTool was just a string
|
||||
this.name = name;
|
||||
|
||||
//copy options to this object
|
||||
if (options.cursor) {
|
||||
//passed statically as a string
|
||||
if (typeof options.cursor == 'string') this.cursor = options.cursor;
|
||||
//passed a function which should be used as a getter function
|
||||
if (typeof options.cursor == 'function') Object.defineProperty(this, 'cursor', { get: options.cursor});
|
||||
}
|
||||
|
||||
if (options.imageCursor) this.cursor = "url(\'/pixel-editor/"+options.imageCursor+".png\'), auto";
|
||||
|
||||
this.currentBrushSize = 1;
|
||||
this.previousBrushSize = 1;
|
||||
if (options.brushPreview) {
|
||||
this.brushPreview = true;
|
||||
}
|
||||
|
||||
//add to tool object so it can be referenced
|
||||
tool[name] = this;
|
||||
}
|
||||
|
||||
get brushSize () {
|
||||
return this.currentBrushSize;
|
||||
}
|
||||
|
||||
set brushSize (value) {
|
||||
this.currentBrushSize = value;
|
||||
this.updateCursor();
|
||||
}
|
||||
|
||||
|
||||
//switch to this tool (replaced global changeTool())
|
||||
switchTo () {
|
||||
// Ending any selection in progress
|
||||
if (currentTool.name.includes("select") && !this.name.includes("select") && !selectionCanceled) {
|
||||
endSelection();
|
||||
}
|
||||
|
||||
//set tool and temp tje tje tpp <--- he's speaking the language of the gods, don't delete
|
||||
currentTool = this;
|
||||
currentToolTemp = this;
|
||||
|
||||
var tools = document.getElementById("tools-menu").children;
|
||||
|
||||
for (var i = 0; i < tools.length; i++) {
|
||||
tools[i].classList.remove("selected");
|
||||
}
|
||||
|
||||
let buttonNode = document.getElementById(this.name + "-button");
|
||||
//give the button of the selected tool the .selected class if the tool has a button
|
||||
if(buttonNode != null && buttonNode.parentNode != null) {
|
||||
document.getElementById(this.name+"-button").parentNode.classList.add("selected");
|
||||
}
|
||||
|
||||
//change cursor
|
||||
this.updateCursor();
|
||||
}
|
||||
|
||||
updateCursor () {
|
||||
//switch to that tools cursor
|
||||
canvasView.style.cursor = this.cursor || 'default';
|
||||
|
||||
//if the tool uses a brush preview, make it visible and update the size
|
||||
if (this.brushPreview) {
|
||||
//console.log('brush size',this.currentBrushSize)
|
||||
brushPreview.style.display = 'block';
|
||||
brushPreview.style.width = this.currentBrushSize * zoom + 'px';
|
||||
brushPreview.style.height = this.currentBrushSize * zoom + 'px';
|
||||
}
|
||||
|
||||
//show / hide eyedropper color preview
|
||||
if (this.eyedropperPreview) eyedropperPreview.style.display = 'block';
|
||||
else eyedropperPreview.style.display = 'none';
|
||||
|
||||
//moveSelection
|
||||
if (currentTool.name == 'moveselection') {
|
||||
if (cursorInSelectedArea()) {
|
||||
canMoveSelection = true;
|
||||
canvasView.style.cursor = 'move';
|
||||
brushPreview.style.display = 'none';
|
||||
}
|
||||
else {
|
||||
canvasView.style.cursor = 'crosshair';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
moveBrushPreview(cursorLocation) {
|
||||
let toSub = 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';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*global dragging currentTool, currentToolTemp, selectionCanceled, endSelection*/
|
|
@ -1,45 +0,0 @@
|
|||
//init variables
|
||||
var canvasSize;
|
||||
var zoom = 7;
|
||||
var dragging = false;
|
||||
var lastMouseClickPos = [0,0];
|
||||
var dialogueOpen = true;
|
||||
var documentCreated = false;
|
||||
var pixelEditorMode = "Basic";
|
||||
|
||||
//common elements
|
||||
var brushPreview = document.getElementById("brush-preview");
|
||||
var eyedropperPreview = document.getElementById("eyedropper-preview");
|
||||
var canvasView = document.getElementById("canvas-view");
|
||||
var colors = document.getElementsByClassName("color-button");
|
||||
var colorsMenu = document.getElementById("colors-menu");
|
||||
var popUpContainer = document.getElementById("pop-up-container");
|
||||
|
||||
// main canvas
|
||||
var canvas = document.getElementById('pixel-canvas');
|
||||
var currentGlobalColor;
|
||||
|
||||
// Layers
|
||||
var layers = [];
|
||||
// Currently selected layer
|
||||
var currentLayer;
|
||||
|
||||
// VFX layer used to draw previews of the selection and things like that
|
||||
var VFXLayer;
|
||||
// VFX canvas
|
||||
var VFXCanvas = document.getElementById('vfx-canvas');
|
||||
|
||||
// TMP layer
|
||||
var TMPLayer;
|
||||
// TMP canvas
|
||||
var TMPCanvas = document.getElementById('tmp-canvas');
|
||||
|
||||
// Pixel grid layer
|
||||
var pixelGrid;
|
||||
// Pixel grid canvas
|
||||
var pixelGridCanvas;
|
||||
|
||||
// Index of the first layer the user can use in the layers array
|
||||
var firstUserLayerIndex = 2;
|
||||
// Number of layers that are only used by the editor
|
||||
var nAppLayers = 3;
|
|
@ -1,15 +0,0 @@
|
|||
function closeCompatibilityWarning() {
|
||||
document.getElementById("compatibility-warning").style.visibility = "hidden";
|
||||
}
|
||||
|
||||
//check browser/version
|
||||
if (
|
||||
(bowser.firefox && bowser.version >= 28) ||
|
||||
(bowser.chrome && bowser.version >= 29) ||
|
||||
(!bowser.mobile && !bowser.tablet)
|
||||
)
|
||||
console.log("compatibility check passed");
|
||||
|
||||
|
||||
//show warning
|
||||
else document.getElementById("compatibility-warning").style.visibility = "visible";
|
|
@ -0,0 +1,9 @@
|
|||
const MIN_Z_INDEX = -5000;
|
||||
const MAX_Z_INDEX = 5000;
|
||||
|
||||
// Index of the first layer the user can use in the layers array
|
||||
const firstUserLayerIndex = 2;
|
||||
// Number of layers that are only used by the editor
|
||||
const nAppLayers = 3;
|
||||
|
||||
const MIN_ZOOM_LEVEL = 0.5;
|
|
@ -66,8 +66,6 @@ palettes["Gameboy Color"] = {"name":"Nintendo Gameboy (Black Zero)","author":"",
|
|||
const loadPaletteButtonSplash = document.getElementById('load-palette-button-splash');
|
||||
|
||||
splashPalettes.refresh = function () {
|
||||
splashPalettes.innerHTML = '';
|
||||
palettesMenu.innerHTML = '';
|
||||
|
||||
Object.keys(palettes).forEach((paletteName,) => {
|
||||
|
||||
|
@ -110,7 +108,6 @@ palettes["Gameboy Color"] = {"name":"Nintendo Gameboy (Black Zero)","author":"",
|
|||
|
||||
splashPalettes.refresh();
|
||||
|
||||
|
||||
const loadPaletteButtonEvent = () => {
|
||||
document.getElementById('load-palette-browse-holder').click();
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
class Checkerboard extends Layer {
|
||||
// Checkerboard color 1
|
||||
firstCheckerBoardColor = 'rgba(179, 173, 182, 1)';
|
||||
// Checkerboard color 2
|
||||
secondCheckerBoardColor = 'rgba(204, 200, 206, 1)';
|
||||
// Square size for the checkerboard
|
||||
checkerBoardSquareSize = 16;
|
||||
|
||||
// Setting current colour (each square has a different colour
|
||||
currentColor = undefined;
|
||||
// Saving number of squares filled until now
|
||||
nSquaresFilled = 0;
|
||||
|
||||
constructor(width, height, canvas, menuEntry) {
|
||||
super(width, height, document.getElementById('checkerboard'), menuEntry);
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
initialize() {
|
||||
super.initialize();
|
||||
console.log("Square size: " + this.checkerBoardSquareSize);
|
||||
this.currentColor = this.firstCheckerBoardColor;
|
||||
this.fillCheckerboard();
|
||||
}
|
||||
|
||||
/** Fills the checkerboard canvas with squares with alternating colours
|
||||
*
|
||||
*/
|
||||
fillCheckerboard() {
|
||||
this.context.clearRect(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
|
||||
|
||||
// Cycling through the canvas (using it as a matrix)
|
||||
for (let i=0; i<currFile.canvasSize[0] / this.checkerBoardSquareSize; i++) {
|
||||
this.nSquaresFilled = 0;
|
||||
|
||||
for (let j=0; j<currFile.canvasSize[1] / this.checkerBoardSquareSize; j++) {
|
||||
let rectX;
|
||||
let rectY;
|
||||
|
||||
// Managing the not perfect squares (the ones at the sides if the canvas' sizes are not powers of checkerBoardSquareSize
|
||||
if (i * this.checkerBoardSquareSize < currFile.canvasSize[0]) {
|
||||
rectX = i * this.checkerBoardSquareSize;
|
||||
}
|
||||
else {
|
||||
rectX = currFile.canvasSize[0];
|
||||
}
|
||||
|
||||
if (j * this.checkerBoardSquareSize < currFile.canvasSize[1]) {
|
||||
rectY = j * this.checkerBoardSquareSize;
|
||||
}
|
||||
else {
|
||||
rectY = currFile.canvasSize[1];
|
||||
}
|
||||
|
||||
// Selecting the colour
|
||||
this.context.fillStyle = this.currentColor;
|
||||
this.context.fillRect(rectX, rectY, this.checkerBoardSquareSize, this.checkerBoardSquareSize);
|
||||
|
||||
// Changing colour
|
||||
this.changeCheckerboardColor();
|
||||
this.nSquaresFilled++;
|
||||
}
|
||||
|
||||
// If the number of filled squares was even, I change colour for the next column
|
||||
if ((this.nSquaresFilled % 2) == 0) {
|
||||
this.changeCheckerboardColor();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Simply switches the checkerboard colour
|
||||
changeCheckerboardColor() {
|
||||
if (this.currentColor == this.firstCheckerBoardColor) {
|
||||
this.currentColor = this.secondCheckerBoardColor;
|
||||
} else if (this.currentColor == this.secondCheckerBoardColor) {
|
||||
this.currentColor = this.firstCheckerBoardColor;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,319 @@
|
|||
// TODO: add undo for layer drag n drop
|
||||
|
||||
/** Handler class for a single canvas (a single layer)
|
||||
*
|
||||
* @param width Canvas width
|
||||
* @param height Canvas height
|
||||
* @param canvas HTML canvas element or the ID of the canvas related to the layer
|
||||
*/
|
||||
class Layer {
|
||||
static layerCount = 1;
|
||||
static maxZIndex = 3;
|
||||
|
||||
static unusedIDs = [];
|
||||
static currentID = 1;
|
||||
|
||||
static layerOptions = document.getElementById("layer-properties-menu");
|
||||
|
||||
// TODO: this is simply terrible. menuEntry can either be an HTML element, a string or undefined.
|
||||
// If it's an HTML element it is added, if it's a string nothing happens, if it's undefined the
|
||||
// first menuEntry is used (the one that appears on load)
|
||||
constructor(width, height, canvas, menuEntry) {
|
||||
// REFACTOR: the canvas should actually be a Canvas instance
|
||||
this.canvas = Util.getElement(canvas);
|
||||
this.canvas.width = width;
|
||||
this.canvas.height = height;
|
||||
// REFACTOR: the context could be an attribute of the canvas class, but it's a bit easier
|
||||
// to just type layer.context, we should discuss this
|
||||
this.context = this.canvas.getContext('2d');
|
||||
this.isSelected = false;
|
||||
this.isVisible = true;
|
||||
this.isLocked = false;
|
||||
|
||||
this.oldLayerName = null;
|
||||
|
||||
if (typeof menuEntry == "string")
|
||||
this.menuEntry = document.getElementById("layers-menu").firstElementChild;
|
||||
else if (menuEntry !== undefined)
|
||||
this.menuEntry = menuEntry;
|
||||
|
||||
let id = Layer.unusedIDs.pop();
|
||||
|
||||
if (id == null) {
|
||||
id = Layer.currentID;
|
||||
Layer.currentID++;
|
||||
}
|
||||
|
||||
this.id = "layer" + id;
|
||||
|
||||
// Binding the events
|
||||
if (this.menuEntry !== undefined) {
|
||||
this.name = this.menuEntry.getElementsByTagName("p")[0].innerHTML;
|
||||
this.menuEntry.id = "layer" + id;
|
||||
|
||||
this.menuEntry.onmouseover = () => this.hover();
|
||||
this.menuEntry.onmouseout = () => this.unhover();
|
||||
this.menuEntry.onclick = () => this.selectLayer();
|
||||
this.menuEntry.getElementsByTagName("button")[0].onclick = () => this.toggleLock();
|
||||
this.menuEntry.getElementsByTagName("button")[1].onclick = () => this.toggleVisibility();
|
||||
|
||||
this.menuEntry.addEventListener("mouseup", this.openOptionsMenu, false);
|
||||
this.menuEntry.addEventListener("dragstart", this.layerDragStart, false);
|
||||
this.menuEntry.addEventListener("drop", this.layerDragDrop, false);
|
||||
this.menuEntry.addEventListener("dragover", this.layerDragOver, false);
|
||||
this.menuEntry.addEventListener("dragleave", this.layerDragLeave, false);
|
||||
this.menuEntry.addEventListener("dragend", this.layerDragEnd, false);
|
||||
|
||||
Events.onCustom("del", this.tryDelete.bind(this));
|
||||
|
||||
this.menuEntry.getElementsByTagName("canvas")[0].getContext('2d').imageSmoothingEnabled = false;
|
||||
}
|
||||
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
hasCanvas() {
|
||||
return this.menuEntry != null;
|
||||
}
|
||||
|
||||
tryDelete() {
|
||||
if (Input.getLastTarget() != this.menuEntry && Input.getLastTarget().parentElement != this.menuEntry)
|
||||
return;
|
||||
LayerList.deleteLayer();
|
||||
}
|
||||
|
||||
// Initializes the canvas
|
||||
initialize() {
|
||||
//resize canvas
|
||||
this.canvas.style.width = (this.canvas.width*currFile.zoom)+'px';
|
||||
this.canvas.style.height = (this.canvas.height*currFile.zoom)+'px';
|
||||
|
||||
//show canvas
|
||||
this.canvas.style.display = 'block';
|
||||
|
||||
//center canvas in window
|
||||
this.canvas.style.left = 64+currFile.canvasView.clientWidth/2-(currFile.canvasSize[0]*currFile.zoom/2)+'px';
|
||||
this.canvas.style.top = 48+currFile.canvasView.clientHeight/2-(currFile.canvasSize[1]*currFile.zoom/2)+'px';
|
||||
|
||||
this.context.imageSmoothingEnabled = false;
|
||||
this.context.mozImageSmoothingEnabled = false;
|
||||
}
|
||||
|
||||
rename() {
|
||||
this.menuEntry.getElementsByTagName("p")[0].setAttribute("contenteditable", false);
|
||||
|
||||
if (this.oldLayerName != null) {
|
||||
let name = this.menuEntry.getElementsByTagName("p")[0].innerHTML;
|
||||
|
||||
for (let i=0; i<currFile.layers.length; i++) {
|
||||
if (name === currFile.layers[i].name) {
|
||||
name += ' (1)';
|
||||
i = 0;
|
||||
}
|
||||
}
|
||||
this.name = name;
|
||||
this.menuEntry.getElementsByTagName("p")[0].innerHTML = name;
|
||||
|
||||
new HistoryState().RenameLayer(this.oldLayerName, name, currFile.currentLayer);
|
||||
this.oldLayerName = null;
|
||||
}
|
||||
}
|
||||
|
||||
hover() {
|
||||
// Hides all the layers but the current one
|
||||
for (let i=1; i<currFile.layers.length - nAppLayers; i++) {
|
||||
if (currFile.layers[i] !== this) {
|
||||
currFile.layers[i].canvas.style.opacity = 0.3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unhover() {
|
||||
// Shows all the layers again
|
||||
for (let i=1; i<currFile.layers.length - nAppLayers; i++) {
|
||||
if (currFile.layers[i] !== this) {
|
||||
currFile.layers[i].canvas.style.opacity = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setID(id) {
|
||||
this.id = id;
|
||||
if (this.menuEntry != null) {
|
||||
this.menuEntry.id = id;
|
||||
}
|
||||
}
|
||||
|
||||
// Resizes canvas
|
||||
resize() {
|
||||
let newWidth = (this.canvas.width * currFile.zoom) + 'px';
|
||||
let newHeight = (this.canvas.height *currFile.zoom)+ 'px';
|
||||
|
||||
this.canvas.style.width = newWidth;
|
||||
this.canvas.style.height = newHeight;
|
||||
}
|
||||
|
||||
setCanvasOffset (offsetLeft, offsetTop) {
|
||||
//horizontal offset
|
||||
var minXOffset = -currFile.canvasSize[0] * currFile.zoom;
|
||||
var maxXOffset = window.innerWidth - 300;
|
||||
|
||||
if (offsetLeft < minXOffset)
|
||||
this.canvas.style.left = minXOffset +'px';
|
||||
else if (offsetLeft > maxXOffset)
|
||||
this.canvas.style.left = maxXOffset +'px';
|
||||
else
|
||||
this.canvas.style.left = offsetLeft +'px';
|
||||
|
||||
//vertical offset
|
||||
var minYOffset = -currFile.canvasSize[1] * currFile.zoom + 164;
|
||||
var maxYOffset = window.innerHeight - 100;
|
||||
|
||||
if (offsetTop < minYOffset)
|
||||
this.canvas.style.top = minYOffset +'px';
|
||||
else if (offsetTop > maxYOffset)
|
||||
this.canvas.style.top = maxYOffset +'px';
|
||||
else
|
||||
this.canvas.style.top = offsetTop +'px';
|
||||
}
|
||||
|
||||
// Copies the otherLayer's position and size
|
||||
copyData(otherLayer) {
|
||||
this.canvas.style.width = otherLayer.canvas.style.width;
|
||||
this.canvas.style.height = otherLayer.canvas.style.height;
|
||||
|
||||
this.canvas.style.left = otherLayer.canvas.style.left;
|
||||
this.canvas.style.top = otherLayer.canvas.style.top;
|
||||
}
|
||||
|
||||
selectLayer(hideOptions = true) {
|
||||
if (hideOptions)
|
||||
LayerList.closeOptionsMenu();
|
||||
// Deselecting the old layer
|
||||
currFile.currentLayer.deselectLayer();
|
||||
|
||||
// Selecting the current layer
|
||||
this.isSelected = true;
|
||||
this.menuEntry.classList.add("selected-layer");
|
||||
currFile.currentLayer = this;
|
||||
}
|
||||
|
||||
toggleLock() {
|
||||
if (this.isLocked) {
|
||||
this.unlock();
|
||||
}
|
||||
else {
|
||||
this.lock();
|
||||
}
|
||||
}
|
||||
|
||||
toggleVisibility() {
|
||||
if (this.isVisible) {
|
||||
this.hide();
|
||||
}
|
||||
else {
|
||||
this.show();
|
||||
}
|
||||
}
|
||||
|
||||
deselectLayer() {
|
||||
this.isSelected = false;
|
||||
this.menuEntry.classList.remove("selected-layer");
|
||||
}
|
||||
|
||||
lock() {
|
||||
this.isLocked = true;
|
||||
this.menuEntry.getElementsByClassName("layer-button")[0].style.visibility = "visible";
|
||||
|
||||
this.menuEntry.getElementsByClassName("default-icon")[0].style.display = "none";
|
||||
this.menuEntry.getElementsByClassName("edited-icon")[0].style.display = "inline-block";
|
||||
}
|
||||
|
||||
unlock() {
|
||||
this.isLocked = false;
|
||||
this.menuEntry.getElementsByClassName("layer-button")[0].style.visibility = "hidden";
|
||||
|
||||
this.menuEntry.getElementsByClassName("default-icon")[0].style.display = "inline-block";
|
||||
this.menuEntry.getElementsByClassName("edited-icon")[0].style.display = "none";
|
||||
}
|
||||
|
||||
show() {
|
||||
this.isVisible = true;
|
||||
this.canvas.style.visibility = "visible";
|
||||
this.menuEntry.getElementsByClassName("layer-button")[1].style.visibility = "hidden";
|
||||
|
||||
// Changing icon
|
||||
this.menuEntry.getElementsByClassName("default-icon")[1].style.display = "inline-block";
|
||||
this.menuEntry.getElementsByClassName("edited-icon")[1].style.display = "none";
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.isVisible = false;
|
||||
this.canvas.style.visibility = "hidden";
|
||||
this.menuEntry.getElementsByClassName("layer-button")[1].style.visibility = "visible";
|
||||
|
||||
// Changing icon
|
||||
this.menuEntry.getElementsByClassName("default-icon")[1].style.display = "none";
|
||||
this.menuEntry.getElementsByClassName("edited-icon")[1].style.display = "inline-block";
|
||||
}
|
||||
|
||||
updateLayerPreview() {
|
||||
// Getting the canvas
|
||||
let destination = this.menuEntry.getElementsByTagName("canvas")[0];
|
||||
let widthRatio = currFile.canvasSize[0] / currFile.canvasSize[1];
|
||||
let heightRatio = currFile.canvasSize[1] / currFile.canvasSize[0];
|
||||
|
||||
// Computing width and height for the preview image
|
||||
let previewWidth = destination.width;
|
||||
let previewHeight = destination.height;
|
||||
|
||||
// If the sprite is rectangular, I apply the ratio to the preview as well
|
||||
if (widthRatio < 1) {
|
||||
previewWidth = destination.width * widthRatio;
|
||||
}
|
||||
else if (widthRatio > 1) {
|
||||
previewHeight = destination.height * heightRatio;
|
||||
}
|
||||
|
||||
// La appiccico sulla preview
|
||||
destination.getContext('2d').clearRect(0, 0, destination.width, destination.height);
|
||||
destination.getContext('2d').drawImage(this.canvas,
|
||||
// This is necessary to center the preview in the canvas
|
||||
(destination.width - previewWidth) / 2, (destination.height - previewHeight) / 2,
|
||||
previewWidth, previewHeight);
|
||||
}
|
||||
|
||||
drawLine(x0,y0,x1,y1, brushSize) {
|
||||
var dx = Math.abs(x1-x0);
|
||||
var dy = Math.abs(y1-y0);
|
||||
var sx = (x0 < x1 ? 1 : -1);
|
||||
var sy = (y0 < y1 ? 1 : -1);
|
||||
var err = dx-dy;
|
||||
|
||||
while (true) {
|
||||
//set pixel
|
||||
// If the current tool is the brush
|
||||
if (ToolManager.currentTool().name == 'brush' || ToolManager.currentTool().name == 'rectangle' || ToolManager.currentTool().name == 'ellipse') {
|
||||
// I fill the rect
|
||||
currFile.currentLayer.context.fillRect(x0-Math.floor(brushSize/2), y0-Math.floor(brushSize/2), brushSize, brushSize);
|
||||
} else if (ToolManager.currentTool().name == 'eraser') {
|
||||
// In case I'm using the eraser I must clear the rect
|
||||
currFile.currentLayer.context.clearRect(x0-Math.floor(brushSize/2), y0-Math.floor(brushSize/2), brushSize, brushSize);
|
||||
}
|
||||
|
||||
//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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
// OPTIMIZABLE: render the grid only for the current viewport
|
||||
class PixelGrid extends Layer {
|
||||
// Start colour of the pixel grid (can be changed in the preferences)
|
||||
pixelGridColor = "#000000";
|
||||
// Distance between one line and another in HTML pixels
|
||||
lineDistance = 10;
|
||||
// The grid is not visible by default
|
||||
pixelGridVisible = false;
|
||||
// The grid is enabled, but is disabled in order to save performance with big sprites
|
||||
pixelGridEnabled = true;
|
||||
|
||||
constructor(width, height, canvas, menuEntry) {
|
||||
super(width, height, canvas, menuEntry);
|
||||
this.initialize();
|
||||
Events.onCustom("refreshPixelGrid", this.fillPixelGrid.bind(this));
|
||||
Events.onCustom("switchedToAdvanced", this.togglePixelGrid.bind(this));
|
||||
Events.onCustom("switchedToBasic", this.togglePixelGrid.bind(this));
|
||||
}
|
||||
|
||||
initialize() {
|
||||
super.initialize();
|
||||
this.fillPixelGrid();
|
||||
}
|
||||
|
||||
disablePixelGrid() {
|
||||
this.pixelGridEnabled = false;
|
||||
this.canvas.style.display = "none";
|
||||
}
|
||||
|
||||
enablePixelGrid() {
|
||||
if (!this.pixelGridVisible)
|
||||
return;
|
||||
this.pixelGridEnabled = true;
|
||||
this.canvas.style.display = "inline-block";
|
||||
}
|
||||
|
||||
hidePixelGrid() {
|
||||
let button = document.getElementById("toggle-pixelgrid-button");
|
||||
this.pixelGridVisible = false;
|
||||
button.innerHTML = "Show pixel grid";
|
||||
this.canvas.style.display = "none";
|
||||
}
|
||||
showPixelGrid() {
|
||||
let button = document.getElementById("toggle-pixelgrid-button");
|
||||
this.pixelGridVisible = true;
|
||||
button.innerHTML = "Hide pixel grid";
|
||||
this.canvas.style.display = "inline-block";
|
||||
}
|
||||
|
||||
repaintPixelGrid(factor) {
|
||||
this.lineDistance += factor;
|
||||
this.fillPixelGrid();
|
||||
}
|
||||
|
||||
/** Shows or hides the pixel grid depening on its current visibility
|
||||
* (triggered by the show pixel grid button in the top menu)
|
||||
*
|
||||
*/
|
||||
togglePixelGrid(newState) {
|
||||
//Set the state based on the passed newState variable, otherwise just toggle it
|
||||
if (newState == 'on')
|
||||
this.showPixelGrid();
|
||||
else if (newState == 'off')
|
||||
this.hidePixelGrid();
|
||||
else
|
||||
if (this.pixelGridVisible)
|
||||
this.hidePixelGrid();
|
||||
else
|
||||
this.showPixelGrid();
|
||||
}
|
||||
|
||||
/** Fills the pixelGridCanvas with the pixelgrid
|
||||
*
|
||||
*/
|
||||
fillPixelGrid() {
|
||||
let originalSize = currFile.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
|
||||
this.canvas.width = originalSize[0] * Math.round(this.lineDistance);
|
||||
this.canvas.height = originalSize[1] * Math.round(this.lineDistance);
|
||||
|
||||
this.context.strokeStyle = Settings.getCurrSettings().pixelGridColour;
|
||||
|
||||
console.log("Line ditance: " + this.lineDistance)
|
||||
|
||||
// OPTIMIZABLE, could probably be a bit more elegant
|
||||
// Draw horizontal lines
|
||||
for (let i=0; i<this.canvas.width / Math.round(this.lineDistance); i++) {
|
||||
this.context.beginPath();
|
||||
this.context.moveTo(i * Math.round(this.lineDistance) + 0.5, 0);
|
||||
this.context.lineTo(i * Math.round(this.lineDistance) + 0.5,
|
||||
originalSize[1] * Math.round(this.lineDistance));
|
||||
this.context.stroke();
|
||||
this.context.closePath();
|
||||
}
|
||||
|
||||
// Draw vertical lines
|
||||
for (let i=0; i<this.canvas.height / Math.round(this.lineDistance); i++) {
|
||||
this.context.beginPath();
|
||||
this.context.moveTo(0, i * Math.round(this.lineDistance) + 0.5);
|
||||
this.context.lineTo(originalSize[0] * Math.round(this.lineDistance),
|
||||
i * Math.round(this.lineDistance) + 0.5);
|
||||
this.context.stroke();
|
||||
this.context.closePath();
|
||||
}
|
||||
|
||||
if (!this.pixelGridVisible) {
|
||||
this.canvas.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -550,10 +550,9 @@ if (!window.jscolor) { window.jscolor = (function () {
|
|||
//console.log(e.target,'=====================================')
|
||||
//if they clicked on the delete button [lospec]
|
||||
if (e.target.className == 'delete-color-button') {
|
||||
//saveHistoryState({type: 'deletecolor', colorValue: jsc.picker.owner.toString(), canvas: canvas.context.getImageData(0, 0, canvasSize[0], canvasSize[1])});
|
||||
new HistoryStateDeleteColor(jsc.picker.owner.toString());
|
||||
new HistoryState().DeleteColor(jsc.picker.owner.toString());
|
||||
|
||||
deleteColor(jsc.picker.owner.styleElement);
|
||||
ColorModule.deleteColor(jsc.picker.owner.styleElement);
|
||||
}
|
||||
else if (e.target.className == 'jscolor-picker-bottom') {
|
||||
//console.log('clicked color picker bottom')
|
||||
|
@ -1071,18 +1070,12 @@ if (!window.jscolor) { window.jscolor = (function () {
|
|||
this.hide = function () {
|
||||
///console.log(this.styleElement)
|
||||
if (isPickerOwner()) {
|
||||
|
||||
//console.log('color picker hidden')
|
||||
|
||||
//set the color to old color, in case the color is a duplicate that hasn't been resolved yet [lospec]
|
||||
var hexInput = document.getElementById('jscolor-hex-input');
|
||||
var oldColor = '#'+rgbToHex(hexInput.oldColor);
|
||||
var oldColor = '#'+Color.rgbToHex(hexInput.oldColor);
|
||||
this.fromString(oldColor);
|
||||
document.getElementById('duplicate-color-warning').style.visibility = 'hidden';
|
||||
|
||||
//dialog is closed
|
||||
dialogueOpen = false;
|
||||
|
||||
detachPicker();
|
||||
}
|
||||
};
|
||||
|
@ -1091,9 +1084,6 @@ if (!window.jscolor) { window.jscolor = (function () {
|
|||
this.show = function () {
|
||||
drawPicker();
|
||||
|
||||
//a dialog is open
|
||||
dialogueOpen = true;
|
||||
|
||||
//[lospec]
|
||||
//find the hex input element
|
||||
var hexInput = document.getElementById('jscolor-hex-input');
|
||||
|
@ -1105,21 +1095,21 @@ if (!window.jscolor) { window.jscolor = (function () {
|
|||
this.exportColor();
|
||||
|
||||
//set old color for updating colors on canvas
|
||||
hexInput.oldColor = hexToRgb(hexInput.value);
|
||||
hexInput.oldColor = Color.hexToRgb(hexInput.value);
|
||||
|
||||
//set the color element to the clicked button
|
||||
hexInput.colorElement = this.styleElement;
|
||||
|
||||
//disable delete button if last color
|
||||
var colors = document.getElementsByClassName('color-button');
|
||||
var deleteButton = document.getElementsByClassName('delete-color-button')[0];
|
||||
let colors = document.getElementsByClassName('color-button');
|
||||
let deleteButton = document.getElementsByClassName('delete-color-button')[0];
|
||||
if(colors.length == 1)
|
||||
deleteButton.classList.add('disabled');
|
||||
else
|
||||
deleteButton.classList.remove('disabled');
|
||||
|
||||
//hide duplicate color warning
|
||||
var duplicateColorWarning = document.getElementById('duplicate-color-warning');
|
||||
let duplicateColorWarning = document.getElementById('duplicate-color-warning');
|
||||
duplicateColorWarning.style.visibility = 'hidden';
|
||||
};
|
||||
|
|
@ -1,89 +1,140 @@
|
|||
/**utilities**/
|
||||
//=include util/on.js
|
||||
//=include util/onChildren.js
|
||||
//=include util/onClick.js
|
||||
//=include util/onClickChildren.js
|
||||
//=include util/select.js
|
||||
//=include util/getSetText.js
|
||||
//=include util/getSetValue.js
|
||||
//=include util/hexToRgb.js
|
||||
//=include util/rgbToHex.js
|
||||
//=include util/rgbToHsl.js
|
||||
//=include util/hslToRgb.js
|
||||
/** EXTERNALS AND LIBRARIES **/
|
||||
//=include lib/cookies.js
|
||||
//=include _pixelEditorUtility.js
|
||||
//=include lib/jscolor.js
|
||||
//=include lib/sortable.js
|
||||
//=include _algorithms.js
|
||||
|
||||
//=include data/consts.js
|
||||
//=include data/palettes.js
|
||||
|
||||
/** UTILITY AND INPUT **/
|
||||
//=include Util.js
|
||||
//=include Events.js
|
||||
//=include Dialogue.js
|
||||
//=include History.js
|
||||
//=include Settings.js
|
||||
//=include EditorState.js
|
||||
|
||||
/**init**/
|
||||
//=include _consts.js
|
||||
//=include _variables.js
|
||||
//=include _settings.js
|
||||
/** COLOR-RELATED **/
|
||||
//=include Color.js
|
||||
//=include ColorPicker.js
|
||||
//=include PaletteBlock.js
|
||||
|
||||
/**dropdown formatting**/
|
||||
//=include _editorMode.js
|
||||
//=include _presets.js
|
||||
//=include _palettes.js
|
||||
/** BASE CLASSES **/
|
||||
//=include File.js
|
||||
//=include Tool.js
|
||||
//=include layers/Layer.js
|
||||
|
||||
/**functions**/
|
||||
//=include _tools.js
|
||||
//=include tools/*.js
|
||||
//=include _newPixel.js
|
||||
//=include _createColorPalette.js
|
||||
//=include _changeZoom.js
|
||||
//=include _addColor.js
|
||||
//=include _colorChanged.js
|
||||
//=include _initColor.js
|
||||
//=include _dialogue.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
|
||||
//=include _checkerboard.js
|
||||
//=include _pixelGrid.js
|
||||
//=include _layer.js
|
||||
//=include _copyPaste.js
|
||||
//=include _resizeCanvas.js
|
||||
//=include _resizeSprite.js
|
||||
//=include _colorPicker.js
|
||||
//=include _paletteBlock.js
|
||||
//=include _splashPage.js
|
||||
//=include _pixelExport.js
|
||||
//=include _saveProject.js
|
||||
/** SPECIAL LAYERS **/
|
||||
//=include layers/Checkerboard.js
|
||||
//=include layers/PixelGrid.js
|
||||
|
||||
/**load file**/
|
||||
//=include _loadImage.js
|
||||
//=include _loadPalette.js
|
||||
/** TOOLS **/
|
||||
//=include tools/DrawingTool.js
|
||||
//=include tools/ResizableTool.js
|
||||
//=include tools/SelectionTool.js
|
||||
|
||||
/**event listeners**/
|
||||
//=include _hotkeyListener.js
|
||||
//=include _mouseEvents.js
|
||||
//=include tools/BrushTool.js
|
||||
//=include tools/EraserTool.js
|
||||
//=include tools/LineTool.js
|
||||
//=include tools/RectangleTool.js
|
||||
//=include tools/FillTool.js
|
||||
//=include tools/EyedropperTool.js
|
||||
//=include tools/PanTool.js
|
||||
//=include tools/ZoomTool.js
|
||||
//=include tools/RectangularSelectionTool.js
|
||||
//=include tools/MoveSelectionTool.js
|
||||
|
||||
/**buttons**/
|
||||
//=include _toolButtons.js
|
||||
//=include _addColorButton.js
|
||||
//=include _clickedColor.js
|
||||
//=include _fileMenu.js
|
||||
//=include _createButton.js
|
||||
//=include _rectSelect.js
|
||||
//=include _move.js
|
||||
//=include _rectangle.js
|
||||
//=include _ellipse.js
|
||||
/** MODULES AND MENUS **/
|
||||
//=include SplashPage.js
|
||||
//=include PresetModule.js
|
||||
//=include ColorModule.js
|
||||
//=include ToolManager.js
|
||||
//=include LayerList.js
|
||||
|
||||
/**onload**/
|
||||
//=include _onLoad.js
|
||||
//=include _onbeforeunload.js
|
||||
/** STARTUP AND FILE MANAGEMENT **/
|
||||
//=include Startup.js
|
||||
//=include FileManager.js
|
||||
//=include TopMenuModule.js
|
||||
|
||||
/**libraries**/
|
||||
//=include _jscolor.js
|
||||
/** HTML INPUT EVENTS **/
|
||||
//=include Input.js
|
||||
|
||||
/**feature toggles**/
|
||||
//=include _featureToggles.js
|
||||
/** IHER **/
|
||||
//=include FeatureToggles.js
|
||||
|
||||
// Controls execution of this preset module
|
||||
PresetModule.instrumentPresetMenu();
|
||||
PresetModule.instrumentPresetMenu();
|
||||
|
||||
//when the page is done loading, you can get ready to start
|
||||
window.onload = function () {
|
||||
featureToggles.onLoad();
|
||||
|
||||
ToolManager.currentTool().updateCursor();
|
||||
|
||||
//check if there are any url parameters
|
||||
if (window.location.pathname.replace('/pixel-editor/','').length <= 1) {
|
||||
//show splash screen
|
||||
Dialogue.showDialogue('splash', false);
|
||||
}
|
||||
//url parameters were specified
|
||||
else {
|
||||
let args = window.location.pathname.split('/');
|
||||
let paletteSlug = args[2];
|
||||
let dimentions = args[3];
|
||||
|
||||
//fetch palette via lospec palette API
|
||||
fetch('https://lospec.com/palette-list/'+paletteSlug+'.json')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
//palette loaded successfully
|
||||
palettes[paletteSlug] = data;
|
||||
palettes[paletteSlug].specified = true;
|
||||
|
||||
//refresh list of palettes
|
||||
document.getElementById('palette-menu-splash').refresh();
|
||||
|
||||
//if the dimentions were specified
|
||||
if (dimentions && dimentions.length >= 3 && dimentions.includes('x')) {
|
||||
let width = dimentions.split('x')[0];
|
||||
let height = dimentions.split('x')[1];
|
||||
|
||||
//create new document
|
||||
Startup.newPixel(width, height);
|
||||
}
|
||||
//dimentions were not specified -- show splash screen with palette preselected
|
||||
else {
|
||||
//show splash
|
||||
Dialogue.showDialogue('new-pixel', false);
|
||||
}
|
||||
})
|
||||
//error fetching url (either palette doesn't exist, or lospec is down)
|
||||
.catch((error) => {
|
||||
console.warn('failed to load palette "'+paletteSlug+'"', error);
|
||||
//proceed to splash screen
|
||||
Dialogue.showDialogue('splash', false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//prevent user from leaving page with unsaved data
|
||||
window.onbeforeunload = function() {
|
||||
if (EditorState.documentCreated)
|
||||
return 'You will lose your pixel if it\'s not saved!';
|
||||
|
||||
else return;
|
||||
};
|
||||
|
||||
// Compatibility functions
|
||||
function closeCompatibilityWarning() {
|
||||
document.getElementById("compatibility-warning").style.visibility = "hidden";
|
||||
}
|
||||
|
||||
//check browser/version
|
||||
if (
|
||||
(bowser.firefox && bowser.version >= 28) ||
|
||||
(bowser.chrome && bowser.version >= 29) ||
|
||||
(!bowser.mobile && !bowser.tablet)
|
||||
)
|
||||
console.log("compatibility check passed");
|
||||
//show warning
|
||||
else document.getElementById("compatibility-warning").style.visibility = "visible";
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
class BrushTool extends ResizableTool {
|
||||
constructor(name, options, switchFunction) {
|
||||
super(name, options);
|
||||
|
||||
Events.on('click', this.mainButton, switchFunction, this);
|
||||
Events.on('click', this.biggerButton, this.increaseSize.bind(this));
|
||||
Events.on('click', this.smallerButton, this.decreaseSize.bind(this));
|
||||
}
|
||||
|
||||
onStart(mousePos) {
|
||||
super.onStart(mousePos);
|
||||
new HistoryState().EditCanvas();
|
||||
}
|
||||
|
||||
onDrag(mousePos, cursorTarget) {
|
||||
super.onDrag(mousePos);
|
||||
|
||||
if (cursorTarget === undefined)
|
||||
return;
|
||||
//draw line to current pixel
|
||||
if (cursorTarget.className == 'drawingCanvas' || cursorTarget.className == 'drawingCanvas') {
|
||||
currFile.currentLayer.drawLine(Math.floor(this.prevMousePos[0]/currFile.zoom),
|
||||
Math.floor(this.prevMousePos[1]/currFile.zoom),
|
||||
Math.floor(this.currMousePos[0]/currFile.zoom),
|
||||
Math.floor(this.currMousePos[1]/currFile.zoom),
|
||||
this.currSize
|
||||
);
|
||||
}
|
||||
|
||||
currFile.currentLayer.updateLayerPreview();
|
||||
}
|
||||
|
||||
onEnd(mousePos) {
|
||||
super.onEnd(mousePos);
|
||||
}
|
||||
|
||||
onSelect() {
|
||||
super.onSelect();
|
||||
}
|
||||
|
||||
onDeselect() {
|
||||
super.onDeselect();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
class DrawingTool extends Tool {
|
||||
constructor (name, options) {
|
||||
super(name, options);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
class EraserTool extends ResizableTool {
|
||||
constructor(name, options, switchFunction) {
|
||||
super(name, options);
|
||||
|
||||
Events.on('click', this.mainButton, switchFunction, this);
|
||||
Events.on('click', this.biggerButton, this.increaseSize.bind(this));
|
||||
Events.on('click', this.smallerButton, this.decreaseSize.bind(this));
|
||||
}
|
||||
|
||||
onStart(mousePos) {
|
||||
super.onStart(mousePos);
|
||||
new HistoryState().EditCanvas();
|
||||
}
|
||||
|
||||
onDrag(mousePos, cursorTarget) {
|
||||
super.onDrag(mousePos);
|
||||
|
||||
if (cursorTarget === undefined)
|
||||
return;
|
||||
//draw line to current pixel
|
||||
if (cursorTarget.className == 'drawingCanvas' || cursorTarget.className == 'drawingCanvas') {
|
||||
currFile.currentLayer.drawLine(Math.floor(this.prevMousePos[0]/currFile.zoom),
|
||||
Math.floor(this.prevMousePos[1]/currFile.zoom),
|
||||
Math.floor(this.currMousePos[0]/currFile.zoom),
|
||||
Math.floor(this.currMousePos[1]/currFile.zoom),
|
||||
this.currSize
|
||||
);
|
||||
}
|
||||
|
||||
currFile.currentLayer.updateLayerPreview();
|
||||
}
|
||||
|
||||
onEnd(mousePos) {
|
||||
super.onEnd(mousePos);
|
||||
this.endMousePos = mousePos;
|
||||
}
|
||||
|
||||
onSelect() {
|
||||
super.onSelect();
|
||||
}
|
||||
|
||||
onDeselect() {
|
||||
super.onDeselect();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
class EyedropperTool extends Tool {
|
||||
eyedropperPreview = document.getElementById("eyedropper-preview");
|
||||
selectedColor = {r:0, g:0, b:0};
|
||||
|
||||
constructor(name, options, switchFunction) {
|
||||
super(name, options);
|
||||
|
||||
Events.on('click', this.mainButton, switchFunction, this);
|
||||
}
|
||||
|
||||
onStart(mousePos, target) {
|
||||
super.onStart(mousePos);
|
||||
|
||||
if (target.className != 'drawingCanvas')
|
||||
return;
|
||||
|
||||
this.eyedropperPreview.style.display = 'block';
|
||||
this.onDrag(mousePos);
|
||||
}
|
||||
|
||||
onDrag(mousePos) {
|
||||
super.onDrag(mousePos);
|
||||
|
||||
this.selectedColor = this.pickColor(mousePos);
|
||||
|
||||
this.eyedropperPreview.style.borderColor = '#' + Color.rgbToHex(this.selectedColor);
|
||||
this.eyedropperPreview.style.left = mousePos[0] + currFile.currentLayer.canvas.offsetLeft - 30 + 'px';
|
||||
this.eyedropperPreview.style.top = mousePos[1] + currFile.currentLayer.canvas.offsetTop - 30 + 'px';
|
||||
|
||||
const colorLightness = Math.max(this.selectedColor.r,this.selectedColor.g,this.selectedColor.b);
|
||||
|
||||
//for the darkest 50% of colors, change the eyedropper preview to dark mode
|
||||
if (colorLightness>127) this.eyedropperPreview.classList.remove('dark');
|
||||
else this.eyedropperPreview.classList.add('dark');
|
||||
|
||||
this.changeColor();
|
||||
}
|
||||
|
||||
onEnd(mousePos) {
|
||||
super.onEnd(mousePos);
|
||||
this.eyedropperPreview.style.display = 'none';
|
||||
}
|
||||
|
||||
changeColor() {
|
||||
let colorHex = Color.rgbToHex(this.selectedColor);
|
||||
ColorModule.updateCurrentColor('#' + Color.rgbToHex(this.selectedColor));
|
||||
|
||||
let colors = document.getElementsByClassName('color-button');
|
||||
for (let i = 0; i < colors.length; i++) {
|
||||
//if picked color matches this color
|
||||
if (colorHex == colors[i].jscolor.toString()) {
|
||||
//remove current color selection
|
||||
document.querySelector("#colors-menu li.selected")?.classList.remove("selected");
|
||||
|
||||
//set current color
|
||||
for (let i=2; i<currFile.layers.length; i++) {
|
||||
currFile.layers[i].context.fillStyle = '#' + colorHex;
|
||||
}
|
||||
|
||||
//make color selected
|
||||
colors[i].parentElement.classList.add('selected');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 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
|
||||
*/
|
||||
pickColor(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<currFile.layers.length-3; i++) {
|
||||
// Getting the colour of the pixel in the cursorLocation
|
||||
tmpColour = currFile.layers[i].context.getImageData(Math.floor(cursorLocation[0]/currFile.zoom),Math.floor(cursorLocation[1]/currFile.zoom),1,1).data;
|
||||
|
||||
// If it's not empty, I check if it's on the top of the previous colour
|
||||
if (currFile.layers[i].canvas.style.zIndex > max || Util.isPixelEmpty(selectedColor) || selectedColor === undefined) {
|
||||
max = currFile.layers[i].canvas.style.zIndex;
|
||||
|
||||
if (!Util.isPixelEmpty(tmpColour)) {
|
||||
selectedColor = tmpColour;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the final colour was empty, I return black
|
||||
if (Util.isPixelEmpty(tmpColour) && selectedColor === undefined) {
|
||||
selectedColor = [0, 0, 0];
|
||||
}
|
||||
|
||||
return {r:selectedColor[0], g:selectedColor[1], b:selectedColor[2]};
|
||||
}
|
||||
|
||||
|
||||
onSelect() {
|
||||
super.onSelect();
|
||||
}
|
||||
|
||||
onDeselect() {
|
||||
super.onDeselect();
|
||||
this.eyedropperPreview.style.display = 'none';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
class FillTool extends DrawingTool {
|
||||
constructor(name, options, switchFunction) {
|
||||
super(name, options);
|
||||
|
||||
Events.on('click', this.mainButton, switchFunction, this);
|
||||
}
|
||||
|
||||
onStart(mousePos, target) {
|
||||
super.onStart(mousePos);
|
||||
|
||||
if (target.className != 'drawingCanvas')
|
||||
return;
|
||||
this.fill(mousePos);
|
||||
currFile.currentLayer.updateLayerPreview();
|
||||
|
||||
new HistoryState().EditCanvas();
|
||||
}
|
||||
|
||||
|
||||
fill(cursorLocation) {
|
||||
//changes a pixels color
|
||||
function colorPixel(tempImage, pixelPos, fillColor) {
|
||||
//console.log('colorPixel:',pixelPos);
|
||||
tempImage.data[pixelPos] = fillColor.r;
|
||||
tempImage.data[pixelPos + 1] = fillColor.g;
|
||||
tempImage.data[pixelPos + 2] = fillColor.b;
|
||||
tempImage.data[pixelPos + 3] = 255;
|
||||
}
|
||||
|
||||
//change x y to color value passed from the function and use that as the original color
|
||||
function matchStartColor(tempImage, pixelPos, color) {
|
||||
//console.log('matchPixel:',x,y)
|
||||
|
||||
let r = tempImage.data[pixelPos];
|
||||
let g = tempImage.data[pixelPos + 1];
|
||||
let b = tempImage.data[pixelPos + 2];
|
||||
let 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] && a == color[3]);
|
||||
}
|
||||
|
||||
//temporary image holds the data while we change it
|
||||
let tempImage = currFile.currentLayer.context.getImageData(0, 0, currFile.canvasSize[0], currFile.canvasSize[1]);
|
||||
|
||||
//this is an array that holds all of the pixels at the top of the cluster
|
||||
let topmostPixelsArray = [[Math.floor(cursorLocation[0]/currFile.zoom), Math.floor(cursorLocation[1]/currFile.zoom)]];
|
||||
//console.log('topmostPixelsArray:',topmostPixelsArray)
|
||||
|
||||
//the offset of the pixel in the temp image data to start with
|
||||
let startingPosition = (topmostPixelsArray[0][1] * currFile.canvasSize[0] + topmostPixelsArray[0][0]) * 4;
|
||||
|
||||
//the color of the cluster that is being filled
|
||||
let clusterColor = [tempImage.data[startingPosition],tempImage.data[startingPosition+1],tempImage.data[startingPosition+2], tempImage.data[startingPosition+3]];
|
||||
|
||||
//the color to fill with
|
||||
let fillColor = Color.hexToRgb(currFile.currentLayer.context.fillStyle);
|
||||
|
||||
//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 &&
|
||||
clusterColor[3] != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
//loop until there are no more values left in this array
|
||||
while (topmostPixelsArray.length) {
|
||||
let reachLeft, reachRight;
|
||||
|
||||
//move the most recent pixel from the array and set it as our current working pixels
|
||||
let currentPixel = topmostPixelsArray.pop();
|
||||
|
||||
//set the values of this pixel to x/y variables just for readability
|
||||
let x = currentPixel[0];
|
||||
let y = currentPixel[1];
|
||||
|
||||
//this variable holds the index of where the starting values for the current pixel are in the data array
|
||||
//we multiply the number of rows down (y) times the width of each row, then add x. at the end we multiply by 4 because
|
||||
//each pixel has 4 values, rgba
|
||||
let pixelPos = (y * currFile.canvasSize[0] + x) * 4;
|
||||
|
||||
//move up in the image until you reach the top or the pixel you hit was not the right color
|
||||
while (y-- >= 0 && matchStartColor(tempImage, pixelPos, clusterColor)) {
|
||||
pixelPos -= currFile.canvasSize[0] * 4;
|
||||
}
|
||||
pixelPos += currFile.canvasSize[0] * 4;
|
||||
++y;
|
||||
reachLeft = false;
|
||||
reachRight = false;
|
||||
|
||||
while (y++ < currFile.canvasSize[1] - 1 && matchStartColor(tempImage, pixelPos, clusterColor)) {
|
||||
colorPixel(tempImage, pixelPos, fillColor);
|
||||
if (x > 0) {
|
||||
if (matchStartColor(tempImage, pixelPos - 4, clusterColor)) {
|
||||
if (!reachLeft) {
|
||||
topmostPixelsArray.push([x - 1, y]);
|
||||
reachLeft = true;
|
||||
}
|
||||
}
|
||||
else if (reachLeft) {
|
||||
reachLeft = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (x < currFile.canvasSize[0] - 1) {
|
||||
if (matchStartColor(tempImage, pixelPos + 4, clusterColor)) {
|
||||
if (!reachRight) {
|
||||
topmostPixelsArray.push([x + 1, y]);
|
||||
reachRight = true;
|
||||
}
|
||||
}
|
||||
else if (reachRight) {
|
||||
reachRight = false;
|
||||
}
|
||||
}
|
||||
|
||||
pixelPos += currFile.canvasSize[0] * 4;
|
||||
}
|
||||
}
|
||||
currFile.currentLayer.context.putImageData(tempImage, 0, 0);
|
||||
}
|
||||
|
||||
onDrag(mousePos, cursorTarget) {
|
||||
}
|
||||
|
||||
onEnd(mousePos) {
|
||||
super.onEnd(mousePos);
|
||||
}
|
||||
|
||||
onSelect() {
|
||||
super.onSelect();
|
||||
}
|
||||
|
||||
onDeselect() {
|
||||
super.onDeselect();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
class LineTool extends ResizableTool {
|
||||
constructor(name, options, switchFunction) {
|
||||
super(name, options);
|
||||
|
||||
Events.on('click', this.mainButton, switchFunction, this);
|
||||
Events.on('click', this.biggerButton, this.increaseSize.bind(this));
|
||||
Events.on('click', this.smallerButton, this.decreaseSize.bind(this));
|
||||
}
|
||||
|
||||
onStart(mousePos) {
|
||||
super.onStart(mousePos);
|
||||
|
||||
// Putting the tmp layer on top of everything
|
||||
currFile.TMPLayer.canvas.style.zIndex = parseInt(currFile.currentLayer.canvas.style.zIndex, 10) + 1;
|
||||
|
||||
this.startMousePos[0] = Math.floor(mousePos[0]) + 0.5;
|
||||
this.startMousePos[1] = Math.floor(mousePos[1]) + 0.5;
|
||||
|
||||
new HistoryState().EditCanvas();
|
||||
}
|
||||
|
||||
onDrag(mousePos, cursorTarget) {
|
||||
super.onDrag(mousePos);
|
||||
|
||||
// Drawing the line at the right position
|
||||
this.drawLine(mousePos);
|
||||
}
|
||||
|
||||
/** Finishes drawing the rect, decides the end coordinates and moves the preview rectangle to the
|
||||
* current layer
|
||||
*
|
||||
* @param {*} mousePos The position of the mouse when the user stopped dragging
|
||||
*/
|
||||
onEnd(mousePos) {
|
||||
super.onEnd(mousePos);
|
||||
|
||||
const tmpContext = currFile.TMPLayer.context;
|
||||
const tmpCanvas = currFile.TMPLayer.canvas;
|
||||
// Setting the correct linewidth and colour
|
||||
currFile.currentLayer.context.lineWidth = this.currSize;
|
||||
|
||||
// Drawing the line
|
||||
currFile.currentLayer.context.drawImage(tmpCanvas, 0, 0);
|
||||
|
||||
// Update the layer preview
|
||||
currFile.currentLayer.updateLayerPreview();
|
||||
// Clearing the tmp canvas
|
||||
tmpContext.clearRect(0, 0, currFile.TMPLayer.canvas.width, currFile.TMPLayer.canvas.height);
|
||||
}
|
||||
|
||||
onSelect() {
|
||||
super.onSelect();
|
||||
}
|
||||
|
||||
onDeselect() {
|
||||
super.onDeselect();
|
||||
}
|
||||
|
||||
drawLine(mousePos) {
|
||||
let x0 = Math.floor(this.startMousePos[0]/currFile.zoom);
|
||||
let y0 = Math.floor(this.startMousePos[1]/currFile.zoom);
|
||||
let x1 = Math.floor(mousePos[0]/currFile.zoom);
|
||||
let y1 = Math.floor(mousePos[1]/currFile.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 canvas = document.getElementById('tmp-canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
canvas.style.zIndex = parseInt(currFile.currentLayer.canvas.style.zIndex, 10) + 1;
|
||||
|
||||
while (true) {
|
||||
context.fillRect(x0-Math.floor(this.currSize/2), y0-Math.floor(this.currSize/2), this.currSize, this.currSize);
|
||||
|
||||
//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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,174 @@
|
|||
class MoveSelectionTool extends DrawingTool {
|
||||
currSelection = undefined;
|
||||
selectionTool = undefined;
|
||||
endTool = undefined;
|
||||
switchFunc = undefined;
|
||||
lastCopiedSelection = undefined;
|
||||
cutting = false;
|
||||
|
||||
constructor (name, options, switchFunc, endTool) {
|
||||
super(name, options, switchFunc);
|
||||
|
||||
this.switchFunc = switchFunc;
|
||||
this.endTool = endTool;
|
||||
|
||||
Events.onCustom("esc-pressed", this.endSelection.bind(this));
|
||||
|
||||
Events.onCustom("ctrl+c", this.copySelection.bind(this));
|
||||
Events.onCustom("ctrl+x", this.cutSelection.bind(this));
|
||||
Events.onCustom("ctrl+v", this.pasteSelection.bind(this));
|
||||
}
|
||||
|
||||
copySelection() {
|
||||
this.lastCopiedSelection = this.currSelection;
|
||||
this.cutting = false;
|
||||
}
|
||||
|
||||
cutSelection() {
|
||||
if (currFile.currentLayer.isLocked)
|
||||
return;
|
||||
this.cutting = true;
|
||||
this.lastCopiedSelection = this.currSelection;
|
||||
this.endSelection();
|
||||
this.currSelection = this.lastCopiedSelection;
|
||||
// Cut the data
|
||||
currFile.currentLayer.context.clearRect(this.currSelection.left-0.5, this.currSelection.top-0.5,
|
||||
this.currSelection.width, this.currSelection.height);
|
||||
}
|
||||
|
||||
pasteSelection() {
|
||||
if (currFile.currentLayer.isLocked)
|
||||
return;
|
||||
if (this.lastCopiedSelection === undefined)
|
||||
return;
|
||||
// Finish the current selection and start a new one with the same data
|
||||
if (!this.cutting) {
|
||||
this.endSelection();
|
||||
}
|
||||
this.cutting = false;
|
||||
|
||||
this.switchFunc(this);
|
||||
this.currSelection = this.lastCopiedSelection;
|
||||
|
||||
// Putting the vfx layer on top of everything
|
||||
currFile.VFXLayer.canvas.style.zIndex = MAX_Z_INDEX;
|
||||
this.onDrag(this.currMousePos);
|
||||
|
||||
new HistoryState().EditCanvas();
|
||||
}
|
||||
|
||||
onStart(mousePos, mouseTarget) {
|
||||
super.onStart(mousePos, mouseTarget);
|
||||
|
||||
if (!this.cursorInSelectedArea(mousePos) &&
|
||||
!Util.isChildOfByClass(mouseTarget, "editor-top-menu")) {
|
||||
this.endSelection();
|
||||
}
|
||||
}
|
||||
|
||||
onDrag(mousePos) {
|
||||
super.onDrag(mousePos);
|
||||
|
||||
this.currSelection = this.selectionTool.moveAnts(mousePos[0]/currFile.zoom,
|
||||
mousePos[1]/currFile.zoom, this.currSelection.width, this.currSelection.height);
|
||||
|
||||
// clear the entire tmp layer
|
||||
currFile.TMPLayer.context.clearRect(0, 0, currFile.TMPLayer.canvas.width, currFile.TMPLayer.canvas.height);
|
||||
// put the image data on the tmp layer with offset
|
||||
currFile.TMPLayer.context.putImageData(
|
||||
this.currSelection.data,
|
||||
Math.round(mousePos[0] / currFile.zoom) - this.currSelection.width / 2,
|
||||
Math.round(mousePos[1] / currFile.zoom) - this.currSelection.height / 2);
|
||||
}
|
||||
|
||||
onEnd(mousePos) {
|
||||
super.onEnd(mousePos);
|
||||
}
|
||||
|
||||
onSelect() {
|
||||
super.onSelect();
|
||||
}
|
||||
|
||||
onDeselect() {
|
||||
super.onDeselect();
|
||||
}
|
||||
|
||||
setSelectionData(data, tool) {
|
||||
this.currSelection = data;
|
||||
this.selectionTool = tool;
|
||||
this.firstTimeMove = true;
|
||||
}
|
||||
|
||||
onHover(mousePos) {
|
||||
super.onHover(mousePos);
|
||||
|
||||
if (this.cursorInSelectedArea(mousePos)) {
|
||||
currFile.canvasView.style.cursor = 'move';
|
||||
}
|
||||
else {
|
||||
currFile.canvasView.style.cursor = 'default';
|
||||
}
|
||||
}
|
||||
|
||||
cursorInSelectedArea(cursorPos) {
|
||||
// Getting the coordinates relatively to the canvas
|
||||
let x = cursorPos[0] / currFile.zoom;
|
||||
let y = cursorPos[1] / currFile.zoom;
|
||||
|
||||
if (this.currSelection.left <= x && x <= this.currSelection.right) {
|
||||
if (y <= this.currSelection.bottom && y >= this.currSelection.top) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
endSelection() {
|
||||
if (this.currSelection == undefined)
|
||||
return;
|
||||
// Clearing the tmp (move preview) and vfx (ants) layers
|
||||
currFile.TMPLayer.context.clearRect(0, 0, currFile.TMPLayer.canvas.width, currFile.TMPLayer.canvas.height);
|
||||
currFile.VFXLayer.context.clearRect(0, 0, currFile.VFXLayer.canvas.width, currFile.VFXLayer.canvas.height);
|
||||
|
||||
// I have to save the underlying data, so that the transparent pixels in the clipboard
|
||||
// don't override the coloured pixels in the canvas
|
||||
let underlyingImageData = currFile.currentLayer.context.getImageData(
|
||||
this.currSelection.left, this.currSelection.top,
|
||||
this.currSelection.width+1, this.currSelection.height+1
|
||||
);
|
||||
let pasteData = this.currSelection.data.data.slice();
|
||||
|
||||
for (let i=0; i<underlyingImageData.data.length; i+=4) {
|
||||
let currentMovePixel = [
|
||||
pasteData[i], pasteData[i+1], pasteData[i+2], pasteData[i+3]
|
||||
];
|
||||
|
||||
let currentUnderlyingPixel = [
|
||||
underlyingImageData.data[i], underlyingImageData.data[i+1],
|
||||
underlyingImageData.data[i+2], underlyingImageData.data[i+3]
|
||||
];
|
||||
|
||||
// If the pixel of the clipboard is empty, but the one below it isn't, I use the pixel below
|
||||
if (Util.isPixelEmpty(currentMovePixel)) {
|
||||
if (!Util.isPixelEmpty(currentUnderlyingPixel)) {
|
||||
pasteData[i] = currentUnderlyingPixel[0];
|
||||
pasteData[i+1] = currentUnderlyingPixel[1];
|
||||
pasteData[i+2] = currentUnderlyingPixel[2];
|
||||
pasteData[i+3] = currentUnderlyingPixel[3];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currFile.currentLayer.context.putImageData(new ImageData(pasteData, this.currSelection.width+1),
|
||||
this.currSelection.left, this.currSelection.top
|
||||
);
|
||||
|
||||
this.currSelection = undefined;
|
||||
currFile.currentLayer.updateLayerPreview();
|
||||
currFile.VFXLayer.canvas.style.zIndex = MIN_Z_INDEX;
|
||||
|
||||
// Switch to brush
|
||||
this.switchFunc(this.endTool);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
class PanTool extends Tool {
|
||||
|
||||
constructor(name, options, switchFunction) {
|
||||
super(name, options);
|
||||
|
||||
Events.on('click', this.mainButton, switchFunction, this);
|
||||
}
|
||||
|
||||
onStart(mousePos, target) {
|
||||
super.onStart(mousePos);
|
||||
if (target.className != 'drawingCanvas')
|
||||
return;
|
||||
currFile.canvasView.style.cursor = "url(\'/pixel-editor/pan-held.png\'), auto";
|
||||
}
|
||||
|
||||
onDrag(mousePos, target) {
|
||||
super.onDrag(mousePos);
|
||||
if (target.className != 'drawingCanvas')
|
||||
return;
|
||||
|
||||
// Setting first layer position
|
||||
currFile.layers[0].setCanvasOffset(currFile.layers[0].canvas.offsetLeft + (mousePos[0] - this.startMousePos[0]), currFile.layers[0].canvas.offsetTop + (mousePos[1] - this.startMousePos[1]));
|
||||
// Copying that position to the other layers
|
||||
for (let i=1; i<currFile.layers.length; i++) {
|
||||
currFile.layers[i].copyData(currFile.layers[0]);
|
||||
}
|
||||
}
|
||||
|
||||
onEnd(mousePos, target) {
|
||||
super.onEnd(mousePos);
|
||||
if (target.className != 'drawingCanvas')
|
||||
return;
|
||||
|
||||
currFile.canvasView.style.cursor = "url(\'/pixel-editor/pan.png\'), auto";
|
||||
}
|
||||
|
||||
onSelect() {
|
||||
super.onSelect();
|
||||
currFile.canvasView.style.cursor = "url(\'/pixel-editor/pan.png\'), auto";
|
||||
}
|
||||
|
||||
onDeselect() {
|
||||
super.onDeselect();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,145 @@
|
|||
// TODO: FIX SELECTION
|
||||
|
||||
class RectangleTool extends ResizableTool {
|
||||
// Saving the empty rect svg
|
||||
emptyRectangleSVG = document.getElementById("rectangle-empty-button-svg");
|
||||
// and the full rect svg so that I can change them when the user changes rect modes
|
||||
fullRectangleSVG = document.getElementById("rectangle-full-button-svg");
|
||||
// Current fill mode
|
||||
currFillMode = 'empty';
|
||||
|
||||
switchFunction = null;
|
||||
|
||||
constructor(name, options, switchFunction) {
|
||||
super(name, options);
|
||||
|
||||
this.switchFunction = switchFunction;
|
||||
Events.on('click', this.mainButton, this.changeFillType.bind(this));
|
||||
Events.on('click', this.biggerButton, this.increaseSize.bind(this));
|
||||
Events.on('click', this.smallerButton, this.decreaseSize.bind(this));
|
||||
}
|
||||
|
||||
changeFillType() {
|
||||
if (this.isSelected)
|
||||
if (this.currFillMode == 'empty') {
|
||||
this.currFillMode = 'fill';
|
||||
this.emptyRectangleSVG.setAttribute('display', 'none');
|
||||
this.fullRectangleSVG.setAttribute('display', 'visible');
|
||||
}
|
||||
else {
|
||||
this.currFillMode = 'empty'
|
||||
this.emptyRectangleSVG.setAttribute('display', 'visible');
|
||||
this.fullRectangleSVG.setAttribute('display', 'none');
|
||||
}
|
||||
else
|
||||
this.switchFunction(this);
|
||||
}
|
||||
|
||||
onStart(mousePos) {
|
||||
super.onStart(mousePos);
|
||||
|
||||
// Putting the tmp layer on top of everything
|
||||
currFile.TMPLayer.canvas.style.zIndex = parseInt(currFile.currentLayer.canvas.style.zIndex, 10) + 1;
|
||||
|
||||
this.startMousePos[0] = Math.floor(mousePos[0] / currFile.zoom) + 0.5;
|
||||
this.startMousePos[1] = Math.floor(mousePos[1] / currFile.zoom) + 0.5;
|
||||
|
||||
new HistoryState().EditCanvas();
|
||||
}
|
||||
|
||||
onDrag(mousePos, cursorTarget) {
|
||||
|
||||
// Drawing the rect at the right position
|
||||
this.drawRect(Math.floor(mousePos[0] / currFile.zoom) + 0.5, Math.floor(mousePos[1] / currFile.zoom) + 0.5);
|
||||
}
|
||||
|
||||
/** Finishes drawing the rect, decides the end coordinates and moves the preview rectangle to the
|
||||
* current layer
|
||||
*
|
||||
* @param {*} mousePos The position of the mouse when the user stopped dragging
|
||||
*/
|
||||
onEnd(mousePos) {
|
||||
super.onEnd(mousePos);
|
||||
let tmpContext = currFile.TMPLayer.context;
|
||||
|
||||
let endRectX = Math.floor(mousePos[0] / currFile.zoom) + 0.5;
|
||||
let endRectY = Math.floor(mousePos[1] / currFile.zoom) + 0.5;
|
||||
let startRectX = this.startMousePos[0];
|
||||
let startRectY = this.startMousePos[1];
|
||||
|
||||
// Inverting end and start (start must always be the top left corner)
|
||||
if (endRectX < startRectX) {
|
||||
let tmp = endRectX;
|
||||
endRectX = startRectX;
|
||||
startRectX = tmp;
|
||||
}
|
||||
// Same for the y
|
||||
if (endRectY < startRectY) {
|
||||
let tmp = endRectY;
|
||||
endRectY = startRectY;
|
||||
startRectY = tmp;
|
||||
}
|
||||
|
||||
// Drawing the rect
|
||||
startRectY -= 0.5;
|
||||
endRectY -= 0.5;
|
||||
endRectX -= 0.5;
|
||||
startRectX -= 0.5;
|
||||
|
||||
// Setting the correct linewidth and colour
|
||||
currFile.currentLayer.context.lineWidth = this.currSize;
|
||||
|
||||
// Drawing the rect using 4 lines
|
||||
currFile.currentLayer.drawLine(startRectX, startRectY, endRectX, startRectY, this.currSize);
|
||||
currFile.currentLayer.drawLine(endRectX, startRectY, endRectX, endRectY, this.currSize);
|
||||
currFile.currentLayer.drawLine(endRectX, endRectY, startRectX, endRectY, this.currSize);
|
||||
currFile.currentLayer.drawLine(startRectX, endRectY, startRectX, startRectY, this.currSize);
|
||||
|
||||
// If I have to fill it, I do so
|
||||
if (this.currFillMode == 'fill') {
|
||||
currFile.currentLayer.context.fillRect(startRectX, startRectY, endRectX - startRectX, endRectY - startRectY);
|
||||
}
|
||||
|
||||
// Update the layer preview
|
||||
currFile.currentLayer.updateLayerPreview();
|
||||
// Clearing the tmp canvas
|
||||
tmpContext.clearRect(0, 0, currFile.TMPLayer.canvas.width, currFile.TMPLayer.canvas.height);
|
||||
}
|
||||
|
||||
onSelect() {
|
||||
super.onSelect();
|
||||
}
|
||||
|
||||
onDeselect() {
|
||||
super.onDeselect();
|
||||
}
|
||||
|
||||
/** Draws a rectangle with end coordinates given by x and y on the tmp 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
|
||||
*/
|
||||
drawRect(x, y) {
|
||||
// Getting the tmp context
|
||||
let tmpContext = currFile.TMPLayer.context;
|
||||
|
||||
// Clearing the tmp canvas
|
||||
tmpContext.clearRect(0, 0, currFile.TMPLayer.canvas.width, currFile.TMPLayer.canvas.height);
|
||||
|
||||
// Drawing the rect
|
||||
tmpContext.lineWidth = this.currSize;
|
||||
|
||||
// Drawing the rect
|
||||
tmpContext.beginPath();
|
||||
if ((this.currSize % 2 ) == 0) {
|
||||
tmpContext.rect(this.startMousePos[0] - 0.5, this.startMousePos[1] - 0.5, x - this.startMousePos[0], y - this.startMousePos[1]);
|
||||
}
|
||||
else {
|
||||
tmpContext.rect(this.startMousePos[0], this.startMousePos[1], x - this.startMousePos[0], y - this.startMousePos[1]);
|
||||
}
|
||||
|
||||
tmpContext.setLineDash([]);
|
||||
tmpContext.stroke();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,157 @@
|
|||
class RectangularSelectionTool extends SelectionTool {
|
||||
switchFunc = undefined;
|
||||
moveTool = undefined;
|
||||
currSelection = {};
|
||||
|
||||
constructor (name, options, switchFunc, moveTool) {
|
||||
super(name, options, switchFunc);
|
||||
|
||||
this.switchFunc = switchFunc;
|
||||
this.moveTool = moveTool;
|
||||
Events.on('click', this.mainButton, switchFunc, this);
|
||||
}
|
||||
|
||||
onStart(mousePos) {
|
||||
super.onStart(mousePos);
|
||||
|
||||
// Putting the vfx layer on top of everything
|
||||
currFile.VFXLayer.canvas.style.zIndex = MAX_Z_INDEX;
|
||||
|
||||
// Saving the start coords of the rect
|
||||
this.startMousePos[0] = Math.round(this.startMousePos[0] / currFile.zoom) - 0.5;
|
||||
this.startMousePos[1] = Math.round(this.startMousePos[1] / currFile.zoom) - 0.5;
|
||||
|
||||
// Avoiding external selections
|
||||
if (this.startMousePos[0] < 0) {
|
||||
this.startMousePos[0] = 0;
|
||||
}
|
||||
else if (this.startMousePos[0] > currFile.currentLayer.canvas.width) {
|
||||
this.startMousePos[0] = currFile.currentLayer.canvas.width;
|
||||
}
|
||||
|
||||
if (this.startMousePos[1] < 0) {
|
||||
this.startMousePos[1] = 0;
|
||||
}
|
||||
else if (this.startMousePos[1] > currFile.currentLayer.canvas.height) {
|
||||
this.startMousePos[1] = currFile.currentLayer.canvas.height;
|
||||
}
|
||||
|
||||
// Drawing the rect
|
||||
this.drawSelection(this.startMousePos[0], this.startMousePos[1]);
|
||||
}
|
||||
|
||||
onDrag(mousePos) {
|
||||
super.onDrag(mousePos);
|
||||
|
||||
// Drawing the rect
|
||||
this.drawSelection(Math.round(mousePos[0] / currFile.zoom) + 0.5, Math.round(mousePos[1] / currFile.zoom) + 0.5);
|
||||
}
|
||||
|
||||
onEnd(mousePos) {
|
||||
super.onEnd(mousePos);
|
||||
new HistoryState().EditCanvas();
|
||||
|
||||
// Getting the end position
|
||||
this.endMousePos[0] = Math.round(this.endMousePos[0] / currFile.zoom) + 0.5;
|
||||
this.endMousePos[1] = Math.round(this.endMousePos[1] / currFile.zoom) + 0.5;
|
||||
|
||||
// Inverting end and start (start must always be the top left corner)
|
||||
if (this.endMousePos[0] < this.startMousePos[0]) {
|
||||
let tmp = this.endMousePos[0];
|
||||
this.endMousePos[0] = this.startMousePos[0];
|
||||
this.startMousePos[0] = tmp;
|
||||
}
|
||||
// Same for the y
|
||||
if (this.endMousePos[1] < this.startMousePos[1]) {
|
||||
let tmp = this.endMousePos[1];
|
||||
this.endMousePos[1] = this.startMousePos[1];
|
||||
this.startMousePos[1] = tmp;
|
||||
}
|
||||
|
||||
// Switch to the move tool so that the user can move the selection
|
||||
this.switchFunc(this.moveTool);
|
||||
// Preparing data for the move tool
|
||||
let dataWidth = this.endMousePos[0] - this.startMousePos[0];
|
||||
let dataHeight = this.endMousePos[1] - this.startMousePos[1];
|
||||
|
||||
this.currSelection = {
|
||||
left: this.startMousePos[0], right: this.endMousePos[0],
|
||||
top: this.startMousePos[1], bottom: this.endMousePos[1],
|
||||
|
||||
width: dataWidth,
|
||||
height: dataHeight,
|
||||
|
||||
data: currFile.currentLayer.context.getImageData(
|
||||
this.startMousePos[0], this.startMousePos[1],
|
||||
dataWidth + 1, dataHeight + 1)
|
||||
};
|
||||
|
||||
// Moving the selection to the TMP layer. It will be moved back to the original
|
||||
// layer if the user will cancel or end the selection
|
||||
currFile.currentLayer.context.clearRect(this.startMousePos[0] - 0.5, this.startMousePos[1] - 0.5,
|
||||
dataWidth + 1, dataHeight + 1);
|
||||
// Moving those pixels from the current layer to the tmp layer
|
||||
currFile.TMPLayer.context.putImageData(this.currSelection.data, this.startMousePos[0], this.startMousePos[1]);
|
||||
|
||||
this.moveTool.setSelectionData(this.currSelection, this);
|
||||
console.log("data set");
|
||||
}
|
||||
|
||||
onSelect() {
|
||||
super.onSelect();
|
||||
}
|
||||
|
||||
onDeselect() {
|
||||
super.onDeselect();
|
||||
}
|
||||
|
||||
drawSelection(x, y) {
|
||||
// Getting the vfx context
|
||||
let vfxContext = currFile.VFXLayer.context;
|
||||
|
||||
// Clearing the vfx canvas
|
||||
vfxContext.clearRect(0, 0, currFile.VFXLayer.canvas.width, currFile.VFXLayer.canvas.height);
|
||||
vfxContext.lineWidth = 1;
|
||||
vfxContext.strokeStyle = 'black';
|
||||
vfxContext.setLineDash([4]);
|
||||
|
||||
// Drawing the rect
|
||||
vfxContext.beginPath();
|
||||
vfxContext.rect(this.startMousePos[0], this.startMousePos[1], x - this.startMousePos[0], y - this.startMousePos[1]);
|
||||
|
||||
vfxContext.stroke();
|
||||
}
|
||||
|
||||
/** Moves the rect ants to the specified position
|
||||
*
|
||||
* @param {*} x X coordinate of the rect ants
|
||||
* @param {*} y Y coordinat of the rect ants
|
||||
* @param {*} width Width of the selection
|
||||
* @param {*} height Height of the selectione
|
||||
*
|
||||
* @return The data regarding the current position and size of the selection
|
||||
*/
|
||||
moveAnts(x, y, width, height) {
|
||||
// Getting the vfx context
|
||||
let vfxContext = currFile.VFXLayer.context;
|
||||
let ret = this.currSelection;
|
||||
|
||||
// Clearing the vfx canvas
|
||||
vfxContext.clearRect(0, 0, currFile.VFXLayer.canvas.width, currFile.VFXLayer.canvas.height);
|
||||
vfxContext.lineWidth = 1;
|
||||
vfxContext.setLineDash([4]);
|
||||
|
||||
// Fixing the coordinates
|
||||
this.currSelection.left = Math.round(Math.round(x) - 0.5 - Math.round(width / 2)) + 0.5;
|
||||
this.currSelection.top = Math.round(Math.round(y) - 0.5 - Math.round(height / 2)) + 0.5;
|
||||
this.currSelection.right = this.currSelection.left + Math.round(width);
|
||||
this.currSelection.bottom = this.currSelection.top + Math.round(height);
|
||||
|
||||
// Drawing the rect
|
||||
vfxContext.beginPath();
|
||||
vfxContext.rect(this.currSelection.left, this.currSelection.top, width, height);
|
||||
vfxContext.stroke();
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
class ResizableTool extends DrawingTool {
|
||||
startResizePos = undefined;
|
||||
currSize = 1;
|
||||
prevSize = 1;
|
||||
|
||||
constructor(name, options, switchFunc) {
|
||||
super(name, options, switchFunc);
|
||||
}
|
||||
|
||||
onRightStart(mousePos, mouseEvent) {
|
||||
this.startResizePos = mousePos;
|
||||
}
|
||||
|
||||
onRightDrag(mousePos, mouseEvent) {
|
||||
//get new brush size based on x distance from original clicking location
|
||||
let distanceFromClick = mousePos[0]/currFile.zoom - this.startResizePos[0]/currFile.zoom;
|
||||
|
||||
let brushSizeChange = Math.round(distanceFromClick/10);
|
||||
let newBrushSize = this.currSize + brushSizeChange;
|
||||
|
||||
//set the brush to the new size as long as its bigger than 1 and less than 128
|
||||
this.currSize = Math.min(Math.max(1,newBrushSize), 128);
|
||||
|
||||
//fix offset so the cursor stays centered
|
||||
this.updateCursor();
|
||||
this.onHover(this.startResizePos, mouseEvent);
|
||||
}
|
||||
|
||||
onRightEnd(mousePos, mouseEvent) {
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
class SelectionTool extends Tool {
|
||||
constructor(name, options, switchFunc) {
|
||||
super(name, options, switchFunc);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
class ZoomTool extends Tool {
|
||||
constructor (name, options) {
|
||||
super(name, options, undefined);
|
||||
}
|
||||
onMouseWheel(mousePos, mode) {
|
||||
super.onMouseWheel(mousePos, mode);
|
||||
|
||||
// Computing current width and height
|
||||
let oldWidth = currFile.canvasSize[0] * currFile.zoom;
|
||||
let oldHeight = currFile.canvasSize[1] * currFile.zoom;
|
||||
let newWidth, newHeight;
|
||||
let prevZoom = currFile.zoom;
|
||||
let zoomed = false;
|
||||
|
||||
//change zoom level
|
||||
//if you want to zoom out, and the zoom isnt already at the smallest level
|
||||
if (mode == 'out' && currFile.zoom > MIN_ZOOM_LEVEL) {
|
||||
zoomed = true;
|
||||
if (currFile.zoom > 2)
|
||||
currFile.zoom -= Math.ceil(currFile.zoom / 10);
|
||||
else
|
||||
currFile.zoom -= Math.ceil(currFile.zoom * 2 / 10) / 2;
|
||||
|
||||
newWidth = currFile.canvasSize[0] * currFile.zoom;
|
||||
newHeight = currFile.canvasSize[1] * currFile.zoom;
|
||||
|
||||
//adjust canvas position
|
||||
currFile.layers[0].setCanvasOffset(
|
||||
currFile.layers[0].canvas.offsetLeft + (oldWidth - newWidth) * mousePos[0]/oldWidth,
|
||||
currFile.layers[0].canvas.offsetTop + (oldHeight - newHeight) * mousePos[1]/oldHeight);
|
||||
}
|
||||
//if you want to zoom in
|
||||
else if (mode == 'in' && currFile.zoom + Math.ceil(currFile.zoom/10) < window.innerHeight/4) {
|
||||
zoomed = true;
|
||||
if (currFile.zoom > 2)
|
||||
currFile.zoom += Math.ceil(currFile.zoom/10);
|
||||
else {
|
||||
if (currFile.zoom + currFile.zoom/10 > 2) {
|
||||
currFile.zoom += Math.ceil(currFile.zoom/10);
|
||||
currFile.zoom = Math.ceil(currFile.zoom);
|
||||
}
|
||||
else {
|
||||
currFile.zoom += Math.ceil(currFile.zoom * 2 / 10) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
newWidth = currFile.canvasSize[0] * currFile.zoom;
|
||||
newHeight = currFile.canvasSize[1] * currFile.zoom;
|
||||
|
||||
//adjust canvas position
|
||||
currFile.layers[0].setCanvasOffset(
|
||||
currFile.layers[0].canvas.offsetLeft - Math.round((newWidth - oldWidth)*mousePos[0]/oldWidth),
|
||||
currFile.layers[0].canvas.offsetTop - Math.round((newHeight - oldHeight)*mousePos[1]/oldHeight));
|
||||
}
|
||||
|
||||
//resize canvas
|
||||
currFile.layers[0].resize();
|
||||
// adjust brush size
|
||||
ToolManager.currentTool().updateCursor();
|
||||
|
||||
// Adjust pixel grid thickness
|
||||
if (zoomed) {
|
||||
if (currFile.zoom <= 7)
|
||||
currFile.pixelGrid.disablePixelGrid();
|
||||
else if (currFile.zoom >= 20 && mode == 'in') {
|
||||
currFile.pixelGrid.enablePixelGrid();
|
||||
currFile.pixelGrid.repaintPixelGrid((currFile.zoom - prevZoom) * 0.6);
|
||||
}
|
||||
else if (prevZoom >= 20 && mode == 'out') {
|
||||
currFile.pixelGrid.enablePixelGrid();
|
||||
currFile.pixelGrid.repaintPixelGrid((currFile.zoom - prevZoom) * 0.6);
|
||||
}
|
||||
else {
|
||||
currFile.pixelGrid.enablePixelGrid();
|
||||
}
|
||||
}
|
||||
|
||||
for (let i=1; i<currFile.layers.length; i++) {
|
||||
currFile.layers[i].copyData(currFile.layers[0]);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
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;
|
|
@ -21,12 +21,12 @@ let endEllipseY;
|
|||
*/
|
||||
function startEllipseDrawing(mouseEvent) {
|
||||
// Putting the vfx layer on top of everything
|
||||
VFXCanvas.style.zIndex = parseInt(currentLayer.canvas.style.zIndex, 10) + 1;;
|
||||
VFXLayer.canvas.style.zIndex = parseInt(currentLayer.canvas.style.zIndex, 10) + 1;;
|
||||
// Updating flag
|
||||
isDrawingEllipse = true;
|
||||
|
||||
// Saving the start coords of the ellipse
|
||||
let cursorPos = getCursorPosition(mouseEvent);
|
||||
let cursorPos = Input.getCursorPosition(mouseEvent);
|
||||
startEllipseX = Math.floor(cursorPos[0] / zoom) + 0.5;
|
||||
startEllipseY = Math.floor(cursorPos[1] / zoom) + 0.5;
|
||||
|
||||
|
@ -39,7 +39,7 @@ function startEllipseDrawing(mouseEvent) {
|
|||
* @param {*} mouseEvent The mouseEvent from which we'll get the mouse position
|
||||
*/
|
||||
function updateEllipseDrawing(mouseEvent) {
|
||||
let pos = getCursorPosition(mouseEvent);
|
||||
let pos = Input.getCursorPosition(mouseEvent);
|
||||
|
||||
// Drawing the ellipse at the right position
|
||||
drawEllipse(Math.round(pos[0] / zoom) + 0.5, Math.round(pos[1] / zoom) + 0.5);
|
||||
|
@ -53,8 +53,8 @@ function updateEllipseDrawing(mouseEvent) {
|
|||
*/
|
||||
function endEllipseDrawing(mouseEvent) {
|
||||
// Getting the end position
|
||||
let currentPos = getCursorPosition(mouseEvent);
|
||||
let vfxContext = VFXCanvas.getContext("2d");
|
||||
let currentPos = Input.getCursorPosition(mouseEvent);
|
||||
let vfxContext = VFXLayer.context;
|
||||
|
||||
endEllipseX = Math.round(currentPos[0] / zoom) + 0.5;
|
||||
endEllipseY = Math.round(currentPos[1] / zoom) + 0.5;
|
||||
|
@ -80,15 +80,14 @@ function endEllipseDrawing(mouseEvent) {
|
|||
endEllipseX -= 0.5;
|
||||
startEllipseX -= 0.5;
|
||||
|
||||
// Setting the correct linewidth and colour
|
||||
// Setting the correct linewidth
|
||||
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);
|
||||
currentLayer.drawLine(startEllipseX, startEllipseY, endEllipseX, startEllipseY, tool.ellipse.brushSize);
|
||||
currentLayer.drawLine(endEllipseX, startEllipseY, endEllipseX, endEllipseY, tool.ellipse.brushSize);
|
||||
currentLayer.drawLine(endEllipseX, endEllipseY, startEllipseX, endEllipseY, tool.ellipse.brushSize);
|
||||
currentLayer.drawLine(startEllipseX, endEllipseY, startEllipseX, startEllipseY, tool.ellipse.brushSize);
|
||||
|
||||
// If I have to fill it, I do so
|
||||
if (ellipseDrawMode == 'fill') {
|
||||
|
@ -96,7 +95,7 @@ function endEllipseDrawing(mouseEvent) {
|
|||
}
|
||||
|
||||
// Clearing the vfx canvas
|
||||
vfxContext.clearRect(0, 0, VFXCanvas.width, VFXCanvas.height);
|
||||
vfxContext.clearRect(0, 0, VFXLayer.canvas.width, VFXLayer.canvas.height);
|
||||
}
|
||||
|
||||
// TODO: [ELLIPSE] Make it draw ellipse instead of copy-pasted rectangle
|
||||
|
@ -108,14 +107,13 @@ function endEllipseDrawing(mouseEvent) {
|
|||
*/
|
||||
function drawEllipse(x, y) {
|
||||
// Getting the vfx context
|
||||
let vfxContext = VFXCanvas.getContext("2d");
|
||||
let vfxContext = VFXLayer.context;
|
||||
|
||||
// Clearing the vfx canvas
|
||||
vfxContext.clearRect(0, 0, VFXCanvas.width, VFXCanvas.height);
|
||||
vfxContext.clearRect(0, 0, VFXLayer.canvas.width, VFXLayer.canvas.height);
|
||||
|
||||
// Drawing the ellipse
|
||||
vfxContext.lineWidth = tool.ellipse.brushSize;
|
||||
vfxContext.strokeStyle = currentGlobalColor;
|
||||
|
||||
// Drawing the ellipse
|
||||
vfxContext.beginPath();
|
|
@ -1,79 +0,0 @@
|
|||
//GET: ajax(String url, Function success [,timeout])
|
||||
//POST: ajax(String url, Object postData, Function success [,timeout])
|
||||
Util.ajax = function (url, arg2, arg3, arg4) {
|
||||
if (typeof arg2 == 'function') {
|
||||
var success = arg2;
|
||||
var timeout = arg3 || 10000;
|
||||
}
|
||||
else {
|
||||
var postData = arg2;
|
||||
var success = arg3;
|
||||
var timeout = arg4 || 10000;
|
||||
}
|
||||
|
||||
var start = new Date();
|
||||
console.log('AJAX - STARTING REQUEST', url, '(' + timeout + ')');
|
||||
|
||||
//start new request
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = function() {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
var result;
|
||||
|
||||
//try to parse as json
|
||||
try {
|
||||
result = JSON.parse(this.response);
|
||||
console.log('AJAX - COMPLETE ('+(new Date()-start)+'ms) - json:', result);
|
||||
}
|
||||
catch (e) {
|
||||
result = this.response;
|
||||
console.log('AJAX - COMPLETE ('+(new Date()-start)+'ms) - string:', this.response, e);
|
||||
}
|
||||
|
||||
//return result
|
||||
success(result);
|
||||
|
||||
xhr = null;
|
||||
}
|
||||
else if (this.readyState == 4) {
|
||||
console.log('ajax failed', this.readyState, this.status);
|
||||
success({ error: 'failure' });
|
||||
}
|
||||
};
|
||||
|
||||
xhr.ontimeout = function(e) {
|
||||
console.log('ajax request timed out')
|
||||
success({ error: 'timeout' });
|
||||
};
|
||||
|
||||
if (postData) {
|
||||
//post request
|
||||
console.log('post data: ', postData instanceof FormData, postData);
|
||||
|
||||
//the the input isn't already formdata, convert it to form data
|
||||
if (!(postData instanceof FormData)) {
|
||||
console.log('converting to form data');
|
||||
|
||||
var formData = new FormData();
|
||||
|
||||
for (var key in postData) {
|
||||
formData.append(key, postData[key]);
|
||||
}
|
||||
|
||||
postData = formData;
|
||||
}
|
||||
|
||||
//send to server
|
||||
xhr.open("POST", url, true);
|
||||
xhr.timeout = timeout;
|
||||
xhr.send(postData);
|
||||
}
|
||||
else {
|
||||
//get request
|
||||
xhr.open("GET", url, true);
|
||||
xhr.timeout = timeout;
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
return xhr;
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
//get text of specified element
|
||||
function getText(elementId) {
|
||||
var element = (typeof elementId == 'string' ? document.getElementById(elementId) : elementId);
|
||||
return element.textContent;
|
||||
}
|
||||
|
||||
function setText(elementId, text) {
|
||||
var element = (typeof elementId == 'string' ? document.getElementById(elementId) : elementId);
|
||||
element.textContent = text;
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
|
||||
|
||||
function getValue(elementId) {
|
||||
var element = (typeof elementId == 'string' ? document.getElementById(elementId) : elementId);
|
||||
console.log("setting: " + elementId + ": " + element.value);
|
||||
return element.value;
|
||||
}
|
||||
|
||||
function setValue(elementId, value) {
|
||||
var element = (typeof elementId == 'string' ? document.getElementById(elementId) : elementId);
|
||||
element.value = value;
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue