fletcher
fletcher

Reputation: 73

CSS/JS blur except around the mouse

I'm trying to develop a website from scratch. I'm using html, css and js (with jQuery and other js libraries as I need them). I wanted to have code that could be dropped anywhere without needing to install extra plugins/script. All that should be needed should be in a single folder I can place anywhere and run (plug and play style) so I have all libraries (only bootstrap, jQuery and Vague.js atm) downloaded.

In one of the pages I have multiple images moving in a carousel like way. The images cover the page and are the only thing (besides the logo and menu buttons) on the page. What I need to do is have the screen/images blurred and only a circle around the cursor be visible. Currently I managed to blur all images and have them focused when the mouse is over them but I can't figure out how to have only a small portion of the image focused.

I know that this is not the ideal way of handling things but to get the carousel working smoothly and be replicable in multiple pages I had to duplicate all images and have a css transform operation with keyframes moving -50%. This part I know could be better and you can ignore. What I currently have is:

<div class="scrollWrapper">
        <div id="scroll" class="imgScroll" onclick="toggleAnimation();">
            <img class="scrollingImage" src="image1.jpg" alt="Window showcase image">
            <img class="scrollingImage" src="image2.jpg" alt="Window showcase image">
            <img class="scrollingImage" src="image3.jpg" alt="Window showcase image">
            <img class="scrollingImage" src="image4.jpg" alt="Window showcase image">
            <img class="scrollingImage" src="image5.jpg" alt="Window showcase image">
            <img class="scrollingImage" src="image1.jpg" alt="Window showcase image">
            <img class="scrollingImage" src="image2.jpg" alt="Window showcase image">
            <img class="scrollingImage" src="image3.jpg" alt="Window showcase image">
            <img class="scrollingImage" src="image4.jpg" alt="Window showcase image">
            <img class="scrollingImage" src="image5.jpg" alt="Window showcase image">
        </div>
    </div>
.scrollWrapper{
    overflow: hidden;
}

.imgScroll{
    background-color: dimgrey;
    width: max-content;
    font-size: 0;
    z-index: -999;
    height: 100vh;
}

.scrollingImage{

    filter: blur(10px);
    -webkit-filter: blur(10px);

    display: inline-block;
    height: 100%;
}

.scrollingImage:hover{
    filter: blur(0px);
    -webkit-filter: blur(0px);
}

@keyframes scrolling{
    0% {transform: translateX(0%);}
    100% {transform: translateX(-50%);}
}

.imgScroll{
    animation: scrolling 60s linear infinite;
    z-index: -1000;
}

body .scrollWrapper .pause{
    animation-play-state: paused;
    -webkit-animation-play-state: paused;
    -moz-animation-play-state:paused;
    -o-animation-play-state:paused; 
}
function toggleAnimation() {
    scroll = document.getElementById('scroll');
    if(scroll.classList.contains('pause')){
        scroll.classList.remove('pause');
    }else{
        scroll.classList.add('pause');
    }
}

My current idea is to blur the div with the class scrollWrapper and somehow have an area around the cursor being focused but I'm not sure how to do it.

I've looked around and found this post but the solutions work with a single static image as far as I can tell and not with multiple moving ones. I'm now messing around with Vague.js but can't figure out how to do it.

I'm not a web developer and have worked very little with js/jquery so I'm starting to feel stupid for not being able to figure this out... This is my last resort before changing to something completely different so any help will be much appreciated.

Upvotes: 3

Views: 3869

Answers (1)

Kaiido
Kaiido

Reputation: 136628

Doing this with the new CSS backdrop-filter is quite easy, the hardest part being to set up a hole in the filter.

Luckily we have stackoverflow.

For a circle, the simplest is probably to use a radial-gradient as a mask-image, as shown in this answer.

const blur_elem = document.getElementById( "blur-around" );
document.onmousemove = (evt) => {
  blur_elem.style.transform = `translate(${evt.clientX}px, ${evt.clientY}px)`;
};
#blur-around {
  position: fixed;
  z-index: 999;
  pointer-events: none;
  /* twice the viewport size so it always covers fully */
  width: 200vw;
  height: 200vh;
  /* negative offset by half so we are sure we cover the full viewport */
  left: -100vw;
  top: -100vh;
  /* we'll use transform translate to move it */
  transform-origin: center;  
  -webkit-backdrop-filter: blur(15px);
  backdrop-filter: blur(15px);
  -webkit-mask-image: radial-gradient(50px at 50% 50%, transparent 100%, black 100%);
  mask-image: radial-gradient(50px at 50% 50% , transparent 100%, black 100%)
}

/* falback for browsers that don't have backdrop-filter */
@supports not ((backdrop-filter: blur(0px)) or (-webkit-backdrop-filter: blur(0px))) {
  #blur-around {
    background-color: rgba(255,255,255,.8);
  }
}
<div id="blur-around"></div>
<p>Works over any content</p>
<img src="https://picsum.photos/250/250">
<img src="https://picsum.photos/360/200">

Unfortunately, Safari doesn't support entirely the mask-image property, so we may need something else.

We can also use CSS clip-path with an evenodd path, as shown in that answer.

Unfortunately, Chrome still doesn't support the path() function for clip-path, so we have to be creative and instead use the polygon() function and define each point's vertex.
Still, for a rectangular shape all it requires is to draw first the outer rectangle the size of our element, and then the inner one wherever we want, while ensuring we always close both of these shapes.

const blur_elem = document.getElementById( "blur-around" );
document.onmousemove = (evt) => {
  blur_elem.style.transform = `translate(${evt.clientX}px, ${evt.clientY}px)`;
};
#blur-around {
  position: fixed;
  z-index: 999;
  pointer-events: none;
  /* twice the viewport size so it always covers fully */
  width: 200vw;
  height: 200vh;
  /* negative offset by half so we are sure we cover the full viewport */
  left: -100vw;
  top: -100vh;
  /* we'll use transform translate to move it */
  transform-origin: center;  
  -webkit-backdrop-filter: blur(15px);
  backdrop-filter: blur(15px);

  --rect-size: 100px;
  clip-path: polygon( evenodd,
    /* outer rect */
    0 0,       /* top - left */
    100% 0,    /* top - right */
    100% 100%, /* bottom - right */
    0% 100%,   /* bottom - left */
    0 0,       /* and top - left again */
    /* do the same with inner rect */
    calc(50% - var(--rect-size) / 2) calc(50% - var(--rect-size) / 2),
    calc(50% + var(--rect-size) / 2) calc(50% - var(--rect-size) / 2),
    calc(50% + var(--rect-size) / 2) calc(50% + var(--rect-size) / 2),
    calc(50% - var(--rect-size) / 2) calc(50% + var(--rect-size) / 2),
    calc(50% - var(--rect-size) / 2) calc(50% - var(--rect-size) / 2)
   );
}

/* falback for browsers that don't have backdrop-filter */
@supports not ((backdrop-filter: blur(0px)) or (-webkit-backdrop-filter: blur(0px))) {
  #blur-around {
    background-color: rgba(255,255,255,.8);
  }
}
<div id="blur-around"></div>
<p>Works over any content</p>
<img src="https://picsum.photos/250/250">
<img src="https://picsum.photos/360/200">

To make it with a circle though (as has been required in comments) starts to be a bit less readable and while it can be hardcoded in CSS too, it would make for such a big rule that I prefer to leave directly a javascript generator in this answer:

function makeCircleHoleClipPathRule( radius ) {

  const inner_path = [];
  const circumference = Math.PI * radius;
  const step = Math.PI * 2 / circumference;
  // we are coming from top-left corner
  const start_step = circumference * (5 / 8);
  for( let i = start_step; i < circumference + start_step; i++ ) {
    const angle = step * i;
    const x = radius * Math.cos( angle );
    const y = radius * Math.sin( angle );
    const str = `calc( 50% + ${ x }px ) calc( 50% + ${ y }px )`;
    inner_path.push( str );
  }
  // avoid rounding issues
  inner_path.push( inner_path[ 0 ] );

  return `polygon( evenodd,
    /* outer rect */
    0 0,       /* top - left */
    100% 0,    /* top - right */
    100% 100%, /* bottom - right */
    0% 100%,   /* bottom - left */
    0 0,       /* and top - left again */
    ${ inner_path.join( "," ) }
   )`;

}

const blur_elem = document.getElementById( "blur-around" );
// set the clip-path rule
blur_elem.style.clipPath = makeCircleHoleClipPathRule( 50 );

document.onmousemove = (evt) => {
  blur_elem.style.transform = `translate(${evt.clientX}px, ${evt.clientY}px)`;
};
#blur-around {
  position: fixed;
  z-index: 999;
  pointer-events: none;
  /* twice the viewport size so it always covers fully */
  width: 200vw;
  height: 200vh;
  /* negative offset by half so we are sure we cover the full viewport */
  left: -100vw;
  top: -100vh;
  /* we'll use transform translate to move it */
  transform-origin: center;
  -webkit-backdrop-filter: blur(15px);
  backdrop-filter: blur(15px);
}
/* falback for browsers that dont have backdrop-filter */
@supports not ((backdrop-filter: blur(0px)) or (-webkit-backdrop-filter: blur(0px))) {
  #blur-around {
    background-color: rgba(255,255,255,.8);
  }
}
<div id="blur-around"></div>
<p>Works over any content</p>
<img src="https://picsum.photos/250/250">
<img src="https://picsum.photos/360/200">

However, backdrop-filter is currently supported only in latest Blink + Webkit browsers, Gecko is still lacking support for it. Since I doubt there are many other cross-browser solutions, you could try this polyfill which will duplicate your page's content in an iframe (i.e not very performant).

Upvotes: 3

Related Questions