画布增加的新功能
This commit is contained in:
121
src/component/Canvas/CanvasEditor/components/tools/AngleTool.vue
Normal file
121
src/component/Canvas/CanvasEditor/components/tools/AngleTool.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<div class="angle-tool">
|
||||
<div
|
||||
ref="dishRef"
|
||||
class="dish"
|
||||
@mousedown.stop="mousedown"
|
||||
@touchmove.stop="mousedown"
|
||||
>
|
||||
<div class="pointer" :style="{ transform: `rotate(${angle}deg)` }">
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input">
|
||||
<input type="number" v-model="angle" @input="onInput" @change="onChange" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, defineProps, defineEmits, watch } from "vue";
|
||||
import { calculateAngle } from "../../utils/helper";
|
||||
// Props
|
||||
const props = defineProps({
|
||||
angle: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(["change", "input"]);
|
||||
const angle = ref(props.angle);
|
||||
watch(() => props.angle, (value) => {
|
||||
angle.value = value;
|
||||
});
|
||||
const dishRef = ref<HTMLDivElement>();
|
||||
const mousedown = (e: MouseEvent | TouchEvent) => {
|
||||
const mousemove = (e: MouseEvent | TouchEvent) => {
|
||||
if (!dishRef.value) return;
|
||||
const { left, top, width, height } =
|
||||
dishRef.value.getBoundingClientRect();
|
||||
const centerX = left + width / 2;
|
||||
const centerY = top + height / 2;
|
||||
const { clientX, clientY } = e?.touches?.[0] || e;
|
||||
angle.value = calculateAngle(centerX, centerY, clientX, clientY, true);
|
||||
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 = () => emit("input", angle.value);
|
||||
var changeTime: any = null;
|
||||
const onChange = () => {
|
||||
clearTimeout(changeTime);
|
||||
changeTime = setTimeout(() => emit("change", angle.value), 500);
|
||||
};
|
||||
// var angleTime = null;
|
||||
// watch(angle, (value) => {
|
||||
// emit("input", value);
|
||||
// clearTimeout(angleTime);
|
||||
// angleTime = setTimeout(() => emit("change", value), 50);
|
||||
// });
|
||||
// defineExpose({
|
||||
// open,
|
||||
// close,
|
||||
// });
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.angle-tool {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
> .dish {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 1px solid #000;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
> .pointer {
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
> span {
|
||||
position: absolute;
|
||||
top: 10%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
width: 35%;
|
||||
height: 35%;
|
||||
background-color: #000;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
> .input {
|
||||
margin-left: 5px;
|
||||
font-size: 14px;
|
||||
color: #000;
|
||||
flex: 1;
|
||||
// min-width: 45px;
|
||||
// max-width: 80px;
|
||||
// width: 50px;
|
||||
> input {
|
||||
width: 100%;
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<a-select
|
||||
class="my-select"
|
||||
:size="size"
|
||||
@change="change"
|
||||
:defaultValue="defaultValue"
|
||||
@dropdownVisibleChange="dropdownVisibleChange"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="v in list"
|
||||
:key="v.value"
|
||||
:value="v.value"
|
||||
:title="v.tip"
|
||||
@mouseover.stop.prevent="mouseover(v)"
|
||||
@mouseleave="mouseleave(v)"
|
||||
>{{ v.label }}</a-select-option
|
||||
>
|
||||
</a-select>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, defineProps, defineEmits, watch } from "vue";
|
||||
const props = defineProps({
|
||||
defaultValue: {
|
||||
default: "",
|
||||
},
|
||||
list: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: "small",
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(["change", "active"]);
|
||||
const isChange = ref(false);
|
||||
const initValue = ref(props.defaultValue);
|
||||
const activeValue = ref(props.defaultValue);
|
||||
const timeout = ref(null);
|
||||
const mouseover = (v) => {
|
||||
clearTimeout(timeout.value);
|
||||
if (v.value === activeValue.value) return;
|
||||
emit("active", v.value, activeValue.value);
|
||||
activeValue.value = v.value;
|
||||
};
|
||||
const mouseleave = () => {
|
||||
clearTimeout(timeout.value);
|
||||
timeout.value = setTimeout(() => {
|
||||
dropdownVisibleChange(false);
|
||||
}, 100);
|
||||
};
|
||||
const change = (v) => {
|
||||
isChange.value = true;
|
||||
emit("change", v, initValue.value);
|
||||
};
|
||||
const dropdownVisibleChange = (v) => {
|
||||
if (v) {
|
||||
isChange.value = false;
|
||||
initValue.value = props.defaultValue;
|
||||
} else if (!isChange.value) {
|
||||
emit("active", initValue.value, activeValue.value);
|
||||
activeValue.value = initValue.value;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,190 @@
|
||||
<template>
|
||||
<div class="offset-tool">
|
||||
<div
|
||||
class="dish"
|
||||
@mousedown="mousedown"
|
||||
@touchstart="mousedown"
|
||||
ref="dishRef"
|
||||
>
|
||||
<span
|
||||
:style="{ top: data.top + '%', left: data.left + '%' }"
|
||||
></span>
|
||||
</div>
|
||||
<input
|
||||
class="top"
|
||||
type="range"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="0.1"
|
||||
v-model="data.top"
|
||||
@input="onInput"
|
||||
@change="onChange"
|
||||
/>
|
||||
<input
|
||||
class="left"
|
||||
type="range"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="0.1"
|
||||
v-model="data.left"
|
||||
@input="onInput"
|
||||
@change="onChange"
|
||||
/>
|
||||
<span class="tip"
|
||||
>x:{{ tofix(data.left) }}% y:{{ tofix(data.top) }}%</span
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, defineProps, defineEmits, watch } from "vue";
|
||||
const props = defineProps({
|
||||
top: {
|
||||
type: Number,
|
||||
default: 50,
|
||||
},
|
||||
left: {
|
||||
type: Number,
|
||||
default: 50,
|
||||
},
|
||||
});
|
||||
const tofix = (v: number | string) => Number(Number(v).toFixed(1));
|
||||
const emit = defineEmits(["change", "input"]);
|
||||
const data = reactive({
|
||||
top: tofix(props.top),
|
||||
left: tofix(props.left),
|
||||
});
|
||||
watch(
|
||||
() => props.top,
|
||||
(v) => (data.top = tofix(v))
|
||||
);
|
||||
watch(
|
||||
() => props.left,
|
||||
(v) => (data.left = tofix(v))
|
||||
);
|
||||
const dishRef = ref<HTMLDivElement>();
|
||||
const mousedown = (e: MouseEvent | TouchEvent) => {
|
||||
if (!dishRef.value) return;
|
||||
const mousemove = (e: MouseEvent | TouchEvent) => {
|
||||
if (!dishRef.value) return;
|
||||
const { left, top, width, height } =
|
||||
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 - left) / width) * 100;
|
||||
var y = ((Y - top) / height) * 100;
|
||||
if (x < 0) x = 0;
|
||||
if (x > 100) x = 100;
|
||||
if (y < 0) y = 0;
|
||||
if (y > 100) y = 100;
|
||||
data.left = tofix(x);
|
||||
data.top = tofix(y);
|
||||
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 = () => emit("input", { ...data });
|
||||
var changeTime: any = null;
|
||||
const onChange = () => {
|
||||
clearTimeout(changeTime);
|
||||
changeTime = setTimeout(() => emit("change", { ...data }), 500);
|
||||
};
|
||||
// var offsetTime = null;
|
||||
// watch(data, (v) => {
|
||||
// const obj = { ...v };
|
||||
// emit("input", obj);
|
||||
// clearTimeout(offsetTime);
|
||||
// offsetTime = setTimeout(() => emit("change", obj), 50);
|
||||
// });
|
||||
|
||||
// defineExpose({
|
||||
// open,
|
||||
// close,
|
||||
// });
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.offset-tool {
|
||||
width: 125px;
|
||||
height: 125px;
|
||||
display: flex;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
--gap: 15px;
|
||||
> .dish {
|
||||
margin: var(--gap) 0 0 var(--gap);
|
||||
flex: 1;
|
||||
border: 1px solid #000;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
background-color: #fff;
|
||||
> span {
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
position: absolute;
|
||||
top: 0%;
|
||||
left: 0%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: #000;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
> .tip {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
bottom: 0;
|
||||
font-size: 10px;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
color: #666;
|
||||
}
|
||||
> input.left {
|
||||
right: 0;
|
||||
}
|
||||
> input.top {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
transform-origin: left bottom;
|
||||
transform: rotate(90deg) translateX(-100%);
|
||||
}
|
||||
> input {
|
||||
position: absolute;
|
||||
width: calc(100% - var(--gap));
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
height: 8px;
|
||||
border-radius: 8px;
|
||||
background: rgba(0, 0, 0, 0.1); /* 更柔和的颜色 */
|
||||
// outline: none;
|
||||
&::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #4285f4; /* 蓝色滑块 */
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
&::-webkit-slider-thumb:hover {
|
||||
background: #3b77db;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
160
src/component/Canvas/CanvasEditor/components/tools/Slider.vue
Normal file
160
src/component/Canvas/CanvasEditor/components/tools/Slider.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<div class="slider">
|
||||
<div class="input-range">
|
||||
<span
|
||||
class="tip"
|
||||
:style="{
|
||||
'--progress': (value - props.min) / (props.max - props.min),
|
||||
}"
|
||||
>{{ props.tipFormatter(value) }}</span
|
||||
>
|
||||
<input
|
||||
type="range"
|
||||
v-model="value"
|
||||
:min="props.min"
|
||||
:max="props.max"
|
||||
:step="props.step"
|
||||
@input="onInput"
|
||||
@change="onChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="input" v-show="isInput">
|
||||
<input
|
||||
type="number"
|
||||
v-model="value"
|
||||
:min="props.min"
|
||||
:max="props.max"
|
||||
:step="props.step"
|
||||
@input="onInput"
|
||||
@change="onChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, defineProps, defineEmits, watch } from "vue";
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
min: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: 100,
|
||||
},
|
||||
step: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
tipFormatter: {
|
||||
type: Function,
|
||||
default: (v) => v,
|
||||
},
|
||||
isInput: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(["change", "input"]);
|
||||
const value = ref(props.value);
|
||||
watch(
|
||||
() => props.value,
|
||||
(v) => (value.value = v)
|
||||
);
|
||||
const onInput = () => emit("input", Number(value.value));
|
||||
var changeTime: any = null;
|
||||
const onChange = () => {
|
||||
clearTimeout(changeTime);
|
||||
changeTime = setTimeout(() => emit("change", Number(value.value)), 500);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.slider {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
--input-thumb-size: 12px;
|
||||
width: 150px;
|
||||
// &:focus-within,
|
||||
&:hover {
|
||||
> .input-range > .tip {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
> .input-range {
|
||||
position: relative;
|
||||
flex: 2;
|
||||
> input {
|
||||
width: 100%;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
height: 5px;
|
||||
border-radius: 5px;
|
||||
background: rgba(0, 0, 0, 0.1); /* 更柔和的颜色 */
|
||||
outline: none;
|
||||
&::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: var(--input-thumb-size);
|
||||
height: var(--input-thumb-size);
|
||||
border-radius: 50%;
|
||||
background: #4285f4; /* 蓝色滑块 */
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
&::-webkit-slider-thumb:hover {
|
||||
background: #3b77db;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
> .tip {
|
||||
position: absolute;
|
||||
font-size: 10px;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
color: #666;
|
||||
top: 0;
|
||||
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: 0.4rem 0.8rem;
|
||||
border-radius: 0.4rem;
|
||||
font-size: 1.2rem;
|
||||
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>
|
||||
Reference in New Issue
Block a user