pipipi-pikachu 5 lat temu
rodzic
commit
f100f2cd9e

+ 1 - 0
src/store/constants.ts

@@ -12,6 +12,7 @@ export enum MutationTypes {
   SET_CREATING_ELEMENT = 'setCreatingElement',
   SET_AVAILABLE_FONTS = 'setAvailableFonts',
   SET_TOOLBAR_STATE = 'setToolbarState',
+  SET_CLIPING_IMAGE_ELEMENT_ID = 'setClipingImageElementId',
 
   // slides
   SET_SLIDES = 'setSlides',

+ 2 - 0
src/store/index.ts

@@ -31,6 +31,7 @@ export interface State {
   ctrlKeyState: boolean;
   shiftKeyState: boolean;
   screening: boolean;
+  clipingImageElementId: string;
 }
 
 const state: State = {
@@ -52,6 +53,7 @@ const state: State = {
   ctrlKeyState: false,
   shiftKeyState: false,
   screening: false,
+  clipingImageElementId: '',
 }
 
 export default createStore({

+ 4 - 0
src/store/mutations.ts

@@ -62,6 +62,10 @@ export const mutations: MutationTree<State> = {
     state.toolbarState = type
   },
 
+  [MutationTypes.SET_CLIPING_IMAGE_ELEMENT_ID](state, elId) {
+    state.clipingImageElementId = elId
+  },
+
   // slides
 
   [MutationTypes.SET_SLIDES](state, slides: Slide[]) {

+ 7 - 7
src/types/slides.ts

@@ -40,13 +40,13 @@ export interface PPTTextElement {
 }
 
 export interface ImageElementFilters {
-  'blur': string;
-  'brightness': string;
-  'contrast': string;
-  'grayscale': string;
-  'saturate': string;
-  'hue-rotate': string;
-  'opacity': string;
+  'blur'?: string;
+  'brightness'?: string;
+  'contrast'?: string;
+  'grayscale'?: string;
+  'saturate'?: string;
+  'hue-rotate'?: string;
+  'opacity'?: string;
 }
 export interface PPTImageElement {
   type: 'image';

+ 11 - 13
src/views/Editor/Canvas/Operate/ImageClipHandler.vue

@@ -16,7 +16,7 @@
       class="top-image-content" 
       :style="{
         ...topImgWrapperPositionStyle,
-        clipPath: clipPath,
+        clipPath,
       }"
     >
       <img 
@@ -49,6 +49,8 @@
 
 <script lang="ts">
 import { computed, defineComponent, onMounted, onUnmounted, PropType, reactive, ref } from 'vue'
+import { useStore } from 'vuex'
+import { State } from '@/store'
 import { KEYS } from '@/configs/hotkey'
 import { ImageClipData, ImageClipDataRange, ImageClipedEmitData } from '@/types/edit'
 
@@ -63,16 +65,11 @@ export default defineComponent({
     },
     clipData: {
       type: Object as PropType<ImageClipData>,
-      required: true,
     },
     clipPath: {
       type: String,
       required: true,
     },
-    canvasScale: {
-      type: Number,
-      required: true,
-    },
     width: {
       type: Number,
       required: true,
@@ -91,6 +88,9 @@ export default defineComponent({
     },
   },
   setup(props, { emit }) {
+    const store = useStore<State>()
+    const canvasScale = computed(() => store.state.canvasScale)
+
     const topImgWrapperPosition = reactive({
       top: 0,
       left: 0,
@@ -172,9 +172,7 @@ export default defineComponent({
     }
 
     const clip = () => {
-      if(isSettingClipRange.value) return
-
-      if(!currentRange.value) {
+      if(isSettingClipRange.value || !currentRange.value) {
         emit('clip', null)
         return
       }
@@ -251,8 +249,8 @@ export default defineComponent({
         const currentPageX = e.pageX
         const currentPageY = e.pageY
 
-        const moveX = (currentPageX - startPageX) / props.canvasScale / props.width * 100
-        const moveY = (currentPageY - startPageY) / props.canvasScale / props.height * 100
+        const moveX = (currentPageX - startPageX) / canvasScale.value / props.width * 100
+        const moveY = (currentPageY - startPageY) / canvasScale.value / props.height * 100
 
         let targetLeft = originPositopn.left + moveX
         let targetTop = originPositopn.top + moveY
@@ -307,8 +305,8 @@ export default defineComponent({
         const currentPageX = e.pageX
         const currentPageY = e.pageY
 
-        let moveX = (currentPageX - startPageX) / props.canvasScale / props.width * 100
-        let moveY = (currentPageY - startPageY) / props.canvasScale / props.height * 100
+        let moveX = (currentPageX - startPageX) / canvasScale.value / props.width * 100
+        let moveY = (currentPageY - startPageY) / canvasScale.value / props.height * 100
 
         let targetLeft, targetTop, targetWidth, targetHeight
 

+ 38 - 18
src/views/Editor/Canvas/Operate/ImageElementOperate.vue

@@ -1,16 +1,23 @@
 <template>
-  <ImageClipHandler
+  <div 
+    class="clip-wrapper" 
+    :style="{
+      width: elementInfo.width + 'px',
+      height: elementInfo.height + 'px',
+    }"
     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)"
-  />
+  >
+    <ImageClipHandler
+      :src="elementInfo.src"
+      :clipData="elementInfo.clip"
+      :width="elementInfo.width"
+      :height="elementInfo.height"
+      :top="elementInfo.top"
+      :left="elementInfo.left"
+      :clipPath="clipShape.style"
+      @clip="range => clip(range)"
+    />
+  </div>
   <div class="image-element-operate" v-else>
     <BorderLine 
       class="operate-border-line"
@@ -38,12 +45,12 @@
 </template>
 
 <script lang="ts">
-import { computed, defineComponent, PropType, ref } from 'vue'
+import { computed, defineComponent, PropType } from 'vue'
 import { useStore } from 'vuex'
-import { State } from '@/store'
-
+import { MutationTypes, State } from '@/store'
 import { PPTImageElement } from '@/types/slides'
 import { OperateResizeHandler, ImageClipedEmitData } from '@/types/edit'
+import { CLIPPATHS, ClipPathTypes } from '@/configs/imageClip'
 import useCommonOperate from '../hooks/useCommonOperate'
 
 import RotateHandler from './RotateHandler.vue'
@@ -85,17 +92,23 @@ export default defineComponent({
   setup(props) {
     const store = useStore<State>()
     const canvasScale = computed(() => store.state.canvasScale)
+    const clipingImageElementId = computed(() => store.state.clipingImageElementId)
 
     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 clipShape = computed(() => {
+      if(!props.elementInfo || !props.elementInfo.clip) return CLIPPATHS.rect
+      const shape = props.elementInfo.clip.shape || ClipPathTypes.RECT
 
-    const isCliping = computed(() => clipingImageElId.value === props.elementInfo.id)
+      return CLIPPATHS[shape]
+    })
+
+    const isCliping = computed(() => clipingImageElementId.value === props.elementInfo.id)
 
     const clip = (data: ImageClipedEmitData) => {
-      clipingImageElId.value = ''
+      store.commit(MutationTypes.SET_CLIPING_IMAGE_ELEMENT_ID, '')
       
       if(!data) return
 
@@ -113,6 +126,7 @@ export default defineComponent({
     }
 
     return {
+      clipShape,
       scaleWidth,
       resizeHandlers,
       borderLines,
@@ -121,4 +135,10 @@ export default defineComponent({
     }
   },
 })
-</script>
+</script>
+
+<style lang="scss" scoped>
+.clip-wrapper {
+  position: relative;
+}
+</style>

+ 196 - 3
src/views/Editor/Toolbar/ElementStylePanel/ImageStylePanel.vue

@@ -1,13 +1,206 @@
 <template>
   <div class="image-style-panel">
-    image-style-panel
+    <div 
+      class="origin-image"
+      :style="{ backgroundImage: `url(${handleElement.src})` }"
+    ></div>
+
+    <Button class="full-width-btn" @click="clipImage()">裁剪图片</Button>
+
+    <Popover trigger="click">
+      <template #content>
+        <div class="filter">
+          <div class="filter-item" v-for="filter in filterOptions" :key="filter.key">
+            <div class="name">{{filter.label}}</div>
+            <Slider
+              class="filter-slider"
+              :max="filter.max"
+              :min="filter.min"
+              :step="filter.step"
+              :value="filter.value"
+              @change="value => updateFilter(filter, value)"
+            />
+            <div class="value">{{`${filter.value}${filter.unit}`}}</div>
+          </div>
+        </div>
+      </template>
+      <Button class="full-width-btn">设置滤镜</Button>
+    </Popover>
+    
+    <div class="row">
+      <div style="flex: 2;">水平翻转:</div>
+      <div class="switch-wrapper" style="flex: 3;">
+        <Switch 
+          :checked="flip.x === 180" 
+          @change="checked => updateImage({ flip: { x: checked ? 180 : 0, y: flip.y } })" 
+        />
+      </div>
+    </div>
+    <div class="row">
+      <div style="flex: 2;">垂直翻转:</div>
+      <div class="switch-wrapper" style="flex: 3;">
+        <Switch 
+          :checked="flip.y === 180" 
+          @change="checked => updateImage({ flip: { x: flip.x, y: checked ? 180 : 0 } })" 
+        />
+      </div>
+    </div>
+
+    <Divider />
+    <ElementOutline />
+    <Divider />
+    <ElementShadow />
+    <Divider />
+    
+    <Button class="full-width-btn">替换图片</Button>
+    <Button class="full-width-btn">重置样式</Button>
   </div>
 </template>
 
 <script lang="ts">
-import { defineComponent } from 'vue'
+import { computed, defineComponent, ref, Ref, watch } from 'vue'
+import { useStore } from 'vuex'
+import { MutationTypes, State } from '@/store'
+import { PPTImageElement } from '@/types/slides'
+import useHistorySnapshot from '@/hooks/useHistorySnapshot'
+
+import ElementOutline from '../common/ElementOutline.vue'
+import ElementShadow from '../common/ElementShadow.vue'
+
+interface FilterOption {
+  label: string;
+  key: string;
+  default: number;
+  value: number;
+  unit: string;
+  max: number;
+  step: number;
+}
+
+const defaultFilters: FilterOption[] = [
+  { label: '模糊', key: 'blur', default: 0, value: 0, unit: 'px', max: 10, step: 1 },
+  { label: '亮度', key: 'brightness', default: 100, value: 100, unit: '%', max: 200, step: 5 },
+  { label: '对比度', key: 'contrast', default: 100, value: 100, unit: '%', max: 200, step: 5 },
+  { label: '灰度', key: 'grayscale', default: 0, value: 0, unit: '%', max: 100, step: 5 },
+  { label: '饱和度', key: 'saturate', default: 100, value: 100, unit: '%', max: 200, step: 5 },
+  { label: '色相', key: 'hue-rotate', default: 0, value: 0, unit: 'deg', max: 360, step: 10 },
+  { label: '不透明度', key: 'opacity', default: 100, value: 100, unit: '%', max: 100, step: 5 },
+]
 
 export default defineComponent({
   name: 'image-style-panel',
+  components: {
+    ElementOutline,
+    ElementShadow,
+  },
+  setup() {
+    const store = useStore<State>()
+    const handleElement: Ref<PPTImageElement> = computed(() => store.getters.handleElement)
+
+    const flip = ref({
+      x: 0,
+      y: 0,
+    })
+
+    const filterOptions: Ref<FilterOption[]> = ref(JSON.parse(JSON.stringify(defaultFilters)))
+
+    watch(handleElement, () => {
+      if(!handleElement.value) return
+
+      if(handleElement.value.flip) {
+        flip.value = {
+          x: handleElement.value.flip.x || 0,
+          y: handleElement.value.flip.y || 0,
+        }
+      }
+      else flip.value = { x: 0, y: 0 }
+
+      const filters = handleElement.value.filters
+      if(filters) {
+        filterOptions.value = defaultFilters.map(item => {
+          if(filters[item.key] !== undefined) return { ...item, value: parseInt(filters[item.key]) }
+          return item
+        })
+      }
+      else filterOptions.value = JSON.parse(JSON.stringify(defaultFilters))
+    }, { deep: true, immediate: true })
+
+    const { addHistorySnapshot } = useHistorySnapshot()
+
+    const updateImage = (props: Partial<PPTImageElement>) => {
+      store.commit(MutationTypes.UPDATE_ELEMENT, { id: handleElement.value.id, props })
+      addHistorySnapshot()
+    }
+
+    const updateFilter = (filter: FilterOption, value: number) => {
+      const originFilters = handleElement.value.filters || {}
+      const filters = { ...originFilters, [filter.key]: `${value}${filter.unit}` }
+      const props = { filters }
+      store.commit(MutationTypes.UPDATE_ELEMENT, { id: handleElement.value.id, props })
+      addHistorySnapshot()
+    }
+
+    const clipImage = () => {
+      setTimeout(() => {
+        store.commit(MutationTypes.SET_CLIPING_IMAGE_ELEMENT_ID, handleElement.value.id)
+      }, 0)
+    }
+
+    return {
+      filterOptions,
+      flip,
+      handleElement,
+      updateImage,
+      updateFilter,
+      clipImage,
+    }
+  },
 })
-</script>
+</script>
+
+<style lang="scss" scoped>
+.row {
+  width: 100%;
+  display: flex;
+  align-items: center;
+  margin-bottom: 10px;
+}
+.switch-wrapper {
+  text-align: right;
+}
+.origin-image {
+  height: 100px;
+  background-size: contain;
+  background-repeat: no-repeat;
+  background-position: center;
+  background-color: $lightGray;
+  margin-bottom: 10px;
+}
+.full-width-btn {
+  width: 100%;
+  margin-bottom: 10px;
+}
+
+.filter {
+  width: 280px;
+  font-size: 12px;
+}
+.filter-item {
+  padding: 8px 5px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+
+  .name {
+    width: 60px;
+  }
+  .filter-slider {
+    flex: 1;
+    margin: 0 6px;
+  }
+  .value {
+    width: 40px;
+    text-align: right;
+  }
+}
+</style>

+ 14 - 2
src/views/components/element/ImageElement/index.vue

@@ -1,7 +1,10 @@
 <template>
   <div 
     class="editable-element-image"
-    :class="{ 'lock': elementInfo.lock }"
+    :class="{
+      'lock': elementInfo.lock,
+      'cliping': clipingImageElementId === elementInfo.id,
+    }"
     :style="{
       top: elementInfo.top + 'px',
       left: elementInfo.left + 'px',
@@ -60,7 +63,8 @@
 
 <script lang="ts">
 import { computed, defineComponent, PropType } from 'vue'
-
+import { useStore } from 'vuex'
+import { State } from '@/store'
 import { PPTImageElement } from '@/types/slides'
 import { ContextmenuItem } from '@/components/Contextmenu/types'
 import { CLIPPATHS, ClipPathTypes } from '@/configs/imageClip'
@@ -92,6 +96,9 @@ export default defineComponent({
     },
   },
   setup(props) {
+    const store = useStore<State>()
+    const clipingImageElementId = computed(() => store.state.clipingImageElementId)
+
     const shadow = computed(() => props.elementInfo.shadow)
     const { shadowStyle } = useElementShadow(shadow)
 
@@ -151,6 +158,7 @@ export default defineComponent({
     })
 
     return {
+      clipingImageElementId,
       shadowStyle,
       handleSelectElement,
       clipShape,
@@ -169,6 +177,10 @@ export default defineComponent({
   &.lock .element-content {
     cursor: default;
   }
+
+  &.cliping {
+    visibility: hidden;
+  }
 }
 
 .element-content {