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(); }