|
<template> |
|
<div class="image-style-panel"> |
|
<div |
|
class="origin-image" |
|
:style="{ backgroundImage: `url(${handleImageElement.src})` }" |
|
></div> |
|
|
|
<ElementFlip /> |
|
|
|
<ButtonGroup class="row" passive> |
|
<Button first style="width: calc(100% / 6 * 5);" @click="clipImage()"><IconTailoring class="btn-icon" /> 裁剪图片</Button> |
|
<Popover trigger="click" v-model:value="clipPanelVisible" style="width: calc(100% / 6);"> |
|
<template #content> |
|
<div class="clip"> |
|
<div class="title">按形状:</div> |
|
<div class="shape-clip"> |
|
<div |
|
class="shape-clip-item" |
|
v-for="(item, key) in shapeClipPathOptions" |
|
:key="key" |
|
@click="presetImageClip(key as string)" |
|
> |
|
<div class="shape" :style="{ clipPath: item.style }"></div> |
|
</div> |
|
</div> |
|
|
|
<template v-for="typeItem in ratioClipOptions" :key="typeItem.label"> |
|
<div class="title" v-if="typeItem.label">按{{typeItem.label}}:</div> |
|
<ButtonGroup class="row"> |
|
<Button |
|
style="flex: 1;" |
|
v-for="item in typeItem.children" |
|
:key="item.key" |
|
@click="presetImageClip('rect', item.ratio)" |
|
>{{item.key}}</Button> |
|
</ButtonGroup> |
|
</template> |
|
</div> |
|
</template> |
|
<Button last class="popover-btn" style="width: 100%;"><IconDown /></Button> |
|
</Popover> |
|
</ButtonGroup> |
|
|
|
<div class="row"> |
|
<div style="width: 40%;">圆角半径:</div> |
|
<NumberInput |
|
:value="handleImageElement.radius || 0" |
|
@update:value="value => updateImage({ radius: value })" |
|
style="width: 60%;" |
|
/> |
|
</div> |
|
|
|
<Divider /> |
|
<ElementColorMask /> |
|
<Divider /> |
|
<ElementFilter /> |
|
<Divider /> |
|
<ElementOutline /> |
|
<Divider /> |
|
<ElementShadow /> |
|
<Divider /> |
|
|
|
<FileInput @change="files => replaceImage(files)"> |
|
<Button class="full-width-btn"><IconTransform class="btn-icon" /> 替换图片</Button> |
|
</FileInput> |
|
<Button class="full-width-btn" @click="resetImage()"><IconUndo class="btn-icon" /> 重置样式</Button> |
|
<Button class="full-width-btn" @click="setBackgroundImage()"><IconTheme class="btn-icon" /> 设为背景</Button> |
|
</div> |
|
</template> |
|
|
|
<script lang="ts" setup> |
|
import { type Ref, ref } from 'vue' |
|
import { storeToRefs } from 'pinia' |
|
import { useMainStore, useSlidesStore } from '@/store' |
|
import type { PPTImageElement, SlideBackground } from '@/types/slides' |
|
import { CLIPPATHS } from '@/configs/imageClip' |
|
import { getImageDataURL, getImageSize } from '@/utils/image' |
|
import useHistorySnapshot from '@/hooks/useHistorySnapshot' |
|
|
|
import ElementOutline from '../common/ElementOutline.vue' |
|
import ElementShadow from '../common/ElementShadow.vue' |
|
import ElementFlip from '../common/ElementFlip.vue' |
|
import ElementFilter from '../common/ElementFilter.vue' |
|
import ElementColorMask from '../common/ElementColorMask.vue' |
|
import FileInput from '@/components/FileInput.vue' |
|
import Divider from '@/components/Divider.vue' |
|
import Button from '@/components/Button.vue' |
|
import ButtonGroup from '@/components/ButtonGroup.vue' |
|
import Popover from '@/components/Popover.vue' |
|
import NumberInput from '@/components/NumberInput.vue' |
|
|
|
const shapeClipPathOptions = CLIPPATHS |
|
const ratioClipOptions = [ |
|
{ |
|
label: '纵横比(正方形)', |
|
children: [ |
|
{ key: '1:1', ratio: 1 / 1 }, |
|
], |
|
}, |
|
{ |
|
label: '纵横比(纵向)', |
|
children: [ |
|
{ key: '2:3', ratio: 3 / 2 }, |
|
{ key: '3:4', ratio: 4 / 3 }, |
|
{ key: '3:5', ratio: 5 / 3 }, |
|
{ key: '4:5', ratio: 5 / 4 }, |
|
], |
|
}, |
|
{ |
|
label: '纵横比(横向)', |
|
children: [ |
|
{ key: '3:2', ratio: 2 / 3 }, |
|
{ key: '4:3', ratio: 3 / 4 }, |
|
{ key: '5:3', ratio: 3 / 5 }, |
|
{ key: '5:4', ratio: 4 / 5 }, |
|
], |
|
}, |
|
{ |
|
children: [ |
|
{ key: '16:9', ratio: 9 / 16 }, |
|
{ key: '16:10', ratio: 10 / 16 }, |
|
], |
|
}, |
|
] |
|
|
|
const mainStore = useMainStore() |
|
const slidesStore = useSlidesStore() |
|
const { handleElement, handleElementId } = storeToRefs(mainStore) |
|
const { currentSlide } = storeToRefs(slidesStore) |
|
|
|
const handleImageElement = handleElement as Ref<PPTImageElement> |
|
|
|
const clipPanelVisible = ref(false) |
|
|
|
const { addHistorySnapshot } = useHistorySnapshot() |
|
|
|
|
|
const clipImage = () => { |
|
mainStore.setClipingImageElementId(handleElementId.value) |
|
clipPanelVisible.value = false |
|
} |
|
|
|
|
|
const getImageElementDataBeforeClip = () => { |
|
const _handleElement = handleElement.value as PPTImageElement |
|
|
|
|
|
const imgWidth = _handleElement.width |
|
const imgHeight = _handleElement.height |
|
const imgLeft = _handleElement.left |
|
const imgTop = _handleElement.top |
|
const originClipRange: [[number, number], [number, number]] = _handleElement.clip ? _handleElement.clip.range : [[0, 0], [100, 100]] |
|
|
|
const originWidth = imgWidth / ((originClipRange[1][0] - originClipRange[0][0]) / 100) |
|
const originHeight = imgHeight / ((originClipRange[1][1] - originClipRange[0][1]) / 100) |
|
const originLeft = imgLeft - originWidth * (originClipRange[0][0] / 100) |
|
const originTop = imgTop - originHeight * (originClipRange[0][1] / 100) |
|
|
|
return { |
|
originClipRange, |
|
originWidth, |
|
originHeight, |
|
originLeft, |
|
originTop, |
|
} |
|
} |
|
|
|
const updateImage = (props: Partial<PPTImageElement>) => { |
|
if (!handleElement.value) return |
|
slidesStore.updateElement({ id: handleElementId.value, props }) |
|
addHistorySnapshot() |
|
} |
|
|
|
|
|
const presetImageClip = (shape: string, ratio = 0) => { |
|
const _handleElement = handleElement.value as PPTImageElement |
|
|
|
const { |
|
originClipRange, |
|
originWidth, |
|
originHeight, |
|
originLeft, |
|
originTop, |
|
} = getImageElementDataBeforeClip() |
|
|
|
|
|
if (ratio) { |
|
const imageRatio = originHeight / originWidth |
|
|
|
const min = 0 |
|
const max = 100 |
|
let range: [[number, number], [number, number]] |
|
|
|
if (imageRatio > ratio) { |
|
const distance = ((1 - ratio / imageRatio) / 2) * 100 |
|
range = [[min, distance], [max, max - distance]] |
|
} |
|
else { |
|
const distance = ((1 - imageRatio / ratio) / 2) * 100 |
|
range = [[distance, min], [max - distance, max]] |
|
} |
|
updateImage({ |
|
clip: { ..._handleElement.clip, shape, range }, |
|
left: originLeft + originWidth * (range[0][0] / 100), |
|
top: originTop + originHeight * (range[0][1] / 100), |
|
width: originWidth * (range[1][0] - range[0][0]) / 100, |
|
height: originHeight * (range[1][1] - range[0][1]) / 100, |
|
}) |
|
} |
|
|
|
else { |
|
const clipData = { ..._handleElement.clip, shape, range: originClipRange } |
|
let props: Partial<PPTImageElement> = { clip: clipData } |
|
if (shape === 'rect') props = { clip: clipData, radius: 0 } |
|
updateImage(props) |
|
} |
|
clipImage() |
|
} |
|
|
|
|
|
const replaceImage = (files: FileList) => { |
|
const imageFile = files[0] |
|
if (!imageFile) return |
|
getImageDataURL(imageFile).then(dataURL => { |
|
const originWidth = handleImageElement.value.width |
|
const originHeight = handleImageElement.value.height |
|
const originLeft = handleImageElement.value.left |
|
const originTop = handleImageElement.value.top |
|
const centerX = originLeft + originWidth / 2 |
|
const centerY = originTop + originHeight / 2 |
|
|
|
getImageSize(dataURL).then(({ width, height }) => { |
|
const h = originHeight |
|
const w = width * (originHeight / height) |
|
const l = centerX - w / 2 |
|
const t = centerY - h / 2 |
|
|
|
slidesStore.removeElementProps({ |
|
id: handleElementId.value, |
|
propName: 'clip', |
|
}) |
|
updateImage({ |
|
src: dataURL, |
|
width: w, |
|
height: h, |
|
left: l, |
|
top: t, |
|
}) |
|
}) |
|
}) |
|
} |
|
|
|
|
|
const resetImage = () => { |
|
const _handleElement = handleElement.value as PPTImageElement |
|
|
|
if (_handleElement.clip) { |
|
const { |
|
originWidth, |
|
originHeight, |
|
originLeft, |
|
originTop, |
|
} = getImageElementDataBeforeClip() |
|
|
|
updateImage({ |
|
left: originLeft, |
|
top: originTop, |
|
width: originWidth, |
|
height: originHeight, |
|
}) |
|
} |
|
|
|
slidesStore.removeElementProps({ |
|
id: handleElementId.value, |
|
propName: ['clip', 'outline', 'flip', 'shadow', 'filters', 'colorMask', 'radius'], |
|
}) |
|
addHistorySnapshot() |
|
} |
|
|
|
|
|
const setBackgroundImage = () => { |
|
const _handleElement = handleElement.value as PPTImageElement |
|
|
|
const background: SlideBackground = { |
|
...currentSlide.value.background, |
|
type: 'image', |
|
image: { |
|
src: _handleElement.src, |
|
size: 'cover' |
|
}, |
|
} |
|
slidesStore.updateSlide({ background }) |
|
addHistorySnapshot() |
|
} |
|
</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; |
|
} |
|
.btn-icon { |
|
margin-right: 3px; |
|
} |
|
|
|
.clip { |
|
width: 260px; |
|
font-size: 12px; |
|
|
|
.title { |
|
margin-bottom: 5px; |
|
} |
|
} |
|
.shape-clip { |
|
margin-bottom: 10px; |
|
|
|
@include flex-grid-layout(); |
|
} |
|
.shape-clip-item { |
|
display: flex; |
|
justify-content: center; |
|
align-items: center; |
|
cursor: pointer; |
|
|
|
@include flex-grid-layout-children(5, 16%); |
|
|
|
&:hover .shape { |
|
background-color: #ccc; |
|
} |
|
|
|
.shape { |
|
width: 40px; |
|
height: 40px; |
|
background-color: #e1e1e1; |
|
} |
|
} |
|
.popover-btn { |
|
padding: 0 3px; |
|
} |
|
</style> |