深度画布形状属性设置

This commit is contained in:
lzp
2026-03-25 17:27:46 +08:00
parent 03d1230441
commit 5faeded50b
5 changed files with 415 additions and 45 deletions

View File

@@ -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"

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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>