Javascript 使用查询/角度绘制画布

Javascript 使用查询/角度绘制画布,javascript,jquery,angularjs,canvas,Javascript,Jquery,Angularjs,Canvas,以下是我想要实现的目标。用鼠标点击在屏幕上画一个圆(第一个圆)。然后连续单击鼠标绘制连续的圆圈,并将每个圆圈连接到第一个圆圈 我一直到这里 现在的任务是,如果任何一个圆的y坐标与第一个圆的y坐标相同,则连接是一条直线,否则它应该是一条s曲线/倒s曲线,这取决于下一个圆是在第一个圆的y轴之上还是之下 可以假设所有连续圆都位于第一个圆的右侧 这是我的密码 var app = angular.module('plunker', []); app.controller('MainController'

以下是我想要实现的目标。用鼠标点击在屏幕上画一个圆(第一个圆)。然后连续单击鼠标绘制连续的圆圈,并将每个圆圈连接到第一个圆圈

我一直到这里

现在的任务是,如果任何一个圆的y坐标与第一个圆的y坐标相同,则连接是一条直线,否则它应该是一条s曲线/倒s曲线,这取决于下一个圆是在第一个圆的y轴之上还是之下

可以假设所有连续圆都位于第一个圆的右侧

这是我的密码

var app = angular.module('plunker', []);

app.controller('MainController', function($scope) {


    var canvas=document.getElementById("canvas");
    var ctx=canvas.getContext("2d");
    var cw=canvas.width;
    var ch=canvas.height;

    function reOffset(){
      var BB=canvas.getBoundingClientRect();
      offsetX=BB.left;
      offsetY=BB.top;        
    }
    var offsetX,offsetY;
    reOffset();
    window.onscroll=function(e){ reOffset(); }

    var isDown=false;
    var startX,startY;

    var radius=10;
    var lastX,lastY;

    ctx.fillStyle='red';


  $("#canvas").mousedown(function(e){handleMouseDown(e);});


    function drawCircle(cx,cy){
      if(lastX){
        ctx.globalCompositeOperation='destination-over';
        ctx.beginPath();
        ctx.moveTo(lastX,lastY);
        ctx.lineTo(cx,cy);
        ctx.stroke();
        ctx.globalCompositeOperation='source-over';
      }else{
        lastX=cx;
        lastY=cy;
      }
      ctx.beginPath();
      ctx.arc(cx,cy,radius,0,Math.PI*2);
      ctx.closePath();
      ctx.fill();
    }

    function handleMouseDown(e){
      // tell the browser we're handling this event
      e.preventDefault();
      e.stopPropagation();

      mx=parseInt(e.clientX-offsetX);
      my=parseInt(e.clientY-offsetY);

      drawCircle(mx,my);
    }

});
这里有一个指向plunk的链接,可以演示该行为


非常感谢您的帮助。

我不知道您对哪种s曲线感兴趣。据我所知,它总是只有两个点连接,第一个点和其他点,你正在寻找某种二次曲线来连接。在这种情况下,您可以通过连接两个ctx.quadraticCurveTo调用来构建s曲线

ctx.beginPath();
ctx.moveTo(lastX,lastY);

ctx.quadraticCurveTo(
  (lastX+cx)/2, lastY,
  (lastX+cx)/2, (lastY+cy)/2
);
ctx.quadraticCurveTo(
  (lastX+cx)/2, cy,
  cx, cy
);

ctx.lineWidth = 3;

要使每个连接器避开现有的圆,必须使用寻路算法(例如a*)

寻路算法将为您提供一组从Circle1到Circle2的点,以避免所有其他圆

然后,可以使用该组点使用样条曲线在这些圆之间构建连接件。请参阅Stackoverflow的Ken Fyrstenberg关于如何绘制样条曲线的非常好的答案。确保样条曲线上的张力保持紧密(接近零),以便样条曲线连接器不会偏离无障碍路径太远:

这是一个很好的脚本,实现了Brian Grinstead的a*算法:

下面是Brian Grinstead的a*脚本演示:

为了避免只提供链接的答案,我在下面附上了来自GitHub的Brian脚本

但说真的…如果GitHub消失了,我们中的许多订户都有麻烦了

// javascript-astar 0.4.0
// http://github.com/bgrins/javascript-astar
// Freely distributable under the MIT License.
// Implements the astar search algorithm in javascript using a Binary Heap.
// Includes Binary Heap (with modifications) from Marijn Haverbeke.
// http://eloquentjavascript.net/appendix2.html

(function(definition) {
    /* global module, define */
    if(typeof module === 'object' && typeof module.exports === 'object') {
        module.exports = definition();
    } else if(typeof define === 'function' && define.amd) {
        define([], definition);
    } else {
        var exports = definition();
        window.astar = exports.astar;
        window.Graph = exports.Graph;
    }
})(function() {

function pathTo(node){
    var curr = node,
        path = [];
    while(curr.parent) {
        path.push(curr);
        curr = curr.parent;
    }
    return path.reverse();
}

function getHeap() {
    return new BinaryHeap(function(node) {
        return node.f;
    });
}

var astar = {
    /**
    * Perform an A* Search on a graph given a start and end node.
    * @param {Graph} graph
    * @param {GridNode} start
    * @param {GridNode} end
    * @param {Object} [options]
    * @param {bool} [options.closest] Specifies whether to return the
               path to the closest node if the target is unreachable.
    * @param {Function} [options.heuristic] Heuristic function (see
    *          astar.heuristics).
    */
    search: function(graph, start, end, options) {
        graph.cleanDirty();
        options = options || {};
        var heuristic = options.heuristic || astar.heuristics.manhattan,
            closest = options.closest || false;

        var openHeap = getHeap(),
            closestNode = start; // set the start node to be the closest if required

        start.h = heuristic(start, end);

        openHeap.push(start);

        while(openHeap.size() > 0) {

            // Grab the lowest f(x) to process next.  Heap keeps this sorted for us.
            var currentNode = openHeap.pop();

            // End case -- result has been found, return the traced path.
            if(currentNode === end) {
                return pathTo(currentNode);
            }

            // Normal case -- move currentNode from open to closed, process each of its neighbors.
            currentNode.closed = true;

            // Find all neighbors for the current node.
            var neighbors = graph.neighbors(currentNode);

            for (var i = 0, il = neighbors.length; i < il; ++i) {
                var neighbor = neighbors[i];

                if (neighbor.closed || neighbor.isWall()) {
                    // Not a valid node to process, skip to next neighbor.
                    continue;
                }

                // The g score is the shortest distance from start to current node.
                // We need to check if the path we have arrived at this neighbor is the shortest one we have seen yet.
                var gScore = currentNode.g + neighbor.getCost(currentNode),
                    beenVisited = neighbor.visited;

                if (!beenVisited || gScore < neighbor.g) {

                    // Found an optimal (so far) path to this node.  Take score for node to see how good it is.
                    neighbor.visited = true;
                    neighbor.parent = currentNode;
                    neighbor.h = neighbor.h || heuristic(neighbor, end);
                    neighbor.g = gScore;
                    neighbor.f = neighbor.g + neighbor.h;
                    graph.markDirty(neighbor);
                    if (closest) {
                        // If the neighbour is closer than the current closestNode or if it's equally close but has
                        // a cheaper path than the current closest node then it becomes the closest node
                        if (neighbor.h < closestNode.h || (neighbor.h === closestNode.h && neighbor.g < closestNode.g)) {
                            closestNode = neighbor;
                        }
                    }

                    if (!beenVisited) {
                        // Pushing to heap will put it in proper place based on the 'f' value.
                        openHeap.push(neighbor);
                    }
                    else {
                        // Already seen the node, but since it has been rescored we need to reorder it in the heap
                        openHeap.rescoreElement(neighbor);
                    }
                }
            }
        }

        if (closest) {
            return pathTo(closestNode);
        }

        // No result was found - empty array signifies failure to find path.
        return [];
    },
    // See list of heuristics: http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html
    heuristics: {
        manhattan: function(pos0, pos1) {
            var d1 = Math.abs(pos1.x - pos0.x);
            var d2 = Math.abs(pos1.y - pos0.y);
            return d1 + d2;
        },
        diagonal: function(pos0, pos1) {
            var D = 1;
            var D2 = Math.sqrt(2);
            var d1 = Math.abs(pos1.x - pos0.x);
            var d2 = Math.abs(pos1.y - pos0.y);
            return (D * (d1 + d2)) + ((D2 - (2 * D)) * Math.min(d1, d2));
        }
    },
    cleanNode:function(node){
        node.f = 0;
        node.g = 0;
        node.h = 0;
        node.visited = false;
        node.closed = false;
        node.parent = null;
    }
};

/**
* A graph memory structure
* @param {Array} gridIn 2D array of input weights
* @param {Object} [options]
* @param {bool} [options.diagonal] Specifies whether diagonal moves are allowed
*/
function Graph(gridIn, options) {
    options = options || {};
    this.nodes = [];
    this.diagonal = !!options.diagonal;
    this.grid = [];
    for (var x = 0; x < gridIn.length; x++) {
        this.grid[x] = [];

        for (var y = 0, row = gridIn[x]; y < row.length; y++) {
            var node = new GridNode(x, y, row[y]);
            this.grid[x][y] = node;
            this.nodes.push(node);
        }
    }
    this.init();
}

Graph.prototype.init = function() {
    this.dirtyNodes = [];
    for (var i = 0; i < this.nodes.length; i++) {
        astar.cleanNode(this.nodes[i]);
    }
};

Graph.prototype.cleanDirty = function() {
    for (var i = 0; i < this.dirtyNodes.length; i++) {
        astar.cleanNode(this.dirtyNodes[i]);
    }
    this.dirtyNodes = [];
};

Graph.prototype.markDirty = function(node) {
    this.dirtyNodes.push(node);
};

Graph.prototype.neighbors = function(node) {
    var ret = [],
        x = node.x,
        y = node.y,
        grid = this.grid;

    // West
    if(grid[x-1] && grid[x-1][y]) {
        ret.push(grid[x-1][y]);
    }

    // East
    if(grid[x+1] && grid[x+1][y]) {
        ret.push(grid[x+1][y]);
    }

    // South
    if(grid[x] && grid[x][y-1]) {
        ret.push(grid[x][y-1]);
    }

    // North
    if(grid[x] && grid[x][y+1]) {
        ret.push(grid[x][y+1]);
    }

    if (this.diagonal) {
        // Southwest
        if(grid[x-1] && grid[x-1][y-1]) {
            ret.push(grid[x-1][y-1]);
        }

        // Southeast
        if(grid[x+1] && grid[x+1][y-1]) {
            ret.push(grid[x+1][y-1]);
        }

        // Northwest
        if(grid[x-1] && grid[x-1][y+1]) {
            ret.push(grid[x-1][y+1]);
        }

        // Northeast
        if(grid[x+1] && grid[x+1][y+1]) {
            ret.push(grid[x+1][y+1]);
        }
    }

    return ret;
};

Graph.prototype.toString = function() {
    var graphString = [],
        nodes = this.grid, // when using grid
        rowDebug, row, y, l;
    for (var x = 0, len = nodes.length; x < len; x++) {
        rowDebug = [];
        row = nodes[x];
        for (y = 0, l = row.length; y < l; y++) {
            rowDebug.push(row[y].weight);
        }
        graphString.push(rowDebug.join(" "));
    }
    return graphString.join("\n");
};

function GridNode(x, y, weight) {
    this.x = x;
    this.y = y;
    this.weight = weight;
}

GridNode.prototype.toString = function() {
    return "[" + this.x + " " + this.y + "]";
};

GridNode.prototype.getCost = function(fromNeighbor) {
    // Take diagonal weight into consideration.
    if (fromNeighbor && fromNeighbor.x != this.x && fromNeighbor.y != this.y) {
        return this.weight * 1.41421;
    }
    return this.weight;
};

GridNode.prototype.isWall = function() {
    return this.weight === 0;
};

function BinaryHeap(scoreFunction){
    this.content = [];
    this.scoreFunction = scoreFunction;
}

BinaryHeap.prototype = {
    push: function(element) {
        // Add the new element to the end of the array.
        this.content.push(element);

        // Allow it to sink down.
        this.sinkDown(this.content.length - 1);
    },
    pop: function() {
        // Store the first element so we can return it later.
        var result = this.content[0];
        // Get the element at the end of the array.
        var end = this.content.pop();
        // If there are any elements left, put the end element at the
        // start, and let it bubble up.
        if (this.content.length > 0) {
            this.content[0] = end;
            this.bubbleUp(0);
        }
        return result;
    },
    remove: function(node) {
        var i = this.content.indexOf(node);

        // When it is found, the process seen in 'pop' is repeated
        // to fill up the hole.
        var end = this.content.pop();

        if (i !== this.content.length - 1) {
            this.content[i] = end;

            if (this.scoreFunction(end) < this.scoreFunction(node)) {
                this.sinkDown(i);
            }
            else {
                this.bubbleUp(i);
            }
        }
    },
    size: function() {
        return this.content.length;
    },
    rescoreElement: function(node) {
        this.sinkDown(this.content.indexOf(node));
    },
    sinkDown: function(n) {
        // Fetch the element that has to be sunk.
        var element = this.content[n];

        // When at 0, an element can not sink any further.
        while (n > 0) {

            // Compute the parent element's index, and fetch it.
            var parentN = ((n + 1) >> 1) - 1,
                parent = this.content[parentN];
            // Swap the elements if the parent is greater.
            if (this.scoreFunction(element) < this.scoreFunction(parent)) {
                this.content[parentN] = element;
                this.content[n] = parent;
                // Update 'n' to continue at the new position.
                n = parentN;
            }
            // Found a parent that is less, no need to sink any further.
            else {
                break;
            }
        }
    },
    bubbleUp: function(n) {
        // Look up the target element and its score.
        var length = this.content.length,
            element = this.content[n],
            elemScore = this.scoreFunction(element);

        while(true) {
            // Compute the indices of the child elements.
            var child2N = (n + 1) << 1,
                child1N = child2N - 1;
            // This is used to store the new position of the element, if any.
            var swap = null,
                child1Score;
            // If the first child exists (is inside the array)...
            if (child1N < length) {
                // Look it up and compute its score.
                var child1 = this.content[child1N];
                child1Score = this.scoreFunction(child1);

                // If the score is less than our element's, we need to swap.
                if (child1Score < elemScore){
                    swap = child1N;
                }
            }

            // Do the same checks for the other child.
            if (child2N < length) {
                var child2 = this.content[child2N],
                    child2Score = this.scoreFunction(child2);
                if (child2Score < (swap === null ? elemScore : child1Score)) {
                    swap = child2N;
                }
            }

            // If the element needs to be moved, swap it, and continue.
            if (swap !== null) {
                this.content[n] = this.content[swap];
                this.content[swap] = element;
                n = swap;
            }
            // Otherwise, we are done.
            else {
                break;
            }
        }
    }
};

return {
    astar: astar,
    Graph: Graph
};

});
//javascript astar 0.4.0
// http://github.com/bgrins/javascript-astar
//可根据麻省理工学院许可证自由分发。
//使用二进制堆在javascript中实现astar搜索算法。
//包括来自Marijn Haverbeke的二进制堆(带修改)。
// http://eloquentjavascript.net/appendix2.html
(功能(定义){
/*全局模块,定义*/
if(typeof module=='object'&&typeof module.exports=='object'){
module.exports=定义();
}else if(typeof define==='function'&&define.amd){
定义([],定义);
}否则{
var exports=definition();
window.astar=exports.astar;
window.Graph=exports.Graph;
}
})(功能(){
函数路径(节点){
var curr=节点,
路径=[];
while(当前父项){
路径推送(curr);
curr=curr.parent;
}
返回路径:reverse();
}
函数getHeap(){
返回新的二进制堆(函数(节点){
返回节点f;
});
}
var astar={
/**
*在给定开始和结束节点的图形上执行A*搜索。
*@param{Graph}图
*@param{GridNode}start
*@param{GridNode}end
*@param{Object}[选项]
*@param{bool}[options.closest]指定是否返回
如果无法到达目标,则指向最近节点的路径。
*@param{Function}[options.heuristic]启发式函数(参见
*启发法)。
*/
搜索:功能(图形、开始、结束、选项){
graph.cleandrity();
选项=选项| |{};
var heuristic=options.heuristic | | astar.heuristics.manhattan,
最近的=选项。最近的| |错误;
var openHeap=getHeap(),
closestNode=start;//如果需要,将开始节点设置为最接近的
start.h=启发式(开始,结束);
push(启动);
while(openHeap.size()>0){
//抓取最低的f(x)进行下一步处理。堆为我们保持排序。
var currentNode=openHeap.pop();
//End case——找到结果后,返回跟踪路径。
如果(当前节点===结束){
返回路径到(当前节点);
}
//正常情况——将currentNode从打开移动到关闭,处理其每个邻居。
currentNode.closed=true;
//查找当前节点的所有邻居。
var邻居=图形邻居(currentNode);
对于(变量i=0,il=0.length;i