-
-
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 8 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,344 @@ 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.startLayerX = 0; | ||
| this.startLayerY = 0; | ||
| this.currentLayerX = 0; | ||
| this.currentLayerY = 0; | ||
| this.bufferLayer = null; | ||
| this.hasDrawn = false; | ||
|
|
||
| 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>`; | ||
|
|
||
| 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(); | ||
| } | ||
|
|
||
| draw() { | ||
| if (this.isDrawing) { | ||
| this.editor.ctx.save(); | ||
| this.editor.ctx.strokeStyle = this.color; | ||
| this.editor.ctx.lineWidth = this.strokeWidth * this.editor.zoomLevel; | ||
| this.editor.ctx.setLineDash([]); | ||
| this.editor.ctx.beginPath(); | ||
| if (this.shape === 'rectangle') { | ||
| let [x, y] = this.editor.imageCoordToCanvasCoord(Math.min(this.startX, this.currentX), Math.min(this.startY, this.currentY)); | ||
| let [w, h] = [Math.abs(this.currentX - this.startX) * this.editor.zoomLevel, Math.abs(this.currentY - this.startY) * this.editor.zoomLevel]; | ||
| this.editor.ctx.rect(x, y, w, h); | ||
| } | ||
| else if (this.shape === 'circle') { | ||
| let [cx, cy] = this.editor.imageCoordToCanvasCoord((this.startX + this.currentX) / 2, (this.startY + this.currentY) / 2); | ||
| let radius = Math.sqrt(Math.pow(this.currentX - this.startX, 2) + Math.pow(this.currentY - this.startY, 2)) / 2 * this.editor.zoomLevel; | ||
| this.editor.ctx.arc(cx, cy, radius, 0, 2 * Math.PI); | ||
| } | ||
| this.editor.ctx.stroke(); | ||
| this.editor.ctx.restore(); | ||
| } | ||
| } | ||
|
|
||
| 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) { | ||
| if (e.button !== 0 && e.button !== undefined) { | ||
|
||
| return; | ||
| } | ||
| if (this.isDrawing) { | ||
| this.finishDrawing(); | ||
| } | ||
| this.editor.updateMousePosFrom(e); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm pretty sure this is redundant |
||
| 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; | ||
| this.hasDrawn = false; | ||
| let target = this.editor.activeLayer; | ||
| if (!target) { | ||
| this.bufferLayer = null; | ||
| this.isDrawing = false; | ||
| return; | ||
| } | ||
| let [layerX, layerY] = target.canvasCoordToLayerCoord(this.editor.mouseX, this.editor.mouseY); | ||
| 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) { | ||
| this.editor.updateMousePosFrom(e); | ||
| let [mouseX, mouseY] = this.editor.canvasCoordToImageCoord(this.editor.mouseX, this.editor.mouseY); | ||
| this.currentX = mouseX; | ||
| this.currentY = mouseY; | ||
| let target = this.editor.activeLayer; | ||
| if (target) { | ||
| let [layerX, layerY] = target.canvasCoordToLayerCoord(this.editor.mouseX, this.editor.mouseY); | ||
| this.currentLayerX = layerX; | ||
| this.currentLayerY = layerY; | ||
| } | ||
| this.drawShape(); | ||
| return true; | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| onGlobalMouseMove(e) { | ||
| if (this.isDrawing) { | ||
| this.editor.updateMousePosFrom(e); | ||
| let [mouseX, mouseY] = this.editor.canvasCoordToImageCoord(this.editor.mouseX, this.editor.mouseY); | ||
| this.currentX = mouseX; | ||
| this.currentY = mouseY; | ||
| let target = this.editor.activeLayer; | ||
| if (target) { | ||
| let [layerX, layerY] = target.canvasCoordToLayerCoord(this.editor.mouseX, this.editor.mouseY); | ||
| this.currentLayerX = layerX; | ||
| this.currentLayerY = layerY; | ||
| } | ||
| this.drawShape(); | ||
| return true; | ||
| } | ||
| return false; | ||
|
||
| } | ||
|
|
||
| onMouseUp(e) { | ||
| if (e.button !== 0 && e.button !== undefined) { | ||
| return; | ||
| } | ||
| if (!this.isDrawing) { | ||
| return; | ||
| } | ||
| this.editor.updateMousePosFrom(e); | ||
| let [mouseX, mouseY] = this.editor.canvasCoordToImageCoord(this.editor.mouseX, this.editor.mouseY); | ||
| this.currentX = mouseX; | ||
| this.currentY = mouseY; | ||
| let target = this.editor.activeLayer; | ||
| if (target) { | ||
| let [layerX, layerY] = target.canvasCoordToLayerCoord(this.editor.mouseX, this.editor.mouseY); | ||
| this.currentLayerX = layerX; | ||
| this.currentLayerY = layerY; | ||
| } | ||
| this.finishDrawing(); | ||
| } | ||
|
|
||
| onGlobalMouseUp(e) { | ||
| if (e.button !== 0 && e.button !== undefined) { | ||
| return false; | ||
| } | ||
| if (this.isDrawing) { | ||
| this.editor.updateMousePosFrom(e); | ||
| 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 layerX1 = Math.min(this.startLayerX, this.currentLayerX); | ||
| const layerY1 = Math.min(this.startLayerY, this.currentLayerY); | ||
| const width = Math.abs(this.currentLayerX - this.startLayerX); | ||
| const height = Math.abs(this.currentLayerY - this.startLayerY); | ||
| if (width < 0.5 && height < 0.5) { | ||
| this.bufferLayer.hasAnyContent = false; | ||
| this.hasDrawn = false; | ||
| this.editor.redraw(); | ||
| return; | ||
| } | ||
| this.bufferLayer.ctx.save(); | ||
| this.bufferLayer.ctx.strokeStyle = this.color; | ||
| this.bufferLayer.ctx.lineWidth = this.strokeWidth; | ||
| this.bufferLayer.ctx.setLineDash([]); | ||
| this.bufferLayer.ctx.beginPath(); | ||
| if (this.shape === 'rectangle') { | ||
| this.bufferLayer.ctx.rect(layerX1, layerY1, width, height); | ||
| } | ||
| else if (this.shape === 'circle') { | ||
| const cx = layerX1 + width / 2; | ||
| const cy = layerY1 + height / 2; | ||
| const radius = Math.sqrt(width * width + height * height) / 2; | ||
| this.bufferLayer.ctx.arc(cx, cy, radius, 0, 2 * Math.PI); | ||
| } | ||
| this.bufferLayer.ctx.stroke(); | ||
| 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. | ||
| */ | ||
|
|
@@ -1243,6 +1581,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 +2152,7 @@ class ImageEditor { | |
| this.ctx.restore(); | ||
| } | ||
|
|
||
|
|
||
|
||
| redraw() { | ||
| if (!this.canvas) { | ||
| return; | ||
|
|
||
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.
I see at least 3x copies of this same core shape drawing code -- it should probably be unified to a single function that gets called from the different places,
drawShapeToCanvaslooks like the most generalized