I need to draw concentric arcs of various sizes using raphael.js. I tried to understand the code behind http://raphaeljs.com/polar-clock.html, which is very similar to what I want, but, whithout comments, it is quite difficult to fathom.
Ideally, I would need a function that creates a path that is at a given distance from some center point, starts at some angle and ends at some other angle.
That answer is ok, but cant be animated. I ripped the important stuff out of polar-clock for you. Here is a red arc that animates growing. enjoy.
// Custom Arc Attribute, position x&y, value portion of total, total value, Radius
var archtype = Raphael("canvas", 200, 100);
archtype.customAttributes.arc = function (xloc, yloc, value, total, R) {
var alpha = 360 / total * value,
a = (90 - alpha) * Math.PI / 180,
x = xloc + R * Math.cos(a),
y = yloc - R * Math.sin(a),
path;
if (total == value) {
path = [
["M", xloc, yloc - R],
["A", R, R, 0, 1, 1, xloc - 0.01, yloc - R]
];
} else {
path = [
["M", xloc, yloc - R],
["A", R, R, 0, +(alpha > 180), 1, x, y]
];
}
return {
path: path
};
};
//make an arc at 50,50 with a radius of 30 that grows from 0 to 40 of 100 with a bounce
var my_arc = archtype.path().attr({
"stroke": "#f00",
"stroke-width": 14,
arc: [50, 50, 0, 100, 30]
});
my_arc.animate({
arc: [50, 50, 40, 100, 30]
}, 1500, "bounce");
Here's how I have done it. The following code allows you to specify a start and end angle as well as an inner and outer radius (useful for doing those trendy donut style pie charts). The solution doesn't rely on approximating a curve with line segments and can be animated as per the clock example mentioned in the original question.
First create your Raphael drawing area; the following assumes a div with id "raphael_paper" in your HTML file:
var paper = Raphael("raphael_paper", 800, 800);
to this Raphael object we add a custom arc attribute, a function which takes the center of a circle (x and y coords), a start angle, an end angle, an inner radius and an outer radius:
paper.customAttributes.arc = function (centerX, centerY, startAngle, endAngle, innerR, outerR) {
var radians = Math.PI / 180,
largeArc = +(endAngle - startAngle > 180);
// calculate the start and end points for both inner and outer edges of the arc segment
// the -90s are about starting the angle measurement from the top get rid of these if this doesn't suit your needs
outerX1 = centerX + outerR * Math.cos((startAngle-90) * radians),
outerY1 = centerY + outerR * Math.sin((startAngle-90) * radians),
outerX2 = centerX + outerR * Math.cos((endAngle-90) * radians),
outerY2 = centerY + outerR * Math.sin((endAngle-90) * radians),
innerX1 = centerX + innerR * Math.cos((endAngle-90) * radians),
innerY1 = centerY + innerR * Math.sin((endAngle-90) * radians),
innerX2 = centerX + innerR * Math.cos((startAngle-90) * radians),
innerY2 = centerY + innerR * Math.sin((startAngle-90) * radians);
// build the path array
var path = [
["M", outerX1, outerY1], //move to the start point
["A", outerR, outerR, 0, largeArc, 1, outerX2, outerY2], //draw the outer edge of the arc
["L", innerX1, innerY1], //draw a line inwards to the start of the inner edge of the arc
["A", innerR, innerR, 0, largeArc, 0, innerX2, innerY2], //draw the inner arc
["z"] //close the path
];
return {path: path};
};
now we can use this to draw arcs of a specified thickness, starting and ending wherever we want them to eg.
var redParams = {stroke: "#f00", "stroke-width": 1, fill:"#eee"},
greenParams = {stroke: "#0f0", "stroke-width": 1, fill:"#eee"},
blueParams = {stroke: "#00f", "stroke-width": 1, fill:"#eee"},
cx = 300, cy = 300, innerRadius = 100, outerRadius = 250,
var red = paper.path().attr(redParams).attr({arc: [cx, cy, 0, 90, innerRadius, outerRadius]});
var green = paper.path().attr(greenParams).attr({arc: [cx, cy, 270, 320, innerRadius, outerRadius]});
var blue = paper.path().attr(blueParams).attr({arc: [cx, cy, 95, 220, innerRadius, outerRadius]});
This should result in three grey arc segments with red, blue and green 1px borders.
Actually found the answer myself. I first thought of something fancy involving bezier curves, but this just works.
-> creates a path using SVG path syntax, which works as is with raphael
function arc(center, radius, startAngle, endAngle) {
angle = startAngle;
coords = toCoords(center, radius, angle);
path = "M " + coords[0] + " " + coords[1];
while(angle<=endAngle) {
coords = toCoords(center, radius, angle);
path += " L " + coords[0] + " " + coords[1];
angle += 1;
}
return path;
}
function toCoords(center, radius, angle) {
var radians = (angle/180) * Math.PI;
var x = center[0] + Math.cos(radians) * radius;
var y = center[1] + Math.sin(radians) * radius;
return [x, y];
}
Just to remove some guesswork from user592699's answer, this is the complete code that works:
<script src="raphael.js"></script>
<script>
var paper = Raphael(20, 20, 320, 320);
function arc(center, radius, startAngle, endAngle) {
angle = startAngle;
coords = toCoords(center, radius, angle);
path = "M " + coords[0] + " " + coords[1];
while(angle<=endAngle) {
coords = toCoords(center, radius, angle);
path += " L " + coords[0] + " " + coords[1];
angle += 1;
}
return path;
}
function toCoords(center, radius, angle) {
var radians = (angle/180) * Math.PI;
var x = center[0] + Math.cos(radians) * radius;
var y = center[1] + Math.sin(radians) * radius;
return [x, y];
}
paper.path(arc([100, 100], 80, 0, 270)); // draw an arc
// centered at (100, 100),
// radius 80, starting at degree 0,
// beginning at coordinate (80, 0)
// which is relative to the center
// of the circle,
// going clockwise, until 270 degree
</script>
For those who want the arc to be made with closed path and not stroke, I have extended genkilabs answer to make a solution. In cases when you need to give outer stroke to your arc, this might help.
// Custom Arc Attribute, position x&y, value portion of total, total value, Radius, width
var archtype = Raphael("canvas", 200, 100);
archtype.customAttributes.arc = function (xloc, yloc, value, total, R, width) {
if(!width) width = R * 0.4;
var alpha = 360 / total * value,
a = (90 - alpha) * Math.PI / 180,
w = width / 2,
r1 = R + w,
r2 = R - w,
x1 = xloc + r1 * Math.cos(a),
y1 = yloc - r1 * Math.sin(a),
x2 = xloc + r2 * Math.cos(a),
y2 = yloc - r2 * Math.sin(a),
path;
if (total == value) {
path = [
["M", xloc, yloc - r1],
["A", r1, r1, 0, 1, 1, xloc - 0.01, yloc - r1],
["Z"],
["M", xloc - 0.01, yloc - r2],
["A", r2, r2, 0, 1, 0, xloc, yloc - r2],
["Z"]
];
} else {
path = [
["M", xloc, yloc - r1],
["A", r1, r1, 0, +(alpha > 180), 1, x1, y1],
["L", x2, y2],
["A", r2, r2, 0, +(alpha > 180), 0, xloc, yloc - r2],
["L", xloc, yloc - r1],
["Z"]
];
}
return {
path: path
};
};
//make an arc at 50,50 with a radius of 30 that grows from 0 to 40 of 100 with a bounce
var my_arc = archtype.path().attr({
"fill": "#00f",
"stroke": "#f00",
"stroke-width": 5,
arc: [50, 50, 0, 100, 30]
});
my_arc.animate({
arc: [50, 50, 40, 100, 30]
}, 1500, "bounce");
JSFiddle
You can also do this without having to use loops. The following achieves this and works with negative angles as well.
Pass in a Raphael object as r. The angles start with 0 degrees, which is the top of the circle rather than the right as was listed in a couple of other solutions.
function drawArc(r, centerX, centerY, radius, startAngle, endAngle) {
var startX = centerX+radius*Math.cos((90-startAngle)*Math.PI/180);
var startY = centerY-radius*Math.sin((90-startAngle)*Math.PI/180);
var endX = centerX+radius*Math.cos((90-endAngle)*Math.PI/180);
var endY = centerY-radius*Math.sin((90-endAngle)*Math.PI/180);
var flg1 = 0;
if (startAngle>endAngle)
flg1 = 1;
else if (startAngle<180 && endAngle<180)
flg1 = 0;
else if (startAngle>180 && endAngle>180)
flg1 = 0;
else if (startAngle<180 && endAngle>180)
flg1 = 0; // edited for bugfix here, previously this was 1
else if (startAngle>180 && endAngle<180)
flg1 = 1;
return r.path([['M',startX, startY],['A',radius,radius,0,flg1,1,endX,endY]]);
};
I have adapted genkilabs answer to include rotation and inversion abilities. Also, how much of the ring is filled was changed to a single-number percent. (The inversion was adapted from this post). Hope it's helpful!
paper.customAttributes.arc = function (xloc, yloc, percent, rad, rot, invert) {
var alpha = 3.6 * percent,
a = (90 - alpha) * Math.PI / 180,
x = xloc + rad * Math.cos(a),
y = yloc - rad * Math.sin(a),
path;
if (invert) {
x = xloc - rad * Math.cos(a);
}
if (percent >= 100) {
path = [
["M", xloc, yloc - rad],
["A", rad, rad, 0, 1, 1, xloc - 0.01, yloc - rad]
];
} else {
path = [
["M", xloc, yloc - rad],
["A", rad, rad, 0, +(alpha > 180), +(!invert), x, y]
];
}
return {
path: path,
transform: "r"+rot+","+xloc+","+yloc,
};
};
Related
What I try to achieve is to rotate a line around rectangle center so it always stays in its bounds touching them (or having some padding).
Now I have the following routine for this, as you see I use tan calculations dividing my rectangle into 8 parts (red lines)
It works so far, but for some reason I have inconsistency using other calculation for radius drawing (green line), the lines won't always match as expected and I wonder why.
Basically the same could be achieved using just sin/cos calculations and finding cross points between lines and rect borders, but for some reason I could not get it to work.
std::pair<Point, Point>
MathUtils::calculateRotatingLine(Size size, double degrees)
{
auto width = size.width;
auto height = size.height;
double diagonalAngleTopRight = radiansToDegrees(atan((width / 2) / (height / 2)));
double diagonalAngleBottomRight = 90 + (90 - diagonalAngleTopRight);
double diagonalAngleBottomLeft = 180 + diagonalAngleTopRight;
double diagonalAngleTopLeft = 180 + diagonalAngleBottomRight;
double x, y;
/*
* *8*1*
* 7* *2
* 6* *3
* *5*4*
*/
// 1
if (degrees >= 0 && degrees <= diagonalAngleTopRight) {
x = width / 2 + height / 2 * tan(degreesToRadians(degrees));
y = 0;
}
// 2
else if (degrees > diagonalAngleTopRight && degrees <= 90) {
x = width;
y = width / 2 * tan(degreesToRadians(degrees - diagonalAngleTopRight));
}
// 3
else if (degrees > 90 && degrees <= diagonalAngleBottomRight) {
x = width;
y = height / 2 + width / 2 * tan(degreesToRadians(degrees - 90));
}
// 4
else if (degrees > diagonalAngleBottomRight && degrees <= 180) {
x = width - height / 2 * tan(degreesToRadians(degrees - diagonalAngleBottomRight));
y = height;
}
// 5
else if (degrees > 180 && degrees <= diagonalAngleBottomLeft) {
x = width / 2 - height / 2 * tan(degreesToRadians(degrees - 180));
y = height;
}
// 6
else if (degrees > diagonalAngleBottomLeft && degrees <= 270) {
x = 0;
y = height - width / 2 * tan(degreesToRadians(degrees - diagonalAngleBottomLeft));
}
// 7
else if (degrees > 270 && degrees <= diagonalAngleTopLeft) {
x = 0;
y = height / 2 - width / 2 * tan(degreesToRadians(degrees - 270));
}
// 8
else {
x = height / 2 * tan(degreesToRadians(degrees - diagonalAngleTopLeft));
y = 0;
}
return {Point{width / 2, height / 2}, Point{x, y}};
}
Green line calculation
Point
MathUtils::calculateCirclePoint(double radius, double degrees)
{
return {radius * cos(degreesToRadians(degrees)), radius * sin(degreesToRadians(degrees))};
}
EDIT
Awesome, it works thanks to #MBo
Point
MathUtils::calculateCrossPoint(Size size, double degrees)
{
auto x0 = size.width / 2;
auto y0 = size.height / 2;
auto vx = cos(degreesToRadians(degrees - 90));
auto vy = sin(degreesToRadians(degrees - 90));
//potential border positions
auto ex = vx > 0 ? size.width : 0;
auto ey = vy > 0 ? size.height : 0;
//check for horizontal/vertical directions
if (vx == 0) {
return {x0, ey};
}
if (vy == 0) {
return {ex, y0};
}
// in general case find times of intersections with horizontal and vertical edge line
auto tx = (ex - x0) / vx;
auto ty = (ey - y0) / vy;
// and get intersection for smaller parameter value
if (tx <= ty) {
return {ex, y0 + tx * vy};
}
return {x0 + ty * vx, ey};
}
Pseudocode to find intersection of ray emitted from rectangle center (with angle an in radians) with edges. (Works also for other (x0,y0) positions)
x0 = width / 2;
y0 = height / 2;
vx = cos(an);
vy = sin(an);
//potential border positions
ex = vx > 0? width: 0
ey = vy > 0? height: 0
//check for horizontal/vertical directions
if vx = 0 then
return cx = x0, cy = ey
if vy = 0 then
return cx = ex, cy = y0
//in general case find times of intersections with horizontal and vertical edge line
tx = (ex - x0) / vx
ty = (ey - y0) / vy
//and get intersection for smaller parameter value
if tx <= ty then
return cx = ex, cy = y0 + tx * vy
else
return cx = x0 + ty * vx, cy = ey
I'm trying to calculate the points in a cuboid given its centre (which is a Vector3) and the lengths of the sides along the x, y and z axis. I found the following on math.stackexchange.com: https://math.stackexchange.com/questions/107778/simplest-equation-for-drawing-a-cube-based-on-its-center-and-or-other-vertices which says I can use the following formulae:
The constructor for the World class is:
World::World(Vector3 o, float d1, float d2, float d3) : origin(o)
{
// If we consider an edge length to be d, we need to find r such that
// 2r = d in order to calculate the positions of each vertex in the world.
float r1 = d1 / 2,
r2 = d2 / 2,
r3 = d3 / 2;
for (int i = 0; i < 8; i++)
{
/* Sets up the vertices of the cube.
*
* #see http://bit.ly/1cc2RPG
*/
float x = o.getX() + (std::pow(-1, i&1) * r1),
y = o.getY() + (std::pow(-1, i&2) * r2),
z = o.getZ() + (std::pow(-1, i&4) * r3);
points[i] = Vector3(x, y, z);
std::cout << points[i] << "\n";
}
}
And I passing the following parameters to the constructor:
Vector3 o(0, 0, 0);
World w(o, 100.f, 100.f, 100.f);
The coordinates being output for all 8 vertices are:
(50, 50, 50)
(-50, 50, 50)
(50, 50, 50)
(-50, 50, 50)
(50, 50, 50)
(-50, 50, 50)
(50, 50, 50)
(-50, 50, 50)
Which cannot be correct. Any guidance would be very much appreciated!
The problem lies in the bitwise & inside your pow calls:
In the y and z components, they always return 0 and 2 or 4, respectively. -1^2 = -1^4 = 1, which is why the sign of these components is always positive. You could try (i&2)!=0 or (i&2) >> 1 for the y component instead. The same goes for the z component.
Change this:
float x = o.getX() + (std::pow(-1, i&1) * r1),
y = o.getY() + (std::pow(-1, i&2) * r2),
z = o.getZ() + (std::pow(-1, i&4) * r3);
To this:
float x = o.getX() + (std::pow(-1, (i ) & 1) * r1), // pow(-1, 0) == 1, pow(-1, 1) == -1
y = o.getY() + (std::pow(-1, (i >> 1) & 1) * r2), // pow(-1, 0) == 1, pow(-1, 1) == -1
z = o.getZ() + (std::pow(-1, (i >> 2) & 1) * r3); // pow(-1, 0) == 1, pow(-1, 1) == -1
Or even to this:
float x = o.getX() + (std::pow(-1, (i )) * r1), // pow(-1, {0, 2, 4, 6}) == 1, pow(-1, {1, 3, 5, 7}) == -1
y = o.getY() + (std::pow(-1, (i >> 1)) * r2), // pow(-1, {0, 2}) == 1, pow(-1, {1, 3}) == -1
z = o.getZ() + (std::pow(-1, (i >> 2)) * r3); // pow(-1, 0) == 1, pow(-1, 1) == -1
The problem is that as written even though the values you mask out identify weather or not the lengths need to be negated. They are not in the correct place value to get the desired properties from the exponentiation of -1.
Rewriting the code as I have above will solve this issue, however it would be more readable and in general more permanent just to unroll the loop and manually write if each one is an addition or subtraction without using the pow function.
I was able to find an example of a Polar clock at http://raphaeljs.com/polar-clock.html
I modified it to draw concentric circles, but I need the arc to start at 6 o'clock. I am trying to dissect how it works, but haven't been able to figure it out.
JS Fiddle:
http://jsfiddle.net/5frQ8/
var r = Raphael("holder", 600, 600);
// Custom Attribute
r.customAttributes.arc = function (value, total, R, color)
{
var alpha = 360 / total * value,
a = (90 - alpha) * Math.PI / 180,
x = 300 + R * Math.cos(a),
y = 300 - R * Math.sin(a),
path;
if (total == value)
{
path = [["M", 300, 300 - R], ["A", R, R, 0, 1, 1, 299.99, 300 - R]];
}
else
{
path = [["M", 300, 300 - R], ["A", R, R, 0, +(alpha > 180), 1, x, y]];
}
return {path: path, stroke: color,"stroke-width": 30};
};
//West
r.path().attr({arc: [575, 2000, 200, '#19A69C']});
//Total#
r.path().attr({arc: [1000, 2000, 160, '#FEDC38']});
//East
r.path().attr({arc: [425, 2000, 120, '#7BBD26']});
I have modified the main function to make the arcs start from 6 o'clock equivalent position. Please note that the formulae to find a point in polar coordinates are always:
x = centerX + radius * cos(angle)
y = centerY + radius * sin(angle)
Find the starting and ending points accordingly.
To change the starting angle by "delta", all angles should be added by "delta". Thus,
newAngle = angle + delta
The values of delta are -90 and +90 for the arcs to start from 12 o'clock and 6 o'clock respectively.
The arc drawing function is changed accordingly.
// Custom Attribute
r.customAttributes.arc = function (value, total, R, color)
{
var angleShift = 90,
alpha = 360 / total * value,
a = (alpha + angleShift) * Math.PI / 180,
x = 300 + R * Math.cos(a),
y = 300 + R * Math.sin(a),
path;
if (total == value)
{
path = [["M", 300, 300 + R], ["A", R, R, 0, 1, 1, 300.01, 300 + R]];
}
else
{
path = [["M", 300, 300 + R], ["A", R, R, 0, +(alpha > 180), 1, x, y]];
}
return {path: path, stroke: color,"stroke-width": 30};
};
see jsfiddle
Here's my code:
var paper = Raphael("holder");
function sector(cx, cy, r, startAngle, endAngle) {
var x1 = cx + r * Math.cos(-startAngle),
y1 = cy + r * Math.sin(-startAngle),
x2 = cx + r * Math.cos(-endAngle),
y2 = cy + r * Math.sin(-endAngle);
return ['M', cx, cy, 'L', x1, y1, 'A', r, r, 0, +(endAngle - startAngle > Math.PI), 0, x2, y2, 'z'];
}
var path = paper.path(sector(200, 200, 107, 0, 0.25)).attr({
'fill': '#fff',
'fill-opacity': 0.5,
'stroke': 'none'
});
path.animate({
path: sector(200, 200, 107, 0, Math.PI / 2)
}, 1000);
The problem is that in the intermediate animation it doesn't follow a circular path, I get this weird flattened thing instead:
How do I make the animation remain circular throughout?
I essentially want to create a "loading pie". The pie should animate into a full circle.
Animating from "empty" to 60% looks even worse: http://jsfiddle.net/mnbayazit/Fh43X/3/
Figured it out after digging through some of their examples.
var r = Raphael("holder");
r.customAttributes.segment = function (x, y, r, a1, a2) {
var flag = (a2 - a1) > 180,
clr = (a2 - a1) / 360;
a1 = (a1 % 360) * Math.PI / 180;
a2 = (a2 % 360) * Math.PI / 180;
return {
path: [
["M", x, y],
["l", r * Math.cos(a1), r * Math.sin(a1)],
["A", r, r, 0, +flag, 1, x + r * Math.cos(a2), y + r * Math.sin(a2)],
["z"]
]
};
};
var p = r.path().attr({
segment: [200, 200, 100, 0, 0],
stroke: 'none',
fill: '#fff',
'fill-opacity': 0.5
});
p.animate({
segment: [200, 200, 100, 0, 359]
}, 2000);
I am seeing an annoying wobble in my animation, I have stripped out the code which contains the actual animation:
var side = 400;
var paper = new Raphael($(this), 100, side);
paper.customAttributes.arc = function (xloc, yloc, value, total, R) {
var alpha = 360 / total * value,
a = (90 - alpha) * Math.PI / 180,
x = xloc + R * Math.cos(a),
y = yloc - R * Math.sin(a),
path;
if (total == value) {
path = [
["M", xloc, yloc - R],
["A", R, R, 0, 1, 1, xloc - 0.01, yloc - R]
];
} else {
path = [
["M", xloc, yloc - R],
["A", R, R, 0, +(alpha > 180), 1, x, y]
];
}
return {
path: path
};
};
var arcWidth = 180 - 120;
var strokeRadius = (120 + arcWidth/2);
var indicatorArc = paper.path().attr({
"stroke": "#4B6384",
"stroke-width": 100,
arc: [side/2, side/2, 0, 100, strokeRadius]
});
indicatorArc.animate({
arc: [side/2, side/2, 75, 100, strokeRadius]
}, 1500, "<>", function(){
// anim complete here
});
I have put it in a jsfiddle for you to see for yourself, please check in firefox and chrome, you will notice in chrome the edges wobble substantially, is there anything I can do to eliminate this?
fiddle here: run many times
So I know this is over two years old now, but if anyone comes across this issue, it's a known bug within Chrome's 2D drawing engine.
https://code.google.com/p/skia/issues/detail?id=2769