如何使用锚点修改曲线点?
本演示展示了如何创建可以通过拖动锚点进行修改的交互式曲线(抛物线和贝塞尔曲线)。这种技术通常用于矢量图形编辑器,使用户能够创建和调整自定义曲线。
说明: 使用鼠标或手指拖动锚点,以修改抛物线(红色)和贝塞尔曲线(蓝色)的曲率。
- Vanilla
- React
- Vue
import Konva from 'konva'; const width = window.innerWidth; const height = window.innerHeight; // 创建舞台和图层 const stage = new Konva.Stage({ container: 'container', width: width, height: height, }); const layer = new Konva.Layer(); stage.add(layer); // 构建锚点的函数 function buildAnchor(x, y) { const anchor = new Konva.Circle({ x: x, y: y, radius: 20, stroke: '#666', fill: '#ddd', strokeWidth: 2, draggable: true, }); layer.add(anchor); // 添加悬停样式 anchor.on('mouseover', function () { document.body.style.cursor = 'pointer'; this.strokeWidth(4); }); anchor.on('mouseout', function () { document.body.style.cursor = 'default'; this.strokeWidth(2); }); // 当锚点移动时更新曲线 anchor.on('dragmove', function () { updateDottedLines(); }); return anchor; } // 更新虚线点(显示控制点)的函数 function updateDottedLines() { const q = quad; const b = bezier; const quadLinePath = layer.findOne('#quadLinePath'); const bezierLinePath = layer.findOne('#bezierLinePath'); // 更新抛物线的控制点线 quadLinePath.points([ q.start.x(), q.start.y(), q.control.x(), q.control.y(), q.end.x(), q.end.y(), ]); // 更新贝塞尔曲线的控制点线 bezierLinePath.points([ b.start.x(), b.start.y(), b.control1.x(), b.control1.y(), b.control2.x(), b.control2.y(), b.end.x(), b.end.y(), ]); } // 创建具有自定义形状的抛物线 const quadraticLine = new Konva.Shape({ stroke: 'red', strokeWidth: 4, sceneFunc: (ctx, shape) => { ctx.beginPath(); ctx.moveTo(quad.start.x(), quad.start.y()); ctx.quadraticCurveTo( quad.control.x(), quad.control.y(), quad.end.x(), quad.end.y() ); ctx.fillStrokeShape(shape); }, }); layer.add(quadraticLine); // 创建具有自定义形状的贝塞尔曲线 const bezierLine = new Konva.Shape({ stroke: 'blue', strokeWidth: 5, sceneFunc: (ctx, shape) => { ctx.beginPath(); ctx.moveTo(bezier.start.x(), bezier.start.y()); ctx.bezierCurveTo( bezier.control1.x(), bezier.control1.y(), bezier.control2.x(), bezier.control2.y(), bezier.end.x(), bezier.end.y() ); ctx.fillStrokeShape(shape); }, }); layer.add(bezierLine); // 创建虚线以显示抛物线的控制点 const quadLinePath = new Konva.Line({ dash: [10, 10, 0, 10], strokeWidth: 3, stroke: 'black', lineCap: 'round', id: 'quadLinePath', opacity: 0.3, points: [0, 0], }); layer.add(quadLinePath); // 创建虚线以展示贝塞尔曲线的控制点 const bezierLinePath = new Konva.Line({ dash: [10, 10, 0, 10], strokeWidth: 3, stroke: 'black', lineCap: 'round', id: 'bezierLinePath', opacity: 0.3, points: [0, 0], }); layer.add(bezierLinePath); // 为抛物线创建锚点 const quad = { start: buildAnchor(60, 30), control: buildAnchor(240, 110), end: buildAnchor(80, 160), }; // 为贝塞尔曲线创建锚点 const bezier = { start: buildAnchor(280, 20), control1: buildAnchor(530, 40), control2: buildAnchor(480, 150), end: buildAnchor(300, 150), }; // 更新控制点线 updateDottedLines();
import React from 'react'; import { Stage, Layer, Circle, Line, Shape } from 'react-konva'; const ModifyCurvesDemo = () => { const width = window.innerWidth; const height = window.innerHeight; const [quadPoints, setQuadPoints] = React.useState({ start: { x: 60, y: 30 }, control: { x: 240, y: 110 }, end: { x: 80, y: 160 }, }); const [bezierPoints, setBezierPoints] = React.useState({ start: { x: 280, y: 20 }, control1: { x: 530, y: 40 }, control2: { x: 480, y: 150 }, end: { x: 300, y: 150 }, }); const [hoveredAnchor, setHoveredAnchor] = React.useState(null); const handleDragMove = (e, points, setPoints, pointName) => { setPoints({ ...points, [pointName]: { x: e.target.x(), y: e.target.y() } }); }; const handleCursor = (e, pointId, isEnter) => { const stage = e.target.getStage(); stage.container().style.cursor = isEnter ? 'pointer' : 'default'; setHoveredAnchor(isEnter ? pointId : null); }; const renderAnchor = (point, pointName, prefix, onDragMove) => ( <Circle key={prefix + pointName} x={point.x} y={point.y} radius={20} stroke="#666" fill="#ddd" strokeWidth={hoveredAnchor === prefix + pointName ? 4 : 2} draggable onDragMove={onDragMove} onMouseEnter={e => handleCursor(e, prefix + pointName, true)} onMouseLeave={e => handleCursor(e, prefix + pointName, false)} /> ); const quadAnchors = Object.entries(quadPoints).map(([name, point]) => renderAnchor(point, name, 'quad-', e => handleDragMove(e, quadPoints, setQuadPoints, name)) ); const bezierAnchors = Object.entries(bezierPoints).map(([name, point]) => renderAnchor(point, name, 'bezier-', e => handleDragMove(e, bezierPoints, setBezierPoints, name)) ); return ( <Stage width={width} height={height}> <Layer> <Shape sceneFunc={(ctx, shape) => { ctx.beginPath(); ctx.moveTo(quadPoints.start.x, quadPoints.start.y); ctx.quadraticCurveTo( quadPoints.control.x, quadPoints.control.y, quadPoints.end.x, quadPoints.end.y ); ctx.fillStrokeShape(shape); }} stroke="red" strokeWidth={4} /> <Shape sceneFunc={(ctx, shape) => { ctx.beginPath(); ctx.moveTo(bezierPoints.start.x, bezierPoints.start.y); ctx.bezierCurveTo( bezierPoints.control1.x, bezierPoints.control1.y, bezierPoints.control2.x, bezierPoints.control2.y, bezierPoints.end.x, bezierPoints.end.y ); ctx.fillStrokeShape(shape); }} stroke="blue" strokeWidth={5} /> <Line points={[ quadPoints.start.x, quadPoints.start.y, quadPoints.control.x, quadPoints.control.y, quadPoints.end.x, quadPoints.end.y ]} dash={[10, 10, 0, 10]} strokeWidth={3} stroke="black" lineCap="round" opacity={0.3} /> <Line points={[ bezierPoints.start.x, bezierPoints.start.y, bezierPoints.control1.x, bezierPoints.control1.y, bezierPoints.control2.x, bezierPoints.control2.y, bezierPoints.end.x, bezierPoints.end.y ]} dash={[10, 10, 0, 10]} strokeWidth={3} stroke="black" lineCap="round" opacity={0.3} /> {quadAnchors} {bezierAnchors} </Layer> </Stage> ); }; export default ModifyCurvesDemo;
<template> <v-stage :config="stageConfig"> <v-layer> <!-- 抛物线 --> <v-shape :config="quadraticConfig" /> <!-- 贝塞尔曲线 --> <v-shape :config="bezierConfig" /> <!-- 控制线 --> <v-line :config="quadLineConfig" /> <v-line :config="bezierLineConfig" /> <!-- 抛物线的锚点 --> <v-circle v-for="(point, name) in quadPoints" :key="`quad-${name}`" :config="{ x: point.x, y: point.y, radius: 20, stroke: '#666', fill: '#ddd', strokeWidth: hoveredPoint === `quad-${name}` ? 4 : 2, draggable: true }" @dragend="e => handleQuadDragEnd(e, name)" @mouseenter="handleMouseEnter(`quad-${name}`, e)" @mouseleave="handleMouseLeave(`quad-${name}`, e)" /> <!-- 贝塞尔曲线的锚点 --> <v-circle v-for="(point, name) in bezierPoints" :key="`bezier-${name}`" :config="{ x: point.x, y: point.y, radius: 20, stroke: '#666', fill: '#ddd', strokeWidth: hoveredPoint === `bezier-${name}` ? 4 : 2, draggable: true }" @dragend="e => handleBezierDragEnd(e, name)" @mouseenter="handleMouseEnter(`bezier-${name}`, e)" @mouseleave="handleMouseLeave(`bezier-${name}`, e)" /> </v-layer> </v-stage> </template> <script setup> import { ref, reactive, computed } from 'vue'; // 舞台配置 const stageConfig = { width: window.innerWidth, height: window.innerHeight }; const hoveredPoint = ref(null); // 抛物线的锚点 const quadPoints = reactive({ start: { x: 60, y: 30 }, control: { x: 240, y: 110 }, end: { x: 80, y: 160 } }); // 贝塞尔曲线的锚点 const bezierPoints = reactive({ start: { x: 280, y: 20 }, control1: { x: 530, y: 40 }, control2: { x: 480, y: 150 }, end: { x: 300, y: 150 } }); // 抛物线的配置 const quadraticConfig = computed(() => ({ stroke: 'red', strokeWidth: 4, sceneFunc: (ctx, shape) => { ctx.beginPath(); ctx.moveTo(quadPoints.start.x, quadPoints.start.y); ctx.quadraticCurveTo( quadPoints.control.x, quadPoints.control.y, quadPoints.end.x, quadPoints.end.y ); ctx.fillStrokeShape(shape); } })); // 贝塞尔曲线的配置 const bezierConfig = computed(() => ({ stroke: 'blue', strokeWidth: 5, sceneFunc: (ctx, shape) => { ctx.beginPath(); ctx.moveTo(bezierPoints.start.x, bezierPoints.start.y); ctx.bezierCurveTo( bezierPoints.control1.x, bezierPoints.control1.y, bezierPoints.control2.x, bezierPoints.control2.y, bezierPoints.end.x, bezierPoints.end.y ); ctx.fillStrokeShape(shape); } })); // 抛物线控制线的配置 const quadLineConfig = computed(() => ({ points: [ quadPoints.start.x, quadPoints.start.y, quadPoints.control.x, quadPoints.control.y, quadPoints.end.x, quadPoints.end.y ], dash: [10, 10, 0, 10], strokeWidth: 3, stroke: 'black', lineCap: 'round', opacity: 0.3 })); // 贝塞尔曲线控制线的配置 const bezierLineConfig = computed(() => ({ points: [ bezierPoints.start.x, bezierPoints.start.y, bezierPoints.control1.x, bezierPoints.control1.y, bezierPoints.control2.x, bezierPoints.control2.y, bezierPoints.end.x, bezierPoints.end.y ], dash: [10, 10, 0, 10], strokeWidth: 3, stroke: 'black', lineCap: 'round', opacity: 0.3 })); // 处理鼠标悬停效果的事件 const handleMouseEnter = (pointId, e) => { const stage = e.target.getStage(); stage.container().style.cursor = 'pointer'; hoveredPoint.value = pointId; }; const handleMouseLeave = (pointId, e) => { const stage = e.target.getStage(); stage.container().style.cursor = 'default'; hoveredPoint.value = null; }; // 处理抛物线锚点拖动结束 const handleQuadDragEnd = (e, pointName) => { quadPoints[pointName].x = e.target.x(); quadPoints[pointName].y = e.target.y(); }; // 处理贝塞尔曲线锚点拖动结束 const handleBezierDragEnd = (e, pointName) => { bezierPoints[pointName].x = e.target.x(); bezierPoints[pointName].y = e.target.y(); }; </script>