0

I've got what I think is quite an interesting problem that needs an elegant solution...

I have an RGB value, for example 205,50,63.

I am trying to simulate the colour of an RGB LED on a webpage as if it were REAL-LIFE LIGHT.

For example, the RGB colour 255,0,0 would display as red, both on the LED and on the webpage.

Likewise, the RGB colour 255,255,255 would display as white, both on the LED and on the webpage.

BUT the RGB colour 0,0,0 would display as off on the LED and would be displayed as black on the webpage.

What I am trying to achieve is that both 0,0,0 and 255,255,255 display as white. As if the dimmer the LED is, the whiter it gets.

Ive been trying to apply a proportional algorithm to the values and then layer <div> over the top of each other with no luck. Any thoughts?

liquide
  • 1,346
  • 3
  • 20
  • 28
GeoReb
  • 21
  • 1
  • 10
  • Please post the code you have so that we can see and point out what is wrong with your attempt. – Bergi Dec 04 '16 at 18:24
  • Are you after a visual FX or a mathematical model? The best way to simulate a real object is to set some up and take photos or video, use a colour picker and zoom in see how the gradient spreads out. Study how the pixels colours change as the LEDs change brightness and hue. It will help you get the desired FX – Blindman67 Dec 04 '16 at 18:34

3 Answers3

1

I'm not sure what the case you're imagining is, but reading your desired output, what is wrong with simply scaling up so the maximum value becomes 255?

function scaleUp(rgb) {
    let max = Math.max(rgb.r, rgb.g, rgb.b);
    if (!max) { // 0 or NaN
        return {r: 255, g: 255, b: 255};
    }
    let factor = 255 / max;
    return {
        r: factor * rgb.r,
        g: factor * rgb.g,
        b: factor * rgb.b,
    };
}

So you would get results like

scaleUp({r: 0, g: 0, b: 0}); // {r: 255, g: 255, b: 255}
scaleUp({r: 255, g: 0, b: 0}); // {r: 255, g: 0, b: 0}
scaleUp({r: 50, g: 80, b: 66}); // {r: 159.375, g: 255, b: 210.375}

Notice this collapses all {x, 0, 0} to {255, 0, 0}, meaning {1, 0, 0} is vastly different to {1, 1, 1}. If this is not desirable you'd need to consider special handling of such cases


More RGB hints; you get smoother "more natural" light transitions etc if you square and root around your op, e.g. rather than x + y, do sqrt(x*x + y*y)

This leads to a different idea of how to solve the problem; adding white and scaling down

function scaleDown(rgb) {
    let whiteAdded = {
        r: Math.sqrt(255 * 255 + rgb.r * rgb.r),
        g: Math.sqrt(255 * 255 + rgb.g * rgb.g),
        b: Math.sqrt(255 * 255 + rgb.b * rgb.b)
    };
    return scaleUp(whiteAdded);
}

This time

scaleDown({r: 0, g: 0, b: 0}); // {r: 255, g: 255, b: 255}
scaleDown({r: 255, g: 0, b: 0}); // {r: 255, g: 180.3122292025696, b: 180.3122292025696}
scaleDown({r: 50, g: 80, b: 66}); // {r: 247.94043129928136, g: 255, b: 251.32479296236951}

and have less of a jump around edge points, e.g.

scaleDown({r: 1, g: 0, b: 0}); // {r: 255, g: 254.99803923830171, b: 254.99803923830171}

Finally, notice this maps rgb onto the the range 180..255, so you could transform this to 0..255 if you want to preserve your "true red"s etc

function solution(rgb) {
    let high = scaleDown(rgb);
    return {
        r: 3.4 * (high.r - 180),
        g: 3.4 * (high.g - 180),
        b: 3.4 * (high.b - 180),
    };
}

So

solution({r: 255, g: 0, b: 0}); // {r: 255, g: 1.0615792887366295, b: 1.0615792887366295}
solution({r: 1, g: 0, b: 0}); // {r: 255, g: 254.99333341022583, b: 254.99333341022583}
solution({r: 50, g: 80, b: 66}); // {r: 230.9974664175566, g: 255, b: 242.50429607205635}
Paul S.
  • 64,864
  • 9
  • 122
  • 138
  • I understand what you've done here, brilliant, thanks! But what about `scaleUp({r: 10, g: 0, b: 0});`? I would like the output colour to be mostly white with a slight hint of red as apposed to solid red. Is that possible? @Paul S. – GeoReb Dec 04 '16 at 18:15
  • 1
    I've just tested your updated solution and it's spot on! Thankyou @Paul S. – GeoReb Dec 04 '16 at 19:31
1

I think you should consider HSV color space for this problem. Assuming you have a hue set to red (354° in your example) you can manipulate saturation and value to get desired result. The idea is to reduce saturation along with value so when dimming the light you loose the saturation. In the edge case when saturation gets to 0%, value is also set to 100% yielding white light.

Take a look at images down below. Please note H, S, V values.

You start with the base case:

Initial state

Then you dim:

Transition

And finally get desaturated color:

Saturation 0%

In the terms of code it would be

dim is in range 0.0 to 1.0
hsv(dim) -> {
    saturation = baseSaturation * (1 - dim)
    value = baseValue + (1 - baseValue) * dim
}
hue is constant
Przemysław Zalewski
  • 3,836
  • 1
  • 23
  • 23
  • If HSV is the solution you're looking for, also take a look at [another of my answers](http://stackoverflow.com/a/17243070/1615483) where I explain how to convert between RGB and HSV, etc. http://stackoverflow.com/a/17243070/1615483 – Paul S. Dec 04 '16 at 18:13
  • Excellent reponse, thankyou so much! I like your approach, however, how would i know how much to 'dim' my RGB value, to get the desired result? For example, I'd RBG value `10,0,0` to be mostly white with a hint of red, but that needs to be calculated automatically? Is that possible, @Ciunkos ? – GeoReb Dec 04 '16 at 18:25
  • Calculate HSV of RGB 10,0,0 first by using @Paul S. code. It yields mostly black (HSV 0, 100%, 3%). By using the calculation, dim it 100% which will yield saturation 100% and value 97% which is white with some red tint. – Przemysław Zalewski Dec 04 '16 at 18:33
  • In order to get correct dim value I think you should consider some energy conservation function. For example the light should emit constant energy, even when dimmed (lost saturation). So you need to calculate function `energy baseCase = energy dimmedCase` and then you can extract dim value that satisfied this equation. – Przemysław Zalewski Dec 04 '16 at 18:42
0

As there is already and answer I will not go into too much detail.

The demo simulates Multi colour clear LED

Colour is created by overlapping 3+ images for RGB using composite operation "lighten". This is an additive process. There is also a white channel that adds white light to the whole LED. The RGB channels have additional gain added to even out the effect, and when blue is high red is driven down.

When there is no light just the image of the LED is shown. There is also a contrast image draw befor and after the 4 colour channels RGB & white.

With some better source images (this only uses one per channel, should have 2-3) a very realist FX can be created. Note that the surrounding environment will also affect the look.

// Load media (set of images for led)
var mediaReady = false;
var leds = new Image();
leds.src = "https://i.stack.imgur.com/tT1YV.png";
leds.onload = function () {
    mediaReady = true;
}
var canLed = document.createElement("canvas");
canLed.width = 31;
canLed.height = 47;
var ctxLed = canLed.getContext("2d")
    // display canvas
    var canvas = document.createElement("canvas");
canvas.width = 31 * 20;
canvas.height = 47;
var ctx = canvas.getContext("2d");
var div = document.createElement("div");
div.style.background = "#999";
div.style.position = "absolute";
div.style.top = div.style.left = "0px";
div.style.width = div.style.height = "100%";
var div1 = document.createElement("div");
div1.style.fontFamily="Arial";
div1.style.fontSize = "28px";
div1.textContent ="Simple LED using layered RGB & white images.";
div.appendChild(div1);
div.appendChild(canvas);
document.body.appendChild(div);

const cPow = [1 / 7, 1 / 1, 1 / 3, 1 / 5]; // output gain for g,b,r,w (w is white)
var colourCurrent = {
    r : 0,
    g : 0,
    b : 0,
    w : 0
}
function easeInOut(x, pow) { // ease function
    x = x < 0 ? 0 : x > 1 ? 1 : x;
    xx = Math.pow(x, pow);
    return xx / (xx + Math.pow(1 - x, pow));
}
var FX = { // composite operations
    light : "lighter",
    norm : "source-over",
    tone : "screen",
    block : "color-dodge",
    hard : "hard-light",

}
function randB(min, max) { // random bell
    if (max === undefined) {
        max = min;
        min = 0;
    }
    var r = (Math.random() + Math.random() + Math.random() + Math.random() + Math.random()) / 5;
    return (max - min) * r + min;
}
function randL(min, max) { // linear
    if (max === undefined) {
        max = min;
        min = 0;
    }
    var r = Math.random();
    return (max - min) * r + min;
}

function drawSprite(index, alpha, fx) {

    ctxLed.globalAlpha = alpha;
    ctxLed.globalCompositeOperation = fx;
    ctxLed.drawImage(leds, index * 32, 0, 31, 47, 0, 0, 31, 47);
}
var gbrw = [0, 0, 0, 0];
// Draws a LED using colours in col (sorry had images in wrong order so colour channels are green, blue, red and white
function drawLed(col) {
    // get normalised values for each channel
    gbrw[0] = col.g / 255;
    gbrw[1] = col.b / 255;
    gbrw[2] = col.r / 255;
    gbrw[3] = col.w / 255;
    gbrw[2] *= 1 - gbrw[1]; // suppress red if blue high
    var total = (col.g / 255) * cPow[0] + (col.b / 255) * cPow[1] + (col.r / 255) * cPow[2] + (col.w / 255) * cPow[3];
    total /= 8;
    // display background
    drawSprite(4, 1, FX.norm);
    // show contrast by summing highlights
    drawSprite(4, Math.pow(total, 4), FX.light);
    // display each channel in turn
    var i = 0;
    while (i < 4) {
        var v = gbrw[i]; // get channel normalised value
        // add an ease curve and push intensity to full (over exposed)
        v = easeInOut(Math.min(1, v), 2) * 4 * cPow[i]; // cPow is channel final gain
        while (v > 0) { // add intensity for channel
            drawSprite(i, easeInOut(Math.min(1, v), 4), FX.light);
            if(i === 1){ // if blue add a little white
                 drawSprite(4, easeInOut(Math.min(1, v)/4, 4), FX.light);
            }

            v -= 1;
        }
        i++;
    }
    drawSprite(4, (1 - Math.pow(total, 4)) / 2, FX.block);
    drawSprite(4, 0.06, FX.hard);

}
var gbrwT = [0, 0, 0, 0];
var move = 0.2;
ctx.fillRect(0, 0, canvas.width, canvas.height);
function update(time) {
    if (mediaReady) {
        time /= 1000;
        var t = Math.sin(time / ((Math.sin(time / 5000) * 12300))) * 100;
        var t = Math.sin(time / 12300) * 100;
        var ttr = Math.sin(time / 12300 + t);
        var ttg = Math.sin(time / 12400 + t * 10);
        var ttb = Math.sin(time / 12500 + t * 15);
        var ttw = Math.sin(time / 12600 + t * 20);
        var tr = time / (2360 + t);
        var tg = time / (2360 + t * 2);
        var tb = time / (2360 + t * 3);
        var tw = time / (2360 + t * 4);
        for (var i = 0; i * 31 < canvas.width; i++) {
            colourCurrent.r = Math.sin(tr) * 128 + 128;
            colourCurrent.g = Math.sin(tg) * 128 + 128;
            colourCurrent.b = Math.sin(tb) * 128 + 128;
            colourCurrent.w = Math.sin(tw) * 128 + 128;
            tr += ttr;
            tg += ttg;
            tb += ttb;
            tw += ttw;
            drawLed(colourCurrent);
            ctx.drawImage(canLed, i * 31, 0);
        }
    }

    requestAnimationFrame(update);

}
requestAnimationFrame(update);
Blindman67
  • 51,134
  • 11
  • 73
  • 136