This commit is contained in:
pipipi-pikachu 2020-12-14 22:29:09 +08:00
parent dc388ac6ab
commit 2d245dd88b
6 changed files with 301 additions and 26 deletions

33
src/hooks/useDropImage.ts Normal file
View File

@ -0,0 +1,33 @@
import { ref, onMounted, onUnmounted, Ref } from 'vue'
export default (elementRef: Ref<HTMLElement | null>) => {
const imageFile = ref<File | null>(null)
const handleDrop = (e: DragEvent) => {
if(!e.dataTransfer) return
const file = e.dataTransfer.items[0]
if( file.kind === 'file' && file.type.indexOf('image') !== -1 ) {
const _imageFile = file.getAsFile()
if(_imageFile) imageFile.value = _imageFile
}
}
onMounted(() => {
elementRef.value && elementRef.value.addEventListener('drop', handleDrop)
document.ondragleave = e => e.preventDefault()
document.ondrop = e => e.preventDefault()
document.ondragenter = e => e.preventDefault()
document.ondragover = e => e.preventDefault()
})
onUnmounted(() => {
elementRef.value && elementRef.value.removeEventListener('drop', handleDrop)
document.ondragleave = null
document.ondrop = null
document.ondragenter = null
document.ondragover = null
})
return imageFile
}

View File

@ -40,13 +40,15 @@
</template>
<script lang="ts">
import { computed, defineComponent, onMounted, onUnmounted, reactive, ref } from 'vue'
import { computed, defineComponent, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
import { useStore } from 'vuex'
import { State } from '@/store/state'
import { MutationTypes } from '@/store/constants'
import { ContextmenuItem } from '@/components/Contextmenu/types'
import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
import useDropImage from '@/hooks/useDropImage'
import MouseSelection from './MouseSelection.vue'
import SlideBackground from './SlideBackground.vue'
import AlignmentLine, { AlignmentLineProps } from './AlignmentLine.vue'
@ -59,9 +61,15 @@ export default defineComponent({
AlignmentLine,
},
setup() {
const viewportRef = ref<HTMLElement | null>(null)
const isShowGridLines = ref(false)
const alignmentLines = ref<AlignmentLineProps[]>([])
const dropImageFile = useDropImage(viewportRef)
watch(dropImageFile, () => {
console.log(dropImageFile.value)
})
const viewportStyles = reactive({
width: VIEWPORT_SIZE,
height: VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO,
@ -99,14 +107,14 @@ export default defineComponent({
}
const resizeObserver = new ResizeObserver(setViewportSize)
onMounted(() => {
if(canvasRef.value) resizeObserver.observe(canvasRef.value)
})
onUnmounted(() => {
if(canvasRef.value) resizeObserver.unobserve(canvasRef.value)
})
const viewportRef = ref<Element | null>(null)
const mouseSelectionState = reactive({
isShow: false,
top: 0,
@ -212,10 +220,6 @@ export default defineComponent({
},
],
},
{
text: '背景设置',
},
{ divider: true },
{
text: '清空页面',
},

View File

@ -0,0 +1,22 @@
interface AlignLine {
value: number;
range: [number, number];
}
// 对齐参考线去重,对于相同位置的多条参考线,取长度范围的最小值和最大值,并基于此范围将多条参考线合并为一条
export const uniqAlignLines = (lines: AlignLine[]) => {
const uniqLines: AlignLine[] = []
lines.forEach(line => {
const index = uniqLines.findIndex(_line => _line.value === line.value)
if(index === -1) uniqLines.push(line)
else {
const uniqLine = uniqLines[index]
const rangeMin = Math.min(uniqLine.range[0], line.range[0])
const rangeMax = Math.max(uniqLine.range[1], line.range[1])
const range: [number, number] = [rangeMin, rangeMax]
const _line = { value: line.value, range }
uniqLines[index] = _line
}
})
return uniqLines
}

View File

@ -0,0 +1,85 @@
import { PPTElement, PPTTextElement, PPTImageElement, PPTShapeElement, PPTIconElement } from '@/types/slides'
// 获取矩形旋转后在画布中的位置范围
export const getRectRotatedRange = (element: PPTTextElement | PPTImageElement | PPTShapeElement | PPTIconElement) => {
const { left, top, width, height, rotate = 0 } = element
const radius = Math.sqrt( Math.pow(width, 2) + Math.pow(height, 2) ) / 2
const auxiliaryAngle = Math.atan(height / width) * 180 / Math.PI
const tlbraRadian = (180 - rotate - auxiliaryAngle) * Math.PI / 180
const trblaRadian = (auxiliaryAngle - rotate) * Math.PI / 180
const halfWidth = width / 2
const halfHeight = height / 2
const middleLeft = left + halfWidth
const middleTop = top + halfHeight
const xAxis = [
middleLeft + radius * Math.cos(tlbraRadian),
middleLeft + radius * Math.cos(trblaRadian),
middleLeft - radius * Math.cos(tlbraRadian),
middleLeft - radius * Math.cos(trblaRadian),
]
const yAxis = [
middleTop - radius * Math.sin(tlbraRadian),
middleTop - radius * Math.sin(trblaRadian),
middleTop + radius * Math.sin(tlbraRadian),
middleTop + radius * Math.sin(trblaRadian),
]
return {
xRange: [Math.min(...xAxis), Math.max(...xAxis)],
yRange: [Math.min(...yAxis), Math.max(...yAxis)],
}
}
// 获取元素在画布中的位置范围
export const getElementRange = (element: PPTElement) => {
let minX, maxX, minY, maxY
if(element.type === 'line') {
minX = element.left
maxX = element.left + Math.max(element.start[0], element.end[0])
minY = element.top
maxY = element.top + Math.max(element.start[1], element.end[1])
}
else if('rotate' in element && element.rotate) {
const { xRange, yRange } = getRectRotatedRange(element)
minX = xRange[0]
maxX = xRange[1]
minY = yRange[0]
maxY = yRange[1]
}
else {
minX = element.left
maxX = element.left + element.width
minY = element.top
maxY = element.top + element.height
}
return { minX, maxX, minY, maxY }
}
// 获取元素集合在画布中的位置范围
export const getElementListRange = (elementList: PPTElement[]) => {
const leftValues: number[] = []
const topValues: number[] = []
const rightValues: number[] = []
const bottomValues: number[] = []
elementList.forEach(element => {
const { minX, maxX, minY, maxY } = getElementRange(element)
leftValues.push(minX)
topValues.push(minY)
rightValues.push(maxX)
bottomValues.push(maxY)
})
const minX = Math.min(...leftValues)
const maxX = Math.max(...rightValues)
const minY = Math.min(...topValues)
const maxY = Math.max(...bottomValues)
return { minX, maxX, minY, maxY }
}

View File

@ -0,0 +1,80 @@
import { PPTTextElement, PPTImageElement, PPTShapeElement, PPTIconElement } from '@/types/slides'
import { OPERATE_KEYS } from '@/configs/element'
// 给定一个坐标,计算该坐标到(0, 0)点连线的弧度值
// 注意Math.atan2的一般用法是Math.atan2(y, x)返回的是原点(0,0)到(x,y)点的线段与X轴正方向之间的弧度值
// 这里将使用时将x与y的传入顺序交换了为的是获取原点(0,0)到(x,y)点的线段与Y轴正方向之间的弧度值
export const getAngleFromCoordinate = (x: number, y: number) => {
const radian = Math.atan2(x, y)
const angle = 180 / Math.PI * radian
return angle
}
// 计算元素被旋转一定角度后,八个操作点的新坐标
export const getRotateElementPoints = (element: PPTTextElement | PPTImageElement | PPTShapeElement | PPTIconElement, angle: number) => {
const { left, top, width, height } = element
const radius = Math.sqrt( Math.pow(width, 2) + Math.pow(height, 2) ) / 2
const auxiliaryAngle = Math.atan(height / width) * 180 / Math.PI
const tlbraRadian = (180 - angle - auxiliaryAngle) * Math.PI / 180
const trblaRadian = (auxiliaryAngle - angle) * Math.PI / 180
const taRadian = (90 - angle) * Math.PI / 180
const raRadian = angle * Math.PI / 180
const halfWidth = width / 2
const halfHeight = height / 2
const middleLeft = left + halfWidth
const middleTop = top + halfHeight
const leftTopPoint = {
left: middleLeft + radius * Math.cos(tlbraRadian),
top: middleTop - radius * Math.sin(tlbraRadian),
}
const topPoint = {
left: middleLeft + halfHeight * Math.cos(taRadian),
top: middleTop - halfHeight * Math.sin(taRadian),
}
const rightTopPoint = {
left: middleLeft + radius * Math.cos(trblaRadian),
top: middleTop - radius * Math.sin(trblaRadian),
}
const rightPoint = {
left: middleLeft + halfWidth * Math.cos(raRadian),
top: middleTop + halfWidth * Math.sin(raRadian),
}
const rightBottomPoint = {
left: middleLeft - radius * Math.cos(tlbraRadian),
top: middleTop + radius * Math.sin(tlbraRadian),
}
const bottomPoint = {
left: middleLeft - halfHeight * Math.sin(raRadian),
top: middleTop + halfHeight * Math.cos(raRadian),
}
const leftBottomPoint = {
left: middleLeft - radius * Math.cos(trblaRadian),
top: middleTop + radius * Math.sin(trblaRadian),
}
const leftPoint = {
left: middleLeft - halfWidth * Math.cos(raRadian),
top: middleTop - halfWidth * Math.sin(raRadian),
}
return { leftTopPoint, topPoint, rightTopPoint, rightPoint, rightBottomPoint, bottomPoint, leftBottomPoint, leftPoint }
}
// 获取元素某个操作点对角线上另一端的操作点坐标(例如:左上 <-> 右下)
export const getOppositePoint = (direction: number, points: ReturnType<typeof getRotateElementPoints>) => {
const oppositeMap = {
[OPERATE_KEYS.RIGHT_BOTTOM]: points.leftTopPoint,
[OPERATE_KEYS.LEFT_BOTTOM]: points.rightTopPoint,
[OPERATE_KEYS.LEFT_TOP]: points.rightBottomPoint,
[OPERATE_KEYS.RIGHT_TOP]: points.leftBottomPoint,
[OPERATE_KEYS.TOP]: points.bottomPoint,
[OPERATE_KEYS.BOTTOM]: points.topPoint,
[OPERATE_KEYS.LEFT]: points.rightPoint,
[OPERATE_KEYS.RIGHT]: points.leftPoint,
}
return oppositeMap[direction]
}

View File

@ -17,6 +17,7 @@ import { computed, defineComponent, onMounted, onUnmounted, ref } from 'vue'
import { useStore } from 'vuex'
import { State } from '@/store/state'
import { KEYCODE } from '@/configs/keyCode'
import { decrypt } from '@/utils/index'
import { message } from 'ant-design-vue'
@ -87,25 +88,68 @@ export default defineComponent({
if(ctrlKey && !ctrlKeyDown.value) ctrlKeyDown.value = true
if(shiftKey && !shiftKeyDown.value) shiftKeyDown.value = true
if(!editorAreaFocus.value && !thumbnailsFocus.value) return
e.preventDefault()
if(!editorAreaFocus.value && !thumbnailsFocus.value) return
if(ctrlKey && keyCode === KEYCODE.S) save()
if(ctrlKey && keyCode === KEYCODE.C) copy()
if(ctrlKey && keyCode === KEYCODE.X) cut()
if(ctrlKey && keyCode === KEYCODE.Z) undo()
if(ctrlKey && keyCode === KEYCODE.Y) redo()
if(ctrlKey && keyCode === KEYCODE.A) selectAll()
if(ctrlKey && keyCode === KEYCODE.L) lock()
if(!shiftKey && ctrlKey && keyCode === KEYCODE.G) combine()
if(shiftKey && ctrlKey && keyCode === KEYCODE.G) uncombine()
if(keyCode === KEYCODE.DELETE) remove()
if(keyCode === KEYCODE.UP) move(KEYCODE.UP)
if(keyCode === KEYCODE.DOWN) move(KEYCODE.DOWN)
if(keyCode === KEYCODE.LEFT) move(KEYCODE.LEFT)
if(keyCode === KEYCODE.RIGHT) move(KEYCODE.RIGHT)
if(keyCode === KEYCODE.ENTER) create()
if(ctrlKey && keyCode === KEYCODE.S) {
e.preventDefault()
save()
}
if(ctrlKey && keyCode === KEYCODE.C) {
e.preventDefault()
copy()
}
if(ctrlKey && keyCode === KEYCODE.X) {
e.preventDefault()
cut()
}
if(ctrlKey && keyCode === KEYCODE.Z) {
e.preventDefault()
undo()
}
if(ctrlKey && keyCode === KEYCODE.Y) {
e.preventDefault()
redo()
}
if(ctrlKey && keyCode === KEYCODE.A) {
e.preventDefault()
selectAll()
}
if(ctrlKey && keyCode === KEYCODE.L) {
e.preventDefault()
lock()
}
if(!shiftKey && ctrlKey && keyCode === KEYCODE.G) {
e.preventDefault()
combine()
}
if(shiftKey && ctrlKey && keyCode === KEYCODE.G) {
e.preventDefault()
uncombine()
}
if(keyCode === KEYCODE.DELETE) {
e.preventDefault()
remove()
}
if(keyCode === KEYCODE.UP) {
e.preventDefault()
move(KEYCODE.UP)
}
if(keyCode === KEYCODE.DOWN) {
e.preventDefault()
move(KEYCODE.DOWN)
}
if(keyCode === KEYCODE.LEFT) {
e.preventDefault()
move(KEYCODE.LEFT)
}
if(keyCode === KEYCODE.RIGHT) {
e.preventDefault()
move(KEYCODE.RIGHT)
}
if(keyCode === KEYCODE.ENTER) {
e.preventDefault()
create()
}
}
const keyupListener = () => {
@ -118,7 +162,14 @@ export default defineComponent({
}
const pasteText = (text: string) => {
console.log(text)
let content
try {
content = JSON.parse(decrypt(text))
}
catch {
content = text
}
console.log(content)
}
const pasteListener = (e: ClipboardEvent) => {