Related
So I've wrote a microbe animation.
It's all cool, but I think that it would be even better, if the microbe would be able to eat diatoms, and to destroy bubbles.
The issue is that the microbe is made of bezier curves.
I have no idea how to check collision between object made of bezier curves, and a circle in a reasonable way.
The only thing that comes to my mind, is to paint the microbe shape and bubbles a hidden canvas, and then check if they paint to the same pixels. But that would cause big performance issues IMHO.
Code: https://codepen.io/michaelKurowski/pen/opWeKY
class Cell is the cell, while class CellWallNode is a node of bezier curve, in case if somebody needs to look up the implementation.
The bubbles and diatoms can be easily simplified to circles.
Solution to bounds testing object defined by beziers
Below is an example solution to finding if a circle is inside an object defined by a center point and a set of beziers defining the perimeter.
The solution has only been tested for non intersecting cubic beziers. Also will not work if there are more than two intercepts between the object being tested and the center of the cell. However all you need to solve for the more complex bounds is there in the code.
The method
Define a center point to test from as a 2D point
Define the test point as a 2D point
Define a line from the center to the test point
For each bezier
Translate bezier so first point is at start of line
Rotate the bezier such that the line is aligned to the x axis
Solve the bezier polynomials to find the roots (location of x axis intercepts)
Use the roots to find position on bezier curve of line intercept.
Use the closest intercept to the point to find distance from center to perimeter.
If perimeter distance is greater than test point distance plus radius then inside.
Notes
The test is to a point along a line to the center not to a circle which would be a area defined by a triangle. As long as the circle radius is small compared to the size of the beziers the approximation works well.
Not sure if you are using cubic or quadratic beziers so the solution covers both cubic and quadratic beziers.
Example
The snippet creates a set of beziers (cubic) around a center point. the object theBlob holds the animated beziers. The function testBlob tests the mouse position and returns true if inside theBlob. The object bezHelper contains all the functionality needed to solve the problem.
The cubic root solver was derived from github intersections cube root solver.
const bezHelper = (()=>{
// creates a 2D point
const P2 = (x=0, y= x === 0 ? 0 : x.y + (x = x.x, 0)) => ({x, y});
const setP2As = (p,pFrom) => (p.x = pFrom.x, p.y = pFrom.y, p);
// To prevent heap thrashing close over some pre defined 2D points
const v1 = P2();
const v2 = P2();
const v3 = P2();
const v4 = P2();
var u,u1,u2;
// solves quadratic for bezier 2 returns first root
function solveBezier2(A, B, C){
// solve the 2nd order bezier equation.
// There can be 2 roots, u,u1 hold the results;
// 2nd order function a+2(-a+b)x+(a-2b+c)x^2
a = (A - 2 * B + C);
b = 2 * ( - A + B);
c = A;
a1 = 2 * a;
c = b * b - 4 * a * c;
if(c < 0){
u = Infinity;
u1 = Infinity;
return u;
}else{
b1 = Math.sqrt(c);
}
u = (-b + b1) / a1;
u1 = (-b - b1) / a1;
return u;
}
// solves cubic for bezier 3 returns first root
function solveBezier3(A, B, C, D){
// There can be 3 roots, u,u1,u2 hold the results;
// Solves 3rd order a+(-2a+3b)t+(2a-6b+3c)t^2+(-a+3b-3c+d)t^3 Cardano method for finding roots
// this function was derived from http://pomax.github.io/bezierinfo/#intersections cube root solver
// Also see https://en.wikipedia.org/wiki/Cubic_function#Cardano.27s_method
function crt(v) {
if(v<0) return -Math.pow(-v,1/3);
return Math.pow(v,1/3);
}
function sqrt(v) {
if(v<0) return -Math.sqrt(-v);
return Math.sqrt(v);
}
var a, b, c, d, p, p3, q, q2, discriminant, U, v1, r, t, mp3, cosphi,phi, t1, sd;
u2 = u1 = u = -Infinity;
d = (-A + 3 * B - 3 * C + D);
a = (3 * A - 6 * B + 3 * C) / d;
b = (-3 * A + 3 * B) / d;
c = A / d;
p = (3 * b - a * a) / 3;
p3 = p / 3;
q = (2 * a * a * a - 9 * a * b + 27 * c) / 27;
q2 = q / 2;
a /= 3;
discriminant = q2 * q2 + p3 * p3 * p3;
if (discriminant < 0) {
mp3 = -p / 3;
r = sqrt(mp3 * mp3 * mp3);
t = -q / (2 * r);
cosphi = t < -1 ? -1 : t > 1 ? 1 : t;
phi = Math.acos(cosphi);
t1 = 2 * crt(r);
u = t1 * Math.cos(phi / 3) - a;
u1 = t1 * Math.cos((phi + 2 * Math.PI) / 3) - a;
u2 = t1 * Math.cos((phi + 4 * Math.PI) / 3) - a;
return u;
}
if(discriminant === 0) {
U = q2 < 0 ? crt(-q2) : -crt(q2);
u = 2 * U - a;
u1 = -U - a;
return u;
}
sd = sqrt(discriminant);
u = crt(sd - q2) - crt(sd + q2) - a;
return u;
}
// get a point on the bezier at pos ( from 0 to 1 values outside this range will be outside the bezier)
// p1, p2 are end points and cp1, cp2 are control points.
// ret is the resulting point. If given it is set to the result, if not given a new point is created
function getPositionOnBez(pos,p1,p2,cp1,cp2,ret = P2()){
if(pos === 0){
ret.x = p1.x;
ret.y = p1.y;
return ret;
}else
if(pos === 1){
ret.x = p2.x;
ret.y = p2.y;
return ret;
}
v1.x = p1.x;
v1.y = p1.y;
var c = pos;
if(cp2 === undefined){
v2.x = cp1.x;
v2.y = cp1.y;
v1.x += (v2.x - v1.x) * c;
v1.y += (v2.y - v1.y) * c;
v2.x += (p2.x - v2.x) * c;
v2.y += (p2.y - v2.y) * c;
ret.x = v1.x + (v2.x - v1.x) * c;
ret.y = v1.y + (v2.y - v1.y) * c;
return ret;
}
v2.x = cp1.x;
v2.y = cp1.y;
v3.x = cp2.x;
v3.y = cp2.y;
v1.x += (v2.x - v1.x) * c;
v1.y += (v2.y - v1.y) * c;
v2.x += (v3.x - v2.x) * c;
v2.y += (v3.y - v2.y) * c;
v3.x += (p2.x - v3.x) * c;
v3.y += (p2.y - v3.y) * c;
v1.x += (v2.x - v1.x) * c;
v1.y += (v2.y - v1.y) * c;
v2.x += (v3.x - v2.x) * c;
v2.y += (v3.y - v2.y) * c;
ret.x = v1.x + (v2.x - v1.x) * c;
ret.y = v1.y + (v2.y - v1.y) * c;
return ret;
}
const cubicBez = 0;
const quadraticBez = 1;
const none = 2;
var type = none;
// working bezier
const p1 = P2();
const p2 = P2();
const cp1 = P2();
const cp2 = P2();
// rotated bezier
const rp1 = P2();
const rp2 = P2();
const rcp1 = P2();
const rcp2 = P2();
// translate and rotate bezier
function transformBez(pos,rot){
const ax = Math.cos(rot);
const ay = Math.sin(rot);
var x = p1.x - pos.x;
var y = p1.y - pos.y;
rp1.x = x * ax - y * ay;
rp1.y = x * ay + y * ax;
x = p2.x - pos.x;
y = p2.y - pos.y;
rp2.x = x * ax - y * ay;
rp2.y = x * ay + y * ax;
x = cp1.x - pos.x;
y = cp1.y - pos.y;
rcp1.x = x * ax - y * ay;
rcp1.y = x * ay + y * ax;
if(type === cubicBez){
x = cp2.x - pos.x;
y = cp2.y - pos.y;
rcp2.x = x * ax - y * ay;
rcp2.y = x * ay + y * ax;
}
}
function getPosition2(pos,ret){
return getPositionOnBez(pos,p1,p2,cp1,undefined,ret);
}
function getPosition3(pos,ret){
return getPositionOnBez(pos,p1,p2,cp1,cp2,ret);
}
const API = {
getPosOnQBez(pos,p1,cp1,p2,ret){
return getPositionOnBez(pos,p1,p2,cp1,undefined,ret);
},
getPosOnCBez(pos,p1,cp1,cp2,p2,ret){
return getPositionOnBez(pos,p1,p2,cp1,cp2,ret);
},
set bezQ(points){
setP2As(p1, points[0]);
setP2As(cp1, points[1]);
setP2As(p2, points[2]);
type = quadraticBez;
},
set bezC(points){
setP2As(p1, points[0]);
setP2As(cp1, points[1]);
setP2As(cp2, points[2]);
setP2As(p2, points[3]);
type = cubicBez;
},
isInside(center, testPoint, pointRadius){
drawLine(testPoint , center);
v1.x = (testPoint.x - center.x);
v1.y = (testPoint.y - center.y);
const pointDist = Math.sqrt(v1.x * v1.x + v1.y * v1.y)
const dir = -Math.atan2(v1.y,v1.x);
transformBez(center,dir);
if(type === cubicBez){
solveBezier3(rp1.y, rcp1.y, rcp2.y, rp2.y);
if (u < 0 || u > 1) { u = u1 }
if (u < 0 || u > 1) { u = u2 }
if (u < 0 || u > 1) { return }
getPosition3(u, v4);
}else{
solveBezier2(rp1.y, rcp1.y, rp2.y);
if (u < 0 || u > 1) { u = u1 }
if (u < 0 || u > 1) { return }
getPosition2(u, v4);
}
drawCircle(v4);
const dist = Math.sqrt((v4.x - center.x) ** 2 + (v4.y - center.y) ** 2);
const dist1 = Math.sqrt((v4.x - testPoint.x) ** 2 + (v4.y - testPoint.y) ** 2);
return dist1 < dist && dist > pointDist - pointRadius;
}
}
return API;
})();
const ctx = canvas.getContext("2d");
const m = {x : 0, y : 0};
document.addEventListener("mousemove",e=>{
var b = canvas.getBoundingClientRect();
m.x = e.pageX - b.left - scrollX - 2;
m.y = e.pageY - b.top - scrollY - 2;
});
function drawCircle(p,r = 5,col = "black"){
ctx.beginPath();
ctx.strokeStyle = col;
ctx.arc(p.x,p.y,r,0,Math.PI*2)
ctx.stroke();
}
function drawLine(p1,p2,r = 5,col = "black"){
ctx.beginPath();
ctx.strokeStyle = col;
ctx.lineTo(p1.x,p1.y);
ctx.lineTo(p2.x,p2.y);
ctx.stroke();
}
const w = 400;
const h = 400;
const diag = Math.sqrt(w * w + h * h);
// creates a 2D point
const P2 = (x=0, y= x === 0 ? 0 : x.y + (x = x.x, 0)) => ({x, y});
const setP2As = (p,pFrom) => (p.x = pFrom.x, p.y = pFrom.y, p);
// random int and double
const randI = (min, max = min + (min = 0)) => (Math.random()*(max - min) + min) | 0;
const rand = (min = 1, max = min + (min = 0)) => Math.random() * (max - min) + min;
const theBlobSet = [];
const theBlob = [];
function createCubicBlob(segs){
const step = Math.PI / segs;
for(var i = 0; i < Math.PI * 2; i += step){
const dist = rand(diag * (1/6), diag * (1/5));
const ang = i + rand(-step * 0.2,step * 0.2);
const p = P2(
w / 2 + Math.cos(ang) * dist,
h / 2 + Math.sin(ang) * dist
);
theBlobSet.push(p);
theBlob.push(P2(p));
}
theBlobSet[theBlobSet.length -1] = theBlobSet[0];
theBlob[theBlobSet.length -1] = theBlob[0];
}
createCubicBlob(8);
function animateTheBlob(time){
for(var i = 0; i < theBlobSet.length-1; i++){
const ang = Math.sin(time + i) * 6;
theBlob[i].x = theBlobSet[i].x + Math.cos(ang) * diag * 0.04;
theBlob[i].y = theBlobSet[i].y + Math.sin(ang) * diag * 0.04;
}
}
function drawTheBlob(){
ctx.strokeStyle = "black";
ctx.lineWidth = 3;
ctx.beginPath();
var i = 0;
ctx.moveTo(theBlob[i].x,theBlob[i++].y);
while(i < theBlob.length){
ctx.bezierCurveTo(
theBlob[i].x,theBlob[i++].y,
theBlob[i].x,theBlob[i++].y,
theBlob[i].x,theBlob[i++].y
);
}
ctx.stroke();
}
var center = P2(w/2,h/2);
function testBlob(){
var i = 0;
while(i < theBlob.length-3){
bezHelper.bezC = [theBlob[i++], theBlob[i++], theBlob[i++], theBlob[i]];
if(bezHelper.isInside(center,m,6)){
return true;
}
}
return false;
}
// main update function
function update(timer){
ctx.clearRect(0,0,w,h);
animateTheBlob(timer/1000)
drawTheBlob();
if(testBlob()){
ctx.strokeStyle = "red";
}else{
ctx.strokeStyle = "black";
}
ctx.beginPath();
ctx.arc(m.x,m.y,5,0,Math.PI*2)
ctx.stroke();
requestAnimationFrame(update);
}
requestAnimationFrame(update);
canvas { border : 2px solid black; }
<canvas id="canvas" width = "400" height = "400"></canvas>
I had created an animation of bubbles in which al the circle will expand which are 50px neer to the mouse.
so here is the trick. you can just simply change mouseX,mouseY with your microbe's X and Y coordinates and 50 to the radius of your microbe.
And when my bubbles get bigger, so there you can destroy you air bubbles.
here is the link to my Animation.
https://ankittorenzo.github.io/canvasAnimations/Elements/Bubbles/
here is the link to my GitHub Code.
https://github.com/AnkitTorenzo/canvasAnimations/blob/master/Elements/Bubbles/js/main.js
Let Me Know if you have any problem.
I'm trying to generate an elliptical arc by approximating a bezier curve as in the post https://mortoray.com/2017/02/16/rendering-an-svg-elliptical-arc-as-bezier-curves/
However my implementation doesn't seem to fetch the right result. (Red line is SVG and black line is canvas path)
This is my code
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
// M100,350
// a45,35 -30 0,1 50,-25
canvas.width = document.body.clientWidth;
canvas.height = document.body.clientHeight;
ctx.strokeWidth = 2;
ctx.strokeStyle = "#000000";
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max)
}
function svgAngle(ux, uy, vx, vy ) {
var dot = ux*vx + uy*vy;
var len = Math.sqrt(ux*ux + uy*uy) * Math.sqrt(vx*vx + vy*vy);
var ang = Math.acos( clamp(dot / len,-1,1) );
if ( (ux*vy - uy*vx) < 0)
ang = -ang;
return ang;
}
function generateBezierPoints(rx, ry, phi, flagA, flagS, x1, y1, x2, y2) {
var rX = Math.abs(rx);
var rY = Math.abs(ry);
var dx2 = (x1 - x2)/2;
var dy2 = (y1 - y2)/2;
var x1p = Math.cos(phi)*dx2 + Math.sin(phi)*dy2;
var y1p = -Math.sin(phi)*dx2 + Math.cos(phi)*dy2;
var rxs = rX * rX;
var rys = rY * rY;
var x1ps = x1p * x1p;
var y1ps = y1p * y1p;
var cr = x1ps/rxs + y1ps/rys;
if (cr > 1) {
var s = Math.sqrt(cr);
rX = s * rX;
rY = s * rY;
rxs = rX * rX;
rys = rY * rY;
}
var dq = (rxs * y1ps + rys * x1ps);
var pq = (rxs*rys - dq) / dq;
var q = Math.sqrt( Math.max(0,pq) );
if (flagA === flagS)
q = -q;
var cxp = q * rX * y1p / rY;
var cyp = - q * rY * x1p / rX;
var cx = Math.cos(phi)*cxp - Math.sin(phi)*cyp + (x1 + x2)/2;
var cy = Math.sin(phi)*cxp + Math.cos(phi)*cyp + (y1 + y2)/2;
var theta = svgAngle( 1,0, (x1p-cxp) / rX, (y1p - cyp)/rY );
var delta = svgAngle(
(x1p - cxp)/rX, (y1p - cyp)/rY,
(-x1p - cxp)/rX, (-y1p-cyp)/rY);
delta = delta - Math.PI * 2 * Math.floor(delta / (Math.PI * 2));
if (!flagS)
delta -= 2 * Math.PI;
var n1 = theta, n2 = delta;
// E(n)
// cx +acosθcosη−bsinθsinη
// cy +asinθcosη+bcosθsinη
function E(n) {
var enx = cx + rx * Math.cos(phi) * Math.cos(n) - ry * Math.sin(phi) * Math.sin(n);
var eny = cy + rx * Math.sin(phi) * Math.cos(n) + ry * Math.cos(phi) * Math.sin(n);
return {x: enx,y: eny};
}
// E'(n)
// −acosθsinη−bsinθcosη
// −asinθsinη+bcosθcosη
function Ed(n) {
var ednx = -1 * rx * Math.cos(phi) * Math.sin(n) - ry * Math.sin(phi) * Math.cos(n);
var edny = -1 * rx * Math.sin(phi) * Math.sin(n) + ry * Math.cos(phi) * Math.cos(n);
return {x: ednx, y: edny};
}
var en1 = E(n1);
var en2 = E(n2);
var edn1 = Ed(n1);
var edn2 = Ed(n2);
var alpha = Math.sin(n2 - n1) * (Math.sqrt(4 + 3 * Math.pow(Math.tan((n2 - n1)/2), 2)) - 1)/3;
console.log(en1, en2);
return {
cpx1: en1.x + alpha*edn1.x,
cpy1: en1.y + alpha*edn1.y,
cpx2: en2.x - alpha*edn2.x,
cpy2: en2.y - alpha*edn2.y
};
}
// M100,100
ctx.moveTo(100,100)
// a45,35 -30 0,1 50,-25
cp = generateBezierPoints(
45,35, // Radii
-30 * Math.PI / 180, // xAngle
0, // Large arc flag
1, // Sweep flag
100,100, // Endpoint1
100 + 50, 100 - 25 // Endpoint2
);
ctx.bezierCurveTo(cp.cpx1,cp.cpy1,cp.cpx2,cp.cpy2,150,75);
ctx.stroke()
I need help with understanding where I'm going wrong
UPDATE:
I went through the post a couple more times and there is one part of the post that I don't quite understand which may also be lacking in my implementation.
All I had to do was subdivide the angle range into small sections to get a good approximation. I didn’t quite understand the paper’s error calculations, but I found another paper by Joe Cridge indicating divisions of π/2 provides a potential one pixel error on a fairly high resolution device. So I chose π/4 to ensure smooth animation, even for partial arcs on high density mobile devices.
I don't understand what the author means by subdividing the angles...
So apparently an elliptical arc cannot be approximated with a single bezier curve, so it takes multiple bezier curves by dividing the two angles into ranges.
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
// M100,350
// a45,35 -30 0,1 50,-25
canvas.width = document.body.clientWidth;
canvas.height = document.body.clientHeight;
ctx.strokeWidth = 2;
ctx.strokeStyle = "#000000";
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max)
}
function svgAngle(ux, uy, vx, vy ) {
var dot = ux*vx + uy*vy;
var len = Math.sqrt(ux*ux + uy*uy) * Math.sqrt(vx*vx + vy*vy);
var ang = Math.acos( clamp(dot / len,-1,1) );
if ( (ux*vy - uy*vx) < 0)
ang = -ang;
return ang;
}
function generateBezierPoints(rx, ry, phi, flagA, flagS, x1, y1, x2, y2) {
var rX = Math.abs(rx);
var rY = Math.abs(ry);
var dx2 = (x1 - x2)/2;
var dy2 = (y1 - y2)/2;
var x1p = Math.cos(phi)*dx2 + Math.sin(phi)*dy2;
var y1p = -Math.sin(phi)*dx2 + Math.cos(phi)*dy2;
var rxs = rX * rX;
var rys = rY * rY;
var x1ps = x1p * x1p;
var y1ps = y1p * y1p;
var cr = x1ps/rxs + y1ps/rys;
if (cr > 1) {
var s = Math.sqrt(cr);
rX = s * rX;
rY = s * rY;
rxs = rX * rX;
rys = rY * rY;
}
var dq = (rxs * y1ps + rys * x1ps);
var pq = (rxs*rys - dq) / dq;
var q = Math.sqrt( Math.max(0,pq) );
if (flagA === flagS)
q = -q;
var cxp = q * rX * y1p / rY;
var cyp = - q * rY * x1p / rX;
var cx = Math.cos(phi)*cxp - Math.sin(phi)*cyp + (x1 + x2)/2;
var cy = Math.sin(phi)*cxp + Math.cos(phi)*cyp + (y1 + y2)/2;
var theta = svgAngle( 1,0, (x1p-cxp) / rX, (y1p - cyp)/rY );
var delta = svgAngle(
(x1p - cxp)/rX, (y1p - cyp)/rY,
(-x1p - cxp)/rX, (-y1p-cyp)/rY);
delta = delta - Math.PI * 2 * Math.floor(delta / (Math.PI * 2));
if (!flagS)
delta -= 2 * Math.PI;
var n1 = theta, n2 = delta;
// E(n)
// cx +acosθcosη−bsinθsinη
// cy +asinθcosη+bcosθsinη
function E(n) {
var enx = cx + rx * Math.cos(phi) * Math.cos(n) - ry * Math.sin(phi) * Math.sin(n);
var eny = cy + rx * Math.sin(phi) * Math.cos(n) + ry * Math.cos(phi) * Math.sin(n);
return {x: enx,y: eny};
}
// E'(n)
// −acosθsinη−bsinθcosη
// −asinθsinη+bcosθcosη
function Ed(n) {
var ednx = -1 * rx * Math.cos(phi) * Math.sin(n) - ry * Math.sin(phi) * Math.cos(n);
var edny = -1 * rx * Math.sin(phi) * Math.sin(n) + ry * Math.cos(phi) * Math.cos(n);
return {x: ednx, y: edny};
}
var n = [];
n.push(n1);
var interval = Math.PI/4;
while(n[n.length - 1] + interval < n2)
n.push(n[n.length - 1] + interval)
n.push(n2);
function getCP(n1, n2) {
var en1 = E(n1);
var en2 = E(n2);
var edn1 = Ed(n1);
var edn2 = Ed(n2);
var alpha = Math.sin(n2 - n1) * (Math.sqrt(4 + 3 * Math.pow(Math.tan((n2 - n1)/2), 2)) - 1)/3;
console.log(en1, en2);
return {
cpx1: en1.x + alpha*edn1.x,
cpy1: en1.y + alpha*edn1.y,
cpx2: en2.x - alpha*edn2.x,
cpy2: en2.y - alpha*edn2.y,
en1: en1,
en2: en2
};
}
var cps = []
for(var i = 0; i < n.length - 1; i++) {
cps.push(getCP(n[i],n[i+1]));
}
return cps;
}
// M100,100
ctx.moveTo(100,100)
// a45,35 -30 0,1 50,-25
var rx = 45, ry=35,phi = -30 * Math.PI / 180, fa = 0, fs = 1, x = 100, y = 100, x1 = x + 50, y1 = y - 25;
var cps = generateBezierPoints(rx, ry, phi, fa, fs, x, y, x1, y1);
var limit = 2;
for(var i = 0; i < limit && i < cps.length; i++) {
ctx.bezierCurveTo(cps[i].cpx1, cps[i].cpy1,
cps[i].cpx2, cps[i].cpy2,
i < limit - 1 ? cps[i].en2.x : x1, i < limit - 1 ? cps[i].en2.y : y1);
}
ctx.stroke()
I need to detect collision circle with any line. I have array with verticles of polygon (x, y) and draw this polygon in loop. For detection I use algorithm, which calculate triangle height. Then I check if this height < 0, then circle collided with line.
The picture, that describe this method:
But I have unexpected result. My circle collide with transparent line (what?). I can't explain how it happens.
Demo at jsfiddle: https://jsfiddle.net/f458rdz6/1/
Function, which check the collisions and response it:
var p = polygonPoints;
for (var i = 0, n = p.length; i < n; i++) {
var start = i;
var end = (i + 1) % n;
var x0 = p[start].x;
var y0 = p[start].y;
var x1 = p[end].x;
var y1 = p[end].y;
// detection collision
var dx = x1 - x0;
var dy = y1 - y0;
var len = Math.sqrt(dx * dx + dy * dy);
var dist = (dx * (this.y - y0) - dy * (this.x - x0)) / len;
if (dist < this.radius) {
continue;
}
// calculate reflection, because collided
var wallAngle = Math.atan2(dy, dx);
var wallNormalX = Math.sin(wallAngle);
var wallNormalY = -Math.cos(wallAngle);
var d = 2 * (this.velocityX * wallNormalX + this.velocityY * wallNormalY);
this.x -= d * wallNormalX;
this.y -= d * wallNormalY;
}
var canvas = document.getElementById("myCanvas");
var ctx = canvas.getContext("2d");
var polygonPoints = [
{
x: 240,
y: 130
},
{
x: 140,
y: 100
},
{
x: 180,
y: 250
},
{
x: 320,
y: 280
},
{
x: 400,
y: 50
}
];
var game = {
ball: new Ball()
};
function Ball() {
this.x = canvas.width / 2;
this.y = canvas.height - 100;
this.oldX = this.x - 1;
this.oldY = this.y + 1;
this.velocityX = 0;
this.velocityY = 0;
this.radius = 8;
};
Ball.prototype.draw = function() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fillStyle = '#0095DD';
ctx.fill();
ctx.closePath();
};
Ball.prototype.update = function() {
var x = this.x;
var y = this.y;
this.velocityX = this.x - this.oldX;
this.velocityY = this.y - this.oldY;
this.x += this.velocityX;
this.y += this.velocityY;
this.oldX = x;
this.oldY = y;
};
Ball.prototype.collision = function() {
var p = polygonPoints;
for (var i = 0, n = p.length; i < n; i++) {
var start = i;
var end = (i + 1) % n;
var x0 = p[start].x;
var y0 = p[start].y;
var x1 = p[end].x;
var y1 = p[end].y;
// detection collision
var dx = x1 - x0;
var dy = y1 - y0;
var len = Math.sqrt(dx * dx + dy * dy);
var dist = (dx * (this.y - y0) - dy * (this.x - x0)) / len;
if (dist < this.radius) {
continue;
}
// calculate reflection, because collided
var wallAngle = Math.atan2(dy, dx);
var wallNormalX = Math.sin(wallAngle);
var wallNormalY = -Math.cos(wallAngle);
var d = 2 * (this.velocityX * wallNormalX + this.velocityY * wallNormalY);
this.x -= d * wallNormalX;
this.y -= d * wallNormalY;
}
};
function drawBall() {
ctx.beginPath();
ctx.arc(x, y, ballRadius, 0, Math.PI*2);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
function drawPolygon() {
ctx.beginPath();
ctx.strokeStyle = '#333';
ctx.moveTo(polygonPoints[0].x, polygonPoints[0].y);
for (var i = 1, n = polygonPoints.length; i < n; i++) {
ctx.lineTo(polygonPoints[i].x, polygonPoints[i].y);
}
ctx.lineTo(polygonPoints[0].x, polygonPoints[0].y);
ctx.stroke();
ctx.closePath();
}
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawPolygon();
game.ball.draw();
game.ball.update();
game.ball.collision();
window.requestAnimationFrame(render);
}
render();
canvas {
border: 1px solid #333;
}
<canvas id="myCanvas" width="480" height="320"></canvas>
What the problem? Maybe I need use other method for detect collision? I tried to use this one, but if my circle has high speed this method not working.
Thank you.
Circle line segment intercept
UPDATE
This answer includes line line intercept, moving a line along its normal, distance point (circle) to line, and circle line intercept.
The circle is
var circle = {
radius : 500,
center : point(1000,1000),
}
The line segment is
var line = {
p1 : point(500,500),
p2 : point(2000,1000),
}
A point is
var point = {
x : 100,
y : 100,
}
Thus the function to find the intercept of a line segment width a circle
The function returns an array of up to two point on the line segment. If no points found returns an empty array.
function inteceptCircleLineSeg(circle, line){
var a, b, c, d, u1, u2, ret, retP1, retP2, v1, v2;
v1 = {};
v2 = {};
v1.x = line.p2.x - line.p1.x;
v1.y = line.p2.y - line.p1.y;
v2.x = line.p1.x - circle.center.x;
v2.y = line.p1.y - circle.center.y;
b = (v1.x * v2.x + v1.y * v2.y);
c = 2 * (v1.x * v1.x + v1.y * v1.y);
b *= -2;
d = Math.sqrt(b * b - 2 * c * (v2.x * v2.x + v2.y * v2.y - circle.radius * circle.radius));
if(isNaN(d)){ // no intercept
return [];
}
u1 = (b - d) / c; // these represent the unit distance of point one and two on the line
u2 = (b + d) / c;
retP1 = {}; // return points
retP2 = {}
ret = []; // return array
if(u1 <= 1 && u1 >= 0){ // add point if on the line segment
retP1.x = line.p1.x + v1.x * u1;
retP1.y = line.p1.y + v1.y * u1;
ret[0] = retP1;
}
if(u2 <= 1 && u2 >= 0){ // second add point if on the line segment
retP2.x = line.p1.x + v1.x * u2;
retP2.y = line.p1.y + v1.y * u2;
ret[ret.length] = retP2;
}
return ret;
}
UPDATE
Line line intercept.
Returns a point if found else returns undefined.
function interceptLines(line,line1){
var v1, v2, c, u;
v1 = {};
v2 = {};
v3 = {};
v1.x = line.p2.x - line.p1.x; // vector of line
v1.y = line.p2.y - line.p1.y;
v2.x = line1.p2.x - line1.p1.x; //vector of line2
v2.y = line1.p2.y - line1.p1.y;
var c = v1.x * v2.y - v1.y * v2.x; // cross of the two vectors
if(c !== 0){
v3.x = line.p1.x - line1.p1.x;
v3.y = line.p1.y - line1.p1.y;
u = (v2.x * v3.y - v2.y * v3.x) / c; // unit distance of intercept point on this line
return {x : line.p1.x + v1.x * u, y : line.p1.y + v1.y * u};
}
return undefined;
}
Lift Line
Move line along its normal
function liftLine(line,dist){
var v1,l
v1 = {};
v1.x = line.p2.x - line.p1.x; // convert line to vector
v1.y = line.p2.y - line.p1.y;
l = Math.sqrt(v1.x * v1.x + v1.y * v1.y); // get length;
v1.x /= l; // Assuming you never pass zero length lines
v1.y /= l;
v1.x *= dist; // set the length
v1.y *= dist;
// move the line along its normal the required distance
line.p1.x -= v1.y;
line.p1.y += v1.x;
line.p2.x -= v1.y;
line.p2.y += v1.x;
return line; // if needed
}
Distance circle (or point) to a line segment
Returns the closest distance to the line segment. It is just the circle center that I am using. So you can replace circle with a point
function circleDistFromLineSeg(circle,line){
var v1, v2, v3, u;
v1 = {};
v2 = {};
v3 = {};
v1.x = line.p2.x - line.p1.x;
v1.y = line.p2.y - line.p1.y;
v2.x = circle.center.x - line.p1.x;
v2.y = circle.center.y - line.p1.y;
u = (v2.x * v1.x + v2.y * v1.y) / (v1.y * v1.y + v1.x * v1.x); // unit dist of point on line
if(u >= 0 && u <= 1){
v3.x = (v1.x * u + line.p1.x) - circle.center.x;
v3.y = (v1.y * u + line.p1.y) - circle.center.y;
v3.x *= v3.x;
v3.y *= v3.y;
return Math.sqrt(v3.y + v3.x); // return distance from line
}
// get distance from end points
v3.x = circle.center.x - line.p2.x;
v3.y = circle.center.y - line.p2.y;
v3.x *= v3.x; // square vectors
v3.y *= v3.y;
v2.x *= v2.x;
v2.y *= v2.y;
return Math.min(Math.sqrt(v2.y + v2.x), Math.sqrt(v3.y + v3.x)); // return smaller of two distances as the result
}
I am working on importing a model into a scene using the THREE.js OBJ loader.
I know that I am able to import the geometry fine, because when I assign a MeshNormalMaterial to it, it shows up great. However, if I use anything that requires UV coordinates, It gives me the error:
[.WebGLRenderingContext]GL ERROR :GL_INVALID_OPERATION : glDrawElements: attempt to access out of range vertices in attribute 1
I know this is because the loaded OBJ has no UV coordinates, but I was wondering if there was any way to generate the needed texture coordinates. I have tried
material.needsUpdate = true;
geometry.uvsNeedUpdate = true;
geometry.buffersNeedUpdate = true;
...but to no avail.
Is there any way to automagically generate UV textures using three.js, or do I have to assign the coordinates myself?
To my knowledge there is no automatic way to calculate UV.
You must calculate yourself. Calculate a UV for a plane is quite easy, this site explains how: calculating texture coordinates
For a complex shape, I don't know how. Maybe you could detect planar surface.
EDIT
Here is a sample code for a planar surface (x, y, z) where z = 0:
geometry.computeBoundingBox();
var max = geometry.boundingBox.max,
min = geometry.boundingBox.min;
var offset = new THREE.Vector2(0 - min.x, 0 - min.y);
var range = new THREE.Vector2(max.x - min.x, max.y - min.y);
var faces = geometry.faces;
geometry.faceVertexUvs[0] = [];
for (var i = 0; i < faces.length ; i++) {
var v1 = geometry.vertices[faces[i].a],
v2 = geometry.vertices[faces[i].b],
v3 = geometry.vertices[faces[i].c];
geometry.faceVertexUvs[0].push([
new THREE.Vector2((v1.x + offset.x)/range.x ,(v1.y + offset.y)/range.y),
new THREE.Vector2((v2.x + offset.x)/range.x ,(v2.y + offset.y)/range.y),
new THREE.Vector2((v3.x + offset.x)/range.x ,(v3.y + offset.y)/range.y)
]);
}
geometry.uvsNeedUpdate = true;
The other answers here were a great help but didn't quite fit my requirements to apply a repeating pattern texture to all sides of a shape with mostly flat surfaces. The problem is that using only the x and y components as u and v results in weird stretched textures on vertical surfaces.
My solution below uses surface normals to pick which two components (x, y and z) to map to u and v. It's still pretty crude but it works quite well.
function assignUVs(geometry) {
geometry.faceVertexUvs[0] = [];
geometry.faces.forEach(function(face) {
var components = ['x', 'y', 'z'].sort(function(a, b) {
return Math.abs(face.normal[a]) > Math.abs(face.normal[b]);
});
var v1 = geometry.vertices[face.a];
var v2 = geometry.vertices[face.b];
var v3 = geometry.vertices[face.c];
geometry.faceVertexUvs[0].push([
new THREE.Vector2(v1[components[0]], v1[components[1]]),
new THREE.Vector2(v2[components[0]], v2[components[1]]),
new THREE.Vector2(v3[components[0]], v3[components[1]])
]);
});
geometry.uvsNeedUpdate = true;
}
This function doesn't normalise the UVs to the size of the object. This works better when applying the same texture to different sized objects in the same scene. However depending on the size of your world coordinate system, you'll probably need to scale and repeat the texture as well:
texture.repeat.set(0.1, 0.1);
texture.wrapS = texture.wrapT = THREE.MirroredRepeatWrapping;
Box UV mapping is possibly most useful thing in three.js configurators of any sort, -
https://jsfiddle.net/mmalex/pcjbysn1/
The solution works per face both with indexed and non-indexed buffer geometries.
Example of usage:
//build some mesh
var bufferGeometry = new THREE.BufferGeometry().fromGeometry(new THREE.DodecahedronGeometry(2.5, 0));
let material = new THREE.MeshPhongMaterial({
color: 0x10f0f0,
map: new THREE.TextureLoader().load('http://mbnsay.com/rayys/images/1K_UV_checker.jpg')
});
//find out the dimensions, to let texture size 100% fit without stretching
bufferGeometry.computeBoundingBox();
let bboxSize = bufferGeometry.boundingBox.getSize();
let uvMapSize = Math.min(bboxSize.x, bboxSize.y, bboxSize.z);
//calculate UV coordinates, if uv attribute is not present, it will be added
applyBoxUV(bufferGeometry, new THREE.Matrix4().getInverse(cube.matrix), uvMapSize);
//let three.js know
bufferGeometry.attributes.uv.needsUpdate = true;
The example is based on the following implementation of applyBoxUV
function _applyBoxUV(geom, transformMatrix, bbox, bbox_max_size) {
let coords = [];
coords.length = 2 * geom.attributes.position.array.length / 3;
// geom.removeAttribute('uv');
if (geom.attributes.uv === undefined) {
geom.addAttribute('uv', new THREE.Float32BufferAttribute(coords, 2));
}
//maps 3 verts of 1 face on the better side of the cube
//side of the cube can be XY, XZ or YZ
let makeUVs = function(v0, v1, v2) {
//pre-rotate the model so that cube sides match world axis
v0.applyMatrix4(transformMatrix);
v1.applyMatrix4(transformMatrix);
v2.applyMatrix4(transformMatrix);
//get normal of the face, to know into which cube side it maps better
let n = new THREE.Vector3();
n.crossVectors(v1.clone().sub(v0), v1.clone().sub(v2)).normalize();
n.x = Math.abs(n.x);
n.y = Math.abs(n.y);
n.z = Math.abs(n.z);
let uv0 = new THREE.Vector2();
let uv1 = new THREE.Vector2();
let uv2 = new THREE.Vector2();
// xz mapping
if (n.y > n.x && n.y > n.z) {
uv0.x = (v0.x - bbox.min.x) / bbox_max_size;
uv0.y = (bbox.max.z - v0.z) / bbox_max_size;
uv1.x = (v1.x - bbox.min.x) / bbox_max_size;
uv1.y = (bbox.max.z - v1.z) / bbox_max_size;
uv2.x = (v2.x - bbox.min.x) / bbox_max_size;
uv2.y = (bbox.max.z - v2.z) / bbox_max_size;
} else
if (n.x > n.y && n.x > n.z) {
uv0.x = (v0.z - bbox.min.z) / bbox_max_size;
uv0.y = (v0.y - bbox.min.y) / bbox_max_size;
uv1.x = (v1.z - bbox.min.z) / bbox_max_size;
uv1.y = (v1.y - bbox.min.y) / bbox_max_size;
uv2.x = (v2.z - bbox.min.z) / bbox_max_size;
uv2.y = (v2.y - bbox.min.y) / bbox_max_size;
} else
if (n.z > n.y && n.z > n.x) {
uv0.x = (v0.x - bbox.min.x) / bbox_max_size;
uv0.y = (v0.y - bbox.min.y) / bbox_max_size;
uv1.x = (v1.x - bbox.min.x) / bbox_max_size;
uv1.y = (v1.y - bbox.min.y) / bbox_max_size;
uv2.x = (v2.x - bbox.min.x) / bbox_max_size;
uv2.y = (v2.y - bbox.min.y) / bbox_max_size;
}
return {
uv0: uv0,
uv1: uv1,
uv2: uv2
};
};
if (geom.index) { // is it indexed buffer geometry?
for (let vi = 0; vi < geom.index.array.length; vi += 3) {
let idx0 = geom.index.array[vi];
let idx1 = geom.index.array[vi + 1];
let idx2 = geom.index.array[vi + 2];
let vx0 = geom.attributes.position.array[3 * idx0];
let vy0 = geom.attributes.position.array[3 * idx0 + 1];
let vz0 = geom.attributes.position.array[3 * idx0 + 2];
let vx1 = geom.attributes.position.array[3 * idx1];
let vy1 = geom.attributes.position.array[3 * idx1 + 1];
let vz1 = geom.attributes.position.array[3 * idx1 + 2];
let vx2 = geom.attributes.position.array[3 * idx2];
let vy2 = geom.attributes.position.array[3 * idx2 + 1];
let vz2 = geom.attributes.position.array[3 * idx2 + 2];
let v0 = new THREE.Vector3(vx0, vy0, vz0);
let v1 = new THREE.Vector3(vx1, vy1, vz1);
let v2 = new THREE.Vector3(vx2, vy2, vz2);
let uvs = makeUVs(v0, v1, v2, coords);
coords[2 * idx0] = uvs.uv0.x;
coords[2 * idx0 + 1] = uvs.uv0.y;
coords[2 * idx1] = uvs.uv1.x;
coords[2 * idx1 + 1] = uvs.uv1.y;
coords[2 * idx2] = uvs.uv2.x;
coords[2 * idx2 + 1] = uvs.uv2.y;
}
} else {
for (let vi = 0; vi < geom.attributes.position.array.length; vi += 9) {
let vx0 = geom.attributes.position.array[vi];
let vy0 = geom.attributes.position.array[vi + 1];
let vz0 = geom.attributes.position.array[vi + 2];
let vx1 = geom.attributes.position.array[vi + 3];
let vy1 = geom.attributes.position.array[vi + 4];
let vz1 = geom.attributes.position.array[vi + 5];
let vx2 = geom.attributes.position.array[vi + 6];
let vy2 = geom.attributes.position.array[vi + 7];
let vz2 = geom.attributes.position.array[vi + 8];
let v0 = new THREE.Vector3(vx0, vy0, vz0);
let v1 = new THREE.Vector3(vx1, vy1, vz1);
let v2 = new THREE.Vector3(vx2, vy2, vz2);
let uvs = makeUVs(v0, v1, v2, coords);
let idx0 = vi / 3;
let idx1 = idx0 + 1;
let idx2 = idx0 + 2;
coords[2 * idx0] = uvs.uv0.x;
coords[2 * idx0 + 1] = uvs.uv0.y;
coords[2 * idx1] = uvs.uv1.x;
coords[2 * idx1 + 1] = uvs.uv1.y;
coords[2 * idx2] = uvs.uv2.x;
coords[2 * idx2 + 1] = uvs.uv2.y;
}
}
geom.attributes.uv.array = new Float32Array(coords);
}
function applyBoxUV(bufferGeometry, transformMatrix, boxSize) {
if (transformMatrix === undefined) {
transformMatrix = new THREE.Matrix4();
}
if (boxSize === undefined) {
let geom = bufferGeometry;
geom.computeBoundingBox();
let bbox = geom.boundingBox;
let bbox_size_x = bbox.max.x - bbox.min.x;
let bbox_size_z = bbox.max.z - bbox.min.z;
let bbox_size_y = bbox.max.y - bbox.min.y;
boxSize = Math.max(bbox_size_x, bbox_size_y, bbox_size_z);
}
let uvBbox = new THREE.Box3(new THREE.Vector3(-boxSize / 2, -boxSize / 2, -boxSize / 2), new THREE.Vector3(boxSize / 2, boxSize / 2, boxSize / 2));
_applyBoxUV(bufferGeometry, transformMatrix, uvBbox, boxSize);
}
The answers here are brilliant and helped me a lot.
Only one thing: If you are updating vertices, do not re-assign uvs, but set them, as in (scope is my geometry):
scope.updateUVs = (copy=true) => {
scope.computeBoundingBox();
var max = scope.boundingBox.max;
var min = scope.boundingBox.min;
var offset = new THREE.Vector2(0 - min.x, 0 - min.y);
var range = new THREE.Vector2(max.x - min.x, max.y - min.y);
if (!copy) {
scope.faceVertexUvs[0] = [];
}
var faces = scope.faces;
for (i = 0; i < scope.faces.length ; i++) {
var v1 = scope.vertices[faces[i].a];
var v2 = scope.vertices[faces[i].b];
var v3 = scope.vertices[faces[i].c];
var uv0 = new THREE.Vector2( ( v1.x + offset.x ) / range.x , ( v1.y + offset.y ) / range.y );
var uv1 = new THREE.Vector2( ( v2.x + offset.x ) / range.x , ( v2.y + offset.y ) / range.y );
var uv2 = new THREE.Vector2( ( v3.x + offset.x ) / range.x , ( v3.y + offset.y ) / range.y );
if (copy) {
var uvs =scope.faceVertexUvs[0][i];
uvs[0].copy(uv0);
uvs[1].copy(uv1);
uvs[2].copy(uv2);
} else {
scope.faceVertexUvs[0].push([uv0, uv1, uv2]);
}
}
scope.uvsNeedUpdate = true;
}
This is a general version that works for spherical mapping (yaw, pitch coordinates), see example here, (look at loadSuzanne function):
function assignUVs(geometry) {
geometry.faceVertexUvs[0] = [];
geometry.faces.forEach(function(face) {
var uvs = [];
var ids = [ 'a', 'b', 'c'];
for( var i = 0; i < ids.length; i++ ) {
var vertex = geometry.vertices[ face[ ids[ i ] ] ].clone();
var n = vertex.normalize();
var yaw = .5 - Math.atan( n.z, - n.x ) / ( 2.0 * Math.PI );
var pitch = .5 - Math.asin( n.y ) / Math.PI;
var u = yaw,
v = pitch;
uvs.push( new THREE.Vector2( u, v ) );
}
geometry.faceVertexUvs[ 0 ].push( uvs );
});
geometry.uvsNeedUpdate = true;
}
I'm trying to draw a circle with, not radial gradients, but linear gradients that go around the circle... Basically, I'm trying to create a color wheel and it has to be dynamic as the colors will be customizable... However, I'm completely baffled on how to approach this matter...
I thought I could draw my own circle and color it, then loop the process with a larger radius to fill it out. But that proved to not only be extremely ineffecient but very buggy too...
Here was my first attempt: http://jsfiddle.net/gyFqX/1/
I stuck with that method but changed it to fill a 2x2 square for each point on the circle. It worked alright for blending up to 3 colors, but then you begin to notice it's distortion.
Anyway, I've continued working on it a bit and this is what I have now: http://jsfiddle.net/f3SQ2/
var ctx = $('#canvas')[0].getContext('2d'),
points = [],
thickness = 80;
for( var n = 0; n < thickness; n++ )
rasterCircle( 200, 200, (50 + n) );
function fillPixels() {
var size = points.length,
colors = [
hexToRgb( '#ff0000' ), // Red
hexToRgb( '#ff00ff' ), // Magenta
hexToRgb( '#0000ff' ), // Blue
hexToRgb( '#00ffff' ), // Teal
hexToRgb( '#00ff00' ), // Green
hexToRgb( '#ffff00' ), // Yellow
hexToRgb( '#ff0000' ), // Red
],
colorSpan = colors.length - 1;
if ( colors.length > 0 ) {
var lastPadding = size % colorSpan,
stepSize = size / colorSpan,
steps = null,
cursor = 0;
for ( var index = 0; index < colorSpan; index++ ) {
steps = Math.floor( ( index == colorSpan - 1 ) ? stepSize + lastPadding : stepSize );
createGradient( colors[ index ], colors[ index + 1 ], steps, cursor );
cursor += steps;
}
}
function createGradient( start, end, steps, cursor ) {
for ( var i = 0; i < steps; i++ ) {
var r = Math.floor( start.r + ( i * ( end.r - start.r ) / steps ) ),
g = Math.floor( start.g + ( i * ( end.g - start.g ) / steps ) ),
b = Math.floor( start.b + ( i * ( end.b - start.b ) / steps ) );
ctx.fillStyle = "rgba("+r+","+g+","+b+",1)";
ctx.fillRect( points[cursor][0], points[cursor][1], 2, 2 );
cursor++;
}
}
points = [];
}
function setPixel( x, y ) {
points.push( [ x, y ] );
}
function rasterCircle(x0, y0, radius) {
var f = 1 - radius,
ddF_x = 1,
ddF_y = -2 * radius,
x = 0,
y = radius;
setPixel(x0, y0 + radius);
while(x < y) {
if(f >= 0) {
y--;
ddF_y += 2;
f += ddF_y;
}
x++;
ddF_x += 2;
f += ddF_x;
setPixel(x0 - x, y0 - y);
}
var temp = [];
f = 1 - radius,
ddF_x = 1,
ddF_y = -2 * radius,
x = 0,
y = radius;
while(x < y) {
if(f >= 0) {
y--;
ddF_y += 2;
f += ddF_y;
}
x++;
ddF_x += 2;
f += ddF_x;
temp.push( [x0 - y, y0 - x] );
}
temp.push( [x0 - radius, y0] );
for(var i = temp.length - 1; i > 0; i--)
setPixel( temp[i][0], temp[i][1] );
fillPixels();
}
What I'm trying to accomplish is something like this: http://img252.imageshack.us/img252/3826/spectrum.jpg
The 'brightness' (white to black fade) is not an issue as I know it can be accomplished by using a radial gradient after the color spectrum has been drawn. However, I'd appreciate some help in figuring out how to draw the spectrum itself.
I was even thinking I could draw a linear one and then bend (transform) it, but there aren't any native functions to do that and tackling something such as that is above my skill level. :-/
Check this out: http://jsfiddle.net/f3SQ2/5/
var can = $('#canvas')[0],
ctx = can.getContext('2d'),
radius = 120,
thickness = 80,
p = {
x: can.width,
y: can.height
},
start = Math.PI,
end = start + Math.PI / 2,
step = Math.PI / 180,
ang = 0,
grad,
r = 0,
g = 0,
b = 0,
pct = 0;
ctx.translate(p.x, p.y);
for (ang = start; ang <= end; ang += step) {
ctx.save();
ctx.rotate(-ang);
// linear gradient: black->current color->white
grad = ctx.createLinearGradient(0, radius - thickness, 0, radius);
grad.addColorStop(0, 'black');
h = 360-(ang-start)/(end-start) * 360;
s = '100%';
l = '50%';
grad.addColorStop(.5, 'hsl('+[h,s,l].join()+')');
grad.addColorStop(1, 'white');
ctx.fillStyle = grad;
// the width of three for the rect prevents gaps in the arc
ctx.fillRect(0, radius - thickness, 3, thickness);
ctx.restore();
}
Edit: fixed color spectrum. Apparently we can just give it HSL values, no need for conversions or messy calculations!
Modified a few things to handle scaling better: http://jsfiddle.net/f3SQ2/6/
step = Math.PI / 360
ctx.fillRect(0, radius - thickness, radius/10, thickness);
You could for example set the gradient stops like so:
h = 360-(ang-start)/(end-start) * 360;
s = '100%';
grad.addColorStop(0, 'hsl('+[h,s,'0%'].join()+')'); //black
grad.addColorStop(.5,'hsl('+[h,s,'50%'].join()+')'); //color
grad.addColorStop(1, 'hsl('+[h,s,'100%'].join()+')');//white
My first note would be that the image you linked to has all 3 components it doesn't need to change and could just be a static image.
I adapted some code from a project i'm working on:
http://jsfiddle.net/f3SQ2/1/
function drawColourArc(image) {
var data = image.data;
var i = 0;
var w = image.width, h = image.height;
var result = [0, 0, 0, 1];
var outer = 1, inner = 0.5;
var mid = 0.75;
for (var y = 0; y < h; y++) {
for (var x = 0; x < w; x++) {
var dx = (x / w) - 1, dy = (y / w) - 1;
var angular = ((Math.atan2(dy, dx) + Math.PI) / (2 * Math.PI)) * 4;
var radius = Math.sqrt((dx * dx) + (dy * dy));
if (radius < inner || radius > outer) {
data[i++] = 255;
data[i++] = 255;
data[i++] = 255;
data[i++] = 0;
}
else {
if (radius < mid) {
var saturation = 1;
var brightness = (radius - 0.5) * 4;
}
else {
var saturation = 1- ((radius - 0.75) * 4);
var brightness = 1;
}
result[0] = angular;
result[1] = saturation;
result[2] = brightness;
result[3] = 1;
//Inline HSBToRGB
if (result[1] == 0) {
result[0] = result[1] = result[2] = result[2];
}
else {
var varH = result[0] * 6;
var varI = Math.floor(varH); //Or ... var_i = floor( var_h )
var var1 = result[2] * (1 - result[1]);
var var2 = result[2] * (1 - result[1] * (varH - varI));
var var3 = result[2] * (1 - result[1] * (1 - (varH - varI)));
if (varI == 0 || varI == 6) {
result[0] = result[2];
result[1] = var3;
result[2] = var1;
}
else if (varI == 1) {
result[0] = var2;
result[1] = result[2];
result[2] = var1;
}
else if (varI == 2) {
result[0] = var1;
result[1] = result[2];
result[2] = var3;
}
else if (varI == 3) {
result[0] = var1;
result[1] = var2;
result[2] = result[2];
}
else if (varI == 4) {
result[0] = var3;
result[1] = var1;
result[2] = result[2];
}
else {
result[0] = result[2];
result[1] = var1;
result[2] = var2;
}
}
//End of inline
data[i++] = result[0] * 255;
data[i++] = result[1] * 255;
data[i++] = result[2] * 255;
data[i++] = result[3] * 255;
}
}
}
};
var canvas = document.getElementsByTagName("canvas")[0];
var ctx = canvas.getContext("2d");
var image = ctx.createImageData(canvas.width, canvas.height);
drawColourArc(image);
ctx.putImageData(image, 0, 0);
This does it per-pixel which is accurate but you may want to draw an outline to combat the aliasing. It could be adapted to use custom colours instead of interpolating hue.