@uploadcare/file-uploader
Version:
Building blocks for Uploadcare products integration
730 lines (678 loc) • 18.8 kB
JavaScript
// @ts-check
import { MIN_CROP_SIZE, THUMB_CORNER_SIZE, THUMB_OFFSET, THUMB_SIDE_SIZE } from './cropper-constants.js';
/**
* @param {SVGElement} node
* @param {{ [key: String]: String | Number }} attrs
*/
export function setSvgNodeAttrs(node, attrs) {
for (let p in attrs) node.setAttributeNS(null, p, attrs[p].toString());
}
/**
* @param {String} name
* @param {{ [key: String]: String | Number }} attrs
* @returns {SVGElement}
*/
export function createSvgNode(name, attrs = {}) {
let node = document.createElementNS('http://www.w3.org/2000/svg', name);
setSvgNodeAttrs(node, attrs);
return node;
}
/**
* @param {import('./types.js').Rectangle} rect
* @param {import('./types.js').Direction} direction
* @param {number} sizeMultiplier
*/
export function cornerPath(rect, direction, sizeMultiplier) {
let { x, y, width, height } = rect;
let wMul = direction.includes('w') ? 0 : 1;
let hMul = direction.includes('n') ? 0 : 1;
let xSide = [-1, 1][wMul];
let ySide = [-1, 1][hMul];
let p1 = [
x + wMul * width + THUMB_OFFSET * xSide,
y + hMul * height + THUMB_OFFSET * ySide - THUMB_CORNER_SIZE * sizeMultiplier * ySide,
];
let p2 = [x + wMul * width + THUMB_OFFSET * xSide, y + hMul * height + THUMB_OFFSET * ySide];
let p3 = [
x + wMul * width - THUMB_CORNER_SIZE * sizeMultiplier * xSide + THUMB_OFFSET * xSide,
y + hMul * height + THUMB_OFFSET * ySide,
];
let path = `M ${p1[0]} ${p1[1]} L ${p2[0]} ${p2[1]} L ${p3[0]} ${p3[1]}`;
let center = p2;
return {
d: path,
center,
};
}
/**
* @param {import('./types.js').Rectangle} rect
* @param {Extract<import('./types.js').Direction, 'n' | 's' | 'w' | 'e'>} direction
* @param {number} sizeMultiplier
*/
export function sidePath(rect, direction, sizeMultiplier) {
let { x, y, width, height } = rect;
let wMul = ['n', 's'].includes(direction)
? 0.5
: { w: 0, e: 1 }[/** @type {Extract<import('./types.js').Direction, 'w' | 'e'>} */ (direction)];
let hMul = ['w', 'e'].includes(direction)
? 0.5
: { n: 0, s: 1 }[/** @type {Extract<import('./types.js').Direction, 'n' | 's'>} */ (direction)];
let xSide = [-1, 1][wMul];
let ySide = [-1, 1][hMul];
let p1, p2;
if (['n', 's'].includes(direction)) {
p1 = [x + wMul * width - (THUMB_SIDE_SIZE * sizeMultiplier) / 2, y + hMul * height + THUMB_OFFSET * ySide];
p2 = [x + wMul * width + (THUMB_SIDE_SIZE * sizeMultiplier) / 2, y + hMul * height + THUMB_OFFSET * ySide];
} else {
p1 = [x + wMul * width + THUMB_OFFSET * xSide, y + hMul * height - (THUMB_SIDE_SIZE * sizeMultiplier) / 2];
p2 = [x + wMul * width + THUMB_OFFSET * xSide, y + hMul * height + (THUMB_SIDE_SIZE * sizeMultiplier) / 2];
}
let path = `M ${p1[0]} ${p1[1]} L ${p2[0]} ${p2[1]}`;
let center = [p2[0] - (p2[0] - p1[0]) / 2, p2[1] - (p2[1] - p1[1]) / 2];
return { d: path, center };
}
/** @param {import('./types.js').Direction} direction */
export function thumbCursor(direction) {
if (direction === '') {
return 'move';
}
if (['e', 'w'].includes(direction)) {
return 'ew-resize';
}
if (['n', 's'].includes(direction)) {
return 'ns-resize';
}
if (['nw', 'se'].includes(direction)) {
return 'nwse-resize';
}
return 'nesw-resize';
}
/**
* @param {{
* rect: import('./types.js').Rectangle;
* delta: [Number, Number];
* imageBox: import('./types.js').Rectangle;
* }} options
*/
export function moveRect({ rect, delta: [dx, dy], imageBox }) {
return constraintRect(
{
...rect,
x: rect.x + dx,
y: rect.y + dy,
},
imageBox,
);
}
/**
* @param {import('./types.js').Rectangle} rect1
* @param {import('./types.js').Rectangle} rect2
*/
export function constraintRect(rect1, rect2) {
let { x } = rect1;
let { y } = rect1;
if (rect1.x < rect2.x) {
x = rect2.x;
} else if (rect1.x + rect1.width > rect2.x + rect2.width) {
x = rect2.x + rect2.width - rect1.width;
}
if (rect1.y < rect2.y) {
y = rect2.y;
} else if (rect1.y + rect1.height > rect2.y + rect2.height) {
y = rect2.y + rect2.height - rect1.height;
}
return {
...rect1,
x,
y,
};
}
/**
* @param {{
* rect: import('./types.js').Rectangle;
* delta: [Number, Number];
* aspectRatio?: number;
* imageBox: import('./types.js').Rectangle;
* }} options
*/
function resizeNorth({ rect, delta, aspectRatio, imageBox }) {
const [, dy] = delta;
let { y, width, height } = rect;
y += dy;
height -= dy;
if (aspectRatio) {
width = height * aspectRatio;
}
let x = rect.x + rect.width / 2 - width / 2;
if (y <= imageBox.y) {
y = imageBox.y;
height = rect.y + rect.height - y;
if (aspectRatio) {
width = height * aspectRatio;
x = rect.x + rect.width / 2 - width / 2;
}
}
if (x <= imageBox.x) {
x = imageBox.x;
y = rect.y + rect.height - height;
}
if (x + width >= imageBox.x + imageBox.width) {
x = Math.max(imageBox.x, imageBox.x + imageBox.width - width);
width = imageBox.x + imageBox.width - x;
if (aspectRatio) {
height = width / aspectRatio;
}
y = rect.y + rect.height - height;
}
if (height < MIN_CROP_SIZE) {
height = MIN_CROP_SIZE;
if (aspectRatio) {
width = height * aspectRatio;
x = rect.x + rect.width / 2 - width / 2;
}
y = rect.y + rect.height - height;
}
if (width < MIN_CROP_SIZE) {
width = MIN_CROP_SIZE;
if (aspectRatio) {
height = width / aspectRatio;
x = rect.x + rect.width / 2 - width / 2;
}
y = rect.y + rect.height - height;
}
return { x, y, width, height };
}
/**
* @param {{
* rect: import('./types.js').Rectangle;
* delta: [Number, Number];
* aspectRatio?: number;
* imageBox: import('./types.js').Rectangle;
* }} options
*/
function resizeWest({ rect, delta, aspectRatio, imageBox }) {
const [dx] = delta;
let { x, width, height } = rect;
x += dx;
width -= dx;
if (aspectRatio) {
height = width / aspectRatio;
}
let y = rect.y + rect.height / 2 - height / 2;
if (x <= imageBox.x) {
x = imageBox.x;
width = rect.x + rect.width - x;
if (aspectRatio) {
height = width / aspectRatio;
y = rect.y + rect.height / 2 - height / 2;
}
}
if (y <= imageBox.y) {
y = imageBox.y;
x = rect.x + rect.width - width;
}
if (y + height >= imageBox.y + imageBox.height) {
y = Math.max(imageBox.y, imageBox.y + imageBox.height - height);
height = imageBox.y + imageBox.height - y;
if (aspectRatio) {
width = height * aspectRatio;
}
x = rect.x + rect.width - width;
}
if (height < MIN_CROP_SIZE) {
height = MIN_CROP_SIZE;
if (aspectRatio) {
width = height * aspectRatio;
}
y = rect.y + rect.height / 2 - height / 2;
x = rect.x + rect.width - width;
}
if (width < MIN_CROP_SIZE) {
width = MIN_CROP_SIZE;
if (aspectRatio) {
height = width / aspectRatio;
}
y = rect.y + rect.height / 2 - height / 2;
x = rect.x + rect.width - width;
}
return { x, y, width, height };
}
/**
* @param {{
* rect: import('./types.js').Rectangle;
* delta: [Number, Number];
* aspectRatio?: number;
* imageBox: import('./types.js').Rectangle;
* }} options
*/
function resizeSouth({ rect, delta, aspectRatio, imageBox }) {
const [, dy] = delta;
let { y, width, height } = rect;
height += dy;
if (aspectRatio) {
width = height * aspectRatio;
}
let x = rect.x + rect.width / 2 - width / 2;
if (y + height >= imageBox.y + imageBox.height) {
height = imageBox.y + imageBox.height - y;
if (aspectRatio) {
width = height * aspectRatio;
}
x = rect.x + rect.width / 2 - width / 2;
}
if (x <= imageBox.x) {
x = imageBox.x;
y = rect.y;
}
if (x + width >= imageBox.x + imageBox.width) {
x = Math.max(imageBox.x, imageBox.x + imageBox.width - width);
width = imageBox.x + imageBox.width - x;
if (aspectRatio) {
height = width / aspectRatio;
}
y = rect.y;
}
if (height < MIN_CROP_SIZE) {
height = MIN_CROP_SIZE;
if (aspectRatio) {
width = height * aspectRatio;
}
x = rect.x + rect.width / 2 - width / 2;
}
if (width < MIN_CROP_SIZE) {
width = MIN_CROP_SIZE;
if (aspectRatio) {
height = width / aspectRatio;
}
x = rect.x + rect.width / 2 - width / 2;
}
return { x, y, width, height };
}
/**
* @param {{
* rect: import('./types.js').Rectangle;
* delta: [Number, Number];
* aspectRatio?: number;
* imageBox: import('./types.js').Rectangle;
* }} options
*/
function resizeEast({ rect, delta, aspectRatio, imageBox }) {
const [dx] = delta;
let { x, width, height } = rect;
width += dx;
if (aspectRatio) {
height = width / aspectRatio;
}
let y = rect.y + rect.height / 2 - height / 2;
if (x + width >= imageBox.x + imageBox.width) {
width = imageBox.x + imageBox.width - x;
if (aspectRatio) {
height = width / aspectRatio;
}
y = rect.y + rect.height / 2 - height / 2;
}
if (y <= imageBox.y) {
y = imageBox.y;
x = rect.x;
}
if (y + height >= imageBox.y + imageBox.height) {
y = Math.max(imageBox.y, imageBox.y + imageBox.height - height);
height = imageBox.y + imageBox.height - y;
if (aspectRatio) {
width = height * aspectRatio;
}
x = rect.x;
}
if (height < MIN_CROP_SIZE) {
height = MIN_CROP_SIZE;
if (aspectRatio) {
width = height * aspectRatio;
}
y = rect.y + rect.height / 2 - height / 2;
}
if (width < MIN_CROP_SIZE) {
width = MIN_CROP_SIZE;
if (aspectRatio) {
height = width / aspectRatio;
}
y = rect.y + rect.height / 2 - height / 2;
}
return { x, y, width, height };
}
/**
* @param {{
* rect: import('./types.js').Rectangle;
* delta: [Number, Number];
* aspectRatio?: number;
* imageBox: import('./types.js').Rectangle;
* }} options
*/
function resizeNorthWest({ rect, delta, aspectRatio, imageBox }) {
let [dx, dy] = delta;
let { x, y, width, height } = rect;
if (x + dx < imageBox.x) {
dx = imageBox.x - x;
}
if (y + dy < imageBox.y) {
dy = imageBox.y - y;
}
x += dx;
width -= dx;
y += dy;
height -= dy;
if (aspectRatio && Math.abs(width / height) > aspectRatio) {
dy = width / aspectRatio - height;
height += dy;
y -= dy;
if (y <= imageBox.y) {
height = height - (imageBox.y - y);
width = height * aspectRatio;
x = rect.x + rect.width - width;
y = imageBox.y;
}
} else if (aspectRatio) {
dx = height * aspectRatio - width;
width = width + dx;
x -= dx;
if (x <= imageBox.x) {
width = width - (imageBox.x - x);
height = width / aspectRatio;
x = imageBox.x;
y = rect.y + rect.height - height;
}
}
if (height < MIN_CROP_SIZE) {
height = MIN_CROP_SIZE;
if (aspectRatio) {
width = height * aspectRatio;
}
x = rect.x + rect.width - width;
y = rect.y + rect.height - height;
}
if (width < MIN_CROP_SIZE) {
width = MIN_CROP_SIZE;
if (aspectRatio) {
height = width / aspectRatio;
}
x = rect.x + rect.width - width;
y = rect.y + rect.height - height;
}
return { x, y, width, height };
}
/**
* @param {{
* rect: import('./types.js').Rectangle;
* delta: [Number, Number];
* aspectRatio?: number;
* imageBox: import('./types.js').Rectangle;
* }} options
*/
function resizeNorthEast({ rect, delta, aspectRatio, imageBox }) {
let [dx, dy] = delta;
let { x, y, width, height } = rect;
if (x + width + dx > imageBox.x + imageBox.width) {
dx = imageBox.x + imageBox.width - x - width;
}
if (y + dy < imageBox.y) {
dy = imageBox.y - y;
}
width += dx;
y += dy;
height -= dy;
if (aspectRatio && Math.abs(width / height) > aspectRatio) {
dy = width / aspectRatio - height;
height += dy;
y -= dy;
if (y <= imageBox.y) {
height = height - (imageBox.y - y);
width = height * aspectRatio;
x = rect.x;
y = imageBox.y;
}
} else if (aspectRatio) {
dx = height * aspectRatio - width;
width += dx;
if (x + width >= imageBox.x + imageBox.width) {
width = imageBox.x + imageBox.width - x;
height = width / aspectRatio;
x = imageBox.x + imageBox.width - width;
y = rect.y + rect.height - height;
}
}
if (height < MIN_CROP_SIZE) {
height = MIN_CROP_SIZE;
if (aspectRatio) {
width = height * aspectRatio;
}
y = rect.y + rect.height - height;
}
if (width < MIN_CROP_SIZE) {
width = MIN_CROP_SIZE;
if (aspectRatio) {
height = width / aspectRatio;
}
y = rect.y + rect.height - height;
}
return { x, y, width, height };
}
/**
* @param {{
* rect: import('./types.js').Rectangle;
* delta: [Number, Number];
* aspectRatio?: number;
* imageBox: import('./types.js').Rectangle;
* }} options
*/
function resizeSouthWest({ rect, delta, aspectRatio, imageBox }) {
let [dx, dy] = delta;
let { x, y, width, height } = rect;
if (x + dx < imageBox.x) {
dx = imageBox.x - x;
}
if (y + height + dy > imageBox.y + imageBox.height) {
dy = imageBox.y + imageBox.height - y - height;
}
x += dx;
width -= dx;
height += dy;
if (aspectRatio && Math.abs(width / height) > aspectRatio) {
dy = width / aspectRatio - height;
height += dy;
if (y + height >= imageBox.y + imageBox.height) {
height = imageBox.y + imageBox.height - y;
width = height * aspectRatio;
x = rect.x + rect.width - width;
y = imageBox.y + imageBox.height - height;
}
} else if (aspectRatio) {
dx = height * aspectRatio - width;
width += dx;
x -= dx;
if (x <= imageBox.x) {
width = width - (imageBox.x - x);
height = width / aspectRatio;
x = imageBox.x;
y = rect.y;
}
}
if (height < MIN_CROP_SIZE) {
height = MIN_CROP_SIZE;
if (aspectRatio) {
width = height * aspectRatio;
}
x = rect.x + rect.width - width;
}
if (width < MIN_CROP_SIZE) {
width = MIN_CROP_SIZE;
if (aspectRatio) {
height = width / aspectRatio;
}
x = rect.x + rect.width - width;
}
return { x, y, width, height };
}
/**
* @param {{
* rect: import('./types.js').Rectangle;
* delta: [Number, Number];
* aspectRatio?: number;
* imageBox: import('./types.js').Rectangle;
* }} options
*/
function resizeSouthEast({ rect, delta, aspectRatio, imageBox }) {
let [dx, dy] = delta;
let { x, y, width, height } = rect;
if (x + width + dx > imageBox.x + imageBox.width) {
dx = imageBox.x + imageBox.width - x - width;
}
if (y + height + dy > imageBox.y + imageBox.height) {
dy = imageBox.y + imageBox.height - y - height;
}
width += dx;
height += dy;
if (aspectRatio && Math.abs(width / height) > aspectRatio) {
dy = width / aspectRatio - height;
height += dy;
if (y + height >= imageBox.y + imageBox.height) {
height = imageBox.y + imageBox.height - y;
width = height * aspectRatio;
x = rect.x;
y = imageBox.y + imageBox.height - height;
}
} else if (aspectRatio) {
dx = height * aspectRatio - width;
width += dx;
if (x + width >= imageBox.x + imageBox.width) {
width = imageBox.x + imageBox.width - x;
height = width / aspectRatio;
x = imageBox.x + imageBox.width - width;
y = rect.y;
}
}
if (height < MIN_CROP_SIZE) {
height = MIN_CROP_SIZE;
if (aspectRatio) {
width = height * aspectRatio;
}
}
if (width < MIN_CROP_SIZE) {
width = MIN_CROP_SIZE;
if (aspectRatio) {
height = width / aspectRatio;
}
}
return { x, y, width, height };
}
/**
* @param {{
* rect: import('./types.js').Rectangle;
* delta: [Number, Number];
* direction: import('./types.js').Direction;
* aspectRatio?: number;
* imageBox: import('./types.js').Rectangle;
* }} options
*/
export function resizeRect({ direction, ...rest }) {
switch (direction) {
case 'n':
return resizeNorth(rest);
case 'w':
return resizeWest(rest);
case 's':
return resizeSouth(rest);
case 'e':
return resizeEast(rest);
case 'nw':
return resizeNorthWest(rest);
case 'ne':
return resizeNorthEast(rest);
case 'sw':
return resizeSouthWest(rest);
case 'se':
return resizeSouthEast(rest);
default:
return rest.rect;
}
}
/**
* @param {import('./types.js').Rectangle} rect
* @param {[Number, Number]} point
*/
export function rectContainsPoint(rect, [x, y]) {
return rect.x <= x && x <= rect.x + rect.width && rect.y <= y && y <= rect.y + rect.height;
}
/**
* @param {import('./types.js').Rectangle} rect1
* @param {import('./types.js').Rectangle} rect2
*/
export function isRectInsideRect(rect1, rect2) {
return (
rect1.x >= rect2.x &&
rect1.y >= rect2.y &&
rect1.x + rect1.width <= rect2.x + rect2.width &&
rect1.y + rect1.height <= rect2.y + rect2.height
);
}
/**
* @param {import('./types.js').Rectangle} rect
* @param {number} aspectRatio
*/
export function isRectMatchesAspectRatio(rect, aspectRatio) {
const THRESHOLD = 0.1;
return Math.abs(rect.width / rect.height - aspectRatio) < THRESHOLD;
}
/**
* @param {import('./types.js').ImageSize} imageSize
* @param {Number} angle
* @returns {import('./types.js').ImageSize}
*/
export function rotateSize({ width, height }, angle) {
let swap = (angle / 90) % 2 !== 0;
return { width: swap ? height : width, height: swap ? width : height };
}
/**
* @param {number} width
* @param {number} height
* @param {number} aspectRatio
*/
export function calculateMaxCenteredCropFrame(width, height, aspectRatio) {
const imageAspectRatio = width / height;
let cropWidth, cropHeight;
if (imageAspectRatio > aspectRatio) {
cropWidth = Math.round(height * aspectRatio);
cropHeight = height;
} else {
cropWidth = width;
cropHeight = Math.round(width / aspectRatio);
}
const cropX = Math.round((width - cropWidth) / 2);
const cropY = Math.round((height - cropHeight) / 2);
if (cropX + cropWidth > width) {
cropWidth = width - cropX;
}
if (cropY + cropHeight > height) {
cropHeight = height - cropY;
}
return { x: cropX, y: cropY, width: cropWidth, height: cropHeight };
}
/**
* @param {import('./types.js').Rectangle} rect
* @returns {import('./types.js').Rectangle}
*/
export function roundRect(rect) {
return {
x: Math.round(rect.x),
y: Math.round(rect.y),
width: Math.round(rect.width),
height: Math.round(rect.height),
};
}
/**
* @param {Number} value
* @param {Number} min
* @param {Number} max
* @returns {Number}
*/
export function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}