pipipi-pikachu преди 5 години
родител
ревизия
a7176f8be6

+ 13 - 7
src/configs/imageClip.ts

@@ -1,24 +1,30 @@
+export enum ClipPathTypes {
+  RECT = 'rect',
+  ELLIPSE = 'ellipse',
+  POLYGON = 'polygon',
+}
+
 export const CLIPPATHS = {
   rect: {
     name: '矩形',
-    type: 'rect',
+    type: ClipPathTypes.RECT,
     radius: '0',
     style: '',
   },
   roundRect: {
     name: '圆角矩形',
-    type: 'rect',
+    type: ClipPathTypes.RECT,
     radius: '10%',
     style: 'inset(0 0 0 0 round 10% 10% 10% 10%)',
   },
   ellipse: {
     name: '圆形',
-    type: 'ellipse',
+    type: ClipPathTypes.ELLIPSE,
     style: 'ellipse(50% 50% at 50% 50%)',
   },
   triangle: {
     name: '三角形',
-    type: 'polygon',
+    type: ClipPathTypes.POLYGON,
     style: 'polygon(50% 0%, 0% 100%, 100% 100%)',
     createPath: (width: number, height: number) => {
       return `M ${width / 2} 0 L 0 ${height} L ${width} ${height} Z`
@@ -26,7 +32,7 @@ export const CLIPPATHS = {
   },
   pentagon: {
     name: '五边形',
-    type: 'polygon',
+    type: ClipPathTypes.POLYGON,
     style: 'polygon(50% 0%, 100% 38%, 82% 100%, 18% 100%, 0% 38%)',
     createPath: (width: number, height: number) => {
       return `M ${width / 2} 0 L ${width} ${0.38 * height} L ${0.82 * width} ${height} L ${0.18 * width} ${height} L 0 ${0.38 * height} Z`
@@ -34,7 +40,7 @@ export const CLIPPATHS = {
   },
   rhombus: {
     name: '菱形',
-    type: 'polygon',
+    type: ClipPathTypes.POLYGON,
     style: 'polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)',
     createPath: (width: number, height: number) => {
       return `M ${width / 2} 0 L ${width} ${height / 2} L ${width / 2} ${height} L 0 ${height / 2} Z`
@@ -42,7 +48,7 @@ export const CLIPPATHS = {
   },
   star: {
     name: '五角星',
-    type: 'polygon',
+    type: ClipPathTypes.POLYGON,
     style: 'polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%)',
     createPath: (width: number, height: number) => {
       return `M ${width / 2} 0 L ${0.61 * width} ${0.35 * height} L ${0.98 * width} ${0.35 * height} L ${0.68 * width} ${0.57 * height} L ${0.79 * width} ${0.91 * height} L ${0.50 * width} ${0.70 * height} L ${0.21 * width} ${0.91 * height} L ${0.32 * width} ${0.57 * height} L ${0.02 * width} ${0.35 * height} L ${0.39 * width} ${0.35 * height} Z`

+ 23 - 0
src/types/edit.ts

@@ -25,4 +25,27 @@ export type ElementLockCommand = 'lock' | 'unlock'
 export enum ElementLockCommands {
   LOCK = 'lock',
   UNLOCK = 'unlock',
+}
+
+export type OperateBorderLineType = 't' | 'b' | 'l' | 'r'
+
+export enum OperateBorderLineTypes {
+  T = 't',
+  B = 'b',
+  L = 'l',
+  R = 'r',
+}
+
+export type OperateResizablePointType = 't-l' | 't-c' | 't-r' | 'm-l' | 'm-r' | 'b-l' | 'b-c' | 'b-r' | 'any'
+
+export enum OperateResizablePointTypes {
+  TL = 't-l',
+  TC = 't-c',
+  TR = 't-r',
+  ML = 'm-l',
+  MR = 'm-r',
+  BL = 'b-l',
+  BC = 'b-c',
+  BR = 'b-r',
+  ANY = 'any',
 }

+ 2 - 2
src/types/slides.ts

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

+ 8 - 1
src/views/_common/_element/EditableElement.vue

@@ -1,6 +1,7 @@
 <template>
   <div 
     class="editable-element"
+    ref="elementRef"
     :id="'editable-element-' + elementInfo.elId"
     :style="{ zIndex: elementIndex }"
   >
@@ -10,7 +11,9 @@
       :canvasScale="canvasScale"
       :isActive="isActive"
       :isHandleEl="isHandleEl"
+      :isActiveGroupElement="isActiveGroupElement"
       :isMultiSelect="isMultiSelect"
+      :animationIndex="-1"
       :selectElement="selectElement"
       :rotateElement="rotateElement"
       :scaleElement="scaleElement"
@@ -59,12 +62,16 @@ export default defineComponent({
       type: Boolean,
       required: true,
     },
+    isActiveGroupElement: {
+      type: Boolean,
+      required: true,
+    },
     isMultiSelect: {
       type: Boolean,
       required: true,
     },
     selectElement: {
-      type: Function as PropType<(e: MouseEvent, element: PPTElement, canMove: boolean) => void>,
+      type: Function as PropType<(e: MouseEvent, element: PPTElement, canMove?: boolean) => void>,
       required: true,
     },
     rotateElement: {

+ 14 - 2
src/views/_common/_element/ImageElement/ImageClipHandler.vue

@@ -58,6 +58,16 @@ interface ClipData {
   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({
@@ -193,10 +203,12 @@ export default defineComponent({
         width: (topImgWrapperPosition.width - 100) / 100 * props.width,
         height: (topImgWrapperPosition.height - 100) / 100 * props.height,
       }
-      emit('clip', {
+
+      const clipedEmitData: ClipedEmitData = {
         range: currentRange.value,
         position,
-      })
+      }
+      emit('clip', clipedEmitData)
     }
 
     const keyboardClip = (e: KeyboardEvent) => {

+ 368 - 0
src/views/_common/_element/ImageElement/index.vue

@@ -0,0 +1,368 @@
+<template>
+  <div 
+    class="editable-element image"
+    :class="{'lock': elementInfo.isLock}"
+    :style="{
+      top: elementInfo.top + 'px',
+      left: elementInfo.left + 'px',
+      width: elementInfo.width + 'px',
+      height: elementInfo.height + 'px',
+      transform: `rotate(${elementInfo.rotate}deg)`,
+    }"
+    @mousedown="handleSelectElement($event)" 
+  >
+    <ImageClip
+      v-if="isCliping"
+      :imgUrl="elementInfo.imgUrl"
+      :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: elementInfo.shadow ? `drop-shadow(${elementInfo.shadow})` : '',
+        transform: flip,
+      }"
+    >
+      <RectBorder
+        v-if="clipShape.type === 'rect'"
+        :width="elementInfo.width"
+        :height="elementInfo.height"
+        :radius="clipShape.radius"
+        :borderColor="elementInfo.borderColor"
+        :borderWidth="elementInfo.borderWidth"
+        :borderStyle="elementInfo.borderStyle"
+      />
+      <EllipseBorder
+        v-else-if="clipShape.type === 'ellipse'"
+        :width="elementInfo.width"
+        :height="elementInfo.height"
+        :borderColor="elementInfo.borderColor"
+        :borderWidth="elementInfo.borderWidth"
+        :borderStyle="elementInfo.borderStyle"
+      />
+      <PolygonBorder
+        v-else-if="clipShape.type === 'polygon'"
+        :width="elementInfo.width"
+        :height="elementInfo.height"
+        :createPath="clipShape.createPath"
+        :borderColor="elementInfo.borderColor"
+        :borderWidth="elementInfo.borderWidth"
+        :borderStyle="elementInfo.borderStyle"
+      />
+
+      <div class="img-wrapper" :style="{clipPath: clipShape.style}">
+        <img 
+          :src="elementInfo.imgUrl" 
+          :draggable="false" 
+          :style="{
+            top: imgPosition.top,
+            left: imgPosition.left,
+            width: imgPosition.width,
+            height: imgPosition.height,
+            filter: filter,
+          }" 
+          alt=""
+        />
+      </div>
+    </div>
+
+    <div 
+      class="operate"
+      :class="{
+        'active': isActive,
+        'multi-select': isMultiSelect && isActive,
+        'selected': isHandleEl,
+      }" 
+      :style="{transform: `scale(${1 / canvasScale})`}"
+      v-if="!isCliping"
+    >
+      <BorderLine 
+        class="el-border-line" 
+        v-for="line in borderLines" 
+        :key="line.type" 
+        :type="line.type" 
+        :style="line.style" 
+      />
+      <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)"
+        />
+        <RotateHandle 
+          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 { PPTImageElement } from '@/types/slides'
+import { ElementScaleHandler, OperateResizablePointTypes, OperateBorderLineTypes } from '@/types/edit'
+
+import { CLIPPATHS, ClipPathTypes } from '@/configs/imageClip'
+import { OPERATE_KEYS } from '@/configs/element'
+
+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'
+
+import ImageClip, { ClipedEmitData } from './ImageClipHandler.vue'
+import ImageRectBorder from './ImageRectBorder.vue'
+import ImageEllipseBorder from './ImageEllipseBorder.vue'
+import ImagePolygonBorder from './ImagePolygonBorder.vue'
+
+export default defineComponent({
+  name: 'editable-element-image',
+  components: {
+    RotateHandler,
+    ResizablePoint,
+    BorderLine,
+    AnimationIndex,
+    ImageClip,
+    ImageRectBorder,
+    ImageEllipseBorder,
+    ImagePolygonBorder,
+  },
+  props: {
+    elementInfo: {
+      type: Object as PropType<PPTImageElement>,
+      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: 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: ElementScaleHandler) => void>,
+      required: true,
+    },
+  },
+  setup(props) {
+    const clipingImageElId = ref('')
+
+    const scaleWidth = computed(() => props.elementInfo ? props.elementInfo.width * props.canvasScale : 0)
+    const scaleHeight = computed(() => props.elementInfo ? props.elementInfo.height * props.canvasScale : 0)
+    const isCliping = computed(() => clipingImageElId.value === props.elementInfo.elId)
+
+    const imgPosition = computed(() => {
+      if(!props.elementInfo || !props.elementInfo.clip) {
+        return {
+          top: '0',
+          left: '0',
+          width: '100%',
+          height: '100%',
+        }
+      }
+
+      const [start, end] = props.elementInfo.clip.range
+
+      const widthScale = (end[0] - start[0]) / 100
+      const heightScale = (end[1] - start[1]) / 100
+      const left = start[0] / widthScale
+      const top = start[1] / heightScale
+
+      return {
+        left: -left + '%',
+        top: -top + '%',
+        width: 100 / widthScale + '%',
+        height: 100 / heightScale + '%',
+      }
+    })
+
+    const clipShape = computed(() => {
+      if(!props.elementInfo || !props.elementInfo.clip) return CLIPPATHS.rect
+      const shape = props.elementInfo.clip.shape || ClipPathTypes.RECT
+
+      return CLIPPATHS[shape]
+    })
+
+    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 filter = computed(() => {
+      if(!props.elementInfo.filter) return ''
+      let filter = ''
+      for(const key of Object.keys(props.elementInfo.filter)) {
+        filter += `${key}(${props.elementInfo.filter[key]}) `
+      }
+      return filter
+    })
+
+    const flip = computed(() => {
+      if(!props.elementInfo.flip) return ''
+      const { x, y } = props.elementInfo.flip
+      if(x && y) return `rotateX(${x}deg) rotateY(${y}deg)`
+      else if(x) return `rotateX(${x}deg)`
+      else if(y) return `rotateY(${y}deg)`
+      return ''
+    })
+
+    const handleSelectElement = (e: MouseEvent) => {
+      if(isCliping.value || props.elementInfo.isLock) 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 {
+      isCliping,
+      imgPosition,
+      clipShape,
+      resizablePoints,
+      borderLines,
+      filter,
+      flip,
+      handleSelectElement,
+      clip,
+    }
+  },
+})
+</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 {
+  width: 100%;
+  height: 100%;
+  position: relative;
+  cursor: move;
+
+  .img-wrapper {
+    width: 100%;
+    height: 100%;
+    overflow: hidden;
+    position: relative;
+  }
+
+  img {
+    position: absolute;
+  }
+}
+
+.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>

+ 3 - 7
src/views/_common/_operate/AnimationIndex.vue

@@ -1,8 +1,6 @@
 <template>
   <div class="animation-index">
-    <div class="index" v-for="(item, index) in animations" :key="index">
-      {{index}}
-    </div>
+    {{animationIndex}}
   </div>
 </template>
 
@@ -13,8 +11,8 @@ import { PPTAnimation } from '@/types/slides'
 export default {
   name: 'animation-index',
   props: {
-    animations: {
-      type: Array as PropType<PPTAnimation[]>,
+    animationIndex: {
+      type: Number,
       required: true,
     },
   },
@@ -27,8 +25,6 @@ export default {
   top: 0;
   left: -22px;
   font-size: 12px;
-}
-.index {
   width: 20px;
   height: 20px;
   background-color: #fff;

+ 2 - 3
src/views/_common/_operate/BorderLine.vue

@@ -4,14 +4,13 @@
 
 <script lang="ts">
 import { PropType } from 'vue'
-
-type BorderLineType = 't' | 'b' | 'l' | 'r'
+import { OperateBorderLineType } from '@/types/edit'
 
 export default {
   name: 'border-line',
   props: {
     type: {
-      type: String as PropType<BorderLineType>,
+      type: String as PropType<OperateBorderLineType>,
       required: true,
     },
     isWide: {

+ 2 - 3
src/views/_common/_operate/ResizablePoint.vue

@@ -4,14 +4,13 @@
 
 <script lang="ts">
 import { PropType } from 'vue'
-
-type ResizablePointType = 't-l' | 't-c' | 't-r' | 'm-l' | 'm-r' | 'b-l' | 'b-c' | 'b-r' | 'any'
+import { OperateResizablePointType } from '@/types/edit'
 
 export default {
   name: 'resizable-point',
   props: {
     type: {
-      type: String as PropType<ResizablePointType>,
+      type: String as PropType<OperateResizablePointType>,
       required: true,
     },
   },