| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438 |
- <template>
- <div class="image-style-panel">
- <div
- class="origin-image"
- :style="{ backgroundImage: `url(${handleElement.src})` }"
- ></div>
- <ButtonGroup class="row">
- <Button style="flex: 5;" @click="clipImage()"><IconTailoring class="btn-icon" /> 裁剪图片</Button>
- <Popover trigger="click" v-model:visible="clipPanelVisible">
- <template #content>
- <div class="clip">
- <div class="title">按形状裁剪:</div>
- <div class="shape-clip">
- <div
- class="shape-clip-item"
- v-for="(item, index) in shapeClipPathOptions"
- :key="index"
- @click="presetImageClip(index)"
- >
- <div class="shape" :style="{ backgroundImage: `url(${handleElement.src})`, clipPath: item.style }"></div>
- </div>
- </div>
- <template v-for="type in ratioClipOptions" :key="type.label">
- <div class="title" v-if="type.label">{{type.label}}:</div>
- <ButtonGroup class="row">
- <Button
- style="flex: 1;"
- v-for="item in type.children"
- :key="item.key"
- @click="presetImageClip('rect', item.ratio)"
- >{{item.key}}</Button>
- </ButtonGroup>
- </template>
- </div>
- </template>
- <Button class="no-padding" style="flex: 1;"><IconDown /></Button>
- </Popover>
- </ButtonGroup>
- <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"><IconColorFilter class="btn-icon" /> 设置滤镜</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 />
-
- <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">
- import { computed, defineComponent, ref, watch } from 'vue'
- import { MutationTypes, useStore } from '@/store'
- import { PPTImageElement, Slide } from '@/types/slides'
- import { CLIPPATHS } from '@/configs/imageClip'
- import { getImageDataURL } from '@/utils/image'
- 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 },
- ]
- 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 },
- ],
- },
- ]
- export default defineComponent({
- name: 'image-style-panel',
- components: {
- ElementOutline,
- ElementShadow,
- },
- setup() {
- const store = useStore()
- const handleElement = computed<PPTImageElement>(() => store.getters.handleElement)
- const currentSlide = computed<Slide>(() => store.getters.currentSlide)
- const clipPanelVisible = ref(false)
- const flip = ref({
- x: 0,
- y: 0,
- })
- const filterOptions = ref<FilterOption[]>(JSON.parse(JSON.stringify(defaultFilters)))
- watch(handleElement, () => {
- if(!handleElement.value || handleElement.value.type !== 'image') 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 = () => {
- store.commit(MutationTypes.SET_CLIPING_IMAGE_ELEMENT_ID, handleElement.value.id)
- clipPanelVisible.value = false
- }
- const getImageElementDataBeforeClip = () => {
- // 图片当前宽高位置、裁剪范围
- const imgWidth = handleElement.value.width
- const imgHeight = handleElement.value.height
- const imgLeft = handleElement.value.left
- const imgTop = handleElement.value.top
- const originClipRange = handleElement.value.clip ? handleElement.value.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 presetImageClip = (shape: string, ratio = 0) => {
- const {
- originClipRange,
- originWidth,
- originHeight,
- originLeft,
- originTop,
- } = getImageElementDataBeforeClip()
-
- // 设置形状和纵横比
- if(ratio) {
- const imageRatio = originHeight / originWidth
- const min = 0
- const max = 100
- let range
- 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]]
- }
- store.commit(MutationTypes.UPDATE_ELEMENT, {
- id: handleElement.value.id,
- props: {
- clip: { ...handleElement.value.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 {
- store.commit(MutationTypes.UPDATE_ELEMENT, {
- id: handleElement.value.id,
- props: {
- clip: { ...handleElement.value.clip, shape, range: originClipRange }
- },
- })
- }
- clipImage()
- addHistorySnapshot()
- }
- const replaceImage = (files: File[]) => {
- const imageFile = files[0]
- if(!imageFile) return
- getImageDataURL(imageFile).then(dataURL => {
- const props = { src: dataURL }
- store.commit(MutationTypes.UPDATE_ELEMENT, { id: handleElement.value.id, props })
- })
- addHistorySnapshot()
- }
- const resetImage = () => {
- if(handleElement.value.clip) {
- const {
- originWidth,
- originHeight,
- originLeft,
- originTop,
- } = getImageElementDataBeforeClip()
- store.commit(MutationTypes.UPDATE_ELEMENT, {
- id: handleElement.value.id,
- props: {
- left: originLeft,
- top: originTop,
- width: originWidth,
- height: originHeight,
- },
- })
- }
- store.commit(MutationTypes.REMOVE_ELEMENT_PROPS, {
- id: handleElement.value.id,
- propName: ['clip', 'outline', 'flip', 'shadow', 'filters'],
- })
- addHistorySnapshot()
- }
- const setBackgroundImage = () => {
- const background = {
- ...currentSlide.value.background,
- type: 'image',
- image: handleElement.value.src,
- imageSize: 'cover',
- }
- store.commit(MutationTypes.UPDATE_SLIDE, { background })
- addHistorySnapshot()
- }
- return {
- clipPanelVisible,
- shapeClipPathOptions,
- ratioClipOptions,
- filterOptions,
- flip,
- handleElement,
- updateImage,
- updateFilter,
- clipImage,
- presetImageClip,
- replaceImage,
- resetImage,
- setBackgroundImage,
- }
- },
- })
- </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;
- }
- .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;
- }
- }
- .clip {
- width: 260px;
- font-size: 12px;
- .title {
- margin-bottom: 5px;
- }
- }
- .shape-clip {
- margin-bottom: 10px;
- @include grid-layout-wrapper();
- }
- .shape-clip-item {
- display: flex;
- justify-content: center;
- align-items: center;
- cursor: pointer;
- @include grid-layout-item(5, 16%);
- .shape {
- width: 40px;
- height: 40px;
- background-position: center;
- background-repeat: no-repeat;
- background-size: cover;
- }
- }
- </style>
|