3

I am developing a functionality to let the user choose a dark or light theme and also a theme color for the app. I'm doing it through the css filter, specifically invert(1) to get the dark theme and hue-rotate for the theme color. The dark-light part works perfectly because I excluded some classes from being affected by the invert() filter, mainly images and some svgs, by just setting invert(0) to them. But, as the theme color chooser works with a range selector to bring many color schemes, I can not use the same method to exclude those classes because the value in degrees of the hue-rotate() changes with the user selection. It still can be done if I would be able to programatically calculate the opposite hue an reaply it to those classes, but I've just messed up in the proccess as my javascript knowledge is very limited. So I would appreciate any possible help on accomplishing that.

So I came up with this code:

HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link href="css/light.css" rel="stylesheet" id="theme-link">
</head>
<body id="usrhue">

<h3><label><b>Theme</b></label></h3>
<input type="checkbox" onchange="toggleTheme()"/>
<h3>Theme Color</h1>
<pre><h3><code id="value">hue-rotate(0deg)</code></h3></pre>
<input id="hue-rotate" step="45" type="range" min="0" max="315" value="0">
<div>
<div class="tile">navbar</div>
<svg class="icon" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" x="0" y="0" width="50" height="33" viewbox="0 0 50 33"><path style="fill-opacity:1;fill-rule:evenodd;stroke:none" d="m 50,16.111089 c -0.0023,-0.52719 -0.269301,-1.175411 -0.836045,-1.837787 L 33.538651,0 l 0,4.099986 10.560199,12.011103 -10.560199,12.011102 0,4.099986 15.625304,-14.273302 C 49.730699,17.286499 49.997802,16.638278 50,16.111089 Z m -50,0 c 0.0022,-0.52719 0.2693022,-1.175411 0.8360452,-1.837787 L 16.46135,0 l 0,4.099986 -10.5601995,12.011103 10.5601995,12.011102 0,4.099986 L 0.8360452,17.948875 C 0.2693022,17.286499 0.0021979,16.638278 0,16.111089 Z"/></svg>
</div>

<img src="https://campstoregear.com/wp-content/uploads/2017/09/summer-camp-2018-apparel-design-CD1816-Primary.png">

</body>
</html>

JS

// Theme Chooser
const theme = document.querySelector('#theme-link');
const currentTheme = localStorage.getItem('theme');

if( currentTheme == 'dark' && theme.getAttribute('href')=='css/light.css' ){
theme.href='css/dark.css';
}

function toggleTheme(){
if( theme.getAttribute('href') == 'css/light.css' ) {
    theme.href = 'css/dark.css';
    themeName = 'dark';
} else {
    theme.href = 'css/light.css';
    themeName = 'light';
}
localStorage.setItem( 'theme', themeName );
};

// Theme Color Chooser

const currentThemec = localStorage.getItem('themeColor');
const value = document.getElementById('value');
const body = document.getElementById('usrhue');

if (currentThemec) {
const themeColor = currentThemec;
body.style.filter = currentThemec;
value.textContent = currentThemec;
}

document.getElementById('hue-rotate').oninput = function() {
const filter = 'hue-rotate(xdeg)'.replace('x', this.value);
value.textContent = filter;
body.style.filter = filter;

localStorage.setItem( 'themeColor', body.style.filter );
}

LIGHT.CSS

html {
height:100%;
overflow:hidden;
}

body {
display: flex;
height:100%;
justify-content: center;
align-items:center;
flex-direction:column;
background:#999;
}

input {
margin-bottom:5px;
}

.tile {
width:50px;
height:50px;
display:inline-block;
}

.tile:nth-child(1) {
background: #006648;
}

.tile:nth-child(2) {
background: #007aff;
}

pre {
color:#ff0000;
}

img {
width:5%;
}

h3 {
color:#0000ff;
}

svg  {
fill:#007aff;
width:45px;
display: flex;
justify-content: center;
align-items:center;
flex-direction:column;
}

DARK.CSS

Dark css is the same as light but adding this at the bottom

html, img, svg, pre, code { filter: invert(1) contrast(1) saturate(1);}

.webp, .png, .jpg { filter: invert(0) contrast(1) saturate(1);}

As you will notice if you toggle between light and dark this action do not affect the svg icon nor the image.

But when you change theme color they both are affected.

So the question is to solve this issue by this way (maybe you could think a second, third, fourth or more ways) programatically calculate the opposite hue for each stage of the hue range selector and dynamically apply it to those classes.

I have done some calculation with the help of the inspect dev tool and, for example, let's say that you choose the first stage of the selector, 45deg, then if you apply to img invert(1) hue-rotate(-45deg) on the dark.css and invert(0) hue-rotate(-45deg) on the light.css, then for that specific hue value, the image remains ok. Following the same thread of thinking it is very easy to calculate all the 7 values. But the question of applying them in a dynamic way is still being a huge problem to my limited javascript skills.

I also have found this chunk of code here How to transform black into any given color using only CSS filters

HUE-ROTATE CALCULATION

function hueRotate(angle = 0) {
angle = angle / 180 * Math.PI;
let sin = Math.sin(angle);
let cos = Math.cos(angle);

this.multiply([
    0.213 + cos * 0.787 - sin * 0.213, 0.715 - cos * 0.715 - sin * 0.715, 0.072 - cos * 0.072 + sin * 0.928,
    0.213 - cos * 0.213 + sin * 0.143, 0.715 + cos * 0.285 + sin * 0.140, 0.072 - cos * 0.072 - sin * 0.283,
    0.213 - cos * 0.213 - sin * 0.787, 0.715 - cos * 0.715 + sin * 0.715, 0.072 + cos * 0.928 + sin * 0.072
]);}

Maybe someone with more js experience and ability than mine could use it to accomplish what I need.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
SIMBIOSIS surl
  • 357
  • 3
  • 15
  • Would setting them using CSS variables make it easier? (setting the CSS variables through JS as needed(. – A Haworth Jul 29 '21 at 21:10
  • The problem using only css is that it's a functionality to let the user to choose a color theme to the whole app. And I need the js to make that possible. In this case the provided code is working fine but it applies the hue-rotate filter to images too and that's not desired. Could you please provide an example of what you are proposing? – SIMBIOSIS surl Jul 30 '21 at 11:17

2 Answers2

1

If i'm right, you want to switch the dark theme to the light theme and colorize ui element when user select some color. I redesigned for a more presentable view. I did use previrous solution and improve it.

const value = document.getElementById('value');
const body = document.getElementById('usrhue');
const switcher = document.getElementById('switcher');
const hueRotate = document.getElementById('hue-rotate');

const LIGHT_THEME = 'light';
const DARK_THEME = 'dark';

value.textContent = `Color: Tomato`;
body.classList.add(DARK_THEME);
body.classList.add('tomato');

const toggleTheme = ev => {
  if (ev.target.checked) {
    body.classList.add(LIGHT_THEME);
    body.classList.remove(DARK_THEME);
    return;
  }
  body.classList.add(DARK_THEME);
  body.classList.remove(LIGHT_THEME);
};

const setClassName = (name, color, num, el) => {
  const colorArr = [
    'tomato',
    'wood',
    'green-gross',
    'green-house',
    'fun-green',
    'sky-blue',
    'vivid-violet',
    'rose-guns',
  ];

  const className = name.toLowerCase().split(' ').join('-');
  colorArr.forEach(col => {
    if (col === className) {
      return el.classList.add(className);
    }
    el.classList.remove(col);
  });
};

switcher.addEventListener('change', toggleTheme);

hueRotate.oninput = function() {
  const filter = 'hue-rotate(xdeg)'.replace('x', this.value);
  let name;

  switch (this.value) {
    case '45':
      name = `Wood`;
      setClassName(name, filter, this.value, body);
      return (value.textContent = `Color:${name}`);
    case '90':
      name = `Green House`;
      setClassName(name, filter, this.value, body);
      return (value.textContent = `Color: ${name}`);
    case '135':
      name = `Green Gross`;
      setClassName(name, filter, this.value, body);
      return (value.textContent = `Color: ${name}`);
    case '180':
      name = `Fun Green`;
      setClassName(name, filter, this.value, body);
      return (value.textContent = `Color: ${name}`);
    case '225':
      name = `Sky Blue`;
      setClassName(name, filter, this.value, body);
      return (value.textContent = `Color: ${name}`);
    case '270':
      name = `Vivid Violet`;
      setClassName(name, filter, this.value, body);
      return (value.textContent = `Color: ${name}`);
    case '315':
      name = `Rose Guns`;
      setClassName(name, filter, this.value, body);
      return (value.textContent = `Color: ${name}`);
    default:
      name = `Tomato`;
      setClassName(name, filter, this.value, body);
      return (value.textContent = `Color: ${name}`);
  }
};
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-family: serif;
}

:root {
  --background-color: hsl(240 14% 15% / 1);
  --context-color: hsl(217 8% 68% / 1);
  --buttons-color: var(--context-color);

  --tomato: hsl(0 50% 64% / 1);
  --wood: hsl(45 50% 50% / 1);
  --green-house: hsl(90 50% 50% / 1);
  --green-gross: hsl(135 50% 50% / 1);
  --fun-green: hsl(180 50% 50% / 1);
  --sky-blue: hsl(225 50% 50% / 1);
  --vivid-violet: hsl(270 50% 50% / 1);
  --rose-guns: hsl(315 50% 50% / 1);
}
.dark {
  --background-color: hsl(240 14% 15% / 1);
  --context-color: hsl(217 8% 68% / 1);
  --buttons-color: var(--context-color);
}
.light {
  --background-color: hsl(217 8% 68% / 1);
  --context-color: hsl(240 14% 15% / 1);
  --buttons-color: var(--context-color);
}
.tomato {
  --buttons-color: var(--tomato);
}
.wood {
  --buttons-color: var(--wood);
}
.green-house {
  --buttons-color: var(--green-house);
}
.green-gross {
  --buttons-color: var(--green-gross);
}
.fun-green {
  --buttons-color: var(--fun-green);
}
.sky-blue {
  --buttons-color: var(--sky-blue);
}
.vivid-violet {
  --buttons-color: var(--vivid-violet);
}
.rose-guns {
  --buttons-color: var(--rose-guns);
}

html {
  height: 100%;
}

body {
  display: grid;
  grid-template-columns: 200px minmax(min-content, 500px);
  grid-template-rows: 70px max-content;
  height: 100%;
  align-items: center;
  justify-content: center;
  padding: 0px 10px 20px 10px;
  background-color: var(--background-color);
  color: var(--context-color);
}

button {
  border-radius: 3px;
  background-color: var(--buttons-color);
  color: black;
  transition: all 0.3s ease-in-out;
}
button:hover {
  filter: brightness(0.8);
}

#control {
  display: flex;
  gap: 50px;
}
#theme-switcher,
#theme-colors {
  grid-row: 1;
  padding: 10px;
  color: var(--context-color);
}
#theme-switcher {
  grid-column: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
}
#theme-colors {
  grid-column: 2;
}

#theme-colors p {
  color: var(--buttons-color);
}

#example {
  padding: 10px;
  grid-row: 2;
  grid-column: 2;
}
#example h1 {
  color: var(--buttons-color);
}
#example p {
  padding: 10px 0;
}
#example button {
  padding: 5px 15px;
  border: none;
  cursor: pointer;
}

#images {
  grid-column: 1;
  grid-row: 2;
}
#images img {
  width: 100%;
}
#images svg {
  margin: auto;
}

svg {
  fill: var(--buttons-color);
  width: 45px;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
}
<body id="usrhue">
  <div id="theme-switcher">
    <h3>Theme Switcher</h3>
    <input type="checkbox" id="switcher" />
  </div>
  <div id="theme-colors">
    <h3>Theme Color</h1>
      <p id="value"></p>
      <input id="hue-rotate" step="45" type="range" min="0" max="315" value="0">
  </div>
  <section id=example>
    <h1>Some title</h1>
    <p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Esse dolorem corporis praesentium natus soluta distinctio iure eos facere tenetur, assumenda sequi ducimus architecto provident nobis ad, quia nostrum. Voluptas, expedita.</p>
    <p>Lorem, ipsum dolor sit amet consectetur adipisicing elit. Adipisci hic alias facilis autem!</p>
    <button>Cancel</button>
    <button>Submit</button>
  </section>
  <section id='images'>
    <img src="https://campstoregear.com/wp-content/uploads/2017/09/summer-camp-2018-apparel-design-CD1816-Primary.png">
    <svg class="icon" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
      xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" x="0" y="0" width="50" height="33" viewbox="0 0 50 33"><path style="fill-opacity:1;fill-rule:evenodd;stroke:none" d="m 50,16.111089 c -0.0023,-0.52719 -0.269301,-1.175411 -0.836045,-1.837787 L 33.538651,0 l 0,4.099986 10.560199,12.011103 -10.560199,12.011102 0,4.099986 15.625304,-14.273302 C 49.730699,17.286499 49.997802,16.638278 50,16.111089 Z m -50,0 c 0.0022,-0.52719 0.2693022,-1.175411 0.8360452,-1.837787 L 16.46135,0 l 0,4.099986 -10.5601995,12.011103 10.5601995,12.011102 0,4.099986 L 0.8360452,17.948875 C 0.2693022,17.286499 0.0021979,16.638278 0,16.111089 Z"/></svg>
  </section>
</body>

With localStorage

  const theme = localStorage.getItem('theme');
  const swithTheme = localStorage.getItem('switcher');

  const value = document.getElementById('value');
  const body = document.getElementById('usrhue');
  const switcher = document.getElementById('switcher');
  const hueRotate = document.getElementById('hue-rotate');

  const LIGHT_THEME = 'light';
  const DARK_THEME = 'dark';

  value.textContent = `Color: Tomato`;

  if (!swithTheme) {
    localStorage.setItem('switcher', DARK_THEME);
    body.classList.add(DARK_THEME);
  }

  if (swithTheme) {
    if (swithTheme === LIGHT_THEME) {
      switcher.setAttribute('checked', true);
    }
    body.classList.add(swithTheme);
  }

  if (!theme) {
    localStorage.setItem(
      'theme',
      JSON.stringify({
        color: 'hue-rotate(0deg)',
        num: 0,
        name: `Tomato`,
      })
    );
    body.classList.add('tomato');
  }

  if (theme) {
    const LS = JSON.parse(theme);
    const className = LS.name.toLowerCase().split(' ').join('-');

    body.classList.add(className);
    hueRotate.value = LS.num;
    value.textContent = `Color: ${LS.name}`;
  }

  const toggleTheme = ev => {
    if (ev.target.checked) {
      body.classList.add(LIGHT_THEME);
      body.classList.remove(DARK_THEME);
      localStorage.setItem('switcher', LIGHT_THEME);
      return;
    }
    body.classList.add(DARK_THEME);
    body.classList.remove(LIGHT_THEME);
    localStorage.setItem('switcher', DARK_THEME);
  };

  const setClassName = (name, color, num, el) => {
    const colorArr = [
      'tomato',
      'wood',
      'green-gross',
      'green-house',
      'fun-green',
      'sky-blue',
      'vivid-violet',
      'rose-guns',
    ];

    localStorage.setItem(
      'theme',
      JSON.stringify({
        color,
        num,
        name,
      })
    );

    const className = name.toLowerCase().split(' ').join('-');
    colorArr.forEach(col => {
      if (col === className) {
        return el.classList.add(className);
      }
      el.classList.remove(col);
    });
  };

  switcher.addEventListener('change', toggleTheme);

  hueRotate.oninput = function () {
    const filter = 'hue-rotate(xdeg)'.replace('x', this.value);
    let name;

    switch (this.value) {
      case '45':
        name = `Wood`;
        setClassName(name, filter, this.value, body);
        return (value.textContent = `Color:${name}`);
      case '90':
        name = `Green House`;
        setClassName(name, filter, this.value, body);
        return (value.textContent = `Color: ${name}`);
      case '135':
        name = `Green Gross`;
        setClassName(name, filter, this.value, body);
        return (value.textContent = `Color: ${name}`);
      case '180':
        name = `Fun Green`;
        setClassName(name, filter, this.value, body);
        return (value.textContent = `Color: ${name}`);
      case '225':
        name = `Sky Blue`;
        setClassName(name, filter, this.value, body);
        return (value.textContent = `Color: ${name}`);
      case '270':
        name = `Vivid Violet`;
        setClassName(name, filter, this.value, body);
        return (value.textContent = `Color: ${name}`);
      case '315':
        name = `Rose Guns`;
        setClassName(name, filter, this.value, body);
        return (value.textContent = `Color: ${name}`);
      default:
        name = `Tomato`;
        setClassName(name, filter, this.value, body);
        return (value.textContent = `Color: ${name}`);
    }
  };
Anton
  • 8,058
  • 1
  • 9
  • 27
  • That's exactly what I wanted to achieve. You just read my mind. I was strugling right now with this and you just have done it. One question, how to avoid some classes to be affected by the hue-rotate filter? I mean, images, mainly. I mean, I'm looking into the code and can't get a grasp on how you avoided the image to be changed. Or has it to do with the css variables you setted in css? – SIMBIOSIS surl Aug 07 '21 at 11:34
  • If you mean the picture of `pine cover` in the example. It has `transparent` background `.png` if you replace without `transparency` like `.jpg` is will fine. But if you want to use with `transparency` and without color influece, just wrap the image in to `figure`. Then create a `pseudo-element` or `div` inside the `figure`, put it behind the picture and fill the `background-color` with a regular color. – Anton Aug 07 '21 at 11:59
  • No, this image is just an example one, I've got a lot of webp and don't want them to be affected by the hue-rotate. Something else is that I've already have a css file so I need to understand clearly how yours is working to be able to integrate your great solution into my css file without affecting the UI colors I already have. Do you understand my point? – SIMBIOSIS surl Aug 07 '21 at 12:04
0

Use :not(), MDN lists the following syntax as experimental

body:not(.webp, .png, .jpg) { filter: invert(0) contrast(1) saturate(1);}

Or, according to this, you can also chain them

body:not(.webp):not(.png):not(.jpg) { filter: invert(0) contrast(1) saturate(1);}
Henrique Milli
  • 158
  • 1
  • 10
  • Henrique Milli, thanks for your answer but I knew that I could do that. Any way it does not solve the problem. What I need is to dinamically apply a negative hue of the same value to rotate those classes hue back to 0. – SIMBIOSIS surl Jul 29 '21 at 18:02