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
f100f2cd9e
commit
653bf033a7
@ -1,24 +1,5 @@
|
||||
<template>
|
||||
<div
|
||||
class="clip-wrapper"
|
||||
:style="{
|
||||
width: elementInfo.width + 'px',
|
||||
height: elementInfo.height + 'px',
|
||||
}"
|
||||
v-if="isCliping"
|
||||
>
|
||||
<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>
|
||||
<div class="image-element-operate" :class="{ 'cliping': isCliping }">
|
||||
<BorderLine
|
||||
class="operate-border-line"
|
||||
v-for="line in borderLines"
|
||||
@ -47,16 +28,14 @@
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, PropType } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { MutationTypes, State } from '@/store'
|
||||
import { State } from '@/store'
|
||||
import { PPTImageElement } from '@/types/slides'
|
||||
import { OperateResizeHandler, ImageClipedEmitData } from '@/types/edit'
|
||||
import { CLIPPATHS, ClipPathTypes } from '@/configs/imageClip'
|
||||
import { OperateResizeHandler } from '@/types/edit'
|
||||
import useCommonOperate from '../hooks/useCommonOperate'
|
||||
|
||||
import RotateHandler from './RotateHandler.vue'
|
||||
import ResizeHandler from './ResizeHandler.vue'
|
||||
import BorderLine from './BorderLine.vue'
|
||||
import ImageClipHandler from './ImageClipHandler.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'image-element-operate',
|
||||
@ -65,7 +44,6 @@ export default defineComponent({
|
||||
RotateHandler,
|
||||
ResizeHandler,
|
||||
BorderLine,
|
||||
ImageClipHandler,
|
||||
},
|
||||
props: {
|
||||
elementInfo: {
|
||||
@ -93,52 +71,24 @@ export default defineComponent({
|
||||
const store = useStore<State>()
|
||||
const canvasScale = computed(() => store.state.canvasScale)
|
||||
const clipingImageElementId = computed(() => store.state.clipingImageElementId)
|
||||
const isCliping = computed(() => clipingImageElementId.value === props.elementInfo.id)
|
||||
|
||||
const scaleWidth = computed(() => props.elementInfo.width * canvasScale.value)
|
||||
const scaleHeight = computed(() => props.elementInfo.height * canvasScale.value)
|
||||
const { resizeHandlers, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
|
||||
|
||||
const clipShape = computed(() => {
|
||||
if(!props.elementInfo || !props.elementInfo.clip) return CLIPPATHS.rect
|
||||
const shape = props.elementInfo.clip.shape || ClipPathTypes.RECT
|
||||
|
||||
return CLIPPATHS[shape]
|
||||
})
|
||||
|
||||
const isCliping = computed(() => clipingImageElementId.value === props.elementInfo.id)
|
||||
|
||||
const clip = (data: ImageClipedEmitData) => {
|
||||
store.commit(MutationTypes.SET_CLIPING_IMAGE_ELEMENT_ID, '')
|
||||
|
||||
if(!data) return
|
||||
|
||||
const { range, position } = data
|
||||
const originClip = props.elementInfo.clip || {}
|
||||
|
||||
const _props = {
|
||||
clip: { ...originClip, range },
|
||||
left: props.elementInfo.left + position.left,
|
||||
top: props.elementInfo.top + position.top,
|
||||
width: props.elementInfo.width + position.width,
|
||||
height: props.elementInfo.height + position.height,
|
||||
}
|
||||
console.log(_props)
|
||||
}
|
||||
|
||||
return {
|
||||
clipShape,
|
||||
isCliping,
|
||||
scaleWidth,
|
||||
resizeHandlers,
|
||||
borderLines,
|
||||
isCliping,
|
||||
clip,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.clip-wrapper {
|
||||
position: relative;
|
||||
.image-element-operate.cliping {
|
||||
visibility: hidden;
|
||||
}
|
||||
</style>
|
@ -1,494 +1,496 @@
|
||||
<template>
|
||||
<div
|
||||
class="image-clip-handler"
|
||||
:style="clipWrapperPositionStyle"
|
||||
v-click-outside="clip"
|
||||
>
|
||||
<img
|
||||
class="bottom-img"
|
||||
:src="src"
|
||||
:draggable="false"
|
||||
alt=""
|
||||
:style="bottomImgPositionStyle"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="top-image-content"
|
||||
:style="{
|
||||
...topImgWrapperPositionStyle,
|
||||
clipPath,
|
||||
}"
|
||||
>
|
||||
<img
|
||||
class="top-img"
|
||||
:src="src"
|
||||
:draggable="false"
|
||||
alt=""
|
||||
:style="topImgPositionStyle"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="operate"
|
||||
:style="topImgWrapperPositionStyle"
|
||||
@mousedown.stop="$event => moveClipRange($event)"
|
||||
>
|
||||
<div
|
||||
:class="['clip-point', point]"
|
||||
v-for="point in ['t-l', 't-r', 'b-l', 'b-r']"
|
||||
:key="point"
|
||||
@mousedown.stop="$event => scaleClipRange($event, point)"
|
||||
>
|
||||
<SvgWrapper width="12" height="12" fill="#fff" stroke="#666">
|
||||
<path d="M 12 0 L 0 0 L 0 12 L 3 12 L 3 3 L 12 3 L 12 0 Z"></path>
|
||||
</SvgWrapper>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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'
|
||||
|
||||
type ScaleClipRangeType = 't-l' | 't-r' | 'b-l' | 'b-r'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'image-clip-handler',
|
||||
props: {
|
||||
src: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
clipData: {
|
||||
type: Object as PropType<ImageClipData>,
|
||||
},
|
||||
clipPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
top: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
left: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const store = useStore<State>()
|
||||
const canvasScale = computed(() => store.state.canvasScale)
|
||||
|
||||
const topImgWrapperPosition = reactive({
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
})
|
||||
const clipWrapperPositionStyle = reactive({
|
||||
top: '0',
|
||||
left: '0',
|
||||
})
|
||||
const isSettingClipRange = ref(false)
|
||||
const currentRange = ref<ImageClipDataRange | null>(null)
|
||||
|
||||
const getClipDataTransformInfo = () => {
|
||||
const [start, end] = props.clipData ? props.clipData.range : [[0, 0], [100, 100]]
|
||||
|
||||
const widthScale = (end[0] - start[0]) / 100
|
||||
const heightScale = (end[1] - start[1]) / 100
|
||||
const left = start[0] / widthScale
|
||||
const top = start[1] / heightScale
|
||||
|
||||
return { widthScale, heightScale, left, top }
|
||||
}
|
||||
|
||||
const imgPosition = computed(() => {
|
||||
const { widthScale, heightScale, left, top } = getClipDataTransformInfo()
|
||||
return {
|
||||
left: -left,
|
||||
top: -top,
|
||||
width: 100 / widthScale,
|
||||
height: 100 / heightScale,
|
||||
}
|
||||
})
|
||||
|
||||
const bottomImgPositionStyle = computed(() => {
|
||||
return {
|
||||
top: imgPosition.value.top + '%',
|
||||
left: imgPosition.value.left + '%',
|
||||
width: imgPosition.value.width + '%',
|
||||
height: imgPosition.value.height + '%',
|
||||
}
|
||||
})
|
||||
|
||||
const topImgWrapperPositionStyle = computed(() => {
|
||||
return {
|
||||
top: topImgWrapperPosition.top + '%',
|
||||
left: topImgWrapperPosition.left + '%',
|
||||
width: topImgWrapperPosition.width + '%',
|
||||
height: topImgWrapperPosition.height + '%',
|
||||
}
|
||||
})
|
||||
|
||||
const topImgPositionStyle = computed(() => {
|
||||
const bottomWidth = imgPosition.value.width
|
||||
const bottomHeight = imgPosition.value.height
|
||||
|
||||
const topLeft = topImgWrapperPosition.left
|
||||
const topTop = topImgWrapperPosition.top
|
||||
const topWidth = topImgWrapperPosition.width
|
||||
const topHeight = topImgWrapperPosition.height
|
||||
|
||||
return {
|
||||
left: -topLeft * (100 / topWidth) + '%',
|
||||
top: -topTop * (100 / topHeight) + '%',
|
||||
width: bottomWidth / topWidth * 100 + '%',
|
||||
height: bottomHeight / topHeight * 100 + '%',
|
||||
}
|
||||
})
|
||||
|
||||
const initClipPosition = () => {
|
||||
const { left, top } = getClipDataTransformInfo()
|
||||
topImgWrapperPosition.left = left
|
||||
topImgWrapperPosition.top = top
|
||||
topImgWrapperPosition.width = 100
|
||||
topImgWrapperPosition.height = 100
|
||||
|
||||
clipWrapperPositionStyle.top = -top + '%'
|
||||
clipWrapperPositionStyle.left = -left + '%'
|
||||
}
|
||||
|
||||
const clip = () => {
|
||||
if(isSettingClipRange.value || !currentRange.value) {
|
||||
emit('clip', null)
|
||||
return
|
||||
}
|
||||
|
||||
const { left, top } = getClipDataTransformInfo()
|
||||
|
||||
const position = {
|
||||
left: (topImgWrapperPosition.left - left) / 100 * props.width,
|
||||
top: (topImgWrapperPosition.top - top) / 100 * props.height,
|
||||
width: (topImgWrapperPosition.width - 100) / 100 * props.width,
|
||||
height: (topImgWrapperPosition.height - 100) / 100 * props.height,
|
||||
}
|
||||
|
||||
const clipedEmitData: ImageClipedEmitData = {
|
||||
range: currentRange.value,
|
||||
position,
|
||||
}
|
||||
emit('clip', clipedEmitData)
|
||||
}
|
||||
|
||||
const keyboardClip = (e: KeyboardEvent) => {
|
||||
const key = e.key.toUpperCase()
|
||||
if(key === KEYS.ENTER) clip()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initClipPosition()
|
||||
document.addEventListener('keydown', keyboardClip)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('keydown', keyboardClip)
|
||||
})
|
||||
|
||||
const getRange = () => {
|
||||
const retPosition = {
|
||||
left: parseInt(topImgPositionStyle.value.left),
|
||||
top: parseInt(topImgPositionStyle.value.top),
|
||||
width: parseInt(topImgPositionStyle.value.width),
|
||||
height: parseInt(topImgPositionStyle.value.height),
|
||||
}
|
||||
|
||||
const widthScale = 100 / retPosition.width
|
||||
const heightScale = 100 / retPosition.height
|
||||
|
||||
const start: [number, number] = [
|
||||
-retPosition.left * widthScale,
|
||||
-retPosition.top * heightScale,
|
||||
]
|
||||
const end: [number, number] = [
|
||||
widthScale * 100 + start[0],
|
||||
heightScale * 100 + start[1],
|
||||
]
|
||||
|
||||
currentRange.value = [start, end]
|
||||
}
|
||||
|
||||
const moveClipRange = (e: MouseEvent) => {
|
||||
isSettingClipRange.value = true
|
||||
let isMouseDown = true
|
||||
|
||||
const startPageX = e.pageX
|
||||
const startPageY = e.pageY
|
||||
const bottomPosition = imgPosition.value
|
||||
const originPositopn = {
|
||||
left: topImgWrapperPosition.left,
|
||||
top: topImgWrapperPosition.top,
|
||||
width: topImgWrapperPosition.width,
|
||||
height: topImgWrapperPosition.height,
|
||||
}
|
||||
|
||||
document.onmousemove = e => {
|
||||
if(!isMouseDown) return
|
||||
|
||||
const currentPageX = e.pageX
|
||||
const currentPageY = e.pageY
|
||||
|
||||
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
|
||||
|
||||
// 范围限制
|
||||
if(targetLeft < 0) targetLeft = 0
|
||||
else if(targetLeft + originPositopn.width > bottomPosition.width) {
|
||||
targetLeft = bottomPosition.width - originPositopn.width
|
||||
}
|
||||
if(targetTop < 0) targetTop = 0
|
||||
else if(targetTop + originPositopn.height > bottomPosition.height) {
|
||||
targetTop = bottomPosition.height - originPositopn.height
|
||||
}
|
||||
|
||||
topImgWrapperPosition.left = targetLeft
|
||||
topImgWrapperPosition.top = targetTop
|
||||
}
|
||||
|
||||
document.onmouseup = () => {
|
||||
isMouseDown = false
|
||||
document.onmousemove = null
|
||||
document.onmouseup = null
|
||||
|
||||
getRange()
|
||||
|
||||
setTimeout(() => {
|
||||
isSettingClipRange.value = false
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
const scaleClipRange = (e: MouseEvent, type: ScaleClipRangeType) => {
|
||||
isSettingClipRange.value = true
|
||||
let isMouseDown = true
|
||||
|
||||
const minWidth = 32 / props.width * 100
|
||||
const minHeight = 32 / props.height * 100
|
||||
|
||||
const startPageX = e.pageX
|
||||
const startPageY = e.pageY
|
||||
const bottomPosition = imgPosition.value
|
||||
const originPositopn = {
|
||||
left: topImgWrapperPosition.left,
|
||||
top: topImgWrapperPosition.top,
|
||||
width: topImgWrapperPosition.width,
|
||||
height: topImgWrapperPosition.height,
|
||||
}
|
||||
|
||||
document.onmousemove = e => {
|
||||
if(!isMouseDown) return
|
||||
|
||||
const currentPageX = e.pageX
|
||||
const currentPageY = e.pageY
|
||||
|
||||
let moveX = (currentPageX - startPageX) / canvasScale.value / props.width * 100
|
||||
let moveY = (currentPageY - startPageY) / canvasScale.value / props.height * 100
|
||||
|
||||
let targetLeft, targetTop, targetWidth, targetHeight
|
||||
|
||||
// 根据不同缩放点,计算目标大小和位置,同时做大小和范围的限制
|
||||
if(type === 't-l') {
|
||||
if(originPositopn.left + moveX < 0) {
|
||||
moveX = -originPositopn.left
|
||||
}
|
||||
if(originPositopn.top + moveY < 0) {
|
||||
moveY = -originPositopn.top
|
||||
}
|
||||
if(originPositopn.width - moveX < minWidth) {
|
||||
moveX = originPositopn.width - minWidth
|
||||
}
|
||||
if(originPositopn.height - moveY < minHeight) {
|
||||
moveY = originPositopn.height - minHeight
|
||||
}
|
||||
targetWidth = originPositopn.width - moveX
|
||||
targetHeight = originPositopn.height - moveY
|
||||
targetLeft = originPositopn.left + moveX
|
||||
targetTop = originPositopn.top + moveY
|
||||
}
|
||||
else if(type === 't-r') {
|
||||
if(originPositopn.left + originPositopn.width + moveX > bottomPosition.width) {
|
||||
moveX = bottomPosition.width - (originPositopn.left + originPositopn.width)
|
||||
}
|
||||
if(originPositopn.top + moveY < 0) {
|
||||
moveY = -originPositopn.top
|
||||
}
|
||||
if(originPositopn.width + moveX < minWidth) {
|
||||
moveX = minWidth - originPositopn.width
|
||||
}
|
||||
if(originPositopn.height - moveY < minHeight) {
|
||||
moveY = originPositopn.height - minHeight
|
||||
}
|
||||
targetWidth = originPositopn.width + moveX
|
||||
targetHeight = originPositopn.height - moveY
|
||||
targetLeft = originPositopn.left
|
||||
targetTop = originPositopn.top + moveY
|
||||
}
|
||||
else if(type === 'b-l') {
|
||||
if(originPositopn.left + moveX < 0) {
|
||||
moveX = -originPositopn.left
|
||||
}
|
||||
if(originPositopn.top + originPositopn.height + moveY > bottomPosition.height) {
|
||||
moveY = bottomPosition.height - (originPositopn.top + originPositopn.height)
|
||||
}
|
||||
if(originPositopn.width - moveX < minWidth) {
|
||||
moveX = originPositopn.width - minWidth
|
||||
}
|
||||
if(originPositopn.height + moveY < minHeight) {
|
||||
moveY = minHeight - originPositopn.height
|
||||
}
|
||||
targetWidth = originPositopn.width - moveX
|
||||
targetHeight = originPositopn.height + moveY
|
||||
targetLeft = originPositopn.left + moveX
|
||||
targetTop = originPositopn.top
|
||||
}
|
||||
else {
|
||||
if(originPositopn.left + originPositopn.width + moveX > bottomPosition.width) {
|
||||
moveX = bottomPosition.width - (originPositopn.left + originPositopn.width)
|
||||
}
|
||||
if(originPositopn.top + originPositopn.height + moveY > bottomPosition.height) {
|
||||
moveY = bottomPosition.height - (originPositopn.top + originPositopn.height)
|
||||
}
|
||||
if(originPositopn.width + moveX < minWidth) {
|
||||
moveX = minWidth - originPositopn.width
|
||||
}
|
||||
if(originPositopn.height + moveY < minHeight) {
|
||||
moveY = minHeight - originPositopn.height
|
||||
}
|
||||
targetWidth = originPositopn.width + moveX
|
||||
targetHeight = originPositopn.height + moveY
|
||||
targetLeft = originPositopn.left
|
||||
targetTop = originPositopn.top
|
||||
}
|
||||
|
||||
topImgWrapperPosition.left = targetLeft
|
||||
topImgWrapperPosition.top = targetTop
|
||||
topImgWrapperPosition.width = targetWidth
|
||||
topImgWrapperPosition.height = targetHeight
|
||||
}
|
||||
|
||||
document.onmouseup = () => {
|
||||
isMouseDown = false
|
||||
document.onmousemove = null
|
||||
document.onmouseup = null
|
||||
|
||||
getRange()
|
||||
|
||||
setTimeout(() => isSettingClipRange.value = false, 0)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
clipWrapperPositionStyle,
|
||||
bottomImgPositionStyle,
|
||||
topImgWrapperPositionStyle,
|
||||
topImgPositionStyle,
|
||||
clip,
|
||||
moveClipRange,
|
||||
scaleClipRange,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.image-clip-handler {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
.bottom-img {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.top-image-content {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.operate {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.clip-point {
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
left: 0;
|
||||
top: 0;
|
||||
transform-origin: 0 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
svg {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
&.t-l {
|
||||
cursor: nwse-resize;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
&.t-r {
|
||||
cursor: nesw-resize;
|
||||
left: 100%;
|
||||
top: 0;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
&.b-l {
|
||||
cursor: nesw-resize;
|
||||
left: 0;
|
||||
top: 100%;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
&.b-r {
|
||||
cursor: nwse-resize;
|
||||
left: 100%;
|
||||
top: 100%;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
<template>
|
||||
<div
|
||||
class="image-clip-handler"
|
||||
:style="clipWrapperPositionStyle"
|
||||
v-click-outside="handleClip"
|
||||
>
|
||||
<img
|
||||
class="bottom-img"
|
||||
:src="src"
|
||||
:draggable="false"
|
||||
alt=""
|
||||
:style="bottomImgPositionStyle"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="top-image-content"
|
||||
:style="{
|
||||
...topImgWrapperPositionStyle,
|
||||
clipPath,
|
||||
}"
|
||||
>
|
||||
<img
|
||||
class="top-img"
|
||||
:src="src"
|
||||
:draggable="false"
|
||||
alt=""
|
||||
:style="topImgPositionStyle"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="operate"
|
||||
:style="topImgWrapperPositionStyle"
|
||||
@mousedown.stop="$event => moveClipRange($event)"
|
||||
>
|
||||
<div
|
||||
:class="['clip-point', point]"
|
||||
v-for="point in ['t-l', 't-r', 'b-l', 'b-r']"
|
||||
:key="point"
|
||||
@mousedown.stop="$event => scaleClipRange($event, point)"
|
||||
>
|
||||
<SvgWrapper width="12" height="12" fill="#fff" stroke="#666">
|
||||
<path d="M 12 0 L 0 0 L 0 12 L 3 12 L 3 3 L 12 3 L 12 0 Z"></path>
|
||||
</SvgWrapper>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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'
|
||||
|
||||
type ScaleClipRangeType = 't-l' | 't-r' | 'b-l' | 'b-r'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'image-clip-handler',
|
||||
props: {
|
||||
src: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
clipData: {
|
||||
type: Object as PropType<ImageClipData>,
|
||||
},
|
||||
clipPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
top: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
left: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const store = useStore<State>()
|
||||
const canvasScale = computed(() => store.state.canvasScale)
|
||||
|
||||
const topImgWrapperPosition = reactive({
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
})
|
||||
const clipWrapperPositionStyle = reactive({
|
||||
top: '0',
|
||||
left: '0',
|
||||
})
|
||||
const isSettingClipRange = ref(false)
|
||||
const currentRange = ref<ImageClipDataRange | null>(null)
|
||||
|
||||
const getClipDataTransformInfo = () => {
|
||||
const [start, end] = props.clipData ? props.clipData.range : [[0, 0], [100, 100]]
|
||||
|
||||
const widthScale = (end[0] - start[0]) / 100
|
||||
const heightScale = (end[1] - start[1]) / 100
|
||||
const left = start[0] / widthScale
|
||||
const top = start[1] / heightScale
|
||||
|
||||
return { widthScale, heightScale, left, top }
|
||||
}
|
||||
|
||||
const imgPosition = computed(() => {
|
||||
const { widthScale, heightScale, left, top } = getClipDataTransformInfo()
|
||||
return {
|
||||
left: -left,
|
||||
top: -top,
|
||||
width: 100 / widthScale,
|
||||
height: 100 / heightScale,
|
||||
}
|
||||
})
|
||||
|
||||
const bottomImgPositionStyle = computed(() => {
|
||||
return {
|
||||
top: imgPosition.value.top + '%',
|
||||
left: imgPosition.value.left + '%',
|
||||
width: imgPosition.value.width + '%',
|
||||
height: imgPosition.value.height + '%',
|
||||
}
|
||||
})
|
||||
|
||||
const topImgWrapperPositionStyle = computed(() => {
|
||||
return {
|
||||
top: topImgWrapperPosition.top + '%',
|
||||
left: topImgWrapperPosition.left + '%',
|
||||
width: topImgWrapperPosition.width + '%',
|
||||
height: topImgWrapperPosition.height + '%',
|
||||
}
|
||||
})
|
||||
|
||||
const topImgPositionStyle = computed(() => {
|
||||
const bottomWidth = imgPosition.value.width
|
||||
const bottomHeight = imgPosition.value.height
|
||||
|
||||
const topLeft = topImgWrapperPosition.left
|
||||
const topTop = topImgWrapperPosition.top
|
||||
const topWidth = topImgWrapperPosition.width
|
||||
const topHeight = topImgWrapperPosition.height
|
||||
|
||||
return {
|
||||
left: -topLeft * (100 / topWidth) + '%',
|
||||
top: -topTop * (100 / topHeight) + '%',
|
||||
width: bottomWidth / topWidth * 100 + '%',
|
||||
height: bottomHeight / topHeight * 100 + '%',
|
||||
}
|
||||
})
|
||||
|
||||
const initClipPosition = () => {
|
||||
const { left, top } = getClipDataTransformInfo()
|
||||
topImgWrapperPosition.left = left
|
||||
topImgWrapperPosition.top = top
|
||||
topImgWrapperPosition.width = 100
|
||||
topImgWrapperPosition.height = 100
|
||||
|
||||
clipWrapperPositionStyle.top = -top + '%'
|
||||
clipWrapperPositionStyle.left = -left + '%'
|
||||
}
|
||||
|
||||
const handleClip = () => {
|
||||
if(isSettingClipRange.value) return
|
||||
|
||||
if(!currentRange.value) {
|
||||
emit('clip', null)
|
||||
return
|
||||
}
|
||||
|
||||
const { left, top } = getClipDataTransformInfo()
|
||||
|
||||
const position = {
|
||||
left: (topImgWrapperPosition.left - left) / 100 * props.width,
|
||||
top: (topImgWrapperPosition.top - top) / 100 * props.height,
|
||||
width: (topImgWrapperPosition.width - 100) / 100 * props.width,
|
||||
height: (topImgWrapperPosition.height - 100) / 100 * props.height,
|
||||
}
|
||||
|
||||
const clipedEmitData: ImageClipedEmitData = {
|
||||
range: currentRange.value,
|
||||
position,
|
||||
}
|
||||
emit('clip', clipedEmitData)
|
||||
}
|
||||
|
||||
const keyboardClip = (e: KeyboardEvent) => {
|
||||
const key = e.key.toUpperCase()
|
||||
if(key === KEYS.ENTER) handleClip()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initClipPosition()
|
||||
document.addEventListener('keydown', keyboardClip)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('keydown', keyboardClip)
|
||||
})
|
||||
|
||||
const getRange = () => {
|
||||
const retPosition = {
|
||||
left: parseInt(topImgPositionStyle.value.left),
|
||||
top: parseInt(topImgPositionStyle.value.top),
|
||||
width: parseInt(topImgPositionStyle.value.width),
|
||||
height: parseInt(topImgPositionStyle.value.height),
|
||||
}
|
||||
|
||||
const widthScale = 100 / retPosition.width
|
||||
const heightScale = 100 / retPosition.height
|
||||
|
||||
const start: [number, number] = [
|
||||
-retPosition.left * widthScale,
|
||||
-retPosition.top * heightScale,
|
||||
]
|
||||
const end: [number, number] = [
|
||||
widthScale * 100 + start[0],
|
||||
heightScale * 100 + start[1],
|
||||
]
|
||||
|
||||
currentRange.value = [start, end]
|
||||
}
|
||||
|
||||
const moveClipRange = (e: MouseEvent) => {
|
||||
isSettingClipRange.value = true
|
||||
let isMouseDown = true
|
||||
|
||||
const startPageX = e.pageX
|
||||
const startPageY = e.pageY
|
||||
const bottomPosition = imgPosition.value
|
||||
const originPositopn = {
|
||||
left: topImgWrapperPosition.left,
|
||||
top: topImgWrapperPosition.top,
|
||||
width: topImgWrapperPosition.width,
|
||||
height: topImgWrapperPosition.height,
|
||||
}
|
||||
|
||||
document.onmousemove = e => {
|
||||
if(!isMouseDown) return
|
||||
|
||||
const currentPageX = e.pageX
|
||||
const currentPageY = e.pageY
|
||||
|
||||
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
|
||||
|
||||
// 范围限制
|
||||
if(targetLeft < 0) targetLeft = 0
|
||||
else if(targetLeft + originPositopn.width > bottomPosition.width) {
|
||||
targetLeft = bottomPosition.width - originPositopn.width
|
||||
}
|
||||
if(targetTop < 0) targetTop = 0
|
||||
else if(targetTop + originPositopn.height > bottomPosition.height) {
|
||||
targetTop = bottomPosition.height - originPositopn.height
|
||||
}
|
||||
|
||||
topImgWrapperPosition.left = targetLeft
|
||||
topImgWrapperPosition.top = targetTop
|
||||
}
|
||||
|
||||
document.onmouseup = () => {
|
||||
isMouseDown = false
|
||||
document.onmousemove = null
|
||||
document.onmouseup = null
|
||||
|
||||
getRange()
|
||||
|
||||
setTimeout(() => {
|
||||
isSettingClipRange.value = false
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
const scaleClipRange = (e: MouseEvent, type: ScaleClipRangeType) => {
|
||||
isSettingClipRange.value = true
|
||||
let isMouseDown = true
|
||||
|
||||
const minWidth = 32 / props.width * 100
|
||||
const minHeight = 32 / props.height * 100
|
||||
|
||||
const startPageX = e.pageX
|
||||
const startPageY = e.pageY
|
||||
const bottomPosition = imgPosition.value
|
||||
const originPositopn = {
|
||||
left: topImgWrapperPosition.left,
|
||||
top: topImgWrapperPosition.top,
|
||||
width: topImgWrapperPosition.width,
|
||||
height: topImgWrapperPosition.height,
|
||||
}
|
||||
|
||||
document.onmousemove = e => {
|
||||
if(!isMouseDown) return
|
||||
|
||||
const currentPageX = e.pageX
|
||||
const currentPageY = e.pageY
|
||||
|
||||
let moveX = (currentPageX - startPageX) / canvasScale.value / props.width * 100
|
||||
let moveY = (currentPageY - startPageY) / canvasScale.value / props.height * 100
|
||||
|
||||
let targetLeft, targetTop, targetWidth, targetHeight
|
||||
|
||||
// 根据不同缩放点,计算目标大小和位置,同时做大小和范围的限制
|
||||
if(type === 't-l') {
|
||||
if(originPositopn.left + moveX < 0) {
|
||||
moveX = -originPositopn.left
|
||||
}
|
||||
if(originPositopn.top + moveY < 0) {
|
||||
moveY = -originPositopn.top
|
||||
}
|
||||
if(originPositopn.width - moveX < minWidth) {
|
||||
moveX = originPositopn.width - minWidth
|
||||
}
|
||||
if(originPositopn.height - moveY < minHeight) {
|
||||
moveY = originPositopn.height - minHeight
|
||||
}
|
||||
targetWidth = originPositopn.width - moveX
|
||||
targetHeight = originPositopn.height - moveY
|
||||
targetLeft = originPositopn.left + moveX
|
||||
targetTop = originPositopn.top + moveY
|
||||
}
|
||||
else if(type === 't-r') {
|
||||
if(originPositopn.left + originPositopn.width + moveX > bottomPosition.width) {
|
||||
moveX = bottomPosition.width - (originPositopn.left + originPositopn.width)
|
||||
}
|
||||
if(originPositopn.top + moveY < 0) {
|
||||
moveY = -originPositopn.top
|
||||
}
|
||||
if(originPositopn.width + moveX < minWidth) {
|
||||
moveX = minWidth - originPositopn.width
|
||||
}
|
||||
if(originPositopn.height - moveY < minHeight) {
|
||||
moveY = originPositopn.height - minHeight
|
||||
}
|
||||
targetWidth = originPositopn.width + moveX
|
||||
targetHeight = originPositopn.height - moveY
|
||||
targetLeft = originPositopn.left
|
||||
targetTop = originPositopn.top + moveY
|
||||
}
|
||||
else if(type === 'b-l') {
|
||||
if(originPositopn.left + moveX < 0) {
|
||||
moveX = -originPositopn.left
|
||||
}
|
||||
if(originPositopn.top + originPositopn.height + moveY > bottomPosition.height) {
|
||||
moveY = bottomPosition.height - (originPositopn.top + originPositopn.height)
|
||||
}
|
||||
if(originPositopn.width - moveX < minWidth) {
|
||||
moveX = originPositopn.width - minWidth
|
||||
}
|
||||
if(originPositopn.height + moveY < minHeight) {
|
||||
moveY = minHeight - originPositopn.height
|
||||
}
|
||||
targetWidth = originPositopn.width - moveX
|
||||
targetHeight = originPositopn.height + moveY
|
||||
targetLeft = originPositopn.left + moveX
|
||||
targetTop = originPositopn.top
|
||||
}
|
||||
else {
|
||||
if(originPositopn.left + originPositopn.width + moveX > bottomPosition.width) {
|
||||
moveX = bottomPosition.width - (originPositopn.left + originPositopn.width)
|
||||
}
|
||||
if(originPositopn.top + originPositopn.height + moveY > bottomPosition.height) {
|
||||
moveY = bottomPosition.height - (originPositopn.top + originPositopn.height)
|
||||
}
|
||||
if(originPositopn.width + moveX < minWidth) {
|
||||
moveX = minWidth - originPositopn.width
|
||||
}
|
||||
if(originPositopn.height + moveY < minHeight) {
|
||||
moveY = minHeight - originPositopn.height
|
||||
}
|
||||
targetWidth = originPositopn.width + moveX
|
||||
targetHeight = originPositopn.height + moveY
|
||||
targetLeft = originPositopn.left
|
||||
targetTop = originPositopn.top
|
||||
}
|
||||
|
||||
topImgWrapperPosition.left = targetLeft
|
||||
topImgWrapperPosition.top = targetTop
|
||||
topImgWrapperPosition.width = targetWidth
|
||||
topImgWrapperPosition.height = targetHeight
|
||||
}
|
||||
|
||||
document.onmouseup = () => {
|
||||
isMouseDown = false
|
||||
document.onmousemove = null
|
||||
document.onmouseup = null
|
||||
|
||||
getRange()
|
||||
|
||||
setTimeout(() => isSettingClipRange.value = false, 0)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
clipWrapperPositionStyle,
|
||||
bottomImgPositionStyle,
|
||||
topImgWrapperPositionStyle,
|
||||
topImgPositionStyle,
|
||||
handleClip,
|
||||
moveClipRange,
|
||||
scaleClipRange,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.image-clip-handler {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
.bottom-img {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.top-image-content {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.operate {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.clip-point {
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
left: 0;
|
||||
top: 0;
|
||||
transform-origin: 0 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
svg {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
&.t-l {
|
||||
cursor: nwse-resize;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
&.t-r {
|
||||
cursor: nesw-resize;
|
||||
left: 100%;
|
||||
top: 0;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
&.b-l {
|
||||
cursor: nesw-resize;
|
||||
left: 0;
|
||||
top: 100%;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
&.b-r {
|
||||
cursor: nwse-resize;
|
||||
left: 100%;
|
||||
top: 100%;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,10 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
class="editable-element-image"
|
||||
:class="{
|
||||
'lock': elementInfo.lock,
|
||||
'cliping': clipingImageElementId === elementInfo.id,
|
||||
}"
|
||||
:class="{ 'lock': elementInfo.lock }"
|
||||
:style="{
|
||||
top: elementInfo.top + 'px',
|
||||
left: elementInfo.left + 'px',
|
||||
@ -14,8 +11,20 @@
|
||||
}"
|
||||
@mousedown="$event => handleSelectElement($event)"
|
||||
>
|
||||
<ImageClipHandler
|
||||
v-if="isCliping"
|
||||
: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
|
||||
class="element-content"
|
||||
v-else
|
||||
v-contextmenu="contextmenus"
|
||||
:style="{
|
||||
filter: shadowStyle ? `drop-shadow(${shadowStyle})` : '',
|
||||
@ -64,7 +73,7 @@
|
||||
<script lang="ts">
|
||||
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 { ContextmenuItem } from '@/components/Contextmenu/types'
|
||||
import { CLIPPATHS, ClipPathTypes } from '@/configs/imageClip'
|
||||
@ -73,7 +82,8 @@ import useElementShadow from '@/views/components/element/hooks/useElementShadow'
|
||||
import ImageRectOutline from './ImageRectOutline.vue'
|
||||
import ImageEllipseOutline from './ImageEllipseOutline.vue'
|
||||
import ImagePolygonOutline from './ImagePolygonOutline.vue'
|
||||
|
||||
import ImageClipHandler from './ImageClipHandler.vue'
|
||||
import { ImageClipedEmitData } from '@/types/edit'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'editable-element-image',
|
||||
@ -81,6 +91,7 @@ export default defineComponent({
|
||||
ImageRectOutline,
|
||||
ImageEllipseOutline,
|
||||
ImagePolygonOutline,
|
||||
ImageClipHandler,
|
||||
},
|
||||
props: {
|
||||
elementInfo: {
|
||||
@ -98,6 +109,7 @@ export default defineComponent({
|
||||
setup(props) {
|
||||
const store = useStore<State>()
|
||||
const clipingImageElementId = computed(() => store.state.clipingImageElementId)
|
||||
const isCliping = computed(() => clipingImageElementId.value === props.elementInfo.id)
|
||||
|
||||
const shadow = computed(() => props.elementInfo.shadow)
|
||||
const { shadowStyle } = useElementShadow(shadow)
|
||||
@ -157,7 +169,27 @@ export default defineComponent({
|
||||
return ''
|
||||
})
|
||||
|
||||
const clip = (data: ImageClipedEmitData) => {
|
||||
store.commit(MutationTypes.SET_CLIPING_IMAGE_ELEMENT_ID, '')
|
||||
|
||||
if(!data) return
|
||||
|
||||
const { range, position } = data
|
||||
const originClip = props.elementInfo.clip || {}
|
||||
|
||||
const _props = {
|
||||
clip: { ...originClip, range },
|
||||
left: props.elementInfo.left + position.left,
|
||||
top: props.elementInfo.top + position.top,
|
||||
width: props.elementInfo.width + position.width,
|
||||
height: props.elementInfo.height + position.height,
|
||||
}
|
||||
store.commit(MutationTypes.UPDATE_ELEMENT, { id: props.elementInfo.id, props: _props })
|
||||
}
|
||||
|
||||
return {
|
||||
isCliping,
|
||||
clip,
|
||||
clipingImageElementId,
|
||||
shadowStyle,
|
||||
handleSelectElement,
|
||||
@ -177,10 +209,6 @@ export default defineComponent({
|
||||
&.lock .element-content {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&.cliping {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.element-content {
|
||||
|
Loading…
x
Reference in New Issue
Block a user