mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-05-08 08:58:38 +01:00
Add image editor
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { fabric } from 'fabric';
|
||||
import { clamp } from 'lodash';
|
||||
|
||||
export class MediaEditorFabricCropRect extends fabric.Rect {
|
||||
static PADDING = 4;
|
||||
|
||||
constructor(options?: fabric.IRectOptions) {
|
||||
super({
|
||||
fill: undefined,
|
||||
lockScalingFlip: true,
|
||||
...(options || {}),
|
||||
});
|
||||
|
||||
this.on('modified', this.containBounds.bind(this));
|
||||
}
|
||||
|
||||
private containBounds() {
|
||||
if (!this.canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
const zoom = this.canvas.getZoom() || 1;
|
||||
|
||||
const { left, top, height, width } = this.getBoundingRect();
|
||||
|
||||
const canvasHeight = this.canvas.getHeight();
|
||||
const canvasWidth = this.canvas.getWidth();
|
||||
|
||||
if (height > canvasHeight || width > canvasWidth) {
|
||||
this.canvas.discardActiveObject();
|
||||
} else {
|
||||
this.set(
|
||||
'left',
|
||||
clamp(
|
||||
left / zoom,
|
||||
MediaEditorFabricCropRect.PADDING / zoom,
|
||||
(canvasWidth - width - MediaEditorFabricCropRect.PADDING) / zoom
|
||||
)
|
||||
);
|
||||
this.set(
|
||||
'top',
|
||||
clamp(
|
||||
top / zoom,
|
||||
MediaEditorFabricCropRect.PADDING / zoom,
|
||||
(canvasHeight - height - MediaEditorFabricCropRect.PADDING) / zoom
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
this.setCoords();
|
||||
}
|
||||
|
||||
override render(ctx: CanvasRenderingContext2D): void {
|
||||
super.render(ctx);
|
||||
|
||||
const bounds = this.getBoundingRect();
|
||||
|
||||
const zoom = this.canvas?.getZoom() || 1;
|
||||
const canvasWidth = (this.canvas?.getWidth() || 0) / zoom;
|
||||
const canvasHeight = (this.canvas?.getHeight() || 0) / zoom;
|
||||
const height = bounds.height / zoom;
|
||||
const left = bounds.left / zoom;
|
||||
const top = bounds.top / zoom;
|
||||
const width = bounds.width / zoom;
|
||||
|
||||
ctx.save();
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.4)';
|
||||
// top
|
||||
ctx.fillRect(0, 0, canvasWidth, top);
|
||||
// left
|
||||
ctx.fillRect(0, top, left, height);
|
||||
// bottom
|
||||
ctx.fillRect(0, height + top, canvasWidth, canvasHeight - top);
|
||||
// right
|
||||
ctx.fillRect(left + width, top, canvasWidth - left, height);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
MediaEditorFabricCropRect.prototype.controls = {
|
||||
tl: new fabric.Control({
|
||||
x: -0.5,
|
||||
y: -0.5,
|
||||
actionHandler: fabric.controlsUtils.scalingEqually,
|
||||
render: (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
left: number,
|
||||
top: number,
|
||||
_,
|
||||
rect: fabric.Object
|
||||
) => {
|
||||
const WIDTH = getMinSize(rect.width);
|
||||
|
||||
ctx.save();
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.strokeStyle = '#fff';
|
||||
ctx.lineWidth = 3;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(left - 2, top + WIDTH);
|
||||
ctx.lineTo(left - 2, top - 2);
|
||||
ctx.lineTo(left + WIDTH, top - 2);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.restore();
|
||||
},
|
||||
}),
|
||||
tr: new fabric.Control({
|
||||
x: 0.5,
|
||||
y: -0.5,
|
||||
actionHandler: fabric.controlsUtils.scalingEqually,
|
||||
render: (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
left: number,
|
||||
top: number,
|
||||
_,
|
||||
rect: fabric.Object
|
||||
) => {
|
||||
const WIDTH = getMinSize(rect.width);
|
||||
|
||||
ctx.save();
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.strokeStyle = '#fff';
|
||||
ctx.lineWidth = 3;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(left + 2, top + WIDTH);
|
||||
ctx.lineTo(left + 2, top - 2);
|
||||
ctx.lineTo(left - WIDTH, top - 2);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.restore();
|
||||
},
|
||||
}),
|
||||
bl: new fabric.Control({
|
||||
x: -0.5,
|
||||
y: 0.5,
|
||||
actionHandler: fabric.controlsUtils.scalingEqually,
|
||||
render: (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
left: number,
|
||||
top: number,
|
||||
_,
|
||||
rect: fabric.Object
|
||||
) => {
|
||||
const WIDTH = getMinSize(rect.width);
|
||||
|
||||
ctx.save();
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.strokeStyle = '#fff';
|
||||
ctx.lineWidth = 3;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(left - 2, top - WIDTH);
|
||||
ctx.lineTo(left - 2, top + 2);
|
||||
ctx.lineTo(left + WIDTH, top + 2);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.restore();
|
||||
},
|
||||
}),
|
||||
br: new fabric.Control({
|
||||
x: 0.5,
|
||||
y: 0.5,
|
||||
actionHandler: fabric.controlsUtils.scalingEqually,
|
||||
render: (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
left: number,
|
||||
top: number,
|
||||
_,
|
||||
rect: fabric.Object
|
||||
) => {
|
||||
const WIDTH = getMinSize(rect.width);
|
||||
|
||||
ctx.save();
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.strokeStyle = '#fff';
|
||||
ctx.lineWidth = 3;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(left + 2, top - WIDTH);
|
||||
ctx.lineTo(left + 2, top + 2);
|
||||
ctx.lineTo(left - WIDTH, top + 2);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.restore();
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
MediaEditorFabricCropRect.prototype.excludeFromExport = true;
|
||||
MediaEditorFabricCropRect.prototype.borderColor = '#ffffff';
|
||||
MediaEditorFabricCropRect.prototype.cornerColor = '#ffffff';
|
||||
|
||||
function getMinSize(width: number | undefined): number {
|
||||
return Math.min(width || 24, 24);
|
||||
}
|
||||
Reference in New Issue
Block a user