docs: 补充代码注释

This commit is contained in:
pipipi-pikachu 2021-02-17 16:56:15 +08:00
parent 816e2b6bd6
commit 72ea8745ae
16 changed files with 148 additions and 87 deletions

View File

@ -105,8 +105,8 @@ export interface AlignLine {
}
/**
* 线线线
* @param lines 线
* 线线线
* @param lines 线
*/
export const uniqAlignLines = (lines: AlignLine[]) => {
const uniqLines: AlignLine[] = []

View File

@ -29,9 +29,11 @@ export default defineComponent({
const store = useStore()
const canvasScale = computed(() => store.state.canvasScale)
// 线
const left = computed(() => props.axis.x * canvasScale.value + 'px')
const top = computed(() => props.axis.y * canvasScale.value + 'px')
// 线
const sizeStyle = computed(() => {
if (props.type === 'vertical') return { height: props.length * canvasScale.value + 'px' }
return { width: props.length * canvasScale.value + 'px' }

View File

@ -53,6 +53,8 @@ export default defineComponent({
offset.y = y
})
//
//
const createSelection = (e: MouseEvent) => {
let isMouseDown = true
@ -66,15 +68,19 @@ export default defineComponent({
let currentPageX = e.pageX
let currentPageY = e.pageY
// CtrlShift
// 线线
if (ctrlOrShiftKeyActive.value) {
const moveX = currentPageX - startPageX
const moveY = currentPageY - startPageY
//
const absX = Math.abs(moveX)
const absY = Math.abs(moveY)
if (creatingElement.value.type === 'shape') {
// moveXmoveY
//
const isOpposite = (moveY > 0 && moveX < 0) || (moveY < 0 && moveX > 0)
if (absX > absY) {
@ -114,6 +120,7 @@ export default defineComponent({
}
}
// 线线使
const lineData = computed(() => {
if (!start.value || !end.value) return null
if (!creatingElement.value || creatingElement.value.type !== 'line') return null
@ -146,6 +153,7 @@ export default defineComponent({
}
})
//
const position = computed(() => {
if (!start.value || !end.value) return {}

View File

@ -28,6 +28,7 @@ export default defineComponent({
const canvasScale = computed(() => store.state.canvasScale)
const background = computed<SlideBackground | undefined>(() => store.getters.currentSlide?.background)
// 线
const gridColor = computed(() => {
if (!background.value || background.value.type === 'image') return 'rgba(100, 100, 100, 0.5)'
const color = background.value.color
@ -43,6 +44,7 @@ export default defineComponent({
const gridSize = 50
//
const getPath = () => {
const maxX = VIEWPORT_SIZE
const maxY = VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO

View File

@ -60,10 +60,22 @@ export default defineComponent({
maxY: 0,
})
// 线
const width = computed(() => (range.maxX - range.minX) * canvasScale.value)
const height = computed(() => (range.maxY - range.minY) * canvasScale.value)
const { resizeHandlers, borderLines } = useCommonOperate(width, height)
//
const setRange = () => {
const { minX, maxX, minY, maxY } = getElementListRange(localActiveElementList.value)
range.minX = minX
range.maxX = maxX
range.minY = minY
range.maxY = maxY
}
watchEffect(setRange)
//
const disableResize = computed(() => {
return localActiveElementList.value.some(item => {
if (
@ -74,16 +86,6 @@ export default defineComponent({
})
})
const setRange = () => {
const { minX, maxX, minY, maxY } = getElementListRange(localActiveElementList.value)
range.minX = minX
range.maxX = maxX
range.minY = minY
range.maxY = maxY
}
watchEffect(setRange)
return {
...toRefs(range),
canvasScale,

View File

@ -2,6 +2,7 @@ import { computed, Ref } from 'vue'
import { OperateResizeHandlers, OperateBorderLines } from '@/types/edit'
export default (width: Ref<number>, height: Ref<number>) => {
// 元素缩放点
const resizeHandlers = computed(() => {
return [
{ direction: OperateResizeHandlers.LEFT_TOP, style: {} },
@ -13,8 +14,9 @@ export default (width: Ref<number>, height: Ref<number>) => {
{ direction: OperateResizeHandlers.BOTTOM, style: {left: width.value / 2 + 'px', top: height.value + 'px'} },
{ direction: OperateResizeHandlers.RIGHT_BOTTOM, style: {left: width.value + 'px', top: height.value + 'px'} },
]
}
)
})
// 文本元素缩放点
const textElementResizeHandlers = computed(() => {
return [
{ direction: OperateResizeHandlers.LEFT, style: {top: height.value / 2 + 'px'} },
@ -22,6 +24,7 @@ export default (width: Ref<number>, height: Ref<number>) => {
]
})
// 元素选中边框线
const borderLines = computed(() => {
return [
{ type: OperateBorderLines.T, style: {width: width.value + 'px'} },

View File

@ -21,19 +21,20 @@ export default (
if (!activeElementIdList.value.includes(element.id)) return
let isMouseDown = true
// 可视范围宽高,用于边缘对齐吸附
const edgeWidth = VIEWPORT_SIZE
const edgeHeight = VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO
const sorptionRange = 5
const originElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList.value))
const originActiveElementList = originElementList.filter(el => activeElementIdList.value.includes(el.id))
const sorptionRange = 5
const elOriginLeft = element.left
const elOriginTop = element.top
const elOriginWidth = element.width
const elOriginHeight = ('height' in element && element.height) ? element.height : 0
const elOriginRotate = ('rotate' in element && element.rotate) ? element.rotate : 0
const startPageX = e.pageX
const startPageY = e.pageY
@ -41,12 +42,12 @@ export default (
const isActiveGroupElement = element.id === activeGroupElementId.value
// 收集对齐参考线
// 包括页面内出被操作元素以外的所有元素在页面内水平和垂直方向的范围和中心位置、页面边界和水平和垂直的中心位置
// 收集对齐对齐吸附线
// 包括页面内除目标元素外的其他元素在画布中的各个可吸附对齐位置:上下左右四边,水平中心、垂直中心
// 其中线条和被旋转过的元素需要重新计算他们在画布中的中心点位置的范围
let horizontalLines: AlignLine[] = []
let verticalLines: AlignLine[] = []
// 元素在页面内水平和垂直方向的范围和中心位置(需要特殊计算线条和被旋转的元素)
for (const el of elementList.value) {
if (el.type === 'line') continue
if (isActiveGroupElement && el.id === element.id) continue
@ -89,7 +90,7 @@ export default (
verticalLines.push(leftLine, rightLine, verticalCenterLine)
}
// 页面边界、水平和垂直的中心位置
// 画布可视区域的四个边界、水平中心、垂直中心
const edgeTopLine: AlignLine = { value: 0, range: [0, edgeWidth] }
const edgeBottomLine: AlignLine = { value: edgeHeight, range: [0, edgeWidth] }
const edgeHorizontalCenterLine: AlignLine = { value: edgeHeight / 2, range: [0, edgeWidth] }
@ -100,34 +101,34 @@ export default (
horizontalLines.push(edgeTopLine, edgeBottomLine, edgeHorizontalCenterLine)
verticalLines.push(edgeLeftLine, edgeRightLine, edgeVerticalCenterLine)
// 参考线去重
// 对齐吸附线去重
horizontalLines = uniqAlignLines(horizontalLines)
verticalLines = uniqAlignLines(verticalLines)
// 开始移动
document.onmousemove = e => {
const currentPageX = e.pageX
const currentPageY = e.pageY
// 对于鼠标第一次滑动距离过小的操作判定为误操作
// 这里仅在误操作标记未被赋值null第一次触发移动以及被标记为误操作时true当前处于误操作范围但可能会脱离该范围转变成正常操作才会去计算
// 已经被标记为非误操作时false不需要再次计算因为不可能从非误操作转变成误操作
// 如果鼠标滑动距离过小,则将操作判定为误操作:
// 如果误操作标记为null表示是第一次触发移动需要计算当前是否是误操作
// 如果误操作标记为true表示当前还处在误操作范围内但仍然需要继续计算检查后续操作是否还处于误操作
// 如果误操作标记为false表示已经脱离了误操作范围不需要再次计算
if (isMisoperation !== false) {
isMisoperation = Math.abs(startPageX - currentPageX) < sorptionRange &&
Math.abs(startPageY - currentPageY) < sorptionRange
}
if (!isMouseDown || isMisoperation) return
// 鼠标按下后移动的距离
const moveX = (currentPageX - startPageX) / canvasScale.value
const moveY = (currentPageY - startPageY) / canvasScale.value
// 被操作元素需要移动到的位置
// 基础目标位置
let targetLeft = elOriginLeft + moveX
let targetTop = elOriginTop + moveY
// 计算被操作元素在页面中的范围(用于吸附对齐)
// 需要区分计算:多选状态、线条、被旋转的元素
// 注意这里需要用元素的原始信息结合移动信息来计算
// 计算目标元素在画布中的位置范围,用于吸附对齐
// 需要区分单选和多选两种情况,其中多选状态下需要计算多选元素的整体范围;单选状态下需要继续区分线条、普通元素、旋转后的普通元素三种情况
let targetMinX: number, targetMaxX: number, targetMinY: number, targetMaxY: number
if (activeElementIdList.value.length === 1 || isActiveGroupElement) {
@ -201,7 +202,8 @@ export default (
const targetCenterX = targetMinX + (targetMaxX - targetMinX) / 2
const targetCenterY = targetMinY + (targetMaxY - targetMinY) / 2
// 根据收集到的参考线,分别执行垂直和水平两个方向的对齐吸附
// 将收集到的对齐吸附线与计算的目标元素位置范围做对比,二者的差小于设定的值时执行自动对齐校正
// 水平和垂直两个方向需要分开计算
const _alignmentLines: AlignmentLineProps[] = []
let isVerticalAdsorbed = false
let isHorizontalAdsorbed = false
@ -249,15 +251,15 @@ export default (
}
alignmentLines.value = _alignmentLines
// 非多选,或者当前操作的元素时激活的组合元素
// 单选状态下,或者当前选中的多个元素中存在正在操作的元素时,仅修改正在操作的元素的位置
if (activeElementIdList.value.length === 1 || isActiveGroupElement) {
elementList.value = elementList.value.map(el => {
return el.id === element.id ? { ...el, left: targetLeft, top: targetTop } : el
})
}
// 修改元素位置,如果需要修改位置的元素不是被操作的元素(例如多选下的操作)
// 那么其他非操作元素要移动的位置通过操作元素的移动偏移量计算
// 多选状态下,除了修改正在操作的元素的位置,其他被选中的元素也需要修改位置信息
// 其他被选中的元素的位置信息通过正在操作的元素的移动偏移量来进行计算
else {
const handleElement = elementList.value.find(el => el.id === element.id)
if (!handleElement) return
@ -291,7 +293,6 @@ export default (
const currentPageX = e.pageX
const currentPageY = e.pageY
// 对比初始位置,没有实际的位移不更新数据
if (startPageX === currentPageX && startPageY === currentPageY) return
store.commit(MutationTypes.UPDATE_SLIDE, { elements: elementList.value })

View File

@ -15,6 +15,7 @@ export default (elementList: Ref<PPTElement[]>) => {
const { addHistorySnapshot } = useHistorySnapshot()
// 拖拽线条端点
const dragLineElement = (e: MouseEvent, element: PPTLineElement, command: OperateLineHandler) => {
let isMouseDown = true
@ -25,7 +26,7 @@ export default (elementList: Ref<PPTElement[]>) => {
const adsorptionPoints: AdsorptionPoint[] = []
// 获取全部非线条且未旋转元素的8个点作为吸附点
// 获取所有线条以外的未旋转的元素的8个缩放点作为吸附位置
for (let i = 0; i < elementList.value.length; i++) {
const _element = elementList.value[i]
if (_element.type === 'line' || ('rotate' in _element && _element.rotate)) continue
@ -68,19 +69,17 @@ export default (elementList: Ref<PPTElement[]>) => {
const currentPageX = e.pageX
const currentPageY = e.pageY
// 鼠标按下后移动的距离
const moveX = (currentPageX - startPageX) / canvasScale.value
const moveY = (currentPageY - startPageY) / canvasScale.value
// 线条两个端点(起点和终点)基于编辑区域的位置
// 线条起点和终点在编辑区域中的位置
let startX = element.left + element.start[0]
let startY = element.top + element.start[1]
let endX = element.left + element.end[0]
let endY = element.top + element.end[1]
// 根据拖拽的点,选择修改起点或终点的位置
// 两点在水平和垂直方向上有对齐吸附
// 靠近其他元素的吸附点有对齐吸附
// 拖拽起点或终点的位置
// 水平和垂直方向上有吸附
if (command === OperateLineHandlers.START) {
startX = startX + moveX
startY = startY + moveY
@ -114,7 +113,7 @@ export default (elementList: Ref<PPTElement[]>) => {
}
}
// 计算两个端点基于自身元素位置的坐标
// 计算更新起点和终点基于自身元素位置的坐标
const minX = Math.min(startX, endX)
const minY = Math.min(startY, endY)
const maxX = Math.max(startX, endX)
@ -131,7 +130,6 @@ export default (elementList: Ref<PPTElement[]>) => {
end[1] = 0
}
// 修改线条的位置和两点的坐标
elementList.value = elementList.value.map(el => {
if (el.id === element.id) {
return {
@ -154,7 +152,6 @@ export default (elementList: Ref<PPTElement[]>) => {
const currentPageX = e.pageX
const currentPageY = e.pageY
// 对比原始鼠标位置,没有实际的位移不更新数据
if (startPageX === currentPageX && startPageY === currentPageY) return
store.commit(MutationTypes.UPDATE_SLIDE, { elements: elementList.value })

View File

@ -9,10 +9,12 @@ export default (elementRef: Ref<HTMLElement | undefined>) => {
const { createImageElement, createTextElement } = useCreateElement()
// 拖拽元素到画布中
const handleDrop = (e: DragEvent) => {
if (!e.dataTransfer) return
const dataTransferItem = e.dataTransfer.items[0]
// 检查事件对象中是否存在图片,存在则插入图片,否则继续检查是否存在文字,存在则插入文字
if (dataTransferItem.kind === 'file' && dataTransferItem.type.indexOf('image') !== -1) {
const imageFile = dataTransferItem.getAsFile()
if (imageFile) {

View File

@ -8,6 +8,7 @@ export default (viewportRef: Ref<HTMLElement | undefined>) => {
const canvasScale = computed(() => store.state.canvasScale)
const creatingElement = computed(() => store.state.creatingElement)
// 通过鼠标框选时的起点和终点,计算选区的位置大小
const formatCreateSelection = (selectionData: CreateElementSelectionData) => {
const { start, end } = selectionData
@ -29,6 +30,7 @@ export default (viewportRef: Ref<HTMLElement | undefined>) => {
return { left, top, width, height }
}
// 通过鼠标框选时的起点和终点,计算线条在画布中的位置和起点终点
const formatCreateSelectionForLine = (selectionData: CreateElementSelectionData) => {
const { start, end } = selectionData
@ -66,6 +68,7 @@ export default (viewportRef: Ref<HTMLElement | undefined>) => {
const { createTextElement, createShapeElement, createLineElement } = useCreateElement()
// 根据鼠标选区的位置大小插入元素
const insertElementFromCreateSelection = (selectionData: CreateElementSelectionData) => {
if (!creatingElement.value) return

View File

@ -16,6 +16,7 @@ export default (elementList: Ref<PPTElement[]>, viewportRef: Ref<HTMLElement | u
quadrant: 1,
})
// 更新鼠标框选范围
const updateMouseSelection = (e: MouseEvent) => {
if (!viewportRef.value) return
@ -30,6 +31,7 @@ export default (elementList: Ref<PPTElement[]>, viewportRef: Ref<HTMLElement | u
const left = (startPageX - viewportRect.x) / canvasScale.value
const top = (startPageY - viewportRect.y) / canvasScale.value
// 确定框选的起始位置和其他默认值初始化
mouseSelectionState.isShow = false
mouseSelectionState.quadrant = 4
mouseSelectionState.top = top
@ -51,12 +53,15 @@ export default (elementList: Ref<PPTElement[]>, viewportRef: Ref<HTMLElement | u
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
@ -68,8 +73,7 @@ export default (elementList: Ref<PPTElement[]>, viewportRef: Ref<HTMLElement | u
document.onmouseup = null
isMouseDown = false
// 计算当前页面中的每一个元素是否处在鼠标选择范围中(必须完全包裹)
// 将选择范围中的元素添加为激活元素
// 计算画布中的元素是否处在鼠标选择范围中,处在范围中的元素设置为被选中状态
let inRangeElementList: PPTElement[] = []
for (let i = 0; i < elementList.value.length; i++) {
const element = elementList.value[i]
@ -82,6 +86,7 @@ export default (elementList: Ref<PPTElement[]>, viewportRef: Ref<HTMLElement | u
const { minX, maxX, minY, maxY } = getElementRange(element)
// 计算元素是否处在框选范围内时,四个框选方向的计算方式有差异
let isInclude = false
if (quadrant === 4) {
isInclude = minX > mouseSelectionLeft &&
@ -108,11 +113,11 @@ export default (elementList: Ref<PPTElement[]>, viewportRef: Ref<HTMLElement | u
maxY < mouseSelectionTop + mouseSelectionHeight
}
// 被锁定的元素除外
// 被锁定的元素即使在范围内,也不需要设置为选中状态
if (isInclude && !element.lock) inRangeElementList.push(element)
}
// 对于组合元素成员,必须所有成员都在选择范围中才算被选中
// 如果范围内有组合元素的成员,需要该组全部成员都处在范围内,才会被设置为选中状态
inRangeElementList = inRangeElementList.filter(inRangeElement => {
if (inRangeElement.groupId) {
const inRangeElementIdList = inRangeElementList.map(inRangeElement => inRangeElement.id)

View File

@ -3,9 +3,11 @@ import { MutationTypes, useStore } from '@/store'
import { PPTElement, PPTTextElement, PPTImageElement, PPTShapeElement } from '@/types/slides'
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
// 给定一个坐标,计算该坐标到(0, 0)点连线的弧度值
// 注意Math.atan2的一般用法是Math.atan2(y, x)返回的是原点(0,0)到(x,y)点的线段与X轴正方向之间的弧度值
// 这里将使用时将x与y的传入顺序交换了为的是获取原点(0,0)到(x,y)点的线段与Y轴正方向之间的弧度值
/**
* 线
* @param x x
* @param y y
*/
const getAngleFromCoordinate = (x: number, y: number) => {
const radian = Math.atan2(x, y)
const angle = 180 / Math.PI * radian
@ -18,16 +20,18 @@ export default (elementList: Ref<PPTElement[]>, viewportRef: Ref<HTMLElement | u
const { addHistorySnapshot } = useHistorySnapshot()
// 旋转元素
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
@ -37,7 +41,7 @@ export default (elementList: Ref<PPTElement[]>, viewportRef: Ref<HTMLElement | u
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
@ -45,7 +49,7 @@ export default (elementList: Ref<PPTElement[]>, viewportRef: Ref<HTMLElement | u
angle = getAngleFromCoordinate(x, y)
// 45°的倍数位置有吸附效果
// 靠近45倍数的角度时有吸附效果
const sorptionRange = 5
if ( Math.abs(angle) <= sorptionRange ) angle = 0
else if ( angle > 0 && Math.abs(angle - 45) <= sorptionRange ) angle -= (angle - 45)
@ -57,7 +61,6 @@ export default (elementList: Ref<PPTElement[]>, viewportRef: Ref<HTMLElement | u
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.id === el.id ? { ...el, rotate: angle } : el)
}

View File

@ -8,13 +8,18 @@ import { MIN_SIZE } from '@/configs/element'
import { AlignLine, uniqAlignLines } from '@/utils/element'
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
// 计算元素被旋转一定角度后,八个操作点的新坐标
interface RotateElementData {
left: number;
top: number;
width: number;
height: number;
}
/**
*
* @param element
* @param angle
*/
const getRotateElementPoints = (element: RotateElementData, angle: number) => {
const { left, top, width, height } = element
@ -68,7 +73,11 @@ const getRotateElementPoints = (element: RotateElementData, angle: number) => {
return { leftTopPoint, topPoint, rightTopPoint, rightPoint, rightBottomPoint, bottomPoint, leftBottomPoint, leftPoint }
}
// 获取元素某个操作点对角线上另一端的操作点坐标(例如:左上 <-> 右下)
/**
*
* @param direction
* @param points
*/
const getOppositePoint = (direction: string, points: ReturnType<typeof getRotateElementPoints>): { left: number; top: number } => {
const oppositeMap = {
[OperateResizeHandlers.RIGHT_BOTTOM]: points.leftTopPoint,
@ -95,6 +104,7 @@ export default (
const { addHistorySnapshot } = useHistorySnapshot()
// 缩放元素
const scaleElement = (e: MouseEvent, element: Exclude<PPTElement, PPTLineElement>, command: OperateResizeHandlers) => {
let isMouseDown = true
emitter.emit(EmitterEvents.SCALE_ELEMENT_STATE, true)
@ -103,16 +113,17 @@ export default (
const elOriginTop = element.top
const elOriginWidth = element.width
const elOriginHeight = element.height
const fixedRatio = ctrlOrShiftKeyActive.value || ('fixedRatio' in element && element.fixedRatio)
const aspectRatio = elOriginWidth / elOriginHeight
const elRotate = ('rotate' in element && element.rotate) ? element.rotate : 0
const rotateRadian = Math.PI * elRotate / 180
const fixedRatio = ctrlOrShiftKeyActive.value || ('fixedRatio' in element && element.fixedRatio)
const aspectRatio = elOriginWidth / elOriginHeight
const startPageX = e.pageX
const startPageY = e.pageY
// 元素最小缩放限制
const minSize = MIN_SIZE[element.type] || 20
const getSizeWithinRange = (size: number) => size < minSize ? minSize : size
@ -122,16 +133,20 @@ export default (
let horizontalLines: AlignLine[] = []
let verticalLines: AlignLine[] = []
// 旋转后的元素进行缩放时,引入基点的概念,以当前操作的缩放点相对的点为基点
// 例如拖动右下角缩放时,左上角为基点,需要保持左上角不变然后修改其他的点的位置来达到所放的效果
if ('rotate' in element && element.rotate) {
// 元素旋转后的各点坐标以及对角坐标
const { left, top, width, height } = element
points = getRotateElementPoints({ left, top, width, height }, elRotate)
const oppositePoint = getOppositePoint(command, points)
// 基点坐标(以操作点相对的点为基点,例如拖动右下角,实际上是保持左上角不变的前提下修改其他信息)
baseLeft = oppositePoint.left
baseTop = oppositePoint.top
}
// 未旋转的元素具有缩放时的对齐吸附功能,在这处收集对齐对齐吸附线
// 包括页面内除目标元素外的其他元素在画布中的各个可吸附对齐位置:上下左右四边
// 其中线条和被旋转过的元素不参与吸附对齐
else {
const edgeWidth = VIEWPORT_SIZE
const edgeHeight = VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO
@ -159,7 +174,7 @@ export default (
verticalLines.push(leftLine, rightLine)
}
// 页面边界、水平和垂直的中心位置
// 画布可视区域的四个边界、水平中心、垂直中心
const edgeTopLine: AlignLine = { value: 0, range: [0, edgeWidth] }
const edgeBottomLine: AlignLine = { value: edgeHeight, range: [0, edgeWidth] }
const edgeHorizontalCenterLine: AlignLine = { value: edgeHeight / 2, range: [0, edgeWidth] }
@ -175,6 +190,8 @@ export default (
}
// 对齐吸附方法
// 将收集到的对齐吸附线与计算的目标元素当前的位置大小相关数据做对比,差值小于设定的值时执行自动缩放校正
// 水平和垂直两个方向需要分开计算
const alignedAdsorption = (currentX: number | null, currentY: number | null) => {
const sorptionRange = 5
@ -213,6 +230,7 @@ export default (
return correctionVal
}
// 开始缩放
document.onmousemove = e => {
if (!isMouseDown) return
@ -227,21 +245,22 @@ export default (
let left = elOriginLeft
let top = elOriginTop
// 元素被旋转的情况下
// 元素被旋转的情况下,需要根据元素旋转的角度,重新计算需要缩放的距离(鼠标按下后移动的距离)
if (elRotate) {
// 根据元素旋转的角度,修正鼠标按下后移动的距离(因为拖动的方向发生了改变)
const revisedX = (Math.cos(rotateRadian) * x + Math.sin(rotateRadian) * y) / canvasScale.value
let revisedY = (Math.cos(rotateRadian) * y - Math.sin(rotateRadian) * x) / canvasScale.value
// 锁定宽高比例
// 锁定宽高比例(仅四个角可能触发,四条边不会触发)
// 以水平方向上缩放的距离为基础,计算垂直方向上的缩放距离,保持二者具有相同的缩放比例
if (fixedRatio) {
if (command === OperateResizeHandlers.RIGHT_BOTTOM || command === OperateResizeHandlers.LEFT_TOP) revisedY = revisedX / aspectRatio
if (command === OperateResizeHandlers.LEFT_BOTTOM || command === OperateResizeHandlers.RIGHT_TOP) revisedY = -revisedX / aspectRatio
}
// 根据不同的操作点分别计算元素缩放后的大小和位置
// 这里计算的位置是错误的,因为旋转后缩放实际上也改变了元素的位置,需要在后面进行矫正
// 这里计算的大小是正确的,因为上面修正鼠标按下后移动的距离时其实已经进行过了矫正
// 需要注意:
// 此处计算的位置需要在后面重新进行校正,因为旋转后再缩放事实上会改变元素基点的位置(虽然视觉上基点保持不动,但这是【旋转】+【移动】共同作用的结果)
// 但此处计算的大小不需要重新校正,因为前面已经重新计算需要缩放的距离,相当于大小已经经过了校正
if (command === OperateResizeHandlers.RIGHT_BOTTOM) {
width = getSizeWithinRange(elOriginWidth + revisedX)
height = getSizeWithinRange(elOriginHeight + revisedY)
@ -277,7 +296,7 @@ export default (
width = getSizeWithinRange(elOriginWidth + revisedX)
}
// 获取当前元素基点坐标,与初始状态的基点坐标进行对比并矫正差值
// 获取当前元素的基点坐标,与初始状态时的基点坐标进行对比,并计算差值进行元素位置的校正
const currentPoints = getRotateElementPoints({ width, height, left, top }, elRotate)
const currentOppositePoint = getOppositePoint(command, currentPoints)
const currentBaseLeft = currentOppositePoint.left
@ -290,7 +309,9 @@ export default (
top = top - offsetY
}
// 元素未被旋转的情况下,根据所操纵点的位置添加对齐吸附
// 元素未被旋转的情况下,正常计算新的位置大小即可,无需复杂的校正等工作
// 额外需要处理对齐吸附相关的操作
// 锁定宽高比例相关的操作同上,不再赘述
else {
let moveX = x / canvasScale.value
let moveY = y / canvasScale.value
@ -390,6 +411,7 @@ export default (
}
}
// 多选元素缩放
const scaleMultiElement = (e: MouseEvent, range: MultiSelectRange, command: OperateResizeHandlers) => {
let isMouseDown = true
@ -409,17 +431,16 @@ export default (
const currentPageX = e.pageX
const currentPageY = e.pageY
// 鼠标按下后移动的距离
const x = (currentPageX - startPageX) / canvasScale.value
let y = (currentPageY - startPageY) / canvasScale.value
// 锁定宽高比例
// 锁定宽高比例,逻辑同上
if (ctrlOrShiftKeyActive.value) {
if (command === OperateResizeHandlers.RIGHT_BOTTOM || command === OperateResizeHandlers.LEFT_TOP) y = x / aspectRatio
if (command === OperateResizeHandlers.LEFT_BOTTOM || command === OperateResizeHandlers.RIGHT_TOP) y = -x / aspectRatio
}
// 获取鼠标缩放时当前所有激活元素的范围
// 所有选中元素的整体范围
let currentMinX = minX
let currentMaxX = maxX
let currentMinY = minY
@ -454,19 +475,18 @@ export default (
currentMaxX = maxX + x
}
// 多选下所有元素整体宽高
// 所有选中元素整体宽高
const currentOppositeWidth = currentMaxX - currentMinX
const currentOppositeHeight = currentMaxY - currentMinY
// 所有元素的整体宽高与被操作元素宽高的比例
// 当前正在操作元素宽高占所有选中元素的整体宽高的比例
let widthScale = currentOppositeWidth / operateWidth
let heightScale = currentOppositeHeight / operateHeight
if (widthScale <= 0) widthScale = 0
if (heightScale <= 0) heightScale = 0
// 根据上面计算的比例,修改所有被激活元素的位置大小
// 宽高通过乘以对应的比例得到,位置通过将被操作元素在所有元素整体中的相对位置乘以对应比例获得
// 根据前面计算的比例,计算并修改所有选中元素的位置大小
elementList.value = elementList.value.map(el => {
if ((el.type === 'image' || el.type === 'shape') && activeElementIdList.value.includes(el.id)) {
const originElement = originElementList.find(originEl => originEl.id === el.id) as PPTImageElement | PPTShapeElement

View File

@ -14,10 +14,13 @@ export default (
const editorAreaFocus = computed(() => store.state.editorAreaFocus)
const ctrlOrShiftKeyActive = computed<boolean>(() => store.getters.ctrlOrShiftKeyActive)
// 选中元素
const selectElement = (e: MouseEvent, element: PPTElement, canMove = true) => {
if (!editorAreaFocus.value) store.commit(MutationTypes.SET_EDITORAREA_FOCUS, true)
// 如果被点击的元素处于未激活状态,则将他设置为激活元素(单选),或者加入到激活元素中(多选)
// 如果目标元素当前未被选中,则将他设为选中状态
// 此时如果按下Ctrl键或Shift键则进入多选状态将当前已选中的元素和目标元素一桶设置为选中状态否则仅将目标元素设置为选中状态
// 如果目标元素是分组成员,需要将该组合的其他元素一起设置为选中状态
if (!activeElementIdList.value.includes(element.id)) {
let newActiveIdList: string[] = []
@ -26,7 +29,6 @@ export default (
}
else newActiveIdList = [element.id]
// 同时如果该元素是分组成员,需要将和他同组的元素一起激活
if (element.groupId) {
const groupMembersId: string[] = []
elementList.value.forEach((el: PPTElement) => {
@ -39,11 +41,12 @@ export default (
store.commit(MutationTypes.SET_HANDLE_ELEMENT_ID, element.id)
}
// 如果被点击的元素已激活,且按下了多选按钮,则取消其激活状态(除非该元素或分组是最后的一个激活元素)
// 如果目标元素已被选中且按下了Ctrl键或Shift键则取消其被选中状态
// 除非目标元素是最后的一个被选中元素,或者目标元素所在的组合是最后一组选中组合
// 如果目标元素是分组成员,需要将该组合的其他元素一起取消选中状态
else if (ctrlOrShiftKeyActive.value) {
let newActiveIdList: string[] = []
// 同时如果该元素是分组成员,需要将和他同组的元素一起取消
if (element.groupId) {
const groupMembersId: string[] = []
elementList.value.forEach((el: PPTElement) => {
@ -60,12 +63,12 @@ export default (
}
}
// 如果被点击的元素已激活,且没有按下多选按钮,且该元素不是当前操作元素,则将其设置为当前操作元素
// 如果目标元素已被选中,同时目标元素不是当前操作元素,则将其设置为当前操作元素
else if (handleElementId.value !== element.id) {
store.commit(MutationTypes.SET_HANDLE_ELEMENT_ID, element.id)
}
// 如果被点击的元素是当前操作元素,且没有按下多选按钮,则该元素下次保持该状态再次被点击时,将被设置为多选元素中的激活成员
// 如果目标元素已被选中,同时也是当前操作元素,那么当目标元素在该状态下再次被点击时,将被设置为多选元素中的激活成员
else if (activeGroupElementId.value !== element.id) {
const startPageX = e.pageX
const startPageY = e.pageY
@ -84,6 +87,7 @@ export default (
if (canMove) moveElement(e, element)
}
// 选中页面内的全部元素
const selectAllElement = () => {
const unlockedElements = elementList.value.filter(el => !el.lock)
const newActiveElementIdList = unlockedElements.map(el => el.id)

View File

@ -9,7 +9,8 @@ export default (canvasRef: Ref<HTMLElement | undefined>) => {
const store = useStore()
const canvasPercentage = computed(() => store.state.canvasPercentage)
const setViewportSize = () => {
// 计算画布可视区域的位置
const setViewportPosition = () => {
if (!canvasRef.value) return
const canvasWidth = canvasRef.value.clientWidth
const canvasHeight = canvasRef.value.clientHeight
@ -28,8 +29,10 @@ export default (canvasRef: Ref<HTMLElement | undefined>) => {
}
}
watch(canvasPercentage, setViewportSize)
// 可视区域缩放时,更新可视区域的位置
watch(canvasPercentage, setViewportPosition)
// 画布可视区域位置和大小的样式
const viewportStyles = computed(() => ({
width: VIEWPORT_SIZE,
height: VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO,
@ -37,7 +40,8 @@ export default (canvasRef: Ref<HTMLElement | undefined>) => {
top: viewportTop.value,
}))
const resizeObserver = new ResizeObserver(setViewportSize)
// 监听画布尺寸发生变化时,更新可视区域的位置
const resizeObserver = new ResizeObserver(setViewportPosition)
onMounted(() => {
if (canvasRef.value) resizeObserver.observe(canvasRef.value)

View File

@ -159,6 +159,7 @@ export default defineComponent({
const { pasteElement } = useCopyAndPasteElement()
const { enterScreening } = useScreening()
//
const handleClickBlankArea = (e: MouseEvent) => {
store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, [])
if (!ctrlOrShiftKeyActive.value) updateMouseSelection(e)
@ -166,10 +167,12 @@ export default defineComponent({
removeAllRanges()
}
//
const removeEditorAreaFocus = () => {
if (editorAreaFocus.value) store.commit(MutationTypes.SET_EDITORAREA_FOCUS, false)
}
// Ctrl
const { scaleCanvas } = useScaleCanvas()
const throttleScaleCanvas = throttle(scaleCanvas, 100, { leading: true, trailing: false })
@ -181,11 +184,13 @@ export default defineComponent({
else if (e.deltaY < 0) throttleScaleCanvas('+')
}
// 线
const showGridLines = computed(() => store.state.showGridLines)
const toggleGridLines = () => {
store.commit(MutationTypes.SET_GRID_LINES_STATE, !showGridLines.value)
}
//
const creatingElement = computed(() => store.state.creatingElement)
const { insertElementFromCreateSelection } = useInsertFromCreateSelection(viewportRef)