This commit is contained in:
pipipi-pikachu 2020-12-21 17:25:43 +08:00
parent ff810d823a
commit d704f7b386
9 changed files with 495 additions and 444 deletions

View File

@ -40,7 +40,7 @@ module.exports = {
'no-with': 'error', 'no-with': 'error',
'max-depth': ['error', 5], 'max-depth': ['error', 5],
'consistent-this': ['error', 'self'], 'consistent-this': ['error', 'self'],
'max-lines': ['error', 1500], 'max-lines': ['error', 1000],
'no-multi-str': 'error', 'no-multi-str': 'error',
'space-infix-ops': 'error', 'space-infix-ops': 'error',
'space-before-blocks': ['error', 'always'], 'space-before-blocks': ['error', 'always'],

View File

@ -1,6 +1,6 @@
{ {
"name": "ppt_online_editor", "name": "hamster_ppt",
"version": "0.1.0", "version": "0.0.1",
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",

View File

@ -6,7 +6,7 @@
<meta name="renderer" content="webkit" /> <meta name="renderer" content="webkit" />
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico"> <link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>演示文稿在线编辑</title> <title>Hamster 幻灯片 v0.0.1-dev</title>
</head> </head>
<body> <body>
<noscript> <noscript>

View File

@ -11,7 +11,7 @@ export default (elementList: Ref<PPTElement[]>) => {
// 组合元素为当前所有激活元素添加一个相同的groupId // 组合元素为当前所有激活元素添加一个相同的groupId
const combineElements = () => { const combineElements = () => {
if(!activeElementList.value.length) return null if(!activeElementList.value.length) return
let newElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList)) let newElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
const groupId = createRandomCode() const groupId = createRandomCode()
@ -37,9 +37,9 @@ export default (elementList: Ref<PPTElement[]>) => {
// 取消组合元素移除所有被激活元素的groupId // 取消组合元素移除所有被激活元素的groupId
const uncombineElements = () => { const uncombineElements = () => {
if(!activeElementList.value.length) return null if(!activeElementList.value.length) return
const hasElementInGroup = activeElementList.value.some(item => item.groupId) const hasElementInGroup = activeElementList.value.some(item => item.groupId)
if(!hasElementInGroup) return null if(!hasElementInGroup) return
const newElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList)) const newElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
for(const element of newElementList) { for(const element of newElementList) {

View File

@ -0,0 +1,131 @@
import { createRandomCode } from '@/utils/common'
import { getImageSize } from '@/utils/image'
import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
import { TableElementCell } from '@/types/slides'
import {
DEFAULT_IMAGE,
DEFAULT_TEXT,
DEFAULT_SHAPE,
DEFAULT_LINE,
DEFAULT_CHART,
DEFAULT_TABLE,
} from '@/configs/element'
interface CommonElementPosition {
top: number;
left: number;
width: number;
height: number;
}
interface LineElementPosition {
top: number;
left: number;
start: [number, number];
end: [number, number];
}
export default () => {
const insertImage = (imgUrl: string) => {
getImageSize(imgUrl).then(({ width, height }) => {
const scale = width / height
if(scale < VIEWPORT_ASPECT_RATIO && width > VIEWPORT_SIZE) {
width = VIEWPORT_SIZE
height = width * scale
}
else if(height > VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO) {
height = VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO
width = height / scale
}
return {
...DEFAULT_IMAGE,
elId: createRandomCode(),
imgUrl,
width,
height,
}
})
}
const insertChart = (chartType: string, data: Object) => {
return {
...DEFAULT_CHART,
elId: createRandomCode(),
chartType,
data,
}
}
const insertTable = (rowCount: number, colCount: number) => {
const row: TableElementCell[] = new Array(colCount).fill({ colspan: 1, rowspan: 1, content: '' })
const data: TableElementCell[][] = new Array(rowCount).fill(row)
const DEFAULT_CELL_WIDTH = 80
const DEFAULT_CELL_HEIGHT = 35
const DEFAULT_BORDER_WIDTH = 2
const colSizes: number[] = new Array(colCount).fill(DEFAULT_CELL_WIDTH)
const rowSizes: number[] = new Array(rowCount).fill(DEFAULT_CELL_HEIGHT)
return {
...DEFAULT_TABLE,
elId: createRandomCode(),
width: colCount * DEFAULT_CELL_WIDTH + DEFAULT_BORDER_WIDTH,
height: rowCount * DEFAULT_CELL_HEIGHT + DEFAULT_BORDER_WIDTH,
colSizes,
rowSizes,
data,
}
}
const insertText = (position: CommonElementPosition) => {
const { left, top, width, height } = position
return {
...DEFAULT_TEXT,
elId: createRandomCode(),
left,
top,
width,
height,
}
}
const insertShape = (position: CommonElementPosition, svgCode: string) => {
const { left, top, width, height } = position
return {
...DEFAULT_SHAPE,
elId: createRandomCode(),
left,
top,
width,
height,
svgCode,
}
}
const insertLine = (position: LineElementPosition, marker: [string, string], lineType: string) => {
const { left, top, start, end } = position
return {
...DEFAULT_LINE,
elId: createRandomCode(),
left,
top,
start,
end,
marker,
lineType,
}
}
return {
insertImage,
insertChart,
insertTable,
insertText,
insertShape,
insertLine,
}
}

View File

@ -0,0 +1,315 @@
import { Ref, computed } 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[]>,
activeGroupElementId: Ref<string>,
canvasScale: Ref<number>,
alignmentLines: Ref<AlignmentLineProps[]>,
) => {
const store = useStore<State>()
const activeElementIdList = computed(() => store.state.activeElementIdList)
const dragElement = (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 => {
if(activeElementIdList.value.includes(el.elId)) {
if(el.elId === element.elId) {
return {
...el,
left: targetLeft,
top: targetTop,
}
}
return {
...el,
left: el.left + (targetLeft - handleElement.left),
top: el.top + (targetTop - handleElement.top),
}
}
return el
})
}
}
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 {
dragElement,
}
}

View File

@ -1,312 +1,37 @@
import { Ref, computed, callWithErrorHandling } from 'vue' import { Ref, computed } from 'vue'
import { useStore } from 'vuex' import { useStore } from 'vuex'
import { State, MutationTypes } from '@/store' import { State, MutationTypes } from '@/store'
import { ElementTypes, PPTElement } from '@/types/slides' import { PPTElement } from '@/types/slides'
import { AlignmentLineProps } from '../types/index' import { KEYS } from '@/configs/hotkey'
import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
import { getRectRotatedRange } from '../utils/elementRange'
import { AlignLine, uniqAlignLines } from '../utils/alignLines'
export default ( export default (elementList: Ref<PPTElement[]>) => {
elementList: Ref<PPTElement[]>,
activeGroupElementId: Ref<string>,
canvasScale: Ref<number>,
alignmentLines: Ref<AlignmentLineProps[]>,
) => {
const store = useStore<State>() const store = useStore<State>()
const activeElementIdList = computed(() => store.state.activeElementIdList) const activeElementIdList = computed(() => store.state.activeElementIdList)
const moveElement = (e: MouseEvent, element: PPTElement) => { const moveElement = (command: string) => {
if(!activeElementIdList.value.includes(element.elId)) return const newElementList = elementList.value.map(el => {
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 => {
if(activeElementIdList.value.includes(el.elId)) { if(activeElementIdList.value.includes(el.elId)) {
if(el.elId === element.elId) { let { left, top } = el
return { switch(command) {
...el, case KEYS.LEFT:
left: targetLeft, left = left - 1
top: targetTop, break
} case KEYS.RIGHT:
} left = left + 1
return { break
...el, case KEYS.UP:
left: el.left + (targetLeft - handleElement.left), top = top - 1
top: el.top + (targetTop - handleElement.top), break
case KEYS.DOWN:
top = top + 1
break
default: break
} }
return { ...el, left, top }
} }
return el return el
}) })
} store.commit(MutationTypes.UPDATE_SLIDE, { elements: newElementList })
}
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 { return {

View File

@ -88,7 +88,7 @@ import useCopyAndPasteElement from './hooks/useCopyAndPasteElement'
import useRotateElement from './hooks/useRotateElement' import useRotateElement from './hooks/useRotateElement'
import useScaleElement from './hooks/useScaleElement' import useScaleElement from './hooks/useScaleElement'
import useSelectElement from './hooks/useSelectElement' import useSelectElement from './hooks/useSelectElement'
import useMoveElement from './hooks/useMoveElement' import useDragElement from './hooks/useDragElement'
import useMouseSelection from './hooks/useMouseSelection' import useMouseSelection from './hooks/useMouseSelection'
import useDropImageElement from './hooks/useDropImageElement' import useDropImageElement from './hooks/useDropImageElement'
@ -136,8 +136,8 @@ export default defineComponent({
const { mouseSelectionState, updateMouseSelection } = useMouseSelection(elementList, viewportRef, canvasScale) const { mouseSelectionState, updateMouseSelection } = useMouseSelection(elementList, viewportRef, canvasScale)
const { moveElement } = useMoveElement(elementList, activeGroupElementId, canvasScale, alignmentLines) const { dragElement } = useDragElement(elementList, activeGroupElementId, canvasScale, alignmentLines)
const { selectElement, selectAllElement } = useSelectElement(elementList, activeGroupElementId, moveElement) const { selectElement, selectAllElement } = useSelectElement(elementList, activeGroupElementId, dragElement)
const { scaleElement, scaleMultiElement } = useScaleElement(elementList, canvasScale, activeGroupElementId, alignmentLines) const { scaleElement, scaleMultiElement } = useScaleElement(elementList, canvasScale, activeGroupElementId, alignmentLines)
const { rotateElement } = useRotateElement(elementList, viewportRef, canvasScale) const { rotateElement } = useRotateElement(elementList, viewportRef, canvasScale)
const { orderElement } = useOrderElement(elementList) const { orderElement } = useOrderElement(elementList)

View File

@ -1,120 +0,0 @@
import { createRandomCode } from '@/utils/common'
import { getImageSize } from '@/utils/image'
import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
import { TableElementCell } from '@/types/slides'
import {
DEFAULT_IMAGE,
DEFAULT_TEXT,
DEFAULT_SHAPE,
DEFAULT_LINE,
DEFAULT_CHART,
DEFAULT_TABLE,
} from '@/configs/defaultElement'
interface CommonElementPosition {
top: number;
left: number;
width: number;
height: number;
}
interface LineElementPosition {
top: number;
left: number;
start: [number, number];
end: [number, number];
}
export const insertImage = (imgUrl: string) => {
getImageSize(imgUrl).then(({ width, height }) => {
const scale = width / height
if(scale < VIEWPORT_ASPECT_RATIO && width > VIEWPORT_SIZE) {
width = VIEWPORT_SIZE
height = width * scale
}
else if(height > VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO) {
height = VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO
width = height / scale
}
return {
...DEFAULT_IMAGE,
elId: createRandomCode(),
imgUrl,
width,
height,
}
})
}
export const insertChart = (chartType: string, data: Object) => {
return {
...DEFAULT_CHART,
elId: createRandomCode(),
chartType,
data,
}
}
export const insertTable = (rowCount: number, colCount: number) => {
const row: TableElementCell[] = new Array(colCount).fill({ colspan: 1, rowspan: 1, content: '' })
const data: TableElementCell[][] = new Array(rowCount).fill(row)
const DEFAULT_CELL_WIDTH = 80
const DEFAULT_CELL_HEIGHT = 35
const DEFAULT_BORDER_WIDTH = 2
const colSizes: number[] = new Array(colCount).fill(DEFAULT_CELL_WIDTH)
const rowSizes: number[] = new Array(rowCount).fill(DEFAULT_CELL_HEIGHT)
return {
...DEFAULT_TABLE,
elId: createRandomCode(),
width: colCount * DEFAULT_CELL_WIDTH + DEFAULT_BORDER_WIDTH,
height: rowCount * DEFAULT_CELL_HEIGHT + DEFAULT_BORDER_WIDTH,
colSizes,
rowSizes,
data,
}
}
export const insertText = (position: CommonElementPosition) => {
const { left, top, width, height } = position
return {
...DEFAULT_TEXT,
elId: createRandomCode(),
left,
top,
width,
height,
}
}
export const insertShape = (position: CommonElementPosition, svgCode: string) => {
const { left, top, width, height } = position
return {
...DEFAULT_SHAPE,
elId: createRandomCode(),
left,
top,
width,
height,
svgCode,
}
}
export const insertLine = (position: LineElementPosition, marker: [string, string], lineType: string) => {
const { left, top, start, end } = position
return {
...DEFAULT_LINE,
elId: createRandomCode(),
left,
top,
start,
end,
marker,
lineType,
}
}