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°),您可以操纵 saturation
和 value
以获得所需的结果。
这个想法是降低饱和度和价值,所以当调暗灯光时,你会失去饱和度。在饱和度为 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);
我遇到了一个我认为非常有趣的问题,需要一个优雅的解决方案...
我有一个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°),您可以操纵 saturation
和 value
以获得所需的结果。
这个想法是降低饱和度和价值,所以当调暗灯光时,你会失去饱和度。在饱和度为 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);