SVG: a circle of Circles - javascript

So to explain simply i've this SVG circle element as so:
var circle = function(cx,cy,r) {
var svgCircle = document.createElementNS(NS,"circle");
svgCircle.setAttributeNS(null,"cx", cx);
svgCircle.setAttributeNS(null,"cy", cy);
svgCircle.setAttributeNS(null,"r", r);
return svgCircle;
}
it does the job it needs todo however i now need to create an abitrary amount of circles I.E 7, and position each so that they look like the outline of a circle.
so a circle made up of circles.
I just can't seem to understand how it works. Also an explanation or some guidance would go a long way!

This will draw satellite circles around a center-point
The key is to convert your calculated polar (angle,radius) coordinates to cartesian (x,y). See the added function polarToCartesian() in the example below:
<svg id="my_svg" height="400" width="400"></svg>
<script type="text/javascript">
function drawCircle(cx,cy,r){
var svgCircle = document.createElementNS('http://www.w3.org/2000/svg',"circle");
svgCircle.setAttributeNS(null,"cx", cx);
svgCircle.setAttributeNS(null,"cy", cy);
svgCircle.setAttributeNS(null,"r", r);
svgCircle.setAttributeNS(null,"stroke",'blue')
svgCircle.setAttributeNS(null,"fill",'transparent')
return svgCircle;
}
function polarToCartesian(center_x, center_y, radius, angle_in_degrees) {
var return_value = {}
var angle_in_radians = angle_in_degrees * Math.PI / 180.0;
return_value.x = center_x + radius * Math.cos(angle_in_radians);
return_value.y = center_y + radius * Math.sin(angle_in_radians);
return return_value;
}
/* ==============================
Loop to Draw Satellite circles
============================== */
// The center is the same for all circles
var cx = 200
var cy = 200
var radius_of_satellites_from_center = 100
var radius_of_small_circles = 10
var number_of_satellite_circles = 7
// The angle increments for each circle drawn
for(var n=0; n<number_of_satellite_circles; n++){
// Find how many degrees separate each circle
var degrees_of_separation = 360/number_of_satellite_circles
var angle_as_degrees = degrees_of_separation * n
var coordinates = polarToCartesian(cx, cy, radius_of_satellites_from_center, angle_as_degrees)
document.getElementById('my_svg').appendChild( drawCircle(coordinates.x,coordinates.y,radius_of_small_circles) )
}
</script>

This will draw concentric circles:
<svg id="my_svg" height="400" width="400"></svg>
<script type="text/javascript">
function drawCircle(cx,cy,r){
var svgCircle = document.createElementNS('http://www.w3.org/2000/svg',"circle");
svgCircle.setAttributeNS(null,"cx", cx);
svgCircle.setAttributeNS(null,"cy", cy);
svgCircle.setAttributeNS(null,"r", r);
svgCircle.setAttributeNS(null,"stroke",'blue')
svgCircle.setAttributeNS(null,"fill",'transparent')
return svgCircle;
}
/* ===============================
Loop to Draw concentric circles
=============================== */
// The center is the same for all circles
var cx = 200
var cy = 200
// More settings you can change
var starting_radius = 50
var number_of_circles = 7
var gap_between_circles = 3
// The radius increments for each circle drawn
for(var n=number_of_circles; n>0; n--){
var radius = starting_radius + n * gap_between_circles
document.getElementById('my_svg').appendChild( drawCircle(cx,cy,radius) )
}
</script>

Related

Circle Layout node angles cytoscape

How can i get the angle of each node with the center of the circumference in cytoscape using circle layout?
Visual example:
First step to get the angle of the node, get the center of the circumference
So i get 3 points of the 3rd first nodes to get it:
AX= cy.nodes()[0]._private.position.x
AY= cy.nodes()[0]._private.position.y
BX= cy.nodes()[1]._private.position.x
BY= cy.nodes()[1]._private.position.y
CX= cy.nodes()[2]._private.position.x
CY= cy.nodes()[2]._private.position.y
var yDelta_a = BY - AY
var xDelta_a = BX - AX;
var yDelta_b = CY - BY;
var xDelta_b = CX - BX;
var aSlope = yDelta_a / xDelta_a;
var bSlope = yDelta_b / xDelta_b;
//Get center circumference
coordCentroCircunferenciaX = (aSlope*bSlope*(AY - CY) + bSlope*(AX + BX) - aSlope*(BX+CX) )/(2* (bSlope-aSlope) );
coordCentroCircunferenciaY = -1*(coordCentroCircunferenciaX- (AX+BX)/2)/aSlope + (AY+BY)/2;
Then iterate through the nodes getting the angles:
for (i = 1;i< cy.nodes().length; i=i+1) { //starts nodes loop
let nodo=cy.nodes()[i];
array.push(nodo._private.data.name);
//get the node position(x,y)
nodox=nodo._private.position.x
nodoy=nodo._private.position.y
Now with 3 points: center circumference , node position and another point in the x axis
C = { x: coordCentroCircunferenciaX, y: coordCentroCircunferenciaY };
A = { x: nodox, y: 0 };
B = { x: nodox,y:nodoy };
Then i get the angle with this function:
function find_angle(A,B,C) {
var AB = Math.sqrt(Math.pow(B.x-A.x,2)+ Math.pow(B.y-A.y,2));
var BC = Math.sqrt(Math.pow(B.x-C.x,2)+ Math.pow(B.y-C.y,2));
var AC = Math.sqrt(Math.pow(C.x-A.x,2)+ Math.pow(C.y-A.y,2));
return Math.acos((BC*BC+AB*AB-AC*AC)/(2*BC*AB))*(180/Math.PI);
}
Finally this is the angle with the center of the circumference:
let angulo=Math.round(find_angle(A,B,C))
So with this angle i can get this effect to rotate node labels:

Converting 360 degree view to equirectangular in node js?

I have been trying to convert the 360 degree camera, single fish eye image, to equirectangular viewer in node js for the past two days. In stackoverflow, the same question is asked and answered in pseudo code. I have been trying to convert pseudo code to node js and cleared some errors. Now the project runs without error but the output image is blank.
From that pseudo, I dont know the polar_w, polar_h and geo_w, geo_h, geo and polar value, so, it gave static value to show the output. Here is a link which i followed to convert pseudo code to node js.
How to convert spherical coordinates to equirectangular projection coordinates?.
Here is the code I tried for converting spherical image to equirectangular viewer:
exports.sphereImage=(request, response)=>{
var Jimp = require('jimp');
// Photo resolution
var img_w_px = 1280;
var img_h_px = 720;
var polar_w = 1280;
var polar_h = 720;
var geo_w = 1280;
var geo_h = 720;
var img_h_deg = 70;
var img_w_deg = 30;
// Camera field-of-view angles
var img_ha_deg = 70;
var img_va_deg = 40;
// Camera rotation angles
var hcam_deg = 230;
var vcam_deg = 60;
// Camera rotation angles in radians
var hcam_rad = hcam_deg/180.0*Math.PI;
var vcam_rad = vcam_rad/180.0*Math.PI;
// Rotation around y-axis for vertical rotation of camera
var rot_y = [
[Math.cos(vcam_rad), 0, Math.sin(vcam_rad)],
[0, 1, 0],
[-Math.sin(vcam_rad), 0, Math.cos(vcam_rad)]
];
// Rotation around z-axis for horizontal rotation of camera
var rot_z = [
[Math.cos(hcam_rad), -Math.sin(hcam_rad), 0],
[Math.sin(hcam_rad), Math.cos(hcam_rad), 0],
[0, 0, 1]
];
Jimp.read('./public/images/4-18-2-42.jpg', (err, lenna) => {
polar = new Jimp(img_w_px, img_h_px);
geo = new Jimp(img_w_px, img_h_px);
for(var i=0; i<img_h_px; ++i)
{
for(var j=0; j<img_w_px; ++j)
{
// var p = img.getPixelAt(i, j);
var p = lenna.getPixelColor(i, j)
// var p = getPixels(img, { x: i, y: j })
// Calculate relative position to center in degrees
var p_theta = (j - img_w_px / 2.0) / img_w_px * img_w_deg / 180.0 * Math.PI;
var p_phi = -(i - img_h_px / 2.0) / img_h_px * img_h_deg / 180.0 *Math. PI;
// Transform into cartesian coordinates
var p_x = Math.cos(p_phi) * Math.cos(p_theta);
var p_y = Math.cos(p_phi) * Math.sin(p_theta);
var p_z = Math.sin(p_phi);
var p0 = {p_x, p_y, p_z};
// Apply rotation matrices (note, z-axis is the vertical one)
// First vertically
var p1 = rot_y[1][2][3] * p0;
var p2 = rot_z[1][2][3] * p1;
// Transform back into spherical coordinates
var theta = Math.atan2(p2[1], p2[0]);
var phi = Math.asin(p2[2]);
// Retrieve longitude,latitude
var longitude = theta / Math.PI * 180.0;
var latitude = phi / Math.PI * 180.0;
// Now we can use longitude,latitude coordinates in many different
projections, such as:
// Polar projection
{
var polar_x_px = (0.5*Math.PI + phi)*0.5 * Math.cos(theta)
/Math.PI*180.0 * polar_w;
var polar_y_px = (0.5*Math.PI + phi)*0.5 * Math.sin(theta)
/Math.PI*180.0 * polar_h;
polar.setPixelColor(p, polar_x_px, polar_y_px);
}
// Geographical (=equirectangular) projection
{
var geo_x_px = (longitude + 180) * geo_w;
var geo_y_px = (latitude + 90) * geo_h;
// geo.setPixel(geo_x_px, geo_y_px, p.getRGB());
geo.setPixelColor(p, geo_x_px, geo_y_px);
}
// ...
}
}
geo.write('./public/images/4-18-2-42-00001.jpg');
polar.write('./public/images/4-18-2-42-00002.jpg');
});
}
And tried another method by slicing image into four parts to detect car. Sliced image into four parts using image-slice module and to read and write jimp module is used. But unfortunately cars not detected properly.
Here is the code i used for slicing image:
exports.sliceImage=(request, response)=>{
var imageToSlices = require('image-to-slices');
var lineXArray = [540, 540];
var lineYArray = [960, 960];
var source = './public/images/4-18-2-42.jpg'; // width: 300, height: 300
imageToSlices(source, lineXArray, lineYArray, {
saveToDir: './public/images/',
clipperOptions: {
canvas: require('canvas')
}
}, function() {
console.log('the source image has been sliced into 9 sections!');
});
}//sliceImage
And for detect car from image i used opencv4nodejs. Cars are not detected properly. here is the code i used for detect car:
function runDetectCarExample(img=null){
if(img==null){
img = cv.imread('./public/images/section-1.jpg');
}else
{
img=cv.imread(img);
}
const minConfidence = 0.06;
const predictions = classifyImg(img).filter(res => res.confidence > minConfidence && res.className=='car');
const drawClassDetections = makeDrawClassDetections(predictions);
const getRandomColor = () => new cv.Vec(Math.random() * 255, Math.random() * 255, 255);
drawClassDetections(img, 'car', getRandomColor);
cv.imwrite('./public/images/section-'+Math.random()+'.jpg', img);
var name="distanceFromCamera";
var focalLen= 1.6 ;//Focal length in mm
var realObjHeight=254 ;//Real Height of Object in mm
var cameraFrameHeight=960;//Height of Image in pxl
var imgHeight=960;//Image Height in pxl
var sensorHeight=10;//Sensor height in mm
var R = 6378.1 //#Radius of the Earth
var brng = 1.57 //#Bearing is 90 degrees converted to radians.
var hc=(200/100);//Camera height in m
predictions
.forEach((data)=> {
// imgHeight=img.rows;//Image Height in pxl
// realObjHeight=data.rect.height;
// data.rect[name]=((focalLen)*(realObjHeight)*
(cameraFrameHeight))/((imgHeight)*(sensorHeight));
var dc=(((data.rect.width * focalLen) / img.cols)*2.54)*100; // meters
console.log(Math.floor(parseInt(data.rect.width)));
// var dc=((Math.floor(parseInt(data.rect.width)* 0.264583) * focalLen) / img.cols); // mm
var lat1=13.0002855;//13.000356;
var lon1=80.2046441;//80.204632;
// Gate 13.0002855,80.2046441
// Brazil Polsec : -19.860566, -43.969436
// var d=Math.sqrt((dc*dc)+(hc*hc));
// d=(data.rect[name])/1000;
data.rect[name]=d=dc/1000;
lat1 =toRadians(lat1);
lon1 = toRadians(lon1);
brng =toRadians(90);
// lat2 = Math.asin( Math.sin(lat1)*Math.cos(d/R) +
// Math.cos(lat1)*Math.sin(d/R)*Math.cos(brng));
// lon2 = lon1 +
Math.atan2(Math.sin(brng)*Math.sin(d/R)*Math.cos(lat1),
// Math.cos(d/R)-Math.sin(lat1)*Math.sin(lat2));
var lat2 = Math.asin(Math.sin(lat1) * Math.cos(d/6371) +
Math.cos(lat1) * Math.sin(d/6371) * Math.cos(brng));
var lon2 = lon1 + Math.atan2(Math.sin(brng) * Math.sin(d/6371) * Math.cos(lat1),
Math.cos(d/6371) - Math.sin(lat1) * Math.sin(lat2));
lat2 = toDegrees(lat2);
lon2 = toDegrees(lon2);
data.rect['latLong']=lat2+','+lon2;
// console.log(brng);
});
response.send(predictions);
cv.imshowWait('img', img);
};
here is the fish eye image which need to be converted to equirectangular.
Any help much appreciated pls....
You are asking how to convert a 360deg fish-eye projection to an equirectangular projection.
In order to do this, for every pixel on the fish-eye image you need to know where to place in onto the output image.
Your input image is 1920x1080, let us assume you want to output it to an equirectangular projection of the same size.
The input circle mapping is defined as:
cx = 960; // center of circle on X-axis
cy = 540; // center of circle on Y-axis
radius = 540; // radius of circle
If you have a pixel at (x,y) in the input image, then we can calculate the spherical coordinates using:
dx = (x - cx) * 1.0 / radius;
dy = (y - cy) * 1.0 / radius;
theta_deg = atan2(dy, dx) / MATH_PI * 180;
phi_deg = acos(sqrt(dx*dx + dy*dy)) / MATH_PI * 180;
outputx = (theta_deg + 180) / 360.0 * outputwidth_px;
outputy = (phi_deg + 90) / 180.0 * outputheight_px;
So there we translated (x,y) from the fish-eye image to the (outputx,outputy) in the equirectangular image. In order to not leave the implementation as the dreaded "exercise to the reader", here is some sample Javascript-code using the Jimp-library as used by the OP:
var jimp = require('jimp');
var inputfile = 'input.png';
jimp.read(inputfile, function(err, inputimage)
{
var cx = 960;
var cy = 540;
var radius = 540;
var inputwidth = 1920;
var inputheight = 1080;
var outputwidth = 1920;
var outputheight = 1080;
new jimp(outputwidth, outputheight, 0x000000ff, function(err, outputimage)
{
for(var y=0;y<inputheight;++y)
{
for(var x=0;x<inputwidth;++x)
{
var color = inputimage.getPixelColor(x, y);
var dx = (x - cx) * 1.0 / radius;
var dy = (y - cy) * 1.0 / radius;
var theta_deg = Math.atan2(dy, dx) / Math.PI * 180;
var phi_deg = Math.acos(Math.sqrt(dx*dx + dy*dy)) / Math.PI * 180;
var outputx = Math.round((theta_deg + 180) / 360.0 * outputwidth);
var outputy = Math.round((phi_deg + 90) / 180.0 * outputheight);
outputimage.setPixelColor(color, outputx, outputy);
}
}
outputimage.write('output.png');
});
});
Note that you will still need to do blending of the pixel with neighbouring pixels (for the same reason as when you're resizing the image).
Additionally, in your case, you only have half of the sphere (you can't see the sun in the sky). So you would need to use var outputy = Math.round(phi_deg / 90.0 * outputheight). In order to keep the right aspect ratio, you might want to change the height to 540.
Also note that the given implementation may not be efficient at all, it's better to use the buffer directly.
Anyway, without blending I came up with the result as demonstrated here:
So in order to do blending, you could use the simplest method which is the nearest neighbour approach. In that case, you should invert the formulas in the above example. Instead of moving the pixels from the input image to the right place in the output image, you can go through every pixel in the output image and ask which input pixel we can use for that. This will avoid the black pixels, but may still show artifacts:
var jimp = require('jimp');
var inputfile = 'input.png';
jimp.read(inputfile, function(err, inputimage)
{
var cx = 960;
var cy = 540;
var radius = 540;
var inputwidth = 1920;
var inputheight = 1080;
var outputwidth = 1920;
var outputheight = 1080/2;
var blendmap = {};
new jimp(outputwidth, outputheight, 0x000000ff, function(err, outputimage)
{
for(var y=0;y<outputheight;++y)
{
for(var x=0;x<outputwidth;++x)
{
var theta_deg = 360 - x * 360.0 / outputwidth - 180;
var phi_deg = 90 - y * 90.0 / outputheight;
var r = Math.sin(phi_deg * Math.PI / 180)
var dx = Math.cos(theta_deg * Math.PI / 180) * r;
var dy = Math.sin(theta_deg * Math.PI / 180) * r;
var inputx = Math.round(dx * radius + cx);
var inputy = Math.round(dy * radius + cy);
outputimage.setPixelColor(inputimage.getPixelColor(inputx, inputy), x, y);
}
}
outputimage.write('output.png');
});
});
For reference, in order to convert between Cartesian and Spherical coordinate systems. These are the formulas (taken from here). Note that the z is in your case just 1, a so-called "unit" sphere, so you can just leave it out of the equations. You should also understand that since the camera is actually taking a picture in three dimensions, you also need formulas to work in three dimensions.
Here is the generated output image:
Since I don't see your original input image in your question anymore, in order for anyone to test the code from this answer, you can use the following image:
Run the code with:
mkdir /tmp/test
cd /tmp/test
npm install --permanent jimp
cat <<EOF >/tmp/test/main.js
... paste the javascript code from above ...
EOF
curl https://i.stack.imgur.com/0zWt6.png > input.png
node main.js
Note: In order to further improve the blending, you should remove the Math.round. So for instance, if you need to grab a pixel at x is 0.75, and the pixel on the left at x = 0 is white, and the pixel on the right at x = 1 is black. Then you want to mix both colors into a dark grey color (using ratio 0.75). You would have to do this for both dimensions simultaneously, if you want a nice result. But this should really be in a new question imho.

d3 GeoJSON geoCircle ellipse equivalent

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

Square Analog Clock with Raphaeljs

I am trying to create a kind of square clock with Raphael JS, rather than 12 digits mine has 8 digits. I have found a great example for round clocks on this page;
Emanuele Feronato
I wanted to create my clock based on this one, but I am having trouble while aligning digit dashes to their places. For the round one the base example uses some cosines and sins for calculating their places. But on the square one I want all the dashes to be aligned so that they will touch to the border of square.
Aiming something like this;
I was wondering what is the easiest and best way to calculate these dashes' places for a square one? Is there also a clean mathematical solution as used in the round clock for square one?
Here is the fiddle of my current template;
fiddle
Here is also my current template;
function draw_square_clock(svgId) {
canvas = Raphael(svgId, 200, 200);
var clock = canvas.rect(3, 3, 194, 194);
clock.attr({
"fill": "#ffffff",
"stroke": "#000000",
"stroke-width": "4"
});
var hour_sign;
var text_sign;
for (i = 0.0; i < 12; i = i + 1.5) {
//How am I going to calculate these four values to align hour signs to the edges of square
var start_x = 100 + Math.round(80 * Math.cos(30 * i * Math.PI / 180));
var start_y = 100 + Math.round(80 * Math.sin(30 * i * Math.PI / 180));
var end_x = 100 + Math.round(90 * Math.cos(30 * i * Math.PI / 180));
var end_y = 100 + Math.round(90 * Math.sin(30 * i * Math.PI / 180));
hour_sign = canvas.path("M" + start_x + " " + start_y + "L" + end_x + " " + end_y);
hour_sign.attr({
"stroke-width": "6"
});
hour_sign.transform("t0,0 s2");
}
hour_hand = canvas.path("M100 100L100 50");
hour_hand.attr({
stroke: "#000000",
"stroke-width": 6
});
var pin = canvas.circle(100, 100, 8);
pin.attr("fill", "#000000");
}
$(document).ready(function() {
draw_square_clock('clock_id');
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.4/raphael-min.js"></script>
<div id="clock_id"></div>
Mathematically:
Calculate the length of a corner tick:
// length of tick a 0 degrees
var tickLengthAt0=30;
// calculate length of corner tick
var cornerTickLength=Math.sqrt(2*(tickLengthAt0*tickLengthAt0));
Use trigonometry to calc each of the tick line segments:
var PI=Math.PI;
var cx=150;
var cy=150;
var width=200;
var height=200;
function calcTickPoints(x0,y0,length,angle){
x1=x0+length*Math.cos(angle);
y1=y0+length*Math.sin(angle);
return({ x0:x0, y0:y0, x1:x1, y1:y1 });
}
// Use calcTickPoints to calc the points of each tick
// top-right
var TR=calcTickPoints(cx+width/2,cy-height/2,cornerTickLength,PI*3/4);
// bottom-right
var BR=calcTickPoints(cx+width/2,cy+height/2,cornerTickLength,PI*5/4);
// bottom-left
var BL=calcTickPoints(cx-width/2,cy+height/2,cornerTickLength,PI*7/4);
// top-left
var TL=calcTickPoints(cx-width/2,cy-height/2,cornerTickLength,PI*9/4);
// right
var R=calcTickPoints(cx+width/2,cy,tickLengthAt0,PI);
// bottom
var B=calcTickPoints(cx,cy+height/2,tickLengthAt0,PI*3/2);
// left
var L=calcTickPoints(cx-width/2,cy,tickLengthAt0,PI*2);
// top
var T=calcTickPoints(cx,cy-height/2,tickLengthAt0,PI/2);
Example code (drawn on Canvas, but you can use SVG if desired):
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
var PI=Math.PI;
var PI2=PI*2;
var cx=150;
var cy=150;
var width=200;
var height=200;
var tickLengthAt0=30;
var cornerTickLength=Math.sqrt(2*(tickLengthAt0*tickLengthAt0));
drawFace();
function drawFace(){
// rect
ctx.strokeRect(cx-width/2,cy-height/2,width,height);
// top-right
drawTick(cx+width/2,cy-height/2,cornerTickLength,PI*3/4);
// bottom-right
drawTick(cx+width/2,cy+height/2,cornerTickLength,PI*5/4);
// bottom-left
drawTick(cx-width/2,cy+height/2,cornerTickLength,PI*7/4);
// top-left
drawTick(cx-width/2,cy-height/2,cornerTickLength,PI*9/4);
// right
drawTick(cx+width/2,cy,tickLengthAt0,PI);
// bottom
drawTick(cx,cy+height/2,tickLengthAt0,PI*3/2);
// left
drawTick(cx-width/2,cy,tickLengthAt0,PI*2);
// top
drawTick(cx,cy-height/2,tickLengthAt0,PI/2);
}
function drawTick(x0,y0,length,angle){
x1=x0+length*Math.cos(angle);
y1=y0+length*Math.sin(angle);
ctx.beginPath();
ctx.moveTo(x0,y0);
ctx.lineTo(x1,y1);
ctx.stroke();
return({ x0:x0, y0:y0, x1:x1, y1:y1 });
}
body{ background-color: ivory; }
#canvas{border:1px solid red; margin:0 auto; }
<canvas id="canvas" width=300 height=300></canvas>

Rendering concentric hexes on Canvas

I've written a loop in JavaScript that will render rings of concentric hexagons around a central hexagon on the HTML canvas.
I start with the innermost ring, draw the hex at 3 o'clock, then continue around in a circle until all hexes are rendered. Then I move on to the next ring and repeat.
When you draw hexagons this way (instead of tiling them using solely x and y offsets) any hexagon that is not divisible by 60 is not the same distance to the center hex as those that are divisible by 60 (because these hexes comprise the flat edges, not the vertices, of the larger hex).
The problem I'm having is these hexes (those not divisible by 60 degrees) are rendering in a slightly off position. I'm not sure if it is a floating point math problem, the problem with my algorithm, the problem with my rusty trig, or just plain stupidity. I'm betting 3 out of 4. To cut to the chase, look at the line if (alpha % 60 !== 0) in the code below.
As a point of information, I decided to draw the grid this way because I needed an easy way to map the coordinates of each hex into a data structure, with each hex being identified by its ring # and ID# within that ring. If there is a better way to do it I'm all ears, however, I'd still like to know why my rendering is off.
Here is my very amateur code, so bear with me.
<script type="text/javascript">
window.addEventListener('load', eventWindowLoaded, false);
function eventWindowLoaded() {
canvasApp();
}
function canvasApp(){
var xOrigin;
var yOrigin;
var scaleFactor = 30;
var theCanvas = document.getElementById("canvas");
var context;
if (canvas.getContext) {
context = theCanvas.getContext("2d");
window.addEventListener('resize', resizeCanvas, false);
window.addEventListener('orientationchange', resizeCanvas, false);
resizeCanvas();
}
drawScreen();
function resizeCanvas() {
var imgData = context.getImageData(0,0, theCanvas.width, theCanvas.height);
theCanvas.width = window.innerWidth;
theCanvas.height = window.innerHeight;
context.putImageData(imgData,0,0);
xOrigin = theCanvas.width / 2;
yOrigin = theCanvas.height / 2;
}
function drawScreen() {
var rings = 3;
var alpha = 0;
var modifier = 1;
context.clearRect(0, 0, theCanvas.width, theCanvas.height);
drawHex(0,0);
for (var i = 1; i<=rings; i++) {
for (var j = 1; j<=i*6; j++) {
if (alpha % 60 !== 0) {
var h = modifier * scaleFactor / Math.cos(dtr(360 / (6 * i)));
drawHex(h * (Math.cos(dtr(alpha))), h * Math.sin(dtr(alpha)));
}
else {
drawHex(2 * scaleFactor * i * Math.cos(dtr(alpha)), 2 * scaleFactor * i * Math.sin(dtr(alpha)));
}
alpha += 360 / (i*6);
}
modifier+=2;
}
}
function drawHex(xOff, yOff) {
context.fillStyle = '#aaaaaa';
context.strokeStyle = 'black';
context.lineWidth = 2;
context.lineCap = 'square';
context.beginPath();
context.moveTo(xOrigin+xOff-scaleFactor,yOrigin+yOff-Math.tan(dtr(30))*scaleFactor);
context.lineTo(xOrigin+xOff,yOrigin+yOff-scaleFactor/Math.cos(dtr(30)));
context.lineTo(xOrigin+xOff+scaleFactor,yOrigin+yOff-Math.tan(dtr(30))*scaleFactor);
context.lineTo(xOrigin+xOff+scaleFactor,yOrigin+yOff+Math.tan(dtr(30))*scaleFactor);
context.lineTo(xOrigin+xOff,yOrigin+yOff+scaleFactor/Math.cos(dtr(30)));
context.lineTo(xOrigin+xOff-scaleFactor,yOrigin+yOff+Math.tan(dtr(30))*scaleFactor);
context.closePath();
context.stroke();
}
function dtr(ang) {
return ang * Math.PI / 180;
}
function rtd(ang) {
return ang * 180 / Math.PI;
}
}
</script>
Man it took me longer than I'd like to admit to find the pattern for the hexagonal circles. I'm too tired right now to explain since I think I'll need to make some assisting illustrations in order to explain it.
In short, each "circle" of hexagonal shapes is itself hexagonal. The number of hexagonal shapes along one edge is the same as the number of the steps from the center.
var c = document.getElementById("canvas");
var ctx = c.getContext("2d");
c.width = 500;
c.height = 500;
var hexRadius = 20;
var innerCircleRadius = hexRadius/2*Math.sqrt(3);
var TO_RADIANS = Math.PI/180;
function drawHex(x,y) {
var r = hexRadius;
ctx.beginPath();
ctx.moveTo(x,y-r);
for (var i = 0; i<=6; i++) {
ctx.lineTo(x+Math.cos((i*60-90)*TO_RADIANS)*r,y+Math.sin((i*60-90)*TO_RADIANS)*r);
}
ctx.closePath();
ctx.stroke();
}
drawHexCircle(250,250,4);
function drawHexCircle(x,y,circles) {
var rc = innerCircleRadius;
drawHex(250,250); //center
for (var i = 1; i<=circles; i++) {
for (var j = 0; j<6; j++) {
var currentX = x+Math.cos((j*60)*TO_RADIANS)*rc*2*i;
var currentY = y+Math.sin((j*60)*TO_RADIANS)*rc*2*i;
drawHex(currentX,currentY);
for (var k = 1; k<i; k++) {
var newX = currentX + Math.cos((j*60+120)*TO_RADIANS)*rc*2*k;
var newY = currentY + Math.sin((j*60+120)*TO_RADIANS)*rc*2*k;
drawHex(newX,newY);
}
}
}
}
canvas {
border: 1px solid black;
}
<canvas id="canvas"></canvas>
I think you're trying to use radial coordinates for something that isn't a circle.
As you noted correctly, the (centers of) the vertex hexagons are indeed laid out in a circle and you can use basic radial positioning to lay them out. However, the non-vertex ones are not laid out on an arc of that circle, but on a chord of it (the line connecting two vertex hexagons). So your algorithm, which tries to use a constant h (radius) value for these hexagons, will not lay them out correctly.
You can try interpolating the non-vertex hexagons from the vertex hexagons: the position of of the Kth (out of N) non-vertex hexagon H between vertex hexagons VH1 and VH2 is:
Pos(H) = Pos(VH1) + (K / (N + 1)) * (Pos(VH2)-Pos(VH1))
e.g. in a ring with 4 hexagons per edge (i.e. 2 non-vertex hexagons), look at the line of hexagons between the 3 o'clock and the 5 o'clock: the 3 o'clock is at 0% along that line, the one after that is at 1/3 of the way, the next is at 2/3 of the way, and the 5 o'clock is at 100% of the way. Alternatively you can think of each hexagon along that line as "advancing" by a predetermined vector in the direction between the two vertices until you reach the end of the line.
So basically your algorithm could go through the 6 primary vertex hexagons, each time interpolating the hexagons from the current vertex hexagon to the next. Thus you should probably have three nested loops: one for rings, one for angles on a ring (always six steps), and one for interpolating hexagons along a given angle (number of steps according to ring number).

Categories

Resources