This commit is contained in:
pipipi-pikachu 2021-01-10 23:46:44 +08:00
parent 858318563b
commit f100f2cd9e
8 changed files with 273 additions and 43 deletions

View File

@ -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',

View File

@ -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({

View File

@ -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[]) {

View File

@ -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';

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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 {