'How to fade smoothly between gradients in JavaScript

This code populates a div with a predefined set of gradients and fades through them in a cycle using jQuery's .animate() method:

/// Background Gradient Cycler
var gradients = [
  ['#9eb5d7', '#242424'],
  ['#efe2ae', '#a8acc9'],
  ['#6f7554', '#eee1ad']
]
var gradientsRev = gradients.reverse()
var gradientCover = document.getElementById('gradientCover');
for (var g = 0; g < gradientsRev.length; g++) {
  var gradEl = document.createElement('div')
  gradEl.className = 'gradient'
  gradEl.style.background = `linear-gradient(${gradientsRev[g][0]}, ${gradientsRev[g][1]})`;
  gradientCover.appendChild(gradEl)
}
var gradientEls = document.querySelectorAll('#gradientCover .gradient')

function gradientCycler() {
  function gradeFade(i, opDest) {
    var fadeDur = 20000
    $(gradientEls[i]).animate({
      'opacity': opDest
    }, {
      duration: fadeDur,
      complete: function() {
        if (parseInt(i) > 1) {
          if (parseInt(opDest) === 0) gradeFade(i - 1, 0)
          else gradFadeStart()
        } else {
          gradeFade(gradientEls.length - 1, 1)
        }
      }
    })
  }
  var gradFadeStart = function() {
    $('.gradient').css('opacity', 1);
    gradeFade(gradientEls.length - 1, 0)
  }
  gradFadeStart()
}
gradientCycler()
body {
  margin: 0;
  overflow: hidden;
}

#gradientCover,
.gradient {
  position: absolute;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
}
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<div id="page">
  <div id="gradientCover"></div>
</div>

The problem is that the transition at certain parts is visibly choppy, with banding artifacts --

enter image description here

What can be done to reduce this artifacting so that the transition between the gradients appears smoother and less choppy?



Solution 1:[1]

I think is related to color depth, As a former CG artist I have seen these "artefacts" in software like Maya and Photoshop, to solve the problem it was necessary to increase the number of bits per channel (in Photoshop going from 8 Bits/Channel to 16).

Normally, this issue of bands appears when the two colors of the gradient are close (in term of RGB values) because there are few values of colors available between these two colors

If the gradient is rendered OR displayed (due to the monitor limitation) at a low number of bits per channel these banding effect can appear.

You can check your monitor color depth here.

You can also apply CSS according to this value:

if (screen.colorDepth <= 8)
  //simple blue background color for 8 bit screens
  document.body.style.background = "#0000FF"
else
  //fancy blue background color for modern screens
  document.body.style.background = "#87CEFA"

Solution 2:[2]

Including my codepen tests here as an answer just in case it helps anyone. I managed to get some improved performance by pre-rendering PNGs with the canvas' .toDataURL() method and animating through them in jQuery (also achieved about the same performance with this method but using Three.js):

https://codepen.io/tv/zxBbgK

Here's a working example of the JQuery method:

/// Background Gradient Cycler
var gradients = [
  ['#9eb5d7', '#242424'],
  ['#efe2ae', '#a8acc9'],
  ['#6f7554', '#eee1ad']
]
var gradientsRev = gradients.reverse()
var gradientCover = document.getElementById('gradientCover');
for (var g = 0; g < gradientsRev.length; g++) {
  var gradEl = document.createElement('div')
  gradEl.className = 'gradient'
  var gradCanv = document.createElement('canvas')
  gradCanv.className = 'gradient'
  var ctx = gradCanv.getContext("2d");
  var grd = ctx.createLinearGradient(0, 0, 0, gradCanv.height); 
  grd.addColorStop(0, gradients[g][0]);
  grd.addColorStop(1, gradients[g][1]);
  ctx.fillStyle = grd;
  ctx.fillRect(0, 0, gradCanv.width, gradCanv.height);
  var gradIm = gradCanv.toDataURL("img/png")
  gradEl.style.backgroundImage = `url(${gradIm})`
  gradientCover.appendChild(gradEl)
}
var gradientEls = document.querySelectorAll('#gradientCover .gradient')

function gradientCycler() {
  function gradeFade(i, opDest) {
    var fadeDur = 20000
    $(gradientEls[i]).animate({
      'opacity': opDest
    }, {
      duration: fadeDur,
      complete: function() {
        if (parseInt(i) > 1) {
          if (parseInt(opDest) === 0) gradeFade(i - 1, 0)
          else gradFadeStart()
        } else {
          gradeFade(gradientEls.length - 1, 1)
        }
      }
    })
  }
  var gradFadeStart = function() {
    $('.gradient').css('opacity', 1);
    gradeFade(gradientEls.length - 1, 0)
  }
  gradFadeStart()
}
gradientCycler()
body {
  margin: 0;
  overflow: hidden;
}

#gradientCover,
.gradient {
  position: absolute;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  background-size: 100%;
}
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<div id="page">
  <div id="gradientCover"></div>
</div> 

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Ben Souchet
Solution 2