Image Mask Effect

Image Mask Effect

An immersive transition effect powered by image masks and CSS transforms.

Nucleo icons

Sponsored by Nucleo, a free application to collect, customize and export all your icons as icon font and SVG symbols. Made by the CodyHouse folks!

All the resources available on CodyHouse are released under the BSD-3-Clause license. You can support our project with a Paypal donation 🙌

We’ve been publishing in our library some nice mask effects powered by SVG properties. This time we took advantage of the PNG transparencies to zoom through the mask layer, into a project background image.

If you want to change the color of the .png masks, you can easily do it in Photoshop (or any other graphic tool) by applying a color overlay to the whole image layer. If you plan to create your own masks, please note that this effect works only if there’s an empty space in the very center of the mask.

Inspiration: Offsite Homepage Animation by Hrvoje Grubisic.

Images: Unsplash

Creating the structure

The HTML structure is composed of a list of <section>s wrapped in a .cd-image-mask-effect element. Each <section> contains a div.featured-image (project image), a div.mask (image mask) and a div.cd-project-info for the project content.

<section class="project-1 cd-project-mask">
   <h1>Project Name</h1>
   <div class="featured-image"></div>
   <div class="mask">
      <img src="img/mask-01.png" alt="mask">
      <span class="mask-border mask-border-top"></span>
      <span class="mask-border mask-border-bottom"></span>
      <span class="mask-border mask-border-left"></span>
      <span class="mask-border mask-border-right"></span>
   </div>

   <a href="#0" class="project-trigger">Explore Project</a>

   <a href="#0" class="cd-scroll cd-img-replace">Scroll down</a>

   <div class="cd-project-info" data-url="project-1">
      <!-- content loaded using js -->
   </div>

   <a href="#0" class="project-close cd-img-replace">Close Project</a>
</section> <!-- .cd-project-mask -->

<section class="project-2 cd-project-mask">
   <!-- content here -->
</section>

<!-- other sections here -->

The project content is not included in the HTML but is loaded using JavaScript.

Adding style

Each .cd-project-mask has a height of 100vh (viewport height) and a width of 100%; the project image is set as background-image of the .featured-image element, while the mask image is wrapped inside the .mask element.
Four .mask-border elements have been used to create a frame around the image mask to make sure the project featured image is not visible outside the mask (we used <span> elements rather than pseudo elements because their behaviour was buggy on Safari 9).

.cd-project-mask {
  position: relative;
  height: 100vh;
  width: 100%;
  overflow: hidden;
}

.cd-project-mask .featured-image {
  /* project intro image */
  position: absolute;
  left: 50%;
  top: 50%;
  bottom: auto;
  right: auto;
  transform: translateX(-50%) translateY(-50%);
  height: 100%;
  width: 100%;
  background: url(../img/img-01.jpg) no-repeat center center;
  background-size: cover;
}

.cd-project-mask .mask {
  position: absolute;
  left: 50%;
  top: 50%;
  bottom: auto;
  right: auto;
  transform: translateX(-50%) translateY(-50%);
  width: 300px;
  height: 300px;
}

.cd-project-mask .mask .mask-border {
  /* this is used to create a frame around the mask */
  position: absolute;
}

.cd-project-mask .mask .mask-border-top,
.cd-project-mask .mask .mask-border-bottom {
  /* this is used to create a frame around the mask */
  height: calc(50vh - 150px + 10px);
  width: 100vw;
  left: 50%;
  right: auto;
  transform: translateX(-50%);
}

.cd-project-mask .mask .mask-border-top {
  bottom: calc(100% - 10px);
}

.cd-project-mask .mask .mask-border-bottom {
  top: calc(100% - 10px);
}

.cd-project-mask .mask .mask-border-left,
.cd-project-mask .mask .mask-border-right {
  /* this is used to create a frame around the mask */
  height: 100vh;
  width: calc(50vw - 150px + 10px);
  top: 50%;
  bottom: auto;
  transform: translateY(-50%);
}

.cd-project-mask .mask .mask-border-left {
  left: calc(100% - 10px);
}

.cd-project-mask .mask .mask-border-right {
  right: calc(100% - 10px);
}

When the user selects a project, the class .project-view (added to the wrapper .cd-image-mask-effect) is used to hide all the other projects.
The .mask element is then scaled up to reveal the project featured image and the project content is loaded (more in the Events handling section).

.project-view .cd-project-mask:not(.project-selected) {
/* the project-view class is added to the .cd-image-mask-effect element when a project is selected - hide all not selected projects */ position: absolute; top: 0; left: 0; opacity: 0; visibility: hidden; }

Events handling

To implement this image mask effect, we created a ProjectMask object and used the initProject method to attach the proper event handlers.

function ProjectMask( element ) {
   this.element = element;
   this.projectTrigger = this.element.find('.project-trigger');
   this.projectClose = this.element.find('.project-close'); 
   this.projectTitle = this.element.find('h1');
   this.projectMask = this.element.find('.mask');
   //...
   this.initProject();
}

var revealingProjects = $('.cd-project-mask');
var objProjectMasks = [];

if( revealingProjects.length > 0 ) {
   revealingProjects.each(function(){
      //create ProjectMask objects
      objProjectMasks.push(new ProjectMask($(this)));
   });
}

When the user selects a project, the revealProject method is used to scale up the mask image while the uploadContent method takes care of loading the project content (using the load() function) and adding the new page to the window.history (using the pushState() method).

ProjectMask.prototype.initProject = function() {
   var self = this;

   //open the project
   this.projectTrigger.on('click', function(event){
      event.preventDefault();
      if( !self.animating ) {
         self.animating = true;
         //upload project content
         self.uploadContent();
         //show project content and scale up mask
         self.revealProject();
      }
   });

   //...
};

ProjectMask.prototype.revealProject = function() {
   var self = this;
   //get mask scale value
   self.updateMaskScale();
   //scale up mask and animate project title
   self.projectTitle.attr('style', 'opacity: 0;');
   self.projectMask.css('transform', 'translateX(-50%) translateY(-50%) scale('+self.maskScaleValue+')').one(transitionEnd, function(){
      self.element.addClass('center-title');
      self.projectTitle.attr('style', '');
      self.animating = false;
   });

   //hide the other sections
   self.element.addClass('project-selected content-visible').parent('.cd-image-mask-effect').addClass('project-view');
}

ProjectMask.prototype.uploadContent = function(){
   var self = this;
   //if content has not been loaded -> load it
   if( self.projectContent.find('.content-wrapper').length == 0 ) self.projectContent.load(self.projectContentUrl+'.html .cd-project-info > *');
   if( self.projectContentUrl+'.html'!=window.location ){
      //add the new page to the window.history
      window.history.pushState({path: self.projectContentUrl+'.html'},'',self.projectContentUrl+'.html');
   }
}

Join our newsletter

Get our monthly recap with the latest CodyHouse news