mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2025-04-15 02:20:00 +08:00
update
This commit is contained in:
parent
858318563b
commit
f100f2cd9e
@ -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',
|
||||
|
@ -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({
|
||||
|
@ -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[]) {
|
||||
|
@ -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';
|
||||
|
@ -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
|
||||
|
||||
|
@ -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>
|
@ -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>
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user