| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700 |
- import { useCallback } from 'react';
- import { useCanvasStore } from '@/lib/store';
- import { useKeyboardStore } from '@/lib/store/keyboard';
- import type {
- PPTElement,
- PPTLineElement,
- PPTImageElement,
- PPTShapeElement,
- } from '@/lib/types/slides';
- import {
- OperateResizeHandlers,
- type AlignmentLineProps,
- type MultiSelectRange,
- } from '@/lib/types/edit';
- import { MIN_SIZE } from '@/configs/element';
- import { SHAPE_PATH_FORMULAS } from '@/configs/shapes';
- import { type AlignLine, uniqAlignLines } from '@/lib/utils/element';
- import { useHistorySnapshot } from '@/lib/hooks/use-history-snapshot';
- import { useCanvasOperations } from '@/lib/hooks/use-canvas-operations';
- interface RotateElementData {
- left: number;
- top: number;
- width: number;
- height: number;
- }
- /**
- * Calculate the positions of the eight scale points of a rotated element
- * @param element Original position and size of the element
- * @param angle Rotation angle
- */
- const getRotateElementPoints = (element: RotateElementData, angle: number) => {
- const { left, top, width, height } = element;
- const radius = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2)) / 2;
- const auxiliaryAngle = (Math.atan(height / width) * 180) / Math.PI;
- const tlbraRadian = ((180 - angle - auxiliaryAngle) * Math.PI) / 180;
- const trblaRadian = ((auxiliaryAngle - angle) * Math.PI) / 180;
- const taRadian = ((90 - angle) * Math.PI) / 180;
- const raRadian = (angle * Math.PI) / 180;
- const halfWidth = width / 2;
- const halfHeight = height / 2;
- const middleLeft = left + halfWidth;
- const middleTop = top + halfHeight;
- const leftTopPoint = {
- left: middleLeft + radius * Math.cos(tlbraRadian),
- top: middleTop - radius * Math.sin(tlbraRadian),
- };
- const topPoint = {
- left: middleLeft + halfHeight * Math.cos(taRadian),
- top: middleTop - halfHeight * Math.sin(taRadian),
- };
- const rightTopPoint = {
- left: middleLeft + radius * Math.cos(trblaRadian),
- top: middleTop - radius * Math.sin(trblaRadian),
- };
- const rightPoint = {
- left: middleLeft + halfWidth * Math.cos(raRadian),
- top: middleTop + halfWidth * Math.sin(raRadian),
- };
- const rightBottomPoint = {
- left: middleLeft - radius * Math.cos(tlbraRadian),
- top: middleTop + radius * Math.sin(tlbraRadian),
- };
- const bottomPoint = {
- left: middleLeft - halfHeight * Math.sin(raRadian),
- top: middleTop + halfHeight * Math.cos(raRadian),
- };
- const leftBottomPoint = {
- left: middleLeft - radius * Math.cos(trblaRadian),
- top: middleTop + radius * Math.sin(trblaRadian),
- };
- const leftPoint = {
- left: middleLeft - halfWidth * Math.cos(raRadian),
- top: middleTop - halfWidth * Math.sin(raRadian),
- };
- return {
- leftTopPoint,
- topPoint,
- rightTopPoint,
- rightPoint,
- rightBottomPoint,
- bottomPoint,
- leftBottomPoint,
- leftPoint,
- };
- };
- /**
- * Get the opposite point of a given scale point, e.g. [top] corresponds to [bottom], [left-top] corresponds to [right-bottom]
- * @param direction The current scale point being operated
- * @param points Positions of the eight scale points of the rotated element
- */
- const getOppositePoint = (
- direction: OperateResizeHandlers,
- points: ReturnType<typeof getRotateElementPoints>,
- ): { left: number; top: number } => {
- const oppositeMap = {
- [OperateResizeHandlers.RIGHT_BOTTOM]: points.leftTopPoint,
- [OperateResizeHandlers.LEFT_BOTTOM]: points.rightTopPoint,
- [OperateResizeHandlers.LEFT_TOP]: points.rightBottomPoint,
- [OperateResizeHandlers.RIGHT_TOP]: points.leftBottomPoint,
- [OperateResizeHandlers.TOP]: points.bottomPoint,
- [OperateResizeHandlers.BOTTOM]: points.topPoint,
- [OperateResizeHandlers.LEFT]: points.rightPoint,
- [OperateResizeHandlers.RIGHT]: points.leftPoint,
- };
- return oppositeMap[direction];
- };
- /**
- * Scale element Hook
- *
- * @param elementListRef - Element list ref (stores the latest value)
- * @param setElementList - Element list setter (used to trigger re-render)
- * @param setAlignmentLines - Alignment lines setter
- */
- export function useScaleElement(
- elementListRef: React.RefObject<PPTElement[]>,
- setElementList: React.Dispatch<React.SetStateAction<PPTElement[]>>,
- setAlignmentLines: React.Dispatch<React.SetStateAction<AlignmentLineProps[]>>,
- ) {
- const setScalingState = useCanvasStore.use.setScalingState();
- const activeElementIdList = useCanvasStore.use.activeElementIdList();
- const activeGroupElementId = useCanvasStore.use.activeGroupElementId();
- const canvasScale = useCanvasStore.use.canvasScale();
- const viewportRatio = useCanvasStore.use.viewportRatio();
- const viewportSize = useCanvasStore.use.viewportSize();
- const updateSlide = useCanvasOperations().updateSlide;
- const ctrlOrShiftKeyActive = useKeyboardStore((state) => state.ctrlOrShiftKeyActive());
- const { addHistorySnapshot } = useHistorySnapshot();
- // Scale element
- const scaleElement = useCallback(
- (
- e: React.MouseEvent | React.TouchEvent,
- element: Exclude<PPTElement, PPTLineElement>,
- command: OperateResizeHandlers,
- ) => {
- const native = e.nativeEvent;
- const isTouchEvent = native instanceof TouchEvent;
- if (isTouchEvent && !native.changedTouches?.length) return;
- let isMouseDown = true;
- setScalingState(true);
- const elOriginLeft = element.left;
- const elOriginTop = element.top;
- const elOriginWidth = element.width;
- const elOriginHeight = element.height;
- const originTableCellMinHeight = element.type === 'table' ? element.cellMinHeight : 0;
- const elRotate = 'rotate' in element && element.rotate ? element.rotate : 0;
- const rotateRadian = (Math.PI * elRotate) / 180;
- const fixedRatio = ctrlOrShiftKeyActive || ('fixedRatio' in element && element.fixedRatio);
- const aspectRatio = elOriginWidth / elOriginHeight;
- const startPageX = isTouchEvent ? native.changedTouches[0].pageX : native.pageX;
- const startPageY = isTouchEvent ? native.changedTouches[0].pageY : native.pageY;
- // Minimum scale size limit for element
- const minSize = MIN_SIZE[element.type] || 20;
- const getSizeWithinRange = (size: number, type: 'width' | 'height') => {
- if (!fixedRatio) return size < minSize ? minSize : size;
- let minWidth = minSize;
- let minHeight = minSize;
- const ratio = element.width / element.height;
- if (ratio < 1) minHeight = minSize / ratio;
- if (ratio > 1) minWidth = minSize * ratio;
- if (type === 'width') return size < minWidth ? minWidth : size;
- return size < minHeight ? minHeight : size;
- };
- let points: ReturnType<typeof getRotateElementPoints>;
- let baseLeft = 0;
- let baseTop = 0;
- let horizontalLines: AlignLine[] = [];
- let verticalLines: AlignLine[] = [];
- // When scaling a rotated element, introduce a base point concept: the point opposite to the current scale handle
- // For example, when dragging the bottom-right corner, the top-left corner is the base point that stays fixed while other points move to achieve scaling
- if ('rotate' in element && element.rotate) {
- const { left, top, width, height } = element;
- points = getRotateElementPoints({ left, top, width, height }, elRotate);
- const oppositePoint = getOppositePoint(command, points);
- baseLeft = oppositePoint.left;
- baseTop = oppositePoint.top;
- }
- // Non-rotated elements support alignment snapping during scaling; collect alignment snap lines here
- // Includes snappable alignment positions (top, bottom, left, right edges) of all elements on the canvas except the target element
- // Line elements and rotated elements are excluded from alignment snapping
- else {
- const edgeWidth = viewportSize;
- const edgeHeight = viewportSize * viewportRatio;
- const isActiveGroupElement = element.id === activeGroupElementId;
- for (const el of elementListRef.current) {
- if ('rotate' in el && el.rotate) continue;
- if (el.type === 'line') continue;
- if (isActiveGroupElement && el.id === element.id) continue;
- if (!isActiveGroupElement && activeElementIdList.includes(el.id)) continue;
- const left = el.left;
- const top = el.top;
- const width = el.width;
- const height = el.height;
- const right = left + width;
- const bottom = top + height;
- const topLine: AlignLine = { value: top, range: [left, right] };
- const bottomLine: AlignLine = { value: bottom, range: [left, right] };
- const leftLine: AlignLine = { value: left, range: [top, bottom] };
- const rightLine: AlignLine = { value: right, range: [top, bottom] };
- horizontalLines.push(topLine, bottomLine);
- verticalLines.push(leftLine, rightLine);
- }
- // Four edges of the visible canvas area, horizontal center, and vertical center
- const edgeTopLine: AlignLine = { value: 0, range: [0, edgeWidth] };
- const edgeBottomLine: AlignLine = {
- value: edgeHeight,
- range: [0, edgeWidth],
- };
- const edgeHorizontalCenterLine: AlignLine = {
- value: edgeHeight / 2,
- range: [0, edgeWidth],
- };
- const edgeLeftLine: AlignLine = { value: 0, range: [0, edgeHeight] };
- const edgeRightLine: AlignLine = {
- value: edgeWidth,
- range: [0, edgeHeight],
- };
- const edgeVerticalCenterLine: AlignLine = {
- value: edgeWidth / 2,
- range: [0, edgeHeight],
- };
- horizontalLines.push(edgeTopLine, edgeBottomLine, edgeHorizontalCenterLine);
- verticalLines.push(edgeLeftLine, edgeRightLine, edgeVerticalCenterLine);
- horizontalLines = uniqAlignLines(horizontalLines);
- verticalLines = uniqAlignLines(verticalLines);
- }
- // Alignment snapping method
- // Compare collected alignment snap lines with the target element's current position/size data; auto-correct when the difference is within threshold
- // Horizontal and vertical directions are calculated separately
- const alignedAdsorption = (currentX: number | null, currentY: number | null) => {
- const sorptionRange = 5;
- const _alignmentLines: AlignmentLineProps[] = [];
- let isVerticalAdsorbed = false;
- let isHorizontalAdsorbed = false;
- const correctionVal = { offsetX: 0, offsetY: 0 };
- if (currentY || currentY === 0) {
- for (let i = 0; i < horizontalLines.length; i++) {
- const { value, range } = horizontalLines[i];
- const min = Math.min(...range, currentX || 0);
- const max = Math.max(...range, currentX || 0);
- if (Math.abs(currentY - value) < sorptionRange && !isHorizontalAdsorbed) {
- correctionVal.offsetY = currentY - value;
- isHorizontalAdsorbed = true;
- _alignmentLines.push({
- type: 'horizontal',
- axis: { x: min - 50, y: value },
- length: max - min + 100,
- });
- }
- }
- }
- if (currentX || currentX === 0) {
- for (let i = 0; i < verticalLines.length; i++) {
- const { value, range } = verticalLines[i];
- const min = Math.min(...range, currentY || 0);
- const max = Math.max(...range, currentY || 0);
- if (Math.abs(currentX - value) < sorptionRange && !isVerticalAdsorbed) {
- correctionVal.offsetX = currentX - value;
- isVerticalAdsorbed = true;
- _alignmentLines.push({
- type: 'vertical',
- axis: { x: value, y: min - 50 },
- length: max - min + 100,
- });
- }
- }
- }
- setAlignmentLines(_alignmentLines);
- return correctionVal;
- };
- const handleMouseMove = (e: MouseEvent | TouchEvent) => {
- if (!isMouseDown) return;
- const currentPageX = e instanceof MouseEvent ? e.pageX : e.changedTouches[0].pageX;
- const currentPageY = e instanceof MouseEvent ? e.pageY : e.changedTouches[0].pageY;
- const x = currentPageX - startPageX;
- const y = currentPageY - startPageY;
- let width = elOriginWidth;
- let height = elOriginHeight;
- let left = elOriginLeft;
- let top = elOriginTop;
- // For rotated elements, recalculate the scaling distance based on the rotation angle (distance moved after mouse down)
- if (elRotate) {
- const revisedX = (Math.cos(rotateRadian) * x + Math.sin(rotateRadian) * y) / canvasScale;
- let revisedY = (Math.cos(rotateRadian) * y - Math.sin(rotateRadian) * x) / canvasScale;
- // Lock aspect ratio (only triggered by four corners, not edges)
- // Use horizontal scaling distance as the basis to calculate vertical scaling distance, maintaining the same ratio
- if (fixedRatio) {
- if (
- command === OperateResizeHandlers.RIGHT_BOTTOM ||
- command === OperateResizeHandlers.LEFT_TOP
- )
- revisedY = revisedX / aspectRatio;
- if (
- command === OperateResizeHandlers.LEFT_BOTTOM ||
- command === OperateResizeHandlers.RIGHT_TOP
- )
- revisedY = -revisedX / aspectRatio;
- }
- // Calculate element size and position after scaling based on the operation point
- // Note:
- // The position calculated here needs correction later, because scaling a rotated element changes the base point position (visually the base point stays fixed, but that's the combined result of rotation + translation)
- // However, the size does not need correction since the scaling distance was already recalculated above
- if (command === OperateResizeHandlers.RIGHT_BOTTOM) {
- width = getSizeWithinRange(elOriginWidth + revisedX, 'width');
- height = getSizeWithinRange(elOriginHeight + revisedY, 'height');
- } else if (command === OperateResizeHandlers.LEFT_BOTTOM) {
- width = getSizeWithinRange(elOriginWidth - revisedX, 'width');
- height = getSizeWithinRange(elOriginHeight + revisedY, 'height');
- left = elOriginLeft - (width - elOriginWidth);
- } else if (command === OperateResizeHandlers.LEFT_TOP) {
- width = getSizeWithinRange(elOriginWidth - revisedX, 'width');
- height = getSizeWithinRange(elOriginHeight - revisedY, 'height');
- left = elOriginLeft - (width - elOriginWidth);
- top = elOriginTop - (height - elOriginHeight);
- } else if (command === OperateResizeHandlers.RIGHT_TOP) {
- width = getSizeWithinRange(elOriginWidth + revisedX, 'width');
- height = getSizeWithinRange(elOriginHeight - revisedY, 'height');
- top = elOriginTop - (height - elOriginHeight);
- } else if (command === OperateResizeHandlers.TOP) {
- height = getSizeWithinRange(elOriginHeight - revisedY, 'height');
- top = elOriginTop - (height - elOriginHeight);
- } else if (command === OperateResizeHandlers.BOTTOM) {
- height = getSizeWithinRange(elOriginHeight + revisedY, 'height');
- } else if (command === OperateResizeHandlers.LEFT) {
- width = getSizeWithinRange(elOriginWidth - revisedX, 'width');
- left = elOriginLeft - (width - elOriginWidth);
- } else if (command === OperateResizeHandlers.RIGHT) {
- width = getSizeWithinRange(elOriginWidth + revisedX, 'width');
- }
- // Get current base point coordinates, compare with initial base point, and correct element position by the difference
- const currentPoints = getRotateElementPoints({ width, height, left, top }, elRotate);
- const currentOppositePoint = getOppositePoint(command, currentPoints);
- const currentBaseLeft = currentOppositePoint.left;
- const currentBaseTop = currentOppositePoint.top;
- const offsetX = currentBaseLeft - baseLeft;
- const offsetY = currentBaseTop - baseTop;
- left = left - offsetX;
- top = top - offsetY;
- }
- // For non-rotated elements, simply calculate the new position and size without complex corrections
- // Additionally handle alignment snapping operations
- // Aspect ratio locking logic is the same as above
- else {
- let moveX = x / canvasScale;
- let moveY = y / canvasScale;
- if (fixedRatio) {
- if (
- command === OperateResizeHandlers.RIGHT_BOTTOM ||
- command === OperateResizeHandlers.LEFT_TOP
- )
- moveY = moveX / aspectRatio;
- if (
- command === OperateResizeHandlers.LEFT_BOTTOM ||
- command === OperateResizeHandlers.RIGHT_TOP
- )
- moveY = -moveX / aspectRatio;
- }
- if (command === OperateResizeHandlers.RIGHT_BOTTOM) {
- const { offsetX, offsetY } = alignedAdsorption(
- elOriginLeft + elOriginWidth + moveX,
- elOriginTop + elOriginHeight + moveY,
- );
- moveX = moveX - offsetX;
- moveY = moveY - offsetY;
- if (fixedRatio) {
- if (offsetY) moveX = moveY * aspectRatio;
- else moveY = moveX / aspectRatio;
- }
- width = getSizeWithinRange(elOriginWidth + moveX, 'width');
- height = getSizeWithinRange(elOriginHeight + moveY, 'height');
- } else if (command === OperateResizeHandlers.LEFT_BOTTOM) {
- const { offsetX, offsetY } = alignedAdsorption(
- elOriginLeft + moveX,
- elOriginTop + elOriginHeight + moveY,
- );
- moveX = moveX - offsetX;
- moveY = moveY - offsetY;
- if (fixedRatio) {
- if (offsetY) moveX = -moveY * aspectRatio;
- else moveY = -moveX / aspectRatio;
- }
- width = getSizeWithinRange(elOriginWidth - moveX, 'width');
- height = getSizeWithinRange(elOriginHeight + moveY, 'height');
- left = elOriginLeft - (width - elOriginWidth);
- } else if (command === OperateResizeHandlers.LEFT_TOP) {
- const { offsetX, offsetY } = alignedAdsorption(
- elOriginLeft + moveX,
- elOriginTop + moveY,
- );
- moveX = moveX - offsetX;
- moveY = moveY - offsetY;
- if (fixedRatio) {
- if (offsetY) moveX = moveY * aspectRatio;
- else moveY = moveX / aspectRatio;
- }
- width = getSizeWithinRange(elOriginWidth - moveX, 'width');
- height = getSizeWithinRange(elOriginHeight - moveY, 'height');
- left = elOriginLeft - (width - elOriginWidth);
- top = elOriginTop - (height - elOriginHeight);
- } else if (command === OperateResizeHandlers.RIGHT_TOP) {
- const { offsetX, offsetY } = alignedAdsorption(
- elOriginLeft + elOriginWidth + moveX,
- elOriginTop + moveY,
- );
- moveX = moveX - offsetX;
- moveY = moveY - offsetY;
- if (fixedRatio) {
- if (offsetY) moveX = -moveY * aspectRatio;
- else moveY = -moveX / aspectRatio;
- }
- width = getSizeWithinRange(elOriginWidth + moveX, 'width');
- height = getSizeWithinRange(elOriginHeight - moveY, 'height');
- top = elOriginTop - (height - elOriginHeight);
- } else if (command === OperateResizeHandlers.LEFT) {
- const { offsetX } = alignedAdsorption(elOriginLeft + moveX, null);
- moveX = moveX - offsetX;
- width = getSizeWithinRange(elOriginWidth - moveX, 'width');
- left = elOriginLeft - (width - elOriginWidth);
- } else if (command === OperateResizeHandlers.RIGHT) {
- const { offsetX } = alignedAdsorption(elOriginLeft + elOriginWidth + moveX, null);
- moveX = moveX - offsetX;
- width = getSizeWithinRange(elOriginWidth + moveX, 'width');
- } else if (command === OperateResizeHandlers.TOP) {
- const { offsetY } = alignedAdsorption(null, elOriginTop + moveY);
- moveY = moveY - offsetY;
- height = getSizeWithinRange(elOriginHeight - moveY, 'height');
- top = elOriginTop - (height - elOriginHeight);
- } else if (command === OperateResizeHandlers.BOTTOM) {
- const { offsetY } = alignedAdsorption(null, elOriginTop + elOriginHeight + moveY);
- moveY = moveY - offsetY;
- height = getSizeWithinRange(elOriginHeight + moveY, 'height');
- }
- }
- // Update local element list during mousemove
- const newElements = elementListRef.current.map((el) => {
- if (element.id !== el.id) return el;
- if (el.type === 'shape' && 'pathFormula' in el && el.pathFormula) {
- const pathFormula = SHAPE_PATH_FORMULAS[el.pathFormula];
- let path = '';
- if ('editable' in pathFormula) path = pathFormula.formula(width, height, el.keypoints!);
- else path = pathFormula.formula(width, height);
- return {
- ...el,
- left,
- top,
- width,
- height,
- viewBox: [width, height] as [number, number],
- path,
- };
- }
- if (el.type === 'table') {
- let cellMinHeight =
- originTableCellMinHeight + (height - elOriginHeight) / el.data.length;
- cellMinHeight = cellMinHeight < 36 ? 36 : cellMinHeight;
- if (cellMinHeight === originTableCellMinHeight) return { ...el, left, width };
- return {
- ...el,
- left,
- top,
- width,
- height,
- cellMinHeight: cellMinHeight < 36 ? 36 : cellMinHeight,
- };
- }
- return { ...el, left, top, width, height };
- });
- // Update both ref and state
- elementListRef.current = newElements;
- setElementList(newElements);
- };
- const handleMouseUp = (e: MouseEvent | TouchEvent) => {
- isMouseDown = false;
- document.ontouchmove = null;
- document.ontouchend = null;
- document.onmousemove = null;
- document.onmouseup = null;
- setAlignmentLines([]);
- const currentPageX = e instanceof MouseEvent ? e.pageX : e.changedTouches[0].pageX;
- const currentPageY = e instanceof MouseEvent ? e.pageY : e.changedTouches[0].pageY;
- if (startPageX === currentPageX && startPageY === currentPageY) return;
- setScalingState(false);
- updateSlide({ elements: elementListRef.current });
- addHistorySnapshot();
- };
- if (isTouchEvent) {
- document.ontouchmove = handleMouseMove;
- document.ontouchend = handleMouseUp;
- } else {
- document.onmousemove = handleMouseMove;
- document.onmouseup = handleMouseUp;
- }
- },
- [
- elementListRef,
- setElementList,
- canvasScale,
- activeElementIdList,
- activeGroupElementId,
- viewportRatio,
- viewportSize,
- ctrlOrShiftKeyActive,
- setScalingState,
- setAlignmentLines,
- updateSlide,
- addHistorySnapshot,
- ],
- );
- // Scale multiple selected elements
- const scaleMultiElement = useCallback(
- (e: React.MouseEvent, range: MultiSelectRange, command: OperateResizeHandlers) => {
- let isMouseDown = true;
- const { minX, maxX, minY, maxY } = range;
- const operateWidth = maxX - minX;
- const operateHeight = maxY - minY;
- const aspectRatio = operateWidth / operateHeight;
- const startPageX = e.pageX;
- const startPageY = e.pageY;
- const originElementList: PPTElement[] = JSON.parse(JSON.stringify(elementListRef.current));
- const handleMouseMove = (e: MouseEvent) => {
- if (!isMouseDown) return;
- const currentPageX = e.pageX;
- const currentPageY = e.pageY;
- const x = (currentPageX - startPageX) / canvasScale;
- let y = (currentPageY - startPageY) / canvasScale;
- // Lock aspect ratio, same logic as above
- if (ctrlOrShiftKeyActive) {
- if (
- command === OperateResizeHandlers.RIGHT_BOTTOM ||
- command === OperateResizeHandlers.LEFT_TOP
- )
- y = x / aspectRatio;
- if (
- command === OperateResizeHandlers.LEFT_BOTTOM ||
- command === OperateResizeHandlers.RIGHT_TOP
- )
- y = -x / aspectRatio;
- }
- // Overall range of all selected elements
- let currentMinX = minX;
- let currentMaxX = maxX;
- let currentMinY = minY;
- let currentMaxY = maxY;
- if (command === OperateResizeHandlers.RIGHT_BOTTOM) {
- currentMaxX = maxX + x;
- currentMaxY = maxY + y;
- } else if (command === OperateResizeHandlers.LEFT_BOTTOM) {
- currentMinX = minX + x;
- currentMaxY = maxY + y;
- } else if (command === OperateResizeHandlers.LEFT_TOP) {
- currentMinX = minX + x;
- currentMinY = minY + y;
- } else if (command === OperateResizeHandlers.RIGHT_TOP) {
- currentMaxX = maxX + x;
- currentMinY = minY + y;
- } else if (command === OperateResizeHandlers.TOP) {
- currentMinY = minY + y;
- } else if (command === OperateResizeHandlers.BOTTOM) {
- currentMaxY = maxY + y;
- } else if (command === OperateResizeHandlers.LEFT) {
- currentMinX = minX + x;
- } else if (command === OperateResizeHandlers.RIGHT) {
- currentMaxX = maxX + x;
- }
- // Overall width and height of all selected elements
- const currentOppositeWidth = currentMaxX - currentMinX;
- const currentOppositeHeight = currentMaxY - currentMinY;
- // Ratio of the currently operated element's width/height to the overall width/height of all selected elements
- let widthScale = currentOppositeWidth / operateWidth;
- let heightScale = currentOppositeHeight / operateHeight;
- if (widthScale <= 0) widthScale = 0;
- if (heightScale <= 0) heightScale = 0;
- // Calculate and update the position and size of all selected elements based on the computed ratio
- const newElements = elementListRef.current.map((el) => {
- if ((el.type === 'image' || el.type === 'shape') && activeElementIdList.includes(el.id)) {
- const originElement = originElementList.find((originEl) => originEl.id === el.id) as
- | PPTImageElement
- | PPTShapeElement;
- return {
- ...el,
- width: originElement.width * widthScale,
- height: originElement.height * heightScale,
- left: currentMinX + (originElement.left - minX) * widthScale,
- top: currentMinY + (originElement.top - minY) * heightScale,
- };
- }
- return el;
- });
- elementListRef.current = newElements;
- setElementList(newElements);
- };
- const handleMouseUp = (e: MouseEvent) => {
- isMouseDown = false;
- document.onmousemove = null;
- document.onmouseup = null;
- if (startPageX === e.pageX && startPageY === e.pageY) return;
- updateSlide({ elements: elementListRef.current });
- addHistorySnapshot();
- };
- document.onmousemove = handleMouseMove;
- document.onmouseup = handleMouseUp;
- },
- [
- elementListRef,
- setElementList,
- canvasScale,
- activeElementIdList,
- ctrlOrShiftKeyActive,
- updateSlide,
- addHistorySnapshot,
- ],
- );
- return {
- scaleElement,
- scaleMultiElement,
- };
- }
|