| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501 |
- import { computed, Ref } from 'vue'
- import { useStore } from 'vuex'
- import { State, MutationTypes } from '@/store'
- import { ElementTypes, PPTElement, PPTImageElement, PPTLineElement, PPTShapeElement } from '@/types/slides'
- import { OperatePoints, ElementScaleHandler, AlignmentLineProps, MultiSelectRange } from '@/types/edit'
- import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
- import { AlignLine, uniqAlignLines } from '@/utils/element'
- import useHistorySnapshot from '@/hooks/useHistorySnapshot'
- // 计算元素被旋转一定角度后,八个操作点的新坐标
- interface RotateElementData {
- left: number;
- top: number;
- width: number;
- height: number;
- }
- 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 }
- }
- // 获取元素某个操作点对角线上另一端的操作点坐标(例如:左上 <-> 右下)
- const getOppositePoint = (direction: number, points: ReturnType<typeof getRotateElementPoints>): { left: number; top: number } => {
- const oppositeMap = {
- [OperatePoints.RIGHT_BOTTOM]: points.leftTopPoint,
- [OperatePoints.LEFT_BOTTOM]: points.rightTopPoint,
- [OperatePoints.LEFT_TOP]: points.rightBottomPoint,
- [OperatePoints.RIGHT_TOP]: points.leftBottomPoint,
- [OperatePoints.TOP]: points.bottomPoint,
- [OperatePoints.BOTTOM]: points.topPoint,
- [OperatePoints.LEFT]: points.rightPoint,
- [OperatePoints.RIGHT]: points.leftPoint,
- }
- return oppositeMap[direction]
- }
- export default (
- elementList: Ref<PPTElement[]>,
- activeGroupElementId: Ref<string>,
- alignmentLines: Ref<AlignmentLineProps[]>,
- ) => {
- const store = useStore<State>()
- const activeElementIdList = computed(() => store.state.activeElementIdList)
- const ctrlOrShiftKeyActive: Ref<boolean> = computed(() => store.getters.ctrlOrShiftKeyActive)
- const canvasScale = computed(() => store.state.canvasScale)
- const { addHistorySnapshot } = useHistorySnapshot()
- const scaleElement = (e: MouseEvent, element: Exclude<PPTElement, PPTLineElement>, command: ElementScaleHandler) => {
- let isMouseDown = true
- const elOriginLeft = element.left
- 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 startPageX = e.pageX
- const startPageY = e.pageY
- const minSize = 15
- const getSizeWithinRange = (size: number) => size < minSize ? minSize : size
- let points: ReturnType<typeof getRotateElementPoints>
- let baseLeft = 0
- let baseTop = 0
- 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
- const isActiveGroupElement = element.id === activeGroupElementId.value
-
- for(const el of elementList.value) {
- if('rotate' in el && el.rotate) continue
- if(el.type === ElementTypes.LINE) continue
- if(isActiveGroupElement && el.id === element.id) continue
- if(!isActiveGroupElement && activeElementIdList.value.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)
- }
- // 页面边界、水平和垂直的中心位置
- 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)
- }
-
- // 对齐吸附方法
- const alignedAdsorption = (currentX: number | null, currentY: number | null) => {
- const sorptionRange = 3
- 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) {
- if(!isHorizontalAdsorbed) {
- correctionVal.offsetY = currentY - value
- isHorizontalAdsorbed = true
- }
- _alignmentLines.push({type: 'horizontal', axis: {x: min - 20, y: value}, length: max - min + 40})
- }
- }
- }
- 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) {
- if(!isVerticalAdsorbed) {
- correctionVal.offsetX = currentX - value
- isVerticalAdsorbed = true
- }
- _alignmentLines.push({ type: 'vertical', axis: {x: value, y: min - 20}, length: max - min + 40 })
- }
- }
- }
- alignmentLines.value = _alignmentLines
- return correctionVal
- }
- document.onmousemove = e => {
- if(!isMouseDown) return
- const currentPageX = e.pageX
- const currentPageY = e.pageY
- const x = currentPageX - startPageX
- const y = currentPageY - startPageY
- let width = elOriginWidth
- let height = elOriginHeight
- 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 === OperatePoints.RIGHT_BOTTOM || command === OperatePoints.LEFT_TOP) revisedY = revisedX / aspectRatio
- if(command === OperatePoints.LEFT_BOTTOM || command === OperatePoints.RIGHT_TOP) revisedY = -revisedX / aspectRatio
- }
- // 根据不同的操作点分别计算元素缩放后的大小和位置
- // 这里计算的位置是错误的,因为旋转后缩放实际上也改变了元素的位置,需要在后面进行矫正
- // 这里计算的大小是正确的,因为上面修正鼠标按下后移动的距离时其实已经进行过了矫正
- if(command === OperatePoints.RIGHT_BOTTOM) {
- width = getSizeWithinRange(elOriginWidth + revisedX)
- height = getSizeWithinRange(elOriginHeight + revisedY)
- }
- else if(command === OperatePoints.LEFT_BOTTOM) {
- width = getSizeWithinRange(elOriginWidth - revisedX)
- height = getSizeWithinRange(elOriginHeight + revisedY)
- left = elOriginLeft - (width - elOriginWidth)
- }
- else if(command === OperatePoints.LEFT_TOP) {
- width = getSizeWithinRange(elOriginWidth - revisedX)
- height = getSizeWithinRange(elOriginHeight - revisedY)
- left = elOriginLeft - (width - elOriginWidth)
- top = elOriginTop - (height - elOriginHeight)
- }
- else if(command === OperatePoints.RIGHT_TOP) {
- width = getSizeWithinRange(elOriginWidth + revisedX)
- height = getSizeWithinRange(elOriginHeight - revisedY)
- top = elOriginTop - (height - elOriginHeight)
- }
- else if(command === OperatePoints.TOP) {
- height = getSizeWithinRange(elOriginHeight - revisedY)
- top = elOriginTop - (height - elOriginHeight)
- }
- else if(command === OperatePoints.BOTTOM) {
- height = getSizeWithinRange(elOriginHeight + revisedY)
- }
- else if(command === OperatePoints.LEFT) {
- width = getSizeWithinRange(elOriginWidth - revisedX)
- left = elOriginLeft - (width - elOriginWidth)
- }
- else if(command === OperatePoints.RIGHT) {
- width = getSizeWithinRange(elOriginWidth + revisedX)
- }
- // 获取当前元素基点坐标,与初始状态的基点坐标进行对比并矫正差值
- 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
- }
- // 元素未被旋转的情况下,根据所操纵点的位置添加对齐吸附
- else {
- let moveX = x / canvasScale.value
- let moveY = y / canvasScale.value
- if(fixedRatio) {
- if(command === OperatePoints.RIGHT_BOTTOM || command === OperatePoints.LEFT_TOP) moveY = moveX / aspectRatio
- if(command === OperatePoints.LEFT_BOTTOM || command === OperatePoints.RIGHT_TOP) moveY = -moveX / aspectRatio
- }
- if(command === OperatePoints.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)
- height = getSizeWithinRange(elOriginHeight + moveY)
- }
- else if(command === OperatePoints.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)
- height = getSizeWithinRange(elOriginHeight + moveY)
- left = elOriginLeft - (width - elOriginWidth)
- }
- else if(command === OperatePoints.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)
- height = getSizeWithinRange(elOriginHeight - moveY)
- left = elOriginLeft - (width - elOriginWidth)
- top = elOriginTop - (height - elOriginHeight)
- }
- else if(command === OperatePoints.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)
- height = getSizeWithinRange(elOriginHeight - moveY)
- top = elOriginTop - (height - elOriginHeight)
- }
- else if(command === OperatePoints.LEFT) {
- const { offsetX } = alignedAdsorption(elOriginLeft + moveX, null)
- moveX = moveX - offsetX
- width = getSizeWithinRange(elOriginWidth - moveX)
- left = elOriginLeft - (width - elOriginWidth)
- }
- else if(command === OperatePoints.RIGHT) {
- const { offsetX } = alignedAdsorption(elOriginLeft + elOriginWidth + moveX, null)
- moveX = moveX - offsetX
- width = getSizeWithinRange(elOriginWidth + moveX)
- }
- else if(command === OperatePoints.TOP) {
- const { offsetY } = alignedAdsorption(null, elOriginTop + moveY)
- moveY = moveY - offsetY
- height = getSizeWithinRange(elOriginHeight - moveY)
- top = elOriginTop - (height - elOriginHeight)
- }
- else if(command === OperatePoints.BOTTOM) {
- const { offsetY } = alignedAdsorption(null, elOriginTop + elOriginHeight + moveY)
- moveY = moveY - offsetY
- height = getSizeWithinRange(elOriginHeight + moveY)
- }
- }
-
- elementList.value = elementList.value.map(el => element.id === el.id ? { ...el, left, top, width, height } : el)
- }
- document.onmouseup = e => {
- isMouseDown = false
- document.onmousemove = null
- document.onmouseup = null
- alignmentLines.value = []
- if(startPageX === e.pageX && startPageY === e.pageY) return
- store.commit(MutationTypes.UPDATE_SLIDE, { elements: elementList.value })
- addHistorySnapshot()
- }
- }
- const scaleMultiElement = (e: MouseEvent, range: MultiSelectRange, command: ElementScaleHandler) => {
- 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(elementList.value))
- document.onmousemove = e => {
- if(!isMouseDown) return
-
- 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 === OperatePoints.RIGHT_BOTTOM || command === OperatePoints.LEFT_TOP) y = x / aspectRatio
- if(command === OperatePoints.LEFT_BOTTOM || command === OperatePoints.RIGHT_TOP) y = -x / aspectRatio
- }
- // 获取鼠标缩放时当前所有激活元素的范围
- let currentMinX = minX
- let currentMaxX = maxX
- let currentMinY = minY
- let currentMaxY = maxY
- if(command === OperatePoints.RIGHT_BOTTOM) {
- currentMaxX = maxX + x
- currentMaxY = maxY + y
- }
- else if(command === OperatePoints.LEFT_BOTTOM) {
- currentMinX = minX + x
- currentMaxY = maxY + y
- }
- else if(command === OperatePoints.LEFT_TOP) {
- currentMinX = minX + x
- currentMinY = minY + y
- }
- else if(command === OperatePoints.RIGHT_TOP) {
- currentMaxX = maxX + x
- currentMinY = minY + y
- }
- else if(command === OperatePoints.TOP) {
- currentMinY = minY + y
- }
- else if(command === OperatePoints.BOTTOM) {
- currentMaxY = maxY + y
- }
- else if(command === OperatePoints.LEFT) {
- currentMinX = minX + x
- }
- else if(command === OperatePoints.RIGHT) {
- 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 === ElementTypes.IMAGE || el.type === ElementTypes.SHAPE) && activeElementIdList.value.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
- })
- }
- document.onmouseup = e => {
- isMouseDown = false
- document.onmousemove = null
- document.onmouseup = null
- if(startPageX === e.pageX && startPageY === e.pageY) return
- store.commit(MutationTypes.UPDATE_SLIDE, { elements: elementList.value })
- addHistorySnapshot()
- }
- }
- return {
- scaleElement,
- scaleMultiElement,
- }
- }
|