update
This commit is contained in:
parent
dc388ac6ab
commit
2d245dd88b
|
@ -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
|
||||
}
|
|
@ -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: '清空页面',
|
||||
},
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -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]
|
||||
}
|
|
@ -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) => {
|
||||
|
|
Loading…
Reference in New Issue