Dead Robot Logo Right Logo

Blog 🤖


↩ Back to Blog ← Previous Post Next Post →
Hex color blender

Hex color blender - html app

i vibe-coded a super useful tool for blending 2 hex colors together in a pleasing way :)

simply pick or paste 2 colors in, decide how many blend mid-points you want, then click "blend"

Palette

Pick a color

i've also included the html code so other ppl can use it however they'd like too

html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hex Color Blender</title>
<style>
  body { margin: 0; background: #0b0d0c; min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 24px; box-sizing: border-box; }
</style>
</head>
<body>
<div id="dr-blend-tool">
  <style>
    @font-face {
      font-family: 'DRDynamo';
      src: url('https://deadrobotmusic.com/DR%20Dynamo.woff2') format('woff2');
    }
    #dr-blend-tool {
      --bg: #0b0d0c;
      --panel: #141715;
      --panel-2: #191d1b;
      --line: #262b28;
      --ink: #d7dad7;
      --muted: #7c847e;
      --accent: #14805f;
      --accent-bright: #1caa7c;
      --danger: #c0553f;
      font-family: 'DRDynamo', 'JetBrains Mono', 'Montserrat', sans-serif;
      font-weight: 300;
      color: var(--ink);
      background: var(--bg);
      padding: 24px;
      border-radius: 10px;
      text-align: left;
    }
    #dr-blend-tool * { box-sizing: border-box; font-family: inherit; }
    #dr-blend-tool .dr-blend-panel {
      background: var(--panel);
      border: 1px solid var(--line);
      border-radius: 10px;
      padding: 14px;
      width: 100%;
      max-width: 340px;
      margin: 0 auto;
    }
    #dr-blend-tool .dr-blend-field-row {
      display: grid;
      grid-template-columns: minmax(70px, 108px) 1fr auto;
      align-items: center;
      gap: 10px;
      padding: 6px 0;
    }
    #dr-blend-tool .dr-blend-field-row + .dr-blend-field-row { border-top: 1px solid var(--line); }
    #dr-blend-tool .dr-blend-field-row label {
      color: var(--muted);
      font-size: 13px;
      text-transform: uppercase;
      letter-spacing: .07em;
      font-weight: 600;
    }
    #dr-blend-tool .dr-blend-hex-input {
      background: var(--panel-2);
      border: 1px solid var(--line);
      color: var(--ink);
      font-family: 'DRDynamo', 'JetBrains Mono', SFMono-Regular, Consolas, monospace;
      font-size: 15px;
      padding: 10px 12px;
      border-radius: 7px;
      width: 100%;
      min-width: 0;
      letter-spacing: .03em;
    }
    #dr-blend-tool .dr-blend-hex-input:focus { outline: none; border-color: var(--accent-bright); box-shadow: 0 0 0 3px rgba(28,170,124,.16); }
    #dr-blend-tool .dr-blend-hex-input.dr-blend-invalid { border-color: var(--danger); box-shadow: 0 0 0 3px rgba(192,85,63,.16); }
    #dr-blend-tool select.dr-blend-hex-input {
      -webkit-appearance: none; appearance: none; cursor: pointer;
      background-image: linear-gradient(45deg, transparent 50%, var(--muted) 50%), linear-gradient(135deg, var(--muted) 50%, transparent 50%);
      background-position: calc(100% - 18px) center, calc(100% - 13px) center;
      background-size: 5px 5px, 5px 5px;
      background-repeat: no-repeat;
      width: auto;
      min-width: 76px;
      justify-self: start;
      padding-right: 32px;
    }
    #dr-blend-tool .dr-blend-swatch-btn {
      width: 44px;
      aspect-ratio: 1 / 1;
      border-radius: 7px;
      border: 1px solid var(--line);
      cursor: pointer;
      padding: 0;
      background-color: var(--panel-2);
    }
    #dr-blend-tool .dr-blend-swatch-btn:hover { border-color: var(--accent-bright); }
    /* while the picker overlay is open, keep both main swatches visible above the dim layer */
    #dr-blend-tool.dr-blend-picking .dr-blend-swatch-btn {
      position: relative;
      z-index: 1001;
      pointer-events: none;
    }
    #dr-blend-tool .dr-blend-btn-row { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; margin-top: 12px; }
    #dr-blend-tool .dr-blend-btn {
      background: var(--panel-2); color: var(--ink); border: 1px solid var(--line); border-radius: 7px;
      padding: 12px 18px; font-size: 14px; font-weight: 600; letter-spacing: .02em; cursor: pointer;
      flex: 1 1 auto; text-align: center; min-height: 44px;
    }
    /* main Blend / Clear buttons only -- smaller + hug their own width instead of stretching */
    #dr-blend-tool .dr-blend-btn-row .dr-blend-btn {
      flex: 0 0 auto;
      padding: 7px 24px;
      font-size: 12px;
      min-height: 30px;
    }
    #dr-blend-tool .dr-blend-btn:hover { border-color: var(--accent-bright); }
    #dr-blend-tool .dr-blend-btn.dr-blend-primary { background: var(--accent); border-color: var(--accent); color: #eafff5; }
    #dr-blend-tool .dr-blend-btn.dr-blend-primary:hover { background: var(--accent-bright); }
    #dr-blend-tool .dr-blend-status { font-size: 12px; color: var(--muted); margin-top: 10px; min-height: 16px; }
    #dr-blend-tool .dr-blend-status.dr-blend-err { color: var(--danger); }
    #dr-blend-tool .dr-blend-palette-panel { max-width: 340px; margin: 20px auto 0; }
    #dr-blend-tool .dr-blend-palette-title {
      font-size: 12px; text-transform: uppercase; letter-spacing: .08em; color: var(--muted);
      margin: 0 0 6px; padding: 0 2px;
    }
    #dr-blend-tool .dr-blend-row {
      display: flex; align-items: center; justify-content: space-between; gap: 10px;
      padding: 4px 2px; border-bottom: 1px dotted var(--line);
    }
    #dr-blend-tool .dr-blend-row:last-child { border-bottom: none; }
    #dr-blend-tool .dr-blend-hex-btn {
      flex: 0 0 auto; min-width: 0; background: none; border: none; color: var(--ink);
      font-family: 'DRDynamo', 'JetBrains Mono', SFMono-Regular, Consolas, monospace;
      font-size: 15px; text-align: left; cursor: pointer;
      padding: 8px 2px; letter-spacing: .03em; -webkit-tap-highlight-color: transparent;
    }
    #dr-blend-tool .dr-blend-hex-btn:hover { color: var(--accent-bright); }
    #dr-blend-tool .dr-blend-row.dr-blend-endpoint .dr-blend-hex-btn { font-weight: 600; color: #eef1ee; }
    #dr-blend-tool .dr-blend-swatch {
      flex: 0 0 auto; width: 44px; aspect-ratio: 1 / 1;
      border-radius: 6px; border: 1px solid var(--line);
    }
    #dr-blend-tool .dr-blend-empty { color: var(--muted); font-size: 13px; padding: 14px 2px; line-height: 1.5; }

    /* ---------- picker overlay ---------- */
    #dr-blend-tool #dr-blend-picker-overlay {
      position: fixed; inset: 0; background: rgba(3,4,3,.72); display: none;
      align-items: center; justify-content: center; padding: 18px; z-index: 1000;
      overflow-y: auto;
    }
    #dr-blend-tool #dr-blend-picker-overlay.dr-blend-open { display: flex; }
    #dr-blend-tool #dr-blend-picker-panel {
      background: var(--panel); border: 1px solid var(--line); border-radius: 12px;
      padding: 18px; width: 100%; max-width: 300px;
    }
    #dr-blend-tool #dr-blend-picker-panel h3 { margin: 0 0 12px; font-size: 13px; color: var(--muted); text-transform: uppercase; letter-spacing: .07em; font-weight: 600; }
    #dr-blend-tool #dr-blend-sv-canvas { width: 100%; aspect-ratio: 1/1; border-radius: 8px; cursor: crosshair; touch-action: none; display: block; }
    #dr-blend-tool .dr-blend-sv-wrap { position: relative; }
    #dr-blend-tool .dr-blend-sv-cursor {
      position: absolute; width: 14px; height: 14px; margin-left: -7px; margin-top: -7px;
      border-radius: 50%; border: 2px solid #fff; box-shadow: 0 0 0 1px rgba(0,0,0,.5); pointer-events: none;
    }
    #dr-blend-tool .dr-blend-hue-wrap { position: relative; margin-top: 12px; }
    #dr-blend-tool #dr-blend-hue-canvas { width: 100%; height: 16px; border-radius: 8px; display: block; touch-action: none; cursor: pointer; }
    #dr-blend-tool .dr-blend-hue-cursor {
      position: absolute; top: -3px; width: 6px; height: 22px; margin-left: -3px;
      border-radius: 3px; border: 2px solid #fff; box-shadow: 0 0 0 1px rgba(0,0,0,.5); pointer-events: none;
    }
    #dr-blend-tool .dr-blend-picker-fields { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; margin-top: 14px; }
    #dr-blend-tool .dr-blend-picker-fields label { font-size: 10px; color: var(--muted); display: block; margin-bottom: 3px; letter-spacing: .04em; }
    #dr-blend-tool .dr-blend-picker-fields input {
      width: 100%; background: var(--panel-2); border: 1px solid var(--line); color: var(--ink);
      font-family: 'DRDynamo', 'JetBrains Mono', monospace; font-size: 12px; padding: 6px 4px; border-radius: 5px; text-align: center;
    }
    #dr-blend-tool .dr-blend-hex-field { grid-column: span 4; }
    #dr-blend-tool .dr-blend-hex-field input { text-align: left; padding: 8px 10px; font-size: 13px; }
    #dr-blend-tool .dr-blend-picker-compare-cell label { font-size: 10px; color: var(--muted); display: block; margin-bottom: 3px; letter-spacing: .04em; }
    #dr-blend-tool .dr-blend-compare { display: flex; gap: 4px; }
    #dr-blend-tool .dr-blend-compare-chip {
      flex: 0 0 auto; width: 22px; height: 22px; border-radius: 4px; border: 1px solid var(--line);
    }
    #dr-blend-tool .dr-blend-compare-chip.dr-blend-after { border-color: var(--accent-bright); }
    #dr-blend-tool .dr-blend-picker-buttons { display: flex; gap: 8px; margin-top: 16px; }
    #dr-blend-tool .dr-blend-picker-buttons .dr-blend-btn { margin-top: 0; }
  </style>

  <div class="dr-blend-panel">
    <div class="dr-blend-field-row">
      <label for="dr-blend-color1-hex">Color 1</label>
      <input type="text" id="dr-blend-color1-hex" class="dr-blend-hex-input" value="#33A365" maxlength="7" autocomplete="off" spellcheck="false">
      <button type="button" class="dr-blend-swatch-btn" id="dr-blend-color1-swatch" aria-label="Open picker for Color 1" style="background:#33A365"></button>
    </div>
    <div class="dr-blend-field-row">
      <label for="dr-blend-color2-hex">Color 2</label>
      <input type="text" id="dr-blend-color2-hex" class="dr-blend-hex-input" value="#52298E" maxlength="7" autocomplete="off" spellcheck="false">
      <button type="button" class="dr-blend-swatch-btn" id="dr-blend-color2-swatch" aria-label="Open picker for Color 2" style="background:#52298E"></button>
    </div>
    <div class="dr-blend-field-row">
      <label for="dr-blend-midpoints">Midpoints</label>
      <select id="dr-blend-midpoints" class="dr-blend-hex-input"></select>
      <span></span>
    </div>

    <div class="dr-blend-btn-row">
      <button type="button" class="dr-blend-btn dr-blend-primary" id="dr-blend-blend-btn">Blend</button>
      <button type="button" class="dr-blend-btn" id="dr-blend-clear-btn">Clear</button>
    </div>
    <div class="dr-blend-status" id="dr-blend-status"></div>
  </div>

  <div class="dr-blend-palette-panel">
    <p class="dr-blend-palette-title">Palette</p>
    <div id="dr-blend-palette-list"></div>
  </div>

  <div id="dr-blend-picker-overlay">
    <div id="dr-blend-picker-panel">
      <h3 id="dr-blend-picker-title">Pick a color</h3>
      <div class="dr-blend-sv-wrap">
        <canvas id="dr-blend-sv-canvas" width="260" height="260"></canvas>
        <div class="dr-blend-sv-cursor" id="dr-blend-sv-cursor"></div>
      </div>
      <div class="dr-blend-hue-wrap">
        <canvas id="dr-blend-hue-canvas" width="260" height="16"></canvas>
        <div class="dr-blend-hue-cursor" id="dr-blend-hue-cursor"></div>
      </div>
      <div class="dr-blend-picker-fields">
        <div class="dr-blend-hex-field"><label>Hex</label><input type="text" id="dr-blend-p-hex" maxlength="7" autocomplete="off" spellcheck="false"></div>
        <div><label>R</label><input type="number" id="dr-blend-p-r" min="0" max="255"></div>
        <div><label>G</label><input type="number" id="dr-blend-p-g" min="0" max="255"></div>
        <div><label>B</label><input type="number" id="dr-blend-p-b" min="0" max="255"></div>
        <div class="dr-blend-picker-compare-cell">
          <label>Was / Now</label>
          <div class="dr-blend-compare">
            <div class="dr-blend-compare-chip" id="dr-blend-p-before" title="Original color"></div>
            <div class="dr-blend-compare-chip dr-blend-after" id="dr-blend-p-after" title="Color you're selecting"></div>
          </div>
        </div>
      </div>
      <div class="dr-blend-picker-buttons">
        <button type="button" class="dr-blend-btn" id="dr-blend-picker-cancel">Cancel</button>
        <button type="button" class="dr-blend-btn dr-blend-primary" id="dr-blend-picker-ok">OK</button>
      </div>
    </div>
  </div>
</div>

<script>
(function() {
  /* ---------- color conversions (Lab math mirrors palette_predictor.js) ---------- */
  function srgbToLinear(c) { return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); }
  function linearToSrgb(c) { c = Math.max(0, Math.min(1, c)); return c <= 0.0031308 ? c * 12.92 : 1.055 * Math.pow(c, 1 / 2.4) - 0.055; }

  function rgbToXyz(rgb) {
    var r = rgb[0], g = rgb[1], b = rgb[2];
    var rl = srgbToLinear(r / 255), gl = srgbToLinear(g / 255), bl = srgbToLinear(b / 255);
    return [
      rl * 0.4124564 + gl * 0.3575761 + bl * 0.1804375,
      rl * 0.2126729 + gl * 0.7151522 + bl * 0.0721750,
      rl * 0.0193339 + gl * 0.1191920 + bl * 0.9503041,
    ];
  }
  function xyzToRgbLinear(xyz) {
    var X = xyz[0], Y = xyz[1], Z = xyz[2];
    return [
      X *  3.2404542 + Y * -1.5371385 + Z * -0.4985314,
      X * -0.9692660 + Y *  1.8760108 + Z *  0.0415560,
      X *  0.0556434 + Y * -0.2040259 + Z *  1.0572252,
    ];
  }
  function fLab(t) { var d = 6 / 29; return t > d * d * d ? Math.cbrt(t) : t / (3 * d * d) + 4 / 29; }
  function fLabInv(t) { var d = 6 / 29; return t > d ? t * t * t : 3 * d * d * (t - 4 / 29); }

  function rgbToLab(rgb) {
    var xyz = rgbToXyz(rgb), X = xyz[0], Y = xyz[1], Z = xyz[2];
    var Xn = 0.95047, Yn = 1.0, Zn = 1.08883;
    var fx = fLab(X / Xn), fy = fLab(Y / Yn), fz = fLab(Z / Zn);
    return [116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)];
  }
  function labToRgb(lab) {
    var L = lab[0], a = lab[1], b = lab[2];
    var Xn = 0.95047, Yn = 1.0, Zn = 1.08883;
    var fy = (L + 16) / 116, fx = fy + a / 500, fz = fy - b / 200;
    var xyz = [Xn * fLabInv(fx), Yn * fLabInv(fy), Zn * fLabInv(fz)];
    var lin = xyzToRgbLinear(xyz);
    return lin.map(function(c) { return Math.round(linearToSrgb(c) * 255); });
  }

  function hsvToRgb(h, s, v) {
    s /= 100; v /= 100;
    var c = v * s, x = c * (1 - Math.abs((h / 60) % 2 - 1)), m = v - c;
    var r = 0, g = 0, b = 0;
    if (h < 60) { r = c; g = x; b = 0; }
    else if (h < 120) { r = x; g = c; b = 0; }
    else if (h < 180) { r = 0; g = c; b = x; }
    else if (h < 240) { r = 0; g = x; b = c; }
    else if (h < 300) { r = x; g = 0; b = c; }
    else { r = c; g = 0; b = x; }
    return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255)];
  }
  function rgbToHsv(rgb) {
    var r = rgb[0] / 255, g = rgb[1] / 255, b = rgb[2] / 255;
    var mx = Math.max(r, g, b), mn = Math.min(r, g, b), d = mx - mn;
    var h = 0;
    if (d !== 0) {
      if (mx === r) h = ((g - b) / d) % 6;
      else if (mx === g) h = (b - r) / d + 2;
      else h = (r - g) / d + 4;
      h *= 60; if (h < 0) h += 360;
    }
    var s = mx === 0 ? 0 : d / mx;
    return { h: h, s: s * 100, v: mx * 100 };
  }

  function clampByte(v) { return Math.max(0, Math.min(255, Math.round(v))); }
  function rgbToHex(rgb) {
    function c(v) { return clampByte(v).toString(16).padStart(2, '0'); }
    return ('#' + c(rgb[0]) + c(rgb[1]) + c(rgb[2])).toUpperCase();
  }
  function isValidHex(v) { return /^#?[0-9a-fA-F]{6}$/.test(v.trim()); }
  function hexToRgb(hex) {
    hex = hex.trim().replace('#', '');
    return [parseInt(hex.slice(0, 2), 16), parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16)];
  }
  function normalizeHex(v) {
    v = v.trim();
    if (v.charAt(0) !== '#') v = '#' + v;
    return v.toUpperCase();
  }

  /* ---------- blend ----------
     Each midpoint is a 50/50 average of two methods:
     - Lab-space interpolation (perceptual, keeps chroma through the transition)
     - classic linear RGB interpolation (the "Color Mixer" style channel lerp)
     Averaging the two final RGB results gives a middle ground between the two looks. */
  function blendPalette(hex1, hex2, midpoints) {
    var rgb1 = hexToRgb(hex1), rgb2 = hexToRgb(hex2);
    var lab1 = rgbToLab(rgb1), lab2 = rgbToLab(rgb2);
    var steps = midpoints + 1;
    var out = [];
    for (var i = 0; i <= steps; i++) {
      var t = i / steps;
      var lab = [
        lab1[0] + (lab2[0] - lab1[0]) * t,
        lab1[1] + (lab2[1] - lab1[1]) * t,
        lab1[2] + (lab2[2] - lab1[2]) * t,
      ];
      var labRgb = labToRgb(lab);
      var linRgb = [
        rgb1[0] + (rgb2[0] - rgb1[0]) * t,
        rgb1[1] + (rgb2[1] - rgb1[1]) * t,
        rgb1[2] + (rgb2[2] - rgb1[2]) * t,
      ];
      var rgb = [
        clampByte((labRgb[0] + linRgb[0]) / 2),
        clampByte((labRgb[1] + linRgb[1]) / 2),
        clampByte((labRgb[2] + linRgb[2]) / 2),
      ];
      out.push({ rgb: rgb, hex: rgbToHex(rgb), endpoint: (i === 0 || i === steps) });
    }
    return out;
  }

  /* ---------- DOM refs ---------- */
  var color1Input = document.getElementById('dr-blend-color1-hex');
  var color2Input = document.getElementById('dr-blend-color2-hex');
  var color1Swatch = document.getElementById('dr-blend-color1-swatch');
  var color2Swatch = document.getElementById('dr-blend-color2-swatch');
  var midpointsSelect = document.getElementById('dr-blend-midpoints');
  var blendBtn = document.getElementById('dr-blend-blend-btn');
  var clearBtn = document.getElementById('dr-blend-clear-btn');
  var statusEl = document.getElementById('dr-blend-status');
  var paletteList = document.getElementById('dr-blend-palette-list');

  for (var i = 1; i <= 13; i++) {
    var opt = document.createElement('option');
    opt.value = i; opt.textContent = i;
    if (i === 2) opt.selected = true;
    midpointsSelect.appendChild(opt);
  }

  function setStatus(msg, isErr) {
    statusEl.textContent = msg || '';
    statusEl.classList.toggle('dr-blend-err', !!isErr);
  }

  function liveSwatchUpdate(input, swatchEl) {
    var v = input.value;
    if (isValidHex(v)) {
      input.classList.remove('dr-blend-invalid');
      var normalized = normalizeHex(v);
      if (input.value !== normalized) input.value = normalized;
      swatchEl.style.background = normalized;
    } else if (v.trim() === '') {
      input.classList.remove('dr-blend-invalid');
      swatchEl.style.background = '';
    } else {
      input.classList.add('dr-blend-invalid');
    }
  }
  color1Input.addEventListener('input', function() { liveSwatchUpdate(color1Input, color1Swatch); });
  color2Input.addEventListener('input', function() { liveSwatchUpdate(color2Input, color2Swatch); });

  function renderPalette(items) {
    paletteList.innerHTML = '';
    if (!items || !items.length) {
      var empty = document.createElement('p');
      empty.className = 'dr-blend-empty';
      empty.textContent = 'No palette yet — set two colors above and press Blend.';
      paletteList.appendChild(empty);
      return;
    }
    items.forEach(function(item) {
      var row = document.createElement('div');
      row.className = 'dr-blend-row' + (item.endpoint ? ' dr-blend-endpoint' : '');
      var hexBtn = document.createElement('button');
      hexBtn.type = 'button';
      hexBtn.className = 'dr-blend-hex-btn';
      hexBtn.textContent = item.hex;
      hexBtn.addEventListener('click', function() { copyHex(item.hex, hexBtn); });
      var sw = document.createElement('div');
      sw.className = 'dr-blend-swatch';
      sw.style.background = item.hex;
      row.appendChild(hexBtn);
      row.appendChild(sw);
      paletteList.appendChild(row);
    });
  }

  function copyHex(hex, btn) {
    var orig = btn.textContent;
    var done = function() { btn.textContent = 'copied!'; setTimeout(function() { btn.textContent = orig; }, 900); };
    if (navigator.clipboard && navigator.clipboard.writeText) {
      navigator.clipboard.writeText(hex).then(done).catch(done);
    } else {
      done();
    }
  }

  function runBlend() {
    var v1 = color1Input.value, v2 = color2Input.value;
    var ok1 = isValidHex(v1), ok2 = isValidHex(v2);
    color1Input.classList.toggle('dr-blend-invalid', !ok1);
    color2Input.classList.toggle('dr-blend-invalid', !ok2);
    if (!ok1 || !ok2) {
      setStatus('Enter a valid 6-digit hex code for both colors.', true);
      return;
    }
    var hex1 = normalizeHex(v1), hex2 = normalizeHex(v2);
    color1Input.value = hex1; color2Input.value = hex2;
    color1Swatch.style.background = hex1;
    color2Swatch.style.background = hex2;
    var midpoints = parseInt(midpointsSelect.value, 10);
    var items = blendPalette(hex1, hex2, midpoints);
    renderPalette(items);
    setStatus(items.length + ' colors generated.');
  }

  blendBtn.addEventListener('click', runBlend);

  clearBtn.addEventListener('click', function() {
    color1Input.value = '';
    color2Input.value = '';
    color1Input.classList.remove('dr-blend-invalid');
    color2Input.classList.remove('dr-blend-invalid');
    color1Swatch.style.background = '';
    color2Swatch.style.background = '';
    renderPalette([]);
    setStatus('');
  });

  /* ---------- picker overlay ---------- */
  var overlay = document.getElementById('dr-blend-picker-overlay');
  var pickerPanel = document.getElementById('dr-blend-picker-panel');
  var pickerTitle = document.getElementById('dr-blend-picker-title');
  var svCanvas = document.getElementById('dr-blend-sv-canvas');
  var svCursor = document.getElementById('dr-blend-sv-cursor');
  var hueCanvas = document.getElementById('dr-blend-hue-canvas');
  var hueCursor = document.getElementById('dr-blend-hue-cursor');
  var pHex = document.getElementById('dr-blend-p-hex');
  var pR = document.getElementById('dr-blend-p-r');
  var pG = document.getElementById('dr-blend-p-g');
  var pB = document.getElementById('dr-blend-p-b');
  var pBefore = document.getElementById('dr-blend-p-before');
  var pAfter = document.getElementById('dr-blend-p-after');
  var toolRoot = document.getElementById('dr-blend-tool');

  var picker = { h: 150, s: 50, v: 30 };
  var activeTarget = null; // 'color1' | 'color2'
  var revertSwatchBg = null;
  var revertInputValue = null;

  // Checks whether the picker panel, at its current position, actually overlaps
  // either color swatch on screen -- works regardless of viewport width/height
  // or which page (embedded blog vs. standalone file) it's running in.
  function rectsOverlap(a, b) {
    return !(a.right < b.left || a.left > b.right || a.bottom < b.top || a.top > b.bottom);
  }
  function pickerOverlapsSwatches() {
    var panelRect = pickerPanel.getBoundingClientRect();
    var r1 = color1Swatch.getBoundingClientRect();
    var r2 = color2Swatch.getBoundingClientRect();
    return rectsOverlap(panelRect, r1) || rectsOverlap(panelRect, r2);
  }

  // Must be called AFTER the overlay is made visible (so the panel has a real,
  // measurable position), but still within the same synchronous call as opening
  // it, so the browser never actually paints the "wrong" position first.
  function positionPickerOverlay() {
    // start from the normal centered position
    overlay.style.alignItems = '';
    overlay.style.paddingTop = '';
    pickerPanel.style.transform = '';

    // Narrow viewports (phones, and CMS "mobile preview" frames, which report
    // their own narrow width even when visually scaled) always get repositioned,
    // regardless of what the collision check below finds -- some preview frames
    // don't report rects reliably, so width is the safer signal there.
    var narrow = window.innerWidth <= 700;
    if (!narrow && !pickerOverlapsSwatches()) return; // plenty of room, natural centering is fine

    // Scroll (instantly, no animation) so Color 1's row sits near the top of the
    // viewport. This guarantees Color 1 / Color 2 are visible AND that there's
    // maximum room left below for the full picker panel to fit without needing
    // a manual scroll -- regardless of where the page happened to be scrolled
    // to when it was opened.
    var firstRow = color1Input.closest('.dr-blend-field-row');
    if (firstRow) {
      var firstRowRect = firstRow.getBoundingClientRect();
      var scrollDelta = firstRowRect.top - 16;
      if (Math.abs(scrollDelta) > 2) {
        window.scrollBy(0, scrollDelta);
      }
    }

    var midRow = midpointsSelect.closest('.dr-blend-field-row');
    var top = 16;
    if (midRow) {
      var rowRect = midRow.getBoundingClientRect();
      top = Math.max(16, rowRect.top);
    }
    overlay.style.alignItems = 'flex-start';
    overlay.style.paddingTop = top + 'px';
    pickerPanel.style.transform = 'translateX(-16px)';

    // if it's still overlapping after that (very narrow/short viewport), nudge further left
    if (pickerOverlapsSwatches()) {
      pickerPanel.style.transform = 'translateX(-40px)';
    }
  }

  function openPicker(target) {
    activeTarget = target;
    var input = target === 'color1' ? color1Input : color2Input;
    var swatchEl = target === 'color1' ? color1Swatch : color2Swatch;
    var label = target === 'color1' ? 'Color 1' : 'Color 2';
    pickerTitle.textContent = 'Pick ' + label;
    revertSwatchBg = swatchEl.style.background;
    revertInputValue = input.value;
    var seedHex = isValidHex(input.value) ? normalizeHex(input.value) : (target === 'color1' ? '#33A365' : '#52298E');
    var hsv = rgbToHsv(hexToRgb(seedHex));
    picker = { h: hsv.h, s: hsv.s, v: hsv.v };
    pBefore.style.background = seedHex;
    overlay.classList.add('dr-blend-open');
    toolRoot.classList.add('dr-blend-picking');
    drawHue();
    drawSv();
    syncPickerUI();
    positionPickerOverlay();
  }
  function closePicker() {
    overlay.classList.remove('dr-blend-open');
    toolRoot.classList.remove('dr-blend-picking');
    activeTarget = null;
  }
  function cancelPicker() {
    if (activeTarget) {
      var input = activeTarget === 'color1' ? color1Input : color2Input;
      var swatch = activeTarget === 'color1' ? color1Swatch : color2Swatch;
      swatch.style.background = revertSwatchBg;
      input.value = revertInputValue;
      input.classList.remove('dr-blend-invalid');
    }
    closePicker();
  }
  function currentPickerRgb() { return hsvToRgb(picker.h, picker.s, picker.v); }

  function drawHue() {
    var ctx = hueCanvas.getContext('2d');
    var w = hueCanvas.width, h = hueCanvas.height;
    var g = ctx.createLinearGradient(0, 0, w, 0);
    for (var i = 0; i <= 6; i++) g.addColorStop(i / 6, 'hsl(' + (i * 60) + ',100%,50%)');
    ctx.fillStyle = g;
    ctx.fillRect(0, 0, w, h);
  }
  function drawSv() {
    var ctx = svCanvas.getContext('2d');
    var w = svCanvas.width, h = svCanvas.height;
    ctx.fillStyle = 'hsl(' + picker.h + ',100%,50%)';
    ctx.fillRect(0, 0, w, h);
    var gw = ctx.createLinearGradient(0, 0, w, 0);
    gw.addColorStop(0, 'rgba(255,255,255,1)'); gw.addColorStop(1, 'rgba(255,255,255,0)');
    ctx.fillStyle = gw; ctx.fillRect(0, 0, w, h);
    var gb = ctx.createLinearGradient(0, 0, 0, h);
    gb.addColorStop(0, 'rgba(0,0,0,0)'); gb.addColorStop(1, 'rgba(0,0,0,1)');
    ctx.fillStyle = gb; ctx.fillRect(0, 0, w, h);
  }
  function syncPickerUI() {
    var rgb = currentPickerRgb();
    var hex = rgbToHex(rgb);
    pHex.value = hex;
    pR.value = rgb[0]; pG.value = rgb[1]; pB.value = rgb[2];
    pAfter.style.background = hex;
    if (activeTarget) {
      var liveSwatch = activeTarget === 'color1' ? color1Swatch : color2Swatch;
      var liveInput = activeTarget === 'color1' ? color1Input : color2Input;
      liveSwatch.style.background = hex;
      liveInput.value = hex;
    }
    svCursor.style.left = (picker.s / 100 * svCanvas.clientWidth) + 'px';
    svCursor.style.top = ((1 - picker.v / 100) * svCanvas.clientHeight) + 'px';
    hueCursor.style.left = (picker.h / 360 * hueCanvas.clientWidth) + 'px';
    svCanvas.style.background = 'hsl(' + picker.h + ',100%,50%)';
  }

  function dragHandler(el, onMove) {
    var dragging = false;
    function posFromEvent(ev) {
      var rect = el.getBoundingClientRect();
      var x = Math.max(0, Math.min(rect.width, ev.clientX - rect.left));
      var y = Math.max(0, Math.min(rect.height, ev.clientY - rect.top));
      return { x: x, y: y, w: rect.width, h: rect.height };
    }
    el.addEventListener('pointerdown', function(ev) { dragging = true; el.setPointerCapture(ev.pointerId); onMove(posFromEvent(ev)); ev.preventDefault(); });
    el.addEventListener('pointermove', function(ev) { if (dragging) onMove(posFromEvent(ev)); });
    el.addEventListener('pointerup', function() { dragging = false; });
    el.addEventListener('pointercancel', function() { dragging = false; });
  }
  dragHandler(svCanvas, function(p) { picker.s = p.x / p.w * 100; picker.v = (1 - p.y / p.h) * 100; syncPickerUI(); });
  dragHandler(hueCanvas, function(p) { picker.h = p.x / p.w * 360; drawSv(); syncPickerUI(); });

  pHex.addEventListener('input', function() {
    if (!isValidHex(pHex.value)) return;
    var hsv = rgbToHsv(hexToRgb(pHex.value));
    picker = { h: hsv.h, s: hsv.s, v: hsv.v };
    drawSv(); syncPickerUI();
  });
  [pR, pG, pB].forEach(function(field) {
    field.addEventListener('input', function() {
      var rgb = [pR, pG, pB].map(function(f) { return Math.max(0, Math.min(255, parseInt(f.value, 10) || 0)); });
      var hsv = rgbToHsv(rgb);
      picker = { h: hsv.h, s: hsv.s, v: hsv.v };
      drawSv(); syncPickerUI();
    });
  });

  color1Swatch.addEventListener('click', function() { openPicker('color1'); });
  color2Swatch.addEventListener('click', function() { openPicker('color2'); });
  document.getElementById('dr-blend-picker-cancel').addEventListener('click', cancelPicker);
  document.getElementById('dr-blend-picker-ok').addEventListener('click', function() {
    var hex = rgbToHex(currentPickerRgb());
    var input = activeTarget === 'color1' ? color1Input : color2Input;
    var swatch = activeTarget === 'color1' ? color1Swatch : color2Swatch;
    input.value = hex;
    input.classList.remove('dr-blend-invalid');
    swatch.style.background = hex;
    closePicker();
  });
  overlay.addEventListener('click', function(ev) { if (ev.target === overlay) cancelPicker(); });

  /* ---------- initial state ---------- */
  liveSwatchUpdate(color1Input, color1Swatch);
  liveSwatchUpdate(color2Input, color2Swatch);
  runBlend();
})();
</script>
</body>
</html>