// 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(); const nextLeft = clamp( left / zoom, MediaEditorFabricCropRect.PADDING / zoom, (canvasWidth - width - MediaEditorFabricCropRect.PADDING) / zoom ); const nextTop = clamp( top / zoom, MediaEditorFabricCropRect.PADDING / zoom, (canvasHeight - height - MediaEditorFabricCropRect.PADDING) / zoom ); this.set('left', nextLeft); this.set('top', nextTop); 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, cursorStyle: 'nwse-resize', 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, cursorStyle: 'nesw-resize', 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, cursorStyle: 'nesw-resize', 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, cursorStyle: 'nwse-resize', 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); }