= {},
67 | defaultSettings: Settings
68 | ): Settings => {
69 | return {
70 | container: {
71 | ...defaultSettings.container,
72 | ...userSettings.container,
73 | },
74 | block: {
75 | ...defaultSettings.block,
76 | ...userSettings.block,
77 | },
78 | }
79 | }
80 |
81 | // Calculates the top offset from an element to the window's || document's top, Link: https://plainjs.com/javascript/styles/get-the-position-of-an-element-relative-to-the-document-24/
82 | const calculateOffsetTop = (el: HTMLElement) => {
83 | const rectTop = el.getBoundingClientRect().top
84 | const scrollTop = window.pageYOffset || document.documentElement.scrollTop
85 |
86 | return rectTop + scrollTop
87 | }
88 |
--------------------------------------------------------------------------------
/src/ts/initBlock.ts:
--------------------------------------------------------------------------------
1 | import { VIDEO_EXTENSIONS, ELEMENT_DATA_KEYS, MEDIA_TYPES } from './constants'
2 | import { Block, Container, Settings, Window } from './types'
3 |
4 | export const setBlockSpeed = (blockEl: Block['blockEl'], settings: Settings) => {
5 | const attrSpeed = blockEl.getAttribute(ELEMENT_DATA_KEYS.SPEED)
6 |
7 | // No data attribute defined
8 | if (!attrSpeed) {
9 | return settings.block.speed
10 | }
11 |
12 | const attrSpeedAsNumber = parseInt(attrSpeed, 10)
13 | if (isNaN(attrSpeedAsNumber)) {
14 | console.error('Invalid type for attribute speed for block: ' + blockEl)
15 | throw new Error('Invalid type for attribute speed for block')
16 | }
17 |
18 | return attrSpeedAsNumber === 0 ? settings.block.speed : attrSpeedAsNumber
19 | }
20 |
21 | export const setBlockMediaProps = (blockEl: Block['blockEl'], settings: Settings) => {
22 | let mediatype = blockEl.getAttribute(ELEMENT_DATA_KEYS.MEDIATYPE) as
23 | | keyof typeof MEDIA_TYPES
24 | | null
25 | const mediapath = blockEl.getAttribute(ELEMENT_DATA_KEYS.MEDIAPATH)
26 |
27 | if (mediatype === MEDIA_TYPES.none) {
28 | return {
29 | mediatype,
30 | mediapath,
31 | }
32 | }
33 |
34 | // No data attribute defined
35 | if (!mediatype) {
36 | mediatype = settings.block.mediatype
37 | }
38 |
39 | // Media type set to video
40 | if (mediapath && isVideo(mediatype, mediapath)) {
41 | mediatype = MEDIA_TYPES.video
42 | }
43 |
44 | // No data attribute defined
45 | if (!mediapath && mediatype !== MEDIA_TYPES.none) {
46 | console.error('Media path not defined for block: ' + blockEl)
47 | throw new Error('Media path not defined')
48 | }
49 |
50 | return {
51 | mediatype,
52 | mediapath,
53 | }
54 | }
55 |
56 | export const setBlockMute = (blockEl: Block['blockEl'], settings: Settings) => {
57 | const attrMute = blockEl.getAttribute(ELEMENT_DATA_KEYS.MUTE)
58 |
59 | if (attrMute !== undefined && attrMute !== null) {
60 | return attrMute === 'true'
61 | }
62 |
63 | return settings.block.mute
64 | }
65 |
66 | const setBlockImage = (block: Block) => {
67 | const { mediapath } = block
68 |
69 | block.blockEl.style.backgroundImage = "url('" + mediapath + "')"
70 |
71 | // Check if the background image wasn't set
72 | const backgroundImageFromDOM = window
73 | .getComputedStyle(block.blockEl)
74 | .getPropertyValue('background-image')
75 |
76 | return backgroundImageFromDOM !== 'none'
77 | }
78 |
79 | const videoElClicked = (videoEl: HTMLVideoElement, block: Block) => {
80 | const { pv } = (window as unknown) as Window
81 |
82 | if (pv.unmutedBlock && pv.unmutedBlock.videoEl !== videoEl) {
83 | if (pv.unmutedBlock.videoEl) {
84 | pv.unmutedBlock.videoEl.muted = true
85 | }
86 |
87 | if (pv.unmutedBlock.audioButton) {
88 | pv.unmutedBlock.audioButton.classList.add('mute')
89 | }
90 | }
91 |
92 | pv.unmutedBlock = block
93 | videoEl.muted = !videoEl.muted
94 | block.muted = videoEl.muted
95 |
96 | if (block.audioButton) {
97 | block.audioButton.classList.toggle('mute')
98 | }
99 | }
100 |
101 | const setBlockVideo = (block: Block) => {
102 | const { mediapath } = block
103 |
104 | const videoEl = document.createElement('video')
105 | videoEl.src = mediapath as string
106 | videoEl.autoplay = true
107 | videoEl.loop = true
108 | videoEl.defaultMuted = true
109 | videoEl.muted = true
110 | block.muted = true
111 | block.videoEl = videoEl
112 | block.blockEl.appendChild(videoEl)
113 |
114 | if (typeof window.orientation === 'undefined') {
115 | if (!block.mute) {
116 | videoEl.addEventListener('click', function() {
117 | videoElClicked(videoEl, block)
118 | })
119 | const audioButton = document.createElement('a')
120 | audioButton.href = '#'
121 | audioButton.className += 'audio-icon mute'
122 | audioButton.appendChild(document.createElement('span'))
123 | audioButton.addEventListener('click', function(e) {
124 | e.preventDefault()
125 | videoElClicked(videoEl, block)
126 | })
127 | block.audioButton = audioButton
128 | block.blockEl.insertAdjacentElement('afterend', audioButton)
129 | }
130 | }
131 |
132 | return true
133 | }
134 |
135 | export const setBlockVisual = (block: Block) => {
136 | const { mediatype } = block
137 |
138 | if (mediatype === MEDIA_TYPES.image) {
139 | setBlockImage(block)
140 | return
141 | }
142 | if (mediatype === MEDIA_TYPES.video) {
143 | setBlockVideo(block)
144 | return
145 | }
146 |
147 | console.error('Failed to set media for block:', block)
148 | throw new Error('Failed to set media')
149 | }
150 |
151 | export const setBlockAttributes = (container: Container, block: Block) => {
152 | const { pv } = (window as unknown) as Window
153 |
154 | updateWindowProps()
155 | // calculates the negative top property
156 | // negative scroll distance
157 | // plus container height / factor, because whenever we pass the element we'll always scroll the window faster then the animation (if factor < 1 it'll be increased to all is good)
158 | let marginTop = 0
159 | let scrollDist = 0
160 | let paddingBottom = 0
161 |
162 | // if the pv-block offset is less than the windowheight, then the scrolldist will have to be recalculated
163 | if (container.offset < pv.windowProps.windowHeight) {
164 | scrollDist = (container.height + container.offset) / Math.abs(block.speed)
165 |
166 | if (block.speed > 0) {
167 | marginTop = -Math.abs(container.offset)
168 | paddingBottom = container.height + container.offset
169 | } else {
170 | paddingBottom = scrollDist + container.height
171 | }
172 | } else {
173 | // the pv-block is below the initial windowheight
174 | scrollDist = (container.height + pv.windowProps.windowHeight) / Math.abs(block.speed)
175 | paddingBottom = scrollDist + container.height
176 |
177 | if (block.speed > 0) {
178 | marginTop = -scrollDist
179 | paddingBottom = container.height + pv.windowProps.windowHeight / Math.abs(block.speed)
180 | } else {
181 | paddingBottom = scrollDist + container.height
182 | }
183 | }
184 |
185 | if (Math.abs(marginTop) >= Math.abs(paddingBottom)) paddingBottom = Math.abs(marginTop) + 1
186 |
187 | block.blockEl.style.setProperty('padding-bottom', paddingBottom + 'px')
188 | block.blockEl.style.setProperty('margin-top', marginTop + 'px')
189 | }
190 |
191 | // Returns the extension of a media path
192 | const getExtension = (attrMediapath: string) => {
193 | const extension = attrMediapath
194 | .substr(attrMediapath.lastIndexOf('.') + 1, attrMediapath.length)
195 | .toLowerCase()
196 |
197 | if (extension.length === 0) {
198 | console.error('Invalid extension for media with media path: ' + attrMediapath)
199 | throw new Error('Invalid extension for media')
200 | }
201 |
202 | return extension
203 | }
204 |
205 | // returns `true` if media is a video
206 | const isVideo = (attrMediatype: keyof typeof MEDIA_TYPES, attrMediapath: string) =>
207 | attrMediatype === MEDIA_TYPES.video ||
208 | VIDEO_EXTENSIONS.indexOf(getExtension(attrMediapath)) !== -1
209 |
210 | const updateWindowProps = () => {
211 | const { pv } = (window as unknown) as Window
212 |
213 | pv.windowProps = {
214 | scrollTop: window.scrollY || document.documentElement.scrollTop,
215 | windowHeight: window.innerHeight,
216 | windowMidHeight: window.innerHeight / 2,
217 | }
218 | }
219 |
--------------------------------------------------------------------------------
/src/ts/initContainer.ts:
--------------------------------------------------------------------------------
1 | import { ELEMENT_DATA_KEYS } from './constants'
2 | import { Container, Settings } from './types'
3 |
4 | export const setContainerHeight = (containerEl: Container['containerEl'], settings: Settings) => {
5 | const attrHeight = containerEl.getAttribute(ELEMENT_DATA_KEYS.HEIGHT)
6 |
7 | // No data attribute
8 | if (!attrHeight) return settings.container.height
9 |
10 | // String only consists of integers, add px
11 | if (!isNaN(Number(attrHeight))) return attrHeight + 'px'
12 |
13 | // String has more than integers, assume suffix is either px or vh
14 | const suffix = attrHeight.substr(attrHeight.length - 2, attrHeight.length)
15 | if (suffix === 'px' || suffix === 'vh') return attrHeight
16 |
17 | throw new Error('Invalid height suffix, expected "px" or "vh" but got: ' + suffix)
18 | }
19 |
--------------------------------------------------------------------------------
/src/ts/parallax-vanilla.ts:
--------------------------------------------------------------------------------
1 | import '../less/parallax-vanilla.less'
2 | import { defaultSettings } from './constants'
3 | import { init } from './init'
4 | import { PV, Window } from './types'
5 | import { resize } from './resize'
6 | import { translate } from './translate'
7 |
8 | // eslint-disable-next-line @typescript-eslint/no-extra-semi
9 | ;(window => {
10 | const defineParallaxVanilla = () => {
11 | const pv: PV = {
12 | init,
13 | containerArr: [],
14 | mostReContainerInViewport: -1,
15 | prevScrollTop: -1,
16 | settings: defaultSettings,
17 | windowProps: {
18 | scrollTop: -1,
19 | windowHeight: -1,
20 | windowMidHeight: -1,
21 | },
22 | }
23 |
24 | window.pv = pv // exposes init function to user
25 |
26 | if (typeof window.orientation === 'undefined') {
27 | window.onresize = () => resize()
28 | }
29 |
30 | // Request animation frame, also binds function to window
31 | window.raf = (() => {
32 | return (
33 | window.requestAnimationFrame ||
34 | window.webkitRequestAnimationFrame ||
35 | window.mozRequestAnimationFrame ||
36 | function(callback) {
37 | window.setTimeout(callback, 1000 / 60) // 60 FPS
38 | }
39 | )
40 | })()
41 |
42 | // Main loop for updating variables and performing translates
43 | const mainLoop = () => {
44 | translate()
45 | window.raf(mainLoop)
46 | }
47 |
48 | // Initialize main loop
49 | window.raf(mainLoop)
50 |
51 | return pv
52 | }
53 |
54 | // Define pv
55 | if (typeof window.pv === 'undefined') {
56 | window.pv = defineParallaxVanilla()
57 | console.log('%c parallax-vanilla defined.', 'color: green')
58 | } else {
59 | console.log('%c parallax-vanilla already defined.', 'color: red')
60 | }
61 | })((window as unknown) as Window)
62 |
--------------------------------------------------------------------------------
/src/ts/resize.ts:
--------------------------------------------------------------------------------
1 | import { MEDIA_TYPES } from './constants'
2 | import { setBlockAttributes } from './initBlock'
3 | import { Block, Container, Window } from './types'
4 |
5 | export const resize = () => {
6 | const { pv } = (window as unknown) as Window
7 |
8 | pv.containerArr.forEach((container: Container) => {
9 | container.height = container.containerEl.clientHeight
10 |
11 | container.blocks.forEach((block: Block) => {
12 | if (block.mediatype !== MEDIA_TYPES.none) {
13 | setBlockAttributes(container, block)
14 | }
15 | })
16 | })
17 | }
18 |
--------------------------------------------------------------------------------
/src/ts/translate.ts:
--------------------------------------------------------------------------------
1 | import { Block, Container, Window } from './types'
2 |
3 | export const translate = () => {
4 | const { pv } = (window as unknown) as Window
5 |
6 | // Update selected attributes in windowProps on window raf event
7 | pv.windowProps.scrollTop = window.scrollY || document.documentElement.scrollTop
8 | if (pv.windowProps.scrollTop === pv.prevScrollTop) {
9 | // No scrolling has occured
10 | return
11 | } else {
12 | pv.prevScrollTop = pv.windowProps.scrollTop
13 | }
14 |
15 | // translate the parallax blocks, creating the parallax effect
16 | pv.containerArr.forEach((container: Container, index: number) => {
17 | let calc = 0
18 |
19 | // check if parallax block is in viewport
20 | if (isInViewport(container.offset, container.height)) {
21 | if (index > pv.mostReContainerInViewport) pv.mostReContainerInViewport = index
22 | // if any parallax is within the first windowheight, transform from 0 (pv.scrollTop)
23 | if (container.offset < pv.windowProps.windowHeight) {
24 | calc = pv.windowProps.scrollTop
25 |
26 | // if the parallax is further down on the page
27 | // calculate windowheight - parallax offset + scrollTop to start from 0 whereever it appears
28 | } else {
29 | calc = pv.windowProps.windowHeight - container.offset + pv.windowProps.scrollTop
30 | }
31 |
32 | container.blocks.forEach((block: Block) => {
33 | if (block.videoEl) {
34 | block.videoEl.play()
35 |
36 | if (pv.unmutedBlock === block && !block.muted) {
37 | block.videoEl.muted = block.muted
38 |
39 | if (pv.unmutedBlock.audioButton) {
40 | block.muted
41 | ? pv.unmutedBlock.audioButton.classList.add('mute')
42 | : pv.unmutedBlock.audioButton.classList.remove('mute')
43 | }
44 | }
45 | }
46 |
47 | transform(block.blockEl, 'translate3d(0,' + Math.round(calc / block.speed) + 'px, 0)')
48 | })
49 | } else {
50 | // check if container has at least one video block
51 | if (container.hasVideoBlock) {
52 | // pause blocks with playing videos
53 | container.blocks.forEach((block: Block) => {
54 | if (block.videoEl) {
55 | block.videoEl.pause()
56 | if (pv.unmutedBlock === block) {
57 | block.videoEl.muted = true
58 | }
59 | }
60 | })
61 | }
62 | const nextContainer = pv.containerArr[index + 1]
63 | // check if next container is in view - else break
64 | if (
65 | nextContainer &&
66 | !isInViewport(nextContainer.offset, nextContainer.height) &&
67 | pv.mostReContainerInViewport < index &&
68 | !nextContainerIsSmaller(container, nextContainer)
69 | ) {
70 | return
71 | } else {
72 | if (nextContainer && isInViewport(nextContainer.offset, nextContainer.height)) {
73 | pv.mostReContainerInViewport = index + 1
74 | }
75 | }
76 | }
77 | })
78 | }
79 |
80 | // Transform prefixes for CSS
81 | const transform = (element: HTMLElement, style: string) => {
82 | element.style.webkitTransform = style
83 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
84 | // @ts-ignore
85 | element.style.MozTransform = style
86 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
87 | // @ts-ignore
88 | element.style.msTransform = style
89 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
90 | // @ts-ignore
91 | element.style.OTransform = style
92 | element.style.transform = style
93 | }
94 |
95 | // Check if the container is in view
96 | const isInViewport = (offset: number, height: number) => {
97 | const { pv } = (window as unknown) as Window
98 |
99 | return (
100 | pv.windowProps.scrollTop + pv.windowProps.windowHeight - offset > 0 &&
101 | pv.windowProps.scrollTop < offset + height
102 | )
103 | }
104 |
105 | const nextContainerIsSmaller = (container: Container, nextContainer: Container) =>
106 | container.offset + container.height > nextContainer.offset + nextContainer.height
107 |
--------------------------------------------------------------------------------
/src/ts/types.ts:
--------------------------------------------------------------------------------
1 | import { MEDIA_TYPES } from './constants'
2 | import { init } from './init'
3 |
4 | export interface Settings {
5 | container: {
6 | class: string
7 | height: string
8 | }
9 | block: {
10 | class: string
11 | speed: number
12 | mediatype: keyof typeof MEDIA_TYPES
13 | mediapath: null
14 | mute: boolean
15 | }
16 | }
17 |
18 | export interface Block {
19 | blockEl: HTMLElement
20 | speed: number
21 | mediatype: string | null
22 | mediapath: string | null
23 | mute: boolean
24 | muted: boolean
25 | videoEl?: HTMLVideoElement
26 | audioButton?: HTMLAnchorElement
27 | }
28 |
29 | export interface Container {
30 | containerEl: HTMLElement
31 | offset: number
32 | height: number
33 | blocks: Block[]
34 | hasVideoBlock?: boolean
35 | }
36 |
37 | export interface PV {
38 | containerArr: Container[]
39 | settings: Settings
40 | prevScrollTop: number
41 | mostReContainerInViewport: number
42 | unmutedBlock?: Block
43 | windowProps: {
44 | scrollTop: number
45 | windowHeight: number
46 | windowMidHeight: number
47 | }
48 | init: typeof init
49 | }
50 |
51 | export interface Window {
52 | raf: typeof window.requestAnimationFrame
53 | pv: PV
54 | orientation: typeof window.orientation
55 | requestAnimationFrame: typeof window.requestAnimationFrame
56 | webkitRequestAnimationFrame: typeof window.requestAnimationFrame
57 | mozRequestAnimationFrame: typeof window.requestAnimationFrame
58 | setTimeout: typeof window.setTimeout
59 | onresize: () => void
60 | }
61 |
--------------------------------------------------------------------------------
/test/css/bootstrap.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap v3.3.5 (http://getbootstrap.com)
3 | * Copyright 2011-2016 Twitter, Inc.
4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
5 | */
6 |
7 | /*!
8 | * Generated using the Bootstrap Customizer (http://getbootstrap.com/customize/?id=d5fc771961d5957ea617f1a16b9ba8a0)
9 | * Config saved to config.json and https://gist.github.com/d5fc771961d5957ea617f1a16b9ba8a0
10 | *//*!
11 | * Bootstrap v3.3.6 (http://getbootstrap.com)
12 | * Copyright 2011-2015 Twitter, Inc.
13 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
14 | *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:0.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace, monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:hover,a:focus{color:#23527c;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role="button"]{cursor:pointer}.container{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}.row{margin-left:-15px;margin-right:-15px}.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12{position:relative;min-height:1px;padding-left:15px;padding-right:15px}.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}.clearfix:before,.clearfix:after,.container:before,.container:after,.container-fluid:before,.container-fluid:after,.row:before,.row:after{content:" ";display:table}.clearfix:after,.container:after,.container-fluid:after,.row:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right !important}.pull-left{float:left !important}.hide{display:none !important}.show{display:block !important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none !important}.affix{position:fixed}
--------------------------------------------------------------------------------
/test/css/demo.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Basic body styling
3 | */
4 | body {
5 | background-color: #f4f4f4;
6 | }
7 |
8 | /**
9 | * Remove padding from all bootstrap columns
10 | */
11 | [class*='col'],
12 | [class*='container'] {
13 | padding: 0;
14 | }
15 |
16 | /**
17 | * Allow for absolute comments
18 | */
19 | .para-container {
20 | position: relative;
21 | }
22 |
23 | /**
24 | * Comments
25 | */
26 | pre {
27 | margin: 0;
28 | display: inline-block;
29 | position: absolute;
30 | top: 0;
31 | left: 0;
32 | border-top-left-radius: 0;
33 | border-top-right-radius: 0;
34 | border-bottom-left-radius: 0;
35 | font-size: 12px;
36 | z-index: 1;
37 | padding: 5px;
38 | background-color: rgba(245, 245, 245, 0.5);
39 | white-space: pre-wrap;
40 | }
41 |
42 | /**
43 | * Various filler sized
44 | */
45 | .filler {
46 | position: relative;
47 | }
48 |
49 | .filler > div {
50 | position: absolute;
51 | top: 50%;
52 | left: 50%;
53 | -webkit-transform: translate(-50%, -50%);
54 | -moz-transform: translate(-50%, -50%);
55 | -ms-transform: translate(-50%, -50%);
56 | -o-transform: translate(-50%, -50%);
57 | width: 90%;
58 | text-align: center;
59 | }
60 |
61 | .pv-container {
62 | position: relative; /* In order for tooltips to position properly */
63 | }
64 |
65 | .text-wrapper {
66 | position: absolute;
67 | top: 50%;
68 | left: 50%;
69 | transform: translate3d(-50%, -50%, 0);
70 | width: 100%;
71 | text-align: center;
72 | color: white;
73 | z-index: 9;
74 | text-shadow: 1px 1px 1px black;
75 | }
76 |
--------------------------------------------------------------------------------
/test/media/Sunrise - 7127.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikengervall/parallax-vanilla/401150ac35fb6166a9c78cbe90f7c864687f8d75/test/media/Sunrise - 7127.mp4
--------------------------------------------------------------------------------
/test/media/animal.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikengervall/parallax-vanilla/401150ac35fb6166a9c78cbe90f7c864687f8d75/test/media/animal.mp4
--------------------------------------------------------------------------------
/test/media/butterfly.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikengervall/parallax-vanilla/401150ac35fb6166a9c78cbe90f7c864687f8d75/test/media/butterfly.jpg
--------------------------------------------------------------------------------
/test/media/dolphin.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikengervall/parallax-vanilla/401150ac35fb6166a9c78cbe90f7c864687f8d75/test/media/dolphin.mp4
--------------------------------------------------------------------------------
/test/media/earth.mov:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikengervall/parallax-vanilla/401150ac35fb6166a9c78cbe90f7c864687f8d75/test/media/earth.mov
--------------------------------------------------------------------------------
/test/media/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikengervall/parallax-vanilla/401150ac35fb6166a9c78cbe90f7c864687f8d75/test/media/favicon.png
--------------------------------------------------------------------------------
/test/media/forest.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikengervall/parallax-vanilla/401150ac35fb6166a9c78cbe90f7c864687f8d75/test/media/forest.mp4
--------------------------------------------------------------------------------
/test/media/ladybug.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikengervall/parallax-vanilla/401150ac35fb6166a9c78cbe90f7c864687f8d75/test/media/ladybug.jpg
--------------------------------------------------------------------------------
/test/media/leaves.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikengervall/parallax-vanilla/401150ac35fb6166a9c78cbe90f7c864687f8d75/test/media/leaves.jpg
--------------------------------------------------------------------------------
/test/media/owl.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikengervall/parallax-vanilla/401150ac35fb6166a9c78cbe90f7c864687f8d75/test/media/owl.jpg
--------------------------------------------------------------------------------
/test/media/peach.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikengervall/parallax-vanilla/401150ac35fb6166a9c78cbe90f7c864687f8d75/test/media/peach.jpg
--------------------------------------------------------------------------------
/test/media/plums.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikengervall/parallax-vanilla/401150ac35fb6166a9c78cbe90f7c864687f8d75/test/media/plums.jpg
--------------------------------------------------------------------------------
/test/media/raindrops.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikengervall/parallax-vanilla/401150ac35fb6166a9c78cbe90f7c864687f8d75/test/media/raindrops.mp4
--------------------------------------------------------------------------------
/test/media/reflection.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikengervall/parallax-vanilla/401150ac35fb6166a9c78cbe90f7c864687f8d75/test/media/reflection.jpg
--------------------------------------------------------------------------------
/test/media/sunset.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikengervall/parallax-vanilla/401150ac35fb6166a9c78cbe90f7c864687f8d75/test/media/sunset.jpg
--------------------------------------------------------------------------------
/test/media/tomatoes.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikengervall/parallax-vanilla/401150ac35fb6166a9c78cbe90f7c864687f8d75/test/media/tomatoes.jpg
--------------------------------------------------------------------------------
/test/media/water.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikengervall/parallax-vanilla/401150ac35fb6166a9c78cbe90f7c864687f8d75/test/media/water.mp4
--------------------------------------------------------------------------------
/test/media/zebras.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikengervall/parallax-vanilla/401150ac35fb6166a9c78cbe90f7c864687f8d75/test/media/zebras.jpg
--------------------------------------------------------------------------------
/test/parallax-vanilla.spec.js:
--------------------------------------------------------------------------------
1 | const pv = require('../src/js/parallax-vanilla')
2 |
3 | const mockInit = jest.fn()
4 | jest.mock('../src/js/init', () => mockInit)
5 |
6 | describe('Test cases for parallax-vanilla.js', () => {
7 | it('should initialize variables', () => {
8 | expect(mockInit).not.toHaveBeenCalled() // should not fire without user input
9 | expect(window.raf).toEqual(expect.any(Function))
10 | })
11 | })
12 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "./dist/",
4 | "module": "es6",
5 | "target": "es2017",
6 | "allowJs": true,
7 | "baseUrl": "./src",
8 | // "paths": { "src": ["./*"] },
9 | "esModuleInterop": true,
10 | "lib": ["es2015", "es2016", "es2017", "dom"],
11 | "moduleResolution": "node",
12 | "noImplicitAny": true,
13 | "sourceMap": true,
14 | "strict": true,
15 | "noUnusedParameters": true,
16 | "noUnusedLocals": true,
17 | "resolveJsonModule": true,
18 | "declaration": true
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin')
3 | const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')
4 | const TerserPlugin = require('terser-webpack-plugin')
5 |
6 | module.exports = {
7 | mode: 'development',
8 | entry: './src/ts/parallax-vanilla.ts',
9 | module: {
10 | rules: [
11 | {
12 | test: /\.tsx?$/,
13 | use: 'ts-loader',
14 | exclude: /node_modules/,
15 | },
16 | {
17 | test: /\.less$/,
18 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader'],
19 | },
20 | ],
21 | },
22 | resolve: {
23 | extensions: ['.ts'],
24 | },
25 | output: {
26 | filename: 'parallax-vanilla.js',
27 | path: path.resolve(__dirname, 'dist/'),
28 | },
29 | plugins: [
30 | new MiniCssExtractPlugin({
31 | filename: 'parallax-vanilla.css',
32 | chunkFilename: 'parallax-vanilla.css',
33 | }),
34 | new OptimizeCssAssetsPlugin({
35 | assetNameRegExp: /\.css$/g,
36 | cssProcessorPluginOptions: {
37 | preset: ['default', { discardComments: { removeAll: true } }],
38 | },
39 | canPrint: true,
40 | }),
41 | ],
42 | devtool: 'source-map',
43 | optimization: {
44 | minimize: true,
45 | minimizer: [new TerserPlugin()],
46 | },
47 | }
48 |
--------------------------------------------------------------------------------