RGB 到真实光色 Javascript 转换

RGB to real-life light colour Javascript conversion

我遇到了一个我认为非常有趣的问题,需要一个优雅的解决方案...

我有一个RGB值,例如205,50,63

我正在尝试模拟网页上 RGB LED 的颜色,就好像它是真实的灯光一样。

例如,RGB 颜色 255,0,0 在 LED 和网页上都显示为红色。

同样,RGB 颜色 255,255,255 会在 LED 和网页上显示为白色。

但是 RGB 颜色 0,0,0 在 LED 上显示为关闭,在网页上显示为黑色。

我想要实现的是 0,0,0 和 255,255,255 都显示为白色。好像LED越暗越白。

我一直在尝试对这些值应用比例算法,然后将 <div> 层叠加在彼此之上,但没有成功。有什么想法吗?

我不确定你想象的情况是什么,但是阅读你想要的输出,简单地放大使最大值变成 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,
    };
}

所以你会得到这样的结果

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}

请注意,这会将所有 {x, 0, 0} 折叠为 {255, 0, 0},这意味着 {1, 0, 0}{1, 1, 1} 截然不同。如果这是不可取的,你需要考虑对这种情况的特殊处理


更多RGB提示;如果你围绕你的操作进行平方和根,你会得到更平滑的 "more natural" 光过渡等,例如而不是 x + y,而是 sqrt(x*x + y*y)

这导致对如何解决问题的不同想法;添加白色并缩小

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

这次

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}

并且在边缘点周围的跳跃较少,例如

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

最后,请注意这会将 rgb 映射到范围 180..255,因此如果您想保留 "true red"s 等

,您可以将其转换为 0..255
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),
    };
}

所以

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}

我认为你应该考虑 HSV color space 这个问题。假设您将色调设置为红色(在您的示例中为 354°),您可以操纵 saturationvalue 以获得所需的结果。 这个想法是降低饱和度和价值,所以当调暗灯光时,你会失去饱和度。在饱和度为 0% 的边缘情况下,值也设置为 100%,产生白光。

看看下面的图片。请注意 H、S、V 值。

您从基本情况开始:

那你暗淡:

最后得到不饱和的颜色:

用代码来说就是

dim is in range 0.0 to 1.0
hsv(dim) -> {
    saturation = baseSaturation * (1 - dim)
    value = baseValue + (1 - baseValue) * dim
}
hue is constant

既然已经有了答案,我就不赘述了。

演示模拟多色透明 LED

颜色是通过使用合成操作重叠 3 个以上的 RGB 图像创建的"lighten"。这是一个加法过程。还有一个白色通道,可以为整个 LED 增加白光。 RGB 通道增加了额外的增益以平衡效果,当蓝色高时红色被压低。

当没有光时,只显示 LED 的图像。 RGB & white 4个颜色通道前后还有对比图绘制。

使用一些更好的源图像(每个通道只使用一个,应该有 2-3 个)可以创建非常逼真的 FX。注意周围的环境也会影响观感。

// 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);