diff --git a/src/wwwroot/js/genpage/helpers/image_editor.js b/src/wwwroot/js/genpage/helpers/image_editor.js
index 04148f3c8..0b3e7fded 100644
--- a/src/wwwroot/js/genpage/helpers/image_editor.js
+++ b/src/wwwroot/js/genpage/helpers/image_editor.js
@@ -837,6 +837,363 @@ class ImageEditorToolBucket extends ImageEditorTool {
}
}
+/**
+ * The Shape tool.
+ */
+class ImageEditorToolShape extends ImageEditorTool {
+ constructor(editor) {
+ super(editor, 'shape', 'shape', 'Shape', 'Create basic colored shape outlines.\nClick and drag to draw a shape.\nHotKey: X', 'x');
+ this.cursor = 'crosshair';
+ this.color = '#ff0000';
+ this.strokeWidth = 4;
+ this.shape = 'rectangle';
+ this.isDrawing = false;
+ this.startX = 0;
+ this.startY = 0;
+ this.currentX = 0;
+ this.currentY = 0;
+ this.startLayerX = 0;
+ this.startLayerY = 0;
+ this.currentLayerX = 0;
+ this.currentLayerY = 0;
+ this.bufferLayer = null;
+ this.hasDrawn = false;
+ let colorHTML = `
+
+
+
+
+
+
`;
+ let shapeHTML = `
+
+
+
+
`;
+ let strokeHTML = `
+ `;
+ this.configDiv.innerHTML = colorHTML + shapeHTML + strokeHTML;
+ this.colorText = this.configDiv.querySelector('.id-col1');
+ this.colorSelector = this.configDiv.querySelector('.id-col2');
+ this.colorPickButton = this.configDiv.querySelector('.id-col3');
+ this.shapeSelect = this.configDiv.querySelector('.id-shape');
+ this.strokeNumber = this.configDiv.querySelector('.id-stroke1');
+ this.strokeSelector = this.configDiv.querySelector('.id-stroke2');
+ this.colorText.addEventListener('input', () => {
+ this.colorSelector.value = this.colorText.value;
+ this.onConfigChange();
+ });
+ this.colorSelector.addEventListener('change', () => {
+ this.colorText.value = this.colorSelector.value;
+ this.onConfigChange();
+ });
+ this.colorPickButton.addEventListener('click', () => {
+ if (this.colorPickButton.classList.contains('interrupt-button')) {
+ this.colorPickButton.classList.remove('interrupt-button');
+ this.editor.activateTool(this.id);
+ }
+ else {
+ this.colorPickButton.classList.add('interrupt-button');
+ this.editor.pickerTool.toolFor = this;
+ this.editor.activateTool('picker');
+ }
+ });
+ this.shapeSelect.addEventListener('change', () => {
+ this.shape = this.shapeSelect.value;
+ this.editor.redraw();
+ });
+ enableSliderForBox(this.configDiv.querySelector('.id-stroke-block'));
+ this.strokeNumber.addEventListener('change', () => { this.onConfigChange(); });
+ }
+
+ setColor(col) {
+ this.color = col;
+ this.colorText.value = col;
+ this.colorSelector.value = col;
+ this.colorPickButton.classList.remove('interrupt-button');
+ }
+
+ onConfigChange() {
+ this.color = this.colorText.value;
+ this.strokeWidth = parseInt(this.strokeNumber.value);
+ this.editor.redraw();
+ }
+
+ drawRectangleBorder(ctx, x, y, width, height, thickness) {
+ width = Math.max(1, Math.floor(width));
+ height = Math.max(1, Math.floor(height));
+ thickness = Math.max(1, Math.floor(thickness));
+ thickness = Math.min(thickness, width, height);
+ ctx.fillRect(x, y, width, thickness);
+ ctx.fillRect(x, y + height - thickness, width, thickness);
+ let verticalHeight = height - thickness * 2;
+ if (verticalHeight > 0) {
+ ctx.fillRect(x, y + thickness, thickness, verticalHeight);
+ ctx.fillRect(x + width - thickness, y + thickness, thickness, verticalHeight);
+ }
+ }
+
+ drawShapeToCanvas(ctx, type, x, y, width, height) {
+ ctx.beginPath();
+ if (type == 'rectangle') {
+ ctx.rect(Math.round(x), Math.round(y), Math.round(width), Math.round(height));
+ }
+ else if (type == 'circle') {
+ let radius = Math.sqrt(width * width + height * height) / 2;
+ ctx.arc(Math.round(x + width / 2), Math.round(y + height / 2), Math.round(radius), 0, 2 * Math.PI);
+ }
+ }
+
+ draw() {
+ if (!this.isDrawing) {
+ return;
+ }
+ const target = this.editor.activeLayer;
+ if (!target) {
+ return;
+ }
+ const startX = Math.min(this.startLayerX, this.currentLayerX);
+ const startY = Math.min(this.startLayerY, this.currentLayerY);
+ const endX = Math.max(this.startLayerX, this.currentLayerX);
+ const endY = Math.max(this.startLayerY, this.currentLayerY);
+ const width = endX - startX;
+ const height = endY - startY;
+ if (width == 0 && height == 0) {
+ return;
+ }
+ const [imageX1, imageY1] = target.layerCoordToImageCoord(startX, startY);
+ const [imageX2, imageY2] = target.layerCoordToImageCoord(endX, endY);
+ const [canvasX1, canvasY1] = this.editor.imageCoordToCanvasCoord(imageX1, imageY1);
+ const [canvasX2, canvasY2] = this.editor.imageCoordToCanvasCoord(imageX2, imageY2);
+ const canvasWidth = canvasX2 - canvasX1;
+ const canvasHeight = canvasY2 - canvasY1;
+ this.editor.ctx.save();
+ const prevSmoothing = this.editor.ctx.imageSmoothingEnabled;
+ this.editor.ctx.imageSmoothingEnabled = false;
+ this.editor.ctx.setLineDash([]);
+ if (this.shape == 'rectangle') {
+ const thickness = Math.max(1, Math.round(this.strokeWidth * this.editor.zoomLevel));
+ this.editor.ctx.fillStyle = this.color;
+ this.drawRectangleBorder(this.editor.ctx, Math.round(canvasX1), Math.round(canvasY1), Math.round(canvasWidth), Math.round(canvasHeight), thickness);
+ }
+ else {
+ this.editor.ctx.strokeStyle = this.color;
+ this.editor.ctx.lineWidth = Math.max(1, Math.round(this.strokeWidth * this.editor.zoomLevel));
+ this.drawShapeToCanvas(this.editor.ctx, this.shape, canvasX1, canvasY1, canvasWidth, canvasHeight);
+ this.editor.ctx.stroke();
+ }
+ this.editor.ctx.imageSmoothingEnabled = prevSmoothing;
+ this.editor.ctx.restore();
+ }
+
+ onMouseDown(e) {
+ if (e.button != 0) {
+ return;
+ }
+ if (this.isDrawing) {
+ this.finishDrawing();
+ }
+ this.editor.updateMousePosFrom(e);
+ let [mouseX, mouseY] = this.editor.canvasCoordToImageCoord(this.editor.mouseX, this.editor.mouseY);
+ mouseX = Math.round(mouseX);
+ mouseY = Math.round(mouseY);
+ this.isDrawing = true;
+ this.startX = mouseX;
+ this.startY = mouseY;
+ this.currentX = mouseX;
+ this.currentY = mouseY;
+ this.hasDrawn = false;
+ let target = this.editor.activeLayer;
+ if (!target) {
+ this.bufferLayer = null;
+ this.isDrawing = false;
+ return;
+ }
+ let [layerX, layerY] = target.imageCoordToLayerCoord(mouseX, mouseY);
+ layerX = Math.round(layerX);
+ layerY = Math.round(layerY);
+ this.startLayerX = layerX;
+ this.startLayerY = layerY;
+ this.currentLayerX = layerX;
+ this.currentLayerY = layerY;
+ this.bufferLayer = new ImageEditorLayer(this.editor, target.canvas.width, target.canvas.height, target);
+ this.bufferLayer.opacity = 1;
+ target.childLayers.push(this.bufferLayer);
+ }
+
+ finishDrawing() {
+ if (this.isDrawing && this.bufferLayer) {
+ const parent = this.editor.activeLayer;
+ if (!parent) {
+ this.bufferLayer = null;
+ this.isDrawing = false;
+ this.hasDrawn = false;
+ this.editor.redraw();
+ return;
+ }
+ if (!this.hasDrawn) {
+ const idx = parent.childLayers.indexOf(this.bufferLayer);
+ if (idx !== -1) {
+ parent.childLayers.splice(idx, 1);
+ }
+ this.bufferLayer = null;
+ this.isDrawing = false;
+ this.hasDrawn = false;
+ this.editor.redraw();
+ return;
+ }
+ this.drawShape();
+ const idx = parent.childLayers.indexOf(this.bufferLayer);
+ if (idx !== -1) {
+ parent.childLayers.splice(idx, 1);
+ }
+ const offset = parent.getOffset();
+ parent.saveBeforeEdit();
+ this.bufferLayer.drawToBackDirect(parent.ctx, -offset[0], -offset[1], 1);
+ parent.hasAnyContent = true;
+ this.bufferLayer = null;
+ this.isDrawing = false;
+ this.hasDrawn = false;
+ this.editor.markChanged();
+ this.editor.redraw();
+ }
+ }
+
+ onMouseMove(e) {
+ if (!this.isDrawing) {
+ return;
+ }
+ this.editor.updateMousePosFrom(e);
+ let [mouseX, mouseY] = this.editor.canvasCoordToImageCoord(this.editor.mouseX, this.editor.mouseY);
+ mouseX = Math.round(mouseX);
+ mouseY = Math.round(mouseY);
+ this.currentX = mouseX;
+ this.currentY = mouseY;
+ let target = this.editor.activeLayer;
+ if (target) {
+ let [layerX, layerY] = target.imageCoordToLayerCoord(mouseX, mouseY);
+ this.currentLayerX = Math.round(layerX);
+ this.currentLayerY = Math.round(layerY);
+ }
+ this.drawShape();
+ }
+
+ onGlobalMouseMove(e) {
+ if (this.isDrawing) {
+ this.editor.updateMousePosFrom(e);
+ let [mouseX, mouseY] = this.editor.canvasCoordToImageCoord(this.editor.mouseX, this.editor.mouseY);
+ mouseX = Math.round(mouseX);
+ mouseY = Math.round(mouseY);
+ this.currentX = mouseX;
+ this.currentY = mouseY;
+ let target = this.editor.activeLayer;
+ if (target) {
+ let [layerX, layerY] = target.imageCoordToLayerCoord(mouseX, mouseY);
+ this.currentLayerX = Math.round(layerX);
+ this.currentLayerY = Math.round(layerY);
+ }
+ this.drawShape();
+ }
+
+ onMouseUp(e) {
+ if (e.button != 0 || !this.isDrawing) {
+ return;
+ }
+ if (!this.isDrawing) {
+ return;
+ }
+ this.editor.updateMousePosFrom(e);
+ let [mouseX, mouseY] = this.editor.canvasCoordToImageCoord(this.editor.mouseX, this.editor.mouseY);
+ mouseX = Math.round(mouseX);
+ mouseY = Math.round(mouseY);
+ this.currentX = mouseX;
+ this.currentY = mouseY;
+ let target = this.editor.activeLayer;
+ if (target) {
+ let [layerX, layerY] = target.imageCoordToLayerCoord(mouseX, mouseY);
+ this.currentLayerX = Math.round(layerX);
+ this.currentLayerY = Math.round(layerY);
+ }
+ this.finishDrawing();
+ }
+
+ onGlobalMouseUp(e) {
+ if (e.button != 0 || !this.isDrawing) {
+ return;
+ }
+ if (this.isDrawing) {
+ this.editor.updateMousePosFrom(e);
+ let [mouseX, mouseY] = this.editor.canvasCoordToImageCoord(this.editor.mouseX, this.editor.mouseY);
+ mouseX = Math.round(mouseX);
+ mouseY = Math.round(mouseY);
+ this.currentX = mouseX;
+ this.currentY = mouseY;
+ let target = this.editor.activeLayer;
+ if (target) {
+ let [layerX, layerY] = target.imageCoordToLayerCoord(mouseX, mouseY);
+ this.currentLayerX = Math.round(layerX);
+ this.currentLayerY = Math.round(layerY);
+ }
+ this.finishDrawing();
+ return true;
+ }
+ return false;
+ }
+
+ drawShape() {
+ if (!this.isDrawing || !this.bufferLayer) {
+ return;
+ }
+ const parent = this.editor.activeLayer;
+ if (!parent) {
+ return;
+ }
+ this.bufferLayer.ctx.clearRect(0, 0, this.bufferLayer.canvas.width, this.bufferLayer.canvas.height);
+ const startX = Math.round(Math.min(this.startLayerX, this.currentLayerX));
+ const startY = Math.round(Math.min(this.startLayerY, this.currentLayerY));
+ const endX = Math.round(Math.max(this.startLayerX, this.currentLayerX));
+ const endY = Math.round(Math.max(this.startLayerY, this.currentLayerY));
+ const width = endX - startX;
+ const height = endY - startY;
+ if (width == 0 && height == 0) {
+ this.bufferLayer.hasAnyContent = false;
+ this.hasDrawn = false;
+ this.editor.redraw();
+ return;
+ }
+ this.bufferLayer.ctx.save();
+ const prevSmoothing = this.bufferLayer.ctx.imageSmoothingEnabled;
+ this.bufferLayer.ctx.imageSmoothingEnabled = false;
+ this.bufferLayer.ctx.setLineDash([]);
+ if (this.shape == 'rectangle') {
+ const thickness = Math.max(1, Math.round(this.strokeWidth));
+ this.bufferLayer.ctx.fillStyle = this.color;
+ this.drawRectangleBorder(this.bufferLayer.ctx, startX, startY, width, height, thickness);
+ }
+ else {
+ this.bufferLayer.ctx.strokeStyle = this.color;
+ this.bufferLayer.ctx.lineWidth = Math.max(1, Math.round(this.strokeWidth));
+ this.drawShapeToCanvas(this.bufferLayer.ctx, this.shape, startX, startY, width, height);
+ this.bufferLayer.ctx.stroke();
+ }
+ this.bufferLayer.ctx.imageSmoothingEnabled = prevSmoothing;
+ this.bufferLayer.ctx.restore();
+ this.bufferLayer.hasAnyContent = true;
+ this.hasDrawn = true;
+ this.editor.markChanged();
+ this.editor.redraw();
+ }
+}
+
/**
* The Color Picker tool, a special hidden sub-tool.
*/
@@ -1026,6 +1383,22 @@ class ImageEditorLayer {
return [x2, y2];
}
+ imageCoordToLayerCoord(x, y) {
+ let [offsetX, offsetY] = this.getOffset();
+ let relWidth = this.width / this.canvas.width;
+ let relHeight = this.height / this.canvas.height;
+ [x, y] = [x - offsetX, y - offsetY];
+ let angle = -this.rotation;
+ let [cx, cy] = [this.width / 2, this.height / 2];
+ let [x2, y2] = [x - cx, y - cy];
+ let cos = Math.cos(angle), sin = Math.sin(angle);
+ let xRot = x2 * cos - y2 * sin;
+ let yRot = x2 * sin + y2 * cos;
+ xRot += cx;
+ yRot += cy;
+ return [xRot / relWidth, yRot / relHeight];
+ }
+
layerCoordToCanvasCoord(x, y) {
let [x2, y2] = this.editor.imageCoordToCanvasCoord(x, y);
let [offsetX, offsetY] = this.getOffset();
@@ -1035,6 +1408,22 @@ class ImageEditorLayer {
return [x2, y2];
}
+ layerCoordToImageCoord(x, y) {
+ let relWidth = this.width / this.canvas.width;
+ let relHeight = this.height / this.canvas.height;
+ [x, y] = [x * relWidth, y * relHeight];
+ let angle = this.rotation;
+ let [cx, cy] = [this.width / 2, this.height / 2];
+ let [x2, y2] = [x - cx, y - cy];
+ let cos = Math.cos(angle), sin = Math.sin(angle);
+ let xRot = x2 * cos - y2 * sin;
+ let yRot = x2 * sin + y2 * cos;
+ xRot += cx;
+ yRot += cy;
+ let [offsetX, offsetY] = this.getOffset();
+ return [xRot + offsetX, yRot + offsetY];
+ }
+
drawFilledCircle(x, y, radius, color) {
this.ctx.fillStyle = color;
this.ctx.beginPath();
@@ -1243,6 +1632,7 @@ class ImageEditor {
this.addTool(new ImageEditorToolBrush(this, 'brush', 'paintbrush', 'Paintbrush', 'Draw on the image.\nHotKey: B', false, 'b'));
this.addTool(new ImageEditorToolBrush(this, 'eraser', 'eraser', 'Eraser', 'Erase parts of the image.\nHotKey: E', true, 'e'));
this.addTool(new ImageEditorToolBucket(this));
+ this.addTool(new ImageEditorToolShape(this));
this.pickerTool = new ImageEditorToolPicker(this, 'picker', 'paintbrush', 'Color Picker', 'Pick a color from the image.');
this.addTool(this.pickerTool);
this.activateTool('brush');
@@ -1385,7 +1775,7 @@ class ImageEditor {
}
onKeyDown(e) {
- if (e.key === 'Alt') {
+ if (e.key == 'Alt') {
e.preventDefault();
this.handleAltDown();
}
@@ -1411,7 +1801,7 @@ class ImageEditor {
}
onGlobalKeyUp(e) {
- if (e.key === 'Alt') {
+ if (e.key == 'Alt') {
this.altDown = false;
this.handleAltUp();
}