深度画布形状属性设置
This commit is contained in:
@@ -13,6 +13,8 @@
|
||||
<div class="label">Rotation</div>
|
||||
<div class="value">
|
||||
<depth-input
|
||||
icon="dc-angle"
|
||||
size="8"
|
||||
v-model="angle"
|
||||
type="number"
|
||||
@input="inputFillTransform"
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<div class="content" v-if="isShow" v-show="show">
|
||||
<!-- <basic-info :object="activeObject" /> -->
|
||||
<fill-repeat :object="activeObject" v-if="isRepeat" />
|
||||
<!-- <shape-setting :object="activeObject" v-if="isShape && !isRepeat" /> -->
|
||||
<shape-setting :object="activeObject" v-if="isShape && !isRepeat" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -27,10 +27,10 @@
|
||||
const layers = computed(() => layerManager.layers.value)
|
||||
const activeObject = ref(null)
|
||||
|
||||
// const shapes = ['rect', 'line', 'path', 'triangle', 'polygon', 'ellipse']
|
||||
// const isShape = computed(() => shapes.includes(activeObject.value?.type))
|
||||
const shapes = ['rect', 'line', 'path', 'triangle', 'polygon', 'ellipse']
|
||||
const isShape = computed(() => shapes.includes(activeObject.value?.type))
|
||||
const isRepeat = computed(() => activeObject.value?.fill?.repeat === 'repeat')
|
||||
const isShow = computed(() => isRepeat.value)
|
||||
const isShow = computed(() => isRepeat.value || isShape.value)
|
||||
|
||||
const updateActiveObject = () => {
|
||||
const layer = layerManager.getActiveLayer()
|
||||
@@ -104,6 +104,10 @@
|
||||
width: 100%;
|
||||
height: auto;
|
||||
padding: 0 1.4rem;
|
||||
margin-bottom: 1.6rem;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.v > div {
|
||||
|
||||
@@ -1,21 +1,121 @@
|
||||
<template>
|
||||
<div class="shape-setting v">
|
||||
<!-- <div>
|
||||
<div class="label">填充颜色</div>
|
||||
<div class="value">
|
||||
<el-color-picker
|
||||
v-model="data.fill"
|
||||
show-alpha
|
||||
:predefine="['transparent', '#000', '#f00', '#0f0', '#00f']"
|
||||
@change="onChange"
|
||||
/>
|
||||
<div class="shape-setting h">
|
||||
<div>
|
||||
<div class="title">Position</div>
|
||||
<div class="content">
|
||||
<div>
|
||||
<depth-input
|
||||
before="X"
|
||||
type="number"
|
||||
v-model="data.left"
|
||||
@input="onInpot"
|
||||
@change="onChange"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<depth-input
|
||||
before="Y"
|
||||
type="number"
|
||||
v-model="data.top"
|
||||
@input="onInpot"
|
||||
@change="onChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="content">
|
||||
<div>
|
||||
<depth-input
|
||||
before="W"
|
||||
type="number"
|
||||
v-model="data.width"
|
||||
@input="onInpot"
|
||||
@change="onChange"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<depth-input
|
||||
before="H"
|
||||
type="number"
|
||||
v-model="data.height"
|
||||
@input="onInpot"
|
||||
@change="onChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="title">Scale</div>
|
||||
<div class="content">
|
||||
<div>
|
||||
<depth-input
|
||||
type="number"
|
||||
before="X"
|
||||
after="%"
|
||||
v-model="data.scaleX"
|
||||
@input="onInpot"
|
||||
@change="onChange"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<depth-input
|
||||
type="number"
|
||||
before="Y"
|
||||
after="%"
|
||||
v-model="data.scaleY"
|
||||
@input="onInpot"
|
||||
@change="onChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="title">Appearance</div>
|
||||
<div class="content">
|
||||
<div>
|
||||
<span class="label">Opacity</span>
|
||||
<depth-input
|
||||
type="number"
|
||||
after="%"
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
v-model="data.opacity"
|
||||
@input="onInpot"
|
||||
@change="onChange"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="object.type === 'rect'">
|
||||
<span class="label">Corner Radius</span>
|
||||
<depth-input
|
||||
type="number"
|
||||
v-model="data.radius"
|
||||
min="0"
|
||||
step="1"
|
||||
@input="onInpot"
|
||||
@change="onChange"
|
||||
/>
|
||||
</div>
|
||||
<div v-else></div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div>
|
||||
<span class="label">Color</span>
|
||||
<depth-input
|
||||
type="color"
|
||||
v-model="data.fill"
|
||||
after="%"
|
||||
@input="onInpot"
|
||||
@change="onChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, inject, computed, nextTick, onBeforeUnmount, reactive } from 'vue'
|
||||
import { ref, inject, computed, nextTick, onBeforeUnmount, reactive, watch } from 'vue'
|
||||
import { ElColorPicker } from 'element-plus'
|
||||
import { getTransformScaleAngle } from '../../manager/ObjectManager'
|
||||
import DepthOffsetTool from '../tools/depth-offset-tool.vue'
|
||||
@@ -32,22 +132,50 @@
|
||||
const id = computed(() => props.object.info.id)
|
||||
|
||||
const data = reactive({
|
||||
fill: '',
|
||||
stroke: '',
|
||||
strokeWidth: 0
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
opacity: 100,
|
||||
radius: 0,
|
||||
|
||||
fill: ''
|
||||
// stroke: '',
|
||||
// strokeWidth: 0
|
||||
})
|
||||
const updateData = async () => {
|
||||
await nextTick()
|
||||
data.top = Math.round(props.object.top)
|
||||
data.left = Math.round(props.object.left)
|
||||
data.width = Math.round(props.object.width)
|
||||
data.height = Math.round(props.object.height)
|
||||
data.scaleX = Math.round(props.object.scaleX * 100)
|
||||
data.scaleY = Math.round(props.object.scaleY * 100)
|
||||
data.opacity = Math.round(props.object.opacity * 100)
|
||||
data.radius = Math.round(props.object.rx)
|
||||
data.fill = props.object.fill
|
||||
data.stroke = props.object.stroke
|
||||
data.strokeWidth = props.object.strokeWidth
|
||||
// data.stroke = props.object.stroke
|
||||
// data.strokeWidth = props.object.strokeWidth
|
||||
}
|
||||
updateData()
|
||||
watch(() => props.object, updateData)
|
||||
|
||||
const onInpot = () => setPriority(false)
|
||||
const onChange = () => setPriority(true)
|
||||
const setPriority = (isRecord: boolean) => {
|
||||
const options = { ...data }
|
||||
const options = {
|
||||
...data,
|
||||
opacity: data.opacity / 100,
|
||||
scaleX: data.scaleX / 100,
|
||||
scaleY: data.scaleY / 100
|
||||
}
|
||||
if (props.object.type === 'rect') {
|
||||
options['rx'] = data.radius
|
||||
options['ry'] = data.radius
|
||||
}
|
||||
delete options.radius
|
||||
objectManager.updateProperty(id.value, options, isRecord)
|
||||
}
|
||||
|
||||
@@ -61,8 +189,30 @@
|
||||
|
||||
<style lang="less" scoped>
|
||||
.shape-setting {
|
||||
--details-item-margin-bottom: 1rem;
|
||||
--details-item-margin-bottom: 1.6rem;
|
||||
> div {
|
||||
> .content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
> div {
|
||||
flex: 1;
|
||||
margin-right: 3rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
--depth-input-height: 2.4rem;
|
||||
--depth-input-bg-color: rgba(249, 249, 250, 1);
|
||||
--depth-input-border-color: rgba(230, 230, 231, 1);
|
||||
--depth-input-decorate-color: rgba(69, 71, 84, 0.1);
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
> .label {
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="depth-input">
|
||||
<div class="depth-input" :class="{ color: isColor }">
|
||||
<span class="decorate"></span>
|
||||
<span v-show="icon" class="icon">
|
||||
<svg-icon :name="icon" :size="iconSize" size-unit="px" />
|
||||
@@ -13,12 +13,34 @@
|
||||
@copy.stop
|
||||
@keydown.stop
|
||||
/>
|
||||
<input
|
||||
v-if="isColor"
|
||||
readonly
|
||||
type="text"
|
||||
:value="colorObj.color"
|
||||
@copy.stop
|
||||
@keydown.stop
|
||||
/>
|
||||
<template v-if="isColor">
|
||||
<span class="decorate marginl"></span>
|
||||
<input
|
||||
class="alpha"
|
||||
type="number"
|
||||
:value="Math.round(colorObj.alpha * 100)"
|
||||
min="0"
|
||||
max="100"
|
||||
@input="(e) => onInput(e, 'alpha')"
|
||||
@change="(e) => onChange(e, 'alpha')"
|
||||
@copy.stop
|
||||
@keydown.stop
|
||||
/>
|
||||
</template>
|
||||
<span v-show="after" class="after">{{ after }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, useAttrs, watch } from 'vue'
|
||||
import { ref, useAttrs, watch, computed } from 'vue'
|
||||
const props = defineProps({
|
||||
modelValue: { type: [String, Number] },
|
||||
icon: { default: '', type: String },
|
||||
@@ -28,17 +50,169 @@
|
||||
})
|
||||
const attrs = useAttrs()
|
||||
const emit = defineEmits(['update:modelValue', 'input', 'change'])
|
||||
const onInput = (e) => {
|
||||
var value = e.target.value
|
||||
if (attrs.type === 'number') value = Number(value)
|
||||
emit('update:modelValue', value)
|
||||
emit('input', value)
|
||||
const isColor = computed(() => attrs.type === 'color')
|
||||
const colorObj = computed(() => {
|
||||
if (isColor.value && props.modelValue) {
|
||||
let color = parseColor(props.modelValue)
|
||||
color.color = `#${toHex(color.r)}${toHex(color.g)}${toHex(color.b)}`
|
||||
return color
|
||||
}
|
||||
return { color: '#fff', alpha: 1 }
|
||||
})
|
||||
|
||||
const inputtime = ref(null)
|
||||
const onInput = (e, type?: string) => {
|
||||
clearTimeout(inputtime.value)
|
||||
inputtime.value = setTimeout(() => {
|
||||
const value = handleInputChange(e, type)
|
||||
emit('update:modelValue', value)
|
||||
emit('input', value)
|
||||
}, 10)
|
||||
}
|
||||
const onChange = (e) => {
|
||||
const changetime = ref(null)
|
||||
const onChange = (e, type?: string) => {
|
||||
clearTimeout(changetime.value)
|
||||
changetime.value = setTimeout(() => {
|
||||
const value = handleInputChange(e, type)
|
||||
emit('update:modelValue', value)
|
||||
emit('change', value)
|
||||
}, 50)
|
||||
}
|
||||
// 处理输入事件
|
||||
const handleInputChange = (e, type?: string) => {
|
||||
var value = e.target.value
|
||||
if (attrs.type === 'number') value = Number(value)
|
||||
emit('update:modelValue', value)
|
||||
emit('change', value)
|
||||
if (isColor.value) {
|
||||
const rgba = { ...colorObj.value }
|
||||
if (type === 'alpha') {
|
||||
let a = value / 100
|
||||
value = `rgba(${rgba.r}, ${rgba.g}, ${rgba.b}, ${a})`
|
||||
} else {
|
||||
let { r, g, b } = parseColor(value)
|
||||
value = `rgba(${r}, ${g}, ${b}, ${rgba.a})`
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
function parseColor(colorString) {
|
||||
// 处理 rgba/rgb
|
||||
const rgbaMatch = colorString.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/i)
|
||||
if (rgbaMatch) {
|
||||
return {
|
||||
type: 'rgb',
|
||||
r: parseInt(rgbaMatch[1]),
|
||||
g: parseInt(rgbaMatch[2]),
|
||||
b: parseInt(rgbaMatch[3]),
|
||||
a: rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1,
|
||||
color: `#${toHex(rgbaMatch[1])}${toHex(rgbaMatch[2])}${toHex(rgbaMatch[3])}`,
|
||||
alpha: rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1
|
||||
}
|
||||
}
|
||||
|
||||
// 处理十六进制
|
||||
const hexMatch = colorString.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i)
|
||||
if (hexMatch) {
|
||||
let hex = hexMatch[1]
|
||||
if (hex.length === 3) {
|
||||
hex = hex
|
||||
.split('')
|
||||
.map((c) => c + c)
|
||||
.join('')
|
||||
}
|
||||
return {
|
||||
type: 'hex',
|
||||
r: parseInt(hex.substring(0, 2), 16),
|
||||
g: parseInt(hex.substring(2, 4), 16),
|
||||
b: parseInt(hex.substring(4, 6), 16),
|
||||
a: 1,
|
||||
color: `#${hex}`,
|
||||
alpha: 1
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 hsl/hsla
|
||||
const hslMatch = colorString.match(/hsla?\((\d+),\s*([\d.]+)%,\s*([\d.]+)%(?:,\s*([\d.]+))?\)/i)
|
||||
if (hslMatch) {
|
||||
const h = parseInt(hslMatch[1])
|
||||
const s = parseInt(hslMatch[2])
|
||||
const l = parseInt(hslMatch[3])
|
||||
const a = hslMatch[4] ? parseFloat(hslMatch[4]) : 1
|
||||
const rgb = hslToRgb(h, s, l)
|
||||
|
||||
return {
|
||||
type: 'hsl',
|
||||
h,
|
||||
s,
|
||||
l,
|
||||
r: rgb.r,
|
||||
g: rgb.g,
|
||||
b: rgb.b,
|
||||
a: a,
|
||||
color: `#${toHex(rgb.r)}${toHex(rgb.g)}${toHex(rgb.b)}`,
|
||||
alpha: a
|
||||
}
|
||||
}
|
||||
|
||||
// 处理颜色名称
|
||||
const namedColors = {
|
||||
red: '#ff0000',
|
||||
green: '#00ff00',
|
||||
blue: '#0000ff',
|
||||
black: '#000000',
|
||||
white: '#ffffff',
|
||||
yellow: '#ffff00',
|
||||
cyan: '#00ffff',
|
||||
magenta: '#ff00ff',
|
||||
transparent: 'rgba(0,0,0,0)'
|
||||
}
|
||||
|
||||
if (namedColors[colorString.toLowerCase()]) {
|
||||
return parseColor(namedColors[colorString.toLowerCase()])
|
||||
}
|
||||
|
||||
return parseColor('#fff')
|
||||
}
|
||||
// 将 0-255 的数字转换为十六进制
|
||||
function toHex(num) {
|
||||
// 确保数字在 0-255 范围内
|
||||
num = Math.min(255, Math.max(0, num))
|
||||
// 转换为十六进制,并确保两位数
|
||||
return num.toString(16).padStart(2, '0').toUpperCase()
|
||||
}
|
||||
// HSL 转 RGB
|
||||
function hslToRgb(h, s, l) {
|
||||
h = h / 360
|
||||
s = s / 100
|
||||
l = l / 100
|
||||
|
||||
let r, g, b
|
||||
|
||||
if (s === 0) {
|
||||
r = g = b = l
|
||||
} else {
|
||||
const hue2rgb = (p, q, t) => {
|
||||
if (t < 0) t += 1
|
||||
if (t > 1) t -= 1
|
||||
if (t < 1 / 6) return p + (q - p) * 6 * t
|
||||
if (t < 1 / 2) return q
|
||||
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6
|
||||
return p
|
||||
}
|
||||
|
||||
const q = l < 0.5 ? l * (1 + s) : l + s - l * s
|
||||
const p = 2 * l - q
|
||||
|
||||
r = hue2rgb(p, q, h + 1 / 3)
|
||||
g = hue2rgb(p, q, h)
|
||||
b = hue2rgb(p, q, h - 1 / 3)
|
||||
}
|
||||
|
||||
return {
|
||||
r: Math.round(r * 255),
|
||||
g: Math.round(g * 255),
|
||||
b: Math.round(b * 255)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
@@ -46,37 +220,46 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
border: 1px solid rgba(230, 230, 231, 1);
|
||||
border-radius: 1.7px;
|
||||
height: 17px;
|
||||
border: 1px solid var(--depth-input-border-color, rgba(230, 230, 231, 1));
|
||||
border-radius: 2px;
|
||||
height: var(--depth-input-height, 20px);
|
||||
background-color: var(--depth-input-bg-color, #fff);
|
||||
padding: 0 4px 0 2px;
|
||||
&.color {
|
||||
--depth-input-decorate-margin-right: 10px;
|
||||
--depth-input-input-margin-right: 10px;
|
||||
--depth-input-input-font-align: left;
|
||||
--depth-input-after-color: rgba(181, 181, 181, 1);
|
||||
}
|
||||
> .decorate {
|
||||
width: 2px;
|
||||
background-color: rgba(230, 230, 231, 1);
|
||||
border-radius: 3px;
|
||||
height: 85%;
|
||||
margin-right: 4px;
|
||||
background-color: var(--depth-input-decorate-color, rgba(230, 230, 231, 1));
|
||||
border-radius: 2px;
|
||||
height: 75%;
|
||||
margin-right: var(--depth-input-decorate-margin-right, 4px);
|
||||
}
|
||||
> .iconfont {
|
||||
font-size: 12px;
|
||||
color: #000;
|
||||
margin-right: 2px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
> .before {
|
||||
font-size: 12px;
|
||||
color: #000;
|
||||
margin-right: 2px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
> .after {
|
||||
font-size: 12px;
|
||||
color: #000;
|
||||
margin-left: 1px;
|
||||
color: var(--depth-input-after-color, #000);
|
||||
margin-left: 2px;
|
||||
}
|
||||
> input {
|
||||
font-size: 12px;
|
||||
width: 0;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
margin-right: var(--depth-input-input-margin-right, 0);
|
||||
text-align: var(--depth-input-input-font-align, right);
|
||||
outline: none;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
@@ -87,6 +270,28 @@
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
&.alpha {
|
||||
flex: 0.4;
|
||||
text-align: right;
|
||||
margin-right: 0;
|
||||
}
|
||||
&[type='color'] {
|
||||
flex: 0.5;
|
||||
border-radius: 2px;
|
||||
height: 70%;
|
||||
border: 1px solid rgb(42, 42, 42);
|
||||
display: block;
|
||||
&::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&::-webkit-color-swatch {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user