RGB Sliders wiggle when changing value - javascript

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]));
}

Related

Pixi.js best way to create a dragable and clickable handle that rotates around a point

I have 8 handle graphics that represent 5 different states (closed, flow rate 1, flow rate 2, flow rate 3, flow rate 4). Handle graphics 6,7, and 8 also represent flow rate 1, 2, and 3. The images depict a buret handle that rotates around a center point. For each handle state, I need to show the matching texture. I need the user to be able to drag the handle and have it move through the different graphics as the mouse moves around the center point. I also need the user to be able to click on the right side to increase the flow rate and click on the left side to decrease the flow rate.
I have looking into using getBounds() from the image and using that as a hit box but that seems like it won't work because i am removing the old texture and adding a new one depending on the mouse position when dragging. not to mention the images all have similar dimensions.
I have also though about creating 16 hit boxes (2 for each of the 8 images, 1 on the left side for decreasing flow rate, one on the right side for increasing flow rate) and adding and removing the hit boxes with the texture but this seems overly tedious and i don't think it will work with dragging.
Let me know if you have any ideas!
Thanks
Drag a rotating switch
Assuming you get a mouse coord that is relative to the valve eg mouse event pageX, pageY properties.
You can create a function that takes the element, number valve steps, and mouse coords and spits out the values you want.
function getValueSetting(x, y, valveSteps, valveElement) {
const bounds = valveElement.getBoundingClientRect();
const centerX = (bounds.left + bounds.right) / 2;
const centerY = (bounds.top + bounds.bottom) / 2;
const left = x < centerX;
const distance = Math.hypot(x - centerX, y - centerY);
const pos = (Math.atan2(y - centerY, x - centerX) + Math.PI) / (Math.PI * 2);
return {
left,
right: !left,
distance,
pos: Math.round(pos * valveSteps - (valveSteps / 4)),
};
}
If the valve positions step by 1 hour on the clock make valveSteps = 12
Call the function const valveState = getValueSetting(mouseEvent.pageX, mouseEvent.pageY, 12, valveElment);
The object returned will have bools for left and right of the center, and pos will be one of 12 positions starting at 12 o'clock pos = 0 to 11 o'clock pos === 11. The distance property is the distance from the valve.
In the function the angle position subtracts (valveSteps / 4) because Math.atan2 return 0 at the 3 o'clock mark. The subtract (valveSteps / 4) rotate back 1 quarter turn to set 0 at 12 o'clock.
Example
The example draws 5 valve positions.
Move the mouse over the valve handle (red) and the cursor will change to a pointer. Click and drag the mouse to turn the valve. Once dragging the mouse will hold the valve until you release the button.
If not over the handle, but near the valve clicks left and right will message appropriate message.
const size = 64; // size of image
const valveSteps = 12; // total number of angle steps
const valveStep = (Math.PI * 2) / valveSteps; // angle steps in radians
const startAngle = -valveStep * 2; // visual start angle of handle
const valveStart = 1; // starting pos of valve
setTimeout(() => {
const valves = [
createValve(64, startAngle),
createValve(64, startAngle + valveStep),
createValve(64, startAngle + valveStep * 2),
createValve(64, startAngle + valveStep * 3),
createValve(64, startAngle + valveStep * 4),
];
setValve(valves[0]);
var dragging = false;
var currentPos = 0;
var level = 0;
mouse.onupdate = () => {
const valveSetting = getValueSetting(mouse.x, mouse.y, valveSteps, valveA);
if (valveSetting.distance < size && valveSetting.pos - valveStart === currentPos) {
document.body.style.cursor = "pointer";
} else {
document.body.style.cursor = "default";
}
if (mouse.button && (valveSetting.distance < size || dragging)) {
if (valveSetting.distance < size / 2 && valveSetting.pos - valveStart === currentPos) {
if (valveSetting.pos >= valveStart && valveSetting.pos < valveStart + valves.length) {
dragging = true;
}
}
console.clear()
if (dragging) {
let pos = valveSetting.pos - valveStart;
pos = pos < 0 ? 0 : pos > valves.length - 1 ? valves.length - 1 : pos
setValve(valves[pos]);
currentPos = pos;
console.log("Valve pos: " + pos);
} else if (valveSetting.left) {
level --;
console.log("Turn down " + level);
mouse.button = false;
} else if (valveSetting.right) {
level ++;
console.log("Turn up " + level);
mouse.button = false;
}
} else {
dragging = false;
}
}
},0);
function setValve(image) {
valveA.innerHTML = "";
$$(valveA, image); // appends image to element valveA
}
function getValueSetting(x, y, valveSteps, valveElement) {
const bounds = valveElement.getBoundingClientRect();
const centerX = (bounds.left + bounds.right) / 2;
const centerY = (bounds.top + bounds.bottom) / 2;
const left = x < centerX;
const distance = Math.hypot(x - centerX, y - centerY);
const pos = (Math.atan2(y - centerY, x - centerX) + Math.PI) / (Math.PI * 2);
return {
left,
right: !left,
distance,
pos: Math.round(pos * valveSteps - (valveSteps / 4)),
};
}
function createValve(size, angle) {
const canvas = $("canvas", {width: size, height: size});
const ctx = canvas.getContext("2d");
const r = size * 0.4;
const c = size / 2;
ctx.strokeStyle = "red";
ctx.lineCap = "round";
ctx.lineWidth = 8;
ctx.beginPath();
ctx.lineTo(Math.cos(angle) * r + c, Math.sin(angle) * r + c);
ctx.lineTo(-Math.cos(angle) * r * 0.2 + c, -Math.sin(angle) * r * 0.2 + c);
ctx.stroke();
ctx.beginPath();
ctx.arc(c, c, 8, 0, Math.PI * 2);
ctx.strokeStyle = "black";
ctx.lineWidth = 2;
ctx.stroke();
return canvas;
}
// Boiler plate
const $ = (tag, props = {}) => Object.assign(document.createElement(tag), props);
const $$ = (p, ...sibs) => sibs.reduce((p,sib) => (p.appendChild(sib), p), p);
const mouse = {x : 0, y : 0, button : false}
function mouseEvents(e){
mouse.x = e.pageX;
mouse.y = e.pageY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
mouse.onupdate && mouse.onupdate();
}
["down","up","move"].forEach(name => document.addEventListener("mouse" + name,mouseEvents));
.valveContainer {
position: absolute;
top: 30px;
left 30px;
border: 2px solid white;
}
<div id="valveA" class="valveContainer"></div>

Get bounds of unrotated rotated rectangle

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>

Position div at exact cursors location

I am using this color wheel picker, and I'm trying to add a div as the dragger instead of having it embedded in the canvas. I got it working thanks to these answers.
The problem is, the dragger is a bit off from the cursor. The obvious solution would be to just subtract from the draggers left and top position. Like this:
dragger.style.left = (currentX + radiusPlusOffset - 13) + 'px';
dragger.style.top = (currentY + radiusPlusOffset - 13) + 'px';
Another problem comes up when I subtract 13. If you drag the dragger all the way to the right or bottom, it doesn't go all the way. If you drag it all the way to the left or top, it goes passed the canvas's border.
Basically what I'm trying to achieve, is to have the dragger at the cursor pointers exact location, and the draggable shouldn't go passed the canvas's border. How can I achieve that?
JSFiddle
var b = document.body;
var c = document.getElementsByTagName('canvas')[0];
var a = c.getContext('2d');
var wrapper = document.getElementById('wrapper');
var dragger = document.createElement('div');
dragger.id = 'dragger';
wrapper.appendChild(dragger);
wrapper.insertBefore(dragger, c);
document.body.clientWidth; // fix bug in webkit: http://qfox.nl/weblog/218
(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.c = doc.createElement;
b.a = b.appendChild;
var width = c.width = c.height = 400,
label = b.a(doc.c("p")),
input = b.a(doc.c("input")),
imageData = a.createImageData(width, width),
pixels = imageData.data,
oneHundred = input.value = input.max = 100,
circleOffset = 0,
diameter = width - circleOffset * 2,
radius = diameter / 2,
radiusPlusOffset = radius + circleOffset,
radiusSquared = radius * radius,
two55 = 255,
currentY = oneHundred,
currentX = -currentY,
wheelPixel = circleOffset * 4 * width + circleOffset * 4;
// Math helpers
var math = Math,
PI = math.PI,
PI2 = PI * 2,
sqrt = math.sqrt,
atan2 = math.atan2;
// Setup DOM properties
b.style.textAlign = "center";
label.style.font = "2em courier";
input.type = "range";
// 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 = 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;
}
}
a.putImageData(imageData, 0, 0);
// Bind Event Handlers
input.onchange = redraw;
dragger.onmousedown = c.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);
}
// 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 - c.offsetLeft - radiusPlusOffset || currentX;
currentY = e.pageY - c.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;
}
label.textContent = b.style.background = hsvToRgb(
(theta + PI) / PI2, // Current hue (how many degrees along the circle)
sqrt(d) / radius, // Current saturation (how close to the middle)
input.value / oneHundred // Current value (input type="range" slider value)
)[3];
dragger.style.left = (~~currentX + radiusPlusOffset - 13) + 'px';
dragger.style.top = (~~currentY + radiusPlusOffset - 13) + 'px';
// Reset to color wheel and draw a spot on the current location.
// 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 = 'white';
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 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(" + ~~r + "," + ~~g + "," + ~~b + ")"];
}
// Kick everything off
redraw(0);
/*
// Just an idea I had to kick everything off with some changing colors…
// Probably no way to squeeze this into 1k, but it could probably be a lot smaller than this:
currentX = currentY = 1;
var interval = setInterval(function() {
currentX--;
currentY*=1.05;
redraw(0)
}, 7);
setTimeout(function() {
clearInterval(interval)
}, 700)
*/
})();
#c {
border: 7px solid black;
border-radius: 50%;
}
#wrapper {
width: 400px;
height: 400px;
position: relative;
cursor: pointer;
}
#wrapper:active {
//cursor: none;
}
#dragger {
width: 8px;
height: 8px;
border-radius: 50%;
display: block;
position: absolute;
border: 2px solid black;
}
<div id='wrapper'>
<canvas id="c"></canvas>
</div>
You're just subtracting in the wrong place.
Instead of subtracting from the elements position, subtract directly from the mouse pointers position.
This code actually moves the element, offsetting it relative to the pointer, and making it appear to be outside the borders of the color picker
dragger.style.left = (~~currentX + radiusPlusOffset - 13) + 'px';
dragger.style.top = (~~currentY + radiusPlusOffset - 13) + 'px';
... which is not what you really want, you want the calculated numbers for the pointer to be exactly center of the dragger element, so you should extract from the pointers position instead, that way the limits of the dragger isn't affected, and it stays within the borders of the color picker
currentX = e.pageX - c.offsetLeft - radiusPlusOffset -13 || currentX;
currentY = e.pageY - c.offsetTop - radiusPlusOffset -13 || currentY;
FIDDLE

Creating the Butterfly curve with arrays

I have two questions, the first being how do I access the indexes within my array separately, because my console.log of [n][0] results in two values - x and y. Secondly, for the butterfly curve, https://en.wikipedia.org/wiki/Butterfly_curve_%28transcendental%29, how would I determine the values of t? and reiterate through a certain minimum and maximum. In need of logic support.
Here's my progress so far.
/*function drawButterFly(n){
c.beginPath();
console.log(n[2])
for (var i = 0; i < n.length; i++){
if (i === 0) {
c.moveTo();
} else {
c.lineTo();
}
c.stroke();
}
}*/
function butterFly() {
var r = 5;
var N = 3;
var value = [];
for (var a = 0.2; a < 2*Math.PI; a = a + 0.1){
value.push(a);
}
var t = value[Math.floor(Math.random()*value.length)];
var cos = r*Math.cos(t)*( (Math.exp(Math.cos(t))) - (2*Math.cos(4*t)) - (Math.sin(t/12)^5) );
var sin = r*Math.sin(t)*( (Math.exp(Math.cos(t))) - (2*Math.cos(4*t)) - (Math.sin(t/12)^5) );
var n = [];
for (var u = 0; u < N; u++){
var x = sin * -u;
var y = cos * -u;
n.push([x,y]);
}
drawButterFly(n);
}
Since you're pushing an array here: n.push([x,y]) you can access the x component of the first element with n[0][0] and the y component of the same element with n[0][1]
Example:
var n = [];
n.push( ["x", "y"] );
console.log( n[0][0] );
console.log( n[0][1] );
As for the useful values of t - in the image you've shown, you'll notice that the same butterfly is drawn several times at different sizes. To draw a complete butterfly, you need to use the range for t of [0..2pi]. If you want to draw two butterflies, you need to use the range [0..4pi]. That is it's cyclic over the same period that a circle is. Unlike a circle however, each cycle doesn't draw over the previous one.
Here's a quick and nasty example:
function byId(id) {
return document.getElementById(id);
}
window.addEventListener('load', onDocLoaded, false);
function onDocLoaded(evt) {
butterFly();
}
function butterFly() {
var pointArray = [];
var stepSize = 0.05; // ~125 steps for every 360°
var upperLimit = 4 * Math.PI;
var scale = 20;
for (var t = 0.0; t < upperLimit; t += stepSize) {
var xVal = Math.sin(t) * ((Math.exp(Math.cos(t))) - (2 * Math.cos(4 * t)) - (Math.pow(Math.sin(t / 12), 5)));
var yVal = Math.cos(t) * ((Math.exp(Math.cos(t))) - (2 * Math.cos(4 * t)) - (Math.pow(Math.sin(t / 12), 5)));
pointArray.push([scale * xVal, -scale * yVal]); // -1 value since screen-y direction is opposite direction to cartesian coords y
}
drawButterFly(pointArray);
}
function drawButterFly(pointArray) {
var can = byId('myCan');
var ctx = can.getContext('2d');
var originX, originY;
originX = can.width / 2;
originY = can.height / 2;
ctx.beginPath();
for (var i = 0; i < pointArray.length; i++) {
if (i === 0) {
ctx.moveTo(originX + pointArray[i][0], originX + pointArray[i][1]);
} else {
ctx.lineTo(originX + pointArray[i][0], originY + pointArray[i][1]);
}
}
ctx.closePath();
ctx.stroke();
}
canvas {
border: solid 1px red;
}
<canvas id='myCan' width='256' height='256' />
If I'm not mistaken, the Butterfly curve is given as a pair of parametric equations, meaning you increment t to get the next (x, y) points on your curve. In other words, your t is what you should be using in place of u in your code, and the range of values for t should be 0 .. 24*pi as that's the range in which sin(t / 12) has its unique values).
Here's a version that demonstrates the drawing of the curve to a canvas:
function getPoint(t, S, O) {
var cos_t = Math.cos(t);
var factor = Math.exp(cos_t) - 2 * Math.cos(4*t) - Math.pow(Math.sin(t/12), 5);
return {
x: S * Math.sin(t) * factor + O.x,
y: S * cos_t * factor + O.y
};
}
var canvas = document.getElementById("c");
canvas.width = 300;
canvas.height = 300;
var ctx = canvas.getContext("2d");
// First path
ctx.beginPath();
ctx.strokeStyle = 'blue';
var offset = {x:150, y:120};
var scale = 40;
var maxT = 24 * Math.PI;
var p = getPoint(0, scale, offset);
ctx.moveTo(p.x, canvas.height - p.y);
for (var t = 0.01; t <= maxT; t += 0.01) {
p = getPoint(t, scale, offset);
ctx.lineTo(p.x, canvas.height - p.y);
}
ctx.stroke();
#c {
border: solid 1px black;
}
<canvas id="c"></canvas>
One thing to note: canvases have y = 0 start at the top, so you need to inverse your y (i.e. canvas.height - y) to have your curve orient correctly.
UPDATE: Added animated version
As requested by royhowie, here's an animated version, using requestAnimationFrame:
function getPoint(t, S, O) {
var cos_t = Math.cos(t);
var factor = Math.exp(cos_t) - 2 * Math.cos(4*t) - Math.pow(Math.sin(t/12), 5);
return {
x: S * Math.sin(t) * factor + O.x,
y: S * cos_t * factor + O.y
};
}
var canvas = document.getElementById("c");
canvas.width = 300;
canvas.height = 300;
var ctx = canvas.getContext("2d");
var offset = {x:150, y:120};
var scale = 40;
var maxT = 24 * Math.PI;
var animationID;
var started = false;
var t = 0;
document.getElementById('start').addEventListener('click', function(e) {
e.preventDefault();
if (!started) {
animationID = requestAnimationFrame(animate);
started = true;
}
});
document.getElementById('pause').addEventListener('click', function(e) {
e.preventDefault();
if (started) {
cancelAnimationFrame(animationID);
started = false;
}
});
function animate() {
animationID = requestAnimationFrame(animate);
var p = getPoint(t, scale, offset);
if (t === 0) {
ctx.beginPath();
ctx.strokeStyle = 'blue';
ctx.moveTo(p.x, canvas.height - p.y);
t += 0.01;
} else if (t < maxT) {
ctx.lineTo(p.x, canvas.height - p.y);
ctx.stroke();
t += 0.01;
} else {
cancelAnimationFrame(animationID);
}
}
#c {
border: solid 1px black;
}
<div>
<button id="start">Start</button>
<button id="pause">Pause</button>
</div>
<canvas id="c"></canvas>
Question 1
For an arbitrary integer i, let n[i] = [xi, yi]. xi can then be accessed via n[i][0] and yi via n[i][1]
Question 2
For values of t, I am sure you're gonna want to use sub-integer values, so I recommend using a constant increment value representing the "resolution" of your graph.
Let's call it dt. Also, I'd advise changing your variable names from single letters to something more descriptive, like min_t and max_t, and instead of n I'm going to call your array points.
function drawButterFly(points){
for (var i = 0, n = points.count; i < n; ++i) {
var x = points[i][0];
var y = points[i][1];
...
}
}
function butterFly(min_t, max_t, dt, r) {
var points = [];
for (var t = min_t; t < max_t; t+=dt){
var x = r*Math.sin(t)*...
var y = r*Math.cos(t)*...
points.push([x,y]);
}
drawButterFly(points, dt);
}
I'm not sure what the other loop inside of that function was for, but if you need it, you can adapt from the pattern above.
Usage example: butterFly(0, 10, 0.01, 3) -> t goes from 0 to 10 with an increment of 0.01, and r=3
Regarding your first question it's a better option to replace the multidimensional array containing the x and y coordinates with an object. Then when iterating over the array you can check for the object values.
So instead of:
n.push([x,y]);
you should do:
m.push({
'xPos' : x,
'yPos' : y
})
Later you can access this by m.xPos or m.yPos
Then you can access the x and y values by object literal names.
Regarding the second question: for a good pseudo code implementation of butterfly curves you might check Paul Burke site: http://paulbourke.net/geometry/butterfly/. So t in your case is:
t = i * 24.0 * PI / N;
As you see t is a parametric value which got incremented on each step when iterating over the array.

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