4

I have multiple divs, each of the same class. The class is semi transparent (by setting opacity: 0.2;).

The layout is pretty complex, and occasionally those divs will overlap.

The problem is that wherever they are overlapping the opacity adds up, so the overlapped area is darker. The more elements overlap, the darker it gets. See this image for an explanation:enter image description here

(red and blue borders were added for clarity, they are not present in the real thing)

I'm looking for a way to prevent this, so that the color in the overlapping region does not further darken. Is there a way to do that? Some fancy "mix mode" of sorts?

Ideally, it could all be done in CSS.

Here is an example in JSfiddle: https://jsfiddle.net/begkw16d/

Would appreciate any help. Thank you very much...

jbrendel
  • 2,390
  • 3
  • 18
  • 18
  • 3
    With your rep amount you know a reproducible example is kind of nice to have....either way this would be an interesting one and tough to do as far as I know with just css but might be able to get a good result with some tricks using linear-gradient etc. – Chris W. Apr 28 '22 at 20:36
  • 1
    Try this maybe? https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode – AStombaugh Apr 28 '22 at 20:40
  • @AStombaugh I tried the blend modes... they make a very tiny difference, but just not enough. – jbrendel Apr 29 '22 at 23:02
  • 1
    @AStombaugh I was looking for the opposite solution: to further darken the overlapping areas. Using `mix-blend-mode: multiply` did the trick for me. – jimmy Jun 13 '23 at 19:00

1 Answers1

0

If you are using transparency over a solid background, this solution will work well. If you have a Figma design you can select the transparent element, click the color, click the eye dropper, and select the element again. This will provide a solid hex color that looks exactly the same as the transparent color over the solid background. You can do the same with print screen and a photo editor. This will allow you to keep the same color without having the elements darken when you overlap them. If you'd prefer to do this programmatically with colors provided by the user, then this could help. https://codepen.io/maverickcer/pen/wvxmaea

let hexResult, rgbResult;
function updateData() {
  let background = document.getElementById('background');
  let bgTextarea = document.getElementById('bg');
  let foreground = document.getElementById('foreground');
  let preview = document.getElementById('preview');
  let result = document.getElementById('result');
  let rgb = document.getElementById('rgb');
  let hex = document.getElementById('hex');
  let rgbaTextarea = document.getElementById('rgba');
  let rgba = hexToRgb(rgbToHex(rgbaTextarea.value));
  let bg = hexToRgb(rgbToHex(bgTextarea.value));
  rgbResult = rgbaRgbToRGB(rgba, bg); // define locally if you don't need to copy to clipboard
  hexResult = rgbToHex(rgbResult); // define locally if you don't need to copy to clipboard
  let textColor = lumens(rgbResult);
  background.style.backgroundColor = bgTextarea.value;
  foreground.style.backgroundColor = rgbaTextarea.value;
  foreground.style.color = textColor;
  if (rgbaTextarea.value.indexOf('#') !== -1 && rgbaTextarea.value.indexOf(' ') !== -1) {
    foreground.style.opacity = rgbaTextarea.value?.split(' ')[1];
  }
  result.style.backgroundColor = rgbResult;
  result.style.color = textColor;
  rgb.innerHTML = rgbResult;
  hex.innerHTML = hexResult;
}

function hexToRgb(hex) {
  if (hex.indexOf('rgb') !== -1) return hex;
  if (hex.indexOf(',') !== -1) return hex;
  var value = hex?.split(' ')?.[0]?.replace(/:;\s+/g, ''),
    percentAlpha = hex?.split(' ')?.[1]?.replace(/%/g, ''),
    rValue = parseInt(value.slice(1, 3), 16),
    rColor = rValue > 255 ? '255' : rValue < 0 ? '0' : rValue?.toString(),
    gValue = parseInt(value.slice(3, 5), 16),
    gColor = gValue > 255 ? '255' : gValue < 0 ? '0' : gValue?.toString(),
    bValue = parseInt(value.slice(5, 7), 16),
    bColor = bValue > 255 ? '255' : bValue < 0 ? '0' : bValue?.toString(),
    aValue =
      percentAlpha === undefined
        ? parseInt(value.slice(7, 9), 16) !== NaN
          ? parseInt(value.slice(7, 9), 16)
          : 255
        : percentAlpha > 1
          ? percentAlpha / 100 * 255
          : percentAlpha * 255,
    alpha = aValue >= 255 ? '1' : aValue < 1 ? '0' : (aValue / 255)?.toString(),
    final = `${
      alpha === '1' || alpha === 'NaN'
        ? `rgb(${rColor},${gColor},${bColor})`
        : `rgba(${rColor},${gColor},${bColor},${alpha})`
    }`,
    test =
      /(^rgb\((\d+),\s*(\d+),\s*(\d+)\)$)|(^rgba\((\d+),\s*(\d+),\s*(\d+),\s*(\d+(.\d+)?)*\)$)/i.exec(
        final
      );
  return test ? final : 'rgb(255,255,255)';
}

function rgbToHex(rgb) {
  if (rgb?.indexOf('#') !== -1) return rgb;
  var value = rgb.replace(/([rgba()\s])/g, '')?.split(',')
    rValue = parseInt(value[0], 10),
    rString = rValue > 255 ? 'ff' : rValue < 0 ? '00' : rValue.toString(16),
    rColor = rString ? (rString.length === 1 ? '0' + rString : rString) : 'ff',
    gValue = parseInt(value[1], 10),
    gString = gValue > 255 ? 'ff' : gValue < 0 ? '00' : gValue.toString(16),
    gColor = gString ? (gString.length === 1 ? '0' + gString : gString) : 'ff',
    bValue = parseInt(value[2], 10),
    bString = bValue > 255 ? 'ff' : bValue < 0 ? '00' : bValue.toString(16),
    bColor = bString ? (bString.length === 1 ? '0' + bString : bString) : 'ff',
    aValue =
        !value[3] || parseFloat(value[3]) === 1
        ? 255 // undefined alpha is opaque
        : parseFloat(value[3]) === 0
        ? 0 // zero alpha is transparent
        : value[3]?.indexOf('%') !== -1 // value is percent
        ? (parseFloat(value[3], 10) / 100) * 255 // divide by 100 multiply by 255
        : value[3]?.indexOf('.') !== -1 // value is decimal
        ? parseFloat(value[3], 10) > 1
          ? parseInt(value[3], 10) // value out of 255
          : parseInt(value[3] * 255, 10) // multiply by 255
        : parseInt(value[3], 10) // value exists
        ? parseInt(value[3], 10) // value out of 255
        : 255, // default to 255
    aString =
        aValue >= 255
        ? 'ff'
        : aValue <= 0
        ? '00'
        : parseInt(aValue)?.toString(16),
    aColor = aString.length === 1 ? '0' + aString : aString,
    final = `#${rColor}${gColor}${bColor}${aColor !== 'ff' ? aColor : ''}`.toLowerCase(),
    test = /^#?([a-f0-9]{8}|[a-f0-9]{6}|[a-f0-9]{3})$/i.exec(final);
  return test ? final : '#FFFFFF';
}

function rgbaRgbToRGB(rgba, bg) {
  let rgbav = rgba.split(' ')[0].replace(/([(rgb)a])/g, '').split(','),
    bgv = bg.replace(/([(rgb)a])/g, '').split(','),
    alpha = parseFloat(rgbav[3], 10),
    beta = 1 - alpha,
    alphaR = parseInt(rgbav[0] || 0, 10),
    alphaG = parseInt(rgbav[1] || 0, 10),
    alphaB = parseInt(rgbav[2] || 0, 10),
    betaR = parseInt(bgv[0] || 0, 10),
    betaG = parseInt(bgv[1] || 0, 10),
    betaB = parseInt(bgv[2] || 0, 10),
    targetR = parseInt((beta * (betaR / 255) + alpha * (alphaR / 255)) * 255),
    targetG = parseInt((beta * (betaG / 255) + alpha * (alphaG / 255)) * 255),
    targetB = parseInt((beta * (betaB / 255) + alpha * (alphaB / 255)) * 255),
    final = `rgb(${targetR},${targetG},${targetB})`;
  return alpha?.toString() === 'NaN' || alpha >= 1 ? rgba : alpha <= 0 ? bg : final;
}

function lumens(color) {
  var nums = hexToRgb(color).replace(/([rgba()\s])/g, '')?.split(','),
    r = parseInt(nums[0], 10),
    g = parseInt(nums[1], 10),
    b = parseInt(nums[2], 10),
    lum = (r * 299 + g * 587 + b * 114) / 1000;
  if (lum >= 128) {
    return 'rgb(255,255,255)';
  }
  return 'rgb(0,0,0)';
}

function copyRGB() {
  navigator.clipboard.writeText(rgbResult);
}

function copyHex() {
  navigator.clipboard.writeText(hexResult);
}

updateData();
body {
  overflow: hidden;
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  height: 100vh;
  width: 100vw;
  padding: 0;
  margin: 0;
}
form {
  display: flex;
  flex-direction: column;
  width: calc(100% - 6px);
}
textarea {
  width: 100%;
  height: 100%;
}
#preview {
  position: relative;
}
#background {
  width: 100%;
  height: 100%;
  position: absolute;
  z-index: 1;
  top: 0;
  left: 0;
}
#foreground {
  width: 100%;
  height: 100%;
  position: absolute;
  z-index: 1;
  top: 0;
  left: 0;
  display: flex;
  flex-direction: column;
  padding-top: 3px;
}
#foreground > div {
  height: 50%;
}
#result {
  display: flex;
  flex-direction: column;
}
#hex, #rgb {
  width: 100%;
  height: 100%;
}
#hex {
  padding-top: 3px;
}
#rgb {
  padding-top: 3px;
}
<!--
This simple converter helps determine the solid rgb code for any translucent forground color on an opaque background color. For accessibility, the text color is changed for maximum contrast. Please note that maximum contrast might not be enough contrast in some scenarios and you should always follow the lastest WCAG.

To get started, simply copy/paste the rgb or hex values into the background container and the rgba or octal values into the rgba container. The other areas will automatically update to show the actual real world example in the middle and the calculated value on the right.

Opacity is great in a design to keep a similar color palette and maintian cohesion, but it is horrible for use in responsive and dynamic designs due overlapping elements or the same colors being applied to other elements. This tool allows you to keep pixel perfect designs while making them easier to work with.
-->
<form>
  <textarea id="bg" oninput="updateData()" placeholder="background (rgb or hex only)">rgb(255,255,255)</textarea>
  <textarea id="rgba" oninput="updateData()" placeholder="foreground (rgba, octal, or hex opacity)">rgba(0,0,255,0.625)</textarea>
</form>
<div id="preview">
  <span id="background"></span>
  <span id="foreground">
    <div>background (rgb or hex only)</div>
    <div>foreground (rgba, octal, or hex opacity)</div>
  </span>
</div>
<div id="result">
  <span id="hex" onclick="copyHex()"></span>
  <span id="rgb" onclick="copyRGB()"></span>
</div>