3
\$\begingroup\$

I'm trying to build a generic thing that will update a fixed menu button's color to either 'black' or 'white' depending on which contrasts better with the background color. I've got it working, but I want to optimize the code.

I'm curious how I can improve organization. Am I optimally using ES6? Are there any opportunities to make the code more DRY?

Codepen

/*jshint esversion: 6 */

/* 
 ** Helper Functions
 */
const helperFunctions = {
  // Color contrast
  getContrastYIQ: function getContrastYIQ(rgb) {
    rgb = rgb.substring(4, rgb.length - 1)
      .replace(/ /g, '')
      .split(',');
    const yiq = ((rgb[0] * 299) + (rgb[1] * 587) + (rgb[2] * 114)) / 1000;
    return (yiq >= 128) ? 'black' : 'white';
  },

  // Get element position relative to viewport
  offsetTop: function offsetTop(elem) {
    const offset = {
      top: 0,
      left: 0
    };
    if (elem && elem.getBoundingClientRect) { // check if available
      const rect = elem.getBoundingClientRect();
      offset.top = rect.top;
      offset.left = rect.left;
    }
    return offset.top;
  },

  // DOM Manipulation
  addClass: function addClass(elem, classname) {
    if (classname) {
      if (elem.classList)
        elem.classList.add(classname);
      else
        elem.className += ' ' + classname;
    }
  },

  removeClass: function removeClass(elem, classname) {
    if (classname) {
      if (elem.classList)
        elem.classList.remove(classname);
      else
        elem.className = elem.className.replace(new RegExp('(^|\\b)' + classname.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');
    }
  }
};

/* 
 ** Init
 */
const sections = document.querySelectorAll('section');
const sectionCollection = [];
const menu = document.querySelector('.menu-icon');
const menuOffset = helperFunctions.offsetTop(menu);

// Setting up section obejcts
for (let i = 0; i < sections.length; i++) {

  sectionCollection[i] = new(function() {
    this.element = sections[i];

    // Figuring out best text color - black or white
    this.backgroundColor = window.getComputedStyle(this.element, null).getPropertyValue('background-color');
    this.textColor = helperFunctions.getContrastYIQ(this.backgroundColor);

    // Getting the previous text color, unless it's the first section, where we just default to the same color
    this.previousBackgroundColor = (i > 0) ? window.getComputedStyle(sections[i - 1], null).getPropertyValue('background-color') : this.backgroundColor;
    this.previousTextColor = helperFunctions.getContrastYIQ(this.previousBackgroundColor);

    this.triggered = null;
  });

  console.log(sectionCollection[i]);
}

/* 
 ** Event trigger
 */
window.addEventListener('scroll', function passElement() {
  for (let j = 0; j < sectionCollection.length; j++) {
    let sectionOffset = helperFunctions.offsetTop(sectionCollection[j].element);

    if (sectionOffset < menuOffset && sectionCollection[j].triggered != 'passedDown') {
      helperFunctions.removeClass(menu, sectionCollection[j].previousTextColor);
      helperFunctions.addClass(menu, sectionCollection[j].textColor);
      sectionCollection[j].triggered = 'passedDown';
    }

    if (sectionOffset > menuOffset && sectionCollection[j].triggered != 'passedUp') {
      helperFunctions.removeClass(menu, sectionCollection[j].textColor);
      helperFunctions.addClass(menu, sectionCollection[j].previousTextColor);
      sectionCollection[j].triggered = 'passedUp';
    }
  }
});
html,
body {
  color: white;
  text-align: center;
}

section {
  padding: 5vh;
  min-height: 100vh;
}

h1 {
  padding-top: 5vh;
  text-transform: uppercase;
}

p {
  padding: 15vh 15vw;
}

a,
a:link,
a:active,
a:visited,
a:hover {
  color: #d30;
  text-decoration: none;
}

a:hover {
  text-decoration: underline;
}

.menu-icon {
  position: fixed;
  text-align: right;
  line-height: 1.5em;
  right: 3rem;
  top: 3rem;
  z-index: 100;
  padding-right: 2em;
  color: white;
  cursor: pointer;
  &:before {
    content: "";
    position: absolute;
    right: 0;
    top: 0.15em;
    width: 1.2em;
    height: 0.2em;
    background: white;
    box-shadow: 0 0.4em 0 0 white, 0 0.80em 0 0 white;
  }
}

.black {
  color: black;
  &:before {
    background: black;
    box-shadow: 0 0.4em 0 0 black, 0 0.80em 0 0 black;
  }
}


.white {
  color: white;
  &:before {
    background: white;
    box-shadow: 0 0.4em 0 0 white, 0 0.80em 0 0 white;
  }
}

svg {
  margin-top: 30vh;
  max-width: 5vw;
  fill: none;
  stroke: #FFFFFF;
  stroke-width: 15;
  stroke-linecap: round;
  stroke-miterlimit: 8;
  stroke-dasharray: 0.01, 28;
}

#s0 {
  background-color: black;
}

#s1 {
  background-color: white;
}

#s2 {
  background-color: #111;
}

#s3 {
  background-color: #9f3;
}

#s4 {
  background-color: #145;
}

#s5 {
  margin: none;
  color: black;
  background-color: #f5f;
  a {
    color: black;
    font-weight: 700;
  }
}
<div class="menu-icon">
  MENU</div>
<section id="s0">
  <h1>The Amazing Changing Color Menu Button</h1>
  <p>Scoll Down to see it in action</p>
  <svg version="1.1" id="arrow" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 302 192" style="enable-background:new 0 0 302 192;" xml:space="preserve">
<line class="st0" x1="40.2" y1="86.5" x2="156.8" y2="169.2" style="display: block; opacity: 1; transform: translateY(0px);"></line>
<line class="st0" x1="175.2" y1="153" x2="269.8" y2="86.5" style="display: block; opacity: 1; transform: translateY(0px);"></line>
<line class="st0" x1="40.2" y1="56.5" x2="156.8" y2="139.2" style="display: block; opacity: 1; transform: translateY(0px);"></line>
<line class="st0" x1="175.2" y1="123" x2="269.8" y2="56.5" style="display: block; opacity: 1; transform: translateY(0px);"></line>
<line class="st0" x1="40.2" y1="26.5" x2="156.8" y2="109.2" style="display: block; opacity: 1; transform: translateY(0px);"></line>
<line class="st0" x1="175.2" y1="93" x2="269.8" y2="26.5" style="display: block; opacity: 1; transform: translateY(0px);"></line>
</svg>
</section>
<section id="s1"></section>
<section id="s2"></section>
<section id="s3"></section>
<section id="s4">
  <p>Menu color is changed by grabbing each section's background-color property.</p><code>window.getComputedStyle(section, null).getPropertyValue('background-color');</code>
  <p>The returned <a href="https://en.wikipedia.org/wiki/RGB_color_model">RGB</a> value is then converted to <a href="https://en.wikipedia.org/wiki/YIQ">YIQ</a>.</br>A value of 128 or above in YIQ means text should be black for legibility, otherwise our
    text is white.</p>
</section>
<section id="s5">
</section>

\$\endgroup\$
2
\$\begingroup\$

Two things stand out to me right away.

First, your getContrastYIQ function doesn't so much return a "contrast" metric; it returns a color directly. I'd prefer the function to just calculate the Y (luma) component of the YIQ color, and let another piece of code decide what to do with that information.

I'd also consider extracting the color-string parsing, since - if you're doing things with color - that might be useful later on. That'd reduce your function to one that only calculates the Y component, and done.

Secondly, rather than rolling your own addClass removeClass, I'd consider using the existing classList API. Sure, support is spotty in Internet Explorer, but adding/removing a single class works.

One thing your code doesn't take into account is transparency. If, for instance, the background for the text is more or less transparent, you'd basically need to calculate the composited color of the background on top of its background, etc. etc.. I wouldn't recommend trying to do all that, but rather just make it clear that it's a limitation of the code.

\$\endgroup\$
  • \$\begingroup\$ Thanks for the feedback Flambino! Sounds like you're recommending 3 functions: getRGBColor, getYIQLuma & getBestContrastColor? Am I on the right track for the 1st part of your suggestion? \$\endgroup\$ – Gene Kobilansky Mar 7 '17 at 17:32
  • \$\begingroup\$ I do feel a this approach might be overkill because now I'm saving luma and previous luma on every section object. I guess it could be helpful for other manipulations. I updated the codepen with this new implementation. \$\endgroup\$ – Gene Kobilansky Mar 7 '17 at 17:46
  • \$\begingroup\$ @GeneKobilansky Yeah, for the first part, that's what I was thinking (make 3 functions). Sure, it might be a little overkill, depending on your usecase, but breaking a problem down into its individual parts can often yield cleaner, clearer code. I don't have the time to take a closer look unfortunately, but you're always welcome to post a new question with updated code, and people here can give it a review. That's the preferred way of doing things, if you update code: Brand new question \$\endgroup\$ – Flambino Mar 7 '17 at 19:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.