tui-image-editor
Version:
TOAST UI ImageEditor
565 lines (506 loc) • 17.5 kB
JavaScript
import { fabric } from 'fabric';
import forEach from 'tui-code-snippet/collection/forEach';
import extend from 'tui-code-snippet/object/extend';
import resizeHelper from '@/helper/shapeResizeHelper';
import { capitalizeString, flipObject, setCustomProperty, getCustomProperty } from '@/util';
const FILTER_OPTION_MAP = {
pixelate: 'blocksize',
blur: 'blur',
};
const POSITION_DIMENSION_MAP = {
x: 'width',
y: 'height',
};
const FILTER_NAME_VALUE_MAP = flipObject(FILTER_OPTION_MAP);
/**
* Cached canvas image element for fill image
* @type {boolean}
* @private
*/
let cachedCanvasImageElement = null;
/**
* Get background image of fill
* @param {fabric.Object} shapeObj - Shape object
* @returns {fabric.Image}
* @private
*/
export function getFillImageFromShape(shapeObj) {
const { patternSourceCanvas } = getCustomProperty(shapeObj, 'patternSourceCanvas');
const [fillImage] = patternSourceCanvas.getObjects();
return fillImage;
}
/**
* Reset the image position in the filter type fill area.
* @param {fabric.Object} shapeObj - Shape object
* @private
*/
export function rePositionFilterTypeFillImage(shapeObj) {
const { angle, flipX, flipY } = shapeObj;
const fillImage = getFillImageFromShape(shapeObj);
const rotatedShapeCornerDimension = getRotatedDimension(shapeObj);
const { right, bottom } = rotatedShapeCornerDimension;
let { width, height } = rotatedShapeCornerDimension;
const diffLeft = (width - shapeObj.width) / 2;
const diffTop = (height - shapeObj.height) / 2;
const cropX = shapeObj.left - shapeObj.width / 2 - diffLeft;
const cropY = shapeObj.top - shapeObj.height / 2 - diffTop;
let left = width / 2 - diffLeft;
let top = height / 2 - diffTop;
const fillImageMaxSize = Math.max(width, height) + Math.max(diffLeft, diffTop);
[left, top, width, height] = calculateFillImageDimensionOutsideCanvas({
shapeObj,
left,
top,
width,
height,
cropX,
cropY,
flipX,
flipY,
right,
bottom,
});
fillImage.set({
angle: flipX === flipY ? -angle : angle,
left,
top,
width,
height,
cropX,
cropY,
flipX,
flipY,
});
setCustomProperty(fillImage, { fillImageMaxSize });
}
/**
* Make filter option from fabric image
* @param {fabric.Image} imageObject - fabric image object
* @returns {object}
*/
export function makeFilterOptionFromFabricImage(imageObject) {
return imageObject.filters.map((filter) => {
const [key] = Object.keys(filter);
return {
[FILTER_NAME_VALUE_MAP[key]]: filter[key],
};
});
}
/**
* Calculate fill image position and size for out of Canvas
* @param {Object} options - options for position dimension calculate
* @param {fabric.Object} shapeObj - shape object
* @param {number} left - original left position
* @param {number} top - original top position
* @param {number} width - image width
* @param {number} height - image height
* @param {number} cropX - image cropX
* @param {number} cropY - image cropY
* @param {boolean} flipX - shape flipX
* @param {boolean} flipY - shape flipY
* @returns {Object}
*/
function calculateFillImageDimensionOutsideCanvas({
shapeObj,
left,
top,
width,
height,
cropX,
cropY,
flipX,
flipY,
right,
bottom,
}) {
const overflowAreaPositionFixer = (type, outDistance, imageLeft, imageTop) =>
calculateDistanceOverflowPart({
type,
outDistance,
shapeObj,
flipX,
flipY,
left: imageLeft,
top: imageTop,
});
const [originalWidth, originalHeight] = [width, height];
[left, top, width, height] = calculateDimensionLeftTopEdge(overflowAreaPositionFixer, {
left,
top,
width,
height,
cropX,
cropY,
});
[left, top, width, height] = calculateDimensionRightBottomEdge(overflowAreaPositionFixer, {
left,
top,
insideCanvasRealImageWidth: width,
insideCanvasRealImageHeight: height,
right,
bottom,
cropX,
cropY,
originalWidth,
originalHeight,
});
return [left, top, width, height];
}
/**
* Calculate fill image position and size for for right bottom edge
* @param {Function} overflowAreaPositionFixer - position fixer
* @param {Object} options - options for position dimension calculate
* @param {fabric.Object} shapeObj - shape object
* @param {number} left - original left position
* @param {number} top - original top position
* @param {number} width - image width
* @param {number} height - image height
* @param {number} right - image right
* @param {number} bottom - image bottom
* @param {number} cropX - image cropX
* @param {number} cropY - image cropY
* @param {boolean} originalWidth - image original width
* @param {boolean} originalHeight - image original height
* @returns {Object}
*/
function calculateDimensionRightBottomEdge(
overflowAreaPositionFixer,
{
left,
top,
insideCanvasRealImageWidth,
insideCanvasRealImageHeight,
right,
bottom,
cropX,
cropY,
originalWidth,
originalHeight,
}
) {
let [width, height] = [insideCanvasRealImageWidth, insideCanvasRealImageHeight];
const { width: canvasWidth, height: canvasHeight } = cachedCanvasImageElement;
if (right > canvasWidth && cropX > 0) {
width = originalWidth - Math.abs(right - canvasWidth);
}
if (bottom > canvasHeight && cropY > 0) {
height = originalHeight - Math.abs(bottom - canvasHeight);
}
const diff = {
x: (insideCanvasRealImageWidth - width) / 2,
y: (insideCanvasRealImageHeight - height) / 2,
};
forEach(['x', 'y'], (type) => {
const cropDistance2 = diff[type];
if (cropDistance2 > 0) {
[left, top] = overflowAreaPositionFixer(type, cropDistance2, left, top);
}
});
return [left, top, width, height];
}
/**
* Calculate fill image position and size for for left top
* @param {Function} overflowAreaPositionFixer - position fixer
* @param {Object} options - options for position dimension calculate
* @param {fabric.Object} shapeObj - shape object
* @param {number} left - original left position
* @param {number} top - original top position
* @param {number} width - image width
* @param {number} height - image height
* @param {number} cropX - image cropX
* @param {number} cropY - image cropY
* @returns {Object}
*/
function calculateDimensionLeftTopEdge(
overflowAreaPositionFixer,
{ left, top, width, height, cropX, cropY }
) {
const dimension = {
width,
height,
};
forEach(['x', 'y'], (type) => {
const cropDistance = type === 'x' ? cropX : cropY;
const compareSize = dimension[POSITION_DIMENSION_MAP[type]];
const standardSize = cachedCanvasImageElement[POSITION_DIMENSION_MAP[type]];
if (compareSize > standardSize) {
const outDistance = (compareSize - standardSize) / 2;
dimension[POSITION_DIMENSION_MAP[type]] = standardSize;
[left, top] = overflowAreaPositionFixer(type, outDistance, left, top);
}
if (cropDistance < 0) {
[left, top] = overflowAreaPositionFixer(type, cropDistance, left, top);
}
});
return [left, top, dimension.width, dimension.height];
}
/**
* Make fill property of dynamic pattern type
* @param {fabric.Image} canvasImage - canvas background image
* @param {Array} filterOption - filter option
* @param {fabric.StaticCanvas} patternSourceCanvas - fabric static canvas
* @returns {Object}
*/
export function makeFillPatternForFilter(canvasImage, filterOption, patternSourceCanvas) {
const copiedCanvasElement = getCachedCanvasImageElement(canvasImage);
const fillImage = makeFillImage(copiedCanvasElement, canvasImage.angle, filterOption);
patternSourceCanvas.add(fillImage);
const fabricProperty = {
fill: new fabric.Pattern({
source: patternSourceCanvas.getElement(),
repeat: 'no-repeat',
}),
};
setCustomProperty(fabricProperty, { patternSourceCanvas });
return fabricProperty;
}
/**
* Reset fill pattern canvas
* @param {fabric.StaticCanvas} patternSourceCanvas - fabric static canvas
*/
export function resetFillPatternCanvas(patternSourceCanvas) {
const [innerImage] = patternSourceCanvas.getObjects();
let { fillImageMaxSize } = getCustomProperty(innerImage, 'fillImageMaxSize');
fillImageMaxSize = Math.max(1, fillImageMaxSize);
patternSourceCanvas.setDimensions({
width: fillImageMaxSize,
height: fillImageMaxSize,
});
patternSourceCanvas.renderAll();
}
/**
* Remake filter pattern image source
* @param {fabric.Object} shapeObj - Shape object
* @param {fabric.Image} canvasImage - canvas background image
*/
export function reMakePatternImageSource(shapeObj, canvasImage) {
const { patternSourceCanvas } = getCustomProperty(shapeObj, 'patternSourceCanvas');
const [fillImage] = patternSourceCanvas.getObjects();
const filterOption = makeFilterOptionFromFabricImage(fillImage);
patternSourceCanvas.remove(fillImage);
const copiedCanvasElement = getCachedCanvasImageElement(canvasImage, true);
const newFillImage = makeFillImage(copiedCanvasElement, canvasImage.angle, filterOption);
patternSourceCanvas.add(newFillImage);
}
/**
* Calculate a point line outside the canvas.
* @param {fabric.Image} canvasImage - canvas background image
* @param {boolean} reset - default is false
* @returns {HTMLImageElement}
*/
export function getCachedCanvasImageElement(canvasImage, reset = false) {
if (!cachedCanvasImageElement || reset) {
cachedCanvasImageElement = canvasImage.toCanvasElement();
}
return cachedCanvasImageElement;
}
/**
* Calculate fill image position for out of Canvas
* @param {string} type - 'x' or 'y'
* @param {fabric.Object} shapeObj - shape object
* @param {number} outDistance - distance away
* @param {number} left - original left position
* @param {number} top - original top position
* @returns {Array}
*/
function calculateDistanceOverflowPart({ type, shapeObj, outDistance, left, top, flipX, flipY }) {
const shapePointNavigation = getShapeEdgePoint(shapeObj);
const shapeNeighborPointNavigation = [
[1, 2],
[0, 3],
[0, 3],
[1, 2],
];
const linePointsOutsideCanvas = calculateLinePointsOutsideCanvas(
type,
shapePointNavigation,
shapeNeighborPointNavigation
);
const reatAngles = calculateLineAngleOfOutsideCanvas(
type,
shapePointNavigation,
linePointsOutsideCanvas
);
const { startPointIndex } = linePointsOutsideCanvas;
const diffPosition = getReversePositionForFlip({
outDistance,
startPointIndex,
flipX,
flipY,
reatAngles,
});
return [left + diffPosition.left, top + diffPosition.top];
}
/**
* Calculate fill image position for out of Canvas
* @param {number} outDistance - distance away
* @param {boolean} flipX - flip x statux
* @param {boolean} flipY - flip y statux
* @param {Array} reatAngles - Line angle of the rectangle vertex.
* @returns {Object} diffPosition
*/
function getReversePositionForFlip({ outDistance, startPointIndex, flipX, flipY, reatAngles }) {
const rotationChangePoint1 = outDistance * Math.cos((reatAngles[0] * Math.PI) / 180);
const rotationChangePoint2 = outDistance * Math.cos((reatAngles[1] * Math.PI) / 180);
const isForward = startPointIndex === 2 || startPointIndex === 3;
const diffPosition = {
top: isForward ? rotationChangePoint1 : rotationChangePoint2,
left: isForward ? rotationChangePoint2 : rotationChangePoint1,
};
if (isReverseLeftPositionForFlip(startPointIndex, flipX, flipY)) {
diffPosition.left = diffPosition.left * -1;
}
if (isReverseTopPositionForFlip(startPointIndex, flipX, flipY)) {
diffPosition.top = diffPosition.top * -1;
}
return diffPosition;
}
/**
* Calculate a point line outside the canvas.
* @param {string} type - 'x' or 'y'
* @param {Array} shapePointNavigation - shape edge positions
* @param {Object} shapePointNavigation.lefttop - left top position
* @param {Object} shapePointNavigation.righttop - right top position
* @param {Object} shapePointNavigation.leftbottom - lefttop position
* @param {Object} shapePointNavigation.rightbottom - rightbottom position
* @param {Array} shapeNeighborPointNavigation - Array to find adjacent edges.
* @returns {Object}
*/
function calculateLinePointsOutsideCanvas(
type,
shapePointNavigation,
shapeNeighborPointNavigation
) {
let minimumPoint = 0;
let minimumPointIndex = 0;
forEach(shapePointNavigation, (point, index) => {
if (point[type] < minimumPoint) {
minimumPoint = point[type];
minimumPointIndex = index;
}
});
const [endPointIndex1, endPointIndex2] = shapeNeighborPointNavigation[minimumPointIndex];
return {
startPointIndex: minimumPointIndex,
endPointIndex1,
endPointIndex2,
};
}
/**
* Calculate a point line outside the canvas.
* @param {string} type - 'x' or 'y'
* @param {Array} shapePointNavigation - shape edge positions
* @param {object} shapePointNavigation.lefttop - left top position
* @param {object} shapePointNavigation.righttop - right top position
* @param {object} shapePointNavigation.leftbottom - lefttop position
* @param {object} shapePointNavigation.rightbottom - rightbottom position
* @param {Object} linePointsOfOneVertexIndex - Line point of one vertex
* @param {Object} linePointsOfOneVertexIndex.startPoint - start point index
* @param {Object} linePointsOfOneVertexIndex.endPointIndex1 - end point index
* @param {Object} linePointsOfOneVertexIndex.endPointIndex2 - end point index
* @returns {Object}
*/
function calculateLineAngleOfOutsideCanvas(type, shapePointNavigation, linePointsOfOneVertexIndex) {
const { startPointIndex, endPointIndex1, endPointIndex2 } = linePointsOfOneVertexIndex;
const horizontalVerticalAngle = type === 'x' ? 180 : 270;
return [endPointIndex1, endPointIndex2].map((pointIndex) => {
const startPoint = shapePointNavigation[startPointIndex];
const endPoint = shapePointNavigation[pointIndex];
const diffY = startPoint.y - endPoint.y;
const diffX = startPoint.x - endPoint.x;
return (Math.atan2(diffY, diffX) * 180) / Math.PI - horizontalVerticalAngle;
});
}
/* eslint-disable complexity */
/**
* Calculate a point line outside the canvas for horizontal.
* @param {number} startPointIndex - start point index
* @param {boolean} flipX - flip x statux
* @param {boolean} flipY - flip y statux
* @returns {boolean} flipY - flip y statux
*/
function isReverseLeftPositionForFlip(startPointIndex, flipX, flipY) {
return (
(((!flipX && flipY) || (!flipX && !flipY)) && startPointIndex === 0) ||
(((flipX && flipY) || (flipX && !flipY)) && startPointIndex === 1) ||
(((!flipX && !flipY) || (!flipX && flipY)) && startPointIndex === 2) ||
(((flipX && !flipY) || (flipX && flipY)) && startPointIndex === 3)
);
}
/* eslint-enable complexity */
/* eslint-disable complexity */
/**
* Calculate a point line outside the canvas for vertical.
* @param {number} startPointIndex - start point index
* @param {boolean} flipX - flip x statux
* @param {boolean} flipY - flip y statux
* @returns {boolean} flipY - flip y statux
*/
function isReverseTopPositionForFlip(startPointIndex, flipX, flipY) {
return (
(((flipX && !flipY) || (!flipX && !flipY)) && startPointIndex === 0) ||
(((!flipX && !flipY) || (flipX && !flipY)) && startPointIndex === 1) ||
(((flipX && flipY) || (!flipX && flipY)) && startPointIndex === 2) ||
(((!flipX && flipY) || (flipX && flipY)) && startPointIndex === 3)
);
}
/* eslint-enable complexity */
/**
* Shape edge points
* @param {fabric.Object} shapeObj - Selected shape object on canvas
* @returns {Array} shapeEdgePoint - shape edge positions
*/
function getShapeEdgePoint(shapeObj) {
return [
shapeObj.getPointByOrigin('left', 'top'),
shapeObj.getPointByOrigin('right', 'top'),
shapeObj.getPointByOrigin('left', 'bottom'),
shapeObj.getPointByOrigin('right', 'bottom'),
];
}
/**
* Rotated shape dimension
* @param {fabric.Object} shapeObj - Shape object
* @returns {Object} Rotated shape dimension
*/
function getRotatedDimension(shapeObj) {
const [{ x: ax, y: ay }, { x: bx, y: by }, { x: cx, y: cy }, { x: dx, y: dy }] =
getShapeEdgePoint(shapeObj);
const left = Math.min(ax, bx, cx, dx);
const top = Math.min(ay, by, cy, dy);
const right = Math.max(ax, bx, cx, dx);
const bottom = Math.max(ay, by, cy, dy);
return {
left,
top,
right,
bottom,
width: right - left,
height: bottom - top,
};
}
/**
* Make fill image
* @param {HTMLImageElement} copiedCanvasElement - html image element
* @param {number} currentCanvasImageAngle - current canvas angle
* @param {Array} filterOption - filter option
* @returns {fabric.Image}
* @private
*/
function makeFillImage(copiedCanvasElement, currentCanvasImageAngle, filterOption) {
const fillImage = new fabric.Image(copiedCanvasElement);
forEach(extend({}, ...filterOption), (value, key) => {
const fabricFilterClassName = capitalizeString(key);
const filter = new fabric.Image.filters[fabricFilterClassName]({
[FILTER_OPTION_MAP[key]]: value,
});
fillImage.filters.push(filter);
});
fillImage.applyFilters();
setCustomProperty(fillImage, {
originalAngle: currentCanvasImageAngle,
fillImageMaxSize: Math.max(fillImage.width, fillImage.height),
});
resizeHelper.adjustOriginToCenter(fillImage);
return fillImage;
}