I am trying to draw multiple parallel paths based on one set of coordinates like on this example:
I have my path created based on set of segments, then I clone it five times, and translate this way:
var myPath;
var lineData = []; // Long array of segments
myPath.segments = lineData;
for (var i = 1; i < 5; i++) {
var clone = myPath.clone();
clone.translate(new paper.Point(0, i*5));
}
And here is the result I get:
I want the lines to be full parallel but the distance is always different and they sometimes overlap. Is there a way to fix it or mayby i should try different approach in order to create this kind of curved lines?
A cubic bezier curve cannot be expanded to another parallel cubic bezier curve.
#Blindman67's method of calculating normals (perpendiculars to the tangent line) along the original curve & drawing dots on those perpendiculars will work.
To get started, see this SO Q&A that shows the algorithm to calculate points perpendicular to a Bezier curve.
Here's an example proof-of-concept:
If you just want solid parallel curves, then oversample by setting tCount higher (eg tCount=500). This proof-of-concept uses dots to create the line, but for solid curves you can use a set of line points.
To-Do if you want dotted lines: You'll need to oversample with the algorithm (maybe use tCount=500 instead of 60) and de-dupe the resulting point-set until you have points at uniform distance along the curve. Then your dots won't be sparse & clustered along the curve.
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
// variables defining a cubic bezier curve
var PI2=Math.PI*2;
var s={x:20,y:30};
var c1={x:200,y:40};
var c2={x:40,y:200};
var e={x:270,y:220};
// an array of points plotted along the bezier curve
var points=[];
// we use PI often so put it in a variable
var PI=Math.PI;
// plot 60 points along the curve
// and also calculate the angle of the curve at that point
var tCount=60;
for(var t=0;t<=tCount;t++){
var T=t/tCount;
// plot a point on the curve
var pos=getCubicBezierXYatT(s,c1,c2,e,T);
// calculate the perpendicular angle of the curve at that point
var tx = bezierTangent(s.x,c1.x,c2.x,e.x,T);
var ty = bezierTangent(s.y,c1.y,c2.y,e.y,T);
var a = Math.atan2(ty, tx)-PI/2;
// save the x/y position of the point and the perpendicular angle
// in the points array
points.push({
x:pos.x,
y:pos.y,
angle:a
});
}
var PI2=Math.PI*2;
var radii=[-12,-6,0,6,12];
// fill the background
ctx.fillStyle='navy';
ctx.fillRect(0,0,cw,ch);
// draw a dots perpendicular to each point on the curve
ctx.beginPath();
for(var i=0;i<points.length;i++){
for(var j=-2;j<3;j++){
var r=radii[j+2];
var x=points[i].x+r*Math.cos(points[i].angle);
var y=points[i].y+r*Math.sin(points[i].angle);
ctx.moveTo(x,y);
ctx.arc(x,y,1.5,0,PI2);
}
}
ctx.fillStyle='skyblue';
ctx.fill();
//////////////////////////////////////////
// helper functions
//////////////////////////////////////////
// calculate one XY point along Cubic Bezier at interval T
// (where T==0.00 at the start of the curve and T==1.00 at the end)
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;
}
// calculate the perpendicular angle at interval T on the curve
function bezierTangent(a, b, c, d, t) {
return (3 * t * t * (-a + 3 * b - 3 * c + d) + 6 * t * (a - 2 * b + c) + 3 * (-a + b));
};
body{ background-color: ivory; }
#canvas{border:1px solid red; margin:0 auto; }
<h4>Dotted parallel Bezier Curve.</h4>
<canvas id="canvas" width=300 height=300></canvas>
Related
find midpoint M of an path arc from A to B:
diagram:
i have :
point A(x,y)
point B(x,y)
radius of the arc
i tried following code but getPointAtLength is deprecated.
var myarc = document.getElementById("myarc");
// Get the length of the path
var pathLen = myarc.getTotalLength();
console.log(pathLen);
// How far along the path to we want the position?
var pathDistance = pathLen * 0.5;
console.log(pathDistance);
// Get the X,Y position
var midpoint = myarc.getPointAtLength(pathDistance)
console.log(myarc.getAttribute("d"));
console.log(midpoint);
Geometric calculation:
Сalculalate vector
AB = B - A (AB.x = B.x - A.x, similar for Y)
It's length
lAB = sqrt(AB.x*AB.x + AB.y*AB.y)
Normalized vector
uAB = AB / lAB
Middle point of chord
mAB = (A + B)/2
Arrow value
F = R - sqrt(R*R - lAB*lAB/4)
Now middle of arc:
M.x = mAB.x - uAB.Y * F
M.y = mAB.y + uAB.X * F
Note that there are two points (you need to know circle center orientation relatice to AB), for the second one change signs of the second terms
The title pretty much says it all. I'm looking for a convenient way to generate a geoJSON polygon defining an ellipse similar to d3-geo's d3.geoCircle()(); I want to use this GeoJSON ellipse with d3-geo. To clarify with and example, Cesium has this capability with a simple function allowing you to create an ellipse like so:
var ellipse = new Cesium.EllipseGeometry({
center : Cesium.Cartesian3.fromDegrees(-75.59777, 40.03883),
semiMajorAxis : 500000.0,
semiMinorAxis : 300000.0,
rotation : Cesium.Math.toRadians(60.0)
});
If that function returned GeoJSON I'd be set. What's the best way to generate a GeoJSON polygon defining an ellipse?
D3 doesn't offer anything that can really help here. Vanilla javascript can achieve this fairly easily. First let's create a geojson ellipse in Cartesian coordinate space. After, we can use the haversine formula to draw the ellipse.
Create a geojson ellipse in Cartesian coordinate space.
This is pretty straightforward, the method I'm using is to calculate the radius of the ellipse at a given angle. Using these polar coordinates we can stitch together an ellipse. The formula for the radius of an ellipse at a given point can be found pretty easily, I used this source, which gives us:
So, we can easily iterate through a series of angles, calculate the radius at that angle, and then translate this polar coordinate into a Cartesian coordinate. Perhaps something like:
function createEllipse(a,b,x=0,y=0,rotation=0) {
rotation = rotation / 180 * Math.PI;
var n = n = Math.ceil(36 * (Math.max(a/b,b/a))); // n sampling angles, more for more elongated ellipses
var coords = [];
for (var i = 0; i <= n; i++) {
// get the current angle
var θ = Math.PI*2/n*i + rotation;
// get the radius at that angle
var r = a * b / Math.sqrt(a*a*Math.sin(θ)*Math.sin(θ) + b*b*Math.cos(θ)*Math.cos(θ));
// get the x,y coordinate that marks the ellipse at this angle
x1 = x + Math.cos(θ-rotation) * r;
y1 = y + Math.sin(θ-rotation) * r;
coords.push([x1,y1]);
}
// return a geojson object:
return { "type":"Polygon", "coordinates":[coords] };
}
Note: a/b: axes (in pixels), x/y: center (in pixels), rotation: rotation in degrees
Here's that in a quick snippet:
var geojson = createEllipse(250,50,200,200,45);
var svg = d3.select("body")
.append("svg")
.attr("width",600)
.attr("height",500);
var path = d3.geoPath();
svg.append("path")
.datum(geojson)
.attr("d",path);
function createEllipse(a,b,x=0,y=0,rotation=0) {
rotation = rotation / 180 * Math.PI;
var n = n = Math.ceil(36 * (Math.max(a/b,b/a))); // n sample angles
var coords = [];
for (var i = 0; i <= n; i++) {
// get the current angle
var θ = Math.PI*2/n*i + rotation;
// get the radius at that angle
var r = a * b / Math.sqrt(a*a*Math.sin(θ)*Math.sin(θ) + b*b*Math.cos(θ)*Math.cos(θ));
// get the x,y coordinate that marks the ellipse at this angle
x1 = x + Math.cos(θ-rotation) * r;
y1 = y + Math.sin(θ-rotation) * r;
coords.push([x1,y1]);
}
// return a geojson object:
return { "type":"Polygon", "coordinates":[coords] };
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
Apply the haversine formula.
One of the best resources on the haversine and related functions I know of is at Moveable Type Scripts. The formula I have came from there a few years back and has had a few cosmetic modifications. I'm not going to break down the formula here, as the linked reference should be useful.
So, rather than calculating the Cartesian coordinates, we can take the polar coordinate and use the angle as bearing and the radius as distance in the haversine formula, which should be relatively trivial.
This could look like:
function createEllipse(a,b,x=0,y=0,rotation=0) {
var k = Math.ceil(36 * (Math.max(a/b,b/a))); // sample angles
var coords = [];
for (var i = 0; i <= k; i++) {
// get the current angle
var angle = Math.PI*2 / k * i + rotation
// get the radius at that angle
var r = a * b / Math.sqrt(a*a*Math.sin(angle)*Math.sin(angle) + b*b*Math.cos(angle)*Math.cos(angle));
coords.push(getLatLong([x,y],angle,r));
}
return { "type":"Polygon", "coordinates":[coords] };
}
function getLatLong(center,angle,radius) {
var rEarth = 6371000; // meters
x0 = center[0] * Math.PI / 180; // convert to radians.
y0 = center[1] * Math.PI / 180;
var y1 = Math.asin( Math.sin(y0)*Math.cos(radius/rEarth) + Math.cos(y0)*Math.sin(radius/rEarth)*Math.cos(angle) );
var x1 = x0 + Math.atan2(Math.sin(angle)*Math.sin(radius/rEarth)*Math.cos(y0), Math.cos(radius/rEarth)-Math.sin(y0)*Math.sin(y1));
y1 = y1 * 180 / Math.PI;
x1 = x1 * 180 / Math.PI;
return [x1,y1];
}
// Create & Render the geojson:
var geojson = createEllipse(500000,1000000,50,70); // a,b in meters, x,y, rotation in degrees.
var geojson2 = createEllipse(500000,1000000)
var svg = d3.select("body")
.append("svg")
.attr("width",600)
.attr("height",400);
var g = svg.append("g");
var projection = d3.geoMercator().translate([300,200]).scale(600/Math.PI/2);
var path = d3.geoPath().projection(projection);
g.selectAll("path")
.data([geojson,geojson2])
.enter().append("path")
.attr("d", path);
g.selectAll("circle")
.data([[50,70],[0,0]])
.enter().append("circle")
.attr("cx", function(d) { return projection(d)[0] })
.attr("cy", function(d) { return projection(d)[1] })
.attr("r", 4)
.attr("fill","orange");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
Note: a/b axes in meters, x,y,rotation in degrees
That's a pretty boring demonstration, perhaps this simple demonstration is better:
The formula I'm using assumes a earth is a sphere, not an ellipsoid, this can lead to errors in distance of up to 0.3%. However, depending on map scale, this will often be less than the stroke width.
I might have to try and make a particularly visually challenging version of a tissot's indicatrix with this
Snippets use default parameter values that are not compatible with IE, example block offers IE support
May be someone know good JavaScript library or framework for canvas drawing.
I have following issue:
1) draw some line via free-hand (like Paint/Photoshop pencil)
2) need convert line into "rectangle snake" (build a line using rectangles with fixed width)
look at screenshot
What kind of library will be better to use for this issue?
May be some kind of library alredy has this functionality?
I mean need following functionality:
line approximation
spline separation
converting line to polygons/shapes/objects
Will be great if someone will help me. Thanks!
As you say in your question, your points can be turned into a spline consisting of a set of cubic Bezier curves. Hint: you might simplify the point-set before calculating the spline of the remaining (fewer) points.
Here's how to calculate a set of rectangular-ish polygons along a cubic Bezier curve.
Sidenote: The polygons must be rectangular-ish because it's not possible to create a set of side-connected rectangles along a curved path.
Calculate a set of points along the curve.
Using the points in #1, calculate the tangent angle for each of the points.
Using the points & angles, calculate perpendicular lines extending outward in both directions from each point.
Use the endpoints of the perpendicular lines to build a set of polygons. Each new polygon is built from the prior & current perpendicular endpoints.
Here is annotated code and a Demo:
// canvas related variables
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
// variables defining a cubic bezier curve
var PI=Math.PI;
var PI2=PI*2;
var s={x:20,y:30};
var c1={x:300,y:40};
var c2={x:40,y:150};
var e={x:370,y:170};
// an array of points plotted along the bezier curve
var points=[];
// an array of polygons along the bezier curve
var polys=[];
// plot some points & tangent angles along the curve
for(var t=0;t<=100;t+=4){
var T=t/100;
// plot a point on the curve
var pos=getCubicBezierXYatT(s,c1,c2,e,T);
// calculate the tangent angle of the curve at that point
var tx = bezierTangent(s.x,c1.x,c2.x,e.x,T);
var ty = bezierTangent(s.y,c1.y,c2.y,e.y,T);
var a = Math.atan2(ty, tx)-PI/2;
// save the x/y position of the point and the tangent angle
points.push({
x:pos.x,
y:pos.y,
angle:a
});
}
// create polygons that extend on either side of the
// original points set
for(var i=1;i<points.length;i++){
var p0=points[i-1];
var p1=points[i];
polys.push({
x0:p0.x+20*Math.cos(p0.angle),
y0:p0.y+20*Math.sin(p0.angle),
x1:p1.x+20*Math.cos(p1.angle),
y1:p1.y+20*Math.sin(p1.angle),
x2:p1.x+20*Math.cos(p1.angle-PI),
y2:p1.y+20*Math.sin(p1.angle-PI),
x3:p0.x+20*Math.cos(p0.angle-PI),
y3:p0.y+20*Math.sin(p0.angle-PI),
});
}
// draw the polygons
for(var i=0;i<polys.length;i++){
var r=polys[i];
ctx.beginPath();
ctx.moveTo(r.x0,r.y0);
ctx.lineTo(r.x1,r.y1);
ctx.lineTo(r.x2,r.y2);
ctx.lineTo(r.x3,r.y3);
ctx.closePath();
ctx.fillStyle=randomColor();
ctx.fill();
ctx.stroke();
}
// draw the bezier curve points
ctx.beginPath();
ctx.moveTo(points[0].x,points[0].y);
for(var i=0;i<points.length;i++){
ctx.lineTo(points[i].x,points[i].y);
}
ctx.lineWidth=3;
ctx.strokeStyle='red';
ctx.stroke();
function randomColor(){
return('#'+Math.floor(Math.random()*16777215).toString(16));
}
//////////////////////////////////////////
// helper functions
//////////////////////////////////////////
// calculate one XY point along Cubic Bezier at interval T
// (where T==0.00 at the start of the curve and T==1.00 at the end)
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;
}
// calculate the tangent angle at interval T on the curve
function bezierTangent(a, b, c, d, t) {
return (3 * t * t * (-a + 3 * b - 3 * c + d) + 6 * t * (a - 2 * b + c) + 3 * (-a + b));
};
body{ background-color:white; padding:10px; }
#canvas{border:1px solid red; margin:0 auto; }
<h4>"Rectangular-ish" polygons along the red cubic bezier curve.</h4>
<canvas id="canvas" width=400 height=300></canvas>
Uniform width polygons:
Again, because they are rectangular-ish polygons, you will not be able to get precisely uniform widths. To get semi-uniform widths:
Calculate many points along the curve (maybe 1000+).
Use the distance formula to reduce the 1000+ point-set to a point-set with uniform distances along the curve: var distance=Math.sqrt( (pt1.x-pt0.x)*(pt1.x-pt0.x) + (pt1.y-pt0.y)*(pt1.y-pt0.y) )
I have created an ellipse on my canvas and now I need to draw three lines stemming from the origin. As an example let's say the first line is 90 degrees (vertical) so the point is (0, 10). I need the other two lines to be x pixels away from the point in both directions.
I'm sure I didn't describe this well enough but basically what I am trying to do is from a point on a known ellipse, find another point x distance away that lies on the ellipse.
I have tried looking for an arc of an ellipse but nothing seems to fit what I am looking for.
For an ellipse:
x = a cos(t)
y = b sin(t)
So:
x/a= cos(t)
t = acos(x/a)
y = b sin(acos(x/a))
Plug in your values of a, b, and x and you get y.
See https://www.mathopenref.com/coordparamellipse.html
Rather crudely:
var a=120;
var b=70;
var c=document.getElementById("myCanvas");
var cxt=c.getContext("2d");
var xCentre=c.width / 2;
var yCentre=c.height / 2;
// draw axes
cxt.strokeStyle='blue';
cxt.beginPath();
cxt.moveTo(0, yCentre);
cxt.lineTo(xCentre*2, yCentre);
cxt.stroke();
cxt.beginPath();
cxt.moveTo(xCentre, 0);
cxt.lineTo(xCentre, yCentre*2);
cxt.stroke();
// draw ellipse
cxt.strokeStyle='black';
cxt.beginPath();
for (var i = 0 * Math.PI; i < 2 * Math.PI; i += 0.01 ) {
xPos = xCentre - (a * Math.cos(i));
yPos = yCentre + (b * Math.sin(i));
if (i == 0) {
cxt.moveTo(xPos, yPos);
} else {
cxt.lineTo(xPos, yPos);
}
}
cxt.lineWidth = 2;
cxt.strokeStyle = "#232323";
cxt.stroke();
cxt.closePath();
// draw lines with x=+/- 40
var deltaX=40;
var y1=b*Math.sin(Math.acos(deltaX/a));
cxt.strokeStyle='red';
cxt.beginPath();
cxt.moveTo(xCentre+deltaX, yCentre-y1);
cxt.lineTo(xCentre, yCentre);
cxt.lineTo(xCentre-deltaX, yCentre-y1);
cxt.stroke();
<html>
<head><title>Ellipse</title></head>
<body>
<canvas id="myCanvas" style="position: absolute;" width="400" height="200"></canvas>
</body>
</html>
(Using https://www.scienceprimer.com/draw-oval-html5-canvas as a basis as I've never used HTML canvas before.)
Andrew Morton's answer is adequate, but you can it with one square root instead of a sin and an acos.
Suppose you have an ellipse centered at the origin, with a radius along the X-axis of a and a radius along the Y-axis of b. The equation of this ellipse is
x2/a2 + y2/b2 = 1.
Solving this for y gives
y = ± b sqrt(1 - x2/a2)
You can choose whichever sign is appropriate. Based on your post, you want the positive square root.
Translating to Javascript:
function yForEllipse(a, b, x) {
return b * Math.sqrt(1 - x*x / a * a);
}
I'm trying to develop a small application using html5 and canvas/KineticJS. I'd like to trace a number of rays that start from a 2d point to infinite, just setting a custom angle degree. For example, if I set 90° the app should render four rays (two straight lines, one vertical and one horizontal that meet in my 2d point). If I set 60° I should see 3 straight lines, like an asterisk *
The longest line you'll ever have to draw is the size of the canvas's diagonal:
var r = Math.sqrt(Math.pow(canvas.width, 2) + Math.pow(canvas.height, 2));
Use sin and cos to calculate each of your end points at that radius:
var theta = delta * Math.PI / 180.0;
var dx = r * Math.cos(n * theta);
var dy = r * Math.sin(n * theta);
Then, just draw lines from (x, y) to (x + dx, y + dy). Simples.