下午好,
我带着另一个课程/示例/答案的请求来找你。在HTML5中使用画布时,我已经学会了如何操纵深度(Z-Buffer)和其他一些巧妙的东西。但是,现在我正在尝试找到一种使用画布执行Pathfinding的方法。互联网上的大多数例子对我来说都有点难以理解,因为它们处理路径查找的方式与我想要实现的方式大不相同(这是他们使用基于区块的路径查找)。大多数其他示例似乎也处理框或矩形。
这是我用作绘制多边形的示例的代码: var canvas = document.getElementById(' CanvasPath'); var context = canvas.getContext(' 2d');
// begin custom shape
context.beginPath();
context.moveTo(170, 80);
context.bezierCurveTo(1, 200, 125, 230, 230, 150);
context.bezierCurveTo(250, 180, 320, 200, 340, 200);
context.bezierCurveTo(420, 150, 420, 120, 390, 100);
context.bezierCurveTo(430, 40, 370, 30, 340, 50);
context.bezierCurveTo(320, 5, 250, 20, 250, 50);
context.bezierCurveTo(200, 5, 150, 20, 170, 80);
context.closePath();
context.lineWidth = 2;
context.strokeStyle = 'gray';
context.stroke();
让我们说我有一个小盒子,我想要在那个Polygon中移动(我真的会用线点而不是Bezier曲线创建多边形。我只想在这里展示一个例子)当我点击目标时位置我希望它在...我怎样才能创建一个寻路方法,但是在多边形之外没有框触点的底部点?我假设我需要获取该多边形中的所有像素来创建路径?我想Bezier曲线和点可能需要创建并从数组中推出然后找到路径???
有关方法的任何建议,你能提供一个如何解决这个问题的例子吗?请保持温和......虽然我是一名经验丰富的脚本编写者和程序员,但我还不是很喜欢游戏或图形,我还在学习操纵HTML5中的画布。感谢您的帮助。
答案 0 :(得分:0)
您要问的关键部分是如何缩小多边形,使您的框可以沿着缩小的多边形移动,而不会延伸到原始多边形之外。
下图显示了原始的黑色多边形,缩小的红色多边形和蓝色行进框。
精确缩小多边形
精确缩小多边形的代码非常复杂。它涉及基于每个特定顶点是形成凸角还是凹角来重新定位原始多边形顶点。
这是我从Hans Muller's Blog on Webkits Shape-Padding派生的示例代码。此示例要求以顺时针顺序定义多边形顶点。
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
var shapePadding = 10;
var dragVertexIndex = null;
var hoverLocation = null;
var polygonVertexRadius = 9;
var polygon, marginPolygon, paddingPolygon;
var polygonVertices = [{x: 143, y: 327}, {x: 80, y: 236}, {x: 151, y: 148}, {x: 454, y: 69}, {x: 560, y: 320}];
var polygon = createPolygon(polygonVertices);
var paddingPolygon = createPaddingPolygon(polygon);
drawAll();
/////////////////////////////////
// Polygon creation and drawing
function drawAll(){
draw(polygon,'black');
draw(paddingPolygon,'red');
}
function draw(p,stroke){
var v=p.vertices;
ctx.beginPath();
ctx.moveTo(v[0].x,v[0].y);
for(var i=0;i<v.length;i++){
ctx.lineTo(v[i].x,v[i].y);
}
ctx.closePath();
ctx.strokeStyle=stroke;
ctx.stroke();
}
function createPolygon(vertices){
var polygon = {vertices: vertices};
var edges = [];
var minX = (vertices.length > 0) ? vertices[0].x : undefined;
var minY = (vertices.length > 0) ? vertices[0].y : undefined;
var maxX = minX;
var maxY = minY;
for (var i = 0; i < polygon.vertices.length; i++) {
vertices[i].label = String(i);
vertices[i].isReflex = isReflexVertex(polygon, i);
var edge = {
vertex1: vertices[i],
vertex2: vertices[(i + 1) % vertices.length],
polygon: polygon,
index: i
};
edge.outwardNormal = outwardEdgeNormal(edge);
edge.inwardNormal = inwardEdgeNormal(edge);
edges.push(edge);
var x = vertices[i].x;
var y = vertices[i].y;
minX = Math.min(x, minX);
minY = Math.min(y, minY);
maxX = Math.max(x, maxX);
maxY = Math.max(y, maxY);
}
polygon.edges = edges;
polygon.minX = minX;
polygon.minY = minY;
polygon.maxX = maxX;
polygon.maxY = maxY;
polygon.closed = true;
return polygon;
}
function createPaddingPolygon(polygon){
var offsetEdges = [];
for (var i = 0; i < polygon.edges.length; i++) {
var edge = polygon.edges[i];
var dx = edge.inwardNormal.x * shapePadding;
var dy = edge.inwardNormal.y * shapePadding;
offsetEdges.push(createOffsetEdge(edge, dx, dy));
}
var vertices = [];
for (var i = 0; i < offsetEdges.length; i++) {
var thisEdge = offsetEdges[i];
var prevEdge = offsetEdges[(i + offsetEdges.length - 1) % offsetEdges.length];
var vertex = edgesIntersection(prevEdge, thisEdge);
if (vertex)
vertices.push(vertex);
else {
var arcCenter = polygon.edges[i].vertex1;
appendArc(vertices, arcCenter, shapePadding, prevEdge.vertex2, thisEdge.vertex1, true);
}
}
var paddingPolygon = createPolygon(vertices);
paddingPolygon.offsetEdges = offsetEdges;
return paddingPolygon;
}
//////////////////////
// Support functions
function isReflexVertex(polygon, vertexIndex){
// Assuming that polygon vertices are in clockwise order
var thisVertex = polygon.vertices[vertexIndex];
var nextVertex = polygon.vertices[(vertexIndex + 1) % polygon.vertices.length];
var prevVertex = polygon.vertices[(vertexIndex + polygon.vertices.length - 1) % polygon.vertices.length];
if (leftSide(prevVertex, nextVertex, thisVertex) < 0){return true;} // TBD: return true if thisVertex is inside polygon when thisVertex isn't included
return false;
}
function inwardEdgeNormal(edge){
// Assuming that polygon vertices are in clockwise order
var dx = edge.vertex2.x - edge.vertex1.x;
var dy = edge.vertex2.y - edge.vertex1.y;
var edgeLength = Math.sqrt(dx*dx + dy*dy);
return {x: -dy/edgeLength, y: dx/edgeLength};
}
function outwardEdgeNormal(edge){
var n = inwardEdgeNormal(edge);
return {x: -n.x, y: -n.y};
}
// If the slope of line vertex1,vertex2 greater than the slope of vertex1,p then p is on the left side of vertex1,vertex2 and the return value is > 0.
// If p is colinear with vertex1,vertex2 then return 0, otherwise return a value < 0.
function leftSide(vertex1, vertex2, p){
return ((p.x - vertex1.x) * (vertex2.y - vertex1.y)) - ((vertex2.x - vertex1.x) * (p.y - vertex1.y));
}
function createOffsetEdge(edge, dx, dy){
return {
vertex1: {x: edge.vertex1.x + dx, y: edge.vertex1.y + dy},
vertex2: {x: edge.vertex2.x + dx, y: edge.vertex2.y + dy}
};
}
// based on http://local.wasp.uwa.edu.au/~pbourke/geometry/lineline2d/, edgeA => "line a", edgeB => "line b"
function edgesIntersection(edgeA, edgeB){
var den = (edgeB.vertex2.y - edgeB.vertex1.y) * (edgeA.vertex2.x - edgeA.vertex1.x) - (edgeB.vertex2.x - edgeB.vertex1.x) * (edgeA.vertex2.y - edgeA.vertex1.y);
if (den == 0){return null;} // lines are parallel or conincident
var ua = ((edgeB.vertex2.x - edgeB.vertex1.x) * (edgeA.vertex1.y - edgeB.vertex1.y) - (edgeB.vertex2.y - edgeB.vertex1.y) * (edgeA.vertex1.x - edgeB.vertex1.x)) / den;
var ub = ((edgeA.vertex2.x - edgeA.vertex1.x) * (edgeA.vertex1.y - edgeB.vertex1.y) - (edgeA.vertex2.y - edgeA.vertex1.y) * (edgeA.vertex1.x - edgeB.vertex1.x)) / den;
if (ua < 0 || ub < 0 || ua > 1 || ub > 1){ return null; }
return {x: edgeA.vertex1.x + ua * (edgeA.vertex2.x - edgeA.vertex1.x), y: edgeA.vertex1.y + ua * (edgeA.vertex2.y - edgeA.vertex1.y)};
}
function appendArc(vertices, center, radius, startVertex, endVertex, isPaddingBoundary){
const twoPI = Math.PI * 2;
var startAngle = Math.atan2(startVertex.y - center.y, startVertex.x - center.x);
var endAngle = Math.atan2(endVertex.y - center.y, endVertex.x - center.x);
if (startAngle < 0)
startAngle += twoPI;
if (endAngle < 0)
endAngle += twoPI;
var arcSegmentCount = 5; // An odd number so that one arc vertex will be eactly arcRadius from center.
var angle = ((startAngle > endAngle) ? (startAngle - endAngle) : (startAngle + twoPI - endAngle));
var angle5 = ((isPaddingBoundary) ? -angle : twoPI - angle) / arcSegmentCount;
vertices.push(startVertex);
for (var i = 1; i < arcSegmentCount; ++i) {
var angle = startAngle + angle5 * i;
var vertex = {
x: center.x + Math.cos(angle) * radius,
y: center.y + Math.sin(angle) * radius,
};
vertices.push(vertex);
}
vertices.push(endVertex);
}
&#13;
body{ background-color: ivory; }
#canvas{border:1px solid red;}
&#13;
<h4>Original black polygon and shrunken red polygon</h4>
<canvas id="canvas" width=650 height=400></canvas>
&#13;
计算旅行箱沿多边形的航点
要让您的盒子沿着多边形的内部移动,您需要一个包含多边形每条线的点的数组。您可以使用linear interpolation
计算这些点。
给定一个定义如下行的对象:
var line={
x0:50,
y0:50,
x1:200,
y1:50};
您可以使用线性插值来计算每1px沿该行的点,如下所示:
var allPoints = allLinePoints(line);
function allLinePoints(line){
var raw=[];
var dx = line.x1-line.x0;
var dy = line.y1-line.y0;
var length=Math.sqrt(dx*dx+dy*dy);
var segments=parseInt(length+1);
for(var i=0;i<segments;i++){
var percent=i/segments;
if(i==segments-1){
raw.push({x:line.x1,y:line.y1}); // force last point == p1
}else{
raw.push({ x:line.x0+dx*percent, y:line.y0+dy*percent});
}
}
return(raw);
}
获取距离鼠标最近的多边形点
您可以使用距离公式(从毕达哥拉斯定理导出)来计算哪个计算出的航点最接近鼠标点击位置:
// this will be the index in of the waypoint closest to the mouse
var indexOfClosestPoint;
// iterate all waypoints and find the closest to the mouse
var minLengthSquared=1000000*1000000;
for(var i=0;i<allPoints.length;i++){
var p=allPoints[i];
var dx=mouseX-p.x;
var dy=mouseY-p.y;
var dxy=dx*dx+dy*dy
if(dxy<minLengthSquared){
minLengthSquared=dxy;
indexOfClosestPoint=i;
}
}
将每个航点的方框设置为计算结束航路点
唯一要做的就是设置一个动画循环,重新绘制沿着多边形的每个航点的行进箱,直到它到达终点航点:
// start animating at the first waypoint
var animationIndex=0;
// use requestAnimationFrame to redraw the traveling box
// along each waypoint
function animate(time){
// redraw the line and the box at its current (percent) position
var pt=allPoints[animationIndex];
// redraw the polygon and the traveling box
ctx.strokeStyle='black';
ctx.lineWidth=1;
ctx.clearRect(0,0,cw,ch);
ctx.strokeStyle='black';
drawLines(linePoints);
ctx.strokeStyle='red';
drawLines(insideLinePoints);
ctx.fillStyle='skyblue';
ctx.fillRect(pt.x-boxWidth/2,pt.y-boxHeight/2,boxWidth,boxHeight);
// increase the percentage for the next frame loop
animationIndex++;
// Are we done?
if(animationIndex<=indexOfClosestPoint){
// request another frame loop
requestAnimationFrame(animate);
}else{
// set the flag indicating the animation is complete
isAnimating=false;
}
}
这就是你把它们放在一起时的样子
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(); }
// polygon vertices
var polygonVertices=[
{x:143,y:327},
{x:80,y:236},
{x:151,y:148},
{x:454,y:69},
{x:560,y:320},
];
var shrunkenVertices=getShrunkenVertices(polygonVertices,10);
var polyPoints=getPolygonPoints(shrunkenVertices)
//log(shrunkenVertices);
// animation variables
var isAnimating=false;
var animationIndex=0;
//
var indexOfClosestPoint=-99;
// define the movable box
var boxWidth=12;
var boxHeight=10;
var boxRadius=Math.sqrt(boxWidth*boxWidth+boxHeight*boxHeight);
var boxFill='skyblue';
// listen for mouse events
$("#canvas").mousedown(function(e){handleMouseDown(e);});
$("#canvas").mousemove(function(e){handleMouseMove(e);});
drawPolys(polygonVertices,shrunkenVertices);
////////////////////////////
// animate box to endpoint
////////////////////////////
function animate(time){
ctx.clearRect(0,0,cw,ch);
drawPolys(polygonVertices,shrunkenVertices);
// redraw the line and the box at its current (percent) position
var pt=polyPoints[animationIndex];
ctx.fillStyle=boxFill;
ctx.fillRect(pt.x-boxWidth/2,pt.y-boxHeight/2,boxWidth,boxHeight);
// increase the percentage for the next frame loop
animationIndex++;
// request another frame loop
if(animationIndex<=indexOfClosestPoint){
requestAnimationFrame(animate);
}else{
isAnimating=false;
}
}
////////////////////////////////////
// select box endpoint with click
////////////////////////////////////
function handleMouseDown(e){
// return if we're already animating
if(isAnimating){return;}
// tell the browser we're handling this event
e.preventDefault();
e.stopPropagation();
// start animating
animationIndex=0;
isAnimating=true;
requestAnimationFrame(animate);
}
function handleMouseMove(e){
// return if we're already animating
if(isAnimating){return;}
// tell the browser we're handling this event
e.preventDefault();
e.stopPropagation();
// get mouse position
mouseX=parseInt(e.clientX-offsetX);
mouseY=parseInt(e.clientY-offsetY);
// find the nearest waypoint
indexOfClosestPoint=findNearestPointToMouse(mouseX,mouseY);
// redraw
ctx.clearRect(0,0,cw,ch);
drawPolys(polygonVertices,shrunkenVertices);
// draw a red dot at the nearest waypoint
drawDot(polyPoints[indexOfClosestPoint],'red');
}
function findNearestPointToMouse(mx,my){
// find the nearest waypoint
var minLengthSquared=1000000*1000000;
for(var i=0;i<polyPoints.length;i++){
var p=polyPoints[i];
var dx=mouseX-p.x;
var dy=mouseY-p.y;
var dxy=dx*dx+dy*dy
if(dxy<minLengthSquared){
minLengthSquared=dxy;
indexOfClosestPoint=i;
}
}
return(indexOfClosestPoint);
}
////////////////////////////////
// Drawing functions
////////////////////////////////
function drawPolys(polygon,shrunken){
drawPoly(polygon,'black');
drawPoly(shrunken,'blue');
}
function drawPoly(v,stroke){
ctx.beginPath();
ctx.moveTo(v[0].x,v[0].y);
for(var i=0;i<v.length;i++){
ctx.lineTo(v[i].x,v[i].y);
}
ctx.closePath();
ctx.strokeStyle=stroke;
ctx.stroke();
}
function drawDot(pt,color){
ctx.beginPath();
ctx.arc(pt.x,pt.y,3,0,Math.PI*2);
ctx.closePath();
ctx.fillStyle=color;
ctx.fill();
}
////////////////////////////////
// Get points along a polygon
////////////////////////////////
function getPolygonPoints(vertices){
// For this purpose, be sure to close the polygon
var v=vertices.slice(0);
var v0=v[0];
var vx=v[v.length-1];
if(v0.x!==vx.x || v0.y!==vx.y){v.push(v[0]);}
//
var points=[];
for(var i=1;i<v.length;i++){
var p0=v[i-1];
var p1=v[i];
var line={x0:p0.x,y0:p0.y,x1:p1.x,y1:p1.y};
points=points.concat(getLinePoints(line));
}
return(points);
}
function getLinePoints(line){
var raw=[];
var dx = line.x1-line.x0;
var dy = line.y1-line.y0;
var length=Math.sqrt(dx*dx+dy*dy);
var segments=parseInt(length+1);
for(var i=0;i<segments;i++){
var percent=i/segments;
if(i==segments-1){
raw.push({x:line.x1,y:line.y1}); // force last point == p1
}else{
raw.push({ x:line.x0+dx*percent, y:line.y0+dy*percent});
}
}
return(raw);
}
/////////////////////////
// "shrink" a polygon
/////////////////////////
function getShrunkenVertices(vertices,shapePadding){
var polygon = createPolygon(polygonVertices);
var paddingPolygon = createPaddingPolygon(polygon,shapePadding);
return(paddingPolygon.vertices);
}
function createPolygon(vertices){
var polygon = {vertices: vertices};
var edges = [];
var minX = (vertices.length > 0) ? vertices[0].x : undefined;
var minY = (vertices.length > 0) ? vertices[0].y : undefined;
var maxX = minX;
var maxY = minY;
for (var i = 0; i < polygon.vertices.length; i++) {
vertices[i].label = String(i);
vertices[i].isReflex = isReflexVertex(polygon, i);
var edge = {
vertex1: vertices[i],
vertex2: vertices[(i + 1) % vertices.length],
polygon: polygon,
index: i
};
edge.outwardNormal = outwardEdgeNormal(edge);
edge.inwardNormal = inwardEdgeNormal(edge);
edges.push(edge);
var x = vertices[i].x;
var y = vertices[i].y;
minX = Math.min(x, minX);
minY = Math.min(y, minY);
maxX = Math.max(x, maxX);
maxY = Math.max(y, maxY);
}
polygon.edges = edges;
polygon.minX = minX;
polygon.minY = minY;
polygon.maxX = maxX;
polygon.maxY = maxY;
polygon.closed = true;
return polygon;
}
function createPaddingPolygon(polygon,shapePadding){
var offsetEdges = [];
for (var i = 0; i < polygon.edges.length; i++) {
var edge = polygon.edges[i];
var dx = edge.inwardNormal.x * shapePadding;
var dy = edge.inwardNormal.y * shapePadding;
offsetEdges.push(createOffsetEdge(edge, dx, dy));
}
var vertices = [];
for (var i = 0; i < offsetEdges.length; i++) {
var thisEdge = offsetEdges[i];
var prevEdge = offsetEdges[(i + offsetEdges.length - 1) % offsetEdges.length];
var vertex = edgesIntersection(prevEdge, thisEdge);
if (vertex)
vertices.push(vertex);
else {
var arcCenter = polygon.edges[i].vertex1;
appendArc(vertices, arcCenter, shapePadding, prevEdge.vertex2, thisEdge.vertex1, true);
}
}
var paddingPolygon = createPolygon(vertices);
paddingPolygon.offsetEdges = offsetEdges;
return paddingPolygon;
}
function isReflexVertex(polygon, vertexIndex){
// Assuming that polygon vertices are in clockwise order
var thisVertex = polygon.vertices[vertexIndex];
var nextVertex = polygon.vertices[(vertexIndex + 1) % polygon.vertices.length];
var prevVertex = polygon.vertices[(vertexIndex + polygon.vertices.length - 1) % polygon.vertices.length];
if (leftSide(prevVertex, nextVertex, thisVertex) < 0){return true;} // TBD: return true if thisVertex is inside polygon when thisVertex isn't included
return false;
}
function inwardEdgeNormal(edge){
// Assuming that polygon vertices are in clockwise order
var dx = edge.vertex2.x - edge.vertex1.x;
var dy = edge.vertex2.y - edge.vertex1.y;
var edgeLength = Math.sqrt(dx*dx + dy*dy);
return {x: -dy/edgeLength, y: dx/edgeLength};
}
function outwardEdgeNormal(edge){
var n = inwardEdgeNormal(edge);
return {x: -n.x, y: -n.y};
}
// If the slope of line vertex1,vertex2 greater than the slope of vertex1,p then p is on the left side of vertex1,vertex2 and the return value is > 0.
// If p is colinear with vertex1,vertex2 then return 0, otherwise return a value < 0.
function leftSide(vertex1, vertex2, p){
return ((p.x - vertex1.x) * (vertex2.y - vertex1.y)) - ((vertex2.x - vertex1.x) * (p.y - vertex1.y));
}
function createOffsetEdge(edge, dx, dy){
return {
vertex1: {x: edge.vertex1.x + dx, y: edge.vertex1.y + dy},
vertex2: {x: edge.vertex2.x + dx, y: edge.vertex2.y + dy}
};
}
// based on http://local.wasp.uwa.edu.au/~pbourke/geometry/lineline2d/, edgeA => "line a", edgeB => "line b"
function edgesIntersection(edgeA, edgeB){
var den = (edgeB.vertex2.y - edgeB.vertex1.y) * (edgeA.vertex2.x - edgeA.vertex1.x) - (edgeB.vertex2.x - edgeB.vertex1.x) * (edgeA.vertex2.y - edgeA.vertex1.y);
if (den == 0){return null;} // lines are parallel or conincident
var ua = ((edgeB.vertex2.x - edgeB.vertex1.x) * (edgeA.vertex1.y - edgeB.vertex1.y) - (edgeB.vertex2.y - edgeB.vertex1.y) * (edgeA.vertex1.x - edgeB.vertex1.x)) / den;
var ub = ((edgeA.vertex2.x - edgeA.vertex1.x) * (edgeA.vertex1.y - edgeB.vertex1.y) - (edgeA.vertex2.y - edgeA.vertex1.y) * (edgeA.vertex1.x - edgeB.vertex1.x)) / den;
if (ua < 0 || ub < 0 || ua > 1 || ub > 1){ return null; }
return {x: edgeA.vertex1.x + ua * (edgeA.vertex2.x - edgeA.vertex1.x), y: edgeA.vertex1.y + ua * (edgeA.vertex2.y - edgeA.vertex1.y)};
}
function appendArc(vertices, center, radius, startVertex, endVertex, isPaddingBoundary){
const twoPI = Math.PI * 2;
var startAngle = Math.atan2(startVertex.y - center.y, startVertex.x - center.x);
var endAngle = Math.atan2(endVertex.y - center.y, endVertex.x - center.x);
if (startAngle < 0)
startAngle += twoPI;
if (endAngle < 0)
endAngle += twoPI;
var arcSegmentCount = 5; // An odd number so that one arc vertex will be eactly arcRadius from center.
var angle = ((startAngle > endAngle) ? (startAngle - endAngle) : (startAngle + twoPI - endAngle));
var angle5 = ((isPaddingBoundary) ? -angle : twoPI - angle) / arcSegmentCount;
vertices.push(startVertex);
for (var i = 1; i < arcSegmentCount; ++i) {
var angle = startAngle + angle5 * i;
var vertex = {
x: center.x + Math.cos(angle) * radius,
y: center.y + Math.sin(angle) * radius,
};
vertices.push(vertex);
}
vertices.push(endVertex);
}
/////////////////////////
// End "shrink polygon"
/////////////////////////
&#13;
body{ background-color: ivory; }
#canvas{border:1px solid red;}
&#13;
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<h4>Move mouse to where you want the box to end up<br>Then click to start the box animating from start to end.<br>Note: The starting point is on the bottom left of the polygon</h4>
<canvas id="canvas" width=650 height=400></canvas>
&#13;
使用曲线路径
如果您想使用Bezier曲线建立闭合路径,则必须使用De Casteljau算法计算沿曲线的航路点。您需要对点数进行过采样 - 可能是500的T值介于0.00和1.00之间。这是算法的javascript版本,它计算沿着Cubic Bezier曲线的间隔T的x,y点:
// De Casteljau's algorithm which calculates points along a cubic Bezier curve
// plot a point at interval T along a bezier curve
// T==0.00 at beginning of curve. T==1.00 at ending of curve
// Calculating 300 T's between 0-1 will usually define the curve sufficiently
function getCubicBezierXYatT(startPt,controlPt1,controlPt2,endPt,T){
var x=CubicN(T,startPt.x,controlPt1.x,controlPt2.x,endPt.x);
var y=CubicN(T,startPt.y,controlPt1.y,controlPt2.y,endPt.y);
return({x:x,y:y});
}
// cubic helper formula at T distance
function CubicN(T, a,b,c,d) {
var t2 = T * T;
var t3 = t2 * T;
return a + (-a * 3 + T * (3 * a - a * T)) * T
+ (3 * b + T * (-6 * b + b * 3 * T)) * T
+ (c * 3 - c * 3 * T) * t2
+ d * t3;
}