UNPKG

rfx-parallax

Version:
567 lines (556 loc) 26.1 kB
import { isPlatformBrowser } from '@angular/common'; import * as i0 from '@angular/core'; import { PLATFORM_ID, Injectable, Inject, Directive, Input, Component, NgModule } from '@angular/core'; import { Subject, BehaviorSubject } from 'rxjs'; import { trigger, state, style } from '@angular/animations'; class ResizeEventService { constructor(platformId) { this.platformId = platformId; this.subjectResize = new Subject(); this.resizeEvent = this.onResizeEvent.bind(this); this.isBrowser = isPlatformBrowser(platformId); } ngOnDestroy() { this.destroyListener(); } /** * Create window resize listener. */ createListener() { if (this.isBrowser) { window.addEventListener('resize', this.resizeEvent, { passive: true }); } } /** * Destroy window resize listener. */ destroyListener() { if (this.isBrowser) { window.removeEventListener('resize', this.resizeEvent); } } /** * Trigger window resize event. */ onResizeEvent() { this.subjectResize.next(undefined); } /** * Get window resize event. * @return {Observable<undefined>} - Window resize event. */ getResize() { return this.subjectResize.asObservable(); } } ResizeEventService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.0.2", ngImport: i0, type: ResizeEventService, deps: [{ token: PLATFORM_ID }], target: i0.ɵɵFactoryTarget.Injectable }); ResizeEventService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "13.0.2", ngImport: i0, type: ResizeEventService, providedIn: 'root' }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.0.2", ngImport: i0, type: ResizeEventService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: function () { return [{ type: Object, decorators: [{ type: Inject, args: [PLATFORM_ID] }] }]; } }); class ScrollEventService { constructor() { this.subjectScroll = new BehaviorSubject(0); this.mouseScrollEvent = this.onMouseScroll.bind(this); } ngOnDestroy() { this.destroyListener(); } /** * Create mouse scroll listener. * @param {HTMLElement} element - Element with scroll event. */ createListener(element) { this.element = element; this.element.addEventListener('scroll', this.mouseScrollEvent, { passive: true }); } /** * Destroy mouse scroll listener. */ destroyListener() { var _a; (_a = this.element) === null || _a === void 0 ? void 0 : _a.removeEventListener('scroll', this.mouseScrollEvent); this.element = undefined; } /** * Trigger mouse scroll event. * @param {number} event - Mouse scroll event. */ onMouseScroll(event) { const target = event.target instanceof Document ? event.target.documentElement : event.target; this.subjectScroll.next(target.scrollTop); } /** * Get body scroll event. * @return {Observable<number>} - Body scroll event. */ getMouseScroll() { return this.subjectScroll.asObservable(); } /** * Get body scroll current value. * @return {number} - Body scroll value. */ getMouseScrollValue() { return this.subjectScroll.value; } } ScrollEventService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.0.2", ngImport: i0, type: ScrollEventService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); ScrollEventService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "13.0.2", ngImport: i0, type: ScrollEventService, providedIn: 'root' }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.0.2", ngImport: i0, type: ScrollEventService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: function () { return []; } }); class RfxParallaxService { constructor(resizeEventService, scrollEventService, platformId) { this.resizeEventService = resizeEventService; this.scrollEventService = scrollEventService; this.platformId = platformId; this.isBrowser = isPlatformBrowser(this.platformId); } /** * Start element scroll event, window resize event and element resize event listeners * @param element main element with scroll property */ initListeners(element) { if (this.isBrowser) { this.resizeEventService.createListener(); this.scrollEventService.createListener(element !== null && element !== void 0 ? element : document); } } } RfxParallaxService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.0.2", ngImport: i0, type: RfxParallaxService, deps: [{ token: ResizeEventService }, { token: ScrollEventService }, { token: PLATFORM_ID }], target: i0.ɵɵFactoryTarget.Injectable }); RfxParallaxService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "13.0.2", ngImport: i0, type: RfxParallaxService, providedIn: 'root' }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.0.2", ngImport: i0, type: RfxParallaxService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: function () { return [{ type: ResizeEventService }, { type: ScrollEventService }, { type: Object, decorators: [{ type: Inject, args: [PLATFORM_ID] }] }]; } }); const visibilityAnimation = [ trigger('visibility', [ state('true', style({ opacity: 1 })), state('false', style({ opacity: 0, })) // transition('true <=> false', [ // animate('{{ transitionDurationMs }}ms {{ transitionDelayMs }}ms {{ transitionTimingFunction }}') // ], { params: { transitionDurationMs: 0, transitionDelayMs: 0, transitionTimingFunction: 'ease' }}) ]) ]; class WillChangeDirective { constructor(htmlElement, renderer, scrollEventService) { this.htmlElement = htmlElement; this.renderer = renderer; this.scrollEventService = scrollEventService; this.triggerArea = typeof window !== 'undefined' ? window.innerHeight / 4 * 6 : 0; this.isDisabled = false; this.willChange = true; } ngOnInit() { if (!this.isDisabled) { this.willChangeArea = this.getWillChangeArea(); this.createListener(); } } ngOnDestroy() { this.destroyListener(); } /** * Create listener for scroll event. */ createListener() { this.scrollEventListener = this.scrollEventService.getMouseScroll().subscribe((scroll) => this.checkWillChange(scroll)); } /** * Destroy scroll event listener. */ destroyListener() { var _a; (_a = this.scrollEventListener) === null || _a === void 0 ? void 0 : _a.unsubscribe(); } /** * Refresh will-change area if some properties changed. * @param {SimpleChanges} changes - Changes. */ ngOnChanges(changes) { if (changes['isDisabled'] && (changes['isDisabled'].currentValue !== undefined) && !changes['isDisabled'].firstChange) { this.destroyListener(); return; } if (changes['triggerArea'] && (changes['triggerArea'].currentValue !== undefined) && !changes['triggerArea'].firstChange && !this.isDisabled) { const scroll = this.scrollEventService.getMouseScrollValue(); this.willChangeArea = this.getWillChangeArea(); this.checkWillChange(scroll); } } /** * Check will-change property and * update if needed. * @param {number} scroll - Scroll position. */ checkWillChange(scroll) { if (this.willChangeArea) { const isElementInArea = this.isElementInArea(scroll, this.willChangeArea); if (isElementInArea !== this.willChange) { this.setWillChange(isElementInArea); } } } /** * Check if element is in will-change area. * @param {number} scroll - Scroll position. * @param {SectionAreaModel} willChangeArea - Will-change area. * @returns {boolean} - Is element in will-change area. */ isElementInArea(scroll, willChangeArea) { return scroll >= willChangeArea.top && scroll <= willChangeArea.bottom; } /** * Calculate area where element is probably going to be animated soon. * @returns {number} - Will-change area. */ getWillChangeArea() { const elementRect = this.htmlElement.nativeElement.getBoundingClientRect(); const scroll = this.scrollEventService.getMouseScrollValue(); const elementTop = elementRect.top + scroll; const areaTop = elementTop - this.triggerArea; const areaBottom = elementTop + this.triggerArea; return { top: areaTop, bottom: areaBottom }; } /** * Set element will-change property. * @param {boolean} willChange - will-change enabled or disabled. */ setWillChange(willChange) { this.willChange = willChange; this.renderer.setStyle(this.htmlElement.nativeElement, 'will-change', willChange ? 'transform' : 'auto'); } } WillChangeDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.0.2", ngImport: i0, type: WillChangeDirective, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }, { token: ScrollEventService }], target: i0.ɵɵFactoryTarget.Directive }); WillChangeDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "12.0.0", version: "13.0.2", type: WillChangeDirective, selector: "[libWillChange]", inputs: { triggerArea: "triggerArea", isDisabled: "isDisabled" }, usesOnChanges: true, ngImport: i0 }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.0.2", ngImport: i0, type: WillChangeDirective, decorators: [{ type: Directive, args: [{ selector: '[libWillChange]' }] }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i0.Renderer2 }, { type: ScrollEventService }]; }, propDecorators: { triggerArea: [{ type: Input }], isDisabled: [{ type: Input }] } }); class RfxParallaxComponent { constructor(htmlElement, renderer, scrollEventService, resizeEventService, platformId) { this.htmlElement = htmlElement; this.renderer = renderer; this.scrollEventService = scrollEventService; this.resizeEventService = resizeEventService; this.platformId = platformId; this.parallaxPercentage = 40; this.positionPercentage = 50; this.imageZIndex = -1; this.isDisabled = false; this.visibleOverflow = false; this.imageLeftPx = 0; this.isLoaded = false; this.forceFullWidth = false; this.isBrowser = isPlatformBrowser(this.platformId); } /** * Set container position and overflow. * If parallax is not disabled, create listeners. */ ngOnInit() { if (this.isBrowser) { this.setContainerPosition(this.htmlElement.nativeElement); this.setContainerOverflow(this.htmlElement.nativeElement, this.visibleOverflow); if (!this.isDisabled) { this.createListeners(); } } } /** * Update parallax values when some properies changed. * - isDisabled change, enable or disable parallax and refresh image properties. * - visibleOverflow change, show or hide image overflow on container without refresh. * - imagerUrl, parallaxPercentage or positionPercentage change, refresh image properties. * @param {SimpleChanges} changes - SimpleChanges object. */ ngOnChanges(changes) { if (this.isBrowser) { if (this.hasValueChanged(changes['isDisabled'], true)) { this.destroyListeners(); } else if (this.hasValueChanged(changes['isDisabled'], false)) { this.createListeners(); } if (this.hasValueChanged(changes['visibleOverflow'])) { this.setContainerOverflow(this.htmlElement.nativeElement, changes['visibleOverflow'].currentValue); } if (this.image && ((this.hasValueChanged(changes['imageUrl']) || this.hasValueChanged(changes['parallaxPercentage']) || this.hasValueChanged(changes['positionPercentage'])) || this.hasValueChanged(changes['isDisabled']) || this.hasValueChanged(changes['forceFullWidth']))) { this.setImageProperties(this.image); } } } ngOnDestroy() { this.destroyListeners(); } /** * Check if SimpleChange value has changed. * Eventually check if value corresponds to a new value. * @param {SimpleChange} change - SimpleChange object. * @param {any} newValue - New value. * @returns {boolean} - true if value has changed. */ hasValueChanged(change, newValue = change === null || change === void 0 ? void 0 : change.currentValue) { return (change === null || change === void 0 ? void 0 : change.firstChange) === false && (change === null || change === void 0 ? void 0 : change.currentValue) !== undefined && (change === null || change === void 0 ? void 0 : change.currentValue) === newValue; } /** * Destroy all listeners. */ destroyListeners() { var _a, _b; (_a = this.scrollEventListener) === null || _a === void 0 ? void 0 : _a.unsubscribe(); (_b = this.resizeEventListener) === null || _b === void 0 ? void 0 : _b.unsubscribe(); } /** * Listen to scroll and resize events. * Destroy alreay existing listeners. */ createListeners() { this.destroyListeners(); this.scrollEventListener = this.scrollEventService.getMouseScroll().subscribe((scroll) => this.onMouseScroll(scroll)); this.resizeEventListener = this.resizeEventService.getResize().subscribe(() => this.onResize()); } /** * On mouse scroll event recalculate and change * parallaxed image position. * @param {number} scroll - Scroll value. */ onMouseScroll(scroll) { if (this.parallaxBoundaries && this.image) { const topPx = this.getImageTop(scroll, this.parallaxBoundaries); this.setImageTransform(this.image, this.imageLeftPx, topPx); } } /** * On window resize event reload parallax properties. */ onResize() { if (this.image) { this.setImageProperties(this.image); } } /** * Set image object and loading status. * Set image properties after image is fully loaded * but only if platform is browser. * @param {Event} event - Image loaded event. */ onImageLoaded(event) { this.image = event.target; if (this.isBrowser) { this.setImageProperties(this.image); } this.isLoaded = true; } /** * Set image properties needed for parallax effect. * @param {HTMLImageElement} image - Image to be parallaxed. */ setImageProperties(image) { const scrollTop = this.scrollEventService.getMouseScrollValue(); this.setImageSize(image, this.htmlElement.nativeElement.clientWidth, this.htmlElement.nativeElement.clientHeight, this.parallaxPercentage, this.isDisabled, this.forceFullWidth); this.parallaxBoundaries = this.getParallaxBoundaries(scrollTop, this.htmlElement.nativeElement, image.height, this.parallaxPercentage); this.imageLeftPx = this.getImageLeft(this.htmlElement.nativeElement.clientWidth, image.width, this.positionPercentage); const topPx = this.isDisabled ? this.getDisabledImageTop(this.parallaxBoundaries) : this.getImageTop(scrollTop, this.parallaxBoundaries); this.setImageTransform(image, this.imageLeftPx, topPx); } /** * Set container position to relative * if image is not already in relative or absolute position. * @param {HTMLElement} container */ setContainerPosition(container) { const position = getComputedStyle(container).position; if (position !== 'relative' && position !== 'absolute') { this.renderer.setStyle(container, 'position', 'relative'); } } /** * Set container overflow. * @param {HTMLElement} container - Container element. * @param {boolean} isOverflowVisible - true to show container overflow. */ setContainerOverflow(container, isOverflowVisible) { this.renderer.setStyle(container, 'overflow', isOverflowVisible ? 'visible' : 'hidden'); } /** * Set image size based on container size, * parallax percentage and disabled state. * @param {HTMLImageElement} image - Image element. * @param {number} containerWidth - Container width. * @param {number} containerHeight - Container height. * @param {number} parallaxPercentage - Parallax percentage. * @param {boolean} isDisabled - true to disable parallax effect. * @param {boolean} forceFullWidth - true to force image size to container width. */ setImageSize(image, containerWidth, containerHeight, parallaxPercentage, isDisabled, forceFullWidth) { if (forceFullWidth) { this.renderer.setAttribute(image, 'width', `${containerWidth}px`); this.renderer.setAttribute(image, 'height', `auto`); } else { const minimumHeight = (containerHeight * (100 + (isDisabled ? 0 : parallaxPercentage))) / 100; const ratio = image.naturalHeight / image.naturalWidth; const minimumRatio = minimumHeight / containerWidth; if (ratio > minimumRatio) { this.renderer.setAttribute(image, 'width', `${containerWidth}px`); this.renderer.setAttribute(image, 'height', `auto`); } else { this.renderer.setAttribute(image, 'width', `auto`); this.renderer.setAttribute(image, 'height', `${minimumHeight}px`); } } } /** * Get parallax boundaries. This data is used * to calculate parallax movement. * @param {number} scrollTop - Scroll top. * @param {HTMLElement} container - Container element. * @param {number} imageHeight - Image height. * @param {number} parallaxPercentage - Parallax percentage. * @return {ParallaxBoundariesModel} - Parallax boundaries. */ getParallaxBoundaries(scrollTop, container, imageHeight, parallaxPercentage) { const elementTop = container.getBoundingClientRect().top + scrollTop; const usablePixels = container.clientHeight / 100 * parallaxPercentage; const unusablePixels = imageHeight - container.clientHeight - usablePixels; const startPoint = elementTop - usablePixels - window.innerHeight; const endPoint = elementTop + container.clientHeight + usablePixels; const totalPixels = endPoint - startPoint; return { startPoint, endPoint, totalPixels, usablePixels, unusablePixels }; } /** * Get image left position. * @param {number} containerWidth - Container width. * @param {number} imageWidth - Image width. * @param {number} positionPercentage - Position percentage. * @return {number} - Image left position. */ getImageLeft(containerWidth, imageWidth, positionPercentage) { return (containerWidth - imageWidth) / 100 * positionPercentage; } /** * Get image top position. * @param {number} scrollTop - Scroll top. * @param {ParallaxBoundariesModel} boundaries - Parallax boundaries. * @return {number} - Image top position. */ getImageTop(scrollTop, boundaries) { const area = Math.max(0, Math.min(scrollTop - boundaries.startPoint, boundaries.totalPixels)); const areaPercentage = 100 / boundaries.totalPixels * area; return -boundaries.usablePixels * (1 - areaPercentage / 100) - boundaries.unusablePixels / 2; } /** * Get disabled image top position. * @param {number} boundaries - Parallax boundaries. * @return {number} - Image top position. */ getDisabledImageTop(boundaries) { return (-boundaries.usablePixels - boundaries.unusablePixels) / 2; } /** * Set image transform properties. * @param {HTMLImageElement} image - Image element. * @param {number} leftPx - Image left position. * @param {number} topPx - Image top position. */ setImageTransform(image, leftPx, topPx) { const newTranslate = `translate(${leftPx.toFixed(1)}px, ${topPx.toFixed(1)}px)`; if (image.style.transform !== newTranslate) { this.renderer.setStyle(image, 'transform', newTranslate); } } } RfxParallaxComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.0.2", ngImport: i0, type: RfxParallaxComponent, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }, { token: ScrollEventService }, { token: ResizeEventService }, { token: PLATFORM_ID }], target: i0.ɵɵFactoryTarget.Component }); RfxParallaxComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.0.2", type: RfxParallaxComponent, selector: "[libRfxParallax]", inputs: { imageUrl: "imageUrl", imageAlt: "imageAlt", parallaxPercentage: "parallaxPercentage", positionPercentage: "positionPercentage", imageZIndex: "imageZIndex", visibleOverflow: "visibleOverflow", isDisabled: "isDisabled", forceFullWidth: "forceFullWidth" }, usesOnChanges: true, ngImport: i0, template: "<img\r\n [alt]=\"imageAlt ?? ''\"\r\n [@visibility]=\"isLoaded\"\r\n libWillChange\r\n [isDisabled]=\"isDisabled || !isBrowser\"\r\n class=\"rfx-image\"\r\n [src]=\"imageUrl\"\r\n [style.zIndex]=\"imageZIndex\"\r\n (load)=\"onImageLoaded($event)\" />\r\n\r\n<ng-content></ng-content>\r\n", styles: [".rfx-image{position:absolute;left:0;top:0;will-change:transform;opacity:0}\n"], directives: [{ type: WillChangeDirective, selector: "[libWillChange]", inputs: ["triggerArea", "isDisabled"] }], animations: [ visibilityAnimation ] }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.0.2", ngImport: i0, type: RfxParallaxComponent, decorators: [{ type: Component, args: [{ selector: '[libRfxParallax]', animations: [ visibilityAnimation ], template: "<img\r\n [alt]=\"imageAlt ?? ''\"\r\n [@visibility]=\"isLoaded\"\r\n libWillChange\r\n [isDisabled]=\"isDisabled || !isBrowser\"\r\n class=\"rfx-image\"\r\n [src]=\"imageUrl\"\r\n [style.zIndex]=\"imageZIndex\"\r\n (load)=\"onImageLoaded($event)\" />\r\n\r\n<ng-content></ng-content>\r\n", styles: [".rfx-image{position:absolute;left:0;top:0;will-change:transform;opacity:0}\n"] }] }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i0.Renderer2 }, { type: ScrollEventService }, { type: ResizeEventService }, { type: Object, decorators: [{ type: Inject, args: [PLATFORM_ID] }] }]; }, propDecorators: { imageUrl: [{ type: Input }], imageAlt: [{ type: Input }], parallaxPercentage: [{ type: Input }], positionPercentage: [{ type: Input }], imageZIndex: [{ type: Input }], visibleOverflow: [{ type: Input }], isDisabled: [{ type: Input }], forceFullWidth: [{ type: Input }] } }); class RfxParallaxModule { } RfxParallaxModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.0.2", ngImport: i0, type: RfxParallaxModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); RfxParallaxModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", version: "13.0.2", ngImport: i0, type: RfxParallaxModule, declarations: [RfxParallaxComponent, WillChangeDirective], exports: [RfxParallaxComponent] }); RfxParallaxModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "13.0.2", ngImport: i0, type: RfxParallaxModule, imports: [[]] }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.0.2", ngImport: i0, type: RfxParallaxModule, decorators: [{ type: NgModule, args: [{ declarations: [ RfxParallaxComponent, WillChangeDirective ], imports: [], exports: [ RfxParallaxComponent ] }] }] }); /* * Public API Surface of rfx-parallax */ /** * Generated bundle index. Do not edit. */ export { RfxParallaxComponent, RfxParallaxModule, RfxParallaxService };