├── .editorconfig ├── .gitignore ├── README.md ├── package.json ├── src ├── index.ts ├── parallax-base.ts ├── parallax-bg.directive.ts ├── parallax-header.directive.ts └── parallax.module.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent coding styles between different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | 10 | # We recommend you to keep these unchanged 11 | end_of_line = lf 12 | charset = utf-8 13 | trim_trailing_whitespace = true 14 | insert_final_newline = true 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | dist 4 | aot 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ionic-parallax 2 | 3 | WIP, here's a quick usage example: 4 | 5 | ```bash 6 | npm i --save ionic-parallax 7 | ``` 8 | 9 | ```ts 10 | import { ParallaxModule } from 'ionic-parallax'; 11 | 12 | @NgModule({ 13 | imports: [ 14 | ... 15 | ParallaxModule 16 | ] 17 | }) 18 | ``` 19 | 20 | ```html 21 | 22 | 23 |
24 |
25 |
26 | ``` 27 | 28 | ```scss 29 | ion-card { 30 | position: relative; 31 | 32 | > img[parallax] { 33 | position: absolute; 34 | height: inherit; 35 | } 36 | } 37 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ionic-parallax", 3 | "version": "0.2.0", 4 | "files": [ 5 | "dist" 6 | ], 7 | "description": "", 8 | "main": "dist/index.js", 9 | "typings": "dist/index.d.ts", 10 | "module": "dist/index.js", 11 | "scripts": { 12 | "build": "rm -rf aot dist && ngc" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/zyra/ionic-parallax.git" 17 | }, 18 | "author": "", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/zyra/ionic-parallax/issues" 22 | }, 23 | "homepage": "https://github.com/zyra/ionic-parallax#readme", 24 | "devDependencies": { 25 | "@angular/common": "^4.1.0", 26 | "@angular/compiler": "^4.1.0", 27 | "@angular/compiler-cli": "^4.1.0", 28 | "@angular/core": "^4.1.0", 29 | "@angular/forms": "^4.1.0", 30 | "@angular/platform-browser": "^4.1.0", 31 | "ionic-angular": "^3.1.1", 32 | "rxjs": "^5.3.1", 33 | "typescript": "^2.3.2" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './parallax-bg.directive'; 2 | export * from './parallax-header.directive'; 3 | export * from './parallax.module'; 4 | -------------------------------------------------------------------------------- /src/parallax-base.ts: -------------------------------------------------------------------------------- 1 | import { OnDestroy, AfterViewInit, Renderer2, ElementRef, Input } from '@angular/core'; 2 | import { Content, DomController, Platform } from 'ionic-angular'; 3 | import { Subscription } from 'rxjs/Subscription'; 4 | 5 | export abstract class ParallaxBase implements AfterViewInit, OnDestroy { 6 | 7 | @Input() 8 | ratio: number = 50; 9 | 10 | protected _watch: Subscription; 11 | 12 | constructor( 13 | protected _content: Content, 14 | protected _el: ElementRef, 15 | protected _rnd: Renderer2, 16 | protected _domCtrl: DomController, 17 | protected _plt: Platform 18 | ) {} 19 | 20 | ngAfterViewInit() { 21 | this.listen(); 22 | } 23 | 24 | ngOnDestroy() { 25 | this.unlisten(); 26 | } 27 | 28 | listen(): void { 29 | this._watch = this._content.ionScroll.subscribe(this._onScroll.bind(this)); 30 | } 31 | 32 | unlisten(): void { 33 | this._watch && typeof this._watch.unsubscribe === 'function' && this._watch.unsubscribe(); 34 | } 35 | 36 | getNativeElement(): HTMLElement { 37 | return this._el.nativeElement; 38 | } 39 | 40 | protected abstract _onScroll(ev: any, force: boolean); 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/parallax-bg.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, OnDestroy, ElementRef, Renderer2, AfterViewInit } from '@angular/core'; 2 | import { Content, DomController, Platform } from 'ionic-angular'; 3 | import { ParallaxBase } from './parallax-base'; 4 | 5 | @Directive({ 6 | selector: '[parallax-bg]' 7 | }) 8 | export class ParallaxBackground extends ParallaxBase implements AfterViewInit, OnDestroy, ParallaxBase { 9 | 10 | private _originalHeight: number; 11 | private _newHeight: number; 12 | private _lastY: number = 0; 13 | 14 | constructor( 15 | _content: Content, 16 | _el: ElementRef, 17 | _rnd: Renderer2, 18 | _domCtrl: DomController, 19 | _plt: Platform 20 | ) { 21 | super(_content, _el, _rnd, _domCtrl, _plt); 22 | } 23 | 24 | ngAfterViewInit() { 25 | this._setElementHeight(); 26 | super.ngAfterViewInit(); 27 | } 28 | 29 | private _getDimensions(): any { 30 | 31 | const 32 | el: HTMLElement = this.getNativeElement(), 33 | clientRect: ClientRect = el.getBoundingClientRect(), 34 | contentTop: number = this._content.contentTop, 35 | viewHeight: number = this._content.contentHeight, 36 | distanceYtoBottom: number = clientRect.bottom - contentTop, 37 | distanceYtoTop: number = clientRect.top - contentTop, 38 | elementHeight: number = el.offsetHeight, 39 | distanceYtoCenter: number = distanceYtoTop + elementHeight 40 | ; 41 | 42 | return { 43 | distanceYtoBottom, 44 | distanceYtoTop, 45 | distanceYtoCenter, 46 | viewHeight, 47 | elementHeight 48 | }; 49 | 50 | } 51 | 52 | private _setElementHeight(): void { 53 | 54 | const 55 | el: HTMLElement = this.getNativeElement(), 56 | { viewHeight, elementHeight } = this._getDimensions(), 57 | maxY = (viewHeight + (this._originalHeight = elementHeight)) / viewHeight * this.ratio / 100 + 0.5 58 | ; 59 | 60 | if (isNaN(viewHeight) || viewHeight === 0) { 61 | // in some cases the view height isn't available yet because the page hasn't loaded 62 | // we need to check in a bit 63 | this._plt.timeout(this._setElementHeight.bind(this), 100); 64 | return; 65 | } 66 | 67 | this._newHeight = this._originalHeight * (maxY + 1); 68 | 69 | this._rnd.setStyle(el, 'height', this._newHeight + 'px'); 70 | this._rnd.setStyle(el, 'background-position-x', '50%'); 71 | 72 | this._onScroll(null, true); 73 | 74 | } 75 | 76 | /** 77 | * Returns back the number of pixels that the background moved/should move by 78 | * @return {number} 79 | * @private 80 | */ 81 | protected _getOffsetY(): number { 82 | return this._lastY * this._originalHeight; 83 | } 84 | 85 | protected _onScroll(ev: any, force: boolean = false) { 86 | 87 | this._domCtrl.read(() => { 88 | 89 | const 90 | { distanceYtoBottom, distanceYtoTop, viewHeight, distanceYtoCenter } = this._getDimensions(), 91 | actualDistanceYtoBottom: number = distanceYtoBottom + this._newHeight - this._originalHeight - this._getOffsetY(), 92 | actualDistanceYtoTop: number = distanceYtoTop + this._getOffsetY() 93 | ; 94 | 95 | if (!force && (actualDistanceYtoBottom < 0 || actualDistanceYtoTop > viewHeight)) { 96 | // the element is not visible, let's not do anything about this scroll event 97 | return; 98 | } 99 | 100 | // magic formula 101 | // figure out how much should we move the image from 50% 102 | const y = (viewHeight - distanceYtoCenter) / viewHeight / 2 * 40 / 100 + 0.5; 103 | 104 | // make sure it's > 0 && < 100 105 | // also save this value to memory 106 | this._lastY = Math.min(1, Math.max(y, -1)); 107 | 108 | this._domCtrl.write(() => { 109 | 110 | // update the DOM! 111 | this._rnd.setStyle(this.getNativeElement(), this._plt.Css.transform, `translate3d(0, -${ this._getOffsetY() }px, 0)`) 112 | 113 | }); 114 | 115 | }); 116 | 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /src/parallax-header.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, Input, OnDestroy, ElementRef, Renderer2, AfterViewInit } from '@angular/core'; 2 | import { Content, DomController, Platform, ViewController } from 'ionic-angular'; 3 | import { ParallaxBase } from './parallax-base'; 4 | import { Subscription } from 'rxjs/Subscription'; 5 | import { Observable } from 'rxjs/Observable'; 6 | import 'rxjs/add/observable/fromEvent'; 7 | 8 | @Directive({ 9 | selector: '[parallax-header]' 10 | }) 11 | export class ParallaxHeader extends ParallaxBase implements AfterViewInit, OnDestroy, ParallaxBase { 12 | 13 | @Input() 14 | set fade(val: boolean) { 15 | this._fade = typeof val !== 'boolean' || val; 16 | } 17 | 18 | get fade(): boolean { 19 | return this._fade; 20 | } 21 | 22 | @Input() 23 | set scale(val: boolean) { 24 | this._scale = typeof val !== 'boolean' || val; 25 | } 26 | 27 | get scale(): boolean { 28 | return this._scale; 29 | } 30 | 31 | @Input() 32 | set compact(val: boolean) { 33 | this._compact = typeof val !== 'boolean' || val; 34 | } 35 | 36 | get compact(): boolean { 37 | return this._compact; 38 | } 39 | 40 | @Input() 41 | set bounce(val: boolean) { 42 | this._bounce = typeof val !== 'boolean' || val; 43 | } 44 | 45 | get bounce(): boolean { 46 | return this._bounce; 47 | } 48 | 49 | @Input() 50 | fadeAt: number; 51 | 52 | private _originalHeight: number; 53 | private _fade: boolean = false; 54 | private _scale: boolean = false; 55 | private _compact: boolean = false; 56 | private _bounce: boolean = false; 57 | 58 | private _scrollEndWatch: Subscription; 59 | 60 | constructor( 61 | _content: Content, 62 | _el: ElementRef, 63 | _rnd: Renderer2, 64 | _domCtrl: DomController, 65 | _plt: Platform, 66 | private viewCtrl: ViewController 67 | ) { 68 | super(_content, _el, _rnd, _domCtrl, _plt); 69 | } 70 | 71 | ngAfterViewInit() { 72 | this._originalHeight = this.getNativeElement().offsetHeight; 73 | this.fadeAt = this.fadeAt || this._originalHeight * 0.75; 74 | this._onScroll(null, true); 75 | super.ngAfterViewInit(); 76 | if (this.compact) { 77 | let sub = this.viewCtrl.writeReady.subscribe(() => { 78 | sub.unsubscribe(); 79 | this._content.scrollTo(0, this._originalHeight / 2); 80 | }) 81 | } 82 | } 83 | 84 | listen() { 85 | super.listen(); 86 | if (this.bounce) { 87 | // this._rnd.listen(this._content.getNativeElement(), 'touchend', this._onScrollEnd.bind(this)); 88 | this._scrollEndWatch = Observable.fromEvent(this._content.getNativeElement(), 'touchend').subscribe(this._onScrollEnd.bind(this)); 89 | } 90 | } 91 | 92 | unlisten() { 93 | super.unlisten(); 94 | this._scrollEndWatch && this._scrollEndWatch.unsubscribe && this._scrollEndWatch.unsubscribe(); 95 | } 96 | 97 | protected _onScroll(ev: any, force: boolean = false) { 98 | 99 | this._domCtrl.read(() => { 100 | 101 | let translateAmt, scaleAmt, scrollTop; 102 | scrollTop = this._content.scrollTop; 103 | 104 | this._domCtrl.write(() => { 105 | 106 | if (scrollTop > this._originalHeight) return; 107 | 108 | if (scrollTop >= 0) { 109 | translateAmt = scrollTop / 2; 110 | scaleAmt = 1 - (-scrollTop / this._originalHeight * 0.15); 111 | } else { 112 | translateAmt = 0; 113 | scaleAmt = 1; 114 | } 115 | 116 | let transform = `translate3d(0, ${translateAmt}px, 0)`; 117 | 118 | if (this.scale) { 119 | transform += ` scale3d(${scaleAmt}, ${scaleAmt}, 1)`; 120 | } 121 | 122 | this._rnd.setStyle(this.getNativeElement(), this._plt.Css.transform, transform); 123 | 124 | if (this.fade && (this._originalHeight - scrollTop) <= this.fadeAt) { 125 | let opacity = (this._originalHeight - scrollTop) / this.fadeAt; 126 | this._rnd.setStyle(this.getNativeElement(), 'opacity', opacity); 127 | } 128 | 129 | }); 130 | 131 | }); 132 | 133 | } 134 | 135 | private _onScrollEnd(ev: any) { 136 | if (this._content.scrollTop < this._originalHeight / 2) { 137 | this._content.scrollTo(0, this._originalHeight / 2); 138 | } 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /src/parallax.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { ParallaxBackground } from './parallax-bg.directive'; 3 | import { ParallaxHeader } from './parallax-header.directive'; 4 | 5 | @NgModule({ 6 | declarations: [ 7 | ParallaxBackground, 8 | ParallaxHeader 9 | ], 10 | exports: [ 11 | ParallaxBackground, 12 | ParallaxHeader 13 | ] 14 | }) 15 | export class ParallaxModule {} 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "es2015", 4 | "target": "es5", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "inlineSources": true, 8 | "declaration": true, 9 | "noImplicitAny": false, 10 | "experimentalDecorators": true, 11 | "lib": [ 12 | "dom", 13 | "es2015" 14 | ], 15 | "outDir": "dist" 16 | }, 17 | "exclude": [ 18 | "node_modules", 19 | "dist" 20 | ], 21 | "angularCompilerOptions": { 22 | "genDir": "aot" 23 | } 24 | } --------------------------------------------------------------------------------