-
-
Notifications
You must be signed in to change notification settings - Fork 323
Uploading for review for shape tool #1086
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 5 commits
7819edb
fc02142
95fc127
55f461b
ebed25a
6cb7908
7e0b3be
80df440
44867b7
9b11fd3
35713ae
7fff663
0ea4d71
8bc1afc
179597f
ae577c8
1b31aeb
54b2ce0
52d449e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 = ` | ||
| <div class="image-editor-tool-block"> | ||
| <label>Color: </label> | ||
| <input type="text" class="auto-number id-col1" style="width:75px;flex-grow:0;" value="#ff0000"> | ||
| <input type="color" class="id-col2" value="#ff0000"> | ||
| <button class="basic-button id-col3">Pick</button> | ||
| </div>`; | ||
|
|
||
| let shapeHTML = ` | ||
| <div class="image-editor-tool-block"> | ||
| <label>Shape: </label> | ||
| <select class="id-shape" style="width:100px;"> | ||
| <option value="rectangle">Rectangle</option> | ||
| <option value="circle">Circle</option> | ||
| </select> | ||
| </div>`; | ||
|
|
||
| let strokeHTML = ` | ||
| <div class="image-editor-tool-block id-stroke-block"> | ||
| <label>Width: </label> | ||
| <input type="number" style="width: 40px;" class="auto-number id-stroke1" min="1" max="20" step="1" value="4"> | ||
| <div class="auto-slider-range-wrapper" style="${getRangeStyle(4, 1, 20)}"> | ||
| <input type="range" style="flex-grow: 2" class="auto-slider-range id-stroke2" min="1" max="20" step="1" value="4" oninput="updateRangeStyle(arguments[0])" onchange="updateRangeStyle(arguments[0])"> | ||
| </div> | ||
| </div>`; | ||
|
|
||
| let controlsHTML = ` | ||
| <div class="image-editor-tool-block"> | ||
| <button class="basic-button id-clear">Clear</button> | ||
| </div>`; | ||
|
|
||
| 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); | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this shouldn't be tracked