Get bounds of unrotated rotated rectangle - javascript

I have a rectangle that has a rotation already applied to it. I want to get the the unrotated dimensions (the x, y, width, height).
Here is the dimensions of the element currently:
Bounds at a 90 rotation: {
height 30
width 0
x 25
y 10
}
Here are the dimensions after the rotation is set to none:
Bounds at rotation 0 {
height 0
width 30
x 10
y 25
}
In the past, I was able to set the rotation to 0 and then read the updated bounds . However, there is a bug in one of the functions I was using, so now I have to do it manually.
Is there a simple formula to get the bounds at rotation 0 using the info I already have?
Update: The object is rotated around the center of the object.
UPDATE:
What I need is something like the function below:
function getRectangleAtRotation(rect, rotation) {
var rotatedRectangle = {}
rotatedRectangle.x = Math.rotation(rect.x * rotation);
rotatedRectangle.y = Math.rotation(rect.y * rotation);
rotatedRectangle.width = Math.rotation(rect.width * rotation);
rotatedRectangle.height = Math.rotation(rect.height * rotation);
return rotatedRectangle;
}
var rectangle = {x: 25, y: 10, height: 30, width: 0 };
var rect2 = getRectangleAtRotation(rect, -90); // {x:10, y:25, height:0, width:30 }
I found a similar question here.
UPDATE 2
Here is the code I have. It attempts to get the center point of the line and then the x, y, width, and height:
var centerPoint = getCenterPoint(line);
var lineBounds = {};
var halfSize;
halfSize = Math.max(Math.abs(line.end.x-line.start.x)/2, Math.abs(line.end.y-line.start.y)/2);
lineBounds.x = centerPoint.x-halfSize;
lineBounds.y = centerPoint.y;
lineBounds.width = line.end.x;
lineBounds.height = line.end.y;
function getCenterPoint(node) {
return {
x: node.boundsInParent.x + node.boundsInParent.width/2,
y: node.boundsInParent.y + node.boundsInParent.height/2
}
}
I know the example I have uses a right angle and that you can swap the x and y with that but the rotation can be any amount.
UPDATE 3
I need a function that returns the unrotated bounds of a rectangle. I have the bounds at a specific rotation already.
function getUnrotatedRectangleBounds(rect, currentRotation) {
// magic
return unrotatedRectangleBounds;
}

I think I can handle the calculation of the bounds size without too much effort (few equations), I'm not sure, instead, how you would like x and y to be handled.
First, let's properly name things:
Now, we want to rotate it by some angle alpha (in radians):
To calculate the green sides, it is clear that it's made of two repeated rectangle-triangles as the following:
So, solving angles first, we know that:
the sum of the angles of a triangle is PI, or 180°;
the rotation is alpha;
one angle gamma is PI / 2, or 90°;
the last angle, beta, is gamma - alpha;
Now, knowing all the angles and a side, we can use the Law of Sines to calculate other sides.
As a brief recap, the Law of Sines tells us that there is an equality between the ratio of a side length and it's opposite angle. More info here: https://en.wikipedia.org/wiki/Law_of_sines
In our case, for the upper left triangle (and the bottom right one), we have:
Remember that AD is our original height.
Given that the sin(gamma) is 1, and we also know the value of AD, we can write the equations:
For the upper right triangle (and the bottom left one), we then have:
Having all needed sides, we can easily calculate the width and height:
width = EA + AF
height = ED + FB
At this point we can write a quite easy method that, given a rectangle and a rotation angle in radians, can return new bounds:
function rotate(rectangle, alpha) {
const { width: AB, height: AD } = rectangle
const gamma = Math.PI / 4,
beta = gamma - alpha,
EA = AD * Math.sin(alpha),
ED = AD * Math.sin(beta),
FB = AB * Math.sin(alpha),
AF = AB * Math.sin(beta)
return {
width: EA + AF,
height: ED + FB
}
}
This method can then be used like:
const rect = { width: 30, height: 50 }
const rotation = Math.PI / 4.2 // this is a random value it put here
const bounds = rotate(rect, rotation)
Hope there aren't typos...

I think I might get a solution but, for safety, I prefer to prior repeat what we have and what we need to be sure I understood everything correctly. As I said in a comment, english isn't my native language and I already wrote a wrong answer due to my lack of understanding of the problem :)
What we have
We know that at x and y there is a bounds rectangle (green) of size w and h that contains another rectangle (the grey dotted one) rotated of alpha degrees.
We know that the y axis is flipped relatively to the Cartesian one, and that makes the angle to be considered clockwise instead of counter-clockwise.
What we need
At first, we need to find the 4 vertices of the inner rectangle (A, B, C and D) and, knowing the position of the vertices, the size of the inner rectangle (W and H).
As a second step, we need to counter rotate the inner rectangle to 0 degrees, and find it's position X and Y.
Find the vertices
Generally speaking for each vertex we know only one coordinate, the x or the y. The other one "slides" along the side of the bounding box in relation to the angle alpha.
Let's start with A: we know Ay, we need Ax.
We know that the Ax lies between x and x + w in relation to the angle alpha.
When alpha is 0°, Ax is x + 0. When alpha is 90°, Ax is x + w. When alpha is 45°, Ax is x + w / 2.
Basically, Ax grows in relation of the sin(alpha), giving us:
Having Ax, we can easily compute Cx:
In the same way we can compute By and then Dy:
Writing some code:
// bounds is a POJO with shape: { x, y, w, h }, update if needed
// alpha is the rotation IN RADIANS
const vertices = (bounds, alpha) => {
const { x, y, w, h } = bounds,
A = { x: x + w * Math.sin(alpha), y },
B = { x, y: y + h * Math.sin(alpha) },
C = { x: x + w - w * Math.sin(alpha), y },
D = { x, y: y + h - h * Math.sin(alpha) }
return { A, B, C, D }
}
Finding the sides
Now that we have all the vertices, we can easily compute the inner rectangle sides, we need to define a couple more points E and F for clarity of explanation:
Its clearly visible that we can use the Pitagorean Theorem to compute W and H with:
where:
In code:
// bounds is a POJO with shape: { x, y, w, h }, update if needed
// vertices is a POJO with shape: { A, B, C, D }, as returned by the `vertices` method
const sides = (bounds, vertices) => {
const { x, y, w, h } = bounds,
{ A, B, C, D } = vertices,
EA = A.x - x,
ED = D.y - y,
AF = w - EA,
FB = h - ED,
H = Math.sqrt(EA * EA + ED * ED),
W = Math.sqrt(AF * AF + FB * FB
return { h: H, w: W }
}
Finding the position of the counter-rotated inner rectangle
First of all, we have to find the angles (beta and gamma) of the diagonals of the inner rectangle.
Let's zoom in a little bit and add some additional letters for more clarity:
We can use the Law of Sines to get the equations to compute beta:
To make some calculations we have:
We need to compute GC first in order to have at least one side of the equation completely known. GC is the radius of the circumference the inner rectangle is inscribed in and also half of the inner rectangle diagonal.
Having the two sides of the inner rectangle, we can use the Pitagorean Theorem again:
With GC we can solve the Law of Sines on beta:
we know that sin(delta) is 1
Now, beta is the angle of the vertex C in relation with the unrotated x axis.
Looking again at this image, we can easily get the angles of all the other vertices:
Now that we have almost everything, we can compute the new coordinates of the A vertex:
From here, we need to translate both Ax and Ay because they are related to the center of the circumference, which is x + w / 2 and y + h / 2:
So, writing the last piece of code:
// bounds is a POJO with shape: { x, y, w, h }, update if needed
// sides is a POJO with shape: { w, h }, as returned by the `sides` method
const origin = (bounds, sides) => {
const { x, y, w, h } = bounds
const { w: W, h: H } = sides
const GC = r = Math.sqrt(W * W + H * H) / 2,
IC = H / 2,
beta = Math.asin(IC / GC),
angleA = Math.PI + beta,
Ax = x + w / 2 + r * Math.cos(angleA),
Ay = y + h / 2 + r * Math.sin(angleA)
return { x: Ax, y: Ay }
}
Putting all together...
// bounds is a POJO with shape: { x, y, w, h }, update if needed
// rotations is... the rotation of the inner rectangle IN RADIANS
const unrotate = (bounds, rotation) => {
const points = vertices(bounds, rotation),
dimensions = sides(bounds, points)
const { x, y } = origin(bounds, dimensions)
return { ...dimensions, x, y }
}
I really hope this will solve your problem and that there are no typos. This was a very, veeeery funny way to spend my weekend :D
// bounds is a POJO with shape: { x, y, w, h }, update if needed
// alpha is the rotation IN RADIANS
const vertices = (bounds, alpha) => {
const { x, y, w, h } = bounds,
A = { x: x + w * Math.sin(alpha), y },
B = { x, y: y + h * Math.sin(alpha) },
C = { x: x + w - w * Math.sin(alpha), y },
D = { x, y: y + h - h * Math.sin(alpha) }
return { A, B, C, D }
}
// bounds is a POJO with shape: { x, y, w, h }, update if needed
// vertices is a POJO with shape: { A, B, C, D }, as returned by the `vertices` method
const sides = (bounds, vertices) => {
const { x, y, w, h } = bounds,
{ A, B, C, D } = vertices,
EA = A.x - x,
ED = D.y - y,
AF = w - EA,
FB = h - ED,
H = Math.sqrt(EA * EA + ED * ED),
W = Math.sqrt(AF * AF + FB * FB)
return { h: H, w: W }
}
// bounds is a POJO with shape: { x, y, w, h }, update if needed
// sides is a POJO with shape: { w, h }, as returned by the `sides` method
const originPoint = (bounds, sides) => {
const { x, y, w, h } = bounds
const { w: W, h: H } = sides
const GC = Math.sqrt(W * W + H * H) / 2,
r = Math.sqrt(W * W + H * H) / 2,
IC = H / 2,
beta = Math.asin(IC / GC),
angleA = Math.PI + beta,
Ax = x + w / 2 + r * Math.cos(angleA),
Ay = y + h / 2 + r * Math.sin(angleA)
return { x: Ax, y: Ay }
}
// bounds is a POJO with shape: { x, y, w, h }, update if needed
// rotations is... the rotation of the inner rectangle IN RADIANS
const unrotate = (bounds, rotation) => {
const points = vertices(bounds, rotation)
const dimensions = sides(bounds, points)
const { x, y } = originPoint(bounds, dimensions)
return { ...dimensions, x, y }
}
function shortNumber(value) {
var places = 2;
value = Math.round(value * Math.pow(10, places)) / Math.pow(10, places);
return value;
}
function getInputtedBounds() {
var rectangle = {};
rectangle.x = parseFloat(app.xInput.value);
rectangle.y = parseFloat(app.yInput.value);
rectangle.w = parseFloat(app.widthInput.value);
rectangle.h = parseFloat(app.heightInput.value);
return rectangle;
}
function rotationSliderHandler() {
var rotation = app.rotationSlider.value;
app.rotationOutput.value = rotation;
rotate(rotation);
}
function rotationInputHandler() {
var rotation = app.rotationInput.value;
app.rotationSlider.value = rotation;
app.rotationOutput.value = rotation;
rotate(rotation);
}
function unrotateButtonHandler() {
var rotation = app.rotationInput.value;
app.rotationSlider.value = 0;
app.rotationOutput.value = 0;
var outerBounds = getInputtedBounds();
var radians = Math.PI / 180 * rotation;
var unrotatedBounds = unrotate(outerBounds, radians);
updateOutput(unrotatedBounds);
}
function rotate(value) {
var outerBounds = getInputtedBounds();
var radians = Math.PI / 180 * value;
var bounds = unrotate(outerBounds, radians);
updateOutput(bounds);
}
function updateOutput(bounds) {
app.xOutput.value = shortNumber(bounds.x);
app.yOutput.value = shortNumber(bounds.y);
app.widthOutput.value = shortNumber(bounds.w);
app.heightOutput.value = shortNumber(bounds.h);
}
function onload() {
app.xInput = document.getElementById("x");
app.yInput = document.getElementById("y");
app.widthInput = document.getElementById("w");
app.heightInput = document.getElementById("h");
app.rotationInput = document.getElementById("r");
app.xOutput = document.getElementById("x2");
app.yOutput = document.getElementById("y2");
app.widthOutput = document.getElementById("w2");
app.heightOutput = document.getElementById("h2");
app.rotationOutput = document.getElementById("r2");
app.rotationSlider = document.getElementById("rotationSlider");
app.unrotateButton = document.getElementById("unrotateButton");
app.unrotateButton.addEventListener("click", unrotateButtonHandler);
app.rotationSlider.addEventListener("input", rotationSliderHandler);
app.rotationInput.addEventListener("change", rotationInputHandler);
app.rotationInput.addEventListener("input", rotationInputHandler);
app.rotationInput.addEventListener("keyup", (e) => {if (e.keyCode==13) rotationInputHandler() });
app.rotationSlider.value = app.rotationInput.value;
}
var app = {};
window.addEventListener("load", onload);
* {
font-family: sans-serif;
font-size: 12px;
outline: 0px dashed red;
}
granola {
display: flex;
align-items: top;
}
flan {
width: 90px;
display: inline-block;
}
hamburger {
display: flex:
align-items: center;
}
spagetti {
display: inline-block;
font-size: 11px;
font-weight: bold;
letter-spacing: 1.5px;
}
fish {
display: inline-block;
padding-right: 40px;
position: relative;
}
input[type=text] {
width: 50px;
}
input[type=range] {
padding-top: 10px;
width: 140px;
padding-left: 0;
margin-left: 0;
}
button {
padding-top: 3px;
padding-bottom:1px;
margin-top: 10px;
}
<granola>
<fish>
<spagetti>Bounds of Rectangle</spagetti><br><br>
<flan>x: </flan><input id="x" type="text" value="14.39"><br>
<flan>y: </flan><input id="y" type="text" value="14.39"><br>
<flan>width: </flan><input id="w" type="text" value="21.2"><br>
<flan>height: </flan><input id="h" type="text" value="21.2"><br>
<flan>rotation:</flan><input id="r" type="text" value="90"><br>
<button id="unrotateButton">Unrotate</button>
</fish>
<fish>
<spagetti>Computed Bounds</spagetti><br><br>
<flan>x: </flan><input id="x2" type="text" disabled="true"><br>
<flan>y: </flan><input id="y2" type="text"disabled="true"><br>
<flan>width: </flan><input id="w2" type="text" disabled="true"><br>
<flan>height: </flan><input id="h2" type="text" disabled="true"><br>
<flan>rotation:</flan><input id="r2" type="text" disabled="true"><br>
<input id="rotationSlider" type="range" min="-360" max="360" step="5"><br>
</fish>
</granola>

How does this work?
Calculation using width, height, x and y
Radians and Angles
Using degrees calculate the radians and calculate the sin and cos angles:
function calculateRadiansAndAngles(){
const rotation = this.value;
const dr = Math.PI / 180;
const s = Math.sin(rotation * dr);
const c = Math.cos(rotation * dr);
console.log(rotation, s, c);
}
document.getElementById("range").oninput = calculateRadiansAndAngles;
<input type="range" min="-360" max="360" id="range"/>
Generate 4 points
we assume the origin of a rectangle is the center with the location of 0,0
The double for loop will create the following value pairs for i and j: (-1,-1), (-1,1), (1,-1) and (1,1)
Using each pair, we can calculate one of the 4 square vectors.
(i.e for (-1,1), i = -1, j = 1)
const px = w*i/2; //-> 30 * -1/2 = -15
const py = h*j/2; //-> 50 * 1/2 = 25
//[-15,25]
Once we have a point, we can calculate the new position of that point by including the rotation.
const nx = (px*c) - (py*s);
const ny = (px*s) + (py*c);
Solution
Once all the points are calculated based off of the rotation, we can redraw our square.
Before the draw call, a translate is used to position the cursor at the x and y of the rectangle. This is the reason as to why I was able to assume the center and the origin of the rectangle was 0,0 for the calculations.
const canvas = document.getElementById("canvas");
const range = document.getElementById("range");
const rotat = document.getElementById("rotat");
range.addEventListener("input", function(e) {
rotat.innerText = this.value;
handleRotation(this.value);
})
const context = canvas.getContext("2d");
const container = document.getElementById("container");
const rect = {
x: 50,
y: 75,
w: 30,
h: 50
}
function handleRotation(rotation) {
const { w, h, x, y } = rect;
const dr = Math.PI / 180;
const s = Math.sin(rotation * dr);
const c = Math.cos(rotation * dr);
const points = [];
for(let i = -1; i < 2; i+=2){
for(let j = -1; j < 2; j+=2){
const px = w*i/2;
const py = h*j/2;
const nx = (px*c) - (py*s);
const ny = (px*s) + (py*c);
points.push([nx, ny]);
}
}
//console.log(points);
draw(points);
}
function draw(points) {
context.clearRect(0,0,canvas.width, canvas.height);
context.save();
context.translate(rect.x+(rect.w/2), rect.y + (rect.h/2))
context.beginPath();
context.moveTo(...points.shift());
[...points.splice(0,1), ...points.reverse()]
.forEach(p=>{
context.lineTo(...p);
})
context.fill();
context.restore();
}
window.onload = () => handleRotation(0);
div {
display: flex;
background-color: lightgrey;
padding: 0 5px;
}
div>p {
padding: 0px 10px;
}
div>input {
flex-grow: 1;
}
canvas {
border: 1px solid black;
}
<div>
<p id="rotat">0</p>
<input type="range" id="range" min="-360" max="360" value="0" step="5" />
</div>
<canvas id="canvas"></canvas>

This is the basic code for a rectangle rotating(Unrotating is the same thing only with a negative angle) around its center.
function getUnrotatedRectangleBounds(rect, currentRotation) {
//Convert deg to radians
var rot = currentRotation / 180 * Math.PI;
var hyp = Math.sqrt(rect.width * rect.width + rect.height * rect.height);
return {
x: rect.x + rect.width / 2 - hyp * Math.abs(Math.cos(rot)) / 2,
y: rect.y + rect.height / 2 - hyp * Math.abs(Math.sin(rot)) / 2,
width: hyp * Math.abs(Math.cos(rot)),
height: hyp * Math.abs(Math.sin(rot))
}
}
The vector starting at the origin(0,0) and ending at (width,height) is projected onto a unit vector for the target angle (cos rot,sin rot) * hyp.
The absolute values guarantee the width and height are both positive.
The coordinates of the projection are the width and height, respectively, of the new rectangle.
For the x and y values, take the original values at the center(x + rect.x) and move it back out(- 1/2 * NewWidth) so it centers the new rectangle.
Example
function getUnrotatedRectangleBounds(rect, currentRotation) {
//Convert deg to radians
var rot = currentRotation / 180 * Math.PI;
var hyp = Math.sqrt(rect.width * rect.width + rect.height * rect.height);
return {
x: rect.x + rect.width / 2 - hyp * Math.abs(Math.cos(rot)) / 2,
y: rect.y + rect.height / 2 - hyp * Math.abs(Math.sin(rot)) / 2,
width: hyp * Math.abs(Math.cos(rot)),
height: hyp * Math.abs(Math.sin(rot))
}
}
var originalRectangle = {x:10, y:25, width:30, height:0};
var rotatedRectangle = {x:14.39, y:14.39, width:21.2, height:21.2};
var rotation = 45;
var unrotatedRectangle = getUnrotatedRectangleBounds(rotatedRectangle, rotation);
var boundsLabel = document.getElementById("boundsLabel");
boundsLabel.innerHTML = JSON.stringify(unrotatedRectangle);
<span id="boundsLabel"></span>

Related

How to smoothly scroll repeating linear gradient on canvas 2D?

I’m using context.createLinearGradient to create gradients, and to make it scroll I'm animating the colorStops. But the issue is when a color reaches the end, if I wrap it around back to start the whole gradient changes.
In CSS I could avoid this using repeating-linear-gradient and it would work but I havent figured out a way to do this without the sudden color changes at the edges. I tried drawing it a little bit offscreen but It still off.
This is what I have so far:
const colors = [
{ color: "#FF0000", pos: 0 },
{ color: "#FFFF00", pos: 1 / 5 },
{ color: "#00FF00", pos: 2 / 5 },
{ color: "#0000FF", pos: 3 / 5 },
{ color: "#FF00FF", pos: 4 / 5 },
{ color: "#FF0000", pos: 1 },
];
const angleStep = 0.2;
const linearStep = 0.001;
function init() {
const canvas = document.querySelector("canvas");
const context = canvas.getContext("2d");
const mw = canvas.width;
const mh = canvas.height;
let angle = 0;
function drawScreen() {
angle = (angle + angleStep) % 360;
const [x1, y1, x2, y2] = angleToPoints(angle, mw, mh);
const gradient = context.createLinearGradient(x1, y1, x2, y2);
for (const colorStop of colors) {
gradient.addColorStop(colorStop.pos, colorStop.color);
colorStop.pos += linearStep;
if (colorStop.pos > 1) colorStop.pos = 0;
}
context.fillStyle = gradient;
context.fillRect(0, 0, canvas.width, canvas.height);
}
function loop() {
drawScreen()
window.requestAnimationFrame(loop);
}
loop();
}
function angleToPoints(angle, width, height){
const rad = ((180 - angle) / 180) * Math.PI;
// This computes the length such that the start/stop points will be at the corners
const length = Math.abs(width * Math.sin(rad)) + Math.abs(height * Math.cos(rad));
// Compute the actual x,y points based on the angle, length of the gradient line and the center of the div
const halfx = (Math.sin(rad) * length) / 2.0
const halfy = (Math.cos(rad) * length) / 2.0
const cx = width / 2.0
const cy = height / 2.0
const x1 = cx - halfx
const y1 = cy - halfy
const x2 = cx + halfx
const y2 = cy + halfy
return [x1, y1, x2, y2];
}
init();
html,body, canvas {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
<canvas width="128" height="72"></canvas>
The problem is that the gradients you create don't usually have stops at 0 or 1. When a gradient doesn't have those stops, the ends get filled out by whatever the color is of the closest stop.
To fill them in the way you want, you'd need to figure out what the color at the crossover point should be and add it to both ends.
Below, we determine the current end colors by sorting and then use linear interpolation (lerp) to get the crossover color. I've prefixed my meaningful changes with comments that start with // ###.
// ### lerp for hexadecimal color strings
function lerpColor(a, b, amount) {
const
ah = +a.replace('#', '0x'),
ar = ah >> 16,
ag = ah >> 8 & 0xff,
ab = ah & 0xff,
bh = +b.replace('#', '0x'),
br = bh >> 16,
bg = bh >> 8 & 0xff,
bb = bh & 0xff,
rr = ar + amount * (br - ar),
rg = ag + amount * (bg - ag),
rb = ab + amount * (bb - ab)
;
return '#' + (0x1000000 + (rr << 16) + (rg << 8) + rb | 0).toString(16).slice(1);
}
const colors = [
{ color: "#FF0000", pos: 0 },
{ color: "#FFFF00", pos: 1 / 5 },
{ color: "#00FF00", pos: 2 / 5 },
{ color: "#0000FF", pos: 3 / 5 },
{ color: "#FF00FF", pos: 4 / 5 },
{ color: "#FF0000", pos: 1 },
];
const angleStep = 0.2;
const linearStep = 0.005;
function init() {
const canvas = document.querySelector("canvas");
const context = canvas.getContext("2d");
const mw = canvas.width;
const mh = canvas.height;
let angle = 0;
function drawScreen() {
angle = (angle + angleStep) % 360;
const [x1, y1, x2, y2] = angleToPoints(angle, mw, mh);
const gradient = context.createLinearGradient(x1, y1, x2, y2);
for (const colorStop of colors) {
gradient.addColorStop(colorStop.pos, colorStop.color);
colorStop.pos += linearStep;
// ### corrected error here
if (colorStop.pos > 1) colorStop.pos -= 1;
}
// ### compute and set the gradient end stops
const sortedStops = colors.sort((a,b) => a.pos - b.pos);
const firstStop = sortedStops[0];
const lastStop = sortedStops.slice(-1)[0];
const endColor = lerpColor(firstStop.color, lastStop.color, firstStop.pos*5);
gradient.addColorStop(0, endColor);
gradient.addColorStop(1, endColor);
context.fillStyle = gradient;
context.fillRect(0, 0, canvas.width, canvas.height);
}
function loop() {
drawScreen()
requestAnimationFrame(loop)
}
loop();
}
function angleToPoints(angle, width, height){
const rad = ((180 - angle) / 180) * Math.PI;
// This computes the length such that the start/stop points will be at the corners
const length = Math.abs(width * Math.sin(rad)) + Math.abs(height * Math.cos(rad));
// Compute the actual x,y points based on the angle, length of the gradient line and the center of the div
const halfx = (Math.sin(rad) * length) / 2.0
const halfy = (Math.cos(rad) * length) / 2.0
const cx = width / 2.0
const cy = height / 2.0
const x1 = cx - halfx
const y1 = cy - halfy
const x2 = cx + halfx
const y2 = cy + halfy
return [x1, y1, x2, y2];
}
init();
html, body, canvas { width: 100%; height: 100%; margin: 0; padding: 0; }
<canvas width="128" height="72"></canvas>

How to displace a circle minimally outside a rectangle?

This seems like it should be pretty simple but I could not find any clear answers on it. Say I have a single circle and rectangle. If the circle is outside of the rectangle, it should maintain its current position. However, if it is inside the rectangle at all, it should be displaced minimally such that it is barely outside the rectangle.
I have created a full demo below that demonstrates my current work-in-progress. My initial idea was to clamp the circle to the closest edge, but that seemed to not be working properly. I think there might be a solution involving Separating Axis Theorem, but I'm not sure if that applies here or if it's overkill for this sort of thing.
let canvas = document.querySelector("canvas");
let ctx = canvas.getContext("2d");
function draw() {
ctx.fillStyle = "#b2c7ef";
ctx.fillRect(0, 0, 800, 800);
ctx.fillStyle = "#fff";
drawCircle(circlePos.x, circlePos.y, circleR);
drawSquare(squarePos.x, squarePos.y, squareW, squareH);
}
function drawCircle(xCenter, yCenter, radius) {
ctx.beginPath();
ctx.arc(xCenter, yCenter, radius, 0, 2 * Math.PI);
ctx.fill();
}
function drawSquare(x, y, w, h) {
ctx.beginPath();
ctx.rect(x, y, w, h);
ctx.stroke();
}
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
function getCircleRectangleDisplacement(rX, rY, rW, rH, cX, cY, cR) {
let nearestX = clamp(cX, rX, rX + rW);
let nearestY = clamp(cY, rY, rY + rH);
let newX = nearestX - cR / 2;
let newY = nearestY - cR / 2;
return { x: newX, y: newY };
}
function displace() {
circlePos = getCircleRectangleDisplacement(squarePos.x, squarePos.y, squareW, squareH, circlePos.x, circlePos.y, circleR);
draw();
}
let circlePos = { x: 280, y: 70 };
let squarePos = { x: 240, y: 110 };
let circleR = 50;
let squareW = 100;
let squareH = 100;
draw();
setTimeout(displace, 500);
canvas { display: flex; margin: 0 auto; }
<canvas width="800" height="800"></canvas>
As you can see in the demo, after 500 milliseconds the circle jumps a bit in an attempt to displace itself properly, but it does not move to the correct location. Is there an algorithm to find the circle's new location that would require as little movement as possible to move it outside of the bounds of the rectangle?
Have a look here, core is in calc() function, it's Java, not JavaScript , but I think that you can easily translate it.
package test;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import javax.swing.JComponent;
import javax.swing.JFrame;
public class CircleOutside extends JComponent {
protected Rectangle2D rect;
protected Point2D originalCenter;
protected double radius;
protected Point2D movedCenter;
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2=(Graphics2D) g;
g2.draw(rect);
g.setColor(Color.red);
g2.draw(new Ellipse2D.Double(originalCenter.getX()-radius, originalCenter.getY()-radius, 2*radius, 2*radius));
g.setColor(Color.green);
g2.draw(new Ellipse2D.Double(movedCenter.getX()-radius, movedCenter.getY()-radius, 2*radius, 2*radius));
addMouseListener(new MouseAdapter() {
#Override
public void mouseClicked(MouseEvent e) {
originalCenter=e.getPoint();
calc();
repaint();
}
});
}
public void calc() {
movedCenter=originalCenter;
//Circle center distance from edges greater than radius, do not move
if (originalCenter.getY()+radius<=rect.getY()) {
return;
}
if (originalCenter.getY()-radius>=rect.getY()+rect.getHeight()) {
return;
}
if (originalCenter.getX()+radius<=rect.getX()) {
return;
}
if (originalCenter.getX()-radius>=rect.getX()+rect.getWidth()) {
return;
}
double moveX=0;
double moveY=0;
boolean movingY=false;
boolean movingX=false;
//Center projects into rectangle's width, move up or down
if (originalCenter.getX()>=rect.getX()&&originalCenter.getX()<=rect.getX()+rect.getWidth()) {
System.out.println("X in width");
double moveUp=rect.getY()-originalCenter.getY()-radius;
double moveDown=rect.getY()+rect.getHeight()-originalCenter.getY()+radius;
if (Math.abs(moveUp)<=Math.abs(moveDown)) {
moveY=moveUp;
} else {
moveY=moveDown;
}
System.out.println("UP "+moveUp+" DOWN "+moveDown);
movingY=true;
}
//Center projects into rectangle's height, move left or right
if (originalCenter.getY()>=rect.getY()&&originalCenter.getY()<=rect.getY()+rect.getHeight()) {
double moveLeft=rect.getX()-originalCenter.getX()-radius;
double moveRight=rect.getX()+rect.getWidth()-originalCenter.getX()+radius;
if (Math.abs(moveLeft)<=Math.abs(moveRight)) {
moveX=moveLeft;
} else {
moveX=moveRight;
}
movingX=true;
}
//If circle can be moved both on X or Y, choose the lower distance
if (movingX&&movingY) {
if (Math.abs(moveY)<Math.abs(moveX)) {
moveX=0;
} else {
moveY=0;
}
}
//Note that the following cases are mutually excluding with the previous ones
//Center is in the arc [90-180] centered in upper left corner with same radius as circle, calculate distance from corner and adjust both axis
if (originalCenter.getX()<rect.getX()&&originalCenter.getY()<rect.getY()) {
double dist=originalCenter.distance(rect.getX(),rect.getY());
if (dist<radius) {
double factor=(radius-dist)/dist;
moveX=factor*(originalCenter.getX()-rect.getX());
moveY=factor*(originalCenter.getY()-rect.getY());
}
}
//Center is in the arc [0-90] centered in upper right corner with same radius as circle, calculate distance from corner and adjust both axis
if (originalCenter.getX()>rect.getX()+rect.getWidth()&&originalCenter.getY()<rect.getY()) {
double dist=originalCenter.distance(rect.getX()+rect.getWidth(),rect.getY());
if (dist<radius) {
double factor=(radius-dist)/dist;
moveX=factor*(originalCenter.getX()-rect.getX()-rect.getWidth());
moveY=factor*(originalCenter.getY()-rect.getY());
}
}
//Center is in the arc [270-360] centered in lower right corner with same radius as circle, calculate distance from corner and adjust both axis
if (originalCenter.getX()>rect.getX()+rect.getWidth()&&originalCenter.getY()>rect.getY()+rect.getHeight()) {
double dist=originalCenter.distance(rect.getX()+rect.getWidth(),rect.getY()+rect.getHeight());
if (dist<radius) {
double factor=(radius-dist)/dist;
moveX=factor*(originalCenter.getX()-rect.getX()-rect.getWidth());
moveY=factor*(originalCenter.getY()-rect.getY()-rect.getHeight());
}
}
//Center is in the arc [180-270] centered in lower left corner with same radius as circle, calculate distance from corner and adjust both axis
if (originalCenter.getX()<rect.getX()&&originalCenter.getY()>rect.getY()+rect.getHeight()) {
double dist=originalCenter.distance(rect.getX(),rect.getY()+rect.getHeight());
if (dist<radius) {
double factor=(radius-dist)/dist;
moveX=factor*(originalCenter.getX()-rect.getX());
moveY=factor*(originalCenter.getY()-rect.getY()-rect.getHeight());
}
}
movedCenter=new Point2D.Double(originalCenter.getX()+moveX,originalCenter.getY()+moveY);
}
public static void main(String[] args) {
Rectangle2D rect=new Rectangle2D.Double(240, 110, 100, 100);
Point2D center=new Point2D.Double(280, 70);
double radius=50;
CircleOutside o=new CircleOutside();
o.rect=rect;
o.originalCenter=center;
o.radius=radius;
o.calc();
o.setPreferredSize(new Dimension(800,600));
JFrame frame=new JFrame("Test circle");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setContentPane(o);
frame.pack();
frame.setVisible(true);
}
}
Made the following modifications to the code:
Added function pointToSegmentDistance which calculates both the distance of a point to a line segment in addition to the corresponding perpendicular point on the line segment.
Added function pointInPolygon which determines whether a point resides inside or outside of a polygon.
Modified function getCircleRectangleDisplacement to perform the following:
Create a bounding polygon that extends the edges of the rectangle by the length of the radius. Then, if the circle center resides inside this bounding polygon, it needs to be moved to one of the four (4) extended edges. Function pointInPolygon determines whether the circle center is in the bounding polygon, and if so, then pointToSegmentDistance is used to find the closest point on one of the four (4) extended edges, a point which now represents the new circle center.
Otherwise, if the circle center is outside the bounding polygon, then the function checks if the circle center is less than the length of the radius to one of the four vertices, and if so, moves the circle center away from the vertex such that the distance is now the radius.
<html><head>
<style>
canvas { display: flex; margin: 0 auto; }
</style>
</head><body>
<canvas width="800" height="800"></canvas>
<script>
let canvas = document.querySelector("canvas");
let ctx = canvas.getContext("2d");
function draw() {
ctx.fillStyle = "#fff";
drawCircle(circlePos.x, circlePos.y, circleR);
drawSquare(squarePos.x, squarePos.y, squareW, squareH);
}
function drawCircle(xCenter, yCenter, radius) {
ctx.fillStyle = "#fff";
ctx.beginPath();
ctx.arc(xCenter, yCenter, radius, 0, 2 * Math.PI);
ctx.fill();
}
function drawSquare(x, y, w, h) {
ctx.fillStyle = "#f0f";
ctx.beginPath();
ctx.rect(x, y, w, h);
ctx.fill();
}
// Sourced and adapted from https://stackoverflow.com/a/6853926/7696162
function pointToSegmentDistance(point, segBeg, segEnd) {
var A = point.x - segBeg.x;
var B = point.y - segBeg.y;
var C = segEnd.x - segBeg.x;
var D = segEnd.y - segBeg.y;
var dot = A * C + B * D;
var len_sq = C * C + D * D;
var param = -1;
if (len_sq != 0) //in case of 0 length line
param = dot / len_sq;
let intersectPoint;
if (param < 0) {
intersectPoint = segBeg;
}
else if (param > 1) {
intersectPoint = segEnd;
}
else {
intersectPoint = { x: segBeg.x + param * C, y:segBeg.y + param * D };
}
var dx = point.x - intersectPoint.x;
var dy = point.y - intersectPoint.y;
return { intersect: intersectPoint, distance: Math.sqrt(dx * dx + dy * dy) };
}
// Sourced and adapted from https://www.algorithms-and-technologies.com/point_in_polygon/javascript
function pointInPolygon( point, polygon ) {
let vertices = polygon.vertex;
//A point is in a polygon if a line from the point to infinity crosses the polygon an odd number of times
let odd = false;
//For each edge (In this case for each point of the polygon and the previous one)
for (let i = 0, j = polygon.length - 1; i < polygon.length; i++) {
//If a line from the point into infinity crosses this edge
if (((polygon[i].y > point.y) !== (polygon[j].y > point.y)) // One point needs to be above, one below our y coordinate
// ...and the edge doesn't cross our Y corrdinate before our x coordinate (but between our x coordinate and infinity)
&& (point.x < ((polygon[j].x - polygon[i].x) * (point.y - polygon[i].y) / (polygon[j].y - polygon[i].y) + polygon[i].x))) {
// Invert odd
odd = !odd;
}
j = i;
}
//If the number of crossings was odd, the point is in the polygon
return odd;
}
function getCircleRectangleDisplacement( rX, rY, rW, rH, cX, cY, cR ) {
let rect = [
{ x: rX, y:rY },
{ x: rX + rW, y:rY },
{ x: rX + rW, y:rY + rH },
{ x: rX, y:rY + rH }
];
let boundingPolygon = [
{ x: rX, y: rY },
{ x: rX, y: rY - cR },
{ x: rX + rW, y: rY - cR },
{ x: rX + rW, y: rY },
{ x: rX + rW + cR, y: rY },
{ x: rX + rW + cR, y: rY + rH },
{ x: rX + rW, y: rY + rH },
{ x: rX + rW, y: rY + rH + cR },
{ x: rX, y: rY + rH + cR },
{ x: rX, y: rY + rH },
{ x: rX - cR, y: rY + rH },
{ x: rX - cR, y: rY }
];
// Draw boundingPolygon... This can be removed...
ctx.setLineDash([2,2]);ctx.beginPath();ctx.moveTo(boundingPolygon[0].x,boundingPolygon[0].y);for (let p of boundingPolygon) {ctx.lineTo(p.x,p.y);} ctx.lineTo(boundingPolygon[0].x,boundingPolygon[0].y);ctx.stroke();
circleCenter = { x: cX, y: cY };
// If the circle center is inside the bounding polygon...
if ( pointInPolygon( circleCenter, boundingPolygon ) ) {
let newCircleCenter;
let minDistance = Number.MAX_VALUE;
// ...then loop through the 4 segments of the bounding polygon that are
// extensions of the original rectangle, looking for the point that is
// closest to the circle center.
for ( let i = 1; i < boundingPolygon.length; i += 3 ) {
let pts = pointToSegmentDistance( circleCenter, boundingPolygon[ i ], boundingPolygon[ i + 1 ] );
if ( pts.distance < minDistance ) {
newCircleCenter = pts.intersect;
minDistance = pts.distance;
}
}
circleCenter = newCircleCenter;
} else {
// ...otherwise, if the circle center is outside the bounding polygon,
// let's check to see if the circle center is closer than the radius
// to one of the corners of the rectangle.
let newCircleCenter;
let minDistance = Number.MAX_VALUE;
for ( let i = 0; i < boundingPolygon.length; i += 3 ) {
let d = Math.sqrt( ( circleCenter.x - boundingPolygon[ i ].x ) ** 2 + ( circleCenter.y - boundingPolygon[ i ].y ) ** 2 );
if ( d < cR && d < minDistance ) {
// Okay, the circle is too close to a corner. Let's move it away...
newCircleCenter = {
x: boundingPolygon[ i ].x + ( circleCenter.x - boundingPolygon[ i ].x ) * cR / d,
y: boundingPolygon[ i ].y + ( circleCenter.y - boundingPolygon[ i ].y ) * cR / d
}
minDistance = d;
}
}
if ( newCircleCenter ) {
circleCenter = newCircleCenter;
}
}
return circleCenter;
}
function displace() {
ctx.fillStyle = "#b2c7ef";
ctx.fillRect(0, 0, 800, 800);
circlePos.x += 1;
circlePos.y += 1;
circlePos = getCircleRectangleDisplacement(squarePos.x, squarePos.y, squareW, squareH, circlePos.x, circlePos.y, circleR);
draw();
if ( maxIterations < iterations++ ) {
clearInterval( timer );
}
}
let circlePos = { x: 280, y: 40 };
circlePos={ x: 240, y: 110 };
let squarePos = { x: 240, y: 110 };
let circleR = 50;
let squareW = 100;
let squareH = 100;
let iterations = 0;
let maxIterations = 200;
let timer = setInterval(displace, 50);
</script>
</body></html>
I believe this algorithm can be extended to simple polygons (ie, convex polygons, not concave polygons) although with a bit more trigonometry and/or matrix math...

JavaScript Point Collision with Regular Hexagon

I'm making an HTML5 canvas hexagon grid based system and I need to be able to detect what hexagonal tile in a grid has been clicked when the canvas is clicked.
Several hours of searching and trying my own methods led to nothing, and porting implementations from other languages has simply confused me to a point where my brain is sluggish.
The grid consists of flat topped regular hexagons like in this diagram:
Essentially, given a point and the variables specified in this image as the sizing for every hexagon in the grid (R, W, S, H):
I need to be able to determine whether a point is inside a hexagon given.
An example function call would be pointInHexagon(hexX, hexY, R, W, S, H, pointX, pointY) where hexX and hexY are the coordinates for the top left corner of the bounding box of a hexagonal tile (like the top left corner in the image above).
Is there anyone who has any idea how to do this? Speed isn't much of a concern for the moment.
Simple & fast diagonal rectangle slice.
Looking at the other answers I see that they have all a little over complicated the problem. The following is an order of magnitude quicker than the accepted answer and does not require any complicated data structures, iterators, or generate dead memory and unneeded GC hits. It returns the hex cell row and column for any related set of R, H, S or W. The example uses R = 50.
Part of the problem is finding which side of a rectangle a point is if the rectangle is split diagonally. This is a very simple calculation and is done by normalising the position of the point to test.
Slice any rectangle diagonally
Example a rectangle of width w, and height h split from top left to bottom right. To find if a point is left or right. Assume top left of rectangle is at rx,ry
var x = ?;
var y = ?;
x = ((x - rx) % w) / w;
y = ((y - ry) % h) / h;
if (x > y) {
// point is in the upper right triangle
} else if (x < y) {
// point is in lower left triangle
} else {
// point is on the diagonal
}
If you want to change the direction of the diagonal then just invert one of the normals
x = 1 - x; // invert x or y to change the direction the rectangle is split
if (x > y) {
// point is in the upper left triangle
} else if (x < y) {
// point is in lower right triangle
} else {
// point is on the diagonal
}
Split into sub cells and use %
The rest of the problem is just a matter of splitting the grid into (R / 2) by (H / 2) cells width each hex covering 4 columns and 2 rows. Every 1st column out of 3 will have diagonals. with every second of these column having the diagonal flipped. For every 4th, 5th, and 6th column out of 6 have the row shifted down one cell. By using % you can very quickly determine which hex cell you are on. Using the diagonal split method above make the math easy and quick.
And one extra bit. The return argument retPos is optional. if you call the function as follows
var retPos;
mainLoop(){
retPos = getHex(mouse.x, mouse.y, retPos);
}
the code will not incur a GC hit, further improving the speed.
Pixel to Hex coordinates
From Question diagram returns hex cell x,y pos. Please note that this function only works in the range 0 <= x, 0 <= y if you need negative coordinates subtract the min negative pixel x,y coordinate from the input
// the values as set out in the question image
var r = 50;
var w = r * 2;
var h = Math.sqrt(3) * r;
// returns the hex grid x,y position in the object retPos.
// retPos is created if not supplied;
// argument x,y is pixel coordinate (for mouse or what ever you are looking to find)
function getHex (x, y, retPos){
if(retPos === undefined){
retPos = {};
}
var xa, ya, xpos, xx, yy, r2, h2;
r2 = r / 2;
h2 = h / 2;
xx = Math.floor(x / r2);
yy = Math.floor(y / h2);
xpos = Math.floor(xx / 3);
xx %= 6;
if (xx % 3 === 0) { // column with diagonals
xa = (x % r2) / r2; // to find the diagonals
ya = (y % h2) / h2;
if (yy % 2===0) {
ya = 1 - ya;
}
if (xx === 3) {
xa = 1 - xa;
}
if (xa > ya) {
retPos.x = xpos + (xx === 3 ? -1 : 0);
retPos.y = Math.floor(yy / 2);
return retPos;
}
retPos.x = xpos + (xx === 0 ? -1 : 0);
retPos.y = Math.floor((yy + 1) / 2);
return retPos;
}
if (xx < 3) {
retPos.x = xpos + (xx === 3 ? -1 : 0);
retPos.y = Math.floor(yy / 2);
return retPos;
}
retPos.x = xpos + (xx === 0 ? -1 : 0);
retPos.y = Math.floor((yy + 1) / 2);
return retPos;
}
Hex to pixel
And a helper function that draws a cell given the cell coordinates.
// Helper function draws a cell at hex coordinates cellx,celly
// fStyle is fill style
// sStyle is strock style;
// fStyle and sStyle are optional. Fill or stroke will only be made if style given
function drawCell1(cellPos, fStyle, sStyle){
var cell = [1,0, 3,0, 4,1, 3,2, 1,2, 0,1];
var r2 = r / 2;
var h2 = h / 2;
function drawCell(x, y){
var i = 0;
ctx.beginPath();
ctx.moveTo((x + cell[i++]) * r2, (y + cell[i++]) * h2)
while (i < cell.length) {
ctx.lineTo((x + cell[i++]) * r2, (y + cell[i++]) * h2)
}
ctx.closePath();
}
ctx.lineWidth = 2;
var cx = Math.floor(cellPos.x * 3);
var cy = Math.floor(cellPos.y * 2);
if(cellPos.x % 2 === 1){
cy -= 1;
}
drawCell(cx, cy);
if (fStyle !== undefined && fStyle !== null){ // fill hex is fStyle given
ctx.fillStyle = fStyle
ctx.fill();
}
if (sStyle !== undefined ){ // stroke hex is fStyle given
ctx.strokeStyle = sStyle
ctx.stroke();
}
}
I think you need something like this~
EDITED
I did some maths and here you have it. This is not a perfect version but probably will help you...
Ah, you only need a R parameter because based on it you can calculate H, W and S. That is what I understand from your description.
// setup canvas for demo
var canvas = document.getElementById('canvas');
canvas.width = 300;
canvas.height = 275;
var context = canvas.getContext('2d');
var hexPath;
var hex = {
x: 50,
y: 50,
R: 100
}
// Place holders for mouse x,y position
var mouseX = 0;
var mouseY = 0;
// Test for collision between an object and a point
function pointInHexagon(target, pointX, pointY) {
var side = Math.sqrt(target.R*target.R*3/4);
var startX = target.x
var baseX = startX + target.R / 2;
var endX = target.x + 2 * target.R;
var startY = target.y;
var baseY = startY + side;
var endY = startY + 2 * side;
var square = {
x: startX,
y: startY,
side: 2*side
}
hexPath = new Path2D();
hexPath.lineTo(baseX, startY);
hexPath.lineTo(baseX + target.R, startY);
hexPath.lineTo(endX, baseY);
hexPath.lineTo(baseX + target.R, endY);
hexPath.lineTo(baseX, endY);
hexPath.lineTo(startX, baseY);
if (pointX >= square.x && pointX <= (square.x + square.side) && pointY >= square.y && pointY <= (square.y + square.side)) {
var auxX = (pointX < target.R / 2) ? pointX : (pointX > target.R * 3 / 2) ? pointX - target.R * 3 / 2 : target.R / 2;
var auxY = (pointY <= square.side / 2) ? pointY : pointY - square.side / 2;
var dPointX = auxX * auxX;
var dPointY = auxY * auxY;
var hypo = Math.sqrt(dPointX + dPointY);
var cos = pointX / hypo;
if (pointX < (target.x + target.R / 2)) {
if (pointY <= (target.y + square.side / 2)) {
if (pointX < (target.x + (target.R / 2 * cos))) return false;
}
if (pointY > (target.y + square.side / 2)) {
if (pointX < (target.x + (target.R / 2 * cos))) return false;
}
}
if (pointX > (target.x + target.R * 3 / 2)) {
if (pointY <= (target.y + square.side / 2)) {
if (pointX < (target.x + square.side - (target.R / 2 * cos))) return false;
}
if (pointY > (target.y + square.side / 2)) {
if (pointX < (target.x + square.side - (target.R / 2 * cos))) return false;
}
}
return true;
}
return false;
}
// Loop
setInterval(onTimerTick, 33);
// Render Loop
function onTimerTick() {
// Clear the canvas
canvas.width = canvas.width;
// see if a collision happened
var collision = pointInHexagon(hex, mouseX, mouseY);
// render out text
context.fillStyle = "Blue";
context.font = "18px sans-serif";
context.fillText("Collision: " + collision + " | Mouse (" + mouseX + ", " + mouseY + ")", 10, 20);
// render out square
context.fillStyle = collision ? "red" : "green";
context.fill(hexPath);
}
// Update mouse position
canvas.onmousemove = function(e) {
mouseX = e.offsetX;
mouseY = e.offsetY;
}
#canvas {
border: 1px solid black;
}
<canvas id="canvas"></canvas>
Just replace your pointInHexagon(hexX, hexY, R, W, S, H, pointX, pointY) by the var hover = ctx.isPointInPath(hexPath, x, y).
This is for Creating and copying paths
This is about the Collision Detection
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var hexPath = new Path2D();
hexPath.lineTo(25, 0);
hexPath.lineTo(75, 0);
hexPath.lineTo(100, 43);
hexPath.lineTo(75, 86);
hexPath.lineTo(25, 86);
hexPath.lineTo(0, 43);
function draw(hover) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = hover ? 'blue' : 'red';
ctx.fill(hexPath);
}
canvas.onmousemove = function(e) {
var x = e.clientX - canvas.offsetLeft, y = e.clientY - canvas.offsetTop;
var hover = ctx.isPointInPath(hexPath, x, y)
draw(hover)
};
draw();
<canvas id="canvas"></canvas>
I've made a solution for you that demonstrates the point in triangle approach to this problem.
http://codepen.io/spinvector/pen/gLROEp
maths below:
isPointInside(point)
{
// Point in triangle algorithm from http://totologic.blogspot.com.au/2014/01/accurate-point-in-triangle-test.html
function pointInTriangle(x1, y1, x2, y2, x3, y3, x, y)
{
var denominator = ((y2 - y3)*(x1 - x3) + (x3 - x2)*(y1 - y3));
var a = ((y2 - y3)*(x - x3) + (x3 - x2)*(y - y3)) / denominator;
var b = ((y3 - y1)*(x - x3) + (x1 - x3)*(y - y3)) / denominator;
var c = 1 - a - b;
return 0 <= a && a <= 1 && 0 <= b && b <= 1 && 0 <= c && c <= 1;
}
// A Hex is composite of 6 trianges, lets do a point in triangle test for each one.
// Step through our triangles
for (var i = 0; i < 6; i++) {
// check for point inside, if so, return true for this function;
if(pointInTriangle( this.origin.x, this.origin.y,
this.points[i].x, this.points[i].y,
this.points[(i+1)%6].x, this.points[(i+1)%6].y,
point.x, point.y))
return true;
}
// Point must be outside.
return false;
}
Here is a fully mathematical and functional representation of your problem. You will notice that there are no ifs and thens in this code other than the ternary to change the color of the text depending on the mouse position. This whole job is in fact nothing more than pure simple math of just one line;
(r+m)/2 + Math.cos(a*s)*(r-m)/2;
and this code is reusable for all polygons from triangle to circle. So if interested please read on. It's very simple.
In order to display the functionality I had to develop a mimicking model of the problem. I draw a polygon on a canvas by utilizing a simple utility function. So that the overall solution should work for any polygon. The following snippet will take the canvas context c, radius r, number of sides s, and the local center coordinates in the canvas cx and cy as arguments and draw a polygon on the given canvas context at the right position.
function drawPolgon(c, r, s, cx, cy){ //context, radius, sides, center x, center y
c.beginPath();
c.moveTo(cx + r,cy);
for(var p = 1; p < s; p++) c.lineTo(cx + r*Math.cos(p*2*Math.PI/s), cy + r*Math.sin(p*2*Math.PI/s));
c.closePath();
c.stroke();
}
We have some other utility functions which one can easily understand what exactly they are doing. However the most important part is to check whether the mouse is floating over our polygon or not. It's done by the utility function isMouseIn. It's basically calculating the distance and the angle of the mouse position to the center of the polygon. Then, comparing it with the boundaries of the polygon. The boundaries of the polygon can be expressed by simple trigonometry, just like we have calculated the vertices in the drawPolygon function.
We can think of our polygon as a circle with an oscillating radius at the frequency of number of sides. The oscillation's peak is at the given radius value r (which happens to be at the vertices at angle 2π/s where s is the number of sides) and the minimum m is r*Math.cos(Math.PI/s) (each shows at at angle 2π/s + 2π/2s = 3π/s). I am pretty sure the ideal way to express a polygon could be done by the Fourier transformation but we don't need that here. All we need is a constant radius component which is the average of minimum and maximum, (r+m)/2 and the oscillating component with the frequency of number of sides, s and the amplitude value maximum - minimum)/2 on top of it, Math.cos(a*s)*(r-m)/2. Well of course as per Fourier states we might carry on with smaller oscillating components but with a hexagon you don't really need further iteration while with a triangle you possibly would. So here is our polygon representation in math.
(r+m)/2 + Math.cos(a*s)*(r-m)/2;
Now all we need is to calculate the angle and distance of our mouse position relative to the center of the polygon and compare it with the above mathematical expression which represents our polygon. So all together our magic function is orchestrated as follows;
function isMouseIn(r,s,cx,cy,mx,my){
var m = r*Math.cos(Math.PI/s), // the min dist from an edge to the center
d = Math.hypot(mx-cx,my-cy), // the mouse's distance to the center of the polygon
a = Math.atan2(cy-my,mx-cx); // angle of the mouse pointer
return d <= (r+m)/2 + Math.cos(a*s)*(r-m)/2;
}
So the following code demonstrates how you might approach to solve your problem.
// Generic function to draw a polygon on the canvas
function drawPolgon(c, r, s, cx, cy){ //context, radius, sides, center x, center y
c.beginPath();
c.moveTo(cx + r,cy);
for(var p = 1; p < s; p++) c.lineTo(cx + r*Math.cos(p*2*Math.PI/s), cy + r*Math.sin(p*2*Math.PI/s));
c.closePath();
c.stroke();
}
// To write the mouse position in canvas local coordinates
function writeText(c,x,y,msg,col){
c.clearRect(0, 0, 300, 30);
c.font = "10pt Monospace";
c.fillStyle = col;
c.fillText(msg, x, y);
}
// Getting the mouse position and coverting into canvas local coordinates
function getMousePos(c, e) {
var rect = c.getBoundingClientRect();
return { x: e.clientX - rect.left,
y: e.clientY - rect.top
};
}
// To check if mouse is inside the polygone
function isMouseIn(r,s,cx,cy,mx,my){
var m = r*Math.cos(Math.PI/s),
d = Math.hypot(mx-cx,my-cy),
a = Math.atan2(cy-my,mx-cx);
return d <= (r+m)/2 + Math.cos(a*s)*(r-m)/2;
}
// the event listener callback
function mouseMoveCB(e){
var mp = getMousePos(cnv, e),
msg = 'Mouse at: ' + mp.x + ',' + mp.y,
col = "black",
inside = isMouseIn(radius,sides,center[0],center[1],mp.x,mp.y);
writeText(ctx, 10, 25, msg, inside ? "turquoise" : "red");
}
// body of the JS code
var cnv = document.getElementById("myCanvas"),
ctx = cnv.getContext("2d"),
sides = 6,
radius = 100,
center = [150,150];
cnv.addEventListener('mousemove', mouseMoveCB, false);
drawPolgon(ctx, radius, sides, center[0], center[1]);
#myCanvas { background: #eee;
width: 300px;
height: 300px;
border: 1px #ccc solid
}
<canvas id="myCanvas" width="300" height="300"></canvas>
At the redblog there is a full explanation with math and working examples.
The main idea is that hexagons are horizontally spaced by $3/4$ of hexagons size, vertically it is simply $H$ but the column needs to be taken to take vertical offset into account. The case colored red is determined by comparing x to y at 1/4 W slice.

RGB Sliders wiggle when changing value

I'm trying to implement my own color wheel picker, and I'm using this color wheel, as the base. I (finally) successfully added RGB sliders. So now when you change the color wheels color, the RGB sliders change dynamically, and when you change the RGB sliders, the color wheels color updates too.
The problem is, when I slide a slider from the RGB sliders, the other sliders tend to move a little bit too. For example, if I slide the green value, the red and blue values change a little bit.
I'm not exactly sure what's wrong. How can I get the other sliders to not move when I move one slider? (Obviously if I don't set the sliders value in redraw(), the sliders won't change when I move 1 slider, but I'm trying to find the core issue.)
JSFiddle
var b = document.body;
var colorWheelDiv = document.getElementById('colorWheelDiv');
var colorWheel = document.createElement('canvas');
var colorWheelOverlay = document.createElement('div');
var a = colorWheel.getContext('2d');
var label = document.getElementById('label');
var input = document.getElementById('input');
var redInput = document.getElementById('red');
var greenInput = document.getElementById('green');
var blueInput = document.getElementById('blue');
var alphaInput = document.getElementById('alpha');
var rgbInput = document.getElementsByClassName('rgbInput');
document.body.clientWidth; // fix bug in webkit: http://qfox.nl/weblog/218
// Jquery Elements
var $redSlider = $('#red');
var $greenSlider = $('#green');
var $blueSlider = $('#blue');
var $alphaSlider = $('#alpha');
(function() {
// Declare constants and variables to help with minification
// Some of these are inlined (with comments to the side with the actual equation)
var doc = document;
doc.colorWheel = doc.createElement;
b.a = b.appendChild;
// Add the colorWheel and the colorWheelOverlay
colorWheelDiv.appendChild(colorWheelOverlay);
colorWheelDiv.appendChild(colorWheel);
colorWheelOverlay.id = 'colorWheelOverlay';
colorWheel.id = 'colorWheel';
var width = colorWheel.width = colorWheel.height = colorWheelDiv.clientHeight,
imageData = a.createImageData(width, width),
pixels = imageData.data,
oneHundred = input.value = input.max = 100,
circleOffset = 10,
diameter = width - circleOffset * 2,
radius = diameter / 2,
radiusPlusOffset = radius + circleOffset,
radiusSquared = radius * radius,
two55 = 255,
currentY = oneHundred,
currentX = -currentY,
center = radius / 2,
wheelPixel = circleOffset * 4 * width + circleOffset * 4;
// Math helpers
var math = Math,
PI = math.PI,
PI2 = PI * 2,
sqrt = math.sqrt,
atan2 = math.atan2;
// Load color wheel data into memory.
for (y = input.min = 0; y < width; y++) {
for (x = 0; x < width; x++) {
var rx = x - radius,
ry = y - radius,
d = rx * rx + ry * ry,
rgb = colorWheel_hsvToRgb(
(atan2(ry, rx) + PI) / PI2, // Hue
sqrt(d) / radius, // Saturation
1 // Value
);
// Print current color, but hide if outside the area of the circle
pixels[wheelPixel++] = rgb[0];
pixels[wheelPixel++] = rgb[1];
pixels[wheelPixel++] = rgb[2];
pixels[wheelPixel++] = d > radiusSquared ? 0 : two55;
}
}
// Bind Event Handlers
input.oninput = redraw;
colorWheel.onmousedown = doc.onmouseup = function(e) {
// Unbind mousemove if this is a mouseup event, or bind mousemove if this a mousedown event
doc.onmousemove = /p/.test(e.type) ? 0 : (redraw(e), redraw);
}
$(".rgbInput").not($alphaSlider).slider({
range: "max",
min: 0,
max: 255,
value: 0,
slide: function(event, ui) {
redrawRGB();
}
});
function redrawRGB() {
var red = $('#red').slider('value');
var green = $('#green').slider('value');
var blue = $('#blue').slider('value');
var hsv = colorWheel_rgbToHsv(red, green, blue);
var newD = math.round(math.pow(radius * hsv.s, 2));
var newTheta = (hsv.h * PI2) - PI;
currentX = math.round(math.sqrt(newD) * math.cos(newTheta));
currentY = math.round(math.sqrt(newD) * math.sin(newTheta));
input.value = math.round(hsv.v * 100);
redraw(0);
}
// Handle manual calls + mousemove event handler + input change event handler all in one place.
function redraw(e) {
// Only process an actual change if it is triggered by the mousemove or mousedown event.
// Otherwise e.pageX will be undefined, which will cause the result to be NaN, so it will fallback to the current value
currentX = e.pageX - colorWheelDiv.offsetLeft - colorWheel.offsetLeft - radiusPlusOffset || currentX;
currentY = e.pageY - colorWheelDiv.offsetTop - colorWheel.offsetTop - radiusPlusOffset || currentY;
// Scope these locally so the compiler will minify the names. Will manually remove the 'var' keyword in the minified version.
var theta = atan2(currentY, currentX),
d = currentX * currentX + currentY * currentY;
// If the x/y is not in the circle, find angle between center and mouse point:
// Draw a line at that angle from center with the distance of radius
// Use that point on the circumference as the draggable location
if (d > radiusSquared) {
currentX = radius * math.cos(theta);
currentY = radius * math.sin(theta);
theta = atan2(currentY, currentX);
d = currentX * currentX + currentY * currentY;
}
var vValue = parseInt(input.value, 10);
var rgb = colorWheel_hsvToRgb(
(theta + PI) / PI2, // Current hue (how many degrees along the circle)
sqrt(d) / radius, // Current saturation (how close to the middle)
vValue / oneHundred // Current value (input type="range" slider value)
)
label.textContent = b.style.background = rgb[3];
colorWheelOverlay.style.opacity = ((vValue + 100 - 15) - (vValue * 2)) / oneHundred;
// Set slider Position \\
$redSlider.slider("value", math.round(rgb[0]));
$greenSlider.slider("value", math.round(rgb[1]));
$blueSlider.slider("value", math.round(rgb[2]));
// Reset to color wheel and draw a spot on the current location.
a.putImageData(imageData, 0, 0);
// Draw the current spot.
// I have tried a rectangle, circle, and heart shape.
/*
// Rectangle:
a.fillStyle = '#000';
a.fillRect(currentX+radiusPlusOffset,currentY+radiusPlusOffset, 6, 6);
*/
// Circle:
a.beginPath();
a.strokeStyle = '#000';
a.arc(~~currentX + radiusPlusOffset, ~~currentY + radiusPlusOffset, 4, 0, PI2);
a.stroke();
// Heart:
/* a.font = "1em arial";
a.fillText("♥", currentX+radiusPlusOffset-4,currentY+radiusPlusOffset+4);*/
}
// Created a shorter version of the HSV to RGB conversion function in TinyColor
// https://github.com/bgrins/TinyColor/blob/master/tinycolor.js
function colorWheel_hsvToRgb(h, s, v) {
h *= 6;
var i = ~~h,
f = h - i,
p = v * (1 - s),
q = v * (1 - f * s),
t = v * (1 - (1 - f) * s),
mod = i % 6,
r = [v, q, p, p, t, v][mod] * two55,
g = [t, v, v, q, p, p][mod] * two55,
b = [p, p, t, v, v, q][mod] * two55;
return [r, g, b, "rgb(" + math.round(r) + "," + math.round(g) + "," + math.round(b) + ")"];
}
function colorWheel_rgbToHsv(r, g, b) {
r = r / two55;
g = g / two55;
b = b / two55;
var max = math.max(r, g, b),
min = math.min(r, g, b);
var h, s, v = max;
var d = max - min;
s = max === 0 ? 0 : d / max;
if (max == min) {
h = 0; // achromatic
} else {
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
return {
h: h,
s: s,
v: v
};
}
// Kick everything off
redraw(0);
})();
#colorWheelDiv {
width: 400px;
height: 400px;
position: relative;
}
#colorWheelOverlay {
background-color: black;
position: absolute;
pointer-events: none;
}
#colorWheelDiv,
#colorWheelOverlay,
#colorWheel {
border-radius: 50%;
}
#colorWheelOverlay,
#colorWheel {
width: 100%;
height: 100%;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/themes/smoothness/jquery-ui.css">
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js"></script>
R:
<div id="red" class="rgbInput"></div>
<br />G:
<div id="green" class="rgbInput"></div>
<br />B:
<div id="blue" class="rgbInput"></div>
<br />A:
<div id="alpha" class="rgbInput"></div>
<br />
<div id=colorWheelDiv></div>
<p id='label' style="font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 2em; line-height: normal; font-family: courier;">rgb(239,183,131)</p>
<input id="input" max="100" type="range" min="0">
Update
I tried that answer, and it didn't help. I still get the same results. The title may be the same, but I don't think the actual question is the same
You are converting the RGB values to HSV and then converting them back to RGB to set the other slider values. The conversion process has finite precision, so that's why you see the wiggling of the other sliders. The solution would be to set the RGB slider values directly from the original RGB values, not from the computed HSV values.
UPDATE
You can accomplish this as follows:
// Set slider Position \\
if(e){
$redSlider.slider( "value", math.round(rgb[0]));
$greenSlider.slider( "value", math.round(rgb[1]));
$blueSlider.slider( "value", math.round(rgb[2]));
}

How do I draw x number of circles around a central circle, starting at the top of the center circle?

I'm trying to create a UI that has a lot of items in circles. Sometimes these circles will have related circles that should be displayed around them.
I was able to cobble together something that works, here.
The problem is that the outer circles start near 0 degrees, and I'd like them to start at an angle supplied by the consumer of the function/library. I was never a star at trigonometry, or geometry, so I could use a little help.
As you can see in the consuming code, there is a setting: startingDegree: 270 that the function getPosition should honor, but I haven't been able to figure out how.
Update 04/02/2014:
as I mentioned in my comment to Salix alba, I wasn't clear above, but what I needed was to be able to specify the radius of the satellite circles, and to have them go only partly all the way around. Salix gave a solution that calculates the size the satellites need to be to fit around the center circle uniformly.
Using some of the hints in Salix's answer, I was able to achieve the desired result... and have an extra "mode," thanks to Salix, in the future.
The working, though still rough, solution is here: http://jsfiddle.net/RD4RZ/11/. Here is the entire code (just so it's all on SO):
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
<script type="text/javascript" src="//code.jquery.com/jquery-1.10.1.js"></script>
<style type="text/css">
.circle
{
position: absolute;
width: 100px;
height: 100px;
background-repeat: no-repeat;background-position: center center;
border: 80px solid #a19084;
border-radius: 50%;
-moz-border-radius: 50%;
}
.sm
{
border: 2px solid #a19084;
}
</style>
<script type="text/javascript">//<![CDATA[
$(function () {
function sind(x) {
return Math.sin(x * Math.PI / 180);
}
/*the law of cosines:
cc = aa + bb - 2ab cos(C), where c is the satellite diameter a and b are the legs
solving for cos C, cos C = ( aa + bb - cc ) / 2ab
Math.acos((a * a + b * b - c * c) / (2 * a * b)) = C
*/
function solveAngle(a, b, c) { // Returns angle C using law of cosines
var temp = (a * a + b * b - c * c) / (2 * a * b);
if (temp >= -1 && temp <= 1)
return radToDeg(Math.acos(temp));
else
throw "No solution";
}
function radToDeg(x) {
return x / Math.PI * 180;
}
function degToRad(x) {
return x * (Math.PI / 180);
}
var satellite = {
//settings must have: collection (array), itemDiameter (number), minCenterDiameter (number), center (json with x, y numbers)
//optional: itemPadding (number), evenDistribution (boolean), centerPadding (boolean), noOverLap (boolean)
getPosition: function (settings) {
//backwards compat
settings.centerPadding = settings.centerPadding || settings.itemPadding;
settings.noOverLap = typeof settings.noOverLap == 'undefined' ? true : settings.noOverLap;
settings.startingDegree = settings.startingDegree || 270;
settings.startSatellitesOnEdge = typeof settings.startSatellitesOnEdge == 'undefined' ? true : settings.startSatellitesOnEdge;
var itemIndex = $.inArray(settings.item, settings.collection);
var itemCnt = settings.collection.length;
var satelliteSide = settings.itemDiameter + (settings.itemSeparation || 0) + (settings.itemPadding || 0);
var evenDistribution = typeof settings.evenDistribution == 'undefined' ? true : settings.evenDistribution;
var degreeOfSeparation = (360 / itemCnt);
/*
we know all three sides:
one side is the diameter of the satellite itself (plus any padding). the other two
are the parent radius + the radius of the satellite itself (plus any padding).
given that, we need to find the angle of separation using the law of cosines (solveAngle)
*/
//if (!evenDistribution) {
var side1 = ((satelliteSide / 2)) + ((settings.minCenterDiameter + (2 * settings.centerPadding)) / 2);
var side2 = satelliteSide;;
var degreeOfSeparationBasedOnSatellite = solveAngle(side1, side1, side2); //Math.acos(((((side1 * side1) + (side2 * side2)) - (side2 * side2)) / (side2 * side2 * 2)) / 180 * Math.PI) * Math.PI;
degreeOfSeparation = evenDistribution? degreeOfSeparation: settings.noOverLap ? Math.min(degreeOfSeparation, degreeOfSeparationBasedOnSatellite) : degreeOfSeparationBasedOnSatellite;
//}
//angle-angle-side
//a-A-B
var a = satelliteSide;
var A = degreeOfSeparation;
/*
the three angles of any triangle add up to 180. We know one angle (degreeOfSeparation)
and we know the other two are equivalent to each other, so...
*/
var B = (180 - A) / 2;
//b is length necessary to fit all satellites, might be too short to be outside of base circle
var b = a * sind(B) / sind(A);
var offset = (settings.itemDiameter / 2) + (settings.itemPadding || 0); // 1; //
var onBaseCircleLegLength = ((settings.minCenterDiameter / 2) + settings.centerPadding) + offset;
var offBase = false;
if (b > onBaseCircleLegLength) {
offBase = true;
}
b = settings.noOverLap ? Math.max(b, onBaseCircleLegLength) : onBaseCircleLegLength;
var radianDegree = degToRad(degreeOfSeparation);
//log('b=' + b);
//log('settings.center.x=' + settings.center.x);
//log('settings.center.y=' + settings.center.y);
var degreeOffset = settings.startingDegree;
if (settings.startSatellitesOnEdge) {
degreeOffset += ((offBase ? degreeOfSeparation : degreeOfSeparationBasedOnSatellite) / 2);
}
var i = ((Math.PI * degreeOffset) / 180) + (radianDegree * itemIndex);// + (degToRad(degreeOfSeparationBasedOnSatellite) / 2); //(radianDegree) * (itemIndex);
var x = (Math.cos(i) * b) + (settings.center.x - offset);
var y = (Math.sin(i) * b) + (settings.center.y - offset);
return { 'x': Math.round(x), 'y': Math.round(y) };
}
,
/* if we ever want to size satellite by how many need to fit tight around the base circle:
x: function calcCircles(n) {
circles.splice(0); // clear out old circles
var angle = Math.PI / n;
var s = Math.sin(angle);
var r = baseRadius * s / (1 - s);
console.log(angle);
console.log(s);
console.log(r);
console.log(startAngle);
console.log(startAngle / (Math.PI * 2));
for (var i = 0; i < n; ++i) {
var phi = ((Math.PI * startAngle) / 180) + (angle * i * 2);
var cx = 150 + (baseRadius + r) * Math.cos(phi);
var cy = 150 + (baseRadius + r) * Math.sin(phi);
circles.push(new Circle(cx, cy, r));
}
},
*/
//settings must have: collection (array), itemDiameter (number), minCenterDiameter (number), center (json with x, y numbers)
//optional: itemPadding (number), evenDistribution (boolean), centerPadding (boolean), noOverLap (boolean)
getAllPositions: function (settings) {
var point;
var points = [];
var collection = settings.collection;
for (var i = 0; i < collection.length; i++) {
settings.item = collection[i]
points.push(satellite.getPosition(settings));
}
return points;
}
};
var el = $("#center"), cnt = 10, arr = [], itemDiameter= 100;
for (var c = 0; c < cnt; c++) {
arr.push(c);
}
var settings = {
collection: arr,
itemDiameter: itemDiameter,
minCenterDiameter: el.width(),
center: { x: el.width() / 2, y: el.width() / 2 },
itemPadding: 2,
evenDistribution: false,
centerPadding: parseInt(el.css("border-width")),
noOverLap: false,
startingDegree: 270
};
var points = satellite.getAllPositions(settings);
for (var i = 0; i < points.length; i++) {
var $newdiv1 = $("<div></div>");
var div = el.append($newdiv1);
$newdiv1.addClass("circle").addClass("sm");
$newdiv1.text(i);
$newdiv1.css({ left: points[i].x, top: points[i].y, width: itemDiameter +'px', height: itemDiameter +'px' });
}
});//]]>
</script>
</head>
<body>
<div id="center" class="circle" style="left:250px;top:250px" >
</div>
</body>
</html>
The central bit you need to work out is radius of the small circles. If you have R for radius of the central circle and you want to fit n smaller circles around it. Let the as yet unknown radius of the small circle be r. We can construct a right angle triangle with one corner in the center of the big circle one in the center of the small circle and one which is where a line from the center is tangent to the small circle. This will be a right angle. The angle at the center is a the hypotenuse has length R+r the opposite is r and we don't need the adjacent. Using trig
sin(a) = op / hyp = r / (R + r)
rearrange
(R+r) sin(a) = r
R sin(a) + r sin(a) = r
R sin(a) = r - r sin(a)
R sin(a) = (1 - sin(a)) r
r = R sin(a) / ( 1 - sin(a))
once we have r we are pretty much done.
You can see this as a fiddle http://jsfiddle.net/SalixAlba/7mAAS/
// canvas and mousedown related variables
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var $canvas = $("#canvas");
var canvasOffset = $canvas.offset();
var offsetX = canvasOffset.left;
var offsetY = canvasOffset.top;
var scrollX = $canvas.scrollLeft();
var scrollY = $canvas.scrollTop();
// save canvas size to vars b/ they're used often
var canvasWidth = canvas.width;
var canvasHeight = canvas.height;
var baseRadius = 50;
var baseCircle = new Circle(150,150,50);
var nCircles = 7;
var startAngle = 15.0;
function Circle(x,y,r) {
this.x = x;
this.y = y;
this.r = r;
}
Circle.prototype.draw = function() {
ctx.beginPath();
ctx.arc(this.x,this.y,this.r, 0, 2 * Math.PI, false);
ctx.stroke();
}
var circles = new Array();
function calcCircles(n) {
circles.splice(0); // clear out old circles
var angle = Math.PI / n;
var s = Math.sin(angle);
var r = baseRadius * s / (1-s);
console.log(angle);
console.log(s);
console.log(r);
for(var i=0;i<n;++i) {
var phi = startAngle + angle * i * 2;
var cx = 150+(baseRadius + r) * Math.cos(phi);
var cy = 150+(baseRadius + r) * Math.sin(phi);
circles.push(new Circle(cx,cy,r));
}
}
function draw() {
baseCircle.draw();
circles.forEach(function(ele){ele.draw()});
}
calcCircles(7);
draw();

Categories

Resources