阶段11开发
This commit is contained in:
1 parent
8e9b83f95b
commit
8802bbe25e
21 files changed
+5285
-81
No files matched your search
@@ -8,8 +8,15 @@
|
||||
<line v-for="g in [0.25,0.5,0.75]" :key="'h'+g" x1="0" :y1="py(g)" :x2="px(1)" :y2="py(g)" />
|
||||
</g>
|
||||
<rect :x="px(0)" :y="py(1)" :width="px(1)-px(0)" :height="py(0)-py(1)" class="ce-frame" fill="none" />
|
||||
<!-- 折线 -->
|
||||
<polyline :points="polyPoints" class="ce-line" fill="none" />
|
||||
<path :d="curvePath" class="ce-line" fill="none" />
|
||||
<g v-if="mode === 'bezier'" class="ce-handles">
|
||||
<template v-for="(pt, i) in pts" :key="'handles-'+i">
|
||||
<line v-if="i > 0" :x1="px(pt.x)" :y1="py(pt.y)" :x2="px(inputHandle(i).x)" :y2="py(inputHandle(i).y)" />
|
||||
<circle v-if="i > 0" :cx="px(inputHandle(i).x)" :cy="py(inputHandle(i).y)" r="4" @pointerdown="onHandleDown($event, i, 'in')" />
|
||||
<line v-if="i < pts.length - 1" :x1="px(pt.x)" :y1="py(pt.y)" :x2="px(outputHandle(i).x)" :y2="py(outputHandle(i).y)" />
|
||||
<circle v-if="i < pts.length - 1" :cx="px(outputHandle(i).x)" :cy="py(outputHandle(i).y)" r="4" @pointerdown="onHandleDown($event, i, 'out')" />
|
||||
</template>
|
||||
</g>
|
||||
<!-- 节点:可见圈 r=5 + 命中圈 r=9 -->
|
||||
<g v-for="(pt,i) in pts" :key="i">
|
||||
<circle :cx="px(pt.x)" :cy="py(pt.y)" r="5" class="ce-node" />
|
||||
@@ -17,7 +24,7 @@
|
||||
@pointerdown="onNodeDown($event,i)" @dblclick="onNodeDbl(i,$event)" />
|
||||
</g>
|
||||
</svg>
|
||||
<div class="ce-nav"><span class="ce-tip">双击空白新建·双击节点删除·拖动节点调整</span></div>
|
||||
<div class="ce-nav"><span class="ce-tip">{{ mode === 'bezier' ? '拖动方形手柄调整弧度·双击空白新建·双击节点删除' : '双击空白新建·双击节点删除·拖动节点调整' }}</span></div>
|
||||
</div>
|
||||
<button class="ce-reset" type="button" title="恢复默认曲线" @click="resetCurve">重置</button>
|
||||
</div>
|
||||
@@ -26,15 +33,17 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
type CurvePoint = { x: number; y: number }
|
||||
type CurvePoint = { x: number; y: number; inX?: number; inY?: number; outX?: number; outY?: number }
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: CurvePoint[]
|
||||
defaultValue?: CurvePoint[]
|
||||
mode?: 'linear' | 'bezier'
|
||||
}>(), {
|
||||
defaultValue: () => [{ x: 0, y: 0 }, { x: 1, y: 1 }],
|
||||
mode: 'linear',
|
||||
})
|
||||
const emit = defineEmits<{ (e: 'update:modelValue', v: { x: number; y: number }[]): void }>()
|
||||
const emit = defineEmits<{ (e: 'update:modelValue', v: CurvePoint[]): void }>()
|
||||
|
||||
const svg = ref<SVGSVGElement | null>(null)
|
||||
const pad = 8 // 内边距(px),避免节点贴边
|
||||
@@ -54,17 +63,45 @@ function px(x: number) { return pad + x * (W.value - pad * 2) }
|
||||
// 归一化 y(0-1, y=1 在上) → 像素
|
||||
function py(y: number) { return pad + (1 - y) * (H.value - pad * 2) }
|
||||
|
||||
function normalized(): { x: number; y: number }[] {
|
||||
function normalized(): CurvePoint[] {
|
||||
return [...props.modelValue].sort((a, b) => a.x - b.x)
|
||||
}
|
||||
function set(v: { x: number; y: number }[]) { emit('update:modelValue', [...v].sort((a, b) => a.x - b.x)) }
|
||||
function set(v: CurvePoint[]) { emit('update:modelValue', [...v].sort((a, b) => a.x - b.x)) }
|
||||
|
||||
function resetCurve() {
|
||||
set(props.defaultValue.map((point) => ({ ...point })))
|
||||
}
|
||||
|
||||
const pts = computed(() => normalized())
|
||||
const polyPoints = computed(() => pts.value.map(p => `${px(p.x)},${py(p.y)}`).join(' '))
|
||||
|
||||
function defaultInputHandle(index: number) {
|
||||
const point = pts.value[index], previous = pts.value[Math.max(0, index - 1)]
|
||||
return { x: point.x - (point.x - previous.x) / 3, y: point.y - (point.y - previous.y) / 3 }
|
||||
}
|
||||
function defaultOutputHandle(index: number) {
|
||||
const point = pts.value[index], next = pts.value[Math.min(pts.value.length - 1, index + 1)]
|
||||
return { x: point.x + (next.x - point.x) / 3, y: point.y + (next.y - point.y) / 3 }
|
||||
}
|
||||
function inputHandle(index: number) {
|
||||
const point = pts.value[index], fallback = defaultInputHandle(index)
|
||||
return { x: point.x + (point.inX ?? fallback.x - point.x), y: point.y + (point.inY ?? fallback.y - point.y) }
|
||||
}
|
||||
function outputHandle(index: number) {
|
||||
const point = pts.value[index], fallback = defaultOutputHandle(index)
|
||||
return { x: point.x + (point.outX ?? fallback.x - point.x), y: point.y + (point.outY ?? fallback.y - point.y) }
|
||||
}
|
||||
const curvePath = computed(() => {
|
||||
if (!pts.value.length) return ''
|
||||
let path = `M ${px(pts.value[0].x)} ${py(pts.value[0].y)}`
|
||||
for (let index = 1; index < pts.value.length; index++) {
|
||||
const point = pts.value[index]
|
||||
if (props.mode === 'bezier') {
|
||||
const output = outputHandle(index - 1), input = inputHandle(index)
|
||||
path += ` C ${px(output.x)} ${py(output.y)} ${px(input.x)} ${py(input.y)} ${px(point.x)} ${py(point.y)}`
|
||||
} else path += ` L ${px(point.x)} ${py(point.y)}`
|
||||
}
|
||||
return path
|
||||
})
|
||||
|
||||
// 客户端坐标 → 归一化 (x:0-1 时间, y:0-1 透明度)
|
||||
function toNorm(clientX: number, clientY: number) {
|
||||
@@ -117,6 +154,28 @@ function onNodeDown(ev: PointerEvent, i: number) {
|
||||
window.addEventListener('pointerup', up)
|
||||
}
|
||||
|
||||
function onHandleDown(ev: PointerEvent, index: number, side: 'in' | 'out') {
|
||||
ev.stopPropagation()
|
||||
const move = (event: PointerEvent) => {
|
||||
const target = toNorm(event.clientX, event.clientY)
|
||||
const arr = normalized()
|
||||
const point = arr[index]
|
||||
const minimumX = side === 'in' ? arr[Math.max(0, index - 1)].x : point.x
|
||||
const maximumX = side === 'in' ? point.x : arr[Math.min(arr.length - 1, index + 1)].x
|
||||
const handleX = Math.min(maximumX, Math.max(minimumX, target.x))
|
||||
const dx = Math.round((handleX - point.x) * 100) / 100
|
||||
const dy = Math.round((target.y - point.y) * 100) / 100
|
||||
arr[index] = side === 'in' ? { ...point, inX: dx, inY: dy } : { ...point, outX: dx, outY: dy }
|
||||
set(arr)
|
||||
}
|
||||
const up = () => {
|
||||
window.removeEventListener('pointermove', move)
|
||||
window.removeEventListener('pointerup', up)
|
||||
}
|
||||
window.addEventListener('pointermove', move)
|
||||
window.addEventListener('pointerup', up)
|
||||
}
|
||||
|
||||
// 双击节点删除
|
||||
function onNodeDbl(i: number, ev?: MouseEvent) {
|
||||
ev?.stopPropagation()
|
||||
@@ -146,6 +205,9 @@ onUnmounted(() => { ro?.disconnect() })
|
||||
.ce-grid line { stroke: #1e1e30; stroke-width: 1; }
|
||||
.ce-frame { stroke: #2e2e44; stroke-width: 1; }
|
||||
.ce-line { stroke: #6ea0e8; stroke-width: 2; stroke-linejoin: round; }
|
||||
.ce-handles line { stroke: #8d96b5; stroke-width: 1; stroke-dasharray: 3 2; }
|
||||
.ce-handles circle { fill: #b7c2dd; stroke: #344d94; stroke-width: 1.2; cursor: grab; }
|
||||
.ce-handles circle:active { cursor: grabbing; }
|
||||
.ce-node { fill: #ffffff; stroke: #3a55aa; stroke-width: 1.6; pointer-events: none; }
|
||||
.ce-hit { fill: transparent; stroke: none; cursor: grab; }
|
||||
.ce-hit:active { cursor: grabbing; }
|
||||
|
||||
Reference in new issue
Block a user