Hex color blender - html app
August 11, 2026
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>