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:
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:
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.
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>
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:
Explanation
left2
which is exactly same in dimensions as left
. Except colors are inverted. Background is white and text is blue.left2
superimposes left
exactly.clip-path
#ball1
(small ball) to left2
.#ball1
clips left2
leaving left exposed, except the small area of left2
below it.right2
etc...ball2
for the right side and shifted it to left by 50vw. So it remains below mouse pointer.