Search code examples
csssassmix-blend-mode

cursor color - smooth transition between diferent backgrounds


On a website I'm creating there is a cursor that needs to change its color smoothly.
When it is on a white background the cursor needs to be the blue #0059ff (this is important and I will explain why later on) and when it is on blue then the cursor needs to be white; and the transition needs to be smooth like so:
Two concentric circles over a corner of a white-ish polygon where the word "kontakt" is being written. The two concentric circles are white-ish where over the background, and blue where over the polygon

To get the white color with mix-blend-mode I'm calculating the inverted color using adjust-hue($color, 180) (in SCSS) and applying this color to the cursor.

When the background color is #0000ff then cursor should be #ffff00.

I have started a prototype using mix-blend-mode: difference that works on "primary colors" (basically colors like #ff0000, #ff00ff and so on).
Result:
The text "Hover me" on white over a blue background with the white double circles "cursor" on the right, completely white The same "Hover me" text, this time in blue over the "cursor", now bigger and still white

Problems begin when I try to change the "primary" blue #0000ff to the one needed by the project #0059ff. The inverted color is calculated to be #ffa600 and the result is, let's say, "unsatisfactory" because I want the cursor to be white on some background color and said color on white background.
Same image as before with the cursor on the right, but now the cursor is purple The "Hover Me" text is over the bigger purple "cursor", written in blue.

Calculating the difference will not work with this color and I have no idea how to make it so that when the cursor is not over the white background then the cursor becomes blue (-ish) and when it's over the blue background it becomes white.

My whole code so far:
(SCSS compiled so it can run in StackSnippet)

const bigBall = document.querySelector('.cursor-ball-big');
const smallBall = document.querySelector('.cursor-ball-small');
const allHoverable = document.querySelectorAll('a, .hoverable');

TweenMax.to(bigBall, .3, {fill: 'none'});

allHoverable.forEach(hoverable => {
    hoverable.addEventListener('mouseenter', () => {
        TweenMax.to(bigBall, .3, {scale: 4});
        TweenMax.to(bigBall.querySelector('circle'), .3, {strokeWidth: 1});
    });
    hoverable.addEventListener('mouseleave', () => {
        TweenMax.to(bigBall, .3, {scale: 1});
        TweenMax.to(bigBall.querySelector('circle'), .3, {strokeWidth: 2});
    });
});

document.body.addEventListener('mousemove', e => {
    const {clientX, clientY} = e;

    TweenMax.to(smallBall, .1, {x: clientX - 5, y: clientY - 7});
    TweenMax.to(bigBall, .4, {x: clientX - 15, y: clientY - 17});
});
:root {
  --color1: #0059FF;
  --color2: #FFFFFF;
  --cursor: #ffa600;
}

body {
  height: 100vh;
  cursor: none;
  margin: 0;
  display: flex;
  font-family: monospace;
}

.cursor {
  pointer-events: none;
  mix-blend-mode: difference;
}
.cursor .cursor-ball {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 1000;
}
.cursor .cursor-ball.cursor-ball-small circle {
  fill: var(--cursor);
}
.cursor .cursor-ball.cursor-ball-big circle {
  stroke: var(--cursor);
}

a {
  border-bottom: 2px solid transparent;
  padding: 10px 0;
  margin-top: 25px;
  text-decoration: none;
  display: inline-block;
  cursor: none;
}

.left,
.right {
  height: 100%;
  width: 100%;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.left {
  background-color: var(--color1);
}
.left a {
  border-color: var(--color2);
}
.left h1,
.left p,
.left a {
  color: var(--color2);
}

.right {
  background-color: var(--color2);
}
.right a {
  border-color: var(--color1);
}
.right h1,
.right p,
.right a {
  color: var(--color1);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.20.3/TweenMax.min.js"></script>
<div class="cursor">
    <div class="cursor-ball cursor-ball-big">
        <svg xmlns="http://www.w3.org/2000/svg" height="30" width="30">
            <circle cx="15" cy="15" r="12" stroke-width="2"/>
        </svg>
    </div>
    <div class="cursor-ball cursor-ball-small">
        <svg height="10" width="10">
            <circle cx="5" cy="5" r="4" stroke-width="0"/>
        </svg>
    </div>
</div>

<div class="left">
    <a href="#" title="Hover me">Hover me</a>
</div>

<div class="right">
    <a href="#" title="Hover me">Hover me</a>
</div>


Solution

  • I have no idea how to make it so that when the cursor is not over the white background then the cursor becomes blue (-ish) and when it's over the blue background it becomes white.

    In this case, the mix-blend mode is very limiting. When you want to have completely unrelated colors then it's not possible to use it.

    However, I am able to achieve the desired effect using clip-path:

    const allHoverable = document.querySelectorAll('a, .hoverable');
      const sBall = document.querySelector('#ball1 > circle');
      const bBall = document.querySelector('#ball1 > text');
      const sBall2 = document.querySelector('#ball2 > circle');
      const bBall2 = document.querySelector('#ball2 > text');
    
      allHoverable.forEach(hoverable => {
        hoverable.addEventListener('mouseenter', () => {
          TweenMax.to(bBall, .3, { fontSize: 100, xPercent: -24, yPercent: 12 });
          TweenMax.to(bBall2, .3, { fontSize: 100, xPercent: -24, yPercent: 12 });
        });
        hoverable.addEventListener('mouseleave', () => {
          TweenMax.to(bBall, .3, { fontSize: 50, xPercent: 0, yPercent: 0 });
          TweenMax.to(bBall2, .3, { fontSize: 50, xPercent: 0, yPercent: 0 });
        });
      });
    
      document.body.addEventListener('mousemove', e => {
        const { clientX, clientY } = e;
        TweenMax.to(sBall, .1, { x: clientX - 5, y: clientY - 7 });
        TweenMax.to(bBall, .4, { x: clientX - 25, y: clientY + 10 });
        TweenMax.to(sBall2, .1, { x: clientX - 5 - window.innerWidth / 2, y: clientY - 7 });
        TweenMax.to(bBall2, .4, { x: clientX - 25 - window.innerWidth / 2, y: clientY + 10 });
      });
    :root {
      --color1: #0059FF;
      --color2: #FFFFFF;
      --cursor: #ffa600;
    }
    
    body {
      height: 100vh;
      cursor: none;
      margin: 0;
      display: flex;
      font-family: monospace;
      position: relative;
    }
    
    a {
      border-bottom: 2px solid transparent;
      padding: 10px 0;
      margin-top: 25px;
      text-decoration: none;
      display: inline-block;
      cursor: none;
    }
    
    .left,
    .right {
      height: 100%;
      width: 100%;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
    }
    
    .left2,
    .right2 {
      height: 100%;
      width: 50%;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
    }
    
    
    /* left */
    
    .left {
      background: var(--color1);
    }
    
    .left a {
      border-color: var(--color2);
    }
    
    .left h1,
    .left p,
    .left a {
      color: var(--color2);
    }
    
    
    /* left2 */
    
    .left2 {
      position: absolute;
      top: 0;
      left: 0;
      clip-path: url(#ball1);
      background: var(--color2);
    }
    
    .left2 a {
      border-color: var(--color1);
    }
    
    .left2 h1,
    .left2 p,
    .left2 a {
      color: var(--color1);
    }
    
    
    /* right */
    
    .right {
      background-color: var(--color2);
    }
    
    .right a {
      border-color: var(--color1);
    }
    
    .right h1,
    .right p,
    .right a {
      color: var(--color1);
    }
    
    
    /* right2 */
    
    .right2 {
      position: absolute;
      top: 0;
      right: 0;
      clip-path: url(#ball2);
      background-color: var(--color1);
    }
    
    .right2 a {
      border-color: var(--color2);
    }
    
    .right2 h1,
    .right2 p,
    .right2 a {
      color: var(--color2);
    }
    
    .bBall {
      font-family: 'Josefin Slab', serif;
      font-size: 50px;
      font-weight: 1000;
    }
    
    
    /* for debugging delete later */
    a {
      font-size: 4rem;
      font-weight: bolder;
      letter-spacing: -2px;
    
       -moz-user-select: none;
       -khtml-user-select: none;
       -webkit-user-select: none;
       -ms-user-select: none;
       user-select: none;
    
       -webkit-user-drag: none;
       -khtml-user-drag: none;
       -moz-user-drag: none;
       -o-user-drag: none;
       user-drag: none;
    }
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Josefin+Slab:wght@100&display=swap" rel="stylesheet">
    
    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.20.3/TweenMax.min.js"></script>
    
    <div class="left">
      <a href="#" title="Hover me">Hob</a>
    </div>
    <div class="left2">
      <a href="#" title="Hover me">Hob</a>
    </div>
    
    <div class="right">
      <a href="#" title="Hover me">Hob</a>
    </div>
    <div class="right2">
      <a href="#" title="Hover me">Hob</a>
    </div>
    
    <svg height="0" width="0">
        <defs>
          <clipPath id="ball1">
            <circle cx="0" cy="0" r="5" stroke-width="0" />
            <text x="0" y="0" class="bBall">O</text>
          </clipPath>
    
          <clipPath id="ball2">
            <circle cx="0" cy="0" r="5" stroke-width="0" />
            <text x="0" y="0" class="bBall">O</text>
          </clipPath>
        </defs>
      </svg>

    I've tested the output on Chrome 97 on Windows 10.
    In case the required font is not available, here is how it looks: enter image description here


    Explanation

    • I created element left2 which is exactly same in dimensions as left. Except colors are inverted. Background is white and text is blue.
    • left2 superimposes left exactly.
    • added clip-path #ball1(small ball) to left2.
    • The #ball1 clips left2 leaving left exposed, except the small area of left2 below it.
    • For outer ring(big ball), there is no way to have hollow clip path. So I used font with perfect circular letter 'O' in the svg. 😁 And onHover I am just increasing the font size to make it big.
    • Repeated the same steps for the right side. Created right2 etc...
    • Clip path is relative to the container, so you'll see two cursors for each side at the same time. So had to create duplicate clipPath ball2 for the right side and shifted it to left by 50vw. So it remains below mouse pointer.