问题
I want to give my users an easy way to visually trace a route on a map or a picture. The solution must let the users add control points that they can use to put bends into the route.
It should work with html5 canvas - I currently use the Konvajs library so a solution that uses this would be good.
In the interests of sharing & learning, if you can suggest solutions using other HTML5 canvas libraries that would be good to see too.
Note: This is not the original question posed. However it emerged over time that this was the actual requirement. The OP asked for means to find an arbitrary point part way along a line / curve in an HTML5 canvas so that a draggable control point could be added at that point to edit the line / curve. The accepted answer does not meet this need. However, an answer to this original question would involve serious collision-detection math and potentially use of bezier control points - in other words it would be a big ask whilst the accepted answer is a very approachable solution with consistent UX.
The original question can be seen in via the edit links below this question.
回答1:
How about this idea. You click where you want the next point and the route line extends with new positioning handles along the line segments. If you need arrows you can extend the objects herein as you require. You can easily change colours, stroke width, circle opacity etc with attributes of the route class. The points are available in an array and in the standard Konva.js line points list. The JS is vanilla, no other libraries needed or used.
The Export button shows how to grab the (x,y) fixed point objects for export purposes.
Example video here, working code in below snippet.
// Set up the canvas / stage
var s1 = new Konva.Stage({container: 'container1', width: 600, height: 300});
// Add a layer for line
var lineLayer = new Konva.Layer({draggable: false});
s1.add(lineLayer);
// Add a layer for drag points
var pointLayer = new Konva.Layer({draggable: false});
s1.add(pointLayer);
// Add a rectangle to layer to catch events. Make it semi-transparent
var r = new Konva.Rect({x:0, y: 0, width: 600, height: 300, fill: 'black', opacity: 0.1})
pointLayer.add(r)
// Everything is ready so draw the canvas objects set up so far.
s1.draw()
// generic canvas end
// Class for the draggable point
// Params: route = the parent object, opts = position info, doPush = should we just make it or make it AND store it
var DragPoint = function(route, opts, doPush){
var route = route;
this.x = opts.x;
this.y = opts.y;
this.fixed = opts.fixed;
this.id = randId(); // random id.
if (doPush){ // in some cases we want to create the pt then insert it in the run of the array and not always at the end
route.pts.push(this);
}
// random id generator
function randId() {
return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(2, 10);
}
// mark the pt as fixed - important state, shown by filled point
this.makeFixed = function(){
this.fixed = true;
s1.find('#' + this.id)
.fill(route.fillColor);
}
this.kill = function(){
s1.find('#' + this.id)
.remove();
}
this.draw = function(){
// Add point & pt
var circleId = this.id;
var pt = new Konva.Circle({
id: circleId,
x: this.x,
y: this.y,
radius: route.pointRadius,
opacity: route.pointOpacity,
strokeWidth: 2,
stroke: route.strokeColor,
fill: 'transparent',
draggable: 'true'
})
pt.on('dragstart', function(){
route.drawState = 'dragging';
})
pt.on('dragmove', function(){
var pos = this.getPosition();
route.updatePt(this.id(), pos)
route.calc(this.id());
route.draw();
})
pt.on('dragend', function(){
route.drawState = 'drawing';
var pos = this.getPosition();
route.updatePt(this.getId(), pos);
route.splitPts(this.getId());
route.draw();
})
if (this.fixed){
this.makeFixed();
}
route.ptLayer.add(pt);
route.draw();
}
}
var Route = function() {
this.lineLayer = null;
this.ptLayer = null;
this.drawState = '';
this.fillColor = 'Gold';
this.strokeColor = 'Gold';
this.pointOpacity = 0.5;
this.pointRadius = 10;
this.color = 'LimeGreen';
this.width = 5;
this.pts = []; // array of dragging points.
this.startPt = null;
this.endPt = null;
// reset the points
this.reset = function(){
for (var i = 0; i < this.pts.length; i = i + 1){
this.pts[i].kill();
}
this.pts.length = 0;
this.draw();
}
// Add a point to the route.
this.addPt = function(pos, isFixed){
if (this.drawState === 'dragging'){ // do not add a new point because we were just dragging another
return null;
}
this.startPt = this.startPt || pos;
this.endPt = pos;
// create this new pt
var pt = new DragPoint(this, {x: this.endPt.x, y: this.endPt.y, fixed: isFixed}, true, "A");
pt.draw();
pt.makeFixed(); // always fixed for manual points
// if first point ignore the splitter process
if (this.pts.length > 0){
this.splitPts(pt.id, true);
}
this.startPt = this.endPt; // remember the last point
this.calc(); // calculate the line points from the array
this.draw(); // draw the line
}
// Position the points.
this.calc = function (draggingId){
draggingId = (typeof draggingId === 'undefined' ? '---' : draggingId); // when dragging an unfilled point we have to override its automatic positioning.
for (var i = 1; i < this.pts.length - 1; i = i + 1){
var d2 = this.pts[i];
if (!d2.fixed && d2.id !== draggingId){ // points that have been split are fixed, points that have not been split are repositioned mid way along their line segment.
var d1 = this.pts[i - 1];
var d3 = this.pts[i + 1];
var pos = this.getHalfwayPt(d1, d3);
d2.x = pos.x;
d2.y = pos.y;
}
s1.find('#' + d2.id).position({x: d2.x, y: d2.y}); // tell the shape where to go
}
}
// draw the line
this.draw = function (){
if (this.drawingLine){
this.drawingLine.remove();
}
this.drawingLine = this.newLine(); // initial line point
for (var i = 0; i < this.pts.length; i = i + 1){
this.drawingLine.points(this.drawingLine.points().concat([this.pts[i].x, this.pts[i].y]))
}
this.ptLayer.draw();
this.lineLayer.draw();
}
// When dragging we need to update the position of the point
this.updatePt = function(id, pos){
for (var i = 0; i < this.pts.length; i = i + 1){
if (this.pts[i].id === id){
this.pts[i].x = pos.x;
this.pts[i].y = pos.y;
break;
}
}
}
// Function to add and return a line object. We will extend this line to give the appearance of drawing.
this.newLine = function(){
var line = new Konva.Line({
stroke: this.color,
strokeWidth: this.width,
lineCap: 'round',
lineJoin: 'round',
tension : .1
});
this.lineLayer.add(line)
return line;
}
// make pts either side of the split
this.splitPts = function(id, force){
var idx = -1;
// find the pt in the array
for (var i = 0; i < this.pts.length; i = i + 1){
if (this.pts[i].id === id){
idx = i;
if (this.pts[i].fixed && !force){
return null; // we only split once.
}
//break;
}
}
// If idx is -1 we did not find the pt id !
if ( idx === -1){
return null
}
else if (idx === 0 ) {
return null
}
else { // pt not = 0 or max
// We are now going to insert a new pt either side of the one we just dragged
var d1 = this.pts[idx - 1]; // previous pt to the dragged pt
var d2 = this.pts[idx ]; // the pt pt
var d3 = this.pts[idx + 1]; // the next pt after the dragged pt
d2.makeFixed()// flag this pt as no longer splittable
// get point midway from prev pt and dragged pt
var pos = this.getHalfwayPt(d1, d2);
var pt = new DragPoint(this, {x: pos.x, y: pos.y, foxed: false}, false, "C");
pt.draw();
this.pts.splice(idx, 0, pt);
if (d3){
// get point midway from dragged pt to next
pos = this.getHalfwayPt(d2, d3);
var pt = new DragPoint(this, {x: pos.x, y: pos.y, foxed: false}, false, "D");
pt.draw();
this.pts.splice(idx + 2, 0, pt); // note idx + 2 !
}
}
}
// convert last point array entry to handy x,y object.
this.getPoint = function(pts){
return {x: pts[pts.length - 2], y: pts[pts.length - 1]};
}
this.getHalfwayPt = function(d1, d2){
var pos = {
x: d1.x + (d2.x - d1.x)/2,
y: d1.y + (d2.y - d1.y)/2
}
return pos;
}
this.exportPoints = function(){
var list = [], pt;
console.log('pts=' + this.pts.length)
for (var i = 0; i < this.pts.length; i = i + 1){
pt = this.pts[i]
if (pt.fixed){
console.log('push ' + i)
list.push({x: pt.x, y: pt.y});
}
}
return list;
}
}
var route = new Route();
route.lineLayer = lineLayer;
route.ptLayer = pointLayer;
route.fillColor = 'AliceBlue';
route.strokeColor = 'Red';
route.pointOpacity = 0.5;
route.pointRadius = 7;
route.color = '#2982E8'
// Listen for mouse up on the stage to know when to draw points
s1.on('mouseup touchend', function () {
route.addPt(s1.getPointerPosition(), true);
});
// jquery is used here simply as a quick means to make the buttons work.
// Controls for points export
$('#export').on('click', function(){
if ($(this).html() === "Hide"){
$(this).html('Export');
$('#points').hide();
}
else {
$(this).html('Hide');
$('#points')
.css('display', 'block')
.val(JSON.stringify(route.exportPoints()));
}
})
// reset button
$('#reset').on('click', function(){
route.reset();
})
p
{
padding: 4px;
}
#container1
{
background-image: url('https://i.stack.imgur.com/gADDJ.png');
}
#ctrl
{
position: absolute;
z-index: 10;
margin: 0px;
border: 1px solid red;
}
#points
{
width: 500px;
height: 100px;
display: none;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdn.rawgit.com/konvajs/konva/1.6.5/konva.min.js"></script>
<p>Click to add a point, click to add another, drag a point to make a bend, etc.
</p>
<div id='ctrl'>
<button id='reset'>Reset</button>
<button id='export'>Export</button>
<textarea id='points'></textarea>
</div>
<div id='container1' style="display: inline-block; width: 300px, height: 200px; background-color: silver; overflow: hidden; position: relative;"></div>
<div id='img'></div>
来源:https://stackoverflow.com/questions/53055061/solution-for-route-or-path-marking-with-bends