绘制饱和度/亮度渐变

4
我正在尝试在画布中绘制以下渐变图像,但右下角有问题。
期望的效果:

enter image description here

当前输出:

enter image description here

我可能在这里漏掉了一些非常简单的东西。

function color(r, g, b) {
  var args = Array.prototype.slice.call(arguments);
  if (args.length == 1) {
    args.push(args[0]);
    args.push(args[0]);
  } else if (args.length != 3 && args.length != 4) {
    return;
  }
  return "rgb(" + args.join() + ")";
}

function drawPixel(x, y, fill) {
  var fill = fill || "black";
  context.beginPath();
  context.rect(x, y, 1, 1);
  context.fillStyle = fill;
  context.fill();
  context.closePath();
}

var canvas = document.getElementById("primary");
var context = canvas.getContext("2d");

canvas.width = 256;
canvas.height = 256;

for (var x = 0; x < canvas.width; x++) {
  for (var y = 0; y < canvas.height; y++) {
    var r = 255 - y;
    var g = 255 - x - y;
    var b = 255 - x - y;
    drawPixel(x, y, color(r, g, b));
  }
}
#primary {
    display: block;
    border: 1px solid gray;
}
<canvas id="primary"></canvas>

{{链接1:JSFiddle}}

3个回答

6

使用渐变。

您可以让GPU为您完成大部分的处理。2D复合操作multiply有效地为每个像素乘以两种颜色。因此,对于每个通道和每个像素,colChanDest = Math.floor(colChanDest * (colChanSrc / 255))通过GPU的大规模并行处理能力进行,而不是在单个核心(JavaScript执行上下文)上运行的低级共享线程。

两个渐变

  1. 一个是从上到下的白色到黑色的背景

    var gradB = ctx.createLinearGradient(0,0,0,255); gradB.addColorStop(0,"white"); gradB.addColorStop(1,"black");

  2. 另一个是从左到右透明到不透明的色调

    var swatchHue var col = "rgba(0,0,0,0)" var gradC = ctx.createLinearGradient(0,0,255,0); gradC.addColorStop(0,``hsla(${hueValue},100%,50%,0)``); gradC.addColorStop(1,``hsla(${hueValue},100%,50%,1)``);

注意 上述字符串在SO上未正确呈现,因此我只是将它们加倍显示,可以像演示片段中所做的那样使用单引号。

渲染

然后将两个组合在一起,首先是背景(灰度),然后是复合操作“multiply”。

ctx.fillStyle = gradB;
ctx.fillRect(0,0,255,255);
ctx.fillStyle = gradC;
ctx.globalCompositeOperation = "multiply";
ctx.fillRect(0,0,255,255);
ctx.globalCompositeOperation = "source-over";

仅适用于Hue

重要的是颜色(色相)是一个纯色值,您不能使用随机的RGB值。如果您选择了RGB值,则需要从中提取色相值。

以下函数将RGB值转换为HSL颜色。

function rgbToLSH(red, green, blue, result = {}){
    value hue, sat, lum, min, max, dif, r, g, b;
    r = red/255;
    g = green/255;
    b = blue/255;
    min = Math.min(r,g,b);
    max = Math.max(r,g,b);
    lum = (min+max)/2;
    if(min === max){
        hue = 0;
        sat = 0;
    }else{
        dif = max - min;
        sat = lum > 0.5 ? dif / (2 - max - min) : dif / (max + min);
        switch (max) {
        case r:
            hue = (g - b) / dif;
            break;
        case g:
            hue = 2 + ((b - r) / dif);
            break;
        case b:
            hue = 4 + ((r - g) / dif);
            break;
        }
        hue *= 60;
        if (hue < 0) {
            hue += 360;
        }        
    }
    result.lum = lum * 255;
    result.sat = sat * 255;
    result.hue = hue;
    return result;
}

将所有内容组合在一起

该示例每3秒为随机的红色、绿色、蓝色值呈现一个样本。

请注意,此示例使用Babel,以便在IE上可以正常工作。

var canvas = document.createElement("canvas");
canvas.width = canvas.height = 255;
var ctx = canvas.getContext("2d");
document.body.appendChild(canvas);

function drawSwatch(r, g, b) {
  var col = rgbToLSH(r, g, b);
  var gradB = ctx.createLinearGradient(0, 0, 0, 255);
  gradB.addColorStop(0, "white");
  gradB.addColorStop(1, "black");
  var gradC = ctx.createLinearGradient(0, 0, 255, 0);
  gradC.addColorStop(0, `hsla(${Math.floor(col.hue)},100%,50%,0)`);
  gradC.addColorStop(1, `hsla(${Math.floor(col.hue)},100%,50%,1)`);

  ctx.fillStyle = gradB;
  ctx.fillRect(0, 0, 255, 255);
  ctx.fillStyle = gradC;
  ctx.globalCompositeOperation = "multiply";
  ctx.fillRect(0, 0, 255, 255);
  ctx.globalCompositeOperation = "source-over";
}

function rgbToLSH(red, green, blue, result = {}) {
  var hue, sat, lum, min, max, dif, r, g, b;
  r = red / 255;
  g = green / 255;
  b = blue / 255;
  min = Math.min(r, g, b);
  max = Math.max(r, g, b);
  lum = (min + max) / 2;
  if (min === max) {
    hue = 0;
    sat = 0;
  } else {
    dif = max - min;
    sat = lum > 0.5 ? dif / (2 - max - min) : dif / (max + min);
    switch (max) {
      case r:
        hue = (g - b) / dif;
        break;
      case g:
        hue = 2 + ((b - r) / dif);
        break;
      case b:
        hue = 4 + ((r - g) / dif);
        break;
    }
    hue *= 60;
    if (hue < 0) {
      hue += 360;
    }
  }
  result.lum = lum * 255;
  result.sat = sat * 255;
  result.hue = hue;
  return result;
}

function drawRandomSwatch() {
  drawSwatch(Math.random() * 255, Math.random() * 255, Math.random() * 255);
  setTimeout(drawRandomSwatch, 3000);
}
drawRandomSwatch();

要从x和y坐标计算颜色,您需要先计算色相(Hue),然后计算饱和度(saturation)和明度(value),以获取hsv颜色(注意,hsl和hsv是不同的颜色模型)。

// saturation and value are clamped to prevent rounding errors creating wrong colour
var rgbArray = hsv_to_rgb(
    hue, // as used to create the swatch
    Math.max(0, Math.min(1, x / 255)),   
    Math.max(0, Math.min(1, 1 - y / 255))
);

获取h、s、v颜色的r、g、b值的函数。

/* Function taken from datGUI.js 
   Web site https://workshop.chromeexperiments.com/examples/gui/#1--Basic-Usage
   // h 0-360, s 0-1, and v 0-1
*/

function hsv_to_rgb(h, s, v) {
  var hi = Math.floor(h / 60) % 6;
  var f = h / 60 - Math.floor(h / 60);
  var p = v * (1.0 - s);
  var q = v * (1.0 - f * s);
  var t = v * (1.0 - (1.0 - f) * s);
  var c = [
    [v, t, p],
    [q, v, p],
    [p, v, t],
    [p, q, v],
    [t, p, v],
    [v, p, q]
  ][hi];
  return {
    r: c[0] * 255,
    g: c[1] * 255,
    b: c[2] * 255
  };
}


使用内置的渐变是一个巨大的节省:jsfiddle。然而,我还要在SB框中添加一个小圆圈来选择颜色,并且我需要找到/计算在圆圈中聚焦的颜色。我考虑使用x,y来计算聚焦的颜色(费力的方式),或者我可以直接读取位于x,y位置的像素。有什么建议吗?我可能很快会发布另一个关于此问题的问题。 - akinuri
@akinuri,我已经在答案中添加了你所需要的内容。 - Blindman67

2
我必须使用OpenGL进行此操作,而Blindman67的答案是我找到的唯一资源。 最终,我通过在彼此上方绘制3个矩形来完成它。
  1. 全白色
  2. 水平透明红到不透明红
  3. 垂直透明黑到不透明黑

Resulting Gradient


1
更新:在之前的例子中,我只创建了红色的渐变。稍作修改后,我也可以使用相同的方法创建绿色和蓝色的渐变,但我无法使用它来创建随机色调的渐变。红、绿和蓝很容易实现,因为当一个通道为255时,另外两个通道具有相同的值。对于一个随机的色调,例如140度,情况并非如此。H=140转换为rgb(0,255,85)。红色和蓝色不能有相等的值。这需要不同且更复杂的计算。
Blindman67的答案解决了这个问题。使用内置的渐变,您可以轻松地创建任何随机色调的渐变:jsfiddle。但作为一个非常好奇的人,我仍然想用较困难的方式来实现它,这就是它:
(与Blindman67的比起来,它非常慢...) JSFiddle

function drawPixel(x, y, fillArray) {
  fill = "rgb(" + fillArray.join() + ")" || "black";
  context.beginPath();
  context.rect(x, y, 1, 1);
  context.fillStyle = fill;
  context.fill();
}

var canvas = document.getElementById("primary");
var context = canvas.getContext("2d");

var grad1 = [ [255, 255, 255], [0, 0, 0] ]; // brightness

fillPrimary([255, 0, 0]); // initial hue = 0 (red)

$("#secondary").on("input", function() {
  var hue = parseInt(this.value, 10);
  var clr = hsl2rgb(hue, 100, 50);
  fillPrimary(clr);
});

function fillPrimary(rgb) {
  var grad2 = [ [255, 255, 255], rgb ]; // saturation
  for (var x = 0; x < canvas.width; x++) {
    for (var y = 0; y < canvas.height; y++) {

      var grad1Change = [
        grad1[0][0] - grad1[1][0],
        grad1[0][1] - grad1[1][1],
        grad1[0][2] - grad1[1][2],
      ];
      var currentGrad1Color = [
        grad1[0][0] - (grad1Change[0] * y / 255),
        grad1[0][1] - (grad1Change[1] * y / 255),
        grad1[0][2] - (grad1Change[2] * y / 255)
      ];

      var grad2Change = [
        grad2[0][0] - grad2[1][0],
        grad2[0][1] - grad2[1][1],
        grad2[0][2] - grad2[1][2],
      ];
      var currentGrad2Color = [
        grad2[0][0] - (grad2Change[0] * x / 255),
        grad2[0][1] - (grad2Change[1] * x / 255),
        grad2[0][2] - (grad2Change[2] * x / 255)
      ];

      var multiplied = [
        Math.floor(currentGrad1Color[0] * currentGrad2Color[0] / 255),
        Math.floor(currentGrad1Color[1] * currentGrad2Color[1] / 255),
        Math.floor(currentGrad1Color[2] * currentGrad2Color[2] / 255),
      ];

      drawPixel(x, y, multiplied);
    }
  }
}

function hsl2rgb(h, s, l) {
  h /= 360;
  s /= 100;
  l /= 100;
  var r, g, b;
  if (s == 0) {
    r = g = b = l;
  } 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);
  }
  return [
    Math.round(r * 255),
    Math.round(g * 255),
    Math.round(b * 255),
  ];
}
#primary {
  display: block;
  border: 1px solid gray;
}
#secondary {
  width: 256px;
  height: 15px;
  margin-top: 15px;
  outline: 0;
  display: block;
  border: 1px solid gray;
  box-sizing: border-box;
  -webkit-appearance: none;
  background-image: linear-gradient(to right, red 0%, yellow 16.66%, lime 33.33%, cyan 50%, blue 66.66%, violet 83.33%, red 100%);
}
#secondary::-webkit-slider-thumb {
  -webkit-appearance: none;
  height: 25px;
  width: 10px;
  border-radius: 10px;
  background-color: rgb(230, 230, 230);
  border: 1px solid gray;
  box-shadow: inset 0 0 2px rgba(255, 255, 255, 1), 0 0 2px rgba(255, 255, 255, 1);
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<canvas id="primary" width="256" height="256"></canvas>
<input type="range" min="0" max="360" step="1" value="0" id="secondary" />


好的,我已经找出问题所在。虽然垂直范围始终在[0,255]之间,但水平范围在[0,r]之间。因此,gb不能大于r(显然!)。

enter image description here

function color(r, g, b) {
  var args = Array.prototype.slice.call(arguments);
  if (args.length == 1) {
    args.push(args[0]);
    args.push(args[0]);
  } else if (args.length != 3 && args.length != 4) {
    return;
  }
  return "rgb(" + args.join() + ")";
}

function drawPixel(x, y, fill) {
  var fill = fill || "black";
  context.beginPath();
  context.rect(x, y, 1, 1);
  context.fillStyle = fill;
  context.fill();
  context.closePath();
}

var canvas = document.getElementById("primary");
var context = canvas.getContext("2d");

canvas.width = 256;
canvas.height = 256;

for (var x = 0; x < canvas.width; x++) {
  for (var y = 0; y < canvas.height; y++) {
    var r = 255 - y;
    var g = b = r - Math.floor((x / 255) * r); // tada!
    drawPixel(x, y, color(r, g, b));
  }
}
#primary {
    display: block;
    border: 1px solid gray;
}
<canvas id="primary"></canvas>


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接