/** * demo.js * http://www.codrops.com * * Licensed under the MIT license. * http://www.opensource.org/licenses/mit-license.php * * Copyright 2019, Codrops * http://www.codrops.com */ { // Helper functions const MathUtils = { lineEq: (y2, y1, x2, x1, currentVal) => { // y = mx + b var m = (y2 - y1) / (x2 - x1), b = y1 - m * x1; return m * currentVal + b; }, lerp: (a, b, n) => (1 - n) * a + n * b, getRandomFloat: (min, max) => (Math.random() * (max - min) + min).toFixed(2) }; // Gets the mouse position const getMousePos = (e) => { let posx = 0; let posy = 0; if (!e) e = window.event; if (e.pageX || e.pageY) { posx = e.pageX; posy = e.pageY; } else if (e.clientX || e.clientY) { posx = e.clientX + body.scrollLeft + document.documentElement.scrollLeft; posy = e.clientY + body.scrollTop + document.documentElement.scrollTop; } return { x : posx, y : posy } }; // https://pawelgrzybek.com/page-scroll-in-vanilla-javascript/ function scrollIt(destination, duration = 200, easing = 'linear', callback) { const easings = { linear(t) { return t; }, easeOutQuad(t) { return t * (2 - t); }, }; const start = window.pageYOffset; const startTime = 'now' in window.performance ? performance.now() : new Date().getTime(); const documentHeight = Math.max(document.body.scrollHeight, document.body.offsetHeight, document.documentElement.clientHeight, document.documentElement.scrollHeight, document.documentElement.offsetHeight); const windowHeight = window.innerHeight || document.documentElement.clientHeight || document.getElementsByTagName('body')[0].clientHeight; const destinationOffset = typeof destination === 'number' ? destination : destination.offsetTop; const destinationOffsetToScroll = Math.round(documentHeight - destinationOffset < windowHeight ? documentHeight - windowHeight : destinationOffset); if ('requestAnimationFrame' in window === false) { window.scroll(0, destinationOffsetToScroll); if (callback) { callback(); } return; } function scroll() { const now = 'now' in window.performance ? performance.now() : new Date().getTime(); const time = Math.min(1, ((now - startTime) / duration)); const timeFunction = easings[easing](time); window.scroll(0, Math.abs(Math.ceil((timeFunction * (destinationOffsetToScroll - start)) + start))); if (window.pageYOffset === destinationOffsetToScroll) { if (callback) { callback(); } return; } requestAnimationFrame(scroll); } scroll(); } // Calculate the viewport size let winsize; const calcWinsize = () => winsize = {width: window.innerWidth, height: window.innerHeight}; calcWinsize(); window.addEventListener('resize', calcWinsize); // Track the mouse position let mousepos = {x: winsize.width/2, y: winsize.height/2}; window.addEventListener('mousemove', ev => mousepos = getMousePos(ev)); // Custom cursor class Cursor { constructor(el) { this.DOM = {el: el}; this.DOM.circle = this.DOM.el.querySelector('.cursor__inner--circle'); this.DOM.arrows = { right: this.DOM.el.querySelector('.cursor__side--right'), left: this.DOM.el.querySelector('.cursor__side--left') }; this.bounds = this.DOM.circle.getBoundingClientRect(); this.renderedStyles = { tx: {previous: 0, current: 0, amt: 0.2}, ty: {previous: 0, current: 0, amt: 0.2}, scale: {previous: 1, current: 1, amt: 0.2} }; requestAnimationFrame(() => this.render()); } render() { this.renderedStyles['tx'].current = mousepos.x - this.bounds.width/2; this.renderedStyles['ty'].current = mousepos.y - this.bounds.height/2; for (const key in this.renderedStyles ) { this.renderedStyles[key].previous = MathUtils.lerp(this.renderedStyles[key].previous, this.renderedStyles[key].current, this.renderedStyles[key].amt); } this.DOM.circle.style.transform = `translateX(${(this.renderedStyles['tx'].previous)}px) translateY(${this.renderedStyles['ty'].previous}px) scale(${this.renderedStyles['scale'].previous})`; requestAnimationFrame(() => this.render()); } enter() { this.renderedStyles['scale'].current = 1.9; } leave() { this.renderedStyles['scale'].current = 1; } click() { this.renderedStyles['scale'].previous = 0.4; } showArrows() { TweenMax.to(Object.values(this.DOM.arrows), ANIMATION_SETTINGS.cursor.duration, { ease: ANIMATION_SETTINGS.cursor.ease, startAt: {x: i => i ? 10 : -10 }, opacity: 1, x: 0 }); } hideArrows() { TweenMax.to(Object.values(this.DOM.arrows), ANIMATION_SETTINGS.cursor.duration, { ease: ANIMATION_SETTINGS.cursor.ease, x: i => i ? 10 : -10, opacity: 0 }); } } // Images Grid class ImageGrid { constructor(el) { this.DOM = {el: el}; this.DOM.imageWrap = [...this.DOM.el.querySelectorAll('.grid__item-wrap')]; this.itemsTotal = this.DOM.imageWrap.length; this.DOM.images = [...this.DOM.el.querySelectorAll('.grid__item')]; // Spread the grid items this.spread(); } // Spreads the grid items by randomly positioning them and scaling them down spread(animate = false) { return new Promise((resolve, reject) => { let animateCount = 0; const gridHeight = this.DOM.el.scrollHeight; const gridTop = this.DOM.el.offsetTop; this.DOM.imageWrap.forEach((item) => { const rect = item.getBoundingClientRect(); // Item´s center point const center = {x: rect.left+rect.width/2, y: rect.top+rect.height/2}; // Calculate the item´s quadrant in the viewport const quadrant = center.x >= winsize.width/2 ? center.y <= gridHeight/2 + gridTop ? 1 : 4 : center.y <= gridHeight/2 + gridTop ? 2 : 3; // Now calculate how much to translate the item // The positions will be random but only in the area of the item´s quadrant // Also, consider a margin so the item does not stay completely out of the viewport or its quadrant const margins = {x: winsize.width*.02, y: winsize.height*.04} const tx = quadrant === 1 || quadrant === 4 ? MathUtils.getRandomFloat(-1*center.x + winsize.width/2 + margins.x*4, winsize.width - center.x - margins.x) : MathUtils.getRandomFloat(-1*center.x + margins.x, winsize.width/2 - center.x - margins.x*4); const ty = quadrant === 1 || quadrant === 2 ? MathUtils.getRandomFloat(-1*center.y + margins.y, winsize.height/2 - center.y - margins.y*4) : MathUtils.getRandomFloat(-1*center.y + winsize.height/2 + margins.y*4, winsize.height - center.y - margins.y); // Save the current translation item.dataset.ctx = tx; item.dataset.cty = ty; if ( animate ) { TweenMax.to(item, ANIMATION_SETTINGS.grid.duration, { ease: ANIMATION_SETTINGS.grid.ease, x: tx, y: ty, scale: 0.35, onComplete: () => { ++animateCount; if ( animateCount === this.itemsTotal ) { resolve(); } } }); } else { TweenMax.set(item, { x: tx, y: ty, scale: 0.35 }); resolve(); } }); }); } // Resets the items to the original position (forming again the original grid) collapse() { return new Promise((resolve, reject) => { TweenMax.to(this.DOM.imageWrap, ANIMATION_SETTINGS.grid.duration, { ease: ANIMATION_SETTINGS.grid.ease, x: 0, y: 0, scale: 1.01, onComplete: resolve }); }); } showImages() { TweenMax.set(this.DOM.images, {opacity: 1}); } } // A menu item class MenuItem { constructor(el, imageGrid) { // The main wrapper this.DOM = {el: el}; // The inner link (.menu__item-link) this.DOM.link = this.DOM.el.querySelector('.menu__item-link'); // The explore link this.DOM.explore = this.DOM.el.querySelector('.menu__item-explore'); // We will need the size and position for the calculations needed to drag/translate the menu this.rect = this.DOM.el.getBoundingClientRect(); // The images grid for this menu item this.imageGrid = imageGrid; // As we drag, the letters will switch from only stroke to filled and vice versa // We need to split the letters into spans and create the necessary structure (we will have two spans per letter, one for the stroke version and one for the filled) charming(this.DOM.link, {classPrefix: false}); const linkInner = [...this.DOM.link.querySelectorAll('span')]; linkInner.forEach((span) => { const stroke = span.cloneNode(true); span.classList.add('letter__inner','letter__inner--filled'); stroke.classList.add('letter__inner','letter__inner--stroke'); this.DOM.link.insertBefore(stroke, span.nextSibling); const letter = document.createElement('span'); letter.classList = 'letter'; letter.appendChild(span); letter.appendChild(stroke); this.DOM.link.appendChild(letter); }); this.letters = [...this.DOM.link.querySelectorAll('.letter__inner')]; // Need to recalculate size and position on window resize window.addEventListener('resize', () => this.rect = this.DOM.el.getBoundingClientRect()); } setCurrent() { this.DOM.el.classList.add('menu__item--current'); return this; } unsetCurrent() { this.DOM.el.classList.remove('menu__item--current'); } isCurrent() { return this.DOM.el.classList.contains('menu__item--current'); } // Show/Hide the explore link showExplore() { return this.toggleExplorer('show'); } hideExplore() { return this.toggleExplorer('hide'); } toggleExplorer(action = 'show') { return new Promise((resolve, reject) => { TweenMax.to(this.DOM.explore, ANIMATION_SETTINGS.explore.duration, { ease: ANIMATION_SETTINGS.explore.ease, startAt: action === 'hide' ? null : {scale: 0.5}, opacity: action === 'hide' ? 0 : 1, scale: action === 'hide' ? 0.8 : 1, onComplete: resolve }); }); } // Show/Hide the letters show() { return this.toggle('show'); } hide() { return this.toggle('hide'); } toggle(action = 'show') { return new Promise((resolve, reject) => { const tx = action === 'hide' ? this.isCurrent() ? '-200%' : '100%' : this.isCurrent() ? '-100%' : '0%'; TweenMax.to(this.letters, ANIMATION_SETTINGS.allMenuLettersToggle.duration, { ease: ANIMATION_SETTINGS.allMenuLettersToggle.ease, x: tx, onComplete: resolve }); }); } } // The menu class Menu { constructor(el) { // The menu wrap (.menu-wrap) this.DOM = {el: el}; // The menu element this.DOM.menu = this.DOM.el.querySelector('.menu'); // The draggable container this.DOM.draggable = this.DOM.el.querySelector('.menu-draggable'); // Content wrap this.DOM.pagePreview = document.querySelector('.page--preview'); // The ctrl that closes the grid view and shows back the menu this.DOM.backToMenuCtrl = this.DOM.pagePreview.querySelector('.gridback'); // The image grids (one per menu item) this.imageGrids = []; [...this.DOM.pagePreview.querySelectorAll('.grid')].forEach(item => this.imageGrids.push(new ImageGrid(item))); // MenuItem instances this.menuItems = []; [...this.DOM.menu.querySelectorAll('.menu__item')].forEach((item, position) => this.menuItems.push(new MenuItem(item, this.imageGrids[position]))); // Total number of menu items this.menuItemsTotal = this.menuItems.length; // Index of the current menuItem this.current = 0; // Set the first menu item to current and show its explore link this.menuItems[this.current].setCurrent().showExplore(); // Show the first grid items this.menuItems[this.current].imageGrid.showImages(); // Initialize the Draggabilly (on the x axis) this.draggie = new Draggabilly(this.DOM.draggable, { axis: 'x' }); // The current amount (in pixels) that was dragged this.dragPosition = 0; // Minimum amount to drag in order to navigate to the next/previous menu item this.minDrag = winsize.width*.04; // Set the menu initial position this.layout(); // The following are the values that need to be updated inside the render (rAF) function: // - the menu translation value // - the letters/spans (stroke and filled) translation values // - and the grid images opacity and transform values // The "current" and the "previous" hold the values to interpolate ("current" being the one we want to get to) and the "amt" is the amount to interpolate this.renderedStyles = { menuTranslation: {previous: this.dragPosition + this.initTx, current: this.dragPosition + this.initTx, amt: 0.1}, letterTranslation: {previous: 0, current: 0, amt: 0.1}, imgOpacity: {previous: 1, current: 1, amt: 0.1}, imgScaleX: {previous: 1, current: 1, amt: 0.06}, imgScaleY: {previous: 1, current: 1, amt: 0.06}, imgTranslation: {previous: 0, current: 0, amt: 0.1} }; // Start the rAF loop to render the menu and letters positions this.renderId = requestAnimationFrame(() => this.render()); // Initialize/Bind some events this.initEvents(); } layout() { // Set the menu position/translation so that the first menu item is the current one thus positioned at the center // We need to save these values for later calculations when translating the menu this.initTx = this.currentPosition = winsize.width/2 - this.menuItems[this.current].rect.width/2; TweenMax.set(this.DOM.menu, {x: this.initTx}); } // Window resize resize() { this.minDrag = winsize.width*.04; // Update position this.currentPosition = winsize.width/2 - this.menuItems[this.current].DOM.el.offsetLeft - this.menuItems[this.current].rect.width/2; this.renderedStyles.menuTranslation.current = this.renderedStyles.menuTranslation.previous = this.currentPosition; } isDragging() { // dragDirection is only set when we drag the menu, so this can be used to checked if we are currently dragging return this.dragDirection != undefined && this.dragDirection != ''; } render() { this.renderId = undefined; // Apply the lerp function to the updated values for (const key in this.renderedStyles ) { this.renderedStyles[key].previous = MathUtils.lerp(this.renderedStyles[key].previous, this.renderedStyles[key].current, this.renderedStyles[key].amt); } // Translate the menu TweenMax.set(this.DOM.menu, {x: this.renderedStyles.menuTranslation.previous}); // Switch the filled spans with stroke ones and vice versa // Also update the grid images if ( this.isDragging() && this.currentItem && this.upcomingItem ) { let tx = this.renderedStyles.letterTranslation.previous; TweenMax.set(this.currentItem.letters, {x: this.dragDirection === 'left' ? -1*tx-100 + '%' : tx-100 + '%'}); TweenMax.set(this.upcomingItem.letters, {x: this.dragDirection === 'left' ? tx + '%' : -1*tx + '%'}); TweenMax.set(this.currentItem.imageGrid.DOM.images, { transformOrigin: this.dragDirection === 'left' ? '100% 50%' : '0% 50%', opacity: this.renderedStyles.imgOpacity.previous, scaleX: this.renderedStyles.imgScaleX.previous, scaleY: this.renderedStyles.imgScaleY.previous, x: this.dragDirection === 'left' ? -1*this.renderedStyles.imgTranslation.previous + '%' : this.renderedStyles.imgTranslation.previous + '%' }); TweenMax.set(this.upcomingItem.imageGrid.DOM.images, { transformOrigin: this.dragDirection === 'left' ? '0% 50%' : '100% 50%', opacity: Math.abs(1-this.renderedStyles.imgOpacity.previous), scaleX: 3-this.renderedStyles.imgScaleX.previous, scaleY: 1.8-this.renderedStyles.imgScaleY.previous, x: this.dragDirection === 'left' ? 150 - this.renderedStyles.imgTranslation.previous + '%' : -1*(150 - this.renderedStyles.imgTranslation.previous) + '%' }); } if ( !this.renderId ) { this.renderId = requestAnimationFrame(() => this.render()); } } initEvents() { this.onPointerDown = () => { // Scale up the cursor cursor.renderedStyles['scale'].current = 1.5; // And show the "drag mode" arrows cursor.showArrows(); }; this.onDragStart = () => { if ( this.isAnimating ) return; // Reset the drag direction value this.dragDirection = ''; }; // Save the previous moveVector obj that Draggability provides for every drag move // We need this to track the current direction of dragging in order to compare it later with the initial intended direction // Like so we know if the menu should navigate to the next/previous item or if the navigation needs to be cancelled this.cachedVectorMovement = {x:0,y:0}; this.onDragMove = (event, pointer, moveVector) => { // Update the mouse position mousepos = getMousePos(event); // Return if theres an active animation if ( this.isAnimating ) return; // Track the current direction of the drag if ( moveVector.x != this.cachedVectorMovement.x ) { this.currentDirection = moveVector.x > this.cachedVectorMovement.x ? 'right' : 'left'; this.cachedVectorMovement = moveVector; } if ( this.dragDirection === '' ) { // Hide the explorer link this.menuItems[this.current].hideExplore(); // The initial intended direction this.dragDirection = moveVector.x > 0 ? 'right' : 'left'; // We need to calculate the amount to move the menu as we drag from one point of the screen to another. // If we are switching between two menu items then this value is the distance from the center of the current menu item to the center of the next or previous menuItem (depending on the dragging direction) // otherwise it will be the same as this.minDrag so that when we stop dragging the navigation gets cancelled // Boundary cases if ( this.dragDirection === 'right' && this.current === 0 || this.dragDirection === 'left' && this.current === this.menuItemsTotal - 1 ) { this.amountToMove = this.minDrag; } // else move to the next/previous menuItem else { this.upcomingIdx = this.dragDirection === 'left' ? this.current+1 : this.current-1; this.currentItem = this.menuItems[this.current]; this.upcomingItem = this.menuItems[this.upcomingIdx]; this.amountToMove = Math.abs((this.currentItem.rect.left + this.currentItem.rect.width/2) - (this.upcomingItem.rect.left + this.upcomingItem.rect.width/2)); } } // Update the dragPosition: // We need to map the draggable movement ([0,winsize.width]) to the menu movement ([0,amountToMove]) this.dragPosition = MathUtils.lineEq(this.amountToMove, 0, winsize.width, 0, this.draggie.position.x); // Finally update both the menu translation, letters translation and grid images (rAF render function) this.renderedStyles.menuTranslation.current = this.dragPosition + this.currentPosition; this.renderedStyles.letterTranslation.current = MathUtils.lineEq(100, 0, winsize.width, 0, this.dragDirection === 'left' ? Math.min(this.draggie.position.x, 0) : Math.max(this.draggie.position.x, 0)); this.renderedStyles.imgOpacity.current = MathUtils.lineEq(0, 1, winsize.width, 0, this.dragDirection === 'left' ? Math.abs(Math.min(this.draggie.position.x, 0)) : Math.abs(Math.max(this.draggie.position.x, 0))); this.renderedStyles.imgScaleX.current = MathUtils.lineEq(2, 1, winsize.width, 0, this.dragDirection === 'left' ? Math.abs(Math.min(this.draggie.position.x, 0)) : Math.abs(Math.max(this.draggie.position.x, 0))); this.renderedStyles.imgScaleY.current = MathUtils.lineEq(0.8, 1, winsize.width, 0, this.dragDirection === 'left' ? Math.abs(Math.min(this.draggie.position.x, 0)) : Math.abs(Math.max(this.draggie.position.x, 0))); this.renderedStyles.imgTranslation.current = MathUtils.lineEq(150, 0, winsize.width, 0, this.dragDirection === 'left' ? Math.abs(Math.min(this.draggie.position.x, 0)) : Math.abs(Math.max(this.draggie.position.x, 0))); }; this.onPointerUp = () => { // Scale down the cursor (reset) cursor.renderedStyles['scale'].current = 1; // And hide the "drag mode" arrows cursor.hideArrows(); }; this.onDragEnd = () => { if ( !this.isAnimating ) { this.isAnimating = true; // Cancel the render function (rAF) if ( this.renderId ) { window.cancelAnimationFrame(this.renderId); this.renderId = undefined; } // Cancel the navigation: // Either it didn´t drag enough (<= minDrag) or the drag direction changed to the opposite one, meaning the user stepped back from navigating if ( Math.abs(this.dragPosition) <= this.minDrag || this.dragDirection !== this.currentDirection ) { // Show the explore link this.menuItems[this.current].showExplore(); // Reset the rAF updated values this.renderedStyles.menuTranslation.current = this.renderedStyles.menuTranslation.previous = this.currentPosition; this.renderedStyles.letterTranslation.current = this.renderedStyles.letterTranslation.previous = 0; this.renderedStyles.imgOpacity.current = this.renderedStyles.imgOpacity.previous = 1; this.renderedStyles.imgScaleX.current = this.renderedStyles.imgScaleX.previous = 1; this.renderedStyles.imgScaleY.current = this.renderedStyles.imgScaleY.previous = 1; this.renderedStyles.imgTranslation.current = this.renderedStyles.imgTranslation.previous = 0; const tl = new TimelineMax({ onComplete: () => { // Restart the rAF loop this.renderId = requestAnimationFrame(() => this.render()); // Reset values.. this.currentItem = undefined; this.upcomingItem = undefined; // Able to drag and animate again this.isAnimating = false; } }) // Animate the menu back to the previous position .to(this.DOM.menu, ANIMATION_SETTINGS.menu.duration, { ease: ANIMATION_SETTINGS.menu.ease, x: this.currentPosition }, 0); // Reset the letters translations and grid images if ( this.currentItem && this.upcomingItem ) { tl .to(this.currentItem.letters, ANIMATION_SETTINGS.letters.duration, { ease: ANIMATION_SETTINGS.letters.ease, x: '-100%' }, 0) .to(this.upcomingItem.letters, ANIMATION_SETTINGS.letters.duration, { ease: ANIMATION_SETTINGS.letters.ease, x: '0%' }, 0) .to(this.currentItem.imageGrid.DOM.images, ANIMATION_SETTINGS.images.duration, { ease: ANIMATION_SETTINGS.images.ease, opacity: 1, scaleX: 1, scaleY: 1, x: '0%' }, 0) .to(this.upcomingItem.imageGrid.DOM.images, ANIMATION_SETTINGS.images.duration, { ease: ANIMATION_SETTINGS.images.ease, opacity: 0, scaleX: 2, scaleY: 0.8, x: this.dragDirection === 'left' ? '150%' : '-150%' }, 0); } } // Move to the next/previous menu item else { // Show the explore link this.menuItems[this.upcomingIdx].showExplore(); // Set the updated menu translation value this.currentPosition = this.renderedStyles.menuTranslation.current = this.renderedStyles.menuTranslation.previous = this.dragDirection === 'left' ? this.currentPosition - this.amountToMove : this.currentPosition + this.amountToMove; // Reset letters translation value this.renderedStyles.letterTranslation.current = this.renderedStyles.letterTranslation.previous = 0; // Reset grid images values this.renderedStyles.imgOpacity.current = this.renderedStyles.imgOpacity.previous = 1; this.renderedStyles.imgScaleX.current = this.renderedStyles.imgScaleX.previous = 1; this.renderedStyles.imgScaleY.current = this.renderedStyles.imgScaleY.previous = 1; this.renderedStyles.imgTranslation.current = this.renderedStyles.imgTranslation.previous = 0; const tl = new TimelineMax({ onComplete: () => { // Restart the rAF loop this.renderId = requestAnimationFrame(() => this.render()); // Update the menu item current state this.currentItem.unsetCurrent(); this.upcomingItem.setCurrent(); // Update the current item index value this.current = this.upcomingIdx; // Reset values.. this.currentItem = undefined; this.upcomingItem = undefined; // Able to drag and animate again this.isAnimating = false; } }) // Animate the menu translation .to(this.DOM.menu, ANIMATION_SETTINGS.menu.duration, { ease: ANIMATION_SETTINGS.menu.ease, x: this.currentPosition }, 0) // Animate the letters (current item gets stroke letters while the previous current item gets filled, thus the translation needs to be set differently for the current and upcoming item) .to(this.currentItem.letters, ANIMATION_SETTINGS.letters.duration, { ease: ANIMATION_SETTINGS.letters.ease, x: '0%' }, 0) .to(this.upcomingItem.letters, ANIMATION_SETTINGS.letters.duration, { ease: ANIMATION_SETTINGS.letters.ease, x: '-100%' }, 0) // And animate the grid images .to(this.currentItem.imageGrid.DOM.images, ANIMATION_SETTINGS.images.duration, { ease: ANIMATION_SETTINGS.images.ease, opacity: 0, scaleX: 2, scaleY: 0.8, x: this.dragDirection === 'left' ? '-150%' : '150%' }, 0) .to(this.upcomingItem.imageGrid.DOM.images, ANIMATION_SETTINGS.images.duration, { ease: ANIMATION_SETTINGS.images.ease, opacity: 1, scaleX: 1, scaleY: 1, x: '0%' }, 0); } } // Reset the drag position value this.dragPosition = 0; this.draggie.setPosition(this.dragPosition, this.draggie.position.y); // Reset the drag direction value this.dragDirection = ''; }; // Draggabily events this.draggie.on('pointerDown', this.onPointerDown); this.draggie.on('dragStart', this.onDragStart); this.draggie.on('dragMove', this.onDragMove); this.draggie.on('pointerUp', this.onPointerUp); this.draggie.on('dragEnd', this.onDragEnd); // Clicking the explore opens up the grid for the current menu item for ( let menuItem of this.menuItems ) { menuItem.DOM.explore.addEventListener('click', () => this.showContent()); } // Back to menu from grid view this.DOM.backToMenuCtrl.addEventListener('click', () => this.hideContent()); // Resize window: update menu position window.addEventListener('resize', () => this.resize()); } showBackCtrl() { return this.toggleBackCtrl('show'); } hideBackCtrl() { return this.toggleBackCtrl('hide'); } toggleBackCtrl(action = 'show') { return new Promise((resolve, reject) => { TweenMax.to(this.DOM.backToMenuCtrl, ANIMATION_SETTINGS.backCtrl.duration, { ease: ANIMATION_SETTINGS.backCtrl.ease, startAt: action === 'hide' ? null : {x: '100%'}, opacity: action === 'hide' ? 0 : 1, x: action === 'hide' ? '-100%' : '0%', onComplete: resolve }); }); } showContent() { if ( this.isAnimating ) return; this.isAnimating = true; // Cancel the render function (rAF) if ( this.renderId ) { window.cancelAnimationFrame(this.renderId); this.renderId = undefined; } // Remove this class so we see a scrollable area now this.DOM.pagePreview.classList.remove('page--preview'); let promises = []; // Reset the transforms of the grid items forming again the original grid promises.push(this.menuItems[this.current].imageGrid.collapse()); // Hide the explore link promises.push(this.menuItems[this.current].hideExplore()); // Slide menu items letters out for (let item of this.menuItems) { promises.push(item.hide()); } // Show back control promises.push(this.showBackCtrl()); Promise.all(promises).then(() => this.isAnimating = false); } hideContent() { if ( this.isAnimating ) return; this.isAnimating = true; // First scroll to the top scrollIt(0, 300, 'easeOutQuad', () => { // Add this class to disable scrolling this.DOM.pagePreview.classList.add('page--preview'); // Restart the rAF loop this.renderId = requestAnimationFrame(() => this.render()); let promises = []; // Spread the grid items forming again the original grid promises.push(this.menuItems[this.current].imageGrid.spread(true)); // Show the explore link promises.push(this.menuItems[this.current].showExplore()); // Slide menu items letters in for (let item of this.menuItems) { promises.push(item.show()); } // Hide back control promises.push(this.hideBackCtrl()); Promise.all(promises).then(() => { this.isAnimating = false; }); }); } } const ANIMATION_SETTINGS = { // Animation settings (after the drag ends, the menu, letters and images need to be positioned or reset) menu: {duration: 0.8, ease: Cubic.easeOut}, letters: {duration: 0.8, ease: Cubic.easeOut}, images: {duration: 1, ease: Quint.easeOut}, // Grid grid: {duration: 0.8, ease: Expo.easeOut}, // Hiding the letters to show the images grid allMenuLettersToggle: {duration: 0.8, ease: Expo.easeOut}, // Explore link explore: {duration: 0.6, ease: Expo.easeOut}, // backToMenuCtrl backCtrl: {duration: 0.6, ease: Expo.easeOut}, // Cursor arrows cursor: {duration: 1, ease: Expo.easeOut}, }; // Custom mouse cursor const cursor = new Cursor(document.querySelector('.cursor')); /***********************************/ /****** Custom cursor related ******/ // Activate the enter/leave/click methods of the custom cursor when hovering in/out on every and the back to menu ctrl [...document.querySelectorAll('a'), document.querySelector('button')].forEach((link) => { link.addEventListener('mouseenter', () => cursor.enter()); link.addEventListener('mouseleave', () => cursor.leave()); }); /***********************************/ /********** Preload stuff **********/ // Preload images const preloadImages = () => { return new Promise((resolve, reject) => { imagesLoaded(document.querySelectorAll('.grid__item'), {background: true}, resolve); }); }; // Preload fonts const preloadFonts = () => { return new Promise((resolve, reject) => { WebFont.load({ typekit: { id: 'crf4rue' }, active: resolve }); }); }; Promise.all([ preloadImages(), preloadFonts() ]).then(() => { // the Menu const menu = new Menu(document.querySelector('.menu-wrap')); document.body.classList.remove('loading'); }); }