Skip to content
Merged
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
242 changes: 242 additions & 0 deletions src/wwwroot/js/genpage/helpers/image_editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -837,6 +837,225 @@ 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 = 2;
this.shape = 'rectangle';
this.isDrawing = false;
this.startX = 0;
this.startY = 0;
this.currentX = 0;
this.currentY = 0;

let colorHTML = `
<div class="image-editor-tool-block">
<label>Color:&nbsp;</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:&nbsp;</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>Stroke:&nbsp;</label>
<input type="number" style="width: 40px;" class="auto-number id-stroke1" min="1" max="20" step="1" value="2">
<div class="auto-slider-range-wrapper" style="${getRangeStyle(2, 1, 20)}">
<input type="range" style="flex-grow: 2" class="auto-slider-range id-stroke2" min="1" max="20" step="1" value="2" 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.updateUI();
this.editor.redraw();
});

enableSliderForBox(this.configDiv.querySelector('.id-stroke-block'));
this.strokeNumber.addEventListener('change', () => { this.onConfigChange(); });

this.clearButton.addEventListener('click', () => {
this.clearAllShapes();
});

this.shapes = [];
}

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() {
this.shapes = [];
this.isDrawing = false;
this.editor.redraw();
}


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.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') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

newline after a }

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 && (this.shape === 'rectangle' || this.shape === 'circle')) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

== not ===, but also why is this check here at all?

let [mouseX, mouseY] = this.editor.canvasCoordToImageCoord(this.editor.mouseX, this.editor.mouseY);
this.currentX = mouseX;
this.currentY = mouseY;

// Only add if there's actual size
if (Math.abs(this.currentX - this.startX) > 1 || Math.abs(this.currentY - this.startY) > 1) {
this.shapes.push({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

format wonk

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.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.
*/
Expand Down Expand Up @@ -1243,6 +1462,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');
Expand Down Expand Up @@ -1813,6 +2033,7 @@ class ImageEditor {
this.ctx.restore();
}


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stray edit

redraw() {
if (!this.canvas) {
return;
Expand Down Expand Up @@ -1860,6 +2081,13 @@ 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);
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't have blank lines here

if (this.tools.shape && this.tools.shape.shapes) {
for (let shape of this.tools.shape.shapes) {
this.tools.shape.drawShape(shape);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shapes should either direct draw, or act as sub-layers on a layer. Doesn't really make sense to render in the main function here like this

}
}

this.activeTool.draw();
this.ctx.restore();
}
Expand All @@ -1874,6 +2102,13 @@ class ImageEditor {
layer.drawToBack(ctx, this.finalOffsetX, this.finalOffsetY, 1);
}
}

if (this.tools.shape && this.tools.shape.shapes) {
for (let shape of this.tools.shape.shapes) {
this.tools.shape.drawShapeToCanvas(ctx, shape, 1);
}
}

return canvas.toDataURL(format);
}

Expand All @@ -1899,6 +2134,13 @@ class ImageEditor {
layer.drawToBack(ctx, -minX, -minY, 1);
}
}

if (this.tools.shape && this.tools.shape.shapes) {
for (let shape of this.tools.shape.shapes) {
this.tools.shape.drawShapeToCanvas(ctx, shape, 1, -minX, -minY);
}
}

return canvas.toDataURL(format);
}

Expand Down