This commit is contained in:
pipipi-pikachu 2020-12-25 15:43:33 +08:00
parent ded04bf837
commit bfb24e5055
33 changed files with 650 additions and 556 deletions

View File

@ -63,4 +63,21 @@ export interface MultiSelectRange {
maxX: number;
minY: number;
maxY: number;
}
export type ImageClipDataRange = [[number, number], [number, number]]
export interface ImageClipData {
range: ImageClipDataRange;
path: string;
}
export interface ImageClipedEmitData {
range: ImageClipDataRange;
position: {
left: number;
top: number;
width: number;
height: number;
};
}

View File

@ -25,21 +25,13 @@ export default defineComponent({
type: Number,
required: true,
},
offsetX: {
type: Number,
required: true,
},
offsetY: {
type: Number,
required: true,
},
},
setup(props) {
const store = useStore<State>()
const canvasScale = computed(() => store.state.canvasScale)
const left = computed(() => props.axis.x * canvasScale.value + props.offsetX + 'px')
const top = computed(() => props.axis.y * canvasScale.value + props.offsetY + 'px')
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' }
@ -66,11 +58,11 @@ export default defineComponent({
border: 0 dashed $themeColor;
&.vertical {
margin-left: -0.5px;
transform: translateY(-0.5px);
border-left-width: 1px;
}
&.horizontal {
margin-top: -0.5px;
transform: translateX(-0.5px);
border-top-width: 1px;
}
}

View File

@ -50,26 +50,10 @@
<script lang="ts">
import { computed, defineComponent, onMounted, onUnmounted, PropType, reactive, ref } from 'vue'
import { KEYS } from '@/configs/hotkey'
import { ImageClipData, ImageClipDataRange, ImageClipedEmitData } from '@/types/edit'
import SvgWrapper from '@/components/SvgWrapper.vue'
type ClipDataRange = [[number, number], [number, number]]
interface ClipData {
range: ClipDataRange;
path: string;
}
export interface ClipedEmitData {
range: ClipDataRange;
position: {
left: number;
top: number;
width: number;
height: number;
};
}
type ScaleClipRangeType = 't-l' | 't-r' | 'b-l' | 'b-r'
export default defineComponent({
@ -83,7 +67,7 @@ export default defineComponent({
required: true,
},
clipData: {
type: Object as PropType<ClipData>,
type: Object as PropType<ImageClipData>,
required: true,
},
clipPath: {
@ -123,7 +107,7 @@ export default defineComponent({
left: '0',
})
const isSettingClipRange = ref(false)
const currentRange = ref<ClipDataRange | null>(null)
const currentRange = ref<ImageClipDataRange | null>(null)
const getClipDataTransformInfo = () => {
const [start, end] = props.clipData ? props.clipData.range : [[0, 0], [100, 100]]
@ -209,7 +193,7 @@ export default defineComponent({
height: (topImgWrapperPosition.height - 100) / 100 * props.height,
}
const clipedEmitData: ClipedEmitData = {
const clipedEmitData: ImageClipedEmitData = {
range: currentRange.value,
position,
}

View File

@ -0,0 +1,169 @@
<template>
<ImageClipHandler
v-if="isCliping"
:src="elementInfo.src"
:clipData="elementInfo.clip"
:canvasScale="canvasScale"
:width="elementInfo.width"
:height="elementInfo.height"
:top="elementInfo.top"
:left="elementInfo.left"
:clipPath="clipShape.style"
@clip="range => clip(range)"
/>
<div
class="image-element-operate"
v-else
:class="{
'selected': isSelected,
'multi-select': isMultiSelect && isSelected,
'active': isActive,
}"
>
<BorderLine
class="operate-border-line"
v-for="line in borderLines"
:key="line.type"
:type="line.type"
:style="line.style"
/>
<template v-if="!elementInfo.lock && (isActiveGroupElement || !isMultiSelect)">
<ResizeHandler
class="operate-resize-handler"
v-for="point in resizeHandlers"
:key="point.direction"
:type="point.direction"
:style="point.style"
@mousedown.stop="$event => scaleElement($event, elementInfo, point.direction)"
/>
<RotateHandler
class="operate-rotate-handler"
:style="{ left: scaleWidth / 2 + 'px' }"
@mousedown.stop="rotateElement(elementInfo)"
/>
</template>
<AnimationIndex v-if="animationIndex !== -1" :animationIndex="animationIndex" />
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType, ref } from 'vue'
import { useStore } from 'vuex'
import { State } from '@/store'
import { PPTImageElement } from '@/types/slides'
import { OperateResizeHandler, ImageClipedEmitData } from '@/types/edit'
import useCommonOperate from '../hooks/useCommonOperate'
import RotateHandler from './RotateHandler.vue'
import ResizeHandler from './ResizeHandler.vue'
import BorderLine from './BorderLine.vue'
import AnimationIndex from './AnimationIndex.vue'
import ImageClipHandler from './ImageClipHandler.vue'
export default defineComponent({
name: 'image-element-operate',
components: {
RotateHandler,
ResizeHandler,
BorderLine,
AnimationIndex,
ImageClipHandler,
},
props: {
elementInfo: {
type: Object as PropType<PPTImageElement>,
required: true,
},
isSelected: {
type: Boolean,
required: true,
},
isActive: {
type: Boolean,
required: true,
},
isActiveGroupElement: {
type: Boolean,
required: true,
},
isMultiSelect: {
type: Boolean,
required: true,
},
animationIndex: {
type: Number,
required: true,
},
rotateElement: {
type: Function as PropType<(element: PPTImageElement) => void>,
required: true,
},
scaleElement: {
type: Function as PropType<(e: MouseEvent, element: PPTImageElement, command: OperateResizeHandler) => void>,
required: true,
},
},
setup(props) {
const store = useStore<State>()
const canvasScale = computed(() => store.state.canvasScale)
const scaleWidth = computed(() => props.elementInfo.width * canvasScale.value)
const scaleHeight = computed(() => props.elementInfo.height * canvasScale.value)
const { resizeHandlers, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
const clipingImageElId = ref('')
const isCliping = computed(() => clipingImageElId.value === props.elementInfo.id)
const clip = (data: ImageClipedEmitData) => {
clipingImageElId.value = ''
if(!data) return
const { range, position } = data
const originClip = props.elementInfo.clip || {}
const _props = {
clip: { ...originClip, range },
left: props.elementInfo.left + position.left,
top: props.elementInfo.top + position.top,
width: props.elementInfo.width + position.width,
height: props.elementInfo.height + position.height,
}
console.log(_props)
}
return {
scaleWidth,
resizeHandlers,
borderLines,
isCliping,
clip,
}
},
})
</script>
<style lang="scss" scoped>
.image-element-operate {
&.selected {
.operate-border-line,
.operate-resize-handler,
.operate-rotate-handler {
display: block;
}
}
&.multi-select:not(.selected) .operate-border-line {
border-color: rgba($color: $themeColor, $alpha: .3);
}
.operate-border-line,
.operate-resize-handler,
.operate-rotate-handler {
display: none;
}
}
</style>

View File

@ -2,9 +2,8 @@
<div
class="multi-select-operate"
:style="{
left: minX + 'px',
top: minY + 'px',
transform: `scale(${1 / canvasScale})`,
left: minX * canvasScale + 'px',
top: minY * canvasScale + 'px',
}"
>
<BorderLine v-for="line in borderLines" :key="line.type" :type="line.type" :style="line.style" />
@ -28,10 +27,10 @@ import { State } from '@/store'
import { PPTElement, ElementTypes } from '@/types/slides'
import { getElementListRange } from '@/utils/element'
import { OperateResizeHandler, MultiSelectRange } from '@/types/edit'
import useCommonOperate from '@/views/_common/_element/hooks/useCommonOperate'
import useCommonOperate from '../hooks/useCommonOperate'
import ResizeHandler from '@/views/_common/_operate/ResizeHandler.vue'
import BorderLine from '@/views/_common/_operate/BorderLine.vue'
import ResizeHandler from './ResizeHandler.vue'
import BorderLine from './BorderLine.vue'
export default defineComponent({
name: 'multi-select-operate',
@ -102,6 +101,6 @@ export default defineComponent({
position: absolute;
top: 0;
left: 0;
z-index: 100;
z-index: 101;
}
</style>

View File

@ -0,0 +1,22 @@
<template>
<div class="rotate-handler"></div>
</template>
<script>
export default {
name: 'rotate-handler',
}
</script>
<style lang="scss" scoped>
.rotate-handler {
position: absolute;
width: 10px;
height: 10px;
top: -25px;
margin-left: -5px;
border: 1px solid $themeColor;
background-color: #fff;
border-radius: 1px;
}
</style>

View File

@ -0,0 +1,131 @@
<template>
<div
class="text-element-operate"
:class="{
'selected': isSelected,
'multi-select': isMultiSelect && isSelected,
'active': isActive,
}"
>
<BorderLine
class="operate-border-line"
v-for="line in borderLines"
:key="line.type"
:type="line.type"
:style="line.style"
/>
<template v-if="!elementInfo.lock && (isActiveGroupElement || !isMultiSelect)">
<ResizeHandler
class="operate-resize-handler"
v-for="point in textElementResizeHandlers"
:key="point.direction"
:type="point.direction"
:style="point.style"
@mousedown.stop="$event => scaleElement($event, elementInfo, point.direction)"
/>
<RotateHandler
class="operate-rotate-handler"
:style="{ left: scaleWidth / 2 + 'px' }"
@mousedown.stop="rotateElement(elementInfo)"
/>
</template>
<AnimationIndex v-if="animationIndex !== -1" :animationIndex="animationIndex" />
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue'
import { useStore } from 'vuex'
import { State } from '@/store'
import { PPTTextElement } from '@/types/slides'
import { OperateResizeHandler } from '@/types/edit'
import useCommonOperate from '../hooks/useCommonOperate'
import RotateHandler from './RotateHandler.vue'
import ResizeHandler from './ResizeHandler.vue'
import BorderLine from './BorderLine.vue'
import AnimationIndex from './AnimationIndex.vue'
export default defineComponent({
name: 'text-element-operate',
components: {
RotateHandler,
ResizeHandler,
BorderLine,
AnimationIndex,
},
props: {
elementInfo: {
type: Object as PropType<PPTTextElement>,
required: true,
},
isSelected: {
type: Boolean,
required: true,
},
isActive: {
type: Boolean,
required: true,
},
isActiveGroupElement: {
type: Boolean,
required: true,
},
isMultiSelect: {
type: Boolean,
required: true,
},
animationIndex: {
type: Number,
required: true,
},
rotateElement: {
type: Function as PropType<(element: PPTTextElement) => void>,
required: true,
},
scaleElement: {
type: Function as PropType<(e: MouseEvent, element: PPTTextElement, command: OperateResizeHandler) => void>,
required: true,
},
},
setup(props) {
const store = useStore<State>()
const canvasScale = computed(() => store.state.canvasScale)
const scaleWidth = computed(() => props.elementInfo.width * canvasScale.value)
const scaleHeight = computed(() => props.elementInfo.height * canvasScale.value)
const { textElementResizeHandlers, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
return {
scaleWidth,
textElementResizeHandlers,
borderLines,
}
},
})
</script>
<style lang="scss" scoped>
.text-element-operate {
&.selected {
.operate-border-line,
.operate-resize-handler,
.operate-rotate-handler {
display: block;
}
}
&.multi-select:not(.selected) .operate-border-line {
border-color: rgba($color: $themeColor, $alpha: .3);
}
.operate-border-line,
.operate-resize-handler,
.operate-rotate-handler {
display: none;
}
}
</style>

View File

@ -0,0 +1,97 @@
<template>
<div
class="operate"
:style="{
top: elementInfo.top * canvasScale + 'px',
left: elementInfo.left * canvasScale + 'px',
transform: `rotate(${elementInfo.rotate}deg)`,
'transform-origin': `${elementInfo.width * canvasScale / 2}px ${elementInfo.height * canvasScale / 2}px`,
}"
>
<component
:is="currentOperateComponent"
:elementInfo="elementInfo"
:isSelected="isSelected"
:isActive="isActive"
:isActiveGroupElement="isActiveGroupElement"
:isMultiSelect="isMultiSelect"
:animationIndex="animationIndex"
:rotateElement="rotateElement"
:scaleElement="scaleElement"
></component>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, computed } from 'vue'
import { PPTElement } from '@/types/slides'
import { OperateResizeHandler } from '@/types/edit'
import ImageElementOperate from './ImageElementOperate.vue'
import TextElementOperate from './TextElementOperate.vue'
import { useStore } from 'vuex'
import { State } from '@/store'
export default defineComponent({
name: 'operate',
props: {
elementInfo: {
type: Object as PropType<PPTElement>,
required: true,
},
isSelected: {
type: Boolean,
required: true,
},
isActive: {
type: Boolean,
required: true,
},
isActiveGroupElement: {
type: Boolean,
required: true,
},
isMultiSelect: {
type: Boolean,
required: true,
},
animationIndex: {
type: Number,
default: -1,
},
rotateElement: {
type: Function as PropType<(element: PPTElement) => void>,
required: true,
},
scaleElement: {
type: Function as PropType<(e: MouseEvent, element: PPTElement, command: OperateResizeHandler) => void>,
required: true,
},
},
setup(props) {
const store = useStore<State>()
const canvasScale = computed(() => store.state.canvasScale)
const currentOperateComponent = computed(() => {
const elementTypeMap = {
'image': ImageElementOperate,
'text': TextElementOperate,
}
return elementTypeMap[props.elementInfo.type] || null
})
return {
currentOperateComponent,
canvasScale,
}
},
})
</script>
<style lang="scss" scoped>
.operate {
position: absolute;
z-index: 100;
user-select: none;
}
</style>

View File

@ -13,6 +13,13 @@ 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'} },
{ direction: OperateResizeHandlers.RIGHT, style: {left: width.value + 'px', top: height.value / 2 + 'px'} },
]
})
const borderLines = computed(() => {
@ -26,6 +33,7 @@ export default (width: Ref<number>, height: Ref<number>) => {
return {
resizeHandlers,
textElementResizeHandlers,
borderLines,
}
}

View File

@ -216,21 +216,21 @@ export default (
targetTop = targetTop - (targetMinY - value)
isHorizontalAdsorbed = true
}
_alignmentLines.push({type: 'horizontal', axis: {x: min - 20, y: value}, length: max - min + 40})
_alignmentLines.push({type: 'horizontal', axis: {x: min - 50, y: value}, length: max - min + 100})
}
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})
_alignmentLines.push({type: 'horizontal', axis: {x: min - 50, y: value}, length: max - min + 100})
}
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})
_alignmentLines.push({type: 'horizontal', axis: {x: min - 50, y: value}, length: max - min + 100})
}
}
for(let i = 0; i < verticalLines.length; i++) {
@ -243,21 +243,21 @@ export default (
targetLeft = targetLeft - (targetMinX - value)
isVerticalAdsorbed = true
}
_alignmentLines.push({type: 'vertical', axis: {x: value, y: min - 20}, length: max - min + 40})
_alignmentLines.push({type: 'vertical', axis: {x: value, y: min - 50}, length: max - min + 100})
}
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})
_alignmentLines.push({type: 'vertical', axis: {x: value, y: min - 50}, length: max - min + 100})
}
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.push({type: 'vertical', axis: {x: value, y: min - 50}, length: max - min + 100})
}
}
alignmentLines.value = _alignmentLines

View File

@ -192,7 +192,7 @@ export default (
correctionVal.offsetY = currentY - value
isHorizontalAdsorbed = true
}
_alignmentLines.push({type: 'horizontal', axis: {x: min - 20, y: value}, length: max - min + 40})
_alignmentLines.push({ type: 'horizontal', axis: {x: min - 50, y: value}, length: max - min + 100 })
}
}
}
@ -207,7 +207,7 @@ export default (
correctionVal.offsetX = currentX - value
isVerticalAdsorbed = true
}
_alignmentLines.push({ type: 'vertical', axis: {x: value, y: min - 20}, length: max - min + 40 })
_alignmentLines.push({ type: 'vertical', axis: {x: value, y: min - 50}, length: max - min + 100 })
}
}
}

View File

@ -7,15 +7,37 @@
v-contextmenu="contextmenus"
v-click-outside="removeEditorAreaFocus"
>
<AlignmentLine
v-for="(line, index) in alignmentLines"
:key="index"
:type="line.type"
:axis="line.axis"
:length="line.length"
:offsetX="viewportStyles.left"
:offsetY="viewportStyles.top"
/>
<div
class="operates"
:style="{
left: viewportStyles.left + 'px',
top: viewportStyles.top + 'px',
}"
>
<AlignmentLine
v-for="(line, index) in alignmentLines"
:key="index"
:type="line.type"
:axis="line.axis"
:length="line.length"
/>
<MultiSelectOperate
v-if="activeElementIdList.length > 1"
:elementList="elementList"
:scaleMultiElement="scaleMultiElement"
/>
<Operate
v-for="element in elementList"
:key="element.id"
:elementInfo="element"
:isSelected="activeElementIdList.includes(element.id)"
:isActiveGroupElement="activeGroupElementId === element.id"
:isMultiSelect="activeElementIdList.length > 1"
:rotateElement="rotateElement"
:scaleElement="scaleElement"
/>
</div>
<div
class="viewport"
ref="viewportRef"
@ -35,27 +57,14 @@
:height="mouseSelectionState.height"
:quadrant="mouseSelectionState.quadrant"
/>
<MultiSelectOperate
v-if="activeElementIdList.length > 1"
:elementList="elementList"
:scaleMultiElement="scaleMultiElement"
/>
<SlideBackground />
<EditableElement
v-for="(element, index) in elementList"
:key="element.id"
:elementInfo="element"
:elementIndex="index + 1"
:isSelected="activeElementIdList.includes(element.id)"
:isActive="element.id === handleElementId"
:isActiveGroupElement="activeGroupElementId === element.id"
:isMultiSelect="activeElementIdList.length > 1"
:selectElement="selectElement"
:rotateElement="rotateElement"
:scaleElement="scaleElement"
/>
</div>
</div>
@ -83,11 +92,12 @@ import useCopyAndPasteElement from '@/hooks/useCopyAndPasteElement'
import useSelectAllElement from '@/hooks/useSelectAllElement'
import useScaleCanvas from '@/hooks/useScaleCanvas'
import EditableElement from '@/views/_common/_element/EditableElement.vue'
import EditableElement from '@/views/_element/EditableElement.vue'
import MouseSelection from './MouseSelection.vue'
import SlideBackground from './SlideBackground.vue'
import MultiSelectOperate from './MultiSelectOperate.vue'
import AlignmentLine from './AlignmentLine.vue'
import MultiSelectOperate from './Operate/MultiSelectOperate.vue'
import Operate from './Operate/index.vue'
export default defineComponent({
name: 'editor-canvas',
@ -95,8 +105,9 @@ export default defineComponent({
EditableElement,
MouseSelection,
SlideBackground,
MultiSelectOperate,
AlignmentLine,
MultiSelectOperate,
Operate,
},
setup() {
const store = useStore<State>()
@ -218,4 +229,7 @@ export default defineComponent({
background-color: #fff;
box-shadow: 0 0 20px 0 rgba(0, 0, 0, .1);
}
.operates {
position: absolute;
}
</style>

View File

@ -42,7 +42,7 @@ import { fillDigit } from '@/utils/common'
import { ContextmenuItem } from '@/components/Contextmenu/types'
import useSlideHandler from '@/hooks/useSlideHandler'
import ThumbnailSlide from '@/views/_common/ThumbnailSlide.vue'
import ThumbnailSlide from '@/views/ThumbnailSlide.vue'
export default defineComponent({
name: 'thumbnails',

View File

@ -23,7 +23,7 @@ import { Slide } from '@/types/slides'
import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
import useSlideBackgroundStyle from '@/hooks/useSlideBackgroundStyle'
import BaseElement from '@/views/_common/_element/BaseElement.vue'
import BaseElement from '@/views/_element/BaseElement.vue'
export default defineComponent({
name: 'screen-slide',

View File

@ -172,10 +172,10 @@ export default defineComponent({
z-index: 2;
}
&.prev {
transform: translateX(-100%);
transform: translateY(-100%);
}
&.next {
transform: translateX(100%);
transform: translateY(100%);
}
}
.slide-content {

View File

@ -30,7 +30,7 @@ import { Slide } from '@/types/slides'
import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
import useSlideBackgroundStyle from '@/hooks/useSlideBackgroundStyle'
import BaseElement from '@/views/_common/_element/BaseElement.vue'
import BaseElement from '@/views/_element/BaseElement.vue'
export default defineComponent({
name: 'thumbnail-slide',

View File

@ -1,223 +0,0 @@
<template>
<div
class="editable-element-text"
:class="{ 'lock': elementInfo.lock }"
:style="{
top: elementInfo.top + 'px',
left: elementInfo.left + 'px',
width: elementInfo.width + 'px',
transform: `rotate(${elementInfo.rotate}deg)`,
}"
@mousedown="$event => handleSelectElement($event)"
>
<div class="element-content"
:style="{
backgroundColor: elementInfo.fill,
opacity: elementInfo.opacity,
textShadow: shadowStyle,
}"
v-contextmenu="contextmenus"
>
<ElementOutline
:width="elementInfo.width"
:height="elementInfo.height"
:outline="elementInfo.outline"
/>
<div class="text"
v-html="elementInfo.content"
:contenteditable="isActive && !elementInfo.lock"
@mousedown="$event => handleSelectElement($event, false)"
></div>
</div>
<div
class="operate"
:class="{
'selected': isSelected,
'multi-select': isMultiSelect && isSelected,
'active': isActive,
}"
:style="{ transform: `scale(${1 / canvasScale})` }"
>
<BorderLine
class="operate-border-line"
v-for="line in borderLines"
:key="line.type"
:type="line.type"
:style="line.style"
/>
<template v-if="!elementInfo.lock && (isActiveGroupElement || !isMultiSelect)">
<ResizeHandler
class="operate-resize-handler"
v-for="point in resizeHandlers"
:key="point.direction"
:type="point.direction"
:style="point.style"
@mousedown.stop="$event => scaleElement($event, elementInfo, point.direction)"
/>
<RotateHandler
class="operate-rotate-handler"
: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, PropType } from 'vue'
import { PPTTextElement } from '@/types/slides'
import { OperateResizeHandler } from '@/types/edit'
import useCommonOperate from '@/views/_common/_element/hooks/useCommonOperate'
import ElementOutline from '@/views/_common/_element/ElementOutline.vue'
import RotateHandler from '@/views/_common/_operate/RotateHandler.vue'
import ResizeHandler from '@/views/_common/_operate/ResizeHandler.vue'
import BorderLine from '@/views/_common/_operate/BorderLine.vue'
import AnimationIndex from '@/views/_common/_operate/AnimationIndex.vue'
import useElementShadow from '@/views/_common/_element/hooks/useElementShadow'
export default defineComponent({
name: 'editable-element-text',
components: {
ElementOutline,
RotateHandler,
ResizeHandler,
BorderLine,
AnimationIndex,
},
props: {
elementInfo: {
type: Object as PropType<PPTTextElement>,
required: true,
},
canvasScale: {
type: Number,
required: true,
},
isSelected: {
type: Boolean,
required: true,
},
isActive: {
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: OperateResizeHandler) => void>,
required: true,
},
contextmenus: {
type: Function,
},
},
setup(props) {
const scaleWidth = computed(() => props.elementInfo.width * props.canvasScale)
const scaleHeight = computed(() => props.elementInfo.height * props.canvasScale)
const { resizeHandlers, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
const handleSelectElement = (e: MouseEvent, canMove = true) => {
if(props.elementInfo.lock) return
e.stopPropagation()
props.selectElement(e, props.elementInfo, canMove)
}
const shadow = computed(() => props.elementInfo.shadow)
const { shadowStyle } = useElementShadow(shadow)
return {
scaleWidth,
resizeHandlers,
borderLines,
handleSelectElement,
shadowStyle,
}
},
})
</script>
<style lang="scss" scoped>
.editable-element-text {
position: absolute;
cursor: move;
&.lock .element-content {
cursor: default;
}
}
.element-content {
position: relative;
padding: 10px;
line-height: 1.5;
.text {
position: relative;
cursor: text;
}
}
::v-deep(.text) {
word-break: break-word;
font-family: '微软雅黑';
outline: 0;
::selection {
background-color: rgba(27, 110, 232, 0.3);
color: inherit;
}
}
.operate {
position: absolute;
top: 0;
left: 0;
z-index: 100;
user-select: none;
&.selected {
.operate-border-line,
.operate-resize-handler,
.operate-rotate-handler {
display: block;
}
}
&.multi-select:not(.selected) .operate-border-line {
border-color: rgba($color: $themeColor, $alpha: .3);
}
.operate-border-line,
.operate-resize-handler,
.operate-rotate-handler {
display: none;
}
}
</style>

View File

@ -1,34 +0,0 @@
<template>
<div class="rotate-handler">
<div class="rotate-icon"><IconFont type="icon-rotate" /></div>
</div>
</template>
<script>
export default {
name: 'rotate-handler',
}
</script>
<style lang="scss" scoped>
.rotate-handler {
position: absolute;
top: -24px;
margin: -10px 0 0 -10px;
width: 20px;
height: 20px;
border-radius: 50%;
background-color: #fff;
box-shadow: 1px 1px 2px #888;
.rotate-icon {
width: 100%;
height: 100%;
font-size: 12px;
color: #666;
display: flex;
justify-content: center;
align-items: center;
}
}
</style>

View File

@ -7,16 +7,8 @@
>
<component
:is="currentElementComponent"
:canvasScale="canvasScale"
:elementInfo="elementInfo"
:isSelected="isSelected"
:isActive="isActive"
:isActiveGroupElement="isActiveGroupElement"
:isMultiSelect="isMultiSelect"
:animationIndex="animationIndex"
:selectElement="selectElement"
:rotateElement="rotateElement"
:scaleElement="scaleElement"
:contextmenus="contextmenus"
></component>
</div>
@ -24,9 +16,7 @@
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue'
import { useStore } from 'vuex'
import { State } from '@/store'
import { PPTElement, PPTTextElement, PPTImageElement, PPTShapeElement, PPTLineElement } from '@/types/slides'
import { PPTElement } from '@/types/slides'
import { ContextmenuItem } from '@/components/Contextmenu/types'
import useLockElement from '@/hooks/useLockElement'
@ -36,7 +26,7 @@ import useOrderElement from '@/hooks/useOrderElement'
import useAlignElementToCanvas from '@/hooks/useAlignElementToCanvas'
import useCopyAndPasteElement from '@/hooks/useCopyAndPasteElement'
import { ElementOrderCommands, ElementAlignCommands, OperateResizeHandler } from '@/types/edit'
import { ElementOrderCommands, ElementAlignCommands } from '@/types/edit'
import ImageElement from './ImageElement/index.vue'
import TextElement from './TextElement/index.vue'
@ -52,43 +42,16 @@ export default defineComponent({
type: Number,
required: true,
},
isSelected: {
type: Boolean,
required: true,
},
isActive: {
type: Boolean,
required: true,
},
isActiveGroupElement: {
type: Boolean,
required: true,
},
isMultiSelect: {
type: Boolean,
required: true,
},
animationIndex: {
type: Number,
default: -1,
},
selectElement: {
type: Function as PropType<(e: MouseEvent, element: PPTElement, canMove?: boolean) => void>,
required: true,
},
rotateElement: {
type: Function as PropType<(element: PPTTextElement | PPTImageElement | PPTShapeElement) => void>,
required: true,
},
scaleElement: {
type: Function as PropType<(e: MouseEvent, element: Exclude<PPTElement, PPTLineElement>, command: OperateResizeHandler) => void>,
required: true,
},
},
setup(props) {
const store = useStore<State>()
const canvasScale = computed(() => store.state.canvasScale)
const currentElementComponent = computed(() => {
const elementTypeMap = {
'image': ImageElement,
@ -172,7 +135,6 @@ export default defineComponent({
}
return {
canvasScale,
currentElementComponent,
contextmenus,
}

View File

@ -24,7 +24,7 @@
import { PropType, defineComponent, toRef } from 'vue'
import { PPTElementOutline } from '@/types/slides'
import SvgWrapper from '@/components/SvgWrapper.vue'
import useElementOutline from '@/views/_common/_element/hooks/useElementOutline'
import useElementOutline from '@/views/_element/hooks/useElementOutline'
export default defineComponent({
name: 'element-outline',

View File

@ -65,7 +65,7 @@ import ImageRectOutline from './ImageRectOutline.vue'
import ImageEllipseOutline from './ImageEllipseOutline.vue'
import ImagePolygonOutline from './ImagePolygonOutline.vue'
import useElementShadow from '@/views/_common/_element/hooks/useElementShadow'
import useElementShadow from '@/views/_element/hooks/useElementShadow'
export default defineComponent({
name: 'base-element-image',

View File

@ -27,7 +27,7 @@
import { PropType, defineComponent, toRef } from 'vue'
import { PPTElementOutline } from '@/types/slides'
import SvgWrapper from '@/components/SvgWrapper.vue'
import useElementOutline from '@/views/_common/_element/hooks/useElementOutline'
import useElementOutline from '@/views/_element/hooks/useElementOutline'
export default defineComponent({
name: 'image-ellipse-outline',

View File

@ -24,7 +24,7 @@
import { PropType, defineComponent, toRef } from 'vue'
import { PPTElementOutline } from '@/types/slides'
import SvgWrapper from '@/components/SvgWrapper.vue'
import useElementOutline from '@/views/_common/_element/hooks/useElementOutline'
import useElementOutline from '@/views/_element/hooks/useElementOutline'
export default defineComponent({
name: 'image-polygon-outline',

View File

@ -27,7 +27,7 @@
import { PropType, defineComponent, toRef } from 'vue'
import { PPTElementOutline } from '@/types/slides'
import SvgWrapper from '@/components/SvgWrapper.vue'
import useElementOutline from '@/views/_common/_element/hooks/useElementOutline'
import useElementOutline from '@/views/_element/hooks/useElementOutline'
export default defineComponent({
name: 'image-rect-outline',

View File

@ -11,22 +11,8 @@
}"
@mousedown="$event => handleSelectElement($event)"
>
<ImageClip
v-if="isCliping"
:src="elementInfo.src"
:clipData="elementInfo.clip"
:canvasScale="canvasScale"
:width="elementInfo.width"
:height="elementInfo.height"
:top="elementInfo.top"
:left="elementInfo.left"
:clipPath="clipShape.style"
@clip="range => clip(range)"
/>
<div
class="element-content"
v-if="!isCliping"
v-contextmenu="contextmenus"
:style="{
filter: shadowStyle ? `drop-shadow(${shadowStyle})` : '',
@ -69,74 +55,25 @@
/>
</div>
</div>
<div
class="operate"
:class="{
'selected': isSelected,
'multi-select': isMultiSelect && isSelected,
'active': isActive,
}"
:style="{ transform: `scale(${1 / canvasScale})` }"
v-if="!isCliping"
>
<BorderLine
class="operate-border-line"
v-for="line in borderLines"
:key="line.type"
:type="line.type"
:style="line.style"
/>
<template v-if="!elementInfo.lock && (isActiveGroupElement || !isMultiSelect)">
<ResizeHandler
class="operate-resize-handler"
v-for="point in resizeHandlers"
:key="point.direction"
:type="point.direction"
:style="point.style"
@mousedown.stop="$event => scaleElement($event, elementInfo, point.direction)"
/>
<RotateHandler
class="operate-rotate-handler"
: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 { computed, defineComponent, PropType } from 'vue'
import { PPTImageElement } from '@/types/slides'
import { OperateResizeHandler } from '@/types/edit'
import useCommonOperate from '@/views/_common/_element/hooks/useCommonOperate'
import { ContextmenuItem } from '@/components/Contextmenu/types'
import { CLIPPATHS, ClipPathTypes } from '@/configs/imageClip'
import useElementShadow from '@/views/_element/hooks/useElementShadow'
import RotateHandler from '@/views/_common/_operate/RotateHandler.vue'
import ResizeHandler from '@/views/_common/_operate/ResizeHandler.vue'
import BorderLine from '@/views/_common/_operate/BorderLine.vue'
import AnimationIndex from '@/views/_common/_operate/AnimationIndex.vue'
import ImageClip, { ClipedEmitData } from './ImageClipHandler.vue'
import ImageRectOutline from './ImageRectOutline.vue'
import ImageEllipseOutline from './ImageEllipseOutline.vue'
import ImagePolygonOutline from './ImagePolygonOutline.vue'
import useElementShadow from '@/views/_common/_element/hooks/useElementShadow'
export default defineComponent({
name: 'editable-element-image',
components: {
RotateHandler,
ResizeHandler,
BorderLine,
AnimationIndex,
ImageClip,
ImageRectOutline,
ImageEllipseOutline,
ImagePolygonOutline,
@ -146,55 +83,29 @@ export default defineComponent({
type: Object as PropType<PPTImageElement>,
required: true,
},
canvasScale: {
type: Number,
required: true,
},
isSelected: {
type: Boolean,
required: true,
},
isActive: {
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: PPTImageElement, canMove?: boolean) => void>,
required: true,
},
rotateElement: {
type: Function as PropType<(element: PPTImageElement) => void>,
required: true,
},
scaleElement: {
type: Function as PropType<(e: MouseEvent, element: PPTImageElement, command: OperateResizeHandler) => void>,
required: true,
},
contextmenus: {
type: Function,
type: Function as PropType<() => ContextmenuItem[]>,
},
},
setup(props) {
const clipingImageElId = ref('')
const shadow = computed(() => props.elementInfo.shadow)
const { shadowStyle } = useElementShadow(shadow)
const scaleWidth = computed(() => props.elementInfo.width * props.canvasScale)
const scaleHeight = computed(() => props.elementInfo.height * props.canvasScale)
const handleSelectElement = (e: MouseEvent) => {
if(props.elementInfo.lock) return
e.stopPropagation()
props.selectElement(e, props.elementInfo)
}
const clipShape = computed(() => {
if(!props.elementInfo || !props.elementInfo.clip) return CLIPPATHS.rect
const shape = props.elementInfo.clip.shape || ClipPathTypes.RECT
const { resizeHandlers, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
const isCliping = computed(() => clipingImageElId.value === props.elementInfo.id)
return CLIPPATHS[shape]
})
const imgPosition = computed(() => {
if(!props.elementInfo || !props.elementInfo.clip) {
@ -221,13 +132,6 @@ export default defineComponent({
}
})
const clipShape = computed(() => {
if(!props.elementInfo || !props.elementInfo.clip) return CLIPPATHS.rect
const shape = props.elementInfo.clip.shape || ClipPathTypes.RECT
return CLIPPATHS[shape]
})
const filter = computed(() => {
if(!props.elementInfo.filters) return ''
let filter = ''
@ -246,45 +150,13 @@ export default defineComponent({
return ''
})
const shadow = computed(() => props.elementInfo.shadow)
const { shadowStyle } = useElementShadow(shadow)
const handleSelectElement = (e: MouseEvent) => {
if(isCliping.value || props.elementInfo.lock) return
e.stopPropagation()
props.selectElement(e, props.elementInfo)
}
const clip = (data: ClipedEmitData) => {
clipingImageElId.value = ''
if(!data) return
const { range, position } = data
const originClip = props.elementInfo.clip || {}
const _props = {
clip: { ...originClip, range },
left: props.elementInfo.left + position.left,
top: props.elementInfo.top + position.top,
width: props.elementInfo.width + position.width,
height: props.elementInfo.height + position.height,
}
console.log(_props)
}
return {
scaleWidth,
isCliping,
imgPosition,
clipShape,
resizeHandlers,
borderLines,
filter,
flip,
shadowStyle,
handleSelectElement,
clip,
clipShape,
imgPosition,
filter,
flip,
}
},
})
@ -311,35 +183,8 @@ export default defineComponent({
overflow: hidden;
position: relative;
}
img {
position: absolute;
}
}
.operate {
position: absolute;
top: 0;
left: 0;
z-index: 100;
user-select: none;
&.selected {
.operate-border-line,
.operate-resize-handler,
.operate-rotate-handler {
display: block;
}
}
&.multi-select:not(.selected) .operate-border-line {
border-color: rgba($color: $themeColor, $alpha: .3);
}
.operate-border-line,
.operate-resize-handler,
.operate-rotate-handler {
display: none;
}
}
</style>

View File

@ -28,9 +28,9 @@
<script lang="ts">
import { defineComponent, PropType, computed } from 'vue'
import { PPTTextElement } from '@/types/slides'
import ElementOutline from '@/views/_common/_element/ElementOutline.vue'
import ElementOutline from '@/views/_element/ElementOutline.vue'
import useElementShadow from '@/views/_common/_element/hooks/useElementShadow'
import useElementShadow from '@/views/_element/hooks/useElementShadow'
export default defineComponent({
name: 'base-element-text',

View File

@ -0,0 +1,111 @@
<template>
<div
class="editable-element-text"
:class="{ 'lock': elementInfo.lock }"
:style="{
top: elementInfo.top + 'px',
left: elementInfo.left + 'px',
width: elementInfo.width + 'px',
transform: `rotate(${elementInfo.rotate}deg)`,
}"
@mousedown="$event => handleSelectElement($event)"
>
<div class="element-content"
:style="{
backgroundColor: elementInfo.fill,
opacity: elementInfo.opacity,
textShadow: shadowStyle,
}"
v-contextmenu="contextmenus"
>
<ElementOutline
:width="elementInfo.width"
:height="elementInfo.height"
:outline="elementInfo.outline"
/>
<div class="text"
v-html="elementInfo.content"
:contenteditable="!elementInfo.lock"
@mousedown="$event => handleSelectElement($event, false)"
></div>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue'
import { PPTTextElement } from '@/types/slides'
import { ContextmenuItem } from '@/components/Contextmenu/types'
import useElementShadow from '@/views/_element/hooks/useElementShadow'
import ElementOutline from '@/views/_element/ElementOutline.vue'
export default defineComponent({
name: 'editable-element-text',
components: {
ElementOutline,
},
props: {
elementInfo: {
type: Object as PropType<PPTTextElement>,
required: true,
},
selectElement: {
type: Function as PropType<(e: MouseEvent, element: PPTTextElement, canMove?: boolean) => void>,
required: true,
},
contextmenus: {
type: Function as PropType<() => ContextmenuItem[]>,
},
},
setup(props) {
const handleSelectElement = (e: MouseEvent, canMove = true) => {
if(props.elementInfo.lock) return
e.stopPropagation()
props.selectElement(e, props.elementInfo, canMove)
}
const shadow = computed(() => props.elementInfo.shadow)
const { shadowStyle } = useElementShadow(shadow)
return {
handleSelectElement,
shadowStyle,
}
},
})
</script>
<style lang="scss" scoped>
.editable-element-text {
position: absolute;
cursor: move;
&.lock .element-content {
cursor: default;
}
}
.element-content {
position: relative;
padding: 10px;
line-height: 1.5;
.text {
position: relative;
cursor: text;
}
}
::v-deep(.text) {
word-break: break-word;
font-family: '微软雅黑';
outline: 0;
::selection {
background-color: rgba(27, 110, 232, 0.3);
color: inherit;
}
}
</style>