Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/FiDA_Front
This commit is contained in:
@@ -1,79 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="my-input">
|
|
||||||
<span class="decorate"></span>
|
|
||||||
<span v-show="icon" class="icon">
|
|
||||||
<svg-icon :name="icon" :size="iconSize" size-unit="px" />
|
|
||||||
</span>
|
|
||||||
<span v-show="before" class="before">{{ before }}</span>
|
|
||||||
<input v-bind="attrs" :value="modelValue" @input="onInput" @copy.stop @keydown.stop />
|
|
||||||
<span v-show="after" class="after">{{ after }}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, useAttrs, watch } from 'vue'
|
|
||||||
const props = defineProps({
|
|
||||||
modelValue: { type: [String, Number] },
|
|
||||||
icon: { default: '', type: String },
|
|
||||||
iconSize: { default: '10', type: [Number, String] },
|
|
||||||
before: { default: '', type: String },
|
|
||||||
after: { default: '', type: String }
|
|
||||||
})
|
|
||||||
const attrs = useAttrs()
|
|
||||||
const emit = defineEmits(['update:modelValue', 'input'])
|
|
||||||
const onInput = (e) => {
|
|
||||||
var value = e.target.value
|
|
||||||
if (attrs.type === 'number') value = Number(value)
|
|
||||||
emit('update:modelValue', value)
|
|
||||||
emit('input', value)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<style scoped lang="less">
|
|
||||||
.my-input {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
border: 1px solid rgba(230, 230, 231, 1);
|
|
||||||
border-radius: 1.7px;
|
|
||||||
height: 17px;
|
|
||||||
padding: 0 4px 0 2px;
|
|
||||||
> .decorate {
|
|
||||||
width: 2px;
|
|
||||||
background-color: rgba(230, 230, 231, 1);
|
|
||||||
border-radius: 3px;
|
|
||||||
height: 85%;
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
> .iconfont {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #000;
|
|
||||||
margin-right: 2px;
|
|
||||||
}
|
|
||||||
> .before {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #000;
|
|
||||||
margin-right: 2px;
|
|
||||||
}
|
|
||||||
> .after {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #000;
|
|
||||||
margin-left: 1px;
|
|
||||||
}
|
|
||||||
> input {
|
|
||||||
font-size: 12px;
|
|
||||||
width: 0;
|
|
||||||
flex: 1;
|
|
||||||
text-align: right;
|
|
||||||
outline: none;
|
|
||||||
border: none;
|
|
||||||
background-color: transparent;
|
|
||||||
padding: 0;
|
|
||||||
-moz-appearance: textfield; /* Firefox */
|
|
||||||
&::-webkit-outer-spin-button,
|
|
||||||
&::-webkit-inner-spin-button {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="my-select">
|
|
||||||
<el-select :model-value="modelValue" @change="onChange" v-bind="attrs">
|
|
||||||
<el-option v-for="v in list" :key="v.value" :label="v.label" :value="v.value" />
|
|
||||||
</el-select>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, useAttrs, watch } from 'vue'
|
|
||||||
const props = defineProps({
|
|
||||||
modelValue: { required: true },
|
|
||||||
list: { default: () => [], type: [Array, Object] }
|
|
||||||
})
|
|
||||||
const attrs = useAttrs()
|
|
||||||
const emit = defineEmits(['update:modelValue', 'change'])
|
|
||||||
const onChange = (value) => {
|
|
||||||
emit('update:modelValue', value)
|
|
||||||
emit('change', value)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<style scoped lang="less">
|
|
||||||
.my-select {
|
|
||||||
&:deep(.el-select) {
|
|
||||||
--el-select-input-font-size: 12px;
|
|
||||||
.el-select__wrapper {
|
|
||||||
font-size: 12px;
|
|
||||||
min-height: 0;
|
|
||||||
height: 28px;
|
|
||||||
padding: 0 8px;
|
|
||||||
}
|
|
||||||
.el-select__selected-item,
|
|
||||||
.el-select__input-wrapper,
|
|
||||||
.el-select__placeholder {
|
|
||||||
line-height: normal;
|
|
||||||
}
|
|
||||||
.el-select__input {
|
|
||||||
height: 24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.el-popper {
|
|
||||||
.el-select-dropdown {
|
|
||||||
li {
|
|
||||||
padding-left: 8px;
|
|
||||||
height: 30px;
|
|
||||||
line-height: 30px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="my-textarea">
|
|
||||||
<textarea
|
|
||||||
:placeholder="placeholder"
|
|
||||||
:value="modelValue"
|
|
||||||
@input="onInput"
|
|
||||||
@change="onChange"
|
|
||||||
@copy.stop
|
|
||||||
@keydown.stop
|
|
||||||
></textarea>
|
|
||||||
<div class="bths">
|
|
||||||
<button><svg-icon name="mobang" size="10" size-unit="px" /></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, ref, markRaw, onMounted } from 'vue'
|
|
||||||
const emit = defineEmits(['update:modelValue', 'input', 'change'])
|
|
||||||
const props = defineProps({
|
|
||||||
modelValue: { type: String },
|
|
||||||
placeholder: {
|
|
||||||
type: String,
|
|
||||||
default: 'Enter the scene you want to describe...'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const onInput = (e) => {
|
|
||||||
const value = e.target.value
|
|
||||||
emit('update:modelValue', value)
|
|
||||||
emit('input', value)
|
|
||||||
}
|
|
||||||
const onChange = (e) => {
|
|
||||||
emit('change', e.target.value)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
.my-textarea {
|
|
||||||
width: 100%;
|
|
||||||
height: 115px;
|
|
||||||
border: 1px solid #e4e4e7;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 10px 5px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
> textarea {
|
|
||||||
padding: 0 5px;
|
|
||||||
width: 100%;
|
|
||||||
flex: 1;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
resize: none;
|
|
||||||
font-family: Medium;
|
|
||||||
font-size: 10px;
|
|
||||||
color: #333;
|
|
||||||
&::placeholder {
|
|
||||||
color: #c9c9c9;
|
|
||||||
}
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 4px;
|
|
||||||
}
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
border-radius: 4px;
|
|
||||||
background: rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
border-radius: 4px;
|
|
||||||
background: rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
> .bths {
|
|
||||||
padding: 5px 5px 0;
|
|
||||||
> button {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
padding: 0;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
border: 1px solid #e4e4e7;
|
|
||||||
&:active {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,217 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="offset-tool">
|
|
||||||
<div class="input" v-show="showInput">
|
|
||||||
<my-input v-model="left" type="number" before="X" after="%" :min="-100" :max="100" />
|
|
||||||
<my-input v-model="top" type="number" before="Y" after="%" :min="-100" :max="100" />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="dish"
|
|
||||||
@mousedown="mousedown"
|
|
||||||
@touchstart="mousedown"
|
|
||||||
ref="dishRef"
|
|
||||||
v-show="showDish"
|
|
||||||
>
|
|
||||||
<img src="/src/assets/images/icon/xyz.png" />
|
|
||||||
<span class="ball" :style="ballStyle"></span>
|
|
||||||
<span class="tip x">X: {{ left }}%</span>
|
|
||||||
<span class="tip y">Y: {{ top }}%</span>
|
|
||||||
<span class="line x"></span>
|
|
||||||
<span class="line y"></span>
|
|
||||||
<span class="line z" :style="lineZStyle"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, watch, computed } from 'vue'
|
|
||||||
import MyInput from './my-input.vue'
|
|
||||||
const props = defineProps({
|
|
||||||
modelValue: { type: Object as () => { x: number; y: number } },
|
|
||||||
showInput: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
showDish: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const emit = defineEmits(['update:modelValue', 'change', 'input'])
|
|
||||||
// 工具的实际坐标 -100 ~ 100
|
|
||||||
const top = ref(Math.round(props.modelValue.y))
|
|
||||||
const left = ref(Math.round(props.modelValue.x))
|
|
||||||
|
|
||||||
// 原点的坐标 0 ~ 100
|
|
||||||
const ballStyle = computed(() => ({
|
|
||||||
top: 50 + top.value / 2 + '%',
|
|
||||||
left: 50 + left.value / 2 + '%'
|
|
||||||
}))
|
|
||||||
watch(
|
|
||||||
() => props.modelValue.x,
|
|
||||||
(v) => (left.value = v)
|
|
||||||
)
|
|
||||||
watch(
|
|
||||||
() => props.modelValue.y,
|
|
||||||
(v) => (top.value = v)
|
|
||||||
)
|
|
||||||
const dishRef = ref<HTMLDivElement>()
|
|
||||||
const mousedown = (e: MouseEvent | TouchEvent) => {
|
|
||||||
if (!dishRef.value) return
|
|
||||||
const mousemove = (e: MouseEvent | TouchEvent) => {
|
|
||||||
if (!dishRef.value) return
|
|
||||||
const rect = dishRef.value.getBoundingClientRect()
|
|
||||||
const X = e.clientX || (e as TouchEvent).touches[0].clientX
|
|
||||||
const Y = e.clientY || (e as TouchEvent).touches[0].clientY
|
|
||||||
var x = ((X - rect.left) / rect.width) * 100
|
|
||||||
var y = ((Y - rect.top) / rect.height) * 100
|
|
||||||
if (x < 0) x = 0
|
|
||||||
if (x > 100) x = 100
|
|
||||||
if (y < 0) y = 0
|
|
||||||
if (y > 100) y = 100
|
|
||||||
left.value = Math.round((x - 50) * 2)
|
|
||||||
top.value = Math.round((y - 50) * 2)
|
|
||||||
onInput()
|
|
||||||
}
|
|
||||||
mousemove(e)
|
|
||||||
const mouseup = () => {
|
|
||||||
onChange()
|
|
||||||
document.removeEventListener('mousemove', mousemove)
|
|
||||||
document.removeEventListener('touchmove', mousemove)
|
|
||||||
document.removeEventListener('mouseup', mouseup)
|
|
||||||
document.removeEventListener('touchend', mouseup)
|
|
||||||
}
|
|
||||||
document.addEventListener('mousemove', mousemove)
|
|
||||||
document.addEventListener('touchmove', mousemove)
|
|
||||||
document.addEventListener('mouseup', mouseup)
|
|
||||||
document.addEventListener('touchend', mouseup)
|
|
||||||
}
|
|
||||||
const onInput = () => {
|
|
||||||
const value = {
|
|
||||||
x: left.value,
|
|
||||||
y: top.value
|
|
||||||
}
|
|
||||||
emit('update:modelValue', value)
|
|
||||||
emit('input', value)
|
|
||||||
}
|
|
||||||
var changeTime: any = null
|
|
||||||
const onChange = () => {
|
|
||||||
clearTimeout(changeTime)
|
|
||||||
changeTime = setTimeout(() => {
|
|
||||||
const value = {
|
|
||||||
x: left.value,
|
|
||||||
y: top.value
|
|
||||||
}
|
|
||||||
emit('update:modelValue', value)
|
|
||||||
emit('change', value)
|
|
||||||
}, 500)
|
|
||||||
}
|
|
||||||
const lineZStyle = computed(() => ({
|
|
||||||
'--rotateZ': calculateAngle(0, 0, left.value, top.value) + 'deg',
|
|
||||||
width: calculateDistance(0, 0, left.value, top.value) / 2 + '%'
|
|
||||||
}))
|
|
||||||
// 计算角度
|
|
||||||
function calculateAngle(x1: number, y1: number, x2: number, y2: number) {
|
|
||||||
const deltaX = x2 - x1
|
|
||||||
const deltaY = y1 - y2
|
|
||||||
let angle = Math.atan2(deltaX, deltaY) * (180 / Math.PI) - 90
|
|
||||||
return angle
|
|
||||||
}
|
|
||||||
// 计算距离
|
|
||||||
function calculateDistance(x1: number, y1: number, x2: number, y2: number) {
|
|
||||||
const deltaX = x2 - x1
|
|
||||||
const deltaY = y2 - y1
|
|
||||||
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY)
|
|
||||||
return distance
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
.offset-tool {
|
|
||||||
position: relative;
|
|
||||||
> .input {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
> * {
|
|
||||||
flex: 1;
|
|
||||||
margin-right: 10px;
|
|
||||||
&:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
> .dish {
|
|
||||||
width: 115px;
|
|
||||||
height: 115px;
|
|
||||||
border: 1px solid #eaeaea;
|
|
||||||
border-radius: 3.4px;
|
|
||||||
cursor: pointer;
|
|
||||||
position: relative;
|
|
||||||
background-color: #f6f6f6;
|
|
||||||
margin-top: 20px;
|
|
||||||
> * {
|
|
||||||
position: absolute;
|
|
||||||
pointer-events: none;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
> img {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
bottom: 3.5px;
|
|
||||||
right: 3.5px;
|
|
||||||
}
|
|
||||||
> .ball {
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
width: 8.5px;
|
|
||||||
height: 8.5px;
|
|
||||||
border: 1px solid #fff;
|
|
||||||
background-color: #333;
|
|
||||||
border-radius: 50%;
|
|
||||||
box-shadow: 0px 0.68px 0.17px 0px rgba(0, 0, 0, 0.26);
|
|
||||||
}
|
|
||||||
> .tip {
|
|
||||||
font-size: 8.5px;
|
|
||||||
color: #000;
|
|
||||||
line-height: 24px;
|
|
||||||
&.x {
|
|
||||||
top: 50%;
|
|
||||||
right: 0%;
|
|
||||||
transform: translate(100%, -50%);
|
|
||||||
padding-left: 6px;
|
|
||||||
}
|
|
||||||
&.y {
|
|
||||||
top: 0%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -100%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
> .line {
|
|
||||||
border-color: #d9d9d9;
|
|
||||||
border-style: dashed;
|
|
||||||
border-width: 0;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
&.x {
|
|
||||||
width: 100%;
|
|
||||||
border-top-width: 1px;
|
|
||||||
}
|
|
||||||
&.y {
|
|
||||||
height: 100%;
|
|
||||||
border-left-width: 1px;
|
|
||||||
}
|
|
||||||
&.z {
|
|
||||||
width: 50%;
|
|
||||||
border-top-width: 1px;
|
|
||||||
border-color: #454754;
|
|
||||||
transform: translate(0%, -50%) rotateZ(var(--rotateZ));
|
|
||||||
transform-origin: left center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="pixel-ratio-selection">
|
|
||||||
<div
|
|
||||||
v-for="v in list"
|
|
||||||
:key="v"
|
|
||||||
:class="{ active: v === modelValue }"
|
|
||||||
@click="onChange(v)"
|
|
||||||
:style="{ '--w': v.split(':')[0], '--h': v.split(':')[1] }"
|
|
||||||
>
|
|
||||||
{{ v }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { reactive, defineExpose } from 'vue'
|
|
||||||
const emit = defineEmits(['update:modelValue', 'change'])
|
|
||||||
const props = defineProps({
|
|
||||||
modelValue: { type: String },
|
|
||||||
list: {
|
|
||||||
type: Array,
|
|
||||||
default: () => ['1:1', '4:3', '3:4', '16:9']
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const data = reactive({})
|
|
||||||
const onChange = (v) => {
|
|
||||||
emit('update:modelValue', v)
|
|
||||||
emit('change', v)
|
|
||||||
}
|
|
||||||
defineExpose({ data })
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
.pixel-ratio-selection {
|
|
||||||
width: 100%;
|
|
||||||
height: 34px;
|
|
||||||
border-radius: 6px;
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 0 17px;
|
|
||||||
user-select: none;
|
|
||||||
> div {
|
|
||||||
text-align: center;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #7c7c7c;
|
|
||||||
height: 21px;
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
&.active {
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
border-radius: 1px;
|
|
||||||
border: 1px solid #7c7c7c;
|
|
||||||
width: calc(var(--w) / max(var(--w), var(--h)) * 10px);
|
|
||||||
height: calc(var(--h) / max(var(--w), var(--h)) * 10px);
|
|
||||||
margin-right: 4px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="slider" :disabled="disabled">
|
|
||||||
<div
|
|
||||||
class="input-range"
|
|
||||||
:style="{
|
|
||||||
'--progress': (value - props.min) / (props.max - props.min)
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<span class="tip">{{ props.tipFormatter(value) }}</span>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
v-model="value"
|
|
||||||
v-bind="$attrs"
|
|
||||||
@input="onInput"
|
|
||||||
@change="onChange"
|
|
||||||
:disabled="disabled"
|
|
||||||
:min="props.min"
|
|
||||||
:max="props.max"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="input" v-show="isInput">
|
|
||||||
<my-input
|
|
||||||
type="number"
|
|
||||||
v-model="value"
|
|
||||||
v-bind="$attrs"
|
|
||||||
@input="onInput"
|
|
||||||
@change="onChange"
|
|
||||||
:disabled="disabled"
|
|
||||||
:min="props.min"
|
|
||||||
:max="props.max"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, defineProps, defineEmits, watch } from 'vue'
|
|
||||||
import MyInput from './my-input.vue'
|
|
||||||
const props = defineProps({
|
|
||||||
modelValue: { type: Number },
|
|
||||||
disabled: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
min: {
|
|
||||||
type: Number,
|
|
||||||
default: 0
|
|
||||||
},
|
|
||||||
max: {
|
|
||||||
type: Number,
|
|
||||||
default: 100
|
|
||||||
},
|
|
||||||
tipFormatter: {
|
|
||||||
type: Function,
|
|
||||||
default: (v) => v
|
|
||||||
},
|
|
||||||
isInput: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const emit = defineEmits(['update:modelValue', 'change', 'input'])
|
|
||||||
watch(
|
|
||||||
() => props.modelValue,
|
|
||||||
(v) => {
|
|
||||||
value.value = v
|
|
||||||
}
|
|
||||||
)
|
|
||||||
const value = ref(props.modelValue)
|
|
||||||
const onInput = () => {
|
|
||||||
if (props.disabled) return
|
|
||||||
const v = Number(value.value)
|
|
||||||
emit('update:modelValue', v)
|
|
||||||
emit('input', v)
|
|
||||||
}
|
|
||||||
const onChange = () => {
|
|
||||||
if (props.disabled) return
|
|
||||||
const v = Number(value.value)
|
|
||||||
emit('update:modelValue', v)
|
|
||||||
emit('change', v)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
.slider {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
--input-thumb-size: 8px;
|
|
||||||
--backcolor1: var(--slider-thumb-color1, #4285f4);
|
|
||||||
--backcolor2: var(--slider-thumb-color2, rgba(0, 0, 0, 0.1));
|
|
||||||
&:hover {
|
|
||||||
> .input-range > .tip {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
> .input-range {
|
|
||||||
position: relative;
|
|
||||||
flex: 2;
|
|
||||||
display: flex;
|
|
||||||
> input {
|
|
||||||
width: 100%;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
appearance: none;
|
|
||||||
height: 3px;
|
|
||||||
border-radius: 3px;
|
|
||||||
outline: none;
|
|
||||||
background: linear-gradient(
|
|
||||||
to right,
|
|
||||||
var(--backcolor1) 0%,
|
|
||||||
var(--backcolor1) calc(var(--progress) * 100%),
|
|
||||||
var(--backcolor2) calc(var(--progress) * 100%),
|
|
||||||
var(--backcolor2) 100%
|
|
||||||
);
|
|
||||||
&::-webkit-slider-thumb {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
appearance: none;
|
|
||||||
width: var(--input-thumb-size);
|
|
||||||
height: var(--input-thumb-size);
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--backcolor1); /* 蓝色滑块 */
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
&::-webkit-slider-thumb:hover {
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
> .tip {
|
|
||||||
position: absolute;
|
|
||||||
font-size: 10px;
|
|
||||||
pointer-events: none;
|
|
||||||
user-select: none;
|
|
||||||
color: #666;
|
|
||||||
top: calc(var(--input-thumb-size) / -2 - 3.5px);
|
|
||||||
left: calc(
|
|
||||||
(100% - var(--input-thumb-size)) * var(--progress) + var(--input-thumb-size) / 2
|
|
||||||
);
|
|
||||||
transform: translate(-50%, -100%);
|
|
||||||
background-color: rgba(0, 0, 0, 0.7);
|
|
||||||
color: white;
|
|
||||||
padding: 3px 5px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 10px;
|
|
||||||
white-space: nowrap;
|
|
||||||
pointer-events: none;
|
|
||||||
display: none;
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 97%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-left: 5px solid transparent;
|
|
||||||
border-right: 5px solid transparent;
|
|
||||||
border-top: 5px solid rgba(0, 0, 0, 0.8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
> .input {
|
|
||||||
flex: 1;
|
|
||||||
margin-left: 10px;
|
|
||||||
> input {
|
|
||||||
border-radius: 3px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="upload-file">
|
|
||||||
<div class="preview" v-if="url">
|
|
||||||
<img :src="url" @error="onChange(null)" />
|
|
||||||
<div class="close" @click="onChange(null)">
|
|
||||||
<svg-icon name="close-border" size="16" size-unit="px" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="control" v-else>
|
|
||||||
<div class="icon"><svg-icon name="upload" size="17" size-unit="px" /></div>
|
|
||||||
<div class="txt">{{ tip }}</div>
|
|
||||||
<div class="btn" @click="onSelectFile">Select File</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { reactive, computed } from 'vue'
|
|
||||||
const emit = defineEmits(['update:modelValue', 'change'])
|
|
||||||
const props = defineProps({
|
|
||||||
modelValue: { type: [File, Object, String, null] },
|
|
||||||
tip: { type: String, default: 'Upload your files' }
|
|
||||||
})
|
|
||||||
const data = reactive({
|
|
||||||
file: null
|
|
||||||
})
|
|
||||||
const url = computed(() => {
|
|
||||||
const type = props.modelValue?.constructor
|
|
||||||
var str = ''
|
|
||||||
if (type === File) {
|
|
||||||
str = URL.createObjectURL(props.modelValue as File)
|
|
||||||
} else if (type === String) {
|
|
||||||
str = props.modelValue as string
|
|
||||||
}
|
|
||||||
return str
|
|
||||||
})
|
|
||||||
const onChange = (v) => {
|
|
||||||
emit('update:modelValue', v)
|
|
||||||
emit('change', v)
|
|
||||||
}
|
|
||||||
const onSelectFile = () => {
|
|
||||||
const input = document.createElement('input')
|
|
||||||
input.type = 'file'
|
|
||||||
input.accept = 'image/png, image/jpeg, image/jpg'
|
|
||||||
input.addEventListener('change', (e) => {
|
|
||||||
const file = e.target.files[0]
|
|
||||||
if (file) onChange(file)
|
|
||||||
})
|
|
||||||
input.click()
|
|
||||||
}
|
|
||||||
defineExpose({ data })
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
.upload-file {
|
|
||||||
width: 100%;
|
|
||||||
height: 99px;
|
|
||||||
border-radius: 10px;
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
> .control {
|
|
||||||
text-align: center;
|
|
||||||
> .txt {
|
|
||||||
margin-top: 6px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-size: 8px;
|
|
||||||
color: #7c7c7c;
|
|
||||||
}
|
|
||||||
> .btn {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
box-shadow: 0px 0.75px 0px 0px rgba(0, 0, 0, 0.02);
|
|
||||||
min-width: 39px;
|
|
||||||
height: 13px;
|
|
||||||
border-radius: 2.3px;
|
|
||||||
background-color: #fff;
|
|
||||||
font-size: 6px;
|
|
||||||
color: #000;
|
|
||||||
border: 1px solid #d9d9d9;
|
|
||||||
cursor: pointer;
|
|
||||||
&:active {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
> .preview {
|
|
||||||
width: 80px;
|
|
||||||
height: 80px;
|
|
||||||
position: relative;
|
|
||||||
> img {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
> .close {
|
|
||||||
position: absolute;
|
|
||||||
top: 0.1px;
|
|
||||||
right: 0.1px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: #fff;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { fabric } from 'fabric-with-all'
|
import { fabric } from 'fabric-with-all'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { createCanvas } from '../tools/canvasFactory'
|
import { createCanvas } from '../tools/canvasFactory'
|
||||||
import { AnimationManager } from './animationManager'
|
import { AnimationManager } from './AnimationManager'
|
||||||
import { detectDeviceType } from '../tools/index'
|
import { detectDeviceType } from '../tools/index'
|
||||||
import { CanvasEventManager } from "./events/CanvasEventManager";
|
import { CanvasEventManager } from "./events/CanvasEventManager";
|
||||||
import { OperationType } from '../tools/layerHelper'
|
import { OperationType } from '../tools/layerHelper'
|
||||||
|
|||||||
@@ -1,843 +0,0 @@
|
|||||||
import { gsap } from 'gsap'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 画布动画管理器
|
|
||||||
* 负责处理画布平移、缩放等动画效果
|
|
||||||
*/
|
|
||||||
export class AnimationManager {
|
|
||||||
/**
|
|
||||||
* 创建动画管理器
|
|
||||||
* @param {fabric.Canvas} canvas fabric.js画布实例
|
|
||||||
* @param {Object} options 配置选项
|
|
||||||
*/
|
|
||||||
constructor(canvas, options = {}) {
|
|
||||||
this.canvas = canvas
|
|
||||||
this.currentZoom = options.currentZoom || { value: 100 }
|
|
||||||
|
|
||||||
// 动画相关属性
|
|
||||||
this._zoomAnimation = null
|
|
||||||
this._panAnimation = null
|
|
||||||
this._lastWheelTime = 0
|
|
||||||
this._lastWheelProcessTime = 0 // 上次处理wheel事件的时间
|
|
||||||
this._wheelEvents = []
|
|
||||||
|
|
||||||
// 检测设备类型,Mac设备使用更短的节流时间确保响应性
|
|
||||||
this._isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0
|
|
||||||
this._wheelThrottleTime = this._isMac
|
|
||||||
? options.wheelThrottleTime || 8 // Mac设备使用更短的节流时间
|
|
||||||
: options.wheelThrottleTime || 30
|
|
||||||
|
|
||||||
this._accumulatedWheelDelta = 0 // 累积滚轮增量
|
|
||||||
this._wheelAccumulationTimeout = null // 滚轮累积超时
|
|
||||||
|
|
||||||
// Mac设备使用更短的累积时间窗口,确保及时响应
|
|
||||||
this._wheelAccumulationTime = this._isMac ? 60 : 120 // 滚轮累积时间窗口(毫秒)
|
|
||||||
|
|
||||||
// 添加新的状态跟踪变量
|
|
||||||
this._wasPanning = false // 是否有平移动画正在进行
|
|
||||||
this._wasZooming = false // 是否有缩放动画正在进行
|
|
||||||
this._combinedAnimation = null // 组合动画引用
|
|
||||||
|
|
||||||
// Mac特有的动画优化变量 - 使用最小防抖机制
|
|
||||||
if (this._isMac) {
|
|
||||||
this._lastMacAnimationTime = 0 // 上次Mac动画时间
|
|
||||||
this._macAnimationCooldown = 2 // 最小的动画冷却时间,确保最大响应性
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化GSAP默认配置
|
|
||||||
gsap.defaults({
|
|
||||||
ease: options.defaultEase || (this._isMac ? 'power2.out' : 'power2.out'), // Mac使用简单高效的缓动
|
|
||||||
duration: options.defaultDuration || (this._isMac ? 0.3 : 0.3), // Mac使用标准持续时间
|
|
||||||
overwrite: 'auto' // 自动覆盖同一对象上的动画
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 使用 GSAP 实现平滑缩放动画
|
|
||||||
* @param {Object} point 缩放中心点 {x, y}
|
|
||||||
* @param {Number} targetZoom 目标缩放值
|
|
||||||
* @param {Object} options 动画选项
|
|
||||||
*/
|
|
||||||
animateZoom(point, targetZoom, options = {}) {
|
|
||||||
if (!this.canvas) return
|
|
||||||
|
|
||||||
// 限制缩放范围
|
|
||||||
targetZoom = Math.min(Math.max(targetZoom, 0.1), 20)
|
|
||||||
|
|
||||||
// 当前缩放值
|
|
||||||
const currentZoom = this.canvas.getZoom()
|
|
||||||
|
|
||||||
// 如果变化太小,直接应用缩放
|
|
||||||
if (Math.abs(targetZoom - currentZoom) < 0.01) {
|
|
||||||
this._applyZoom(point, targetZoom)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 停止任何进行中的缩放动画
|
|
||||||
if (this._zoomAnimation) {
|
|
||||||
// 不是直接 kill,而是获取当前进度值作为新的起点
|
|
||||||
const currentProgress = this._zoomAnimation.progress()
|
|
||||||
const currentZoomValue = this._zoomAnimation.targets()[0].value
|
|
||||||
this._zoomAnimation.kill()
|
|
||||||
this._zoomAnimation = null
|
|
||||||
|
|
||||||
// 从当前过渡中的值开始新动画,而不是从最初的值
|
|
||||||
const zoomObj = { value: currentZoomValue }
|
|
||||||
const currentVpt = [...this.canvas.viewportTransform]
|
|
||||||
|
|
||||||
// 计算过渡动画持续时间 - 根据当前值到目标值的距离比例
|
|
||||||
const progressRatio =
|
|
||||||
Math.abs(targetZoom - currentZoomValue) / Math.abs(targetZoom - currentZoom)
|
|
||||||
const duration = options.duration || 0.3 * progressRatio
|
|
||||||
|
|
||||||
// 计算缩放后目标位置需要的修正,保持缩放点不变
|
|
||||||
const animOptions = {
|
|
||||||
value: targetZoom,
|
|
||||||
duration: duration,
|
|
||||||
ease: options.ease || 'power2.out',
|
|
||||||
onUpdate: () => {
|
|
||||||
// 更新缩放值显示
|
|
||||||
this.currentZoom.value = Math.round(zoomObj.value * 100)
|
|
||||||
|
|
||||||
// 计算过渡中的变换矩阵
|
|
||||||
const zoom = zoomObj.value
|
|
||||||
const scale = zoom / currentZoomValue
|
|
||||||
const currentScaleFactor = scale
|
|
||||||
|
|
||||||
// 应用变换
|
|
||||||
const vpt = this.canvas.viewportTransform
|
|
||||||
vpt[0] = currentVpt[0] * scale
|
|
||||||
vpt[3] = currentVpt[3] * scale
|
|
||||||
|
|
||||||
// 应用平移修正以保持缩放点
|
|
||||||
const adjustX = (1 - currentScaleFactor) * point.x
|
|
||||||
const adjustY = (1 - currentScaleFactor) * point.y
|
|
||||||
vpt[4] = currentVpt[4] * scale + adjustX
|
|
||||||
vpt[5] = currentVpt[5] * scale + adjustY
|
|
||||||
|
|
||||||
this.canvas.renderAll()
|
|
||||||
},
|
|
||||||
onComplete: () => {
|
|
||||||
this._zoomAnimation = null
|
|
||||||
|
|
||||||
// 确保最终状态准确
|
|
||||||
this._applyZoom(point, targetZoom, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 启动 GSAP 动画
|
|
||||||
this._zoomAnimation = gsap.to(zoomObj, animOptions)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没有正在进行的动画,创建新的缩放动画
|
|
||||||
const zoomObj = { value: currentZoom }
|
|
||||||
const currentVpt = [...this.canvas.viewportTransform]
|
|
||||||
|
|
||||||
// 计算缩放后目标位置需要的修正,保持缩放点不变
|
|
||||||
const scaleFactor = targetZoom / currentZoom
|
|
||||||
const invertedScaleFactor = 1 / scaleFactor
|
|
||||||
|
|
||||||
// 这个数学公式确保缩放点在屏幕上的位置保持不变
|
|
||||||
const dx = point.x - point.x * invertedScaleFactor
|
|
||||||
const dy = point.y - point.y * invertedScaleFactor
|
|
||||||
|
|
||||||
// 创建动画配置
|
|
||||||
const animOptions = {
|
|
||||||
value: targetZoom,
|
|
||||||
duration: options.duration || 0.3,
|
|
||||||
ease: options.ease || (this._isMac ? 'expo.out' : 'power2.out'), // Mac使用更平滑的缓动
|
|
||||||
onUpdate: () => {
|
|
||||||
// 更新缩放值显示
|
|
||||||
this.currentZoom.value = Math.round(zoomObj.value * 100)
|
|
||||||
|
|
||||||
// 计算过渡中的变换矩阵
|
|
||||||
const zoom = zoomObj.value
|
|
||||||
const scale = zoom / currentZoom
|
|
||||||
const currentScaleFactor = scale
|
|
||||||
|
|
||||||
// 应用变换
|
|
||||||
const vpt = this.canvas.viewportTransform
|
|
||||||
vpt[0] = currentVpt[0] * scale
|
|
||||||
vpt[3] = currentVpt[3] * scale
|
|
||||||
|
|
||||||
// 应用平移修正以保持缩放点
|
|
||||||
const adjustX = (1 - currentScaleFactor) * point.x
|
|
||||||
const adjustY = (1 - currentScaleFactor) * point.y
|
|
||||||
vpt[4] = currentVpt[4] * scale + adjustX
|
|
||||||
vpt[5] = currentVpt[5] * scale + adjustY
|
|
||||||
|
|
||||||
this.canvas.renderAll()
|
|
||||||
},
|
|
||||||
onComplete: () => {
|
|
||||||
this._zoomAnimation = null
|
|
||||||
|
|
||||||
// 确保最终状态准确
|
|
||||||
this._applyZoom(point, targetZoom, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 启动 GSAP 动画
|
|
||||||
this._zoomAnimation = gsap.to(zoomObj, animOptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 应用缩放(内部使用)
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_applyZoom(point, zoom, skipUpdate = false) {
|
|
||||||
if (!skipUpdate) {
|
|
||||||
this.currentZoom.value = Math.round(zoom * 100)
|
|
||||||
}
|
|
||||||
this.canvas.zoomToPoint(point, zoom)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 使用 GSAP 实现平滑平移动画
|
|
||||||
* @param {Object} targetPosition 目标位置 {x, y}
|
|
||||||
* @param {Object} options 动画选项
|
|
||||||
*/
|
|
||||||
animatePan(targetPosition, options = {}) {
|
|
||||||
if (!this.canvas) return
|
|
||||||
|
|
||||||
// 停止任何进行中的平移动画
|
|
||||||
if (this._panAnimation) {
|
|
||||||
this._panAnimation.kill()
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentVpt = [...this.canvas.viewportTransform]
|
|
||||||
const position = {
|
|
||||||
x: -currentVpt[4],
|
|
||||||
y: -currentVpt[5]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算平移距离
|
|
||||||
const dx = targetPosition.x - position.x
|
|
||||||
const dy = targetPosition.y - position.y
|
|
||||||
|
|
||||||
// 如果距离太小,直接应用平移
|
|
||||||
if (Math.abs(dx) < 1 && Math.abs(dy) < 1) {
|
|
||||||
this._applyPan(targetPosition.x, targetPosition.y)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建动画配置
|
|
||||||
const animOptions = {
|
|
||||||
x: targetPosition.x,
|
|
||||||
y: targetPosition.y,
|
|
||||||
duration: options.duration || 0.3,
|
|
||||||
ease: options.ease || (this._isMac ? 'circ.out' : 'power2.out'), // Mac使用更柔和的缓动
|
|
||||||
onUpdate: () => {
|
|
||||||
this._applyPan(position.x, position.y)
|
|
||||||
},
|
|
||||||
onComplete: () => {
|
|
||||||
this._panAnimation = null
|
|
||||||
// 确保最终位置准确
|
|
||||||
this._applyPan(targetPosition.x, targetPosition.y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 启动 GSAP 动画
|
|
||||||
this._panAnimation = gsap.to(position, animOptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 应用平移(内部使用)
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_applyPan(x, y) {
|
|
||||||
if (!this.canvas) return
|
|
||||||
|
|
||||||
const vpt = this.canvas.viewportTransform
|
|
||||||
vpt[4] = -x
|
|
||||||
vpt[5] = -y
|
|
||||||
|
|
||||||
this.canvas.renderAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 使用动画平移到指定元素
|
|
||||||
* @param {Object} elementId 元素ID
|
|
||||||
*/
|
|
||||||
panToElement(elementId) {
|
|
||||||
if (!this.canvas) return
|
|
||||||
|
|
||||||
const obj = this.canvas.getObjects().find((obj) => obj.id === elementId)
|
|
||||||
if (!obj) return
|
|
||||||
|
|
||||||
const zoom = this.canvas.getZoom()
|
|
||||||
const center = obj.getCenterPoint()
|
|
||||||
|
|
||||||
// 计算目标中心位置
|
|
||||||
const targetX = center.x * zoom - this.canvas.width / 2
|
|
||||||
const targetY = center.y * zoom - this.canvas.height / 2
|
|
||||||
|
|
||||||
// 动画平移
|
|
||||||
this.animatePan(
|
|
||||||
{ x: targetX, y: targetY },
|
|
||||||
{
|
|
||||||
duration: 0.6,
|
|
||||||
ease: this._isMac ? 'back.out(0.3)' : 'power3.out' // Mac使用轻微回弹效果
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重置缩放(带平滑动画)
|
|
||||||
* @param {Boolean} animated 是否使用动画
|
|
||||||
*/
|
|
||||||
async resetZoom(animated = true) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
if (animated) {
|
|
||||||
// 停止任何进行中的动画
|
|
||||||
if (this._zoomAnimation) {
|
|
||||||
this._zoomAnimation.kill()
|
|
||||||
}
|
|
||||||
if (this._panAnimation) {
|
|
||||||
this._panAnimation.kill()
|
|
||||||
}
|
|
||||||
|
|
||||||
const center = {
|
|
||||||
x: this.canvas.width / 2,
|
|
||||||
y: this.canvas.height / 2
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取当前变换矩阵
|
|
||||||
const currentVpt = [...this.canvas.viewportTransform]
|
|
||||||
const currentZoom = this.canvas.getZoom()
|
|
||||||
|
|
||||||
// 创建一个对象来动画整个视图变换
|
|
||||||
const viewTransform = {
|
|
||||||
zoom: currentZoom,
|
|
||||||
panX: currentVpt[4],
|
|
||||||
panY: currentVpt[5]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用GSAP同时动画缩放和平移
|
|
||||||
gsap.to(viewTransform, {
|
|
||||||
zoom: 1,
|
|
||||||
panX: 0,
|
|
||||||
panY: 0,
|
|
||||||
duration: 0.5,
|
|
||||||
ease: this._isMac ? 'back.out(0.2)' : 'power3.out', // Mac使用轻微回弹效果
|
|
||||||
onUpdate: () => {
|
|
||||||
// 更新缩放显示值
|
|
||||||
this.currentZoom.value = Math.round(viewTransform.zoom * 100)
|
|
||||||
|
|
||||||
// 应用新的变换
|
|
||||||
const vpt = this.canvas.viewportTransform
|
|
||||||
vpt[0] = viewTransform.zoom
|
|
||||||
vpt[3] = viewTransform.zoom
|
|
||||||
vpt[4] = viewTransform.panX
|
|
||||||
vpt[5] = viewTransform.panY
|
|
||||||
|
|
||||||
this.canvas.renderAll()
|
|
||||||
},
|
|
||||||
onComplete: () => {
|
|
||||||
// 确保最终状态准确
|
|
||||||
this.canvas.setViewportTransform([1, 0, 0, 1, 0, 0])
|
|
||||||
this.currentZoom.value = 100
|
|
||||||
this._zoomAnimation = null
|
|
||||||
this._panAnimation = null
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this.canvas.setViewportTransform([1, 0, 0, 1, 0, 0])
|
|
||||||
this.currentZoom.value = 100
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理鼠标滚轮缩放
|
|
||||||
* @param {Object} opt 事件对象
|
|
||||||
*/
|
|
||||||
handleMouseWheel(opt) {
|
|
||||||
const now = Date.now()
|
|
||||||
let delta = opt.e.deltaY
|
|
||||||
|
|
||||||
// 记录事件用于计算速度和惯性
|
|
||||||
this._wheelEvents.push({
|
|
||||||
delta: delta,
|
|
||||||
point: { x: opt.e.offsetX, y: opt.e.offsetY },
|
|
||||||
time: now,
|
|
||||||
hasPanAnimation: this._wasPanning,
|
|
||||||
hasZoomAnimation: this._wasZooming
|
|
||||||
})
|
|
||||||
|
|
||||||
// 保留最近的事件记录
|
|
||||||
if (this._wheelEvents.length > 10) {
|
|
||||||
this._wheelEvents.shift()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否是第一个事件或者距离上次处理已经过了足够时间
|
|
||||||
const isFirstEvent = !this._wheelAccumulationTimeout
|
|
||||||
const timeSinceLastProcess = now - (this._lastWheelProcessTime || 0)
|
|
||||||
|
|
||||||
if (isFirstEvent || timeSinceLastProcess > this._wheelAccumulationTime) {
|
|
||||||
// 立即处理第一个事件或长时间没有处理的事件,确保响应性
|
|
||||||
this._processAccumulatedWheel(opt)
|
|
||||||
this._lastWheelProcessTime = now
|
|
||||||
|
|
||||||
// 清理之前的累积
|
|
||||||
this._accumulatedWheelDelta = 0
|
|
||||||
|
|
||||||
// 如果有pending的timeout,清除它
|
|
||||||
if (this._wheelAccumulationTimeout) {
|
|
||||||
clearTimeout(this._wheelAccumulationTimeout)
|
|
||||||
this._wheelAccumulationTimeout = null
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 累积后续事件
|
|
||||||
this._accumulatedWheelDelta += delta
|
|
||||||
|
|
||||||
// 如果正在累积中,清除之前的定时器
|
|
||||||
if (this._wheelAccumulationTimeout) {
|
|
||||||
clearTimeout(this._wheelAccumulationTimeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置新的定时器,处理累积的事件
|
|
||||||
this._wheelAccumulationTimeout = setTimeout(() => {
|
|
||||||
this._processAccumulatedWheel(opt)
|
|
||||||
this._lastWheelProcessTime = Date.now()
|
|
||||||
|
|
||||||
// 清理
|
|
||||||
this._accumulatedWheelDelta = 0
|
|
||||||
this._wheelAccumulationTimeout = null
|
|
||||||
}, this._wheelThrottleTime)
|
|
||||||
}
|
|
||||||
|
|
||||||
opt.e.preventDefault()
|
|
||||||
opt.e.stopPropagation()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理累积的滚轮事件并应用缩放
|
|
||||||
* @private
|
|
||||||
* @param {Object} lastOpt 最后一个滚轮事件
|
|
||||||
*/
|
|
||||||
_processAccumulatedWheel(lastOpt) {
|
|
||||||
if (!this._wheelEvents.length) return
|
|
||||||
|
|
||||||
const now = Date.now()
|
|
||||||
|
|
||||||
// Mac设备的轻量防抖检查 - 进一步减少冷却时间,确保响应性
|
|
||||||
if (this._isMac && now - this._lastMacAnimationTime < this._macAnimationCooldown) {
|
|
||||||
// 如果距离上次动画时间太短,只延迟很短时间,不阻塞太久
|
|
||||||
if (this._wheelAccumulationTimeout) {
|
|
||||||
clearTimeout(this._wheelAccumulationTimeout)
|
|
||||||
}
|
|
||||||
this._wheelAccumulationTimeout = setTimeout(() => {
|
|
||||||
this._processAccumulatedWheel(lastOpt)
|
|
||||||
}, Math.min(this._macAnimationCooldown, 3)) // 最多延迟3ms
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentZoom = this.canvas.getZoom()
|
|
||||||
|
|
||||||
// 分析滚轮事件模式,计算平均增量、速度和加速度
|
|
||||||
let sumDelta = 0
|
|
||||||
let count = 0
|
|
||||||
let earliestTime = now
|
|
||||||
let latestTime = 0
|
|
||||||
let point = {
|
|
||||||
x: lastOpt.e.offsetX,
|
|
||||||
y: lastOpt.e.offsetY
|
|
||||||
}
|
|
||||||
|
|
||||||
// 判断是否在事件收集期间有平移或缩放动画
|
|
||||||
let hadPanAnimation = false
|
|
||||||
let hadZoomAnimation = false
|
|
||||||
|
|
||||||
// 计算平均增量和速度
|
|
||||||
this._wheelEvents.forEach((event) => {
|
|
||||||
sumDelta += event.delta
|
|
||||||
count++
|
|
||||||
earliestTime = Math.min(earliestTime, event.time)
|
|
||||||
latestTime = Math.max(latestTime, event.time)
|
|
||||||
|
|
||||||
// 使用最后记录的点作为缩放中心
|
|
||||||
if (event.time > latestTime) {
|
|
||||||
point = event.point
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否有动画状态
|
|
||||||
if (event.hasPanAnimation) hadPanAnimation = true
|
|
||||||
if (event.hasZoomAnimation) hadZoomAnimation = true
|
|
||||||
})
|
|
||||||
|
|
||||||
// 计算平均增量
|
|
||||||
const avgDelta = sumDelta / count
|
|
||||||
|
|
||||||
// 计算滚动速度 - 基于事件频率和时间跨度
|
|
||||||
const timeSpan = latestTime - earliestTime + 1 // 避免除以零
|
|
||||||
const eventsPerSecond = (count / timeSpan) * 1000
|
|
||||||
|
|
||||||
// 速度系数: 速度越快,缩放越敏感
|
|
||||||
let speedFactor = Math.min(3, Math.max(0.5, eventsPerSecond / 10))
|
|
||||||
|
|
||||||
// 计算缩放因子,应用速度系数
|
|
||||||
// 针对Mac设备优化:Mac触控板的deltaY值通常较小,需要适度增加敏感度
|
|
||||||
let zoomFactorBase = 0.999
|
|
||||||
if (this._isMac) {
|
|
||||||
// Mac设备的触控板需要适度的敏感度,避免过度反应
|
|
||||||
zoomFactorBase = 0.995 // 适度降低基数,增加缩放敏感度
|
|
||||||
|
|
||||||
// 检测是否为触控板滚动(小幅度、高频次的特征)
|
|
||||||
const avgAbsDelta = Math.abs(avgDelta)
|
|
||||||
if (avgAbsDelta < 50 && count > 2) {
|
|
||||||
// 触控板滚动,适度增加敏感度
|
|
||||||
speedFactor *= 1.6 // 适度增加敏感度倍数
|
|
||||||
zoomFactorBase = 0.993 // 进一步调整基数
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const zoomFactor = zoomFactorBase ** (avgDelta * speedFactor)
|
|
||||||
let targetZoom = currentZoom * zoomFactor
|
|
||||||
|
|
||||||
// 限制缩放范围
|
|
||||||
targetZoom = Math.min(Math.max(targetZoom, 0.1), 20)
|
|
||||||
|
|
||||||
// 根据滚动速度和缩放幅度计算动画持续时间
|
|
||||||
// 速度快时缩短动画时间,缩放幅度大时延长动画时间
|
|
||||||
const zoomRatio = Math.abs(targetZoom - currentZoom) / currentZoom
|
|
||||||
|
|
||||||
let duration
|
|
||||||
if (this._isMac) {
|
|
||||||
// Mac设备使用平衡的动画时间控制
|
|
||||||
if (speedFactor > 2) {
|
|
||||||
// 快速操作:快速但平滑
|
|
||||||
duration = Math.min(
|
|
||||||
0.18,
|
|
||||||
Math.max(0.08, (zoomRatio * 0.3) / Math.sqrt(speedFactor))
|
|
||||||
)
|
|
||||||
} else if (speedFactor > 1.2) {
|
|
||||||
// 中等速度:标准响应
|
|
||||||
duration = Math.min(0.25, Math.max(0.1, (zoomRatio * 0.4) / Math.sqrt(speedFactor)))
|
|
||||||
} else {
|
|
||||||
// 慢速精确操作:确保平滑
|
|
||||||
duration = Math.min(0.3, Math.max(0.12, (zoomRatio * 0.5) / Math.sqrt(speedFactor)))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
duration = Math.min(0.5, Math.max(0.15, (zoomRatio * 0.8) / Math.sqrt(speedFactor)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据滚动速度选择不同的缓动效果
|
|
||||||
let easeType
|
|
||||||
if (this._isMac) {
|
|
||||||
// Mac设备使用更简单、性能更好的缓动函数
|
|
||||||
// 避免复杂的指数和回弹效果,减少计算量
|
|
||||||
if (speedFactor > 2) {
|
|
||||||
// 快速滚动:使用简单的缓出效果
|
|
||||||
easeType = 'power2.out'
|
|
||||||
} else if (speedFactor > 1.2) {
|
|
||||||
// 中等速度:使用平滑的缓出
|
|
||||||
easeType = 'power1.out'
|
|
||||||
} else {
|
|
||||||
// 慢速精确操作:使用线性过渡
|
|
||||||
easeType = 'power1.out'
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 非Mac设备保持原有的缓动
|
|
||||||
easeType = speedFactor > 1.5 ? 'power1.out' : 'power2.out'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据是否有其他动画正在进行,选择合适的动画方法
|
|
||||||
if (hadPanAnimation || this._wasPanning) {
|
|
||||||
// 如果有平移动画,使用组合动画以保持平滑过渡
|
|
||||||
this.animateCombinedTransform(point, targetZoom, {
|
|
||||||
duration: duration,
|
|
||||||
ease: easeType
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// 如果没有其他动画,使用标准缩放动画
|
|
||||||
this.animateZoom(point, targetZoom, {
|
|
||||||
duration: duration,
|
|
||||||
ease: easeType
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新Mac设备的最后动画时间
|
|
||||||
if (this._isMac) {
|
|
||||||
this._lastMacAnimationTime = now
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理事件记录
|
|
||||||
this._wheelEvents = []
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 计算并应用拖动结束后的惯性效果
|
|
||||||
* @param {Array} positions 拖动过程中记录的位置数组
|
|
||||||
* @param {Boolean} isTouchDevice 是否是触摸设备
|
|
||||||
*/
|
|
||||||
applyInertiaEffect(positions, isTouchDevice) {
|
|
||||||
if (!positions || positions.length <= 1) return
|
|
||||||
|
|
||||||
const lastPos = positions[positions.length - 1]
|
|
||||||
const firstPos = positions[0]
|
|
||||||
const deltaTime = lastPos.time - firstPos.time
|
|
||||||
|
|
||||||
if (deltaTime <= 0) return
|
|
||||||
|
|
||||||
// 计算速度向量 (像素/毫秒)
|
|
||||||
const velocityX = (lastPos.x - firstPos.x) / deltaTime
|
|
||||||
const velocityY = (lastPos.y - firstPos.y) / deltaTime
|
|
||||||
const speed = Math.sqrt(velocityX * velocityX + velocityY * velocityY)
|
|
||||||
|
|
||||||
// 仅当速度足够大时应用惯性效果
|
|
||||||
if (speed > 0.2) {
|
|
||||||
// 计算惯性距离,基于速度和衰减因子
|
|
||||||
const decayFactor = 300 // 调整此值以改变惯性效果的强度
|
|
||||||
const inertiaDistanceX = velocityX * decayFactor
|
|
||||||
const inertiaDistanceY = velocityY * decayFactor
|
|
||||||
|
|
||||||
// 计算目标位置
|
|
||||||
const vpt = this.canvas.viewportTransform
|
|
||||||
const currentPos = {
|
|
||||||
x: -vpt[4],
|
|
||||||
y: -vpt[5]
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetPos = {
|
|
||||||
x: currentPos.x - inertiaDistanceX,
|
|
||||||
y: currentPos.y - inertiaDistanceY
|
|
||||||
}
|
|
||||||
|
|
||||||
// 应用惯性动画,速度越大,动画时间越长
|
|
||||||
const animationDuration = Math.min(1.2, Math.max(0.6, speed * 2))
|
|
||||||
|
|
||||||
// 应用惯性动画
|
|
||||||
this.animatePan(targetPos, {
|
|
||||||
duration: animationDuration, // 动态计算持续时间
|
|
||||||
ease: this._isMac ? 'quart.out' : 'power3.out' // Mac使用更自然的减速效果
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 平滑过渡停止所有动画
|
|
||||||
* 用于在需要中断当前动画时提供更自然的过渡,而不是硬性中断
|
|
||||||
* @param {Object} options 过渡选项
|
|
||||||
*/
|
|
||||||
smoothStopAnimations(options = {}) {
|
|
||||||
const duration = options.duration || 0.15 // 默认短暂过渡时间
|
|
||||||
|
|
||||||
// 处理缩放动画
|
|
||||||
if (this._zoomAnimation) {
|
|
||||||
const zoomObj = this._zoomAnimation.targets()[0]
|
|
||||||
const currentZoom = this.canvas.getZoom()
|
|
||||||
|
|
||||||
// 创建短暂的过渡动画到当前值
|
|
||||||
gsap.to(zoomObj, {
|
|
||||||
value: currentZoom,
|
|
||||||
duration: duration,
|
|
||||||
ease: this._isMac ? 'circ.out' : 'power1.out', // Mac使用更平滑的缓动
|
|
||||||
onUpdate: () => {
|
|
||||||
this.currentZoom.value = Math.round(zoomObj.value * 100)
|
|
||||||
this.canvas.renderAll()
|
|
||||||
},
|
|
||||||
onComplete: () => {
|
|
||||||
if (this._zoomAnimation) {
|
|
||||||
this._zoomAnimation.kill()
|
|
||||||
this._zoomAnimation = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理平移动画
|
|
||||||
if (this._panAnimation) {
|
|
||||||
const panObj = this._panAnimation.targets()[0]
|
|
||||||
const vpt = this.canvas.viewportTransform
|
|
||||||
const currentPos = { x: -vpt[4], y: -vpt[5] }
|
|
||||||
|
|
||||||
// 创建短暂的过渡动画到当前位置
|
|
||||||
gsap.to(panObj, {
|
|
||||||
x: currentPos.x,
|
|
||||||
y: currentPos.y,
|
|
||||||
duration: duration,
|
|
||||||
ease: this._isMac ? 'circ.out' : 'power1.out', // Mac使用更平滑的缓动
|
|
||||||
onUpdate: () => {
|
|
||||||
this._applyPan(panObj.x, panObj.y)
|
|
||||||
},
|
|
||||||
onComplete: () => {
|
|
||||||
if (this._panAnimation) {
|
|
||||||
this._panAnimation.kill()
|
|
||||||
this._panAnimation = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置画布交互动画
|
|
||||||
* 为对象交互添加流畅的动画效果
|
|
||||||
*/
|
|
||||||
setupInteractionAnimations() {
|
|
||||||
if (!this.canvas) return
|
|
||||||
|
|
||||||
// 启用对象旋转的流畅动画
|
|
||||||
this._setupRotationAnimation()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置旋转动画
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_setupRotationAnimation() {
|
|
||||||
if (!fabric) return
|
|
||||||
|
|
||||||
// 保存原始旋转方法
|
|
||||||
const originalRotate = fabric.Object.prototype.rotate
|
|
||||||
const isMac = this._isMac // 保存Mac检测结果
|
|
||||||
|
|
||||||
// 覆盖旋转方法以添加动画
|
|
||||||
fabric.Object.prototype.rotate = function (angle) {
|
|
||||||
const currentAngle = this.angle || 0
|
|
||||||
|
|
||||||
if (Math.abs(angle - currentAngle) > 0.1) {
|
|
||||||
gsap.to(this, {
|
|
||||||
angle: angle,
|
|
||||||
duration: 0.3,
|
|
||||||
ease: isMac ? 'back.out(0.3)' : 'power2.out', // Mac使用轻微回弹
|
|
||||||
onUpdate: () => {
|
|
||||||
this.canvas && this.canvas.renderAll()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果角度差异很小,使用原始方法
|
|
||||||
return originalRotate.call(this, angle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理滚轮缩放,同时兼容正在进行的平移动画
|
|
||||||
* @param {Object} point 缩放中心点
|
|
||||||
* @param {Number} targetZoom 目标缩放值
|
|
||||||
* @param {Object} options 动画选项
|
|
||||||
*/
|
|
||||||
animateCombinedTransform(point, targetZoom, options = {}) {
|
|
||||||
if (!this.canvas) return
|
|
||||||
|
|
||||||
// 限制缩放范围
|
|
||||||
targetZoom = Math.min(Math.max(targetZoom, 0.1), 20)
|
|
||||||
|
|
||||||
// 当前状态
|
|
||||||
const currentZoom = this.canvas.getZoom()
|
|
||||||
const currentVpt = [...this.canvas.viewportTransform]
|
|
||||||
const currentPos = { x: -currentVpt[4], y: -currentVpt[5] }
|
|
||||||
|
|
||||||
// 如果有正在进行的动画,先停止它们
|
|
||||||
if (this._combinedAnimation) {
|
|
||||||
this._combinedAnimation.kill()
|
|
||||||
this._combinedAnimation = null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._zoomAnimation) {
|
|
||||||
this._zoomAnimation.kill()
|
|
||||||
this._zoomAnimation = null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._panAnimation) {
|
|
||||||
this._panAnimation.kill()
|
|
||||||
this._panAnimation = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建一个统一的变换对象来动画
|
|
||||||
const transform = {
|
|
||||||
zoom: currentZoom,
|
|
||||||
panX: currentVpt[4],
|
|
||||||
panY: currentVpt[5],
|
|
||||||
progress: 0 // 用于动画进度跟踪
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取平移目标位置(如果有的话)
|
|
||||||
let panTarget = { x: currentPos.x, y: currentPos.y }
|
|
||||||
if (this._wasPanning) {
|
|
||||||
// 如果之前有平移动画,尝试获取平移的目标位置
|
|
||||||
const vpt = this.canvas.viewportTransform
|
|
||||||
panTarget = {
|
|
||||||
x: currentPos.x,
|
|
||||||
y: currentPos.y
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算新的变换矩阵,同时考虑平移和缩放
|
|
||||||
const scaleFactor = targetZoom / currentZoom
|
|
||||||
|
|
||||||
// 创建动画
|
|
||||||
this._combinedAnimation = gsap.to(transform, {
|
|
||||||
zoom: targetZoom,
|
|
||||||
progress: 1,
|
|
||||||
duration: options.duration || 0.3,
|
|
||||||
ease: options.ease || (this._isMac ? 'expo.out' : 'power2.out'), // Mac使用更平滑的缓动
|
|
||||||
onUpdate: () => {
|
|
||||||
// 计算当前动画阶段的混合变换
|
|
||||||
const currentScaleFactor = transform.zoom / currentZoom
|
|
||||||
|
|
||||||
// 应用缩放
|
|
||||||
const vpt = this.canvas.viewportTransform
|
|
||||||
vpt[0] = currentVpt[0] * (transform.zoom / currentZoom)
|
|
||||||
vpt[3] = currentVpt[3] * (transform.zoom / currentZoom)
|
|
||||||
|
|
||||||
// 平滑混合平移和缩放调整
|
|
||||||
const adjustX = (1 - currentScaleFactor) * point.x
|
|
||||||
const adjustY = (1 - currentScaleFactor) * point.y
|
|
||||||
|
|
||||||
// 如果存在平移目标,进行插值
|
|
||||||
if (this._wasPanning) {
|
|
||||||
const t = transform.progress
|
|
||||||
const interpolatedX = currentPos.x * (1 - t) + panTarget.x * t
|
|
||||||
const interpolatedY = currentPos.y * (1 - t) + panTarget.y * t
|
|
||||||
|
|
||||||
// 结合缩放和平移的调整
|
|
||||||
vpt[4] = -interpolatedX * currentScaleFactor + adjustX
|
|
||||||
vpt[5] = -interpolatedY * currentScaleFactor + adjustY
|
|
||||||
} else {
|
|
||||||
// 只有缩放,保持中心点
|
|
||||||
vpt[4] = currentVpt[4] * currentScaleFactor + adjustX
|
|
||||||
vpt[5] = currentVpt[5] * currentScaleFactor + adjustY
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新缩放值显示
|
|
||||||
this.currentZoom.value = Math.round(transform.zoom * 100)
|
|
||||||
this.canvas.renderAll()
|
|
||||||
},
|
|
||||||
onComplete: () => {
|
|
||||||
this._combinedAnimation = null
|
|
||||||
this._zoomAnimation = null
|
|
||||||
this._panAnimation = null
|
|
||||||
this._wasPanning = false
|
|
||||||
this._wasZooming = false
|
|
||||||
|
|
||||||
// 确保最终状态准确
|
|
||||||
this._applyZoom(point, targetZoom, true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清理资源
|
|
||||||
*/
|
|
||||||
dispose() {
|
|
||||||
if (this._zoomAnimation) {
|
|
||||||
this._zoomAnimation.kill()
|
|
||||||
this._zoomAnimation = null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._panAnimation) {
|
|
||||||
this._panAnimation.kill()
|
|
||||||
this._panAnimation = null
|
|
||||||
}
|
|
||||||
|
|
||||||
this._wheelEvents = []
|
|
||||||
this.canvas = null
|
|
||||||
this.currentZoom = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user