diff --git a/src/utils/element.ts b/src/utils/element.ts index 3e6dd64f..231d5ba1 100644 --- a/src/utils/element.ts +++ b/src/utils/element.ts @@ -105,8 +105,8 @@ export interface AlignLine { } /** - * 将一组参考线进行去重:同位置的的多条参考线仅留下一条,取该位置所有参考线的最大值和最小值为新的范围 - * @param lines 一组参考线信息 + * 将一组对齐吸附线进行去重:同位置的的多条对齐吸附线仅留下一条,取该位置所有对齐吸附线的最大值和最小值为新的范围 + * @param lines 一组对齐吸附线信息 */ export const uniqAlignLines = (lines: AlignLine[]) => { const uniqLines: AlignLine[] = [] diff --git a/src/views/Editor/Canvas/AlignmentLine.vue b/src/views/Editor/Canvas/AlignmentLine.vue index dd802d75..b8e63de9 100644 --- a/src/views/Editor/Canvas/AlignmentLine.vue +++ b/src/views/Editor/Canvas/AlignmentLine.vue @@ -29,9 +29,11 @@ export default defineComponent({ const store = useStore() const canvasScale = computed(() => store.state.canvasScale) + // 吸附对齐线的位置 const left = computed(() => props.axis.x * canvasScale.value + 'px') const top = computed(() => props.axis.y * canvasScale.value + 'px') + // 吸附对齐线的长度 const sizeStyle = computed(() => { if (props.type === 'vertical') return { height: props.length * canvasScale.value + 'px' } return { width: props.length * canvasScale.value + 'px' } diff --git a/src/views/Editor/Canvas/ElementCreateSelection.vue b/src/views/Editor/Canvas/ElementCreateSelection.vue index 0cccc53f..66a20fa3 100644 --- a/src/views/Editor/Canvas/ElementCreateSelection.vue +++ b/src/views/Editor/Canvas/ElementCreateSelection.vue @@ -53,6 +53,8 @@ export default defineComponent({ offset.y = y }) + // 鼠标拖动创建元素生成位置大小 + // 获取范围的起始位置和终点位置 const createSelection = (e: MouseEvent) => { let isMouseDown = true @@ -66,15 +68,19 @@ export default defineComponent({ let currentPageX = e.pageX let currentPageY = e.pageY + // 按住Ctrl键或者Shift键时: + // 对于非线条元素需要锁定宽高比例,对于线条元素需要锁定水平或垂直方向 if (ctrlOrShiftKeyActive.value) { const moveX = currentPageX - startPageX const moveY = currentPageY - startPageY + // 水平和垂直方向的拖动距离,后面以拖动距离较大的方向为基础计算另一方向的数据 const absX = Math.abs(moveX) const absY = Math.abs(moveY) if (creatingElement.value.type === 'shape') { - // moveX和moveY一正一负 + + // 判断是否为反向拖动:从左上到右下为正向操作,此外所有情况都是反向操作 const isOpposite = (moveY > 0 && moveX < 0) || (moveY < 0 && moveX > 0) if (absX > absY) { @@ -114,6 +120,7 @@ export default defineComponent({ } } + // 绘制线条的路径相关数据(仅当绘制元素类型为线条时使用) const lineData = computed(() => { if (!start.value || !end.value) return null if (!creatingElement.value || creatingElement.value.type !== 'line') return null @@ -146,6 +153,7 @@ export default defineComponent({ } }) + // 根据生成范围的起始位置和终点位置,计算元素创建时的位置和大小 const position = computed(() => { if (!start.value || !end.value) return {} diff --git a/src/views/Editor/Canvas/GridLines.vue b/src/views/Editor/Canvas/GridLines.vue index 1e29cd39..fba4044f 100644 --- a/src/views/Editor/Canvas/GridLines.vue +++ b/src/views/Editor/Canvas/GridLines.vue @@ -28,6 +28,7 @@ export default defineComponent({ const canvasScale = computed(() => store.state.canvasScale) const background = computed(() => store.getters.currentSlide?.background) + // 计算网格线的颜色,避免与背景的颜色太接近 const gridColor = computed(() => { if (!background.value || background.value.type === 'image') return 'rgba(100, 100, 100, 0.5)' const color = background.value.color @@ -43,6 +44,7 @@ export default defineComponent({ const gridSize = 50 + // 计算网格路径 const getPath = () => { const maxX = VIEWPORT_SIZE const maxY = VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO diff --git a/src/views/Editor/Canvas/Operate/MultiSelectOperate.vue b/src/views/Editor/Canvas/Operate/MultiSelectOperate.vue index 54294152..7029e92c 100644 --- a/src/views/Editor/Canvas/Operate/MultiSelectOperate.vue +++ b/src/views/Editor/Canvas/Operate/MultiSelectOperate.vue @@ -60,10 +60,22 @@ export default defineComponent({ maxY: 0, }) + // 根据多选元素整体在画布中的范围,计算边框线和缩放点的位置信息 const width = computed(() => (range.maxX - range.minX) * canvasScale.value) const height = computed(() => (range.maxY - range.minY) * canvasScale.value) const { resizeHandlers, borderLines } = useCommonOperate(width, height) + // 计算多选元素整体在画布中的范围 + const setRange = () => { + const { minX, maxX, minY, maxY } = getElementListRange(localActiveElementList.value) + range.minX = minX + range.maxX = maxX + range.minY = minY + range.maxY = maxY + } + watchEffect(setRange) + + // 禁用多选状态下缩放:仅未旋转的图片和形状可以在多选状态下缩放 const disableResize = computed(() => { return localActiveElementList.value.some(item => { if ( @@ -74,16 +86,6 @@ export default defineComponent({ }) }) - const setRange = () => { - const { minX, maxX, minY, maxY } = getElementListRange(localActiveElementList.value) - range.minX = minX - range.maxX = maxX - range.minY = minY - range.maxY = maxY - } - - watchEffect(setRange) - return { ...toRefs(range), canvasScale, diff --git a/src/views/Editor/Canvas/hooks/useCommonOperate.ts b/src/views/Editor/Canvas/hooks/useCommonOperate.ts index bfda5019..a89acbe2 100644 --- a/src/views/Editor/Canvas/hooks/useCommonOperate.ts +++ b/src/views/Editor/Canvas/hooks/useCommonOperate.ts @@ -2,6 +2,7 @@ import { computed, Ref } from 'vue' import { OperateResizeHandlers, OperateBorderLines } from '@/types/edit' export default (width: Ref, height: Ref) => { + // 元素缩放点 const resizeHandlers = computed(() => { return [ { direction: OperateResizeHandlers.LEFT_TOP, style: {} }, @@ -13,8 +14,9 @@ export default (width: Ref, height: Ref) => { { direction: OperateResizeHandlers.BOTTOM, style: {left: width.value / 2 + 'px', top: height.value + 'px'} }, { direction: OperateResizeHandlers.RIGHT_BOTTOM, style: {left: width.value + 'px', top: height.value + 'px'} }, ] - } - ) + }) + + // 文本元素缩放点 const textElementResizeHandlers = computed(() => { return [ { direction: OperateResizeHandlers.LEFT, style: {top: height.value / 2 + 'px'} }, @@ -22,6 +24,7 @@ export default (width: Ref, height: Ref) => { ] }) + // 元素选中边框线 const borderLines = computed(() => { return [ { type: OperateBorderLines.T, style: {width: width.value + 'px'} }, diff --git a/src/views/Editor/Canvas/hooks/useDragElement.ts b/src/views/Editor/Canvas/hooks/useDragElement.ts index d66b6d8b..93fd1134 100644 --- a/src/views/Editor/Canvas/hooks/useDragElement.ts +++ b/src/views/Editor/Canvas/hooks/useDragElement.ts @@ -21,19 +21,20 @@ export default ( if (!activeElementIdList.value.includes(element.id)) return let isMouseDown = true - // 可视范围宽高,用于边缘对齐吸附 const edgeWidth = VIEWPORT_SIZE const edgeHeight = VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO + + const sorptionRange = 5 const originElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList.value)) const originActiveElementList = originElementList.filter(el => activeElementIdList.value.includes(el.id)) - - const sorptionRange = 5 + const elOriginLeft = element.left const elOriginTop = element.top const elOriginWidth = element.width const elOriginHeight = ('height' in element && element.height) ? element.height : 0 const elOriginRotate = ('rotate' in element && element.rotate) ? element.rotate : 0 + const startPageX = e.pageX const startPageY = e.pageY @@ -41,12 +42,12 @@ export default ( const isActiveGroupElement = element.id === activeGroupElementId.value - // 收集对齐参考线 - // 包括页面内出被操作元素以外的所有元素在页面内水平和垂直方向的范围和中心位置、页面边界和水平和垂直的中心位置 + // 收集对齐对齐吸附线 + // 包括页面内除目标元素外的其他元素在画布中的各个可吸附对齐位置:上下左右四边,水平中心、垂直中心 + // 其中线条和被旋转过的元素需要重新计算他们在画布中的中心点位置的范围 let horizontalLines: AlignLine[] = [] let verticalLines: AlignLine[] = [] - // 元素在页面内水平和垂直方向的范围和中心位置(需要特殊计算线条和被旋转的元素) for (const el of elementList.value) { if (el.type === 'line') continue if (isActiveGroupElement && el.id === element.id) continue @@ -89,7 +90,7 @@ export default ( verticalLines.push(leftLine, rightLine, verticalCenterLine) } - // 页面边界、水平和垂直的中心位置 + // 画布可视区域的四个边界、水平中心、垂直中心 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] } @@ -100,34 +101,34 @@ export default ( horizontalLines.push(edgeTopLine, edgeBottomLine, edgeHorizontalCenterLine) verticalLines.push(edgeLeftLine, edgeRightLine, edgeVerticalCenterLine) - // 参考线去重 + // 对齐吸附线去重 horizontalLines = uniqAlignLines(horizontalLines) verticalLines = uniqAlignLines(verticalLines) + // 开始移动 document.onmousemove = e => { const currentPageX = e.pageX const currentPageY = e.pageY - // 对于鼠标第一次滑动距离过小的操作判定为误操作 - // 这里仅在误操作标记未被赋值(null,第一次触发移动),以及被标记为误操作时(true,当前处于误操作范围,但可能会脱离该范围转变成正常操作),才会去计算 - // 已经被标记为非误操作时(false),不需要再次计算(因为不可能从非误操作转变成误操作) + // 如果鼠标滑动距离过小,则将操作判定为误操作: + // 如果误操作标记为null,表示是第一次触发移动,需要计算当前是否是误操作 + // 如果误操作标记为true,表示当前还处在误操作范围内,但仍然需要继续计算检查后续操作是否还处于误操作 + // 如果误操作标记为false,表示已经脱离了误操作范围,不需要再次计算 if (isMisoperation !== false) { isMisoperation = Math.abs(startPageX - currentPageX) < sorptionRange && Math.abs(startPageY - currentPageY) < sorptionRange } if (!isMouseDown || isMisoperation) return - // 鼠标按下后移动的距离 const moveX = (currentPageX - startPageX) / canvasScale.value const moveY = (currentPageY - startPageY) / canvasScale.value - // 被操作元素需要移动到的位置 + // 基础目标位置 let targetLeft = elOriginLeft + moveX let targetTop = elOriginTop + moveY - // 计算被操作元素在页面中的范围(用于吸附对齐) - // 需要区分计算:多选状态、线条、被旋转的元素 - // 注意这里需要用元素的原始信息结合移动信息来计算 + // 计算目标元素在画布中的位置范围,用于吸附对齐 + // 需要区分单选和多选两种情况,其中多选状态下需要计算多选元素的整体范围;单选状态下需要继续区分线条、普通元素、旋转后的普通元素三种情况 let targetMinX: number, targetMaxX: number, targetMinY: number, targetMaxY: number if (activeElementIdList.value.length === 1 || isActiveGroupElement) { @@ -201,7 +202,8 @@ export default ( const targetCenterX = targetMinX + (targetMaxX - targetMinX) / 2 const targetCenterY = targetMinY + (targetMaxY - targetMinY) / 2 - // 根据收集到的参考线,分别执行垂直和水平两个方向的对齐吸附 + // 将收集到的对齐吸附线与计算的目标元素位置范围做对比,二者的差小于设定的值时执行自动对齐校正 + // 水平和垂直两个方向需要分开计算 const _alignmentLines: AlignmentLineProps[] = [] let isVerticalAdsorbed = false let isHorizontalAdsorbed = false @@ -249,15 +251,15 @@ export default ( } alignmentLines.value = _alignmentLines - // 非多选,或者当前操作的元素时激活的组合元素 + // 单选状态下,或者当前选中的多个元素中存在正在操作的元素时,仅修改正在操作的元素的位置 if (activeElementIdList.value.length === 1 || isActiveGroupElement) { elementList.value = elementList.value.map(el => { return el.id === element.id ? { ...el, left: targetLeft, top: targetTop } : el }) } - // 修改元素位置,如果需要修改位置的元素不是被操作的元素(例如多选下的操作) - // 那么其他非操作元素要移动的位置通过操作元素的移动偏移量计算 + // 多选状态下,除了修改正在操作的元素的位置,其他被选中的元素也需要修改位置信息 + // 其他被选中的元素的位置信息通过正在操作的元素的移动偏移量来进行计算 else { const handleElement = elementList.value.find(el => el.id === element.id) if (!handleElement) return @@ -291,7 +293,6 @@ export default ( const currentPageX = e.pageX const currentPageY = e.pageY - // 对比初始位置,没有实际的位移不更新数据 if (startPageX === currentPageX && startPageY === currentPageY) return store.commit(MutationTypes.UPDATE_SLIDE, { elements: elementList.value }) diff --git a/src/views/Editor/Canvas/hooks/useDragLineElement.ts b/src/views/Editor/Canvas/hooks/useDragLineElement.ts index 1d0f7d54..0e029fc7 100644 --- a/src/views/Editor/Canvas/hooks/useDragLineElement.ts +++ b/src/views/Editor/Canvas/hooks/useDragLineElement.ts @@ -15,6 +15,7 @@ export default (elementList: Ref) => { const { addHistorySnapshot } = useHistorySnapshot() + // 拖拽线条端点 const dragLineElement = (e: MouseEvent, element: PPTLineElement, command: OperateLineHandler) => { let isMouseDown = true @@ -25,7 +26,7 @@ export default (elementList: Ref) => { const adsorptionPoints: AdsorptionPoint[] = [] - // 获取全部非线条且未旋转元素的8个点作为吸附点 + // 获取所有线条以外的未旋转的元素的8个缩放点作为吸附位置 for (let i = 0; i < elementList.value.length; i++) { const _element = elementList.value[i] if (_element.type === 'line' || ('rotate' in _element && _element.rotate)) continue @@ -68,19 +69,17 @@ export default (elementList: Ref) => { const currentPageX = e.pageX const currentPageY = e.pageY - // 鼠标按下后移动的距离 const moveX = (currentPageX - startPageX) / canvasScale.value const moveY = (currentPageY - startPageY) / canvasScale.value - // 线条两个端点(起点和终点)基于编辑区域的位置 + // 线条起点和终点在编辑区域中的位置 let startX = element.left + element.start[0] let startY = element.top + element.start[1] let endX = element.left + element.end[0] let endY = element.top + element.end[1] - // 根据拖拽的点,选择修改起点或终点的位置 - // 两点在水平和垂直方向上有对齐吸附 - // 靠近其他元素的吸附点有对齐吸附 + // 拖拽起点或终点的位置 + // 水平和垂直方向上有吸附 if (command === OperateLineHandlers.START) { startX = startX + moveX startY = startY + moveY @@ -114,7 +113,7 @@ export default (elementList: Ref) => { } } - // 计算两个端点基于自身元素位置的坐标 + // 计算更新起点和终点基于自身元素位置的坐标 const minX = Math.min(startX, endX) const minY = Math.min(startY, endY) const maxX = Math.max(startX, endX) @@ -131,7 +130,6 @@ export default (elementList: Ref) => { end[1] = 0 } - // 修改线条的位置和两点的坐标 elementList.value = elementList.value.map(el => { if (el.id === element.id) { return { @@ -154,7 +152,6 @@ export default (elementList: Ref) => { const currentPageX = e.pageX const currentPageY = e.pageY - // 对比原始鼠标位置,没有实际的位移不更新数据 if (startPageX === currentPageX && startPageY === currentPageY) return store.commit(MutationTypes.UPDATE_SLIDE, { elements: elementList.value }) diff --git a/src/views/Editor/Canvas/hooks/useDropImageOrText.ts b/src/views/Editor/Canvas/hooks/useDropImageOrText.ts index bdda4316..fe7e0483 100644 --- a/src/views/Editor/Canvas/hooks/useDropImageOrText.ts +++ b/src/views/Editor/Canvas/hooks/useDropImageOrText.ts @@ -9,10 +9,12 @@ export default (elementRef: Ref) => { const { createImageElement, createTextElement } = useCreateElement() + // 拖拽元素到画布中 const handleDrop = (e: DragEvent) => { if (!e.dataTransfer) return const dataTransferItem = e.dataTransfer.items[0] + // 检查事件对象中是否存在图片,存在则插入图片,否则继续检查是否存在文字,存在则插入文字 if (dataTransferItem.kind === 'file' && dataTransferItem.type.indexOf('image') !== -1) { const imageFile = dataTransferItem.getAsFile() if (imageFile) { diff --git a/src/views/Editor/Canvas/hooks/useInsertFromCreateSelection.ts b/src/views/Editor/Canvas/hooks/useInsertFromCreateSelection.ts index da498407..a3d4e94e 100644 --- a/src/views/Editor/Canvas/hooks/useInsertFromCreateSelection.ts +++ b/src/views/Editor/Canvas/hooks/useInsertFromCreateSelection.ts @@ -8,6 +8,7 @@ export default (viewportRef: Ref) => { const canvasScale = computed(() => store.state.canvasScale) const creatingElement = computed(() => store.state.creatingElement) + // 通过鼠标框选时的起点和终点,计算选区的位置大小 const formatCreateSelection = (selectionData: CreateElementSelectionData) => { const { start, end } = selectionData @@ -29,6 +30,7 @@ export default (viewportRef: Ref) => { return { left, top, width, height } } + // 通过鼠标框选时的起点和终点,计算线条在画布中的位置和起点终点 const formatCreateSelectionForLine = (selectionData: CreateElementSelectionData) => { const { start, end } = selectionData @@ -66,6 +68,7 @@ export default (viewportRef: Ref) => { const { createTextElement, createShapeElement, createLineElement } = useCreateElement() + // 根据鼠标选区的位置大小插入元素 const insertElementFromCreateSelection = (selectionData: CreateElementSelectionData) => { if (!creatingElement.value) return diff --git a/src/views/Editor/Canvas/hooks/useMouseSelection.ts b/src/views/Editor/Canvas/hooks/useMouseSelection.ts index afc8e00c..08bb0e49 100644 --- a/src/views/Editor/Canvas/hooks/useMouseSelection.ts +++ b/src/views/Editor/Canvas/hooks/useMouseSelection.ts @@ -16,6 +16,7 @@ export default (elementList: Ref, viewportRef: Ref { if (!viewportRef.value) return @@ -30,6 +31,7 @@ export default (elementList: Ref, viewportRef: Ref, viewportRef: Ref 0 && offsetHeight > 0 ) quadrant = 4 else if ( offsetWidth < 0 && offsetHeight < 0 ) quadrant = 1 else if ( offsetWidth > 0 && offsetHeight < 0 ) quadrant = 2 else if ( offsetWidth < 0 && offsetHeight > 0 ) quadrant = 3 + // 更新框选范围 mouseSelectionState.isShow = true mouseSelectionState.quadrant = quadrant mouseSelectionState.width = width @@ -68,8 +73,7 @@ export default (elementList: Ref, viewportRef: Ref, viewportRef: Ref mouseSelectionLeft && @@ -108,11 +113,11 @@ export default (elementList: Ref, viewportRef: Ref { if (inRangeElement.groupId) { const inRangeElementIdList = inRangeElementList.map(inRangeElement => inRangeElement.id) diff --git a/src/views/Editor/Canvas/hooks/useRotateElement.ts b/src/views/Editor/Canvas/hooks/useRotateElement.ts index f7c5f551..532c12c2 100644 --- a/src/views/Editor/Canvas/hooks/useRotateElement.ts +++ b/src/views/Editor/Canvas/hooks/useRotateElement.ts @@ -3,9 +3,11 @@ import { MutationTypes, useStore } from '@/store' import { PPTElement, PPTTextElement, PPTImageElement, PPTShapeElement } from '@/types/slides' import useHistorySnapshot from '@/hooks/useHistorySnapshot' -// 给定一个坐标,计算该坐标到(0, 0)点连线的弧度值 -// 注意,Math.atan2的一般用法是Math.atan2(y, x)返回的是原点(0,0)到(x,y)点的线段与X轴正方向之间的弧度值 -// 这里将使用时将x与y的传入顺序交换了,为的是获取原点(0,0)到(x,y)点的线段与Y轴正方向之间的弧度值 +/** + * 计算给定坐标到原点连线的弧度 + * @param x 坐标x + * @param y 坐标y + */ const getAngleFromCoordinate = (x: number, y: number) => { const radian = Math.atan2(x, y) const angle = 180 / Math.PI * radian @@ -18,16 +20,18 @@ export default (elementList: Ref, viewportRef: Ref { let isMouseDown = true let angle = 0 const elOriginRotate = element.rotate || 0 - // 计算元素中心(旋转的中心,坐标原点) const elLeft = element.left const elTop = element.top const elWidth = element.width const elHeight = element.height + + // 元素中心点(旋转中心点) const centerX = elLeft + elWidth / 2 const centerY = elTop + elHeight / 2 @@ -37,7 +41,7 @@ export default (elementList: Ref, viewportRef: Ref { if (!isMouseDown) return - // 计算鼠标基于旋转中心的坐标 + // 计算当前鼠标位置相对元素中心点连线的角度(弧度) const mouseX = (e.pageX - viewportRect.left) / canvasScale.value const mouseY = (e.pageY - viewportRect.top) / canvasScale.value const x = mouseX - centerX @@ -45,7 +49,7 @@ export default (elementList: Ref, viewportRef: Ref 0 && Math.abs(angle - 45) <= sorptionRange ) angle -= (angle - 45) @@ -57,7 +61,6 @@ export default (elementList: Ref, viewportRef: Ref 0 && Math.abs(angle - 180) <= sorptionRange ) angle -= (angle - 180) else if ( angle < 0 && Math.abs(angle + 180) <= sorptionRange ) angle -= (angle + 180) - // 修改元素角度 elementList.value = elementList.value.map(el => element.id === el.id ? { ...el, rotate: angle } : el) } diff --git a/src/views/Editor/Canvas/hooks/useScaleElement.ts b/src/views/Editor/Canvas/hooks/useScaleElement.ts index ce515544..aa257a18 100644 --- a/src/views/Editor/Canvas/hooks/useScaleElement.ts +++ b/src/views/Editor/Canvas/hooks/useScaleElement.ts @@ -8,13 +8,18 @@ import { MIN_SIZE } from '@/configs/element' import { AlignLine, uniqAlignLines } from '@/utils/element' import useHistorySnapshot from '@/hooks/useHistorySnapshot' -// 计算元素被旋转一定角度后,八个操作点的新坐标 interface RotateElementData { left: number; top: number; width: number; height: number; } + +/** + * 计算旋转后的元素八个缩放点的位置 + * @param element 元素原始位置大小信息 + * @param angle 旋转角度 + */ const getRotateElementPoints = (element: RotateElementData, angle: number) => { const { left, top, width, height } = element @@ -68,7 +73,11 @@ const getRotateElementPoints = (element: RotateElementData, angle: number) => { return { leftTopPoint, topPoint, rightTopPoint, rightPoint, rightBottomPoint, bottomPoint, leftBottomPoint, leftPoint } } -// 获取元素某个操作点对角线上另一端的操作点坐标(例如:左上 <-> 右下) +/** + * 获取元素某缩放点相对的另一个点的位置,如:【上】对应【下】、【左上】对应【右下】 + * @param direction 当前操作的缩放点 + * @param points 旋转后的元素八个缩放点的位置 + */ const getOppositePoint = (direction: string, points: ReturnType): { left: number; top: number } => { const oppositeMap = { [OperateResizeHandlers.RIGHT_BOTTOM]: points.leftTopPoint, @@ -95,6 +104,7 @@ export default ( const { addHistorySnapshot } = useHistorySnapshot() + // 缩放元素 const scaleElement = (e: MouseEvent, element: Exclude, command: OperateResizeHandlers) => { let isMouseDown = true emitter.emit(EmitterEvents.SCALE_ELEMENT_STATE, true) @@ -103,16 +113,17 @@ export default ( const elOriginTop = element.top const elOriginWidth = element.width const elOriginHeight = element.height - - const fixedRatio = ctrlOrShiftKeyActive.value || ('fixedRatio' in element && element.fixedRatio) - const aspectRatio = elOriginWidth / elOriginHeight const elRotate = ('rotate' in element && element.rotate) ? element.rotate : 0 const rotateRadian = Math.PI * elRotate / 180 + const fixedRatio = ctrlOrShiftKeyActive.value || ('fixedRatio' in element && element.fixedRatio) + const aspectRatio = elOriginWidth / elOriginHeight + const startPageX = e.pageX const startPageY = e.pageY + // 元素最小缩放限制 const minSize = MIN_SIZE[element.type] || 20 const getSizeWithinRange = (size: number) => size < minSize ? minSize : size @@ -122,16 +133,20 @@ export default ( let horizontalLines: AlignLine[] = [] let verticalLines: AlignLine[] = [] + // 旋转后的元素进行缩放时,引入基点的概念,以当前操作的缩放点相对的点为基点 + // 例如拖动右下角缩放时,左上角为基点,需要保持左上角不变然后修改其他的点的位置来达到所放的效果 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 } + + // 未旋转的元素具有缩放时的对齐吸附功能,在这处收集对齐对齐吸附线 + // 包括页面内除目标元素外的其他元素在画布中的各个可吸附对齐位置:上下左右四边 + // 其中线条和被旋转过的元素不参与吸附对齐 else { const edgeWidth = VIEWPORT_SIZE const edgeHeight = VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO @@ -159,7 +174,7 @@ export default ( verticalLines.push(leftLine, rightLine) } - // 页面边界、水平和垂直的中心位置 + // 画布可视区域的四个边界、水平中心、垂直中心 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] } @@ -175,6 +190,8 @@ export default ( } // 对齐吸附方法 + // 将收集到的对齐吸附线与计算的目标元素当前的位置大小相关数据做对比,差值小于设定的值时执行自动缩放校正 + // 水平和垂直两个方向需要分开计算 const alignedAdsorption = (currentX: number | null, currentY: number | null) => { const sorptionRange = 5 @@ -213,6 +230,7 @@ export default ( return correctionVal } + // 开始缩放 document.onmousemove = e => { if (!isMouseDown) return @@ -227,21 +245,22 @@ export default ( let left = elOriginLeft let top = elOriginTop - // 元素被旋转的情况下 + // 元素被旋转的情况下,需要根据元素旋转的角度,重新计算需要缩放的距离(鼠标按下后移动的距离) if (elRotate) { - // 根据元素旋转的角度,修正鼠标按下后移动的距离(因为拖动的方向发生了改变) const revisedX = (Math.cos(rotateRadian) * x + Math.sin(rotateRadian) * y) / canvasScale.value let revisedY = (Math.cos(rotateRadian) * y - Math.sin(rotateRadian) * x) / canvasScale.value - // 锁定宽高比例 + // 锁定宽高比例(仅四个角可能触发,四条边不会触发) + // 以水平方向上缩放的距离为基础,计算垂直方向上的缩放距离,保持二者具有相同的缩放比例 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 } // 根据不同的操作点分别计算元素缩放后的大小和位置 - // 这里计算的位置是错误的,因为旋转后缩放实际上也改变了元素的位置,需要在后面进行矫正 - // 这里计算的大小是正确的,因为上面修正鼠标按下后移动的距离时其实已经进行过了矫正 + // 需要注意: + // 此处计算的位置需要在后面重新进行校正,因为旋转后再缩放事实上会改变元素基点的位置(虽然视觉上基点保持不动,但这是【旋转】+【移动】共同作用的结果) + // 但此处计算的大小不需要重新校正,因为前面已经重新计算需要缩放的距离,相当于大小已经经过了校正 if (command === OperateResizeHandlers.RIGHT_BOTTOM) { width = getSizeWithinRange(elOriginWidth + revisedX) height = getSizeWithinRange(elOriginHeight + revisedY) @@ -277,7 +296,7 @@ export default ( width = getSizeWithinRange(elOriginWidth + revisedX) } - // 获取当前元素基点坐标,与初始状态的基点坐标进行对比并矫正差值 + // 获取当前元素的基点坐标,与初始状态时的基点坐标进行对比,并计算差值进行元素位置的校正 const currentPoints = getRotateElementPoints({ width, height, left, top }, elRotate) const currentOppositePoint = getOppositePoint(command, currentPoints) const currentBaseLeft = currentOppositePoint.left @@ -290,7 +309,9 @@ export default ( top = top - offsetY } - // 元素未被旋转的情况下,根据所操纵点的位置添加对齐吸附 + // 元素未被旋转的情况下,正常计算新的位置大小即可,无需复杂的校正等工作 + // 额外需要处理对齐吸附相关的操作 + // 锁定宽高比例相关的操作同上,不再赘述 else { let moveX = x / canvasScale.value let moveY = y / canvasScale.value @@ -390,6 +411,7 @@ export default ( } } + // 多选元素缩放 const scaleMultiElement = (e: MouseEvent, range: MultiSelectRange, command: OperateResizeHandlers) => { let isMouseDown = true @@ -409,17 +431,16 @@ export default ( const currentPageX = e.pageX const currentPageY = e.pageY - // 鼠标按下后移动的距离 const x = (currentPageX - startPageX) / canvasScale.value let y = (currentPageY - startPageY) / canvasScale.value - // 锁定宽高比例 + // 锁定宽高比例,逻辑同上 if (ctrlOrShiftKeyActive.value) { if (command === OperateResizeHandlers.RIGHT_BOTTOM || command === OperateResizeHandlers.LEFT_TOP) y = x / aspectRatio if (command === OperateResizeHandlers.LEFT_BOTTOM || command === OperateResizeHandlers.RIGHT_TOP) y = -x / aspectRatio } - // 获取鼠标缩放时当前所有激活元素的范围 + // 所有选中元素的整体范围 let currentMinX = minX let currentMaxX = maxX let currentMinY = minY @@ -454,19 +475,18 @@ export default ( currentMaxX = maxX + x } - // 多选下所有元素整体宽高 + // 所有选中元素的整体宽高 const currentOppositeWidth = currentMaxX - currentMinX const currentOppositeHeight = currentMaxY - currentMinY - // 所有元素的整体宽高与被操作元素宽高的比例 + // 当前正在操作元素宽高占所有选中元素的整体宽高的比例 let widthScale = currentOppositeWidth / operateWidth let heightScale = currentOppositeHeight / operateHeight if (widthScale <= 0) widthScale = 0 if (heightScale <= 0) heightScale = 0 - // 根据上面计算的比例,修改所有被激活元素的位置大小 - // 宽高通过乘以对应的比例得到,位置通过将被操作元素在所有元素整体中的相对位置乘以对应比例获得 + // 根据前面计算的比例,计算并修改所有选中元素的位置大小 elementList.value = elementList.value.map(el => { if ((el.type === 'image' || el.type === 'shape') && activeElementIdList.value.includes(el.id)) { const originElement = originElementList.find(originEl => originEl.id === el.id) as PPTImageElement | PPTShapeElement diff --git a/src/views/Editor/Canvas/hooks/useSelectElement.ts b/src/views/Editor/Canvas/hooks/useSelectElement.ts index d55c07bf..8fb29c21 100644 --- a/src/views/Editor/Canvas/hooks/useSelectElement.ts +++ b/src/views/Editor/Canvas/hooks/useSelectElement.ts @@ -14,10 +14,13 @@ export default ( const editorAreaFocus = computed(() => store.state.editorAreaFocus) const ctrlOrShiftKeyActive = computed(() => store.getters.ctrlOrShiftKeyActive) + // 选中元素 const selectElement = (e: MouseEvent, element: PPTElement, canMove = true) => { if (!editorAreaFocus.value) store.commit(MutationTypes.SET_EDITORAREA_FOCUS, true) - // 如果被点击的元素处于未激活状态,则将他设置为激活元素(单选),或者加入到激活元素中(多选) + // 如果目标元素当前未被选中,则将他设为选中状态 + // 此时如果按下Ctrl键或Shift键,则进入多选状态,将当前已选中的元素和目标元素一桶设置为选中状态,否则仅将目标元素设置为选中状态 + // 如果目标元素是分组成员,需要将该组合的其他元素一起设置为选中状态 if (!activeElementIdList.value.includes(element.id)) { let newActiveIdList: string[] = [] @@ -26,7 +29,6 @@ export default ( } else newActiveIdList = [element.id] - // 同时如果该元素是分组成员,需要将和他同组的元素一起激活 if (element.groupId) { const groupMembersId: string[] = [] elementList.value.forEach((el: PPTElement) => { @@ -39,11 +41,12 @@ export default ( store.commit(MutationTypes.SET_HANDLE_ELEMENT_ID, element.id) } - // 如果被点击的元素已激活,且按下了多选按钮,则取消其激活状态(除非该元素或分组是最后的一个激活元素) + // 如果目标元素已被选中,且按下了Ctrl键或Shift键,则取消其被选中状态 + // 除非目标元素是最后的一个被选中元素,或者目标元素所在的组合是最后一组选中组合 + // 如果目标元素是分组成员,需要将该组合的其他元素一起取消选中状态 else if (ctrlOrShiftKeyActive.value) { let newActiveIdList: string[] = [] - // 同时如果该元素是分组成员,需要将和他同组的元素一起取消 if (element.groupId) { const groupMembersId: string[] = [] elementList.value.forEach((el: PPTElement) => { @@ -60,12 +63,12 @@ export default ( } } - // 如果被点击的元素已激活,且没有按下多选按钮,且该元素不是当前操作元素,则将其设置为当前操作元素 + // 如果目标元素已被选中,同时目标元素不是当前操作元素,则将其设置为当前操作元素 else if (handleElementId.value !== element.id) { store.commit(MutationTypes.SET_HANDLE_ELEMENT_ID, element.id) } - // 如果被点击的元素是当前操作元素,且没有按下多选按钮,则该元素下次保持该状态再次被点击时,将被设置为多选元素中的激活成员 + // 如果目标元素已被选中,同时也是当前操作元素,那么当目标元素在该状态下再次被点击时,将被设置为多选元素中的激活成员 else if (activeGroupElementId.value !== element.id) { const startPageX = e.pageX const startPageY = e.pageY @@ -84,6 +87,7 @@ export default ( if (canMove) moveElement(e, element) } + // 选中页面内的全部元素 const selectAllElement = () => { const unlockedElements = elementList.value.filter(el => !el.lock) const newActiveElementIdList = unlockedElements.map(el => el.id) diff --git a/src/views/Editor/Canvas/hooks/useViewportSize.ts b/src/views/Editor/Canvas/hooks/useViewportSize.ts index 18808d35..30ed0188 100644 --- a/src/views/Editor/Canvas/hooks/useViewportSize.ts +++ b/src/views/Editor/Canvas/hooks/useViewportSize.ts @@ -9,7 +9,8 @@ export default (canvasRef: Ref) => { const store = useStore() const canvasPercentage = computed(() => store.state.canvasPercentage) - const setViewportSize = () => { + // 计算画布可视区域的位置 + const setViewportPosition = () => { if (!canvasRef.value) return const canvasWidth = canvasRef.value.clientWidth const canvasHeight = canvasRef.value.clientHeight @@ -28,8 +29,10 @@ export default (canvasRef: Ref) => { } } - watch(canvasPercentage, setViewportSize) + // 可视区域缩放时,更新可视区域的位置 + watch(canvasPercentage, setViewportPosition) + // 画布可视区域位置和大小的样式 const viewportStyles = computed(() => ({ width: VIEWPORT_SIZE, height: VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO, @@ -37,7 +40,8 @@ export default (canvasRef: Ref) => { top: viewportTop.value, })) - const resizeObserver = new ResizeObserver(setViewportSize) + // 监听画布尺寸发生变化时,更新可视区域的位置 + const resizeObserver = new ResizeObserver(setViewportPosition) onMounted(() => { if (canvasRef.value) resizeObserver.observe(canvasRef.value) diff --git a/src/views/Editor/Canvas/index.vue b/src/views/Editor/Canvas/index.vue index d94dd06d..2c934dda 100644 --- a/src/views/Editor/Canvas/index.vue +++ b/src/views/Editor/Canvas/index.vue @@ -159,6 +159,7 @@ export default defineComponent({ const { pasteElement } = useCopyAndPasteElement() const { enterScreening } = useScreening() + // 点击画布的空白区域:清空焦点元素、设置画布焦点、清除文字选区 const handleClickBlankArea = (e: MouseEvent) => { store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, []) if (!ctrlOrShiftKeyActive.value) updateMouseSelection(e) @@ -166,10 +167,12 @@ export default defineComponent({ removeAllRanges() } + // 移除画布编辑区域焦点 const removeEditorAreaFocus = () => { if (editorAreaFocus.value) store.commit(MutationTypes.SET_EDITORAREA_FOCUS, false) } + // 按住Ctrl键滚动鼠标缩放画布 const { scaleCanvas } = useScaleCanvas() const throttleScaleCanvas = throttle(scaleCanvas, 100, { leading: true, trailing: false }) @@ -181,11 +184,13 @@ export default defineComponent({ else if (e.deltaY < 0) throttleScaleCanvas('+') } + // 开关网格线 const showGridLines = computed(() => store.state.showGridLines) const toggleGridLines = () => { store.commit(MutationTypes.SET_GRID_LINES_STATE, !showGridLines.value) } + // 在鼠标绘制的范围插入元素 const creatingElement = computed(() => store.state.creatingElement) const { insertElementFromCreateSelection } = useInsertFromCreateSelection(viewportRef)