diff --git a/src/wwwroot/js/genpage/helpers/image_editor.js b/src/wwwroot/js/genpage/helpers/image_editor.js index 04148f3c8..bf414a9b0 100644 --- a/src/wwwroot/js/genpage/helpers/image_editor.js +++ b/src/wwwroot/js/genpage/helpers/image_editor.js @@ -837,6 +837,284 @@ class ImageEditorToolBucket extends ImageEditorTool { } } +/** + * The Shape tool. + */ +class ImageEditorToolShape extends ImageEditorTool { + constructor(editor) { + super(editor, 'shape', 'shape', 'Shape', 'Create different shapes for AI editing.\nRectangle: Click and drag\nCircle: Click and drag\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.shapes = []; // Track shapes for undo functionality + this.shapesLayer = null; // Sub-layer for storing shapes + + let colorHTML = ` +
+ + + + +
`; + + let shapeHTML = ` +
+ + +
`; + + let strokeHTML = ` +
+ + +
+ +
+
`; + + let controlsHTML = ` +
+ +
`; + + this.configDiv.innerHTML = colorHTML + shapeHTML + strokeHTML + controlsHTML; + + 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.clearButton = this.configDiv.querySelector('.id-clear'); + + 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(); }); + + this.clearButton.addEventListener('click', () => { + this.clearAllShapes(); + }); + } + + 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(); + } + + clearAllShapes() { + if (this.shapesLayer && this.shapes.length > 0) { + // Save before edit for undo functionality + this.editor.activeLayer.saveBeforeEdit(); + + // Clear only the shapes sub-layer + this.shapesLayer.ctx.clearRect(0, 0, this.shapesLayer.canvas.width, this.shapesLayer.canvas.height); + this.shapesLayer.hasAnyContent = false; + this.shapes = []; + this.editor.markChanged(); + } + this.isDrawing = false; + this.editor.redraw(); + } + + drawShapeToSubLayer(shape) { + if (!this.shapesLayer) return; + + // Convert image coordinates to canvas coordinates first + let [canvasX1, canvasY1] = this.editor.imageCoordToCanvasCoord(shape.x, shape.y); + let [canvasX2, canvasY2] = this.editor.imageCoordToCanvasCoord(shape.x + shape.width, shape.y + shape.height); + + // Then convert to sub-layer coordinates + let [layerX1, layerY1] = this.shapesLayer.canvasCoordToLayerCoord(canvasX1, canvasY1); + let [layerX2, layerY2] = this.shapesLayer.canvasCoordToLayerCoord(canvasX2, canvasY2); + + this.shapesLayer.ctx.save(); + this.shapesLayer.ctx.strokeStyle = shape.color; + this.shapesLayer.ctx.lineWidth = shape.strokeWidth; + this.shapesLayer.ctx.setLineDash([]); + this.shapesLayer.ctx.beginPath(); + + if (shape.type === 'rectangle') { + let x = Math.min(layerX1, layerX2); + let y = Math.min(layerY1, layerY2); + let w = Math.abs(layerX2 - layerX1); + let h = Math.abs(layerY2 - layerY1); + this.shapesLayer.ctx.rect(x, y, w, h); + } + else if (shape.type === 'circle') { + let cx = (layerX1 + layerX2) / 2; + let cy = (layerY1 + layerY2) / 2; + let radius = Math.sqrt(Math.pow(layerX2 - layerX1, 2) + Math.pow(layerY2 - layerY1, 2)) / 2; + this.shapesLayer.ctx.arc(cx, cy, radius, 0, 2 * Math.PI); + } + + this.shapesLayer.ctx.stroke(); + this.shapesLayer.ctx.restore(); + this.shapesLayer.hasAnyContent = true; + console.log('Drew shape to sub-layer:', shape); + } + + + draw() { + if (this.isDrawing && (this.shape === 'rectangle' || this.shape === 'circle')) { + this.drawShape({ + type: this.shape, + x: this.startX, + y: this.startY, + width: this.currentX - this.startX, + height: this.currentY - this.startY, + color: this.color, + strokeWidth: this.strokeWidth + }); + } + } + + drawShape(shape) { + this.editor.ctx.save(); + this.editor.ctx.strokeStyle = shape.color; + this.editor.ctx.lineWidth = shape.strokeWidth * this.editor.zoomLevel; + this.editor.ctx.setLineDash([]); + this.editor.ctx.beginPath(); + + if (shape.type === 'rectangle') { + let [x, y] = this.editor.imageCoordToCanvasCoord(shape.x, shape.y); + let [w, h] = [shape.width * this.editor.zoomLevel, shape.height * this.editor.zoomLevel]; + this.editor.ctx.rect(x, y, w, h); + } + else if (shape.type === 'circle') { + let [cx, cy] = this.editor.imageCoordToCanvasCoord(shape.x + shape.width / 2, shape.y + shape.height / 2); + let radius = Math.sqrt(shape.width * shape.width + shape.height * shape.height) / 2 * this.editor.zoomLevel; + this.editor.ctx.arc(cx, cy, radius, 0, 2 * Math.PI); + } + + this.editor.ctx.stroke(); + this.editor.ctx.restore(); + } + + drawShapeToCanvas(ctx, shape, zoom, offsetX = 0, offsetY = 0) { + ctx.save(); + ctx.strokeStyle = shape.color; + ctx.lineWidth = shape.strokeWidth; + ctx.setLineDash([]); + ctx.beginPath(); + + if (shape.type === 'rectangle') { + let x = (shape.x + offsetX) * zoom; + let y = (shape.y + offsetY) * zoom; + let w = shape.width * zoom; + let h = shape.height * zoom; + ctx.rect(x, y, w, h); + } + else if (shape.type === 'circle') { + let cx = (shape.x + shape.width / 2 + offsetX) * zoom; + let cy = (shape.y + shape.height / 2 + offsetY) * zoom; + let radius = Math.sqrt(shape.width * shape.width + shape.height * shape.height) / 2 * zoom; + ctx.arc(cx, cy, radius, 0, 2 * Math.PI); + } + + ctx.stroke(); + ctx.restore(); + } + + + onMouseDown(e) { + let [mouseX, mouseY] = this.editor.canvasCoordToImageCoord(this.editor.mouseX, this.editor.mouseY); + + this.isDrawing = true; + this.startX = mouseX; + this.startY = mouseY; + this.currentX = mouseX; + this.currentY = mouseY; + } + + onMouseUp(e) { + if (this.isDrawing) { + let [mouseX, mouseY] = this.editor.canvasCoordToImageCoord(this.editor.mouseX, this.editor.mouseY); + this.currentX = mouseX; + this.currentY = mouseY; + + if (Math.abs(this.currentX - this.startX) > 1 || Math.abs(this.currentY - this.startY) > 1) { + // Create shapes sub-layer if it doesn't exist + if (!this.shapesLayer) { + this.shapesLayer = new ImageEditorLayer(this.editor, this.editor.activeLayer.canvas.width, this.editor.activeLayer.canvas.height, this.editor.activeLayer); + this.editor.activeLayer.childLayers.push(this.shapesLayer); + console.log('Created shapes sub-layer, childLayers count:', this.editor.activeLayer.childLayers.length); + } + + // Save before edit for undo functionality + this.editor.activeLayer.saveBeforeEdit(); + + let shape = { + type: this.shape, + x: Math.min(this.startX, this.currentX), + y: Math.min(this.startY, this.currentY), + width: Math.abs(this.currentX - this.startX), + height: Math.abs(this.currentY - this.startY), + color: this.color, + strokeWidth: this.strokeWidth + }; + + this.shapes.push(shape); + this.drawShapeToSubLayer(shape); + this.editor.markChanged(); + } + + this.isDrawing = false; + this.editor.redraw(); + } + } + + onGlobalMouseMove(e) { + if (this.isDrawing) { + let [mouseX, mouseY] = this.editor.canvasCoordToImageCoord(this.editor.mouseX, this.editor.mouseY); + this.currentX = mouseX; + this.currentY = mouseY; + this.editor.redraw(); + return true; + } + return false; + } +} + /** * The Color Picker tool, a special hidden sub-tool. */ @@ -1243,6 +1521,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'); @@ -1813,6 +2092,7 @@ class ImageEditor { this.ctx.restore(); } + redraw() { if (!this.canvas) { return; @@ -1860,6 +2140,7 @@ class ImageEditor { let [selectX, selectY] = this.imageCoordToCanvasCoord(this.selectX, this.selectY); this.drawSelectionBox(selectX, selectY, this.selectWidth * this.zoomLevel, this.selectHeight * this.zoomLevel, this.uiColor, 8 * this.zoomLevel, 0); } + this.activeTool.draw(); this.ctx.restore(); } @@ -1874,6 +2155,7 @@ class ImageEditor { layer.drawToBack(ctx, this.finalOffsetX, this.finalOffsetY, 1); } } + return canvas.toDataURL(format); } @@ -1899,6 +2181,7 @@ class ImageEditor { layer.drawToBack(ctx, -minX, -minY, 1); } } + return canvas.toDataURL(format); }