This commit is contained in:
pipipi-pikachu 2020-12-19 22:14:29 +08:00
parent 9c63766314
commit 071c608fcf
22 changed files with 1493 additions and 1404 deletions

View File

@ -15,17 +15,7 @@
<script lang="ts"> <script lang="ts">
import { PropType } from 'vue' import { PropType } from 'vue'
import { AlignmentLineAxis } from './types/index'
interface Axis {
x: number;
y: number;
}
export interface AlignmentLineProps {
type: 'vertical' | 'horizontal';
axis: Axis;
length: number;
}
export default { export default {
name: 'alignment-line', name: 'alignment-line',
@ -35,7 +25,7 @@ export default {
required: true, required: true,
}, },
axis: { axis: {
type: Object as PropType<Axis>, type: Object as PropType<AlignmentLineAxis>,
required: true, required: true,
}, },
length: { length: {

View File

@ -32,12 +32,7 @@ import { ElementScaleHandler, OperateResizablePointTypes, OperateBorderLineTypes
import ResizablePoint from '@/views/_common/_operate/ResizablePoint.vue' import ResizablePoint from '@/views/_common/_operate/ResizablePoint.vue'
import BorderLine from '@/views/_common/_operate/BorderLine.vue' import BorderLine from '@/views/_common/_operate/BorderLine.vue'
export interface MultiSelectRange { import { MultiSelectRange } from './types/index'
minX: number;
maxX: number;
minY: number;
maxY: number;
}
export default defineComponent({ export default defineComponent({
name: 'multi-select-operate', name: 'multi-select-operate',

View File

@ -0,0 +1,54 @@
import { Ref } from 'vue'
import { useStore } from 'vuex'
import { State, MutationTypes } from '@/store'
import { PPTElement } from '@/types/slides'
import { ElementAlignCommand, ElementAlignCommands } from '@/types/edit'
import { getElementListRange } from '../utils/elementRange'
import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
export default (elementList: Ref<PPTElement[]>, activeElementList: Ref<PPTElement[]>, activeElementIdList: Ref<string[]>) => {
const store = useStore<State>()
const alignElementToCanvas = (command: ElementAlignCommand) => {
const viewportWidth = VIEWPORT_SIZE
const viewportHeight = VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO
const { minX, maxX, minY, maxY } = getElementListRange(activeElementList.value)
const newElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList.value))
for(const element of newElementList) {
if(!activeElementIdList.value.includes(element.elId)) continue
if(command === ElementAlignCommands.TOP) {
const offsetY = minY - 0
element.top = element.top - offsetY
}
else if(command === ElementAlignCommands.VERTICAL) {
const offsetY = minY + (maxY - minY) / 2 - viewportHeight / 2
element.top = element.top - offsetY
}
else if(command === ElementAlignCommands.BOTTOM) {
const offsetY = maxY - viewportHeight
element.top = element.top - offsetY
}
else if(command === ElementAlignCommands.LEFT) {
const offsetX = minX - 0
element.left = element.left - offsetX
}
else if(command === ElementAlignCommands.HORIZONTAL) {
const offsetX = minX + (maxX - minX) / 2 - viewportWidth / 2
element.left = element.left - offsetX
}
else if(command === ElementAlignCommands.RIGHT) {
const offsetX = maxX - viewportWidth
element.left = element.left - offsetX
}
}
store.commit(MutationTypes.UPDATE_SLIDE, { elements: newElementList })
}
return {
alignElementToCanvas,
}
}

View File

@ -0,0 +1,53 @@
import { Ref } from 'vue'
import { useStore } from 'vuex'
import { State, MutationTypes } from '@/store'
import { PPTElement } from '@/types/slides'
import { createRandomCode } from '@/utils/common'
export default (elementList: Ref<PPTElement[]>, activeElementList: Ref<PPTElement[]>, activeElementIdList: Ref<string[]>) => {
const store = useStore<State>()
// 组合元素为当前所有激活元素添加一个相同的groupId
const combineElements = () => {
if(!activeElementList.value.length) return null
let newElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
const groupId = createRandomCode()
const combineElementList: PPTElement[] = []
for(const element of newElementList) {
if(activeElementIdList.value.includes(element.elId)) {
element.groupId = groupId
combineElementList.push(element)
}
}
// 注意,组合元素的层级应该是连续的,所以需要获取该组元素中最顶层的元素,将组内其他成员从原位置移动到最顶层的元素的下面
const combineElementMaxIndex = newElementList.findIndex(_element => _element.elId === combineElementList[combineElementList.length - 1].elId)
const combineElementIdList = combineElementList.map(_element => _element.elId)
newElementList = newElementList.filter(_element => !combineElementIdList.includes(_element.elId))
const insertIndex = combineElementMaxIndex - combineElementList.length + 1
newElementList.splice(insertIndex, 0, ...combineElementList)
store.commit(MutationTypes.UPDATE_SLIDE, { elements: newElementList })
}
// 取消组合元素移除所有被激活元素的groupId
const uncombineElements = () => {
if(!activeElementList.value.length) return null
const hasElementInGroup = activeElementList.value.some(item => item.groupId)
if(!hasElementInGroup) return null
const newElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
for(const element of newElementList) {
if(activeElementIdList.value.includes(element.elId) && element.groupId) delete element.groupId
}
store.commit(MutationTypes.UPDATE_SLIDE, { elements: newElementList })
}
return {
combineElements,
uncombineElements,
}
}

View File

@ -0,0 +1,49 @@
import { Ref } from 'vue'
import { useStore } from 'vuex'
import { State, MutationTypes } from '@/store'
import { PPTElement } from '@/types/slides'
import { copyText, readClipboard } from '@/utils/clipboard'
import { encrypt, decrypt } from '@/utils/crypto'
import { message } from 'ant-design-vue'
export default (deleteElement: () => void, activeElementList: Ref<PPTElement[]>, activeElementIdList: Ref<string[]>) => {
const store = useStore<State>()
const copyElement = () => {
if(!activeElementIdList.value.length) return
const text = encrypt(JSON.stringify({
type: 'elements',
data: activeElementList.value,
}))
copyText(text).then(() => {
store.commit(MutationTypes.SET_EDITORAREA_FOCUS, true)
message.success('元素已复制到剪贴板', 0.8)
})
}
const cutElement = () => {
copyElement()
deleteElement()
}
const pasteElement = () => {
readClipboard().then(text => {
let clipboardData
try {
clipboardData = JSON.parse(decrypt(text))
}
catch {
clipboardData = text
}
console.log(clipboardData)
}).catch(err => message.warning(err))
}
return {
copyElement,
cutElement,
pasteElement,
}
}

View File

@ -0,0 +1,26 @@
import { Ref } from 'vue'
import { useStore } from 'vuex'
import { State, MutationTypes } from '@/store'
import { PPTElement } from '@/types/slides'
export default (elementList: Ref<PPTElement[]>, activeElementIdList: Ref<string[]>) => {
const store = useStore<State>()
const deleteElement = () => {
if(!activeElementIdList.value.length) return
const newElementList = elementList.value.filter(el => !activeElementIdList.value.includes(el.elId))
store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, [])
store.commit(MutationTypes.UPDATE_SLIDE, { elements: newElementList })
}
const deleteAllElements = () => {
if(!elementList.value.length) return
store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, [])
store.commit(MutationTypes.UPDATE_SLIDE, { elements: [] })
}
return {
deleteElement,
deleteAllElements,
}
}

View File

@ -0,0 +1,41 @@
import { useStore } from 'vuex'
import { Ref } from 'vue'
import { State, MutationTypes } from '@/store'
import { PPTElement } from '@/types/slides'
export default (elementList: Ref<PPTElement[]>, activeElementIdList: Ref<string[]>) => {
const store = useStore<State>()
const lockElement = (handleElement: PPTElement) => {
const newElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList.value))
for(const element of newElementList) {
if(activeElementIdList.value.includes(handleElement.elId)) element.isLock = true
}
store.commit(MutationTypes.UPDATE_SLIDE, { elements: newElementList })
}
const unlockElement = (handleElement: PPTElement) => {
const newElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList.value))
if(handleElement.groupId) {
for(const element of newElementList) {
if(element.groupId === handleElement.groupId) element.isLock = false
}
return newElementList
}
for(const element of newElementList) {
if(element.elId === handleElement.elId) {
element.isLock = false
break
}
}
store.commit(MutationTypes.UPDATE_SLIDE, { elements: newElementList })
}
return {
lockElement,
unlockElement,
}
}

View File

@ -0,0 +1,135 @@
import { Ref, reactive } from 'vue'
import { useStore } from 'vuex'
import { State, MutationTypes } from '@/store'
import { PPTElement } from '@/types/slides'
import { getElementRange } from '../utils/elementRange'
export default (elementList: Ref<PPTElement[]>, viewportRef: Ref<HTMLElement | null>, canvasScale: Ref<number>) => {
const store = useStore<State>()
const mouseSelectionState = reactive({
isShow: false,
top: 0,
left: 0,
width: 0,
height: 0,
quadrant: 1,
})
const updateMouseSelection = (e: MouseEvent) => {
if(!viewportRef.value) return
let isMouseDown = true
const viewportRect = viewportRef.value.getBoundingClientRect()
const minSelectionRange = 5
const startPageX = e.pageX
const startPageY = e.pageY
const left = (startPageX - viewportRect.x) / canvasScale.value
const top = (startPageY - viewportRect.y) / canvasScale.value
mouseSelectionState.isShow = false
mouseSelectionState.quadrant = 4
mouseSelectionState.top = top
mouseSelectionState.left = left
mouseSelectionState.width = 0
mouseSelectionState.height = 0
document.onmousemove = e => {
if(!isMouseDown) return
const currentPageX = e.pageX
const currentPageY = e.pageY
const offsetWidth = (currentPageX - startPageX) / canvasScale.value
const offsetHeight = (currentPageY - startPageY) / canvasScale.value
const width = Math.abs(offsetWidth)
const height = Math.abs(offsetHeight)
if( width < minSelectionRange || height < minSelectionRange ) return
let quadrant = 0
if( offsetWidth > 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
mouseSelectionState.height = height
}
document.onmouseup = () => {
document.onmousemove = null
document.onmouseup = null
isMouseDown = false
// 计算当前页面中的每一个元素是否处在鼠标选择范围中(必须完全包裹)
// 将选择范围中的元素添加为激活元素
let inRangeElementList: PPTElement[] = []
for(let i = 0; i < elementList.value.length; i++) {
const element = elementList.value[i]
const mouseSelectionLeft = mouseSelectionState.left
const mouseSelectionTop = mouseSelectionState.top
const mouseSelectionWidth = mouseSelectionState.width
const mouseSelectionHeight = mouseSelectionState.height
const quadrant = mouseSelectionState.quadrant
const { minX, maxX, minY, maxY } = getElementRange(element)
let isInclude = false
if(quadrant === 4) {
isInclude = minX > mouseSelectionLeft &&
maxX < mouseSelectionLeft + mouseSelectionWidth &&
minY > mouseSelectionTop &&
maxY < mouseSelectionTop + mouseSelectionHeight
}
else if(quadrant === 1) {
isInclude = minX > (mouseSelectionLeft - mouseSelectionWidth) &&
maxX < (mouseSelectionLeft - mouseSelectionWidth) + mouseSelectionWidth &&
minY > (mouseSelectionTop - mouseSelectionHeight) &&
maxY < (mouseSelectionTop - mouseSelectionHeight) + mouseSelectionHeight
}
else if(quadrant === 2) {
isInclude = minX > mouseSelectionLeft &&
maxX < mouseSelectionLeft + mouseSelectionWidth &&
minY > (mouseSelectionTop - mouseSelectionHeight) &&
maxY < (mouseSelectionTop - mouseSelectionHeight) + mouseSelectionHeight
}
else if(quadrant === 3) {
isInclude = minX > (mouseSelectionLeft - mouseSelectionWidth) &&
maxX < (mouseSelectionLeft - mouseSelectionWidth) + mouseSelectionWidth &&
minY > mouseSelectionTop &&
maxY < mouseSelectionTop + mouseSelectionHeight
}
// 被锁定的元素除外
if(isInclude && !element.isLock) inRangeElementList.push(element)
}
// 对于组合元素成员,必须所有成员都在选择范围中才算被选中
inRangeElementList = inRangeElementList.filter(inRangeElement => {
if(inRangeElement.groupId) {
const inRangeElementIdList = inRangeElementList.map(inRangeElement => inRangeElement.elId)
const groupElementList = elementList.value.filter(element => element.groupId === inRangeElement.groupId)
return groupElementList.every(groupElement => inRangeElementIdList.includes(groupElement.elId))
}
return true
})
const inRangeElementIdList = inRangeElementList.map(inRangeElement => inRangeElement.elId)
if(inRangeElementIdList.length) store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, inRangeElementIdList)
mouseSelectionState.isShow = false
}
}
return {
mouseSelectionState,
updateMouseSelection,
}
}

View File

@ -0,0 +1,313 @@
import { Ref } from 'vue'
import { useStore } from 'vuex'
import { State, MutationTypes } from '@/store'
import { ElementTypes, PPTElement } from '@/types/slides'
import { AlignmentLineProps } from '../types/index'
import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
import { getRectRotatedRange } from '../utils/elementRange'
import { AlignLine, uniqAlignLines } from '../utils/alignLines'
export default (
elementList: Ref<PPTElement[]>,
activeElementIdList: Ref<string[]>,
activeGroupElementId: Ref<string>,
canvasScale: Ref<number>,
alignmentLines: Ref<AlignmentLineProps[]>,
) => {
const store = useStore<State>()
const moveElement = (e: MouseEvent, element: PPTElement) => {
if(!activeElementIdList.value.includes(element.elId)) return
let isMouseDown = true
// 可视范围宽高,用于边缘对齐吸附
const edgeWidth = VIEWPORT_SIZE
const edgeHeight = VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO
const originElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList.value))
const originActiveElementList = originElementList.filter(el => activeElementIdList.value.includes(el.elId))
const sorptionRange = 3
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
let isMisoperation: boolean | null = null
const isActiveGroupElement = element.elId === activeGroupElementId.value
// 收集对齐参考线
// 包括页面内出被操作元素以外的所有元素在页面内水平和垂直方向的范围和中心位置、页面边界和水平和垂直的中心位置
let horizontalLines: AlignLine[] = []
let verticalLines: AlignLine[] = []
// 元素在页面内水平和垂直方向的范围和中心位置(需要特殊计算线条和被旋转的元素)
for(const el of elementList.value) {
if(el.type === ElementTypes.LINE) continue
if(isActiveGroupElement && el.elId === element.elId) continue
if(!isActiveGroupElement && activeElementIdList.value.includes(el.elId)) continue
let left, top, width, height
if('rotate' in el && el.rotate) {
const { xRange, yRange } = getRectRotatedRange({
left: el.left,
top: el.top,
width: el.width,
height: el.height,
rotate: el.rotate,
})
left = xRange[0]
top = yRange[0]
width = xRange[1] - xRange[0]
height = yRange[1] - yRange[0]
}
else {
left = el.left
top = el.top
width = el.width
height = el.height
}
const right = left + width
const bottom = top + height
const centerX = top + height / 2
const centerY = left + width / 2
const topLine: AlignLine = { value: top, range: [left, right] }
const bottomLine: AlignLine = { value: bottom, range: [left, right] }
const horizontalCenterLine: AlignLine = { value: centerX, range: [left, right] }
const leftLine: AlignLine = { value: left, range: [top, bottom] }
const rightLine: AlignLine = { value: right, range: [top, bottom] }
const verticalCenterLine: AlignLine = { value: centerY, range: [top, bottom] }
horizontalLines.push(topLine, bottomLine, horizontalCenterLine)
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] }
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)
document.onmousemove = e => {
const currentPageX = e.pageX
const currentPageY = e.pageY
// 对于鼠标第一次滑动距离过小的操作判定为误操作
// 这里仅在误操作标记未被赋值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) {
if(elOriginRotate) {
const { xRange, yRange } = getRectRotatedRange({
left: targetLeft,
top: targetTop,
width: elOriginWidth,
height: elOriginHeight,
rotate: elOriginRotate,
})
targetMinX = xRange[0]
targetMaxX = xRange[1]
targetMinY = yRange[0]
targetMaxY = yRange[1]
}
else if(element.type === 'line') {
targetMinX = targetLeft
targetMaxX = targetLeft + Math.max(element.start[0], element.end[0])
targetMinY = targetTop
targetMaxY = targetTop + Math.max(element.start[1], element.end[1])
}
else {
targetMinX = targetLeft
targetMaxX = targetLeft + elOriginWidth
targetMinY = targetTop
targetMaxY = targetTop + elOriginHeight
}
}
else {
const leftValues = []
const topValues = []
const rightValues = []
const bottomValues = []
for(let i = 0; i < originActiveElementList.length; i++) {
const element = originActiveElementList[i]
const left = element.left + moveX
const top = element.top + moveY
const width = element.width
const height = ('height' in element && element.height) ? element.height : 0
const rotate = ('rotate' in element && element.rotate) ? element.rotate : 0
if('rotate' in element && element.rotate) {
const { xRange, yRange } = getRectRotatedRange({ left, top, width, height, rotate })
leftValues.push(xRange[0])
topValues.push(yRange[0])
rightValues.push(xRange[1])
bottomValues.push(yRange[1])
}
else if(element.type === 'line') {
leftValues.push(left)
topValues.push(top)
rightValues.push(left + Math.max(element.start[0], element.end[0]))
bottomValues.push(top + Math.max(element.start[1], element.end[1]))
}
else {
leftValues.push(left)
topValues.push(top)
rightValues.push(left + width)
bottomValues.push(top + height)
}
}
targetMinX = Math.min(...leftValues)
targetMaxX = Math.max(...rightValues)
targetMinY = Math.min(...topValues)
targetMaxY = Math.max(...bottomValues)
}
const targetCenterX = targetMinX + (targetMaxX - targetMinX) / 2
const targetCenterY = targetMinY + (targetMaxY - targetMinY) / 2
// 根据收集到的参考线,分别执行垂直和水平两个方向的对齐吸附
const _alignmentLines: AlignmentLineProps[] = []
let isVerticalAdsorbed = false
let isHorizontalAdsorbed = false
for(let i = 0; i < horizontalLines.length; i++) {
const { value, range } = horizontalLines[i]
const min = Math.min(...range, targetMinX, targetMaxX)
const max = Math.max(...range, targetMinX, targetMaxX)
if(Math.abs(targetMinY - value) < sorptionRange) {
if(!isHorizontalAdsorbed) {
targetTop = targetTop - (targetMinY - value)
isHorizontalAdsorbed = true
}
_alignmentLines.push({type: 'horizontal', axis: {x: min - 20, y: value}, length: max - min + 40})
}
if(Math.abs(targetMaxY - value) < sorptionRange) {
if(!isHorizontalAdsorbed) {
targetTop = targetTop - (targetMaxY - value)
isHorizontalAdsorbed = true
}
_alignmentLines.push({type: 'horizontal', axis: {x: min - 20, y: value}, length: max - min + 40})
}
if(Math.abs(targetCenterY - value) < sorptionRange) {
if(!isHorizontalAdsorbed) {
targetTop = targetTop - (targetCenterY - value)
isHorizontalAdsorbed = true
}
_alignmentLines.push({type: 'horizontal', axis: {x: min - 20, y: value}, length: max - min + 40})
}
}
for(let i = 0; i < verticalLines.length; i++) {
const { value, range } = verticalLines[i]
const min = Math.min(...range, targetMinY, targetMaxY)
const max = Math.max(...range, targetMinY, targetMaxY)
if(Math.abs(targetMinX - value) < sorptionRange) {
if(!isVerticalAdsorbed) {
targetLeft = targetLeft - (targetMinX - value)
isVerticalAdsorbed = true
}
_alignmentLines.push({type: 'vertical', axis: {x: value, y: min - 20}, length: max - min + 40})
}
if(Math.abs(targetMaxX - value) < sorptionRange) {
if(!isVerticalAdsorbed) {
targetLeft = targetLeft - (targetMaxX - value)
isVerticalAdsorbed = true
}
_alignmentLines.push({type: 'vertical', axis: {x: value, y: min - 20}, length: max - min + 40})
}
if(Math.abs(targetCenterX - value) < sorptionRange) {
if(!isVerticalAdsorbed) {
targetLeft = targetLeft - (targetCenterX - value)
isVerticalAdsorbed = true
}
_alignmentLines.push({type: 'vertical', axis: {x: value, y: min - 20}, length: max - min + 40})
}
}
alignmentLines.value = _alignmentLines
// 非多选,或者当前操作的元素时激活的组合元素
if(activeElementIdList.value.length === 1 || isActiveGroupElement) {
elementList.value = elementList.value.map(el => {
return el.elId === element.elId ? { ...el, left: targetLeft, top: targetTop } : el
})
}
// 修改元素位置,如果需要修改位置的元素不是被操作的元素(例如多选下的操作)
// 那么其他非操作元素要移动的位置通过操作元素的移动偏移量计算
else {
const handleElement = elementList.value.find(el => el.elId === element.elId)
if(!handleElement) return
elementList.value = elementList.value.map(el => {
const newEl = el
activeElementIdList.value.includes(el.elId) ? { ...el, left: targetLeft, top: targetTop } : el
if(activeElementIdList.value.includes(el.elId)) {
if(el.elId === element.elId) {
newEl.left = targetLeft
newEl.top = targetTop
}
else {
newEl.left = newEl.left + (targetLeft - handleElement.left)
newEl.top = newEl.top + (targetTop - handleElement.top)
}
}
return newEl
})
}
}
document.onmouseup = e => {
isMouseDown = false
document.onmousemove = null
document.onmouseup = null
alignmentLines.value = []
const currentPageX = e.pageX
const currentPageY = e.pageY
// 对比初始位置,没有实际的位移不更新数据
if(startPageX === currentPageX && startPageY === currentPageY) return
store.commit(MutationTypes.UPDATE_SLIDE, { elements: elementList.value })
}
}
return {
moveElement,
}
}

View File

@ -0,0 +1,181 @@
import { useStore } from 'vuex'
import { Ref } from 'vue'
import { State, MutationTypes } from '@/store'
import { PPTElement } from '@/types/slides'
import { ElementOrderCommand, ElementOrderCommands } from '@/types/edit'
export default (elementList: Ref<PPTElement[]>) => {
const store = useStore<State>()
// 获取组合元素层级范围(组合成员中的最大层级和最小层级)
const getCombineElementIndexRange = (elementList: PPTElement[], combineElementList: PPTElement[]) => {
const minIndex = elementList.findIndex(_element => _element.elId === combineElementList[0].elId)
const maxIndex = elementList.findIndex(_element => _element.elId === combineElementList[combineElementList.length - 1].elId)
return { minIndex, maxIndex }
}
// 上移一层,返回移动后新的元素列表(下移一层逻辑类似)
const moveUpElement = (elementList: PPTElement[], element: PPTElement) => {
const copyOfElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
// 被操作的元素是组合元素成员
if(element.groupId) {
// 获取该组合元素全部成员,以及组合元素层级范围
const combineElementList = copyOfElementList.filter(_element => _element.groupId === element.groupId)
const { minIndex, maxIndex } = getCombineElementIndexRange(elementList, combineElementList)
// 无法移动(已经处在顶层)
if(maxIndex === elementList.length - 1) return null
// 该组合元素上一层的元素以下简称为【元素next】
const nextElement = copyOfElementList[maxIndex + 1]
// 从元素列表中移除该组合元素全部成员
const movedElementList = copyOfElementList.splice(minIndex, combineElementList.length)
// 如果【元素next】也是组合元素成员另一个组合不是上面被移除的那一组以下简称为【组合next】
// 需要获取【组合next】全部成员的长度将上面移除的组合元素全部成员添加到【组合next】全部成员的上方
if(nextElement.groupId) {
const nextCombineElementList = copyOfElementList.filter(_element => _element.groupId === nextElement.groupId)
copyOfElementList.splice(minIndex + nextCombineElementList.length, 0, ...movedElementList)
}
// 如果【元素next】是单独的元素非组合成员将上面移除的组合元素全部成员添加到【元素next】上方
else copyOfElementList.splice(minIndex + 1, 0, ...movedElementList)
}
// 被操作的元素是单独的元素(非组合成员)
else {
// 元素在元素列表中的层级
const elementIndex = elementList.findIndex(item => item.elId === element.elId)
// 无法移动(已经处在顶层)
if(elementIndex === elementList.length - 1) return null
// 上一层的元素以下简称为【元素next】
const nextElement = copyOfElementList[elementIndex + 1]
// 从元素列表中移除被操作的元素
const movedElement = copyOfElementList.splice(elementIndex, 1)[0]
// 如果【元素next】是组合元素成员
// 需要获取该组合全部成员的长度,将上面移除的元素添加到该组合全部成员的上方
if(nextElement.groupId) {
const combineElementList = copyOfElementList.filter(_element => _element.groupId === nextElement.groupId)
copyOfElementList.splice(elementIndex + combineElementList.length, 0, movedElement)
}
// 如果【元素next】是单独的元素非组合成员将上面移除的元素添加到【元素next】上方
else copyOfElementList.splice(elementIndex + 1, 0, movedElement)
}
return copyOfElementList
}
// 下移一层
const moveDownElement = (elementList: PPTElement[], element: PPTElement) => {
const copyOfElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
if(element.groupId) {
const combineElementList = copyOfElementList.filter(_element => _element.groupId === element.groupId)
const { minIndex } = getCombineElementIndexRange(elementList, combineElementList)
if(minIndex === 0) return null
const prevElement = copyOfElementList[minIndex - 1]
const movedElementList = copyOfElementList.splice(minIndex, combineElementList.length)
if(prevElement.groupId) {
const prevCombineElementList = copyOfElementList.filter(_element => _element.groupId === prevElement.groupId)
copyOfElementList.splice(minIndex - prevCombineElementList.length, 0, ...movedElementList)
}
else copyOfElementList.splice(minIndex - 1, 0, ...movedElementList)
}
else {
const elementIndex = elementList.findIndex(item => item.elId === element.elId)
if(elementIndex === 0) return null
const prevElement = copyOfElementList[elementIndex - 1]
const movedElement = copyOfElementList.splice(elementIndex, 1)[0]
if(prevElement.groupId) {
const combineElementList = copyOfElementList.filter(_element => _element.groupId === prevElement.groupId)
copyOfElementList.splice(elementIndex - combineElementList.length, 0, movedElement)
}
else copyOfElementList.splice(elementIndex - 1, 0, movedElement)
}
return copyOfElementList
}
// 置顶层,返回移动后新的元素列表(置底层逻辑类似)
const moveTopElement = (elementList: PPTElement[], element: PPTElement) => {
const copyOfElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
// 被操作的元素是组合元素成员
if(element.groupId) {
// 获取该组合元素全部成员,以及组合元素层级范围
const combineElementList = copyOfElementList.filter(_element => _element.groupId === element.groupId)
const { minIndex, maxIndex } = getCombineElementIndexRange(elementList, combineElementList)
// 无法移动(已经处在顶层)
if(maxIndex === elementList.length - 1) return null
// 从元素列表中移除该组合元素全部成员,然后添加到元素列表最上方
const movedElementList = copyOfElementList.splice(minIndex, combineElementList.length)
copyOfElementList.push(...movedElementList)
}
// 被操作的元素是单独的元素(非组合成员)
else {
// 元素在元素列表中的层级
const elementIndex = elementList.findIndex(item => item.elId === element.elId)
// 无法移动(已经处在顶层)
if(elementIndex === elementList.length - 1) return null
// 从元素列表中移除该元素,然后添加到元素列表最上方
copyOfElementList.splice(elementIndex, 1)
copyOfElementList.push(element)
}
return copyOfElementList
}
// 置底层
const moveBottomElement = (elementList: PPTElement[], element: PPTElement) => {
const copyOfElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
if(element.groupId) {
const combineElementList = copyOfElementList.filter(_element => _element.groupId === element.groupId)
const { minIndex } = getCombineElementIndexRange(elementList, combineElementList)
if(minIndex === 0) return null
const movedElementList = copyOfElementList.splice(minIndex, combineElementList.length)
copyOfElementList.unshift(...movedElementList)
}
else {
const elementIndex = elementList.findIndex(item => item.elId === element.elId)
if(elementIndex === 0) return null
copyOfElementList.splice(elementIndex, 1)
copyOfElementList.unshift(element)
}
return copyOfElementList
}
const orderElement = (element: PPTElement, command: ElementOrderCommand) => {
let newElementList = null
if(command === ElementOrderCommands.UP) newElementList = moveUpElement(elementList.value, element)
else if(command === ElementOrderCommands.DOWN) newElementList = moveDownElement(elementList.value, element)
else if(command === ElementOrderCommands.TOP) newElementList = moveTopElement(elementList.value, element)
else if(command === ElementOrderCommands.BOTTOM) newElementList = moveBottomElement(elementList.value, element)
store.commit(MutationTypes.UPDATE_SLIDE, { elements: newElementList })
}
return {
orderElement,
}
}

View File

@ -0,0 +1,67 @@
import { Ref } from 'vue'
import { useStore } from 'vuex'
import { State, MutationTypes } from '@/store'
import { PPTElement, PPTTextElement, PPTImageElement, PPTShapeElement } from '@/types/slides'
import { getAngleFromCoordinate } from '../utils/elementRotate'
export default (elementList: Ref<PPTElement[]>, viewportRef: Ref<HTMLElement | null>, canvasScale: Ref<number>) => {
const store = useStore<State>()
const rotateElement = (element: PPTTextElement | PPTImageElement | PPTShapeElement) => {
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
if(!viewportRef.value) return
const viewportRect = viewportRef.value.getBoundingClientRect()
document.onmousemove = e => {
if(!isMouseDown) return
// 计算鼠标基于旋转中心的坐标
const mouseX = (e.pageX - viewportRect.left) / canvasScale.value
const mouseY = (e.pageY - viewportRect.top) / canvasScale.value
const x = mouseX - centerX
const y = centerY - mouseY
angle = getAngleFromCoordinate(x, y)
// 45°的倍数位置有吸附效果
const sorptionRange = 5
if( Math.abs(angle) <= sorptionRange ) angle = 0
else if( angle > 0 && Math.abs(angle - 45) <= sorptionRange ) angle -= (angle - 45)
else if( angle < 0 && Math.abs(angle + 45) <= sorptionRange ) angle -= (angle + 45)
else if( angle > 0 && Math.abs(angle - 90) <= sorptionRange ) angle -= (angle - 90)
else if( angle < 0 && Math.abs(angle + 90) <= sorptionRange ) angle -= (angle + 90)
else if( angle > 0 && Math.abs(angle - 135) <= sorptionRange ) angle -= (angle - 135)
else if( angle < 0 && Math.abs(angle + 135) <= sorptionRange ) angle -= (angle + 135)
else if( angle > 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.elId === el.elId ? { ...el, rotate: angle } : el)
}
document.onmouseup = () => {
isMouseDown = false
document.onmousemove = null
document.onmouseup = null
if(elOriginRotate === angle) return
store.commit(MutationTypes.UPDATE_SLIDE, { elements: elementList.value })
}
}
return {
rotateElement,
}
}

View File

@ -0,0 +1,423 @@
import { computed, Ref } from 'vue'
import { useStore } from 'vuex'
import { State, MutationTypes } from '@/store'
import { ElementTypes, PPTElement, PPTLineElement } from '@/types/slides'
import { OPERATE_KEYS, ElementScaleHandler } from '@/types/edit'
import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
import { getRotateElementPoints, getOppositePoint } from '../utils/elementRotate'
import { AlignLine, uniqAlignLines } from '../utils/alignLines'
import { AlignmentLineProps, MultiSelectRange } from '../types/index'
export default (
elementList: Ref<PPTElement[]>,
canvasScale: Ref<number>,
activeGroupElementId: Ref<string>,
activeElementIdList: Ref<string[]>,
alignmentLines: Ref<AlignmentLineProps[]>,
) => {
const store = useStore<State>()
const ctrlOrShiftKeyActive = computed(() => store.getters.ctrlOrShiftKeyActive)
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 isLockRatio = ctrlOrShiftKeyActive.value || ('lockRatio' in element && element.lockRatio)
const lockRatio = 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.elId === activeGroupElementId.value
for(const el of elementList.value) {
if('rotate' in el && el.rotate) continue
if(el.type === ElementTypes.LINE) continue
if(isActiveGroupElement && el.elId === element.elId) continue
if(!isActiveGroupElement && activeElementIdList.value.includes(el.elId)) 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(isLockRatio) {
if(command === OPERATE_KEYS.RIGHT_BOTTOM || command === OPERATE_KEYS.LEFT_TOP) revisedY = revisedX / lockRatio
if(command === OPERATE_KEYS.LEFT_BOTTOM || command === OPERATE_KEYS.RIGHT_TOP) revisedY = -revisedX / lockRatio
}
// 根据不同的操作点分别计算元素缩放后的大小和位置
// 这里计算的位置是错误的,因为旋转后缩放实际上也改变了元素的位置,需要在后面进行矫正
// 这里计算的大小是正确的,因为上面修正鼠标按下后移动的距离时其实已经进行过了矫正
if(command === OPERATE_KEYS.RIGHT_BOTTOM) {
width = getSizeWithinRange(elOriginWidth + revisedX)
height = getSizeWithinRange(elOriginHeight + revisedY)
}
else if(command === OPERATE_KEYS.LEFT_BOTTOM) {
width = getSizeWithinRange(elOriginWidth - revisedX)
height = getSizeWithinRange(elOriginHeight + revisedY)
left = elOriginLeft - (width - elOriginWidth)
}
else if(command === OPERATE_KEYS.LEFT_TOP) {
width = getSizeWithinRange(elOriginWidth - revisedX)
height = getSizeWithinRange(elOriginHeight - revisedY)
left = elOriginLeft - (width - elOriginWidth)
top = elOriginTop - (height - elOriginHeight)
}
else if(command === OPERATE_KEYS.RIGHT_TOP) {
width = getSizeWithinRange(elOriginWidth + revisedX)
height = getSizeWithinRange(elOriginHeight - revisedY)
top = elOriginTop - (height - elOriginHeight)
}
else if(command === OPERATE_KEYS.TOP) {
height = getSizeWithinRange(elOriginHeight - revisedY)
top = elOriginTop - (height - elOriginHeight)
}
else if(command === OPERATE_KEYS.BOTTOM) {
height = getSizeWithinRange(elOriginHeight + revisedY)
}
else if(command === OPERATE_KEYS.LEFT) {
width = getSizeWithinRange(elOriginWidth - revisedX)
left = elOriginLeft - (width - elOriginWidth)
}
else if(command === OPERATE_KEYS.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(isLockRatio) {
if(command === OPERATE_KEYS.RIGHT_BOTTOM || command === OPERATE_KEYS.LEFT_TOP) moveY = moveX / lockRatio
if(command === OPERATE_KEYS.LEFT_BOTTOM || command === OPERATE_KEYS.RIGHT_TOP) moveY = -moveX / lockRatio
}
if(command === OPERATE_KEYS.RIGHT_BOTTOM) {
const { offsetX, offsetY } = alignedAdsorption(elOriginLeft + elOriginWidth + moveX, elOriginTop + elOriginHeight + moveY)
moveX = moveX - offsetX
moveY = moveY - offsetY
if(isLockRatio) {
if(offsetY) moveX = moveY * lockRatio
else moveY = moveX / lockRatio
}
width = getSizeWithinRange(elOriginWidth + moveX)
height = getSizeWithinRange(elOriginHeight + moveY)
}
else if(command === OPERATE_KEYS.LEFT_BOTTOM) {
const { offsetX, offsetY } = alignedAdsorption(elOriginLeft + moveX, elOriginTop + elOriginHeight + moveY)
moveX = moveX - offsetX
moveY = moveY - offsetY
if(isLockRatio) {
if(offsetY) moveX = -moveY * lockRatio
else moveY = -moveX / lockRatio
}
width = getSizeWithinRange(elOriginWidth - moveX)
height = getSizeWithinRange(elOriginHeight + moveY)
left = elOriginLeft - (width - elOriginWidth)
}
else if(command === OPERATE_KEYS.LEFT_TOP) {
const { offsetX, offsetY } = alignedAdsorption(elOriginLeft + moveX, elOriginTop + moveY)
moveX = moveX - offsetX
moveY = moveY - offsetY
if(isLockRatio) {
if(offsetY) moveX = moveY * lockRatio
else moveY = moveX / lockRatio
}
width = getSizeWithinRange(elOriginWidth - moveX)
height = getSizeWithinRange(elOriginHeight - moveY)
left = elOriginLeft - (width - elOriginWidth)
top = elOriginTop - (height - elOriginHeight)
}
else if(command === OPERATE_KEYS.RIGHT_TOP) {
const { offsetX, offsetY } = alignedAdsorption(elOriginLeft + elOriginWidth + moveX, elOriginTop + moveY)
moveX = moveX - offsetX
moveY = moveY - offsetY
if(isLockRatio) {
if(offsetY) moveX = -moveY * lockRatio
else moveY = -moveX / lockRatio
}
width = getSizeWithinRange(elOriginWidth + moveX)
height = getSizeWithinRange(elOriginHeight - moveY)
top = elOriginTop - (height - elOriginHeight)
}
else if(command === OPERATE_KEYS.LEFT) {
const { offsetX } = alignedAdsorption(elOriginLeft + moveX, null)
moveX = moveX - offsetX
width = getSizeWithinRange(elOriginWidth - moveX)
left = elOriginLeft - (width - elOriginWidth)
}
else if(command === OPERATE_KEYS.RIGHT) {
const { offsetX } = alignedAdsorption(elOriginLeft + elOriginWidth + moveX, null)
moveX = moveX - offsetX
width = getSizeWithinRange(elOriginWidth + moveX)
}
else if(command === OPERATE_KEYS.TOP) {
const { offsetY } = alignedAdsorption(null, elOriginTop + moveY)
moveY = moveY - offsetY
height = getSizeWithinRange(elOriginHeight - moveY)
top = elOriginTop - (height - elOriginHeight)
}
else if(command === OPERATE_KEYS.BOTTOM) {
const { offsetY } = alignedAdsorption(null, elOriginTop + elOriginHeight + moveY)
moveY = moveY - offsetY
height = getSizeWithinRange(elOriginHeight + moveY)
}
}
elementList.value = elementList.value.map(el => element.elId === el.elId ? { ...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 })
}
}
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 lockRatio = 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 === OPERATE_KEYS.RIGHT_BOTTOM || command === OPERATE_KEYS.LEFT_TOP) y = x / lockRatio
if(command === OPERATE_KEYS.LEFT_BOTTOM || command === OPERATE_KEYS.RIGHT_TOP) y = -x / lockRatio
}
// 获取鼠标缩放时当前所有激活元素的范围
let currentMinX = minX
let currentMaxX = maxX
let currentMinY = minY
let currentMaxY = maxY
if(command === OPERATE_KEYS.RIGHT_BOTTOM) {
currentMaxX = maxX + x
currentMaxY = maxY + y
}
else if(command === OPERATE_KEYS.LEFT_BOTTOM) {
currentMinX = minX + x
currentMaxY = maxY + y
}
else if(command === OPERATE_KEYS.LEFT_TOP) {
currentMinX = minX + x
currentMinY = minY + y
}
else if(command === OPERATE_KEYS.RIGHT_TOP) {
currentMaxX = maxX + x
currentMinY = minY + y
}
else if(command === OPERATE_KEYS.TOP) {
currentMinY = minY + y
}
else if(command === OPERATE_KEYS.BOTTOM) {
currentMaxY = maxY + y
}
else if(command === OPERATE_KEYS.LEFT) {
currentMinX = minX + x
}
else if(command === OPERATE_KEYS.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 => {
const newEl = el
if((newEl.type === ElementTypes.IMAGE || newEl.type === ElementTypes.SHAPE) && activeElementIdList.value.includes(newEl.elId)) {
const originElement = originElementList.find(originEl => originEl.elId === el.elId)
if(originElement && (originElement.type === ElementTypes.IMAGE || originElement.type === ElementTypes.SHAPE)) {
newEl.width = originElement.width * widthScale
newEl.height = originElement.height * heightScale
newEl.left = currentMinX + (originElement.left - minX) * widthScale
newEl.top = currentMinY + (originElement.top - minY) * heightScale
}
}
return newEl
})
}
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 })
}
}
return {
scaleElement,
scaleMultiElement,
}
}

View File

@ -0,0 +1,97 @@
import { Ref, computed } from 'vue'
import { useStore } from 'vuex'
import uniq from 'lodash/uniq'
import { State, MutationTypes } from '@/store'
import { PPTElement } from '@/types/slides'
export default (
elementList: Ref<PPTElement[]>,
activeElementIdList: Ref<string[]>,
activeGroupElementId: Ref<string>,
editorAreaFocus: Ref<boolean>,
handleElementId: Ref<string>,
moveElement: (e: MouseEvent, element: PPTElement) => void,
) => {
const store = useStore<State>()
const ctrlOrShiftKeyActive = computed(() => store.getters.ctrlOrShiftKeyActive)
const selectElement = (e: MouseEvent, element: PPTElement, canMove = true) => {
if(!editorAreaFocus.value) store.commit(MutationTypes.SET_EDITORAREA_FOCUS, true)
// 如果被点击的元素处于未激活状态,则将他设置为激活元素(单选),或者加入到激活元素中(多选)
if(!activeElementIdList.value.includes(element.elId)) {
let newActiveIdList: string[] = []
if(ctrlOrShiftKeyActive.value) {
newActiveIdList = [...activeElementIdList.value, element.elId]
}
else newActiveIdList = [element.elId]
// 同时如果该元素是分组成员,需要将和他同组的元素一起激活
if(element.groupId) {
const groupMembersId: string[] = []
elementList.value.forEach((el: PPTElement) => {
if(el.groupId === element.groupId) groupMembersId.push(el.elId)
})
newActiveIdList = [...newActiveIdList, ...groupMembersId]
}
store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, uniq(newActiveIdList))
store.commit(MutationTypes.SET_HANDLE_ELEMENT_ID, element.elId)
}
// 如果被点击的元素已激活,且按下了多选按钮,则取消其激活状态(除非该元素或分组是最后的一个激活元素)
else if(ctrlOrShiftKeyActive.value) {
let newActiveIdList: string[] = []
// 同时如果该元素是分组成员,需要将和他同组的元素一起取消
if(element.groupId) {
const groupMembersId: string[] = []
elementList.value.forEach((el: PPTElement) => {
if(el.groupId === element.groupId) groupMembersId.push(el.elId)
})
newActiveIdList = activeElementIdList.value.filter(elId => !groupMembersId.includes(elId))
}
else {
newActiveIdList = activeElementIdList.value.filter(elId => elId !== element.elId)
}
if(newActiveIdList.length > 0) {
store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, newActiveIdList)
}
}
// 如果被点击的元素已激活,且没有按下多选按钮,且该元素不是当前操作元素,则将其设置为当前操作元素
else if(handleElementId.value !== element.elId) {
store.commit(MutationTypes.SET_HANDLE_ELEMENT_ID, element.elId)
}
else if(activeGroupElementId.value !== element.elId && element.groupId) {
const startPageX = e.pageX
const startPageY = e.pageY
;(e.target as HTMLElement).onmouseup = (e: MouseEvent) => {
const currentPageX = e.pageX
const currentPageY = e.pageY
if(startPageX === currentPageX && startPageY === currentPageY) {
activeGroupElementId.value = element.elId
;(e.target as HTMLElement).onmouseup = null
}
}
}
if(canMove) moveElement(e, element)
}
const selectAllElement = () => {
const unlockedElements = elementList.value.filter(el => !el.isLock)
const newActiveElementIdList = unlockedElements.map(el => el.elId)
store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, newActiveElementIdList)
}
return {
selectElement,
selectAllElement,
}
}

View File

@ -1,68 +0,0 @@
import { ref, reactive, Ref, toRefs } from 'vue'
export default (e: MouseEvent, viewportRef: Ref<HTMLElement | null>, canvasScale: number) => {
const isMouseDown = ref(false)
const mouseSelectionState = reactive({
isShow: false,
top: 0,
left: 0,
width: 0,
height: 0,
quadrant: 4,
})
const startMousePosition = reactive({
x: 0,
y: 0,
})
const minSelectionRange = 5
if(!viewportRef.value) return
isMouseDown.value = true
const viewportRect = viewportRef.value.getBoundingClientRect()
startMousePosition.x = e.pageX
startMousePosition.y = e.pageY
mouseSelectionState.top = (startMousePosition.x - viewportRect.y) / canvasScale
mouseSelectionState.left = (startMousePosition.y - viewportRect.x) / canvasScale
const mousemoveListener = (e: MouseEvent) => {
if(!isMouseDown.value) return
const currentPageX = e.pageX
const currentPageY = e.pageY
const offsetWidth = (currentPageX - startMousePosition.x) / canvasScale
const offsetHeight = (currentPageY - startMousePosition.y) / canvasScale
const width = Math.abs(offsetWidth)
const height = Math.abs(offsetHeight)
if(width < minSelectionRange || height < minSelectionRange) return
let quadrant = 0
if(offsetWidth > 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
mouseSelectionState.height = height
}
const mouseupListener = () => {
isMouseDown.value = false
mouseSelectionState.isShow = false
document.removeEventListener('mousemove', mousemoveListener)
document.removeEventListener('mouseup', mouseupListener)
}
document.addEventListener('mousemove', mousemoveListener)
document.addEventListener('mouseup', mouseupListener)
return { ...toRefs(mouseSelectionState) }
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,17 @@
export interface AlignmentLineAxis {
x: number;
y: number;
}
export interface AlignmentLineProps {
type: 'vertical' | 'horizontal';
axis: AlignmentLineAxis;
length: number;
}
export interface MultiSelectRange {
minX: number;
maxX: number;
minY: number;
maxY: number;
}

View File

@ -1,44 +0,0 @@
import { PPTElement } from '@/types/slides'
import { ElementAlignCommand, ElementAlignCommands } from '@/types/edit'
import { getElementListRange } from './elementRange'
import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
// 将元素对齐到屏幕
export const alignElementToCanvas = (elementList: PPTElement[], activeElementList: PPTElement[], activeElementIdList: string[], command: ElementAlignCommand) => {
const viewportWidth = VIEWPORT_SIZE
const viewportHeight = VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO
const { minX, maxX, minY, maxY } = getElementListRange(activeElementList)
const copyOfElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
for(const element of copyOfElementList) {
if(!activeElementIdList.includes(element.elId)) continue
if(command === ElementAlignCommands.TOP) {
const offsetY = minY - 0
element.top = element.top - offsetY
}
else if(command === ElementAlignCommands.VERTICAL) {
const offsetY = minY + (maxY - minY) / 2 - viewportHeight / 2
element.top = element.top - offsetY
}
else if(command === ElementAlignCommands.BOTTOM) {
const offsetY = maxY - viewportHeight
element.top = element.top - offsetY
}
else if(command === ElementAlignCommands.LEFT) {
const offsetX = minX - 0
element.left = element.left - offsetX
}
else if(command === ElementAlignCommands.HORIZONTAL) {
const offsetX = minX + (maxX - minX) / 2 - viewportWidth / 2
element.left = element.left - offsetX
}
else if(command === ElementAlignCommands.RIGHT) {
const offsetX = maxX - viewportWidth
element.left = element.left - offsetX
}
}
return copyOfElementList
}

View File

@ -1,41 +0,0 @@
import { createRandomCode } from '@/utils/common'
import { PPTElement } from '@/types/slides'
// 组合元素为当前所有激活元素添加一个相同的groupId
export const combineElements = (elementList: PPTElement[], activeElementList: PPTElement[], activeElementIdList: string[]) => {
if(!activeElementList.length) return null
let copyOfElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
const groupId = createRandomCode()
const combineElementList: PPTElement[] = []
for(const element of copyOfElementList) {
if(activeElementIdList.includes(element.elId)) {
element.groupId = groupId
combineElementList.push(element)
}
}
// 注意,组合元素的层级应该是连续的,所以需要获取该组元素中最顶层的元素,将组内其他成员从原位置移动到最顶层的元素的下面
const combineElementMaxIndex = copyOfElementList.findIndex(_element => _element.elId === combineElementList[combineElementList.length - 1].elId)
const combineElementIdList = combineElementList.map(_element => _element.elId)
copyOfElementList = copyOfElementList.filter(_element => !combineElementIdList.includes(_element.elId))
const insertIndex = combineElementMaxIndex - combineElementList.length + 1
copyOfElementList.splice(insertIndex, 0, ...combineElementList)
return copyOfElementList
}
// 取消组合元素移除所有被激活元素的groupId
export const uncombineElements = (elementList: PPTElement[], activeElementList: PPTElement[], activeElementIdList: string[]) => {
if(!activeElementList.length) return null
const hasElementInGroup = activeElementList.some(item => item.groupId)
if(!hasElementInGroup) return null
const copyOfElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
for(const element of copyOfElementList) {
if(activeElementIdList.includes(element.elId) && element.groupId) delete element.groupId
}
return copyOfElementList
}

View File

@ -1,29 +0,0 @@
import { PPTElement } from '@/types/slides'
export const lockElement = (elementList: PPTElement[], handleElement: PPTElement, activeElementIdList: string[]) => {
const copyOfElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
for(const element of copyOfElementList) {
if(activeElementIdList.includes(handleElement.elId)) element.isLock = true
}
return copyOfElementList
}
export const unlockElement = (elementList: PPTElement[], handleElement: PPTElement) => {
const copyOfElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
if(handleElement.groupId) {
for(const element of copyOfElementList) {
if(element.groupId === handleElement.groupId) element.isLock = false
}
return copyOfElementList
}
for(const element of copyOfElementList) {
if(element.elId === handleElement.elId) {
element.isLock = false
break
}
}
return copyOfElementList
}

View File

@ -1,170 +0,0 @@
import { PPTElement } from '@/types/slides'
import { ElementOrderCommand, ElementOrderCommands } from '@/types/edit'
// 获取组合元素层级范围(组合成员中的最大层级和最小层级)
const getCombineElementIndexRange = (elementList: PPTElement[], combineElementList: PPTElement[]) => {
const minIndex = elementList.findIndex(_element => _element.elId === combineElementList[0].elId)
const maxIndex = elementList.findIndex(_element => _element.elId === combineElementList[combineElementList.length - 1].elId)
return { minIndex, maxIndex }
}
// 上移一层,返回移动后新的元素列表(下移一层逻辑类似)
const moveUpElement = (elementList: PPTElement[], element: PPTElement) => {
const copyOfElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
// 被操作的元素是组合元素成员
if(element.groupId) {
// 获取该组合元素全部成员,以及组合元素层级范围
const combineElementList = copyOfElementList.filter(_element => _element.groupId === element.groupId)
const { minIndex, maxIndex } = getCombineElementIndexRange(elementList, combineElementList)
// 无法移动(已经处在顶层)
if(maxIndex === elementList.length - 1) return null
// 该组合元素上一层的元素以下简称为【元素next】
const nextElement = copyOfElementList[maxIndex + 1]
// 从元素列表中移除该组合元素全部成员
const movedElementList = copyOfElementList.splice(minIndex, combineElementList.length)
// 如果【元素next】也是组合元素成员另一个组合不是上面被移除的那一组以下简称为【组合next】
// 需要获取【组合next】全部成员的长度将上面移除的组合元素全部成员添加到【组合next】全部成员的上方
if(nextElement.groupId) {
const nextCombineElementList = copyOfElementList.filter(_element => _element.groupId === nextElement.groupId)
copyOfElementList.splice(minIndex + nextCombineElementList.length, 0, ...movedElementList)
}
// 如果【元素next】是单独的元素非组合成员将上面移除的组合元素全部成员添加到【元素next】上方
else copyOfElementList.splice(minIndex + 1, 0, ...movedElementList)
}
// 被操作的元素是单独的元素(非组合成员)
else {
// 元素在元素列表中的层级
const elementIndex = elementList.findIndex(item => item.elId === element.elId)
// 无法移动(已经处在顶层)
if(elementIndex === elementList.length - 1) return null
// 上一层的元素以下简称为【元素next】
const nextElement = copyOfElementList[elementIndex + 1]
// 从元素列表中移除被操作的元素
const movedElement = copyOfElementList.splice(elementIndex, 1)[0]
// 如果【元素next】是组合元素成员
// 需要获取该组合全部成员的长度,将上面移除的元素添加到该组合全部成员的上方
if(nextElement.groupId) {
const combineElementList = copyOfElementList.filter(_element => _element.groupId === nextElement.groupId)
copyOfElementList.splice(elementIndex + combineElementList.length, 0, movedElement)
}
// 如果【元素next】是单独的元素非组合成员将上面移除的元素添加到【元素next】上方
else copyOfElementList.splice(elementIndex + 1, 0, movedElement)
}
return copyOfElementList
}
// 下移一层
const moveDownElement = (elementList: PPTElement[], element: PPTElement) => {
const copyOfElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
if(element.groupId) {
const combineElementList = copyOfElementList.filter(_element => _element.groupId === element.groupId)
const { minIndex } = getCombineElementIndexRange(elementList, combineElementList)
if(minIndex === 0) return null
const prevElement = copyOfElementList[minIndex - 1]
const movedElementList = copyOfElementList.splice(minIndex, combineElementList.length)
if(prevElement.groupId) {
const prevCombineElementList = copyOfElementList.filter(_element => _element.groupId === prevElement.groupId)
copyOfElementList.splice(minIndex - prevCombineElementList.length, 0, ...movedElementList)
}
else copyOfElementList.splice(minIndex - 1, 0, ...movedElementList)
}
else {
const elementIndex = elementList.findIndex(item => item.elId === element.elId)
if(elementIndex === 0) return null
const prevElement = copyOfElementList[elementIndex - 1]
const movedElement = copyOfElementList.splice(elementIndex, 1)[0]
if(prevElement.groupId) {
const combineElementList = copyOfElementList.filter(_element => _element.groupId === prevElement.groupId)
copyOfElementList.splice(elementIndex - combineElementList.length, 0, movedElement)
}
else copyOfElementList.splice(elementIndex - 1, 0, movedElement)
}
return copyOfElementList
}
// 置顶层,返回移动后新的元素列表(置底层逻辑类似)
const moveTopElement = (elementList: PPTElement[], element: PPTElement) => {
const copyOfElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
// 被操作的元素是组合元素成员
if(element.groupId) {
// 获取该组合元素全部成员,以及组合元素层级范围
const combineElementList = copyOfElementList.filter(_element => _element.groupId === element.groupId)
const { minIndex, maxIndex } = getCombineElementIndexRange(elementList, combineElementList)
// 无法移动(已经处在顶层)
if(maxIndex === elementList.length - 1) return null
// 从元素列表中移除该组合元素全部成员,然后添加到元素列表最上方
const movedElementList = copyOfElementList.splice(minIndex, combineElementList.length)
copyOfElementList.push(...movedElementList)
}
// 被操作的元素是单独的元素(非组合成员)
else {
// 元素在元素列表中的层级
const elementIndex = elementList.findIndex(item => item.elId === element.elId)
// 无法移动(已经处在顶层)
if(elementIndex === elementList.length - 1) return null
// 从元素列表中移除该元素,然后添加到元素列表最上方
copyOfElementList.splice(elementIndex, 1)
copyOfElementList.push(element)
}
return copyOfElementList
}
// 置底层
const moveBottomElement = (elementList: PPTElement[], element: PPTElement) => {
const copyOfElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
if(element.groupId) {
const combineElementList = copyOfElementList.filter(_element => _element.groupId === element.groupId)
const { minIndex } = getCombineElementIndexRange(elementList, combineElementList)
if(minIndex === 0) return null
const movedElementList = copyOfElementList.splice(minIndex, combineElementList.length)
copyOfElementList.unshift(...movedElementList)
}
else {
const elementIndex = elementList.findIndex(item => item.elId === element.elId)
if(elementIndex === 0) return null
copyOfElementList.splice(elementIndex, 1)
copyOfElementList.unshift(element)
}
return copyOfElementList
}
export const orderElement = (elementList: PPTElement[], element: PPTElement, command: ElementOrderCommand) => {
let newElementList = null
if(command === ElementOrderCommands.UP) newElementList = moveUpElement(elementList, element)
else if(command === ElementOrderCommands.DOWN) newElementList = moveDownElement(elementList, element)
else if(command === ElementOrderCommands.TOP) newElementList = moveTopElement(elementList, element)
else if(command === ElementOrderCommands.BOTTOM) newElementList = moveBottomElement(elementList, element)
return newElementList
}

View File

@ -203,7 +203,7 @@ export default defineComponent({
text: '删除', text: '删除',
subText: 'Delete', subText: 'Delete',
icon: 'icon-delete', icon: 'icon-delete',
handler: () => props.deleteElement(), handler: props.deleteElement,
}, },
] ]
} }