【问题标题】:Draw saturation/brightness gradient绘制饱和度/亮度渐变
【发布时间】:2017-01-07 17:57:45
【问题描述】:

我正在尝试在画布中绘制以下渐变图像,但右下角有问题。

想要的效果:

当前输出:

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

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;
}
&lt;canvas id="primary"&gt;&lt;/canvas&gt;

JSFiddle

【问题讨论】:

    标签: javascript canvas linear-gradients color-picker pixel-manipulation


    【解决方案1】:

    使用渐变。

    您可以让 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. 另一个是从左到右从透明到不透明的Hue

      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 上没有正确呈现,所以我只是将它们加倍显示,使用单引号,如演示 sn-p 中所做的那样。

    渲染

    然后将两者分层,先背景(灰度),再用合成操作“乘法”

    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 中提取色调值。

    以下函数将 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 秒呈现一个随机红色、绿色、蓝色值的样本。

    请注意,此示例使用 Balel,因此它可以在 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 坐标计算颜色,您需要计算色调,然后是饱和度和值以获得 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 已将您需要的内容添加到答案中
    【解决方案2】:

    我必须用 OpenGL 来做这件事,而 Blindman67 的回答是我找到的唯一资源。 最后,我通过在彼此顶部绘制 3 个矩形来做到这一点。

    1. 全白
    2. 透明红色到不透明红色,水平方向
    3. 透明黑色到不透明黑色,垂直

    【讨论】:

      【解决方案3】:

      更新:在前面的示例中,我只为红色创建了渐变。稍作修改后,我也可以使用相同的方法创建绿色和蓝色渐变,但我不能使用它为随机色调创建渐变。红色、绿色和蓝色很容易,因为一个通道是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(呵呵!)。

      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;
      }
      &lt;canvas id="primary"&gt;&lt;/canvas&gt;

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2018-04-01
        • 1970-01-01
        • 2020-10-29
        • 2021-01-13
        • 2014-07-05
        • 1970-01-01
        • 2020-05-25
        • 1970-01-01
        相关资源
        最近更新 更多