import FontFaceObserver from "fontfaceobserver"; import imagesLoaded from "imagesLoaded"; import Scene from "./scene"; const scene = new Scene("container"); // helper functions const MathUtils = { // map number x from range [a, b] to [c, d] map: (x, a, b, c, d) => ((x - a) * (d - c)) / (b - a) + c, // linear interpolation lerp: (a, b, n) => (1 - n) * a + n * b }; // body element const body = document.body; let IMAGES; // calculate the viewport size let winsize; const calcWinsize = () => (winsize = { width: window.innerWidth, height: window.innerHeight }); calcWinsize(); // and recalculate on resize window.addEventListener("resize", calcWinsize); window.onbeforeunload = function() { window.scrollTo(0, 0); }; // scroll position and update function let docScroll; const getPageYScroll = () => (docScroll = window.pageYOffset || document.documentElement.scrollTop); window.addEventListener("scroll", getPageYScroll); // Item class Item { constructor(el, scroll) { // the .item element this.scroll = scroll; this.DOM = { el: el.img }; this.currentScroll = docScroll; this.animated = false; this.isBeingAnimatedNow = false; this.shouldRollBack = false; this.shouldUnRoll = false; this.positions = []; // set the initial values this.getSize(); this.mesh = scene.createMesh({ width: this.width, height: this.height, src: this.src, image: this.DOM.el, iWidth: this.DOM.el.width, iHeight: this.DOM.el.height }); scene.scene.add(this.mesh); // use the IntersectionObserver API to check when the element is inside the viewport // only then the element translation will be updated this.intersectionRatio; let options = { root: null, rootMargin: "0px", threshold: [0, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1] }; this.observer = new IntersectionObserver(entries => { entries.forEach(entry => { this.positions.push(entry.boundingClientRect.y); let compareArray = this.positions.slice( this.positions.length - 2, this.positions.length ); let down = compareArray[0] > compareArray[1] ? true : false; this.isVisible = entry.intersectionRatio > 0.0; this.shouldRollBack = false; this.shouldUnRoll = false; if ( entry.intersectionRatio < 0.5 && entry.boundingClientRect.y > 0 && this.isVisible && !down ) { this.shouldRollBack = true; } if ( entry.intersectionRatio > 0.5 && entry.boundingClientRect.y > 0 && this.isVisible ) { this.shouldUnRoll = true; } console.log(this.isVisible,'vis'); this.mesh.visible = this.isVisible; }); }, options); this.observer.observe(this.DOM.el); // init/bind events window.addEventListener("resize", () => this.resize()); this.render(0); } getSize() { // get all the sizes here, bounds and all const bounds = this.DOM.el.getBoundingClientRect(); const fromTop = bounds.top; const windowHeight = window.innerHeight; const withoutHeight = fromTop - windowHeight; const withHeight = fromTop + bounds.height; this.insideTop = withoutHeight - docScroll; this.insideRealTop = fromTop + docScroll; this.insideBottom = withHeight - docScroll + 50; this.width = bounds.width; this.height = bounds.height; this.left = bounds.left; } resize() { // on resize rest sizes and update the translation value this.getSize(); this.mesh.scale.set(this.width, this.height, 200); this.render(this.scroll.renderedStyles.translationY.current); this.scroll.shouldRender = true; } render(currentScroll) { this.currentScroll = currentScroll; this.mesh.position.y = currentScroll + winsize.height / 2 - this.insideRealTop - this.height / 2; this.mesh.position.x = 0 - winsize.width / 2 + this.left + this.width / 2; } } // SmoothScroll class SmoothScroll { constructor() { this.shouldRender = false; // the <main> element this.DOM = { main: document.querySelector("main") }; // the scrollable element // we translate this element when scrolling (y-axis) this.DOM.scrollable = this.DOM.main.querySelector("div[data-scroll]"); // the items on the page this.items = []; this.createItems(); this.listenMouse() // here we define which property will change as we scroll the page // in this case we will be translating on the y-axis // we interpolate between the previous and current value to achieve the smooth scrolling effect this.renderedStyles = { translationY: { // interpolated value previous: 0, // current value current: 0, // amount to interpolate ease: 0.1, // current value setter // in this case the value of the translation will be the same like the document scroll setValue: () => docScroll } }; // set the body's height this.setSize(); // set the initial values this.update(); // the <main> element's style needs to be modified this.style(); // init/bind events this.initEvents(); // start the render loop requestAnimationFrame(() => this.render()); } listenMouse(){ document.addEventListener('mousemove',()=>{ this.shouldRender = true; }) } update() { // sets the initial value (no interpolation) - translate the scroll value for (const key in this.renderedStyles) { this.renderedStyles[key].current = this.renderedStyles[ key ].previous = this.renderedStyles[key].setValue(); } // translate the scrollable element this.setPosition(); this.shouldRender = true; } setPosition() { // translates the scrollable element if ( Math.round(this.renderedStyles.translationY.previous) !== Math.round(this.renderedStyles.translationY.current) || this.renderedStyles.translationY.previous < 10 ) { this.shouldRender = true; this.DOM.scrollable.style.transform = `translate3d(0,${-1 * this.renderedStyles.translationY.previous}px,0)`; // console.log(this.items); for (const item of this.items) { // if the item is inside the viewport call it's render function // this will update the item's inner image translation, based on the document scroll value and the item's position on the viewport if (item.isVisible || item.isBeingAnimatedNow) { item.render(this.renderedStyles.translationY.previous); } } } ; if(scene.targetSpeed>0.01) this.shouldRender = true; if (this.shouldRender) { this.shouldRender = false; scene.render(); } } setSize() { // set the heigh of the body in order to keep the scrollbar on the page // console.log(this.DOM.scrollable.scrollHeight, 'HEIGHT'); body.style.height = `${this.DOM.scrollable.scrollHeight}px`; } createItems() { IMAGES.forEach(image => { if (image.img.classList.contains("js-image")) { this.items.push(new Item(image, this)); } }); } style() { // the <main> needs to "stick" to the screen and not scroll // for that we set it to position fixed and overflow hidden this.DOM.main.style.position = "fixed"; this.DOM.main.style.width = this.DOM.main.style.height = "100%"; this.DOM.main.style.top = this.DOM.main.style.left = 0; this.DOM.main.style.overflow = "hidden"; } initEvents() { // on resize reset the body's height window.addEventListener("resize", () => this.setSize()); } render() { // update the current and interpolated values for (const key in this.renderedStyles) { this.renderedStyles[key].current = this.renderedStyles[key].setValue(); this.renderedStyles[key].previous = MathUtils.lerp( this.renderedStyles[key].previous, this.renderedStyles[key].current, this.renderedStyles[key].ease ); } // and translate the scrollable element this.setPosition(); // loop.. requestAnimationFrame(() => this.render()); } } /***********************************/ /********** Preload stuff **********/ const fontParalucent = new FontFaceObserver("laca-text").load() const fontStarling = new FontFaceObserver("operetta-12").load() // Preload images const preloadImages = new Promise((resolve, reject) => { imagesLoaded(document.querySelectorAll("img"), { background: true }, resolve); }); preloadImages.then(images => { IMAGES = images.images; }); const preloadEverything = [fontStarling, fontParalucent, preloadImages]; // And then.. Promise.all(preloadEverything).then(() => { // Remove the loader document.body.classList.remove("loading"); document.body.classList.add("loaded"); // Get the scroll position getPageYScroll(); // Initialize the Smooth Scrolling new SmoothScroll(); });