<link href='https://fonts.googleapis.com/css?family=Raleway' rel='stylesheet'>

<script src="https://d3js.org/d3.v6.min.js"></script>
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>

<div class="lineWrapper">

  <div class="ControlsWrapper">

    <div>Controls</div>

    <div class="slidersWrapper">
      <div class="slider-container">
        <span class="slider-label">Start</span>
        <input type="range" id="startSlider" class="slider" min="0" max="3" value="0.5" step="0.01">
        <span id="startValue">0.5</span>
      </div>

      <div class="slider-container">
        <span class="slider-label">Rotations</span>
        <input type="range" id="rotSlider" class="slider" min="-30" max="30" value="1.5" step="0.001">
        <span id="rotValue">1.5</span>
      </div>

      <div class="slider-container">
        <span class="slider-label">Gamma</span>
        <input type="range" id="gammaSlider" class="slider" min="0" max="3" value="1" step="0.01">
        <span id="gammaValue">1</span>
      </div>

      <div class="slider-container">
        <span class="slider-label">Min Sat</span>
        <input type="range" id="minSatSlider" class="slider" min="0" max="2" value="1.2" step="0.01">
        <span id="minSatValue">1.2</span>
      </div>

      <div class="slider-container">
        <span class="slider-label">Max Sat</span>
        <input type="range" id="maxSatSlider" class="slider" min="0" max="2" value="1.2" step="0.01">
        <span id="maxSatValue">1.2</span>
      </div>

      <div class="slider-container">
        <span class="slider-label">Min Lightness</span>
        <input type="range" id="minLightSlider" class="slider" min="0" max="1" value="0" step="0.01">
        <span id="minLightValue">0</span>
      </div>

      <div class="slider-container">
        <span class="slider-label">Max Lightness</span>
        <input type="range" id="maxLightSlider" class="slider" min="0" max="1" value="1" step="0.01">
        <span id="maxLightValue">1</span>
      </div>
    </div>

  </div>

  <div class="lineCanvasWrapper">
    <div>RGB Curves</div>
    <canvas id="lineCanvas" width="300" height="300"></canvas>
  </div>
</div>

<!-- <div id="d3-container"></div> -->
<!-- <div id="plotly-container" style="width:600px;height:400px;"></div> -->
<div id="plotly-plot" style="display:none;"></div>

<div class="my3dWrapper">
  <div id="helix3dPlot"></div>
</div>

<div class="my3dWrapper">
  <div id="line3dPlot"></div>
</div>

<div id="ellipseCanvasWrapper">
  <canvas id="ellipseCanvas" width="600" height="300"></canvas>
</div>

<div class="helixGradientWrapper">
  <canvas id="helixGradient"></canvas>
</div>
body {
  font-family: "Raleway";
  font-size: 0.8em;
}

.ControlsWrapper {
  width: 350px;
}
.slidersWrapper {
  border: 1px solid red;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  flex-direction: column;
}

.slider-container {
  margin-bottom: 10px;
  font-size: 1em;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  flex-direction: row;
  flex-wrap: nowrap;
}

.slider-container span:last-child {
  font-family: "Courier New", Courier, monospace;
  margin: 0 1em;
  font-size: 1.1em;
}

.slider-label {
  width: 100px;
  display: inline-block;
}

.slider {
  width: 180px;
}

.my3dWrapper {
  display: flex;
  justify-content: flex-start;
  align-items: center;
  /*   width: fit-content; */
  flex-wrap: wrap;
  border: 1px solid red;
}

.lineWrapper {
  display: flex;
  justify-content: flex-start;
  align-items: flex-start;
  flex-direction: row;
  flex-wrap: wrap;
  align-content: center;
}

#plotly-plot {
  border: 1px solid cyan;
}

#myCanvas {
  border: 1px solid red;
}

#ellipseCanvas {
  border: 1px solid pink;
}

#lineCanvas {
  border: 1px solid blue;
}

#helixGradient {
  width: 100%;
  height: 100%;
}

.helixGradientWrapper {
  margin: 15px 0;
  width: 1000px;
  height: 40px;
  border: 1px solid grey;
}

input[type="range"] {
  cursor: pointer;
}
// Adding event listeners to sliders when the window loads
window.onload = function () {
  handleSliderChange();

  document
    .getElementById("startSlider")
    .addEventListener("input", handleSliderChange);
  document
    .getElementById("rotSlider")
    .addEventListener("input", handleSliderChange);
  document
    .getElementById("gammaSlider")
    .addEventListener("input", handleSliderChange);
  document
    .getElementById("minSatSlider")
    .addEventListener("input", handleSliderChange);
  document
    .getElementById("maxSatSlider")
    .addEventListener("input", handleSliderChange);
  document
    .getElementById("minLightSlider")
    .addEventListener("input", handleSliderChange);
  document
    .getElementById("maxLightSlider")
    .addEventListener("input", handleSliderChange);
};
let rgb;
var currentLayout = {}; // Global variable to store the current layout
let sizePoint = 4;

// var currentCameraSettings = null; // Initialize with null
var currentCameraSettings = {
  up: {
    x: 0,
    y: 0,
    z: 1
  },
  center: {
    x: 0,
    y: 0,
    z: 0
  },
  eye: {
    x: 1.6,
    y: -1.6,
    z: 1.2
  },
  projection: {
    type: "perspective"
  }
}; // Initialize with null

// Function to handle slider changes
function handleSliderChange() {
  var start = parseFloat(document.getElementById("startSlider").value);
  var rot = parseFloat(document.getElementById("rotSlider").value);
  var gamma = parseFloat(document.getElementById("gammaSlider").value);
  var minSat = parseFloat(document.getElementById("minSatSlider").value);
  var maxSat = parseFloat(document.getElementById("maxSatSlider").value);
  var minLight = parseFloat(document.getElementById("minLightSlider").value);
  var maxLight = parseFloat(document.getElementById("maxLightSlider").value);

  document.getElementById("startValue").textContent = start.toFixed(2);
  document.getElementById("rotValue").textContent = rot.toFixed(2);
  document.getElementById("gammaValue").textContent = gamma.toFixed(2);
  document.getElementById("minSatValue").textContent = minSat.toFixed(2);
  document.getElementById("maxSatValue").textContent = maxSat.toFixed(2);
  document.getElementById("minLightValue").textContent = minLight.toFixed(2);
  document.getElementById("maxLightValue").textContent = maxLight.toFixed(2);

  let myLen = 256 * 1 * Math.abs(rot);
  if (myLen >= 256) myLen = 256;
  console.log("myLen :", myLen);

  // Call a function to update your visualization with these values
  // updateVisualization(start, rot, gamma, minSat, maxSat, minLight, maxLight);
  rgb = cubehelix(start, rot, gamma, minSat, maxSat, minLight, maxLight, myLen);
  updatePlotlyVisualization(rgb);
  // update3DScatterPlot(rgb);
  generate3DScatterPlot(rgb);

  // drawLines_2(rgb)
}
// Function to clear the canvas

function updatePlotlyVisualization(rgb) {
  // Get both canvases and their contexts
  const ellipseCanvas = document.getElementById("ellipseCanvas");
  const lineCanvas = document.getElementById("lineCanvas");
  const ctx = ellipseCanvas.getContext("2d");
  const lineCtx = lineCanvas.getContext("2d");

  const helixGradientCanvas = document.getElementById("helixGradient");
  const helixGradientCtx = helixGradientCanvas.getContext("2d");

  const width = ellipseCanvas.width;
  const height = ellipseCanvas.height;

  const lineWidth = lineCanvas.width;
  const lineHeight = lineCanvas.height;

  const ellipseWidth = ellipseCanvas.width;
  const ellipseHeight = ellipseCanvas.height;

  // Function to clear a canvas
  function clearCanvas(canvasCtx) {
    canvasCtx.clearRect(0, 0, canvasCtx.canvas.width, canvasCtx.canvas.height);
  }

  //   XY Plot experimental
  function tracePlotXY() {
    var trace1 = {
      x: [], // x coordinates for circles
      y: [], // y coordinates for circles
      mode: "markers",
      marker: {
        size: [],
        color: []
      }
    };

    for (var i = 0; i <= 255; i++) {
      // Calculating values for circles
      var xCircle = map(i, 0, 255, 300, 1000); // Adjust the mapping as needed
      var yCircle = map(rgb.g[i], 0, 255, 0, 500); // Adjust the mapping as needed
      var size = map(rgb.b[i], 0, 255, 1, 20);
      var color = `rgb(${rgb.r[i]}, ${rgb.g[i]}, ${rgb.b[i]})`;

      trace1.x.push(xCircle);
      trace1.y.push(yCircle);
      trace1.marker.size.push(size);
      trace1.marker.color.push(color);
    }

    var data = [trace1];

    var layout = {
      title: "RGB Visualization",
      xaxis: { title: "X Axis" },
      yaxis: { title: "Y Axis" }
    };

    Plotly.newPlot("plotly-plot", data, layout, { responsive: true });
  }

  // Function to draw ellipses
  function drawEllipses() {
    clearCanvas(ctx);

    for (let i = 0; i <= 255; i++) {
      let sz = map(rgb.b[i], 0, 255, 1, 20);
      let x = map(rgb.r[i], 0, 255, 0, ellipseWidth);
      let y = map(rgb.g[i], 0, 255, ellipseHeight, 0);

      ctx.fillStyle = `rgb(${rgb.r[i]}, ${rgb.g[i]}, ${rgb.b[i]})`;
      ctx.beginPath();
      ctx.ellipse(x, y, sz, sz, 0, 0, 2 * Math.PI);
      ctx.fill();
    }
  }

  // Function to draw lines in the separate canvas
  function drawLines() {
    clearCanvas(lineCtx);

    for (let i = 1; i <= 255; i++) {
      drawLineComponent(lineCtx, rgb.r, "r", i);
      drawLineComponent(lineCtx, rgb.g, "g", i);
      drawLineComponent(lineCtx, rgb.b, "b", i);
    }
  }

  function drawLines_2(rgb) {
    clearCanvas(lineCtx);

    for (let i = 1; i <= 255; i++) {
      drawLineComponent(lineCtx, rgb.r, "r", i);
      drawLineComponent(lineCtx, rgb.g, "g", i);
      drawLineComponent(lineCtx, rgb.b, "b", i);
      //!
      // fill(rgb.r[i], rgb.g[i], rgb.b[i]);
    }
    var traceR = {
      // x: new Array(rgb.r.length).fill(0), // Zero array for R
      x: Array.from({ length: rgb.r.length }, (_, i) => i), // Array of indices
      y: rgb.r,
      // y: rgb.r, // Red values
      z: rgb.r,
      mode: "lines",
      type: "scatter3d",
      line: {
        color: "red",
        width: 4
      },
      name: "Red"
    };

    var traceG = {
      y: rgb.g, // Green values
      x: Array.from({ length: rgb.g.length }, (_, i) => i), // Array of indices
      z: rgb.g, // Green values
      mode: "lines",
      type: "scatter3d",
      line: {
        color: "green",
        width: 4
      },
      name: "Green"
    };

    var traceB = {
      x: Array.from({ length: rgb.b.length }, (_, i) => i), // Array of indices
      y: rgb.b,
      z: rgb.b, // Blue values
      mode: "lines",
      type: "scatter3d",
      line: {
        color: "blue",
        width: 4
      },
      name: "Blue"
    };

    var data = [traceR, traceG, traceB];

    var layout = {
      title: "RGB Lines Visualization",
      autosize: true,
      margin: {
        l: 0,
        r: 0,
        b: 0,
        t: 30
      },

      scene: {
        camera: {
          eye: {
            x: 0,
            y: -2,
            z: 0
          },
          up: {
            x: 0,
            y: 0,
            z: 1
          },
          center: {
            x: 0,
            y: 0,
            z: 0
          },
          projection: {
            type: "orthographic"
          }
        },
        aspectmode: "cube",
        xaxis: {
          title: "R",
          range: [0, 255],
          dtick: 255 / 3
        },
        yaxis: {
          title: "B",
          range: [0, 255],
          dtick: 255 / 3
        },
        zaxis: {
          title: "G",
          range: [0, 255],
          dtick: 255 / 3
        }
      }
    };

    // fill(rgb.r[i], rgb.g[i], rgb.b[i]);
    //

    Plotly.newPlot("line3dPlot", data, layout, { responsive: true });
  }

  function drawLineComponent(ctx, componentArray, component, index) {
    ctx.beginPath();
    if (component === "r") {
      ctx.strokeStyle = `rgb(${componentArray[index]}, 0, 0)`;
    } else if (component === "g") {
      ctx.strokeStyle = `rgb(0, ${componentArray[index]}, 0)`;
    } else {
      // 'b'
      ctx.strokeStyle = `rgb(0, 0, ${componentArray[index]})`;
    }
    ctx.moveTo(
      map(index - 1, 0, 255, 0, lineWidth),
      map(componentArray[index - 1], 0, 255, lineHeight, 0)
    );
    ctx.lineTo(
      map(index, 0, 255, 0, lineWidth),
      map(componentArray[index], 0, 255, lineHeight, 0)
    );

    ctx.stroke();
  }

  function drawGradient(rgb) {
    const canvas = document.getElementById("helixGradient");
    const ctx = canvas.getContext("2d");

    // Clear the canvas
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // Create a linear gradient
    // The gradient goes from left (0) to right (canvas width)
    const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);

    // Add color stops to the gradient
    for (let i = 0; i < rgb.r.length; i++) {
      const color = `rgb(${rgb.r[i]}, ${rgb.g[i]}, ${rgb.b[i]})`;
      const position = i / (rgb.r.length - 1); // Normalize position to range from 0 to 1
      gradient.addColorStop(position, color);
    }

    // Fill the rectangle with the gradient
    ctx.fillStyle = gradient;
    ctx.fillRect(0, 0, canvas.width, canvas.height);
  }

  // Call the draw functions
  tracePlotXY();
  drawEllipses();
  drawLines();
  drawLines_2(rgb);
  drawGradient(rgb);
}

// Call this function with the rgb data to update the 3D plot
function generate3DScatterPlot(rgb) {
  var rot = parseFloat(document.getElementById("rotSlider").value);
  console.log(rot >= 8 ? Math.abs((sizePoint / rot) * 10) : sizePoint);
  var trace = {
    x: rgb.r, // X coordinates (Red values)
    y: rgb.g, // Y coordinates (Green values)
    z: rgb.b, // Z coordinates (Blue values)
    mode: "markers",
    type: "scatter3d",
    marker: {
      // size: 5
      size: rot >= 8 ? Math.abs((sizePoint / rot) * 10) : sizePoint,

      color: rgb.r.map((r, idx) => `rgb(${r}, ${rgb.g[idx]}, ${rgb.b[idx]})`),
      opacity: 0.8
    }
  };

  var data = [trace];

  var layout = {
    title: "Cubehelix Color Scheme",
    scene: {
      xaxis: { title: "R" },
      yaxis: { title: "G" },
      zaxis: { title: "B" },
      camera: currentCameraSettings
    }
  };

  Plotly.newPlot("helix3dPlot", data, layout, { responsive: true });

  // Update the global camera settings whenever the layout changes
  document
    .getElementById("helix3dPlot")
    .on("plotly_relayout", function (eventData) {
      if (eventData["scene.camera"]) {
        currentCameraSettings = eventData["scene.camera"];
        console.log("currentCameraSettings :", currentCameraSettings);
      }
    });
}

// Helper function for mapping values (similar to p5.js map function)
function map(value, start1, stop1, start2, stop2) {
  return start2 + (stop2 - start2) * ((value - start1) / (stop1 - start1));
}

// cubehelix NOT using p5
function cubehelix(
  start,
  rot,
  gamma,
  minSat,
  maxSat,
  minLight,
  maxLight,
  nlev
) {
  var r = [];
  var g = [];
  var b = [];
  // var nlev = 256;
  var nlo = 0;
  var nhi = 0;

  for (var i = 0; i < nlev; i++) {
    var fract = linearMap(i, 0, nlev, minLight, maxLight);
    var angle = 2 * Math.PI * (start / 3.0 + rot * fract + 1.0);
    fract = Math.pow(fract, gamma);
    var satar = linearMap(i, 0, 255, minSat, maxSat);
    var amp = (satar * fract * (1 - fract)) / 2.0;
    var r1 =
      fract + amp * (-0.14861 * Math.cos(angle) + 1.78277 * Math.sin(angle));
    var g1 =
      fract + amp * (-0.29227 * Math.cos(angle) - 0.90649 * Math.sin(angle));
    var b1 = fract + amp * (1.97294 * Math.cos(angle));

    r1 = clamp(r1, 0, 1);
    g1 = clamp(g1, 0, 1);
    b1 = clamp(b1, 0, 1);

    r.push(255 * r1);
    g.push(255 * g1);
    b.push(255 * b1);
  }

  return { r, g, b };
}

function linearMap(value, start1, stop1, start2, stop2) {
  return start2 + (stop2 - start2) * ((value - start1) / (stop1 - start1));
}

function clamp(value, min, max) {
  if (value < min) return min;
  else if (value > max) return max;
  else return value;
}

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.4/p5.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.4/addons/p5.dom.min.js