I am trying to label some circles in a circle pack layout with text that flows along the circle itself.
Here is one experimental jsfiddle:
As you can see, it is possible to render text along the circle, centered at its top. Though browser's rendering of curved SVG text is terrible. But let's say we don't care about it.
Here is another jsfiddle
I would like to place curved labels on this graph, under these conditions:
The circle represents a province (only depth==1) (BRITISH COLUMBIA, ALBERTA, and so forth)
The sum of sizes of all children (in other words, number of parliament seats allotted) of the province is greater than 5.
The name of the province should be all UPPERCASE.
You can see some of my attempts in the code itself. I have been trying for hours. My main problem is that circles in the circle are now somewhere in X Y space, whereas, in the first jsfiddle, all circles have centers in coordinate system origin.
Maybe you can help me by taking a fresh look at this.
Underlying data is based on this table:
(NOTE: This is somewhat related to the question 'Circle packs as nodes of a D3 force layout' I asked the other day, however this is an independent experiment.)
I decided to use regular SVG arcs instead of d3.svg.arc(). I still think it is a right decision. However, here is what I have now: :) jsfiddle
NOTE (since I am answering my question): If some of you already spent time on this problem and found another solution, please post it, and I will accept your answer. Thanks to #FernOfTheAndes for participating in process of finding this solution, as it was filled with pain and misery of working with svg arcs.
Here is jsfiddle of the solution:
As mentioned in comments, the key part was generating arcs as plain vanilla svg arcs, not via d3.svg.arc().
SVG rules for defining arcs are clear, but a little hard to manage. Here is an interactive explorer of svg syntax for arcs.
Also, these two functions helped me during this process of defining the right arcs:
function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
var angleInRadians = (angleInDegrees-90) * Math.PI / 180.0;
return {
x: centerX + (radius * Math.cos(angleInRadians)),
y: centerY + (radius * Math.sin(angleInRadians))
};
}
function describeArc(x, y, radius, startAngle, endAngle){
var start = polarToCartesian(x, y, radius, endAngle);
var end = polarToCartesian(x, y, radius, startAngle);
var arcSweep = endAngle - startAngle <= 180 ? "0" : "1";
var d = [
"M", start.x, start.y,
"A", radius, radius, 0, 1, 1, end.x, end.y
].join(" ");
return d;
}
This is the code that actually directly generates curved labels:
var arcPaths = vis.append("g")
.style("fill","navy");
var labels = arcPaths.append("text")
.style("opacity", function(d) {
if (d.depth == 0) {
return 0.0;
}
if (!d.children) {
return 0.0;
}
var sumOfChildrenSizes = 0;
d.children.forEach(function(child){sumOfChildrenSizes += child.size;});
//alert(sumOfChildrenSizes);
if (sumOfChildrenSizes <= 5) {
return 0.0;
}
return 0.8;
})
.attr("font-size",10)
.style("text-anchor","middle")
.append("textPath")
.attr("xlink:href",function(d,i){return "#s"+i;})
.attr("startOffset",function(d,i){return "50%";})
.text(function(d){return d.name.toUpperCase();})
Fortunately, centering text on an arc was just a matter of setting the right property.
Just updating some of VividD's code.
The describeArc function was not working correctly for arcs with less than 180 degrees, and the start and end variables was flipped on the function and when calling it.
Here is an updated describeArc function that handles all alternatives of arcs, including small arcs and upside-down arcs:
function describeArc(x, y, radius, startAngle, endAngle) {
var start = polarToCartesian(x, y, radius, startAngle);
var end = polarToCartesian(x, y, radius, endAngle);
var arcLength = endAngle - startAngle;
if (arcLength < 0) arcLength += 360;
var longArc = arcLength >= 180 ? 1 : 0;
var d = [
"M", start.x, start.y,
"A", radius, radius, 0, longArc, 1, end.x, end.y
].join(" ");
return d;
}
With this change the function should be called with reversed start and end values that make more sense:
describeArc(d.x, d.y, d.r, -160, 160);
Related
Edit: I could divide the radius with the angle?
Problem: For the sake of learning the arts of collision in HTML5 Canvas, I am currently trying to get a full circle to collide with a segmented circle, in this case a semi circle.
What I Tried: My first thought was a simple circle to circle collision would do but I was wrong. I read various sources on collision detection but all of them were either the standard circle / circle, box / circle, box / box, or polygon collision formulas.
My Question: What is the formula for colliding a full circle with only a segmented circle? It seems something other than just the radius comes into play. Maybe the radians as well?
Attempt:
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var C1 = {x: 45, y: 65, radius: 20};
var C2 = {x: 60, y: 20, radius: 20};
var dx = C1.x - C2.x;
var dy = C1.y - C2.y;
var distance = Math.sqrt(dx * dx + dy * dy);
ctx.beginPath();
ctx.arc(C1.x, C1.y, C1.radius, 0, Math.PI * 2);
ctx.fillStyle = 'green';
ctx.fill();
ctx.beginPath();
ctx.rotate(0.3);
ctx.arc(C2.x, C2.y, C2.radius, 0, Math.PI * 1);
ctx.fillStyle = 'red';
ctx.fill();
if (distance < C1.radius + C2.radius) {
alert('true')
}
else {
alert('false')
}
A demo for to play around with: jsfiddle.net/tonyh90g/1
My learning resource: https://developer.mozilla.org/en-US/docs/Games/Techniques/2D_collision_detection
You're on the right tracks, you will indeed need to not only calculate the distance between centres but also the angle of the intersection point(s) on the segment.
In general cartesian coordinates that angle can be calculated using Math.atan2(dy, dx) where dx and dy are the difference in coordinates from the segment to the circle. If those angles fall between the segment's angle, you have a hit.
[NB: At the exact point of touching there's only one point, but if the two objects end up slightly overlapping, which is not uncommon in animation, you'll get two points].
It's also possible that the circle could intersect the line segments rather than the round portion of the segment, but I think those cases will be correctly caught anyway. This may require further investigation, and if required would need a line-segment / circle intersection test.
I also note in your fiddle that you're using rotate() transforms on the sector. This will foul up your angle calculations unless you specifically account for it. It may be easier to use absolute angles throughout.
I am looking for a way to draw multiple rectangles around a circle at an angle facing the centre. What i have so far, currently just drawing the rectangles around a circle facing one direction lacking the angle inclination towards the centre -
https://thysultan.com/projects/thyplayer/
What i want is for the rectangles to incline at an angle such that each rectangle is facing the centre of the circle at it's designated position.
How would one do that?
Trigonometry approach
To animate the lines towards the center you can use either transforms or trigonometry math. Personally I find it easier with math in cases like this so here is such an example.
See #markE's answer for an example on how to use transforms (transforms can be easier on the eye and in the code in general).
Some prerequisites:
We know canvas is oriented so that a 0° angle points towards right. This is essential if you want to mark the frequency range somehow.
We need to calculate the length of the line from outer radius towards center (inner radius)
We need to calculate the end points of the line based on the angle.
An easy solution can be to make a function that calculates a single line at a center (cx, cy) at a certain angle (in radians), with t as length, t being in the range [0, 1] (as the FFT floating point buffer bins). We also provide an outer and inner radius to enable limiting the line:
function getLine(cx, cy, angle, t, oRadius, iRadius) {
var radiusDiff = oRadius - iRadius, // calc radius diff to get max length
length = radiusDiff * t; // now we have the line length
return {
x1: oRadius * Math.cos(angle), // x1 point (outer)
y1: oRadius * Math.sin(angle), // y1 point (outer)
x2: (oRadius - length) * Math.cos(angle), // x2 point (inner)
y2: (oRadius - length) * Math.sin(angle) // y2 point (inner)
}
}
All we need to do now is to feed it data from the FFT analyzer.
NB: Since all lines points towards center, you will have a crowded center area. Something to have in mind when determine the line widths and inner radius as well as number of bins to use.
Example demo
For the example I will just use some random data for the "FFT" and plot 64 bins.
// angle - in radians
function getLine(cx, cy, angle, t, oRadius, iRadius) {
var radiusDiff = oRadius - iRadius, // calc radius diff to get max length
length = radiusDiff * t; // now we have the line length
return {
x1: cx + oRadius * Math.cos(angle), // x1 point (outer)
y1: cy + oRadius * Math.sin(angle), // y1 point (outer)
x2: cx + (oRadius - length) * Math.cos(angle), // x2 point (inner)
y2: cy + (oRadius - length) * Math.sin(angle) // y2 point (inner)
}
}
// calculate number of steps based on bins
var ctx = document.querySelector("canvas").getContext("2d"),
fftLength = 64,
angleStep = Math.PI * 2 / fftLength,
angle = 0,
line;
ctx.beginPath(); // not needed in demo, but when animated
while(angle < Math.PI*2) {
// our line function in action:
line = getLine(250, 250, angle, getFFT(), 240, 50);
ctx.moveTo(line.x1, line.y1); // add line to path
ctx.lineTo(line.x2, line.y2);
angle += angleStep // get next angle
}
ctx.lineWidth = 5; // beware of center area
ctx.stroke(); // stroke all lines at once
// to smooth the "FFT" random data
function getFFT() {return Math.random() * 0.16 + 0.4}
<canvas width=500 height=500></canvas>
I'm trying to draw speech buble with dragable handler.
That's what I have:
(x,y) - coordinates of the lowest left corner of buble
length of the buble
width of the buble
(x1,y1) coorinates of the handler end
Here is the picture for better understanding:
I know how to draw it in canvas when all coordinates are known. It's pretty simple.
Tutorial
function drawBubble(ctx, x, y, w, h, radius)
{
var r = x + w;
var b = y + h;
ctx.beginPath();
ctx.strokeStyle="black";
ctx.lineWidth="2";
ctx.moveTo(x+radius, y);
ctx.lineTo(x+radius/2, y-10);
ctx.lineTo(x+radius * 2, y);
ctx.lineTo(r-radius, y);
ctx.quadraticCurveTo(r, y, r, y+radius);
ctx.lineTo(r, y+h-radius);
ctx.quadraticCurveTo(r, b, r-radius, b);
ctx.lineTo(x+radius, b);
ctx.quadraticCurveTo(x, b, x, b-radius);
ctx.lineTo(x, y+radius);
ctx.quadraticCurveTo(x, y, x+radius, y);
ctx.stroke();
}
Example in jsFiddle
But the trouble is - how to find coordinates of red dots shown on the picture. Both (x,y) and (x1,y1) are known but change when user drags buble or handler. And in all cases handler should look pretty.
Whould be great if anyone could share the code, it's kinda complicated for me.
Thanks in advance!
You can preserve the corners and draw the pointing bit fixed to a given point. You just need to calculate the correct connection points.
// This is an example with the connection points 20px apart.
// The px and py variables here come from the onmousemove event.
// Finally, this part here is only for the top part of the bubble,
// you have watch for 4 different scenarios, depending on where
// the mouse is and thus what the pointing bit should aim for.
...
var con1 = Math.min(Math.max(x+radius,px-10),r-radius-20);
var con2 = Math.min(Math.max(x+radius+20,px+10),r-radius);
...
if(py < y) dir = 2;
...
ctx.moveTo(x+radius,y);
if(dir==2){
ctx.lineTo(con1,y);
ctx.lineTo(px,py);
ctx.lineTo(con2,y);
ctx.lineTo(r-radius,y);
}
else ctx.lineTo(r-radius,y);
ctx.quadraticCurveTo(r,y,r,y+radius);
...
Like this:
Draggable Bubble
Try clicking on the bubble to drag the pointer.
The handle is already calculated for you so it's simply a matter of preserving its coordinates by doing for example this modification:
function drawBubble(ctx, x, y, w, h, radius) {
...snipped...
var handle = {
x1: x + radius,
y1: y,
x2: x + radius / 2,
y2: y - 10,
x3: x + radius * 2,
y3: y
}
ctx.moveTo(handle.x1, handle.y1);
ctx.lineTo(handle.x2, handle.y2);
ctx.lineTo(handle.x3, handle.y3);
...snipped...
return handle;
}
Modified fiddle here
This is one way to get the coordinates for the handle.
To take it one step further we can modify the above function to also take a handle parameter.
This way you can choose to feed a handle setting or use a default calculated one:
function drawBubble(ctx, x, y, w, h, radius, handle) {
...snipped...
/// use given handle settings or calculate a default one:
handle = handle || {
x1: x + radius,
y1: y,
x2: x + radius / 2,
y2: y - 10,
x3: x + radius * 2,
y3: y
}
ctx.moveTo(handle.x1, handle.y1);
ctx.lineTo(handle.x2, handle.y2);
ctx.lineTo(handle.x3, handle.y3);
...snipped...
return handle;
}
In order to use this you first obtain a default calculate handle by passing in for example null or false to the function.
Then use those coordinates to draw the positions around. For each move clear and redraw the canvas but this time feed the modified handle parameters to the function:
/// first time:
var handle = null, /// first time use default handle
...;
handle = drawBubble(ctx, x, y, w, h, radius, handle);
Then in your mouse operations:
/// modify and update bubble:
handle = drawBubble(ctx, x, y, w, h, radius, handle);
Hope this helps!
Here is an example!
I am trying to reset the green arc inside drawValueArc() so that each time you click the change button, the green arc is removed and redrawn. How can I remove it without removing the entire canvas? Also, as an aside, I have noticed that Math.random() * 405 * Math.PI / 180 doesn't actually always result in an arc that fits inside the gray arc, sometimes it is larger than the gray arc, why is this?
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var cx = 150;
var cy = 150;
var startRadians = 135 * Math.PI / 180;
var endRadians = 405 * Math.PI / 180;
//main arc
ctx.beginPath();
ctx.arc(cx, cy, 58, startRadians, endRadians, false);
ctx.strokeStyle="rgb(220,220,220)";
ctx.lineWidth = 38;
ctx.stroke();
$('#setRandomValue').click(function(){
drawValueArc(Math.random() * 405 * Math.PI / 180);
});
function drawValueArc(val){
//ctx.clearRect(0, 0, W, H);
ctx.beginPath();
ctx.arc(cx, cy, 58, startRadians, val, false);
ctx.strokeStyle = "green";
ctx.lineWidth = 38;
ctx.stroke();
}
Drawing past boundary
The problem you are facing is in first instance the fact you are drawing before and after a 0-degree on the circle. This can be complicated to handle as you need to split in two draws: one for the part up to 0 (360) and one 0 to the remaining part.
There is a simple trick you can use to make this easier to deal with and that is to deal with all angles from 0 and use an offset when you draw.
Demo using redraw base (I moved it to jsfiddle as jsbin did not work for me):
http://jsfiddle.net/3dGLR/
Demo using off-screen canvas
http://jsfiddle.net/AbdiasSoftware/Dg9Jj/
First, some optimizations and settings for the offset:
var startRadians = 0; //just deal with angles
var endRadians = 300;
var deg2rad = Math.PI / 180; //pre-calculate this to save some cpu cycles
var offset = 122; //adjust this to modify rotation
We will now let the main function, drawArc() do all calculations for us so we can focus on the numbers - here we also offset the values:
function drawArc(color, start, end) {
ctx.beginPath();
ctx.arc(cx, cy, 58,
(startRadians + offset) * deg2rad,
(end + offset) * deg2rad, false);
ctx.strokeStyle = color;
ctx.lineWidth = 38;
ctx.stroke();
}
Clearing the previous arc
There are several techniques to clear the previous drawn arc:
You can draw the base arc to an off-screen canvas and use drawImage() to erase the old.
You can do as in the following example, just re-draw it with the base color
As with 2. but subtracting the green arc and draw the base color from the end of the green arc to the end of the base arc.
clearing the whole canvas with fillRect or clearRect.
1 and 3 are the fastest, while 4 is the slowest.
With out re-factored function (drawArc) it's as easy as this:
function drawValueArc(val) {
drawArc("rgb(220,220,220)", startRadians, endRadians);
drawArc("green", startRadians, val);
}
As everything now is 0-based concerning start we really don't need to give any other argument than 0 to the drawArc instead of startRadians. Use the new offset to offset the start position and adjust the endRadians to where you want it to stop.
As you can see in the demo, using this technique keeps everything in check without the need to draw in split.
Tip: if you notice green artifacts on the edges: this is due to anti-alias. Simply reduce the line width for the green color by 2 pixels (see demo 2, off-screen canvas).
Recently I got a task that is to draw circles on my own website with Google Maps API.
The complexity is the center of the circle is representing a "signal transmitter" and I need to make the circle transparent, with the opacity of each pixel reprseting the signal intensity of the exact location.
My basic idea is to extend the "Overlay" of Google Map API, so I have to write it in javascript I think.
The key part is to draw a circle with gradually changing opacity (inner stronger, outter lighter) and idealy, I can specify the opacity of each pixel.
I've been looking for approaches like CSS3, SVG, VML and even jquery and AJAX but still having no idea about how to archve this.
Thank you very much for your helping!
It looks like you're going to have to manually set every pixel, if you want that level of control over the opacity. I'd use something like this:
var centerX = 100 // Center X coordinate of the circle.
var centerY = 100 // Center Y coordinate of the circle.
var radius = 25 // Radius of circle.
//(var diameter = 2 * radius // Diameter of circle)
for(x = -radius; x < radius; x++){
for(y = -radius; y < radius; y++){
var hypotenuse = Math.sqrt((x * x) + (y * y)); // Line from point (x,y) to the center of the circle.
if(hypotenuse < radius){ // If the point falls within the circle
var opacity = hypotenuse / radius; // Should return a value between 0 and 1
drawPointAt(x + centerX, y + centerY, colour, opacity); // You'll have to look up what function to use here, yourself.
}
}
}
Here's a small example of this code returning a circle.
Here I got the solution. It's making use of the Canvas element of HTML5 (which is widely supported).
Here is the javascript code for locating the canvas element and draw the circle with gradually changing transparency. The key part is to use the "Gradient".
//get a reference to the canvas
var ctx = $('#canvas')[0].getContext("2d");
//draw a circle
gradient = ctx.createRadialGradient(200, 200, 0, 200, 200, 200);
gradient.addColorStop("0", "blue");
gradient.addColorStop("1.0", "transparent");
ctx.fillStyle = gradient;
//ctx.beginPath();
ctx.arc(200, 200, 200, 0, Math.PI*2, true);
//ctx.closePath();
ctx.fill();