This commit is contained in:
pipipi-pikachu 2020-12-17 21:13:55 +08:00
parent 7246f89bc0
commit d57cdafa5d
12 changed files with 404 additions and 13 deletions

View File

@ -101,7 +101,7 @@ $subMenuWidth: 120px;
}
.contextmenu-item {
padding: 0 20px;
color: #666;
color: #555;
font-size: 12px;
transition: all 0.3s;
white-space: nowrap;

View File

@ -4,6 +4,16 @@ export enum ClipPathTypes {
POLYGON = 'polygon',
}
export enum ClipPaths {
RECT = 'rect',
ROUNDRECT = 'roundRect',
ELLIPSE = 'ellipse',
TRIANGLE = 'triangle',
PENTAGON = 'pentagon',
RHOMBUS = 'rhombus',
STAR = 'star',
}
export const CLIPPATHS = {
rect: {
name: '矩形',

View File

@ -39,7 +39,7 @@ export interface PPTImageElement extends PPTElementBaseProps, PPTElementSizeProp
filter?: string;
clip?: {
range: [[number, number], [number, number]];
shape: 'rect' | 'ellipse' | 'polygon';
shape: 'rect' | 'roundRect' | 'ellipse' | 'triangle' | 'pentagon' | 'rhombus' | 'star';
};
flip?: { x?: number, y?: number };
shadow?: string;

View File

@ -35,12 +35,35 @@
v-for="(line, index) in alignmentLines" :key="index"
:type="line.type" :axis="line.axis" :length="line.length"
/>
<EditableElement
v-for="(element, index) in elementList"
:key="element.elId"
:elementInfo="element"
:elementIndex="index + 1"
:isActive="activeElementIdList.includes(element.elId)"
:isHandleEl="element.elId === handleElementId"
:isActiveGroupElement="activeGroupElementId === element.elId"
:isMultiSelect="activeElementIdList.length > 1"
:canvasScale="canvasScale"
:selectElement="selectElement"
:rotateElement="rotateElement"
:scaleElement="scaleElement"
:orderElement="orderElement"
:combineElements="combineElements"
:uncombineElements="uncombineElements"
:alignElement="alignElement"
:deleteElement="deleteElement"
:lockElement="lockElement"
:copyElement="copyElement"
:cutElement="cutElement"
/>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
import { computed, defineComponent, reactive, ref, watch } from 'vue'
import { useStore } from 'vuex'
import { State } from '@/store/state'
import { MutationTypes } from '@/store/constants'
@ -51,6 +74,7 @@ import { getImageDataURL } from '@/utils/image'
import useDropImage from '@/hooks/useDropImage'
import useSetViewportSize from './hooks/useSetViewportSize'
import EditableElement from '@/views/_common/_element/EditableElement.vue'
import MouseSelection from './MouseSelection.vue'
import SlideBackground from './SlideBackground.vue'
import AlignmentLine, { AlignmentLineProps } from './AlignmentLine.vue'
@ -58,12 +82,21 @@ import AlignmentLine, { AlignmentLineProps } from './AlignmentLine.vue'
export default defineComponent({
name: 'v-canvas',
components: {
EditableElement,
MouseSelection,
SlideBackground,
AlignmentLine,
},
setup() {
const store = useStore<State>()
const elementList = computed(() => {
const currentSlide = store.getters.currentSlide
return currentSlide ? JSON.parse(JSON.stringify(currentSlide.elements)) : []
})
const activeElementIdList = computed(() => store.state.activeElementIdList)
const handleElementId = computed(() => store.state.handleElementId)
const activeGroupElementId = ref('')
const viewportRef = ref<HTMLElement | null>(null)
const isShowGridLines = ref(false)
const alignmentLines = ref<AlignmentLineProps[]>([])
@ -162,6 +195,40 @@ export default defineComponent({
if(editorAreaFocus.value) store.commit(MutationTypes.SET_EDITORAREA_FOCUS, false)
}
const selectElement = () => {
console.log('selectElement')
}
const rotateElement = () => {
console.log('rotateElement')
}
const scaleElement = () => {
console.log('scaleElement')
}
const orderElement = () => {
console.log('orderElement')
}
const combineElements = () => {
console.log('combineElements')
}
const uncombineElements = () => {
console.log('uncombineElements')
}
const alignElement = () => {
console.log('alignElement')
}
const deleteElement = () => {
console.log('deleteElement')
}
const lockElement = () => {
console.log('lockElement')
}
const copyElement = () => {
console.log('copyElement')
}
const cutElement = () => {
console.log('cutElement')
}
const contextmenus = (): ContextmenuItem[] => {
return [
{
@ -179,6 +246,10 @@ export default defineComponent({
}
return {
elementList,
activeElementIdList,
handleElementId,
activeGroupElementId,
canvasRef,
viewportRef,
viewportStyles,
@ -189,6 +260,17 @@ export default defineComponent({
currentSlide,
isShowGridLines,
alignmentLines,
selectElement,
rotateElement,
scaleElement,
orderElement,
combineElements,
uncombineElements,
alignElement,
deleteElement,
lockElement,
copyElement,
cutElement,
contextmenus,
}
},

View File

@ -13,7 +13,7 @@
:isHandleEl="isHandleEl"
:isActiveGroupElement="isActiveGroupElement"
:isMultiSelect="isMultiSelect"
:animationIndex="-1"
:animationIndex="animationIndex"
:selectElement="selectElement"
:rotateElement="rotateElement"
:scaleElement="scaleElement"
@ -36,8 +36,8 @@ import {
ElementLockCommands,
} from '@/types/edit'
import ImageElement from './ImageElement.index.vue'
import TextElement from './TextElement.index.vue'
import ImageElement from './ImageElement/index.vue'
import TextElement from './TextElement/index.vue'
export default defineComponent({
name: 'editable-element',
@ -70,6 +70,10 @@ export default defineComponent({
type: Boolean,
required: true,
},
animationIndex: {
type: Number,
default: -1,
},
selectElement: {
type: Function as PropType<(e: MouseEvent, element: PPTElement, canMove?: boolean) => void>,
required: true,

View File

@ -20,8 +20,13 @@
</template>
<script lang="ts">
import SvgWrapper from '@/components/SvgWrapper.vue'
export default {
name: 'element-border',
components: {
SvgWrapper,
},
props: {
width: {
type: Number,

View File

@ -51,6 +51,8 @@
import { computed, defineComponent, onMounted, onUnmounted, PropType, reactive, ref } from 'vue'
import { KEYCODE } from '@/configs/keyCode'
import SvgWrapper from '@/components/SvgWrapper.vue'
type ClipDataRange = [[number, number], [number, number]]
interface ClipData {
@ -72,6 +74,9 @@ type ScaleClipRangeType = 't-l' | 't-r' | 'b-l' | 'b-r'
export default defineComponent({
name: 'image-clip-handler',
components: {
SvgWrapper,
},
props: {
imgUrl: {
type: String,

View File

@ -23,8 +23,13 @@
</template>
<script lang="ts">
import SvgWrapper from '@/components/SvgWrapper.vue'
export default {
name: 'image-ellipse-border',
components: {
SvgWrapper,
},
props: {
width: {
type: Number,

View File

@ -20,8 +20,13 @@
</template>
<script lang="ts">
import SvgWrapper from '@/components/SvgWrapper.vue'
export default {
name: 'image-polygon-border',
components: {
SvgWrapper,
},
props: {
width: {
type: Number,

View File

@ -23,8 +23,13 @@
</template>
<script lang="ts">
import SvgWrapper from '@/components/SvgWrapper.vue'
export default {
name: 'image-rect-border',
components: {
SvgWrapper,
},
props: {
width: {
type: Number,

View File

@ -33,7 +33,7 @@
transform: flip,
}"
>
<RectBorder
<ImageRectBorder
v-if="clipShape.type === 'rect'"
:width="elementInfo.width"
:height="elementInfo.height"
@ -42,7 +42,7 @@
:borderWidth="elementInfo.borderWidth"
:borderStyle="elementInfo.borderStyle"
/>
<EllipseBorder
<ImageEllipseBorder
v-else-if="clipShape.type === 'ellipse'"
:width="elementInfo.width"
:height="elementInfo.height"
@ -50,7 +50,7 @@
:borderWidth="elementInfo.borderWidth"
:borderStyle="elementInfo.borderStyle"
/>
<PolygonBorder
<ImagePolygonBorder
v-else-if="clipShape.type === 'polygon'"
:width="elementInfo.width"
:height="elementInfo.height"
@ -102,7 +102,7 @@
:style="point.style"
@mousedown.stop="scaleElement($event, elementInfo, point.direction)"
/>
<RotateHandle
<RotateHandler
class="el-rotate-handle"
:style="{left: scaleWidth / 2 + 'px'}"
@mousedown.stop="rotateElement(elementInfo)"
@ -186,6 +186,9 @@ export default defineComponent({
type: Function as PropType<(e: MouseEvent, element: PPTImageElement, command: ElementScaleHandler) => void>,
required: true,
},
contextmenus: {
type: Function,
},
},
setup(props) {
const clipingImageElId = ref('')
@ -291,6 +294,7 @@ export default defineComponent({
}
return {
scaleWidth,
isCliping,
imgPosition,
clipShape,

View File

@ -0,0 +1,266 @@
<template>
<div
class="editable-element text"
:class="{ 'lock': elementInfo.isLock }"
:style="{
top: elementInfo.top + 'px',
left: elementInfo.left + 'px',
width: elementInfo.width + 'px',
transform: `rotate(${elementInfo.rotate}deg)`,
}"
@mousedown="handleSelectElement($event, false)"
>
<div class="element-content"
:style="{
backgroundColor: elementInfo.fill,
opacity: elementInfo.opacity,
textShadow: elementInfo.shadow,
lineHeight: elementInfo.lineHeight,
letterSpacing: (elementInfo.letterSpacing || 0) + 'px',
}"
v-contextmenu="contextmenus"
>
<ElementBorder
:width="elementInfo.width"
:height="elementInfo.height"
:borderColor="elementInfo.borderColor"
:borderWidth="elementInfo.borderWidth"
:borderStyle="elementInfo.borderStyle"
/>
<div class="text-content"
v-html="elementInfo.content"
:contenteditable="isActive && !elementInfo.isLock"
></div>
</div>
<div
class="operate"
:class="{
'show': isActive,
'multi-select': isMultiSelect && isActive,
'selected': isHandleEl
}"
:style="{ transform: `scale(${1 / canvasScale})` }"
v-contextmenu="contextmenus"
>
<BorderLine
class="el-border-line"
v-for="line in borderLines"
:key="line.type"
:type="line.type"
:style="line.style"
:isWide="true"
@mousedown="handleSelectElement($event)"
/>
<template v-if="!elementInfo.isLock && (isActiveGroupElement || !isMultiSelect)">
<ResizablePoint class="el-resizable-point"
v-for="point in resizablePoints"
:key="point.type"
:type="point.type"
:style="point.style"
@mousedown.stop="scaleElement($event, elementInfo, point.direction)"
/>
<RotateHandler
class="el-rotate-handle"
:style="{ left: scaleWidth / 2 + 'px' }"
@mousedown.stop="rotateElement(elementInfo)"
/>
</template>
<AnimationIndex v-if="animationIndex !== -1" :animationIndex="animationIndex" />
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, ref, PropType } from 'vue'
import { PPTTextElement } from '@/types/slides'
import { ElementScaleHandler, OperateResizablePointTypes, OperateBorderLineTypes } from '@/types/edit'
import { CLIPPATHS, ClipPathTypes } from '@/configs/imageClip'
import { OPERATE_KEYS } from '@/configs/element'
import ElementBorder from '@/views/_common/_element/ElementBorder.vue'
import RotateHandler from '@/views/_common/_operate/RotateHandler.vue'
import ResizablePoint from '@/views/_common/_operate/ResizablePoint.vue'
import BorderLine from '@/views/_common/_operate/BorderLine.vue'
import AnimationIndex from '@/views/_common/_operate/AnimationIndex.vue'
export default defineComponent({
name: 'slide-element-text',
components: {
ElementBorder,
RotateHandler,
ResizablePoint,
BorderLine,
AnimationIndex,
},
props: {
elementInfo: {
type: Object as PropType<PPTTextElement>,
required: true,
},
canvasScale: {
type: Number,
required: true,
},
isActive: {
type: Boolean,
required: true,
},
isHandleEl: {
type: Boolean,
required: true,
},
isActiveGroupElement: {
type: Boolean,
required: true,
},
isMultiSelect: {
type: Boolean,
required: true,
},
animationIndex: {
type: Number,
required: true,
},
selectElement: {
type: Function as PropType<(e: MouseEvent, element: PPTTextElement, canMove?: boolean) => void>,
required: true,
},
rotateElement: {
type: Function as PropType<(element: PPTTextElement) => void>,
required: true,
},
scaleElement: {
type: Function as PropType<(e: MouseEvent, element: PPTTextElement, command: ElementScaleHandler) => void>,
required: true,
},
contextmenus: {
type: Function,
},
},
setup(props) {
const scaleWidth = computed(() => props.elementInfo ? props.elementInfo.width * props.canvasScale : 0)
const scaleHeight = computed(() => props.elementInfo ? props.elementInfo.height * props.canvasScale : 0)
const resizablePoints = computed(() => {
return [
{ type: OperateResizablePointTypes.TL, direction: OPERATE_KEYS.LEFT_TOP, style: {} },
{ type: OperateResizablePointTypes.TC, direction: OPERATE_KEYS.TOP, style: {left: scaleWidth.value / 2 + 'px'} },
{ type: OperateResizablePointTypes.TR, direction: OPERATE_KEYS.RIGHT_TOP, style: {left: scaleWidth.value + 'px'} },
{ type: OperateResizablePointTypes.ML, direction: OPERATE_KEYS.LEFT, style: {top: scaleHeight.value / 2 + 'px'} },
{ type: OperateResizablePointTypes.MR, direction: OPERATE_KEYS.RIGHT, style: {left: scaleWidth.value + 'px', top: scaleHeight.value / 2 + 'px'} },
{ type: OperateResizablePointTypes.BL, direction: OPERATE_KEYS.LEFT_BOTTOM, style: {top: scaleHeight.value + 'px'} },
{ type: OperateResizablePointTypes.BC, direction: OPERATE_KEYS.BOTTOM, style: {left: scaleWidth.value / 2 + 'px', top: scaleHeight.value + 'px'} },
{ type: OperateResizablePointTypes.BR, direction: OPERATE_KEYS.RIGHT_BOTTOM, style: {left: scaleWidth.value + 'px', top: scaleHeight.value + 'px'} },
]
})
const borderLines = computed(() => {
return [
{ type: OperateBorderLineTypes.T, style: {width: scaleWidth.value + 'px'} },
{ type: OperateBorderLineTypes.B, style: {top: scaleHeight.value + 'px', width: scaleWidth.value + 'px'} },
{ type: OperateBorderLineTypes.L, style: {height: scaleHeight.value + 'px'} },
{ type: OperateBorderLineTypes.R, style: {left: scaleWidth.value + 'px', height: scaleHeight.value + 'px'} },
]
})
const handleSelectElement = (e: MouseEvent, canMove = true) => {
if(props.elementInfo.isLock) return
e.stopPropagation()
props.selectElement(e, props.elementInfo, canMove)
}
return {
scaleWidth,
resizablePoints,
borderLines,
handleSelectElement,
}
},
})
</script>
<style lang="scss" scoped>
.editable-element {
position: absolute;
&.lock .el-border-line {
border-color: #888;
}
&:hover .el-border-line {
display: block;
}
&.lock .element-content {
cursor: default;
}
}
.element-content {
position: relative;
padding: 10px;
.text-content {
position: relative;
cursor: text;
}
}
::v-deep .text-content {
word-break: break-word;
font-family: '微软雅黑';
outline: 0;
::selection {
background-color: rgba(27, 110, 232, 0.3);
color: inherit;
}
ul {
list-style-type: disc;
padding-inline-start: 30px;
li {
list-style-type: disc;
}
}
ol {
list-style-type: decimal;
padding-inline-start: 30px;
li {
list-style-type: decimal;
}
}
}
.operate {
position: absolute;
top: 0;
left: 0;
z-index: 100;
user-select: none;
&.active {
.el-border-line,
.el-resizable-point,
.el-rotate-handle {
display: block;
}
}
&.multi-select:not(.selected) .el-border-line {
border-color: rgba($color: $themeColor, $alpha: .5);
}
.el-border-line,
.el-resizable-point,
.el-rotate-handle {
display: none;
}
}
</style>