Merge pull request #13 from nkoder/initial_ellipsis_tool

Boilerplate for ellipse tool: menu item, event handlers
This commit is contained in:
Nicola 2021-04-29 10:10:12 +02:00 committed by GitHub
commit 26aa1023c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 327 additions and 27 deletions

View File

@ -61,6 +61,12 @@ You also need `npm` in version 7 (because of 2nd version of lockfile which was i
If you have any trouble, see this page: https://help.github.com/en/articles/creating-a-pull-request-from-a-fork
### Feature Toggles
Some feature might be hidden by default. Functions to enable/disable them are available inside global `featureToggles` and operate on a `window.localStorage`.
For example use `featureToggles.enableEllipseTool()` to make ellipse tool button visible. Then `featureToggles.disableEllipseTool()` to hide it.
## License
This software may not be resold, redistributed, rehosted or otherwise conveyed to a third party.

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

@ -0,0 +1,6 @@
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
<g>
<ellipse stroke="#000" stroke-width="32" fill="none" cx="255.50001" cy="255.5" id="svg_20" rx="239" ry="187.5"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 221 B

View File

@ -0,0 +1,22 @@
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
<g>
<title>Layer 1</title>
<g id="svg_4"/>
<g id="svg_5"/>
<g id="svg_6"/>
<g id="svg_7"/>
<g id="svg_8"/>
<g id="svg_9"/>
<g id="svg_10"/>
<g id="svg_11"/>
<g id="svg_12"/>
<g id="svg_13"/>
<g id="svg_14"/>
<g id="svg_15"/>
<g id="svg_16"/>
<g id="svg_17"/>
<g id="svg_18"/>
<ellipse stroke="#000" stroke-width="32" fill="#000000" cx="255.50001" cy="255.5" id="svg_20" rx="239" ry="187.5"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 528 B

View File

@ -688,6 +688,7 @@ svg {
#tools-menu li button#zoom-in-button,
#tools-menu li button#eraser-bigger-button,
#tools-menu li button#rectangle-bigger-button,
#tools-menu li button#ellipse-bigger-button,
#tools-menu li button#line-bigger-button {
left: 0;
}
@ -696,6 +697,7 @@ svg {
#tools-menu li button#zoom-out-button,
#tools-menu li button#eraser-smaller-button,
#tools-menu li button#rectangle-smaller-button,
#tools-menu li button#ellipse-smaller-button,
#tools-menu li button#line-smaller-button {
right: 0;
}
@ -708,6 +710,8 @@ svg {
#tools-menu li.selected button#eraser-smaller-button,
#tools-menu li.selected button#rectangle-bigger-button,
#tools-menu li.selected button#rectangle-smaller-button,
#tools-menu li.selected button#ellipse-bigger-button,
#tools-menu li.selected button#ellipse-smaller-button,
#tools-menu li.selected button#line-bigger-button,
#tools-menu li.selected button#line-smaller-button {
display: block;

View File

@ -9,7 +9,7 @@ function line(x0,y0,x1,y1, brushSize) {
while (true) {
//set pixel
// If the current tool is the brush
if (currentTool.name == 'pencil' || currentTool.name == 'rectangle') {
if (currentTool.name == 'pencil' || currentTool.name == 'rectangle' || currentTool.name == 'ellipse') {
// I fill the rect
currentLayer.context.fillRect(x0-Math.floor(brushSize/2), y0-Math.floor(brushSize/2), brushSize, brushSize);
} else if (currentTool.name == 'eraser') {

146
js/_ellipse.js Normal file
View File

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

33
js/_featureToggles.js Normal file
View File

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

View File

@ -65,6 +65,11 @@ function KeyPress(e) {
case 77: case 109:
tool.rectselect.switchTo()
break;
// TODO: [ELLIPSE] Decide on a shortcut to use. "s" was chosen without any in-team consultation.
// ellipse tool, s
case 83:
tool.ellipse.switchTo()
break;
// rectangle tool, u
case 85:
tool.rectangle.switchTo()

View File

@ -22,7 +22,7 @@ window.addEventListener("mousedown", function (mouseEvent) {
else if (mouseEvent.altKey)
currentTool = tool.eyedropper;
else if (mouseEvent.target.className == 'drawingCanvas' &&
(currentTool.name == 'pencil' || currentTool.name == 'eraser' || currentTool.name == 'rectangle' || currentTool.name === 'line'))
(currentTool.name == 'pencil' || currentTool.name == 'eraser' || currentTool.name == 'rectangle' || currentTool.name == 'ellipse' || currentTool.name === 'line'))
new HistoryStateEditCanvas();
else if (currentTool.name == 'moveselection') {
if (!cursorInSelectedArea() &&
@ -46,6 +46,7 @@ window.addEventListener("mousedown", function (mouseEvent) {
currentTool = tool.resizeeraser;
tool.eraser.previousBrushSize = tool.eraser.brushSize;
}
// TODO: [ELLIPSE] Do we need similar logic related to ellipse?
else if (currentTool.name == 'rectangle' && mouseEvent.which == 3) {
currentTool = tool.resizerectangle;
tool.rectangle.previousBrushSize = tool.rectangle.brushSize;
@ -160,6 +161,10 @@ window.addEventListener("mouseup", function (mouseEvent) {
endRectDrawing(mouseEvent);
currentLayer.updateLayerPreview();
}
else if (currentTool.name == 'ellipse' && isDrawingEllipse) {
endEllipseDrawing(mouseEvent);
currentLayer.updateLayerPreview();
}
dragging = false;
currentTool = currentToolTemp;
@ -269,6 +274,21 @@ function draw (mouseEvent) {
updateRectDrawing(mouseEvent);
}
}
else if (currentTool.name == 'ellipse')
{
//hide brush preview outside of canvas / canvas view
if (mouseEvent.target.className == 'drawingCanvas'|| mouseEvent.target.className == 'drawingCanvas')
brushPreview.style.visibility = 'visible';
else
brushPreview.style.visibility = 'hidden';
if (!isDrawingEllipse && dragging) {
startEllipseDrawing(mouseEvent);
}
else if (dragging){
updateEllipseDrawing(mouseEvent);
}
}
else if (currentTool.name == 'pan' && dragging) {
// Setting first layer position
layers[0].setCanvasOffset(layers[0].canvas.offsetLeft + (cursorLocation[0] - lastMouseClickPos[0]), layers[0].canvas.offsetTop + (cursorLocation[1] - lastMouseClickPos[1]));
@ -328,12 +348,15 @@ function draw (mouseEvent) {
//var roundingAmount = 20 - Math.round(distanceFromClick/10);
//this doesnt work in reverse... because... it's not basing it off of the brush size which it should be
var rectangleSizeChange = Math.round(distanceFromClick/10);
// TODO: [ELLIPSE] Do we need similar logic related to ellipse?
var newRectangleSize = tool.rectangle.previousBrushSize + rectangleSizeChange;
//set the brush to the new size as long as its bigger than 1
// TODO: [ELLIPSE] Do we need similar logic related to ellipse?
tool.rectangle.brushSize = Math.max(1,newRectangleSize);
//fix offset so the cursor stays centered
// TODO: [ELLIPSE] Do we need similar logic related to ellipse?
tool.rectangle.moveBrushPreview(lastMouseClickPos);
currentTool.updateCursor();
}

View File

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

View File

@ -1,10 +1,10 @@
// Saving the empty rect svg
var emptySVG = document.getElementById("empty-button-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 fullSVG = document.getElementById("full-button-svg");
var fullRectangleSVG = document.getElementById("rectangle-full-button-svg");
// The start mode is empty rectangle
var drawMode = 'empty';
var rectangleDrawMode = 'empty';
// I'm not drawing a rectangle at the beginning
var isDrawingRect = false;
@ -88,7 +88,7 @@ function endRectDrawing(mouseEvent) {
line(startRectX, endRectY, startRectX, startRectY, tool.rectangle.brushSize);
// If I have to fill it, I do so
if (drawMode == 'fill') {
if (rectangleDrawMode == 'fill') {
currentLayer.context.fillRect(startRectX, startRectY, endRectX - startRectX, endRectY - startRectY);
}
@ -130,13 +130,13 @@ function drawRectangle(x, y) {
*
*/
function setRectToolSvg() {
if (drawMode == 'empty') {
emptySVG.setAttribute('display', 'visible');
fullSVG.setAttribute('display', 'none');
if (rectangleDrawMode == 'empty') {
emptyRectangleSVG.setAttribute('display', 'visible');
fullRectangleSVG.setAttribute('display', 'none');
}
else {
emptySVG.setAttribute('display', 'none');
fullSVG.setAttribute('display', 'visible');
emptyRectangleSVG.setAttribute('display', 'none');
fullRectangleSVG.setAttribute('display', 'visible');
}
}

View File

@ -35,12 +35,12 @@ on('click',"eraser-smaller-button", function(e){
on('click','rectangle-button', function(e){
// If the user clicks twice on the button, they change the draw mode
if (currentTool.name == 'rectangle') {
if (drawMode == 'empty') {
drawMode = 'fill';
if (rectangleDrawMode == 'empty') {
rectangleDrawMode = 'fill';
setRectToolSvg();
}
else {
drawMode = 'empty';
rectangleDrawMode = 'empty';
setRectToolSvg();
}
}
@ -49,6 +49,24 @@ on('click','rectangle-button', function(e){
}
}, false);
// ellipse
on('click','ellipse-button', function(e){
// If the user clicks twice on the button, they change the draw mode
if (currentTool.name == 'ellipse') {
if (ellipseDrawMode == 'empty') {
ellipseDrawMode = 'fill';
setEllipseToolSvg();
}
else {
ellipseDrawMode = 'empty';
setEllipseToolSvg();
}
}
else {
tool.ellipse.switchTo();
}
}, false);
// rectangle bigger
on('click',"rectangle-bigger-button", function(){
tool.rectangle.brushSize++;
@ -60,6 +78,17 @@ on('click',"rectangle-smaller-button", function(e){
tool.rectangle.brushSize--;
}, false);
// ellipse bigger
on('click',"ellipse-bigger-button", function(){
tool.ellipse.brushSize++;
}, false);
// ellipse smaller
on('click',"ellipse-smaller-button", function(e){
if(tool.ellipse.brushSize > 1)
tool.ellipse.brushSize--;
}, false);
//fill
on('click',"fill-button", function(){
tool.fill.switchTo();

View File

@ -73,6 +73,7 @@
//=include _rectSelect.js
//=include _move.js
//=include _rectangle.js
//=include _ellipse.js
/**onload**/
//=include _onLoad.js
@ -80,3 +81,6 @@
/**libraries**/
//=include _jscolor.js
/**feature toggles**/
//=include _featureToggles.js

View File

@ -41,6 +41,10 @@ new Tool('rectangle', {
cursor: 'crosshair',
brushPreview: true,
});
new Tool('ellipse', {
cursor: 'crosshair',
brushPreview: true,
});
new Tool('resizerectangle', {
cursor: 'default',
});

View File

@ -33,6 +33,7 @@
<img src="/pixel-editor/zoom-in.png" />
<img src = "/pixel-editor/eraser.png"/>
<img src = "/pixel-editor/rectselect.png"/>
<!-- TODO: [ELLIPSE] Where is this icon used? Do we need similar one for ellipsis? -->
<img src= "/pixel-editor/rectangle.png">
</div>
@ -119,12 +120,24 @@
</li>
<li class="expanded">
<button title="Rectangle Tool (U)" id="rectangle-button">{{svg "rectangle.svg" width="32" height="32" id = "empty-button-svg"}}
{{svg "fullrect.svg" width="32" height="32" id = "full-button-svg" display = "none"}}</button>
<button title="Rectangle Tool (U)" id="rectangle-button">{{svg "rectangle.svg" width="32" height="32" id = "rectangle-empty-button-svg"}}
{{svg "fullrect.svg" width="32" height="32" id = "rectangle-full-button-svg" display = "none"}}</button>
<button title="Increase Rectangle Size" id="rectangle-bigger-button" class="tools-menu-sub-button">{{svg "plus.svg" width="12" height="12"}}</button>
<button title="Decrease Rectangle Size" id="rectangle-smaller-button" class="tools-menu-sub-button">{{svg "minus.svg" width="12" height="12"}}</button>
</li>
<!-- TODO: [ELLIPSE] Once ellipse is ready for release make it visible by default -->
<li class="expanded" id="tools-menu--ellipse" style="display: none">
<!-- TODO: [ELLIPSE] Decide on a shortcut to use. "S" was chosen without any in-team consultation. -->
<!-- TODO: [ELLIPSE] Decide on icons to use. Current ones are quickly prepared drafts and display with incorrect color. -->
<button title="Ellipse Tool (S)" id="ellipse-button">
{{svg "ellipse.svg" width="32" height="32" id = "ellipse-empty-button-svg"}}
{{svg "filledellipse.svg" width="32" height="32" id = "ellipse-full-button-svg" display = "none"}}
</button>
<button title="Increase Ellipse Size" id="ellipse-bigger-button" class="tools-menu-sub-button">{{svg "plus.svg" width="12" height="12"}}</button>
<button title="Decrease Ellipse Size" id="ellipse-smaller-button" class="tools-menu-sub-button">{{svg "minus.svg" width="12" height="12"}}</button>
</li>
<li><button title="Fill Tool (F)" id="fill-button">{{svg "fill.svg" width="32" height="32"}}</button></li>
<li><button title="Eyedropper Tool (E)" id="eyedropper-button">{{svg "eyedropper.svg" width="32" height="32"}}</button></li>