How to interpolate between 2 points in a requestAnimationframe loop - javascript

I will gladly take a library for this if available? Or any other suggestion to my psuedocode api.
Question:
How do I write a function that takes 4 parameters interpolate(start, end, time, ease) and interpolate over the start to end numbers over time with ease?
Problem:
This feels extra hard because I don't know how to control time or ease in a request animation frame. Secondly, I don't know how to write the bezier curve handler. lastly, optimization if needed.
interpolate(start:number, end:number, time:number, ease) {
// easing
return value;
}
function _draw() {
currentValue = interpolate(0, 10, 0.7, 'cubic-bezier(.62,.28,.23,.99)');
if(currentValue !== lastValue) {
console.log(currentValue)
}
requestAnimationFrame(_draw);
}
So in the end currentValue would log out 10 in it's final tick of the 0.7.

I would suggest to use Penner Equations for generic easings. The functions could be found on github as library:
tween-functions.
A simple demo to show how the value is calculated on each frame:
const Easings = [
function easeInQuad(t, b, c, d) {
return c * (t /= d) * t + b;
},
function easeOutBounce(t, b, c, d) {
if ((t /= d) < (1 / 2.75)) {
return c * (7.5625 * t * t) + b;
} else if (t < (2 / 2.75)) {
return c * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75) + b;
} else if (t < (2.5 / 2.75)) {
return c * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375) + b;
} else {
return c * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375) + b;
}
},
function easeInOutElastic(t, b, c, d) {
// jshint eqeqeq: false, -W041: true
var s = 1.70158;
var p = 0;
var a = c;
if (t == 0) return b;
if ((t /= d / 2) == 2) return b + c;
if (!p) p = d * (0.3 * 1.5);
if (a < Math.abs(c)) {
a = c;
s = p / 4;
} else s = p / (2 * Math.PI) * Math.asin(c / a);
if (t < 1) return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b;
return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p) * 0.5 + c + b;
},
];
const duration = 2000;
const div = document.querySelector('div');
let start = Date.now();
let from = 0;
let to = 300;
let fnCounter = 0;
let fn = Easings[fnCounter];
function tick() {
let now = Date.now();
let elapsed = now - start;
let val = fn(elapsed, from, to - from, duration);
div.style.transform = `translateX(${val}px)`;
if (elapsed >= duration) {
start = now;
let x = from;
from = to;
to = x;
fn = Easings[++fnCounter % Easings.length]
setTimeout(tick, 300);
return;
}
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
div {
position: absolute;
top: 0px;
left: 0;
width: 100px;
height: 100px;
background: orange;
}
<div></div>
For custom bezier curves I would suggest this library: bezier-easing. The calculations here are same: on each tick you get the elapsed time and calculates the percentage from the duration, now you have a tick value from 0 to 1 on each frame.

Related

How to change color of circle when collision detection occurs?

As far as I understood it seems that even after changing the color when the collision is detected it reverts back to blue due to the else statement when it is compared between other circle and they are not colliding. So how would you solve this so that that the instance when the collision between any circle occurs it changes to red
collision detection
this.update = function() {
for (let i = 0; i < circles.length; i++) {
if (this !== circles[i] && getDistance(this.x, this.y, circles[i].x, circles[i].y) <= 200 * 200) {
this.c = 'red';
circles[i].c = 'red';
resolveCollision(this, circles[i]);
} else {
this.c = 'blue';
circles[i].c = 'blue';
}
}
//wall deflection
if (this.x - this.r <= 0 || this.x + this.r >= innerWidth)
this.v.x *= -1
if (this.y - this.r <= 0 || this.y + this.r >= innerHeight)
this.v.y *= -1
this.x += this.v.x;
this.y += this.v.y;
this.draw();
};
//deflection amongst other circles
function resolveCollision(circle, othercircle) {
const xVelocityDiff = circle.v.x - othercircle.v.x;
const yVelocityDiff = circle.v.y - othercircle.v.y;
const xDist = othercircle.x - circle.x;
const yDist = othercircle.y - circle.y;
if (xVelocityDiff * xDist + yVelocityDiff * yDist >= 0) {
const angle = -Math.atan2(othercircle.y - circle.y, othercircle.x - circle.x);
const m1 = circle.m;
const m2 = othercircle.m;
const u1 = rotate(circle.v, angle);
const u2 = rotate(othercircle.v, angle);
const v1 = {
x: u1.x * (m1 - m2) / (m1 + m2) + u2.x * 2 * m2 / (m1 + m2),
y: u1.y
}
const v2 = {
x: u2.x * (m1 - m2) / (m1 + m2) + u1.x * 2 * m2 / (m1 + m2),
y: u2.y
}
const vFinal1 = rotate(v1, -angle);
const vFinal2 = rotate(v2, -angle);
circle.v.x = vFinal1.x;
circle.v.y = vFinal1.y;
othercircle.v.x = vFinal2.x;
othercircle.v.y = vFinal2.y;
}
}
Semaphores
Use a semaphore that holds the collision state of the circle.
Thus in your Circle.prototype would have something like these functions and properties
Circle.prototype = {
collided: false, // when true change color
draw() {
ctx.strokeStyle = this.collided ? "red" : "blue";
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(this.x, this.y, this.r - 1.5, 0, Math.PI * 2);
ctx.stroke();
},
...
...
// in update
update() {
// when collision is detected set semaphore
if (collision) {
this.collided = true;
}
}
}
Counters
Or you may want to only have the color change last for some time. You can modify the semaphore and use it as a counter. On collision set it to the number of frames to change color for.
Circle.prototype = {
collided: 0,
draw() {
ctx.strokeStyle = this.collided ? (this.collided--, "red") : "blue";
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(this.x, this.y, this.r - 1.5, 0, Math.PI * 2);
ctx.stroke();
},
...
...
// in update
update() {
// when collision is detected set semaphore
if (collision) {
this.collided = 60; // 1 second at 60FPS
}
}
}
Example
This example is taken from another answer I did earlier this year.
As there is a lot of code I have highlighted the relevant code with
/*= ANSWER CODE ==============================================================
...
=============================================================================*/
The example uses counters and changes color for 30 frames after a collision with another ball or wall.
I did not use a semaphore as all the balls would be red within a second.
canvas.width = innerWidth -20;
canvas.height = innerHeight -20;
mathExt(); // creates some additional math functions
const ctx = canvas.getContext("2d");
const GRAVITY = 0;
const WALL_LOSS = 1;
const BALL_COUNT = 10; // approx as will not add ball if space can not be found
const MIN_BALL_SIZE = 6;
const MAX_BALL_SIZE = 30;
const VEL_MIN = 1;
const VEL_MAX = 5;
const MAX_RESOLUTION_CYCLES = 100; // Put too many balls (or too large) in the scene and the
// number of collisions per frame can grow so large that
// it could block the page.
// If the number of resolution steps is above this value
// simulation will break and balls can pass through lines,
// get trapped, or worse. LOL
const SHOW_COLLISION_TIME = 30;
const balls = [];
const lines = [];
function Line(x1,y1,x2,y2) {
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
}
Line.prototype = {
draw() {
ctx.moveTo(this.x1, this.y1);
ctx.lineTo(this.x2, this.y2);
},
reverse() {
const x = this.x1;
const y = this.y1;
this.x1 = this.x2;
this.y1 = this.y2;
this.x2 = x;
this.y2 = y;
return this;
}
}
function Ball(x, y, vx, vy, r = 45, m = 4 / 3 * Math.PI * (r ** 3)) {
this.r = r;
this.m = m
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
/*= ANSWER CODE ==============================================================*/
this.collided = 0;
/*============================================================================*/
}
Ball.prototype = {
update() {
this.x += this.vx;
this.y += this.vy;
this.vy += GRAVITY;
},
draw() {
/*= ANSWER CODE ==============================================================*/
ctx.strokeStyle = this.collided ? (this.collided--, "#F00") : "#00F";
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(this.x, this.y, this.r - 1.5, 0, Math.PI * 2);
ctx.stroke();
/* ============================================================================*/
},
interceptLineTime(l, time) {
const u = Math.interceptLineBallTime(this.x, this.y, this.vx, this.vy, l.x1, l.y1, l.x2, l.y2, this.r);
if (u >= time && u <= 1) {
return u;
}
},
checkBallBallTime(t, minTime) {
return t > minTime && t <= 1;
},
interceptBallTime(b, time) {
const x = this.x - b.x;
const y = this.y - b.y;
const d = (x * x + y * y) ** 0.5;
if (d > this.r + b.r) {
const times = Math.circlesInterceptUnitTime(
this.x, this.y,
this.x + this.vx, this.y + this.vy,
b.x, b.y,
b.x + b.vx, b.y + b.vy,
this.r, b.r
);
if (times.length) {
if (times.length === 1) {
if(this.checkBallBallTime(times[0], time)) { return times[0] }
return;
}
if (times[0] <= times[1]) {
if(this.checkBallBallTime(times[0], time)) { return times[0] }
if(this.checkBallBallTime(times[1], time)) { return times[1] }
return
}
if(this.checkBallBallTime(times[1], time)) { return times[1] }
if(this.checkBallBallTime(times[0], time)) { return times[0] }
}
}
},
collideLine(l, time) {
/*= ANSWER CODE ==============================================================*/
this.collided = SHOW_COLLISION_TIME;
/*============================================================================*/
const x1 = l.x2 - l.x1;
const y1 = l.y2 - l.y1;
const d = (x1 * x1 + y1 * y1) ** 0.5;
const nx = x1 / d;
const ny = y1 / d;
const u = (this.vx * nx + this.vy * ny) * 2;
this.x += this.vx * time;
this.y += this.vy * time;
this.vx = (nx * u - this.vx) * WALL_LOSS;
this.vy = (ny * u - this.vy) * WALL_LOSS;
this.x -= this.vx * time;
this.y -= this.vy * time;
},
collide(b, time) { // b is second ball
/*= ANSWER CODE ==============================================================*/
this.collided = SHOW_COLLISION_TIME;
b.collided = SHOW_COLLISION_TIME;
/*============================================================================*/
const a = this;
const m1 = a.m;
const m2 = b.m;
a.x = a.x + a.vx * time;
a.y = a.y + a.vy * time;
b.x = b.x + b.vx * time;
b.y = b.y + b.vy * time;
const x = a.x - b.x
const y = a.y - b.y
const d = (x * x + y * y);
const u1 = (a.vx * x + a.vy * y) / d
const u2 = (x * a.vy - y * a.vx ) / d
const u3 = (b.vx * x + b.vy * y) / d
const u4 = (x * b.vy - y * b.vx ) / d
const mm = m1 + m2;
const vu3 = (m1 - m2) / mm * u1 + (2 * m2) / mm * u3;
const vu1 = (m2 - m1) / mm * u3 + (2 * m1) / mm * u1;
b.vx = x * vu1 - y * u4;
b.vy = y * vu1 + x * u4;
a.vx = x * vu3 - y * u2;
a.vy = y * vu3 + x * u2;
a.x = a.x - a.vx * time;
a.y = a.y - a.vy * time;
b.x = b.x - b.vx * time;
b.y = b.y - b.vy * time;
},
doesOverlap(ball) {
const x = this.x - ball.x;
const y = this.y - ball.y;
return (this.r + ball.r) > ((x * x + y * y) ** 0.5);
}
}
function canAdd(ball) {
for(const b of balls) {
if (ball.doesOverlap(b)) { return false }
}
return true;
}
function create(bCount) {
lines.push(new Line(-10, 20, ctx.canvas.width + 10, 5));
lines.push((new Line(-10, ctx.canvas.height - 2, ctx.canvas.width + 10, ctx.canvas.height - 30)).reverse());
lines.push((new Line(30, -10, 4, ctx.canvas.height + 10)).reverse());
lines.push(new Line(ctx.canvas.width - 3, -10, ctx.canvas.width - 30, ctx.canvas.height + 10));
while (bCount--) {
let tries = 100;
while (tries--) {
const dir = Math.rand(0, Math.TAU);
const vel = Math.rand(VEL_MIN, VEL_MAX);
const ball = new Ball(
Math.rand(MAX_BALL_SIZE + 30, canvas.width - MAX_BALL_SIZE - 30),
Math.rand(MAX_BALL_SIZE + 30, canvas.height - MAX_BALL_SIZE - 30),
Math.cos(dir) * vel,
Math.sin(dir) * vel,
Math.rand(MIN_BALL_SIZE, MAX_BALL_SIZE),
);
if (canAdd(ball)) {
balls.push(ball);
break;
}
}
}
}
function resolveCollisions() {
var minTime = 0, minObj, minBall, resolving = true, idx = 0, idx1, after = 0, e = 0;
while (resolving && e++ < MAX_RESOLUTION_CYCLES) { // too main ball may create very lone resolution cycle. e limits this
resolving = false;
minObj = undefined;
minBall = undefined;
minTime = 1;
idx = 0;
for (const b of balls) {
idx1 = idx + 1;
while (idx1 < balls.length) {
const b1 = balls[idx1++];
const time = b.interceptBallTime(b1, after);
if (time !== undefined) {
if (time <= minTime) {
minTime = time;
minObj = b1;
minBall = b;
resolving = true;
}
}
}
for (const l of lines) {
const time = b.interceptLineTime(l, after);
if (time !== undefined) {
if (time <= minTime) {
minTime = time;
minObj = l;
minBall = b;
resolving = true;
}
}
}
idx ++;
}
if (resolving) {
if (minObj instanceof Ball) {
minBall.collide(minObj, minTime);
} else {
minBall.collideLine(minObj, minTime);
}
after = minTime;
}
}
}
create(BALL_COUNT);
mainLoop();
function mainLoop() {
ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);
resolveCollisions();
for (const b of balls) { b.update() }
for (const b of balls) { b.draw() }
ctx.lineWidth = 1;
ctx.strokeStyle = "#000";
ctx.beginPath();
for(const l of lines) { l.draw() }
ctx.stroke();
requestAnimationFrame(mainLoop);
}
function mathExt() {
Math.TAU = Math.PI * 2;
Math.rand = (min, max) => Math.random() * (max - min) + min;
Math.randI = (min, max) => Math.random() * (max - min) + min | 0; // only for positive numbers 32bit signed int
Math.randItem = arr => arr[Math.random() * arr.length | 0]; // only for arrays with length < 2 ** 31 - 1
// contact points of two circles radius r1, r2 moving along two lines (a,e)-(b,f) and (c,g)-(d,h) [where (,) is coord (x,y)]
Math.circlesInterceptUnitTime = (a, e, b, f, c, g, d, h, r1, r2) => { // args (x1, y1, x2, y2, x3, y3, x4, y4, r1, r2)
const A = a * a, B = b * b, C = c * c, D = d * d;
const E = e * e, F = f * f, G = g * g, H = h * h;
var R = (r1 + r2) ** 2;
const AA = A + B + C + F + G + H + D + E + b * c + c * b + f * g + g * f + 2 * (a * d - a * b - a * c - b * d - c * d - e * f + e * h - e * g - f * h - g * h);
const BB = 2 * (-A + a * b + 2 * a * c - a * d - c * b - C + c * d - E + e * f + 2 * e * g - e * h - g * f - G + g * h);
const CC = A - 2 * a * c + C + E - 2 * e * g + G - R;
return Math.quadRoots(AA, BB, CC);
}
Math.quadRoots = (a, b, c) => { // find roots for quadratic
if (Math.abs(a) < 1e-6) { return b != 0 ? [-c / b] : [] }
b /= a;
var d = b * b - 4 * (c / a);
if (d > 0) {
d = d ** 0.5;
return [0.5 * (-b + d), 0.5 * (-b - d)]
}
return d === 0 ? [0.5 * -b] : [];
}
Math.interceptLineBallTime = (x, y, vx, vy, x1, y1, x2, y2, r) => {
const xx = x2 - x1;
const yy = y2 - y1;
const d = vx * yy - vy * xx;
if (d > 0) { // only if moving towards the line
const dd = r / (xx * xx + yy * yy) ** 0.5;
const nx = xx * dd;
const ny = yy * dd;
return (xx * (y - (y1 + nx)) - yy * (x -(x1 - ny))) / d;
}
}
}
<canvas id="canvas"></canvas>
const collided = {
color: 'red',
get current() {
return this.color
},
set current(clr) {
if (this.color === 'red') {
this.color = 'blue'
} else {
this.color = 'red'
}
}
}
this.update= function(){
for(let i=0;i<circles.length;i++){
if(this!==circles[i] && getDistance(this.x,this.y,circles[i].x,circles[i].y)<=200*200){
this.c=collided.current
collided.current = this.c
circles[i].c=collided.current
resolveCollision(this,circles[i]);
}
// ...
}
}
The trick is to use use getters and setters to ensure that the most recently used color value is never reapplied
First, split the if:
// psudo-code
if this is not circles[i], then
if overlapping, then
do something
else
do something else
else
do nothing
Then, fix if this is not circles[i]:
{x:100,y:100}!={x:100,y:100}, so
either add id to circles and compare ids (my recommendation), or,
compare .xs and .ys (less desired - what if they are equal but not same circle?), or,
use JSON.stringify(a)==JSON.stringify(b).
I would add .overlapping and before the loop, I'd add another loop setting all .overlapping to false, then, withing the modified original loop, I'd check if .overlapping is false, and then if there is a collision, I'd set .overlapping to true for both.
Another way would be to create an array to hole overlapping circles' .ids and check if that array includes the current loop item's .id.

How to plot an ellipse on canvas from 2 points on the ellipse, where slope of major axis (rx), and minor axis (ry) length are unknown

This may be more of a mathematics problem, but maybe there is a simple javascript solution that I am missing.
I want to plot an ellipse on html canvas from user input of a center point, radius of the major (longest) axis, and 2 points will fall on the ellipse.
This should potentially create 2 possible ellipse paths, both of which will center around the center point, and cross through the 2 points.
So for example, if the center = [2, 1] major axis radius a = 10, point 1 u = [4, 2] and point 2 v = [5, 6], what is the minor axis radius b and angle of rotation theta?
So far I have tried to implement an equation that I found from https://math.stackexchange.com/questions/3210414/find-the-angle-of-rotation-and-minor-axis-length-of-ellipse-from-major-axis-leng,
but it does not return valid values. My javascript code looks like this:
function getEllipseFrom2Points(center, u, v, a) {
function getSlope(plusOrMinus) {
return Math.sqrt(((uy * vx - ux * vy) ** 2) / (-ux * uy * (a * (v2x + v2y) - 1) + vx * vy * (a * (u2x + u2y) - 1) - plusOrMinus * (uy * vx - ux * vy) * q) / (u2x * (1 - a * v2y) + v2x * (a * u2y - 1)));
}
function getMinorAxis(plusOrMinus) {
return (u2x + u2y + v2x + v2y - a * (2 * u2x * v2x + 2 * u2y * v2y + 2 * ux * uy * vx * vy + u2y * v2x + u2x * v2y) + plusOrMinus * 2 * (ux * vx + uy * vy) * q);
}
var vx = v[0],
vy = v[1],
ux = u[0],
uy = u[1],
v2x = vx ** 2,
v2y = vy ** 2,
u2x = ux ** 2,
u2y = uy ** 2,
q = Math.sqrt((1 - a * (u2x + u2y)) * (1 - a * (v2x + v2y))),
ellipse1 = { rx: a, ry: getMinorAxis(1), origin: center, rotation: getSlope(1) },
ellipse2 = { rx: a, ry: getMinorAxis(-1), origin: center, rotation: getSlope(-1) };
}
Either the equation that I am following is wrong, or I have implemented it wrong
In case anyone is interested, here is my solution to the problem, which isn't really "the" solution. If anyone can solve this I would still be happy to know.
Since I can't solve for both slope of the major axis and length of the minor axis, I just take a guess at slope and then test how close it is, and then refine the result by trying in a smaller and smaller region. Since the final ellipse that gets drawn is actually an estimation constructed from bezier curves, I can get close enough in a reasonable amount of time.
function getEllipseFrom2Points (center, u, v, a) {
function getSemiMinorAxis([x, y], a, t) {
// equation for rotated ellipse
// b = a(ycos(t) - xsin(t)) / sqrt(a^2 - x^2cos^2(t) - 2xysin(t)cos(t) - y^2sin^2(t)) and
// b = a(xsin(t) - ycos(t)) / sqrt(a^2 - x^2cos^2(t) - 2xysin(t)cos(t) - y^2sin^2(t))
// where a^2 !== (xcos(t) + ysin(t))^2
// and aycos(t) !== axsin(t)
if (a ** 2 !== (x * Math.cos(t) + y * Math.sin(t)) ** 2 &&
a * y * Math.cos(t) !== a * x * Math.sin(t)) {
var b = [],
q = (Math.sqrt(a ** 2 - x ** 2 * (Math.cos(t)) ** 2 - 2 * x * y * Math.sin(t) * Math.cos(t) - y ** 2 * (Math.sin(t)) ** 2));
b[0] = (a * (y * Math.cos(t) - x * Math.sin(t))) / q;
b[1] = (a * (x * Math.sin(t) - y * Math.cos(t))) / q;
return b;
}
}
function getAngle_radians(point1, point2){
return Math.atan2(point2[1] - point1[1], point2[0] - point1[0]);
}
function getDistance(point1, point2) {
return Math.sqrt((point2[0] - point1[0]) ** 2 + (point2[1] - point1[1]) ** 2);
}
function rotatePoint(point, center, radians) {
var x = (point[0] - center[0]) * Math.cos(radians) - (point[1] - center[1]) * Math.sin(radians) + center[0];
var y = (point[1] - center[1]) * Math.cos(radians) + (point[0] - center[0]) * Math.sin(radians) + center[1];
return [x, y];
}
function measure(ellipseRotation, pointOnEllipse, minorAxisLength) {
var d = getDistance(point, pointOnEllipse);
if (d < bestDistanceBetweenPointAndEllipse) {
bestDistanceBetweenPointAndEllipse = d;
bestEstimationOfB = minorAxisLength;
bestEstimationOfR = ellipseRotation;
}
}
function getBestEstimate(min, max) {
var testIncrement = (max - min) / 10;
for (let r = min; r < max; r = r + testIncrement) {
if (radPoint1 < r && radPoint2 < r || radPoint1 > r && radPoint2 > r) {//points both on same side of ellipse
semiMinorAxis = getSemiMinorAxis(v, a, r);
if (semiMinorAxis) {
for (let t = 0; t < circle; t = t + degree) {
ellipsePoint1 = [a * Math.cos(t), semiMinorAxis[0] * Math.sin(t)];
ellipsePoint2 = [a * Math.cos(t), semiMinorAxis[1] * Math.sin(t)];
point = rotatePoint(u, [0, 0], -r);
measure(r, ellipsePoint1, semiMinorAxis[0]);
measure(r, ellipsePoint2, semiMinorAxis[1]);
}
}
}
}
count++;
if (new Date().getTime() - startTime < 200 && count < 10) //refine estimate
getBestEstimate(bestEstimationOfR - testIncrement, bestEstimationOfR + testIncrement);
}
if (center instanceof Array &&
typeof center[0] === "number" &&
typeof center[1] === "number" &&
u instanceof Array &&
typeof u[0] === "number" &&
typeof u[1] === "number" &&
v instanceof Array &&
typeof v[0] === "number" &&
typeof v[1] === "number" &&
typeof a === "number") {
// translate points
u = [u[0] - center[0], u[1] - center[1]];
v = [v[0] - center[0], v[1] - center[1]];
var bestDistanceBetweenPointAndEllipse = a,
point,
semiMinorAxis,
ellipsePoint1,
ellipsePoint2,
bestEstimationOfB,
bestEstimationOfR,
radPoint1 = getAngle_radians([0, 0], v),
radPoint2 = getAngle_radians([0, 0], u),
circle = 2 * Math.PI,
degree = circle / 360,
startTime = new Date().getTime(),
count = 0;
getBestEstimate(0, circle);
var ellipseModel = MakerJs.$(new MakerJs.models.Ellipse(a, bestEstimationOfB))
.rotate(MakerJs.angle.toDegrees(bestEstimationOfR), [0, 0])
.move(center)
.originate([0, 0])
.$result;
return ellipseModel;
}

How to Optimize execution time for RGB to HSL conversion function?

I've created this function to convert RGB color to HSL color. It works perfect.
But I need to make it run faster, because it's used to replace colors on a canvas, and I need to reduce the time of replacement. Since the image contains 360k pixels (600x600px) anything can make it faster.
That's my current implementation:
/**
* Convert RGB Color to HSL Color
* #param {{R: integer, G: integer, B: integer}} rgb
* #returns {{H: number, S: number, L: number}}
*/
Colorize.prototype.rgbToHsl = function(rgb) {
var R = rgb.R/255;
var G = rgb.G/255;
var B = rgb.B/255;
var Cmax = Math.max(R,G,B);
var Cmin = Math.min(R,G,B);
var delta = Cmax - Cmin;
var L = (Cmax + Cmin) / 2;
var S = 0;
var H = 0;
if (delta !== 0) {
S = delta / (1 - Math.abs((2*L) - 1));
switch (Cmax) {
case R:
H = ((G - B) / delta) % 6;
break;
case G:
H = ((B - R) / delta) + 2;
break;
case B:
H = ((R - G) / delta) + 4;
break;
}
H *= 60;
}
// Convert negative angles from Hue
while (H < 0) {
H += 360;
}
return {
H: H,
S: S,
L: L
};
};
tl;dr
Define everything, before calculations
Switch is bad for performance
Loops are bad
Use Closure Compiler for automated optimizations
MEMOIZE! (this one is not available in the benchmark because it uses only one color at the time)
Compare pairs in Math.max and Math.min (if-else works better for bigger numbers as far as I can see)
Benchmark
The benchmark is quite basic; I'm generating a random RGB color every time and use it for the test suit.
The same color for all implementations of a converter.
I'm currently using a fast computer, so your numbers may differ.
At this point it is hard to optimize further, because performance differs, depending on the data.
Optimisations
Define the object for the result value at the very beginning. Allocating memory for the objects upfront somehow improves the performance.
var res = {
H: 0,
S: 0,
L: L
}
// ...
return res;
Not using switch will yield easy performance improvement.
if (delta !== 0) {
S = delta / (1 - Math.abs((2 * L) - 1));
if (Cmax === R) {
H = ((G - B) / delta) % 6;
} else if (Cmax === G) {
H = ((B - R) / delta) + 2;
} else if (Cmax === B) {
H = ((R - G) / delta) + 4;
}
H *= 60;
}
While loop is easily removable by:
if (H < 0) {
remainder = H % 360;
if (remainder !== 0) {
H = remainder + 360;
}
}
I have also applied Closure Compiler to remove redundant operations inside of the code.
You should memoize the results!
Consider refactoring the function to use three arguments so it's possible to have a multi-dimensional hash-map for memozing cache. Alternatively you can try using WeakMap, but the performance of this solution is unknown.
See the article Faster JavaScript Memoization For Improved Application Performance
Results
Node.js
The best one is rgbToHslOptimizedClosure.
node -v
v6.5.0
rgbToHsl x 16,468,872 ops/sec ±1.64% (85 runs sampled)
rgbToHsl_ x 15,795,460 ops/sec ±1.28% (84 runs sampled)
rgbToHslIfElse x 16,091,606 ops/sec ±1.41% (85 runs sampled)
rgbToHslOptimized x 22,147,449 ops/sec ±1.96% (81 runs sampled)
rgbToHslOptimizedClosure x 46,493,753 ops/sec ±1.55% (85 runs sampled)
rgbToHslOptimizedIfElse x 21,825,646 ops/sec ±2.93% (85 runs sampled)
rgbToHslOptimizedClosureIfElse x 38,346,283 ops/sec ±9.02% (73 runs sampled)
rgbToHslOptimizedIfElseConstant x 30,461,643 ops/sec ±2.68% (81 runs sampled)
rgbToHslOptimizedIfElseConstantClosure x 40,625,530 ops/sec ±2.70% (73 runs sampled)
Fastest is rgbToHslOptimizedClosure
Slowest is rgbToHsl_
Browser
Chrome Version 55.0.2883.95 (64-bit)
rgbToHsl x 18,456,955 ops/sec ±0.78% (62 runs sampled)
rgbToHsl_ x 16,629,042 ops/sec ±2.34% (63 runs sampled)
rgbToHslIfElse x 17,177,059 ops/sec ±3.85% (59 runs sampled)
rgbToHslOptimized x 27,552,325 ops/sec ±0.95% (62 runs sampled)
rgbToHslOptimizedClosure x 47,659,771 ops/sec ±3.24% (47 runs sampled)
rgbToHslOptimizedIfElse x 26,033,751 ops/sec ±2.63% (61 runs sampled)
rgbToHslOptimizedClosureIfElse x 43,430,875 ops/sec ±3.55% (59 runs sampled)
rgbToHslOptimizedIfElseConstant x 33,696,558 ops/sec ±3.97% (58 runs sampled)
rgbToHslOptimizedIfElseConstantClosure x 44,529,209 ops/sec ±3.56% (60 runs sampled)
Fastest is rgbToHslOptimizedClosure
Slowest is rgbToHsl_
Run the benchmark yourself
Note, that browser will freeze for a moment.
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
var RGB = { R: getRandomInt(0, 255), G: getRandomInt(0, 255), B: getRandomInt(0, 255) }
// http://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c
/**
* Converts an RGB color value to HSL. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes r, g, and b are contained in the set [0, 255] and
* returns h, s, and l in the set [0, 1].
*
* #param {number} r The red color value
* #param {number} g The green color value
* #param {number} b The blue color value
* #return {Array} The HSL representation
*/
function rgbToHsl_(r, g, b) {
r /= 255, g /= 255, b /= 255;
var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
if (max == min) {
h = s = 0; // achromatic
} else {
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
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, s, l ];
}
function rgbToHsl(rgb) {
var R = rgb.R / 255;
var G = rgb.G / 255;
var B = rgb.B / 255;
var Cmax = Math.max(R, G, B);
var Cmin = Math.min(R, G, B);
var delta = Cmax - Cmin;
var L = (Cmax + Cmin) / 2;
var S = 0;
var H = 0;
if (delta !== 0) {
S = delta / (1 - Math.abs((2 * L) - 1));
switch (Cmax) {
case R:
H = ((G - B) / delta) % 6;
break;
case G:
H = ((B - R) / delta) + 2;
break;
case B:
H = ((R - G) / delta) + 4;
break;
}
H *= 60;
}
// Convert negative angles from Hue
while (H < 0) {
H += 360;
}
return {
H: H,
S: S,
L: L
};
};
function rgbToHslIfElse(rgb) {
var R = rgb.R / 255;
var G = rgb.G / 255;
var B = rgb.B / 255;
var Cmax = Math.max(R, G, B);
var Cmin = Math.min(R, G, B);
var delta = Cmax - Cmin;
var L = (Cmax + Cmin) / 2;
var S = 0;
var H = 0;
if (delta !== 0) {
S = delta / (1 - Math.abs((2 * L) - 1));
if (Cmax === R) {
H = ((G - B) / delta) % 6;
} else if (Cmax === G) {
H = ((B - R) / delta) + 2;
} else if (Cmax === B) {
H = ((R - G) / delta) + 4;
}
H *= 60;
}
// Convert negative angles from Hue
while (H < 0) {
H += 360;
}
return {
H: H,
S: S,
L: L
};
};
function rgbToHslOptimized(rgb) {
var R = rgb.R / 255;
var G = rgb.G / 255;
var B = rgb.B / 255;
var Cmax = Math.max(Math.max(R, G), B);
var Cmin = Math.min(Math.min(R, G), B);
var delta = Cmax - Cmin;
var S = 0;
var H = 0;
var L = (Cmax + Cmin) / 2;
var res = {
H: 0,
S: 0,
L: L
}
var remainder = 0;
if (delta !== 0) {
S = delta / (1 - Math.abs((2 * L) - 1));
switch (Cmax) {
case R:
H = ((G - B) / delta) % 6;
break;
case G:
H = ((B - R) / delta) + 2;
break;
case B:
H = ((R - G) / delta) + 4;
break;
}
H *= 60;
}
if (H < 0) {
remainder = H % 360;
if (remainder !== 0) {
H = remainder + 360;
}
}
res.H = H;
res.S = S;
return res;
}
function rgbToHslOptimizedIfElse(rgb) {
var R = rgb.R / 255;
var G = rgb.G / 255;
var B = rgb.B / 255;
var Cmax = Math.max(Math.max(R, G), B);
var Cmin = Math.min(Math.min(R, G), B);
var delta = Cmax - Cmin;
var S = 0;
var H = 0;
var L = (Cmax + Cmin) / 2;
var res = {
H: 0,
S: 0,
L: L
}
var remainder = 0;
if (delta !== 0) {
S = delta / (1 - Math.abs((2 * L) - 1));
if (Cmax === R) {
H = ((G - B) / delta) % 6;
} else if (Cmax === G) {
H = ((B - R) / delta) + 2;
} else if (Cmax === B) {
H = ((R - G) / delta) + 4;
}
H *= 60;
}
if (H < 0) {
remainder = H % 360;
if (remainder !== 0) {
H = remainder + 360;
}
}
res.H = H;
res.S = S;
return res;
}
function rgbToHslOptimizedIfElseConstant(rgb) {
var R = rgb.R * 0.00392156862745;
var G = rgb.G * 0.00392156862745;
var B = rgb.B * 0.00392156862745;
var Cmax = Math.max(Math.max(R, G), B);
var Cmin = Math.min(Math.min(R, G), B);
var delta = Cmax - Cmin;
var S = 0;
var H = 0;
var L = (Cmax + Cmin) * 0.5;
var res = {
H: 0,
S: 0,
L: L
}
var remainder = 0;
if (delta !== 0) {
S = delta / (1 - Math.abs((2 * L) - 1));
if (Cmax === R) {
H = ((G - B) / delta) % 6;
} else if (Cmax === G) {
H = ((B - R) / delta) + 2;
} else if (Cmax === B) {
H = ((R - G) / delta) + 4;
}
H *= 60;
}
if (H < 0) {
remainder = H % 360;
if (remainder !== 0) {
H = remainder + 360;
}
}
res.H = H;
res.S = S;
return res;
}
function rgbToHslOptimizedIfElseConstantClosure(c) {
var a = .00392156862745 * c.h, e = .00392156862745 * c.f, f = .00392156862745 * c.c, g = Math.max(Math.max(a, e), f), d = Math.min(Math.min(a, e), f), h = g - d, b = c = 0, k = (g + d) / 2, d = {
a: 0,
b: 0,
g: k
};
0 !== h && (c = h / (1 - Math.abs(2 * k - 1)), g === a ? b = (e - f) / h % 6 : g === e ? b = (f - a) / h + 2 : g === f && (b = (a - e) / h + 4), b *= 60);
0 > b && (a = b % 360, 0 !== a && (b = a + 360));
d.a = b;
d.b = c;
return d;
};
function rgbToHslOptimizedClosure(c) {
var a = c.f / 255, e = c.b / 255, f = c.a / 255, k = Math.max(Math.max(a, e), f), d = Math.min(Math.min(a, e), f), g = k - d, b = c = 0, l = (k + d) / 2, d = {
c: 0,
g: 0,
h: l
};
if (0 !== g) {
c = g / (1 - Math.abs(2 * l - 1));
switch (k) {
case a:
b = (e - f) / g % 6;
break;
case e:
b = (f - a) / g + 2;
break;
case f:
b = (a - e) / g + 4;
}
b *= 60;
}
0 > b && (a = b % 360, 0 !== a && (b = a + 360));
d.c = b;
d.g = c;
return d;
}
function rgbToHslOptimizedClosureIfElse(c) {
var a = c.f / 255, e = c.b / 255, f = c.a / 255, g = Math.max(Math.max(a, e), f), d = Math.min(Math.min(a, e), f), h = g - d, b = c = 0, l = (g + d) / 2, d = {
c: 0,
g: 0,
h: l
};
0 !== h && (c = h / (1 - Math.abs(2 * l - 1)), g === a ? b = (e - f) / h % 6 : g === e ? b = (f - a) / h + 2 : g === f && (b = (a - e) / h + 4), b *= 60);
0 > b && (a = b % 360, 0 !== a && (b = a + 360));
d.c = b;
d.g = c;
return d;
}
new Benchmark.Suite()
.add('rgbToHsl', function () {
rgbToHsl(RGB);
})
.add('rgbToHsl_', function () {
rgbToHsl_(RGB.R, RGB.G, RGB.B);
})
.add('rgbToHslIfElse', function () {
rgbToHslIfElse(RGB);
})
.add('rgbToHslOptimized', function () {
rgbToHslOptimized(RGB);
})
.add('rgbToHslOptimizedClosure', function () {
rgbToHslOptimizedClosure(RGB);
})
.add('rgbToHslOptimizedIfElse', function () {
rgbToHslOptimizedIfElse(RGB);
})
.add('rgbToHslOptimizedClosureIfElse', function () {
rgbToHslOptimizedClosureIfElse(RGB);
})
.add('rgbToHslOptimizedIfElseConstant', function () {
rgbToHslOptimizedIfElseConstant(RGB);
})
.add('rgbToHslOptimizedIfElseConstantClosure', function () {
rgbToHslOptimizedIfElseConstantClosure(RGB);
})
// add listeners
.on('cycle', function (event) {
console.log(String(event.target));
})
.on('complete', function () {
console.log('Fastest is ' + this.filter('fastest').map('name'));
console.log('Slowest is ' + this.filter('slowest').map('name'));
})
// run async
.run({ 'async': false });
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
<script src="https://cdn.rawgit.com/bestiejs/benchmark.js/master/benchmark.js"></script>
Avoid the divisions by all means. You can probably eliminate a few by rescaling the relevant variables and constants.
You can also avoid divisions by using a lookup-table of inverses.
I don't think that the switch case is very efficient. I would advise to replace the max/min/switch by a single discussion using a triply nested if where you compare the RGB components, ending in the 6 possible orderings and applying ad-hoc processing to each.
Use a rough approximation instead:
third = Math.PI * 2 / 3;
ctx.fillStyle = 'rgb('+ [
127 + 127 * Math.cos(time - third),
127 + 127 * Math.cos(time),
127 + 127 * Math.cos(time + third)
] +')';
Based on:
http://www.p01.org/artjs_at_ffconf/
http://www.p01.org/artjs_at_ffconf/talk.html

JavaScript Calculate brighter colour

I have a colour value in JS as a string
#ff0000
How would I go about programatically calculating a brighter/lighter version of this colour, for example #ff4848, and be able to calculate the brightness via a percentage, e.g.
increase_brightness('#ff0000', 50); // would make it 50% brighter
function increase_brightness(hex, percent){
// strip the leading # if it's there
hex = hex.replace(/^\s*#|\s*$/g, '');
// convert 3 char codes --> 6, e.g. `E0F` --> `EE00FF`
if(hex.length == 3){
hex = hex.replace(/(.)/g, '$1$1');
}
var r = parseInt(hex.substr(0, 2), 16),
g = parseInt(hex.substr(2, 2), 16),
b = parseInt(hex.substr(4, 2), 16);
return '#' +
((0|(1<<8) + r + (256 - r) * percent / 100).toString(16)).substr(1) +
((0|(1<<8) + g + (256 - g) * percent / 100).toString(16)).substr(1) +
((0|(1<<8) + b + (256 - b) * percent / 100).toString(16)).substr(1);
}
/**
* ('#000000', 50) --> #808080
* ('#EEEEEE', 25) --> #F2F2F2
* ('EEE , 25) --> #F2F2F2
**/
Update
#zyklus's answer is simpler and has the same effect. Please refer to this answer only if you are interested in converting between RGB and HSL.
To set the brightness of RGB:
Convert RGB to HSL
Set the brightness of HSL
Convert back from HSL to RGB
This link used to have code to convert RGB to HSL and reverse:
http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript
/**
* Converts an RGB color value to HSL. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes r, g, and b are contained in the set [0, 255] and
* returns h, s, and l in the set [0, 1].
*
* #param Number r The red color value
* #param Number g The green color value
* #param Number b The blue color value
* #return Array The HSL representation
*/
function rgbToHsl(r, g, b){
r /= 255, g /= 255, b /= 255;
var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
if(max == min){
h = s = 0; // achromatic
}else{
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
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, s, l];
}
/**
* Converts an HSL color value to RGB. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes h, s, and l are contained in the set [0, 1] and
* returns r, g, and b in the set [0, 255].
*
* #param Number h The hue
* #param Number s The saturation
* #param Number l The lightness
* #return Array The RGB representation
*/
function hslToRgb(h, s, l){
var r, g, b;
if(s == 0){
r = g = b = l; // achromatic
}else{
function hue2rgb(p, q, t){
if(t < 0) t += 1;
if(t > 1) t -= 1;
if(t < 1/6) return p + (q - p) * 6 * t;
if(t < 1/2) return q;
if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
}
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
return [r * 255, g * 255, b * 255];
}
I made some example with it. Check this link: http://jsfiddle.net/sangdol/euSLy/4/
And this is the increase_brightness() function:
function increase_brightness(rgbcode, percent) {
var r = parseInt(rgbcode.slice(1, 3), 16),
g = parseInt(rgbcode.slice(3, 5), 16),
b = parseInt(rgbcode.slice(5, 7), 16),
HSL = rgbToHsl(r, g, b),
newBrightness = HSL[2] + HSL[2] * (percent / 100),
RGB;
RGB = hslToRgb(HSL[0], HSL[1], newBrightness);
rgbcode = '#'
+ convertToTwoDigitHexCodeFromDecimal(RGB[0])
+ convertToTwoDigitHexCodeFromDecimal(RGB[1])
+ convertToTwoDigitHexCodeFromDecimal(RGB[2]);
return rgbcode;
}
function convertToTwoDigitHexCodeFromDecimal(decimal){
var code = Math.round(decimal).toString(16);
(code.length > 1) || (code = '0' + code);
return code;
}
You can pass a negative value as a percent argument to make it darken.
In case anyone needs it, I converted the color brightness JavaScript code to ASP / VBScript for a project and thought I would share it with you:
'::Color Brightness (0-100)
'ex. ColorBrightness("#FF0000",25) 'Darker
'ex. ColorBrightness("#FF0000",50) 'Mid
'ex. ColorBrightness("#FF0000",75) 'Lighter
Function ColorBrightness(strRGB,intBrite)
strRGB = Replace(strRGB,"#","")
r = CInt("&h" & Mid(strRGB,1,2))
g = CInt("&h" & Mid(strRGB,3,2))
b = CInt("&h" & Mid(strRGB,5,2))
arrHSL = RGBtoHSL(r, g, b)
dblOrigBrite = CDbl(arrHSL(2) * 100)
arrRGB = HSLtoRGB(arrHSL(0), arrHSL(1), intBrite/100)
newRGB = "#" & HEXtoDEC(arrRGB(0)) & HEXtoDEC(arrRGB(1)) & HEXtoDEC(arrRGB(2))
ColorBrightness = newRGB
End Function
'::RGB to HSL Function
Function RGBtoHSL(r,g,b)
r = CDbl(r/255)
g = CDbl(g/255)
b = CDbl(b/255)
max = CDbl(MaxCalc(r & "," & g & "," & b))
min = CDbl(MinCalc(r & "," & g & "," & b))
h = CDbl((max + min) / 2)
s = CDbl((max + min) / 2)
l = CDbl((max + min) / 2)
If max = min Then
h = 0
s = 0
Else
d = max - min
s = IIf(l > 0.5, d / (2 - max - min), d / (max + min))
Select Case CStr(max)
Case CStr(r)
h = (g - b) / d + (IIf(g < b, 6, 0))
Case CStr(g)
h = (b - r) / d + 2
Case CStr(b)
h = (r - g) / d + 4
End Select
h = h / 6
End If
RGBtoHSL = Split(h & "," & s & "," & l, ",")
End Function
'::HSL to RGB Function
Function HSLtoRGB(h,s,l)
If s = 0 Then
r = l
g = l
b = l
Else
q = IIf(l < 0.5, l * (1 + s), l + s - l * s)
p = 2 * l - q
r = HUEtoRGB(p, q, h + 1/3)
g = HUEtoRGB(p, q, h)
b = HUEtoRGB(p, q, h - 1/3)
End If
HSLtoRGB = Split(r * 255 & "," & g * 255 & "," & b * 255, ",")
End Function
'::Hue to RGB Function
Function HUEtoRGB(p,q,t)
If CDbl(t) < 0 Then t = t + 1
If CDbl(t) > 1 Then t = t - 1
If CDbl(t) < (1/6) Then
HUEtoRGB = p + (q - p) * 6 * t
Exit Function
End If
If CDbl(t) < (1/2) Then
HUEtoRGB = q
Exit Function
End If
If CDbl(t) < (2/3) Then
HUEtoRGB = p + (q - p) * (2/3 - t) * 6
Exit Function
End If
HUEtoRGB = p
End Function
'::Hex to Decimal Function
Function HEXtoDEC(d)
h = Hex(Round(d,0))
h = Right(String(2,"0") & h,2)
HEXtoDEC = h
End Function
'::Max Function
Function MaxCalc(valList)
valList = Split(valList,",")
b = 0
For v = 0 To UBound(valList)
a = valList(v)
If CDbl(a) > CDbl(b) Then b = a
Next
MaxCalc = b
End Function
'::Min Function
Function MinCalc(valList)
valList = Split(valList,",")
For v = 0 To UBound(valList)
a = valList(v)
If b = "" Then b = a
If CDbl(a) < CDbl(b) AND b <> "" Then b = a
Next
MinCalc = b
End Function
'::IIf Emulation Function
Function IIf(condition,conTrue,conFalse)
If (condition) Then
IIf = conTrue
Else
IIf = conFalse
End If
End Function
That way you won't need any conversion of the source color.
check out this fiddle : https://jsfiddle.net/4c47otou/
increase_brightness = function(color,percent){
var ctx = document.createElement('canvas').getContext('2d');
ctx.fillStyle = color;
ctx.fillRect(0,0,1,1);
var color = ctx.getImageData(0,0,1,1);
var r = color.data[0] + Math.floor( percent / 100 * 255 );
var g = color.data[1] + Math.floor( percent / 100 * 255 );
var b = color.data[2] + Math.floor( percent / 100 * 255 );
return 'rgb('+r+','+g+','+b+')';
}
Example usage :
increase_brightness('#0000ff',20);
increase_brightness('khaki',20);
increase_brightness('rgb(12, 7, 54)',20);
// color is a hex color like #aaaaaa and percent is a float, 1.00=100%
// increasing a color by 50% means a percent value of 1.5
function brighten(color, percent) {
var r=parseInt(color.substr(1,2),16);
var g=parseInt(color.substr(3,2),16);
var b=parseInt(color.substr(5,2),16);
return '#'+
Math.min(255,Math.floor(r*percent)).toString(16)+
Math.min(255,Math.floor(g*percent)).toString(16)+
Math.min(255,Math.floor(b*percent)).toString(16);
}
Live sample: http://jsfiddle.net/emM55/
Here is the increaseBrightness function with the RGB->HSL->RGB method. "amount" should be in percent.
HSL<->RGB conversion functions taken from http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript
function increaseBrightness( color, amount ) {
var r = parseInt(color.substr(1, 2), 16);
var g = parseInt(color.substr(3, 2), 16);
var b = parseInt(color.substr(5, 2), 16);
hsl = rgbToHsl( r, g, b );
hsl.l += hsl.l + (amount / 100);
if( hsl.l > 1 ) hsl.l = 1;
rgb = hslToRgb( hsl.h, hsl.s, hsl.l );
var v = rgb.b | (rgb.g << 8) | (rgb.r << 16);
return '#' + v.toString(16);
}
function rgbToHsl(r, g, b){
r /= 255, g /= 255, b /= 255;
var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
if(max == min){
h = s = 0; // achromatic
}else{
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
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, 'l':l};
}
function hslToRgb(h, s, l){
var r, g, b;
if(s == 0){
r = g = b = l; // achromatic
}else{
function hue2rgb(p, q, t){
if(t < 0) t += 1;
if(t > 1) t -= 1;
if(t < 1/6) return p + (q - p) * 6 * t;
if(t < 1/2) return q;
if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
}
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
return { 'r':r * 255, 'g':g * 255, 'b':b * 255 };
}
I found a variation of Sanghyun Lee's reply generates the best result.
Convert RGB to HSL
Set the brightness of HSL
Convert back from HSLto RGB
The difference/variation is how you increase/decrease the brightness.
newBrightness = HSL[2] + HSL[2] * (percent / 100) // Original code
Instead of applying a percentage on the current brightness, it works better if it is treated as absolute increment/decrement. Since the luminosity range is 0 to 1, the percent can be applied on the whole range (1 - 0) * percent/100.
newBrightness = HSL[2] + (percent / 100);
newBrightness = Math.max(0, Math.min(1, newBrightness));
Another nice property of this approach is increment & decrement negate each other.
Image below shows darker and lighter colors with 5% increment. Note, how the palette is reasonably smooth and often ends with black and white.
Palette with original approach - gets stuck at certain colors.
I know this an old question, but I found no answer that simply manipulates css hsl color. I found the old answers here to be too complex and slow, even producing poor results, so a different approach seems warranted. The following alternative is much more performant and less complex.
Of course, this answer requires you to use hsl colors throughout your app, otherwise you still have to do a bunch of conversions! Though, if you need to manipulate brightness eg in a game loop, you should be using hsl values anyway as they are much better suited for programmatic manipulation. The only drawback with hsl from rgb as far as I can tell, is that it's harder to "read" what hue you're seeing like you can with rgb strings.
function toHslArray(hslCss) {
let sep = hslCss.indexOf(",") > -1 ? "," : " "
return hslCss.substr(4).split(")")[0].split(sep)
}
function adjustHslBrightness(color, percent) {
let hsl = toHslArray(color)
return "hsl(" + hsl[0] + "," + hsl[1] + ", " + (percent + "%") + ")"
}
let hsl = "hsl(200, 40%, 40%)"
let hsl2 = adjustHslBrightness(hsl, 80)
function brighten(color, c) {
const calc = (sub1,sub2)=> Math.min(255,Math.floor(parseInt(color.substr(sub1,sub2),16)*c)).toString(16).padStart(2,"0")
return `#${calc(1,2)}${calc(3,2)}${calc(5,2)}`
}
const res = brighten("#23DA4C", .5) // "#116d26"
console.log(res)
What I use:
//hex can be string or number
//rate: 1 keeps the color same. < 1 darken. > 1 lighten.
//to_string: set to true if you want the return value in string
function change_brightness(hex, rate, to_string = false) {
if (typeof hex === 'string') {
hex = hex.replace(/^\s*#|\s*$/g, '');
} else {
hex = hex.toString(16);
}
if (hex.length == 3) {
hex = hex.replace(/(.)/g, '$1$1');
} else {
hex = ("000000" + hex).slice(-6);
}
let r = parseInt(hex.substr(0, 2), 16);
let g = parseInt(hex.substr(2, 2), 16);
let b = parseInt(hex.substr(4, 2), 16);
let h, s, v;
[h, s, v] = rgb2hsv(r, g, b);
v = parseInt(v * rate);
[r, g, b] = hsv2rgb(h, s, v);
hex = ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
if (to_string) return "#" + hex;
return parseInt(hex, 16);
}
function rgb2hsv(r,g,b) {
let v = Math.max(r,g,b), n = v-Math.min(r,g,b);
let h = n && ((v === r) ? (g-b)/n : ((v === g) ? 2+(b-r)/n : 4+(r-g)/n));
return [60*(h<0?h+6:h), v&&n/v, v];
}
function hsv2rgb(h,s,v) {
let f = (n,k=(n+h/60)%6) => v - v*s*Math.max( Math.min(k,4-k,1), 0);
return [f(5),f(3),f(1)];
}
A variant with lodash:
// color('#EBEDF0', 30)
color(hex, percent) {
return '#' + _(hex.replace('#', '')).chunk(2)
.map(v => parseInt(v.join(''), 16))
.map(v => ((0 | (1 << 8) + v + (256 - v) * percent / 100).toString(16))
.substr(1)).join('');
}
First get a quick understanding of hex color codes.
Then it should be pretty easy to break down your color value into RGB, make the adjustments and then return the new color code.

JS function to calculate complementary colour?

Does anybody know, off the top of your heads, a Javascript solution for calculating the complementary colour of a hex value?
There are a number of colour picking suites and palette generators on the web but I haven't seen any that actually calculate the colour dynamically using JS.
A detailed hint or a snippet would be very much appreciated.
Parsed through http://design.geckotribe.com/colorwheel/
// Complement
temprgb={ r: 0, g: 0xff, b: 0xff }; // Cyan
temphsv=RGB2HSV(temprgb);
temphsv.hue=HueShift(temphsv.hue,180.0);
temprgb=HSV2RGB(temphsv);
console.log(temprgb); // Complement is red (0xff, 0, 0)
function RGB2HSV(rgb) {
hsv = new Object();
max=max3(rgb.r,rgb.g,rgb.b);
dif=max-min3(rgb.r,rgb.g,rgb.b);
hsv.saturation=(max==0.0)?0:(100*dif/max);
if (hsv.saturation==0) hsv.hue=0;
else if (rgb.r==max) hsv.hue=60.0*(rgb.g-rgb.b)/dif;
else if (rgb.g==max) hsv.hue=120.0+60.0*(rgb.b-rgb.r)/dif;
else if (rgb.b==max) hsv.hue=240.0+60.0*(rgb.r-rgb.g)/dif;
if (hsv.hue<0.0) hsv.hue+=360.0;
hsv.value=Math.round(max*100/255);
hsv.hue=Math.round(hsv.hue);
hsv.saturation=Math.round(hsv.saturation);
return hsv;
}
// RGB2HSV and HSV2RGB are based on Color Match Remix [http://color.twysted.net/]
// which is based on or copied from ColorMatch 5K [http://colormatch.dk/]
function HSV2RGB(hsv) {
var rgb=new Object();
if (hsv.saturation==0) {
rgb.r=rgb.g=rgb.b=Math.round(hsv.value*2.55);
} else {
hsv.hue/=60;
hsv.saturation/=100;
hsv.value/=100;
i=Math.floor(hsv.hue);
f=hsv.hue-i;
p=hsv.value*(1-hsv.saturation);
q=hsv.value*(1-hsv.saturation*f);
t=hsv.value*(1-hsv.saturation*(1-f));
switch(i) {
case 0: rgb.r=hsv.value; rgb.g=t; rgb.b=p; break;
case 1: rgb.r=q; rgb.g=hsv.value; rgb.b=p; break;
case 2: rgb.r=p; rgb.g=hsv.value; rgb.b=t; break;
case 3: rgb.r=p; rgb.g=q; rgb.b=hsv.value; break;
case 4: rgb.r=t; rgb.g=p; rgb.b=hsv.value; break;
default: rgb.r=hsv.value; rgb.g=p; rgb.b=q;
}
rgb.r=Math.round(rgb.r*255);
rgb.g=Math.round(rgb.g*255);
rgb.b=Math.round(rgb.b*255);
}
return rgb;
}
//Adding HueShift via Jacob (see comments)
function HueShift(h,s) {
h+=s; while (h>=360.0) h-=360.0; while (h<0.0) h+=360.0; return h;
}
//min max via Hairgami_Master (see comments)
function min3(a,b,c) {
return (a<b)?((a<c)?a:c):((b<c)?b:c);
}
function max3(a,b,c) {
return (a>b)?((a>c)?a:c):((b>c)?b:c);
}
I find that taking the bit-wise complement works well, and quickly.
var color = 0x320ae3;
var complement = 0xffffff ^ color;
I'm not sure if it's a perfect complement in the sense of "mixes together to form a 70% grey", however a 70% grey is "pure white" in terms of color timing in film. It occurred to me that XORing the RGB hex out of pure white might be a good first approximation. You could also try a darker grey to see how that works for you.
Again, this is a fast approximation and I make no guarantees that it'll be perfectly accurate.
See https://github.com/alfl/textful/blob/master/app.js#L38 for my implementation.
None of the other functions here worked out the box, so I made this one.
It takes a hex value, converts it to HSL, shifts the hue 180 degrees and converts back to Hex
/* hexToComplimentary : Converts hex value to HSL, shifts
* hue by 180 degrees and then converts hex, giving complimentary color
* as a hex value
* #param [String] hex : hex value
* #return [String] : complimentary color as hex value
*/
function hexToComplimentary(hex){
// Convert hex to rgb
// Credit to Denis http://stackoverflow.com/a/36253499/4939630
var rgb = 'rgb(' + (hex = hex.replace('#', '')).match(new RegExp('(.{' + hex.length/3 + '})', 'g')).map(function(l) { return parseInt(hex.length%2 ? l+l : l, 16); }).join(',') + ')';
// Get array of RGB values
rgb = rgb.replace(/[^\d,]/g, '').split(',');
var r = rgb[0], g = rgb[1], b = rgb[2];
// Convert RGB to HSL
// Adapted from answer by 0x000f http://stackoverflow.com/a/34946092/4939630
r /= 255.0;
g /= 255.0;
b /= 255.0;
var max = Math.max(r, g, b);
var min = Math.min(r, g, b);
var h, s, l = (max + min) / 2.0;
if(max == min) {
h = s = 0; //achromatic
} else {
var d = max - min;
s = (l > 0.5 ? d / (2.0 - max - min) : d / (max + min));
if(max == r && g >= b) {
h = 1.0472 * (g - b) / d ;
} else if(max == r && g < b) {
h = 1.0472 * (g - b) / d + 6.2832;
} else if(max == g) {
h = 1.0472 * (b - r) / d + 2.0944;
} else if(max == b) {
h = 1.0472 * (r - g) / d + 4.1888;
}
}
h = h / 6.2832 * 360.0 + 0;
// Shift hue to opposite side of wheel and convert to [0-1] value
h+= 180;
if (h > 360) { h -= 360; }
h /= 360;
// Convert h s and l values into r g and b values
// Adapted from answer by Mohsen http://stackoverflow.com/a/9493060/4939630
if(s === 0){
r = g = b = l; // achromatic
} else {
var hue2rgb = function hue2rgb(p, q, t){
if(t < 0) t += 1;
if(t > 1) t -= 1;
if(t < 1/6) return p + (q - p) * 6 * t;
if(t < 1/2) return q;
if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
};
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
r = Math.round(r * 255);
g = Math.round(g * 255);
b = Math.round(b * 255);
// Convert r b and g values to hex
rgb = b | (g << 8) | (r << 16);
return "#" + (0x1000000 | rgb).toString(16).substring(1);
}
Rather than reinventing the wheel, I found a library to work with colors.
Tiny Color
This is how you would implement some of the other answers using it.
color1 = tinycolor2('#f00').spin(180).toHexString(); // Hue Shift
color2 = tinycolor2("#f00").complement().toHexString(); // bitwise
Hex and RGB complementry
This is most correct and efficient way to get complementary hex color value
function complementryHexColor(hex){
let r = hex.length == 4 ? parseInt(hex[1] + hex[1], 16) : parseInt(hex.slice(1, 3), 16);
let g = hex.length == 4 ? parseInt(hex[2] + hex[2], 16) : parseInt(hex.slice(3, 5), 16);
let b = hex.length == 4 ? parseInt(hex[3] + hex[3], 16) : parseInt(hex.slice(5), 16);
[r, g, b] = complementryRGBColor(r, g, b);
return '#' + (r < 16 ? '0' + r.toString(16) : r.toString(16)) + (g < 16 ? '0' + g.toString(16) : g.toString(16)) + (b < 16 ? '0' + b.toString(16) : b.toString(16));
}
function complementryRGBColor(r, g, b) {
if (Math.max(r, g, b) == Math.min(r, g, b)) {
return [255 - r, 255 - g, 255 - b];
} else {
r /= 255, g /= 255, b /= 255;
var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
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 = Math.round((h*60) + 180) % 360;
h /= 360;
function hue2rgb(p, q, t) {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1/6) return p + (q - p) * 6 * t;
if (t < 1/2) return q;
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
}
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
return [Math.round(r*255), Math.round(g*255), Math.round(b*255)];
}
}
RGB Complimentary
function componentToHex(c) {
var hex = c.toString(16);
return hex.length == 1 ? "0" + hex : hex;
}
function hexToRgb(hex) {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
function rgbComplimentary(r,g,b){
var hex = "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);
var rgb = 'rgb(' + (hex = hex.replace('#', '')).match(new RegExp('(.{' + hex.length/3 + '})', 'g')).map(function(l) { return parseInt(hex.length%2 ? l+l : l, 16); }).join(',') + ')';
// Get array of RGB values
rgb = rgb.replace(/[^\d,]/g, '').split(',');
var r = rgb[0]/255.0, g = rgb[1]/255.0, b = rgb[2]/255.0;
var max = Math.max(r, g, b);
var min = Math.min(r, g, b);
var h, s, l = (max + min) / 2.0;
if(max == min) {
h = s = 0; //achromatic
} else {
var d = max - min;
s = (l > 0.5 ? d / (2.0 - max - min) : d / (max + min));
if(max == r && g >= b) {
h = 1.0472 * (g - b) / d ;
} else if(max == r && g < b) {
h = 1.0472 * (g - b) / d + 6.2832;
} else if(max == g) {
h = 1.0472 * (b - r) / d + 2.0944;
} else if(max == b) {
h = 1.0472 * (r - g) / d + 4.1888;
}
}
h = h / 6.2832 * 360.0 + 0;
// Shift hue to opposite side of wheel and convert to [0-1] value
h+= 180;
if (h > 360) { h -= 360; }
h /= 360;
// Convert h s and l values into r g and b values
// Adapted from answer by Mohsen http://stackoverflow.com/a/9493060/4939630
if(s === 0){
r = g = b = l; // achromatic
} else {
var hue2rgb = function hue2rgb(p, q, t){
if(t < 0) t += 1;
if(t > 1) t -= 1;
if(t < 1/6) return p + (q - p) * 6 * t;
if(t < 1/2) return q;
if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
};
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
r = Math.round(r * 255);
g = Math.round(g * 255);
b = Math.round(b * 255);
// Convert r b and g values to hex
rgb = b | (g << 8) | (r << 16);
return hexToRgb("#" + (0x1000000 | rgb).toString(16).substring(1));
}
console.log(rgbComplimentary(242, 211, 215));
HEX Complimentary
function hexComplimentary(hex){
var rgb = 'rgb(' + (hex = hex.replace('#', '')).match(new RegExp('(.{' + hex.length/3 + '})', 'g')).map(function(l) { return parseInt(hex.length%2 ? l+l : l, 16); }).join(',') + ')';
// Get array of RGB values
rgb = rgb.replace(/[^\d,]/g, '').split(',');
var r = rgb[0]/255.0, g = rgb[1]/255.0, b = rgb[2]/255.0;
var max = Math.max(r, g, b);
var min = Math.min(r, g, b);
var h, s, l = (max + min) / 2.0;
if(max == min) {
h = s = 0; //achromatic
} else {
var d = max - min;
s = (l > 0.5 ? d / (2.0 - max - min) : d / (max + min));
if(max == r && g >= b) {
h = 1.0472 * (g - b) / d ;
} else if(max == r && g < b) {
h = 1.0472 * (g - b) / d + 6.2832;
} else if(max == g) {
h = 1.0472 * (b - r) / d + 2.0944;
} else if(max == b) {
h = 1.0472 * (r - g) / d + 4.1888;
}
}
h = h / 6.2832 * 360.0 + 0;
// Shift hue to opposite side of wheel and convert to [0-1] value
h+= 180;
if (h > 360) { h -= 360; }
h /= 360;
if(s === 0){
r = g = b = l; // achromatic
} else {
var hue2rgb = function hue2rgb(p, q, t){
if(t < 0) t += 1;
if(t > 1) t -= 1;
if(t < 1/6) return p + (q - p) * 6 * t;
if(t < 1/2) return q;
if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
};
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
r = Math.round(r * 255);
g = Math.round(g * 255);
b = Math.round(b * 255);
// Convert r b and g values to hex
rgb = b | (g << 8) | (r << 16);
return "#" + (0x1000000 | rgb).toString(16).substring(1);
}
console.log(hexComplimentary("#ff5a5a"));
Source: Updated https://stackoverflow.com/a/37657940/6569224 answer

Categories