├── .babelrc.json ├── .browserslistrc ├── .eslintignore ├── .eslintrc ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ └── node.js.yml ├── .gitignore ├── .husky └── pre-commit ├── .npmrc ├── .prettierignore ├── .storybook ├── main.js ├── preview.js └── style.css ├── CONTRIBUTING.md ├── DEVELOPERS.md ├── LICENSE ├── README.md ├── dist ├── esm │ ├── constants.d.ts │ ├── helpers.d.ts │ ├── index.d.ts │ ├── pushInBase.d.ts │ ├── pushInComposition.d.ts │ ├── pushInLayer.d.ts │ ├── pushInScene.d.ts │ ├── pushInStyles.d.ts │ ├── pushInTarget.d.ts │ ├── pushin.d.ts │ ├── pushin.js │ ├── pushin.js.map │ └── types.d.ts ├── pushin.css ├── pushin.min.css └── umd │ ├── pushin.js │ ├── pushin.js.map │ └── pushin.min.js ├── docs ├── images │ ├── pushin-logo-banner.svg │ └── pushin-logo.svg └── index.html ├── jest.config.ts ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── constants.ts ├── helpers.ts ├── index.ts ├── pushInBase.ts ├── pushInComposition.ts ├── pushInLayer.ts ├── pushInScene.ts ├── pushInStyles.ts ├── pushInTarget.ts ├── pushin.css ├── pushin.ts └── types.ts ├── stories ├── A11y.stories.tsx ├── A11y.tsx ├── AutoStart.stories.tsx ├── AutoStart.tsx ├── Introduction.stories.mdx ├── Modes.stories.tsx ├── Modes.tsx ├── Target.stories.tsx ├── Target.tsx ├── Text.stories.tsx ├── Text.tsx └── pushin.css ├── test ├── __mocks__ │ ├── layers.ts │ └── scene.ts ├── pushIn │ ├── destroy.spec.ts │ ├── getScrollY.spec.ts │ └── setScrollLength.spec.ts ├── pushInComposition │ └── setRatio.spec.ts ├── pushInLayer │ ├── getElementScaleX.spec.ts │ ├── getInpoints.spec.ts │ ├── getOutpoints.spec.ts │ ├── getOverlap.spec.ts │ ├── getScaleValue.spec.ts │ ├── getSpeed.spec.ts │ ├── isActive.spec.ts │ ├── setLayerStyle.spec.ts │ ├── setScale.spec.ts │ └── setZIndex.spec.ts ├── pushInScene │ ├── getBreakpointIndex.spec.ts │ ├── getLayers.spec.ts │ ├── resize.spec.ts │ └── setBreakpoints.spec.ts ├── pushInTarget │ ├── setScrollTarget.spec.ts │ └── setTargetHeight.spec.ts └── setup.ts ├── tsconfig.json └── tsconfig.spec.json /.babelrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceType": "unambiguous", 3 | "presets": [ 4 | [ 5 | "@babel/preset-env", 6 | { 7 | "targets": { 8 | "chrome": 100, 9 | "safari": 15, 10 | "firefox": 91 11 | } 12 | } 13 | ], 14 | "@babel/preset-typescript", 15 | "@babel/preset-react" 16 | ], 17 | "plugins": [] 18 | } -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | # Supported browsers 2 | 3 | defaults 4 | last 2 versions 5 | not dead 6 | > 0.2% 7 | ie > 10 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | test -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "extends": [ 5 | "airbnb-base", 6 | "prettier", 7 | "plugin:@typescript-eslint/eslint-recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ], 10 | "plugins": [ 11 | "prettier", 12 | "@typescript-eslint" 13 | ], 14 | "env": { 15 | "node": true, 16 | "browser": true 17 | }, 18 | "rules": { 19 | "no-plusplus": "off", 20 | "no-param-reassign": "off", 21 | "class-methods-use-this": "off", 22 | "prettier/prettier": [ 23 | "error" 24 | ], 25 | "import/prefer-default-export": "off", 26 | "import/extensions": "off", 27 | "import/no-unresolved": "off", 28 | "no-restricted-syntax": "off", 29 | "lines-between-class-members": "off", 30 | "@typescript-eslint/no-non-null-assertion": "off", 31 | "import/no-extraneous-dependencies": [ 32 | "error", 33 | { 34 | "devDependencies": [ 35 | "stories/**/*", 36 | "**/*.spec.js", 37 | "rollup.config.js" 38 | ] 39 | } 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | target-branch: "staging" 13 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: Unit tests 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | test: 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [16.x, 17.x, 18.x] 13 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - uses: actions/cache@v3 19 | id: npm-cache 20 | with: 21 | path: node_modules 22 | key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }} 23 | restore-keys: ${{ runner.os }}-npm 24 | 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v3 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | 30 | - name: Install dependencies 31 | if: steps.npm-cache.outputs.cache-hit != 'true' 32 | run: npm ci 33 | 34 | - run: npm run test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | *.backup 4 | 5 | #testing 6 | lt.log 7 | .nyc_output 8 | coverage 9 | .eslintcache 10 | .cache 11 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint && npm run lint-staged 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | package.json 4 | .vscode 5 | .idea 6 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: ['../**/*.stories.mdx', '../**/*.stories.@(js|jsx|ts|tsx)'], 3 | 4 | addons: [ 5 | '@storybook/addon-links', 6 | '@storybook/addon-essentials', 7 | '@storybook/addon-interactions', 8 | '@storybook/addon-mdx-gfm', 9 | ], 10 | 11 | framework: { 12 | name: '@storybook/react-webpack5', 13 | options: {}, 14 | }, 15 | 16 | typescript: { 17 | reactDocgen: 'react-docgen-typescript-plugin', 18 | }, 19 | 20 | docs: { 21 | autodocs: false, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import './style.css'; 2 | 3 | export const parameters = { 4 | actions: { argTypesRegex: '^on[A-Z].*' }, 5 | controls: { 6 | matchers: { 7 | color: /(background|color)$/i, 8 | date: /Date$/, 9 | }, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /.storybook/style.css: -------------------------------------------------------------------------------- 1 | .sb-show-main.sb-main-padded { 2 | padding-top: 0; 3 | padding-bottom: 0; 4 | } 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute to PushIn.js 2 | 3 | ## Did you find a bug? 4 | 5 | Ensure the bug was not already reported by searching on GitHub under Issues. 6 | 7 | If you're unable to find an open issue addressing the problem, open a new one. Be sure to include a title and clear description, as much relevant information as possible, and a code sample or an executable test case demonstrating the expected behavior that is not occurring. 8 | 9 | Please use the Bug Report issue template to help create a detailed report of the issue. 10 | 11 | ## Did you write a patch that fixes a bug? 12 | 13 | Open a new GitHub pull request with the patch. 14 | 15 | Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable. 16 | 17 | ## Do you intend to add a new feature or change an existing one? 18 | 19 | If there is no open issue for this change, please open a new one using the Feature Request issue template in order to thoroughly document the goal of this feature change. If you have already made this change, please open a pull request and include a link to the corresponding issue for further explanation of what has been done. 20 | 21 | ## Development setup 22 | 23 | For more information on how to set up your development environment, please see [DEVELOPERS.md](DEVELOPERS.md). 24 | 25 | ## More questions? 26 | 27 | Ask any questions about how to use or contribute to PushIn.js in the Discussions section of the GitHub repository. 28 | 29 | Thanks! ❤️ ❤️ ❤️ 30 | 31 | PushIn.js Team 32 | -------------------------------------------------------------------------------- /DEVELOPERS.md: -------------------------------------------------------------------------------- 1 | # PushIn.js Development 2 | 3 | There are just a few requirements to set up this project for local development: 4 | 5 | 1. install `node` v16 or higher 6 | 2. install `npm` 7 | 3. Setup your IDE with ESLint support 8 | 9 | You can get set up with the following steps: 10 | 11 | 1. Clone this repo 12 | 2. Run `npm ci` to install all dependencies 13 | 3. Run `npm run storybook` to start up the development environment 14 | 15 | The `npm run storybook` command will compile all code and begin running a node server at [localhost:6006](). The Storybook docs can be used to test various use cases for this library during development. The page will automatically refresh whenever you make a change to the source code. 16 | 17 | Another useful way to develop this plugin is by writing tests as you make changes (TDD style). Unit tests can be run using Jest. During development, run `npm run test:watch` to continuously test code on each save. 18 | 19 | NOTE: We enforce our code styles with ESLint and Prettier before committing. This means that you will need to fix any errors before committing your code. 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Nathan Blair 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # pushIn.js 4 | 5 | [![made-with-javascript](https://img.shields.io/badge/Made%20with-TypeScript-1f425f.svg)](https://www.typescriptlang.org/) 6 | [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://github.com/nateplusplus/pushin/graphs/commit-activity) 7 | ![Maintainer](https://img.shields.io/badge/maintainer-nateplusplus-blue) 8 | [![GitHub license](https://img.shields.io/github/license/nateplusplus/pushin.svg)](https://github.com/nateplusplus/pushin/blob/main/LICENSE) 9 | [![Node.js CI](https://github.com/nateplusplus/pushin/actions/workflows/node.js.yml/badge.svg)](https://github.com/nateplusplus/pushin/actions/workflows/node.js.yml) 10 | 11 | PushIn.js is a lightweight parallax effect, built with JavaScript, that simulates an interactive dolly-in or push-in animation on a webpage. 12 | 13 | Check out the [live demo](http://pushinjs.com/) for a working example. 14 | 15 | ## Compatibility 16 | 17 | PushIn.js supports all browsers that are [ES5-compliant](http://kangax.github.io/compat-table/es5/). 18 | 19 | ## Installation 20 | 21 | PushIn is available via NPM or a CDN. Follow the detailed instructions in the documentation site: https://pushinjs.com/installation. 22 | 23 | If you're using npm, you can install the package by running: 24 | 25 | ```shell 26 | npm install --save pushin 27 | ``` 28 | 29 | Alternatively, you can use a CDN: 30 | 31 | ``` 32 | 33 | ``` 34 | 35 | ## Configuration 36 | 37 | There are several plugin configurations you can use to customize for your unique project. Refer to [https://pushinjs.com/api](https://pushinjs.com/api) for a detailed breakdown of all available configurations. 38 | 39 | ## Contributing 40 | 41 | Contributors are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) to learn more about the best ways to contribute to this project. 42 | 43 | ## Development Setup 44 | 45 | See [DEVELOPERS.md](DEVELOPERS.md) for details on how to set up your development environment to contribute to this project. 46 | -------------------------------------------------------------------------------- /dist/esm/constants.d.ts: -------------------------------------------------------------------------------- 1 | export declare const DEFAULT_SPEED = 8; 2 | export declare const PUSH_IN_BREAKPOINTS_DATA_ATTRIBUTE = "pushinBreakpoints"; 3 | export declare const PUSH_IN_SPEED_DATA_ATTRIBUTE = "pushinSpeed"; 4 | export declare const PUSH_IN_TO_DATA_ATTRIBUTE = "pushinTo"; 5 | export declare const PUSH_IN_FROM_DATA_ATTRIBUTE = "pushinFrom"; 6 | export declare const PUSH_IN_DEFAULT_BREAKPOINTS: number[]; 7 | export declare const PUSH_IN_LAYER_INDEX_ATTRIBUTE = "data-pushin-layer-index"; 8 | export declare const PUSH_IN_DEFAULT_ASPECT_RATIO: number[]; 9 | export declare const PUSH_IN_DEFAULT_TRANSITION_LENGTH = 200; 10 | export declare const PUSH_IN_DEFAULT_LAYER_DEPTH = 1000; 11 | -------------------------------------------------------------------------------- /dist/esm/helpers.d.ts: -------------------------------------------------------------------------------- 1 | import { PushInOptions } from './types'; 2 | declare global { 3 | interface Window { 4 | pushInStart(options?: PushInOptions | string): void; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /dist/esm/index.d.ts: -------------------------------------------------------------------------------- 1 | import './helpers'; 2 | export { PushIn } from './pushin'; 3 | -------------------------------------------------------------------------------- /dist/esm/pushInBase.d.ts: -------------------------------------------------------------------------------- 1 | export default abstract class PushInBase { 2 | container?: HTMLElement | null; 3 | settings: { 4 | [key: string]: any; 5 | }; 6 | /** 7 | * Get the value for an option from either HTML markup or the JavaScript API. 8 | * Return a string or array of strings. 9 | */ 10 | getStringOption(name: string, container?: HTMLElement | null | undefined): string | string[]; 11 | /** 12 | * Get the value for an option from either HTML markup or the JavaScript API. 13 | * Returns a number or array of numbers. 14 | * If nothing found, returns null. 15 | */ 16 | getNumberOption(name: string, container?: HTMLElement | null | undefined): number | number[] | null; 17 | /** 18 | * Get the value for an option from either HTML markup or the JavaScript API. 19 | * Returns a boolean or array of booleans. 20 | * If nothing found, returns null. 21 | */ 22 | getBoolOption(name: string, container?: HTMLElement | null | undefined): boolean | boolean[] | null; 23 | getAttributeName(name: string): string; 24 | } 25 | -------------------------------------------------------------------------------- /dist/esm/pushInComposition.d.ts: -------------------------------------------------------------------------------- 1 | import { PushInScene } from './pushInScene'; 2 | import { CompositionOptions } from './types'; 3 | import PushInBase from './pushInBase'; 4 | export declare class PushInComposition extends PushInBase { 5 | scene: PushInScene; 6 | options: CompositionOptions; 7 | constructor(scene: PushInScene, options: CompositionOptions); 8 | start(): void; 9 | setContainer(): void; 10 | /** 11 | * Set the aspect ratio based setting. 12 | */ 13 | private setRatio; 14 | } 15 | -------------------------------------------------------------------------------- /dist/esm/pushInLayer.d.ts: -------------------------------------------------------------------------------- 1 | import { PushInScene } from './pushInScene'; 2 | import PushInBase from './pushInBase'; 3 | import { LayerOptions, LayerSettings, LayerParams } from './types'; 4 | export declare class PushInLayer extends PushInBase { 5 | container: HTMLElement; 6 | private index; 7 | scene: PushInScene; 8 | params: LayerParams; 9 | private originalScale; 10 | private ref; 11 | settings: LayerSettings; 12 | isFirst: boolean; 13 | isLast: boolean; 14 | constructor(container: HTMLElement, index: number, scene: PushInScene, options: LayerOptions); 15 | /** 16 | * Set Accessibility features. 17 | * Ensures layers are tabbable and their role is understood by screenreaders. 18 | */ 19 | private setA11y; 20 | /** 21 | * Get the transitions setting, either from the API or HTML attributes. 22 | * 23 | * @return {boolean} 24 | */ 25 | private getTransitions; 26 | /** 27 | * Get the amount of overlap between previous and current layer. 28 | * 29 | * @return {number} 30 | */ 31 | private getOverlap; 32 | /** 33 | * Get the transitionStart setting, either from the API or HTML attributes. 34 | * 35 | * @returns number 36 | */ 37 | private getTransitionStart; 38 | /** 39 | * Get the transitionEnd setting, either from the API or HTML attributes. 40 | * 41 | * @returns number 42 | */ 43 | private getTransitionEnd; 44 | /** 45 | * Get all inpoints for the layer. 46 | */ 47 | private getInpoints; 48 | /** 49 | * Get all outpoints for the layer. 50 | */ 51 | private getOutpoints; 52 | /** 53 | * Get the push-in speed for the layer. 54 | */ 55 | private getSpeed; 56 | /** 57 | * Set the z-index of each layer so they overlap correctly. 58 | */ 59 | setZIndex(total: number): void; 60 | /** 61 | * Set all the layer parameters. 62 | * 63 | * This is used during initalization and 64 | * if the window is resized. 65 | */ 66 | setLayerParams(): void; 67 | private getDepth; 68 | /** 69 | * Get the initial scale of the element at time of DOM load. 70 | */ 71 | private getElementScaleX; 72 | /** 73 | * Whether or not a layer should currently be animated. 74 | */ 75 | private isActive; 76 | /** 77 | * Get the current inpoint for a layer, 78 | * depending on window breakpoint. 79 | */ 80 | private getInpoint; 81 | /** 82 | * Get the current outpoint for a layer, 83 | * depending on window breakpoint. 84 | */ 85 | private getOutpoint; 86 | /** 87 | * Get the scaleX value for the layer. 88 | */ 89 | private getScaleValue; 90 | /** 91 | * Set element scale. 92 | */ 93 | private setScale; 94 | /** 95 | * Set CSS styles to control the effect on each layer. 96 | * 97 | * This will control the scale and opacity of the layer 98 | * as the user scrolls. 99 | */ 100 | setLayerStyle(): void; 101 | /** 102 | * Check if the layer should be visible. 103 | * 104 | * @returns boolean 105 | */ 106 | isVisible(): boolean; 107 | /** 108 | * Set a css class depending on current opacity. 109 | */ 110 | setLayerVisibility(): void; 111 | /** 112 | * Set tabInpoints for this layer. 113 | */ 114 | getTabInpoints(inpoints: number[]): number[]; 115 | /** 116 | * Get the current tabInpoint for a layer, 117 | * depending on window breakpoint. 118 | */ 119 | private getTabInpoint; 120 | } 121 | -------------------------------------------------------------------------------- /dist/esm/pushInScene.d.ts: -------------------------------------------------------------------------------- 1 | import { PushInComposition } from './pushInComposition'; 2 | import { PushInLayer } from './pushInLayer'; 3 | import { PushIn } from './pushin'; 4 | import PushInBase from './pushInBase'; 5 | import { SceneSettings } from './types'; 6 | export declare class PushInScene extends PushInBase { 7 | pushin: PushIn; 8 | layers: PushInLayer[]; 9 | layerDepth: number; 10 | settings: SceneSettings; 11 | composition?: PushInComposition; 12 | layerCount: number; 13 | constructor(pushin: PushIn); 14 | start(): void; 15 | /** 16 | * If there is not a pushin-scene element, create one. 17 | */ 18 | setContainer(): void; 19 | /** 20 | * Get the AutoStart option if provided. 21 | * 22 | * Choices: 23 | * - scroll (default) Start effect on scroll. 24 | * - screen-bottom Start effect when target element top at viewport bottom. 25 | * - screen-top Start effect when target element top at viewport top. 26 | */ 27 | setAutoStart(): void; 28 | setLayerDepth(): void; 29 | /** 30 | * Setup composition for the scene. 31 | */ 32 | setComposition(): void; 33 | /** 34 | * Set scene class names. 35 | */ 36 | private setSceneClasses; 37 | /** 38 | * Resize the PushIn container if using a target container. 39 | */ 40 | resize(): void; 41 | /** 42 | * Set breakpoints for responsive design settings. 43 | */ 44 | private setBreakpoints; 45 | /** 46 | * Find all layers on the page and store them with their parameters 47 | */ 48 | private getLayers; 49 | /** 50 | * Get the array index of the current window breakpoint. 51 | */ 52 | getBreakpointIndex(breakpoints: number[]): number; 53 | /** 54 | * Get the screen-top value of the container. 55 | * 56 | * If using a target, get the top of the 57 | * container relative to the target's top. 58 | * 59 | * @returns {number} 60 | */ 61 | getTop(): number; 62 | /** 63 | * Get the scene inpoints provided by the JavaScript API 64 | * and/or the HTML data-attributes. 65 | * 66 | * @returns {number[]} 67 | */ 68 | getInpoints(): number[]; 69 | /** 70 | * Get the mode setting. 71 | * 72 | * @returns string 73 | */ 74 | getMode(): string; 75 | /** 76 | * Update outpoints to match container height 77 | * if using continuous mode and outpoint not specified. 78 | */ 79 | updateOutpoints(): void; 80 | } 81 | -------------------------------------------------------------------------------- /dist/esm/pushInStyles.d.ts: -------------------------------------------------------------------------------- 1 | declare const pushInStyles = ".pushin {position: relative;}.pushin-scene {display: flex;align-items: center;position: fixed;left: 0;top: 0;width: 100%;height: 100vh;}.pushin-scene--with-target {top: 0;left: auto;height: auto;width: auto;pointer-events: none;overflow: hidden;position: sticky;}.pushin-scene--scroll-target-window {height: 100vh;}.pushin-composition {flex: 0 0 100%;padding-top: 201%;position: relative;}.pushin-layer {display: flex;align-items: center;flex-direction: column;justify-content: center;opacity: 0;pointer-events: none;position: absolute;top: 0;right: 0;bottom: 0;left: 0;}.pushin-layer--visible * {pointer-events: auto;}.pushin-debug {background-color: white;border: 0;border-bottom: 1px;box-shadow: -2px 8px 19px 2px rgba(0, 0, 0, 0.26);padding: 1em;position: fixed;top: 0;width: 100%;-webkit-box-shadow: -2px 8px 19px 2px rgba(0, 0, 0, 0.26);z-index: 10;}@media (min-width: 768px) {.pushin-debug {border: 1px solid black;border-radius: 15px 0 0 15px;border-right: 0;right: 0;top: 50px;width: 250px;}}.pushin-debug__title {font-weight: bold;}"; 2 | export default pushInStyles; 3 | -------------------------------------------------------------------------------- /dist/esm/pushInTarget.d.ts: -------------------------------------------------------------------------------- 1 | import PushInBase from './pushInBase'; 2 | import { PushIn } from './pushin'; 3 | import { TargetSettings } from './types'; 4 | export declare class PushInTarget extends PushInBase { 5 | pushin: PushIn; 6 | settings: TargetSettings; 7 | container: HTMLElement | null; 8 | scrollTarget: HTMLElement | string; 9 | height: number; 10 | constructor(pushin: PushIn, settings: TargetSettings); 11 | start(): void; 12 | /** 13 | * Set the target parameter and make sure 14 | * pushin is always a child of that target. 15 | * 16 | * @param options 17 | */ 18 | setTargetElement(): void; 19 | /** 20 | * Get scrollTarget option from data attribute 21 | * or JavaScript API. 22 | */ 23 | setScrollTarget(): void; 24 | /** 25 | * Set the target height on initialization. 26 | * 27 | * This will be used to calculate scroll length. 28 | * 29 | * @see setScrollLength 30 | */ 31 | setTargetHeight(): void; 32 | /** 33 | * Set overflow-y and scroll-behavior styles 34 | * on the provided target element. 35 | */ 36 | private setTargetOverflow; 37 | } 38 | -------------------------------------------------------------------------------- /dist/esm/pushin.d.ts: -------------------------------------------------------------------------------- 1 | import { PushInScene } from './pushInScene'; 2 | import { PushInTarget } from './pushInTarget'; 3 | import { PushInOptions, PushInSettings } from './types'; 4 | import PushInBase from './pushInBase'; 5 | /** 6 | * PushIn object 7 | * 8 | * Once new object is created, it will initialize itself and 9 | * bind events to begin interacting with dom. 10 | */ 11 | export declare class PushIn extends PushInBase { 12 | container: HTMLElement; 13 | options: PushInOptions; 14 | scene: PushInScene; 15 | private pushinDebug?; 16 | target?: PushInTarget; 17 | scrollY: number; 18 | private lastAnimationFrameId; 19 | cleanupFns: VoidFunction[]; 20 | settings: PushInSettings; 21 | mode: string; 22 | constructor(container: HTMLElement, options?: PushInOptions); 23 | /** 24 | * Initialize the object to start everything up. 25 | */ 26 | start(): void; 27 | /** 28 | * Set the mode. 29 | * 30 | * @returns {string} The mode setting, or "sequential" by default. 31 | */ 32 | setMode(): void; 33 | /** 34 | * Set up the target element for this effect, and where to listen for scrolling. 35 | */ 36 | setTarget(): void; 37 | /** 38 | * Does all necessary cleanups by removing event listeners. 39 | */ 40 | destroy(): void; 41 | /** 42 | * If there is a window object, 43 | * get the current scroll position. 44 | * 45 | * Otherwise default to 0. 46 | */ 47 | private getScrollY; 48 | /** 49 | * Bind event listeners to watch for page load and user interaction. 50 | */ 51 | bindEvents(): void; 52 | /** 53 | * Animation effect, mimicking a camera dolly on the webpage. 54 | */ 55 | private dolly; 56 | /** 57 | * Show or hide layers and set their scale, depending on if active. 58 | */ 59 | private toggleLayers; 60 | /** 61 | * Automatically set the container height based on the greatest outpoint. 62 | * 63 | * If the container has a height set already (e.g. if set by CSS), 64 | * the larger of the two numbers will be used. 65 | */ 66 | private setScrollLength; 67 | loadStyles(): void; 68 | /** 69 | * Show a debugging tool appended to the frontend of the page. 70 | * Can be used to determine best "pushin-from" and "pushin-to" values. 71 | */ 72 | private showDebugger; 73 | } 74 | -------------------------------------------------------------------------------- /dist/esm/pushin.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"pushin.js","sources":["../../src/constants.ts","../../src/pushInBase.ts","../../src/pushInComposition.ts","../../src/pushInLayer.ts","../../src/pushInScene.ts","../../src/pushInTarget.ts","../../src/pushInStyles.ts","../../src/pushin.ts","../../src/helpers.ts"],"sourcesContent":[null,null,null,null,null,null,null,null,null],"names":[],"mappings":";;;AAAO,MAAM,aAAa,GAAG,CAAC,CAAC;AAE/B;AACA;AACO,MAAM,kCAAkC,GAAG,mBAAmB,CAAC;AAEtE;AACA;AACO,MAAM,4BAA4B,GAAG,aAAa,CAAC;AAEnD,MAAM,yBAAyB,GAAG,UAAU,CAAC;AAC7C,MAAM,2BAA2B,GAAG,YAAY,CAAC;AAEjD,MAAM,2BAA2B,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AAEtD,MAAM,6BAA6B,GAAG,yBAAyB,CAAC;AAIhE,MAAM,iCAAiC,GAAG,GAAG,CAAC;AAE9C,MAAM,2BAA2B,GAAG,IAAI;;ACrBjC,MAAgB,UAAU,CAAA;AAOtC;;;AAGG;AACH,IAAA,eAAe,CAAC,IAAY,EAAE,SAAS,GAAG,IAAI,CAAC,SAAS,EAAA;AACtD,QAAA,IAAI,MAAM,CAAC;QACX,MAAM,SAAS,GAAG,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC;QAC9C,IAAI,SAAS,KAAT,IAAA,IAAA,SAAS,KAAT,KAAA,CAAA,GAAA,KAAA,CAAA,GAAA,SAAS,CAAE,YAAY,CAAC,SAAS,CAAC,EAAE;AACtC,YAAA,MAAM,GAAW,SAAS,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC;AACpD,SAAA;aAAM,IAAI,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,QAAQ,EAAE;AAClD,YAAA,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;AAC9B,SAAA;aAAM,IAAI,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,QAAQ,EAAE;;YAElD,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;AACzC,SAAA;AAAM,aAAA,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE;AAC9B,YAAA,MAAM,IAAI,GAAG,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;YACjE,IAAI,IAAI,KAAK,gBAAgB,EAAE;AAC7B,gBAAA,MAAM,GAAa,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;AACxC,aAAA;AACF,SAAA;AAAM,aAAA;YACL,MAAM,GAAG,EAAE,CAAC;AACb,SAAA;;QAGD,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE;AACtD,YAAA,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AAC5B,SAAA;AAED,QAAA,OAAO,MAAM,CAAC;KACf;AAED;;;;AAIG;AACH,IAAA,eAAe,CACb,IAAY,EACZ,SAAS,GAAG,IAAI,CAAC,SAAS,EAAA;QAE1B,IAAI,MAAM,GAAG,IAAI,CAAC;QAClB,MAAM,SAAS,GAAG,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC;QAC9C,IAAI,SAAS,KAAT,IAAA,IAAA,SAAS,KAAT,KAAA,CAAA,GAAA,KAAA,CAAA,GAAA,SAAS,CAAE,YAAY,CAAC,SAAS,CAAC,EAAE;AACtC,YAAA,MAAM,GAAW,SAAS,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC;AACpD,SAAA;AAAM,aAAA,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE;AAC9B,YAAA,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;AAC9B,SAAA;AAED,QAAA,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE;AAC9B,YAAA,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,IAAI,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;AACvD,YAAA,MAAM,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,GAAG,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;AACjD,SAAA;AAED,QAAA,OAAO,MAAM,CAAC;KACf;AAED;;;;AAIG;AACH,IAAA,aAAa,CACX,IAAY,EACZ,SAAS,GAAG,IAAI,CAAC,SAAS,EAAA;QAE1B,IAAI,MAAM,GAAG,IAAI,CAAC;QAClB,MAAM,SAAS,GAAG,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC;QAC9C,IAAI,SAAS,KAAT,IAAA,IAAA,SAAS,KAAT,KAAA,CAAA,GAAA,KAAA,CAAA,GAAA,SAAS,CAAE,YAAY,CAAC,SAAS,CAAC,EAAE;AACtC,YAAA,MAAM,GAAW,SAAS,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC;AACpD,SAAA;AAAM,aAAA,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE;AAC9B,YAAA,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;AAC9B,SAAA;AAED,QAAA,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE;AAC9B,YAAA,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,GAAG,KAAK,OAAO,GAAG,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;AACzE,YAAA,MAAM,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,GAAG,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;AACjD,SAAA;AAED,QAAA,OAAO,MAAM,CAAC;KACf;AAED,IAAA,gBAAgB,CAAC,IAAY,EAAA;AAC3B,QAAA,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAC5B,wBAAwB,EACxB,CAAC,IAAI,EAAE,GAAG,KAAK,CAAC,GAAG,GAAG,GAAG,GAAG,EAAE,IAAI,IAAI,CAAC,WAAW,EAAE,CACrD,CAAC;QACF,OAAO,CAAA,YAAA,EAAe,SAAS,CAAA,CAAE,CAAC;KACnC;AACF;;AC3FK,MAAO,iBAAkB,SAAQ,UAAU,CAAA;;IAE/C,WAAmB,CAAA,KAAkB,EAAS,OAA2B,EAAA;AACvE,QAAA,KAAK,EAAE,CAAC;QADS,IAAK,CAAA,KAAA,GAAL,KAAK,CAAa;QAAS,IAAO,CAAA,OAAA,GAAP,OAAO,CAAoB;AAEvE,QAAA,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC;KACzB;IAEM,KAAK,GAAA;QACV,IAAI,CAAC,YAAY,EAAE,CAAC;QAEpB,IAAI,IAAI,CAAC,SAAS,EAAE;YAClB,IAAI,CAAC,QAAQ,EAAE,CAAC;AACjB,SAAA;KACF;IAEM,YAAY,GAAA;;AACjB,QAAA,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,SAAU,CAAC,aAAa,CACnD,qBAAqB,CACtB,CAAC;AAEF,QAAA,IAAI,SAAS,EAAE;AACb,YAAA,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;AAC5B,SAAA;AAAM,aAAA,IAAI,MAAA,IAAI,CAAC,QAAQ,MAAA,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,KAAA,CAAA,GAAA,EAAA,CAAE,KAAK,EAAE;YAC/B,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;YAC/C,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;AAEnD,YAAA,IAAI,CAAC,SAAS,CAAC,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,SAAU,CAAC,SAAS,CAAC;YAC3D,IAAI,CAAC,KAAK,CAAC,SAAU,CAAC,SAAS,GAAG,EAAE,CAAC;YACrC,IAAI,CAAC,KAAK,CAAC,SAAU,CAAC,WAAW,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAClD,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,MAAK;AACrC,gBAAA,IAAI,CAAC,KAAK,CAAC,SAAU,CAAC,SAAS,GAAG,IAAK,CAAC,SAAU,CAAC,SAAS,CAAC;AAC/D,aAAC,CAAC,CAAC;AACJ,SAAA;KACF;AAED;;AAEG;IACK,QAAQ,GAAA;QACd,IAAI,KAAK,GAAG,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;AAE1C,QAAA,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE;;AAE7B,YAAA,KAAK,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;AACxB,SAAA;AAED,QAAA,IAAI,KAAK,EAAE;AACT,YAAA,MAAM,UAAU,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,GAAG,KAAK,GAAG,GAAG,IAAI,CAAC,GAAG,GAAG,CAAC;AACjE,YAAA,IAAI,CAAC,SAAU,CAAC,KAAK,CAAC,UAAU,GAAG,CAAA,EAAG,UAAU,CAAC,QAAQ,EAAE,GAAG,CAAC;AAChE,SAAA;KACF;AACF;;AC3CK,MAAO,WAAY,SAAQ,UAAU,CAAA;;AASzC,IAAA,WAAA,CACS,SAAsB,EACrB,KAAa,EACd,KAAkB,EACzB,OAAqB,EAAA;AAErB,QAAA,KAAK,EAAE,CAAC;QALD,IAAS,CAAA,SAAA,GAAT,SAAS,CAAa;QACrB,IAAK,CAAA,KAAA,GAAL,KAAK,CAAQ;QACd,IAAK,CAAA,KAAA,GAAL,KAAK,CAAa;AAIzB,QAAA,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC;AAExB,QAAA,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;AAC/B,QAAA,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;AAE7B,QAAA,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;AAC9D,QAAA,MAAM,SAAS,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;QACjE,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAC5C,MAAM,WAAW,GAAG,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC;QAElD,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAC3D,IAAI,CAAC,GAAG,GAAG;YACT,QAAQ;YACR,SAAS;YACT,KAAK;YACL,WAAW;SACZ,CAAC;QAEF,IAAI,CAAC,OAAO,EAAE,CAAC;QACf,IAAI,CAAC,cAAc,EAAE,CAAC;KACvB;AAED;;;AAGG;IACK,OAAO,GAAA;AACb,QAAA,IAAI,CAAC,SAAS,CAAC,YAAY,CACzB,yBAAyB,EACzB,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CACtB,CAAC;QACF,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;QAC7C,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;KACvD;AAED;;;;AAIG;IACK,cAAc,GAAA;;AACpB,QAAA,IAAI,WAAW,GACb,CAAA,EAAA,GAAA,MAAA,IAAI,CAAC,QAAQ,MAAE,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,KAAA,CAAA,GAAA,EAAA,CAAA,WAAW,MAAI,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,EAAA,GAAA,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,YAAY,CAAC;QACtE,IAAI,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,yBAAyB,CAAC,EAAE;YAC1D,MAAM,IAAI,GAAG,IAAI,CAAC,SAAU,CAAC,OAAQ,CAAC,iBAAiB,CAAC;AACxD,YAAA,IAAI,IAAI,EAAE;gBACR,WAAW,GAAG,IAAI,KAAK,OAAO,IAAI,IAAI,KAAK,GAAG,CAAC;AAChD,aAAA;AACF,SAAA;AACD,QAAA,OAAO,WAAW,CAAC;KACpB;AAED;;;;AAIG;IACK,UAAU,GAAA;QAChB,IAAI,OAAO,GAAG,CAAC,CAAC;AAEhB,QAAA,IAAI,IAAI,CAAC,KAAK,GAAG,CAAC,EAAE;AAClB,YAAA,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;AACpD,YAAA,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,SAAS,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;AAChE,YAAA,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,kBAAkB,EAAE,CAAC,CAAC;YAE5D,MAAM,OAAO,GAAG,CAAC,YAAY,GAAG,WAAW,IAAI,CAAC,CAAC;YAEjD,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,GAAG,GAAG,EAAE,YAAY,CAAC,CAAC;AACjD,SAAA;AAED,QAAA,OAAO,OAAO,CAAC;KAChB;AAED;;;;AAIG;IACK,kBAAkB,GAAA;AACxB,QAAA,MAAM,WAAW,GAAG,IAAI,CAAC,cAAc,EAAE,CAAC;QAC1C,IAAI,MAAM,GAAG,IAAI,CAAC,eAAe,CAAC,iBAAiB,CAAC,CAAC;QAErD,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE;;AAEjD,YAAA,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;AACnB,SAAA;QAED,IAAI,KAAK,GAAG,MAAuB,CAAC;AAEpC,QAAA,IAAI,CAAC,KAAK,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,YAAY,EAAE;YACnE,KAAK,GAAG,CAAC,CAAC,CAAC;AACZ,SAAA;AAAM,aAAA,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,OAAO,EAAE;YACjC,KAAK,GAAG,CAAC,CAAC,CAAC;AACZ,SAAA;aAAM,IAAI,CAAC,KAAK,EAAE;YACjB,KAAK,GAAG,iCAAiC,CAAC;AAC3C,SAAA;AAED,QAAA,OAAO,KAAK,CAAC;KACd;AAED;;;;AAIG;IACK,gBAAgB,GAAA;AACtB,QAAA,MAAM,WAAW,GAAG,IAAI,CAAC,cAAc,EAAE,CAAC;QAC1C,IAAI,MAAM,GAAG,IAAI,CAAC,eAAe,CAAC,eAAe,CAAC,CAAC;QAEnD,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE;;AAEjD,YAAA,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;AACnB,SAAA;QAED,IAAI,GAAG,GAAG,MAAuB,CAAC;AAElC,QAAA,IAAI,CAAC,GAAG,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,YAAY,EAAE;YACjE,GAAG,GAAG,CAAC,CAAC,CAAC;AACV,SAAA;AAAM,aAAA,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,MAAM,EAAE;YAC9B,GAAG,GAAG,CAAC,CAAC,CAAC;AACV,SAAA;aAAM,IAAI,CAAC,GAAG,EAAE;YACf,GAAG,GAAG,iCAAiC,CAAC;AACzC,SAAA;AAED,QAAA,OAAO,GAAG,CAAC;KACZ;AAED;;AAEG;IACK,WAAW,CAAC,OAAoB,EAAE,KAAa,EAAA;;AACrD,QAAA,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC;AACvB,QAAA,IAAI,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC;AACnB,QAAA,IAAI,OAAO,CAAC,OAAO,CAAC,2BAA2B,CAAC,EAAE;AAChD,YAAA,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,2BAA2B,CAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CACrE,OAAO,IAAI,QAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CACxC,CAAC;AACH,SAAA;AAAM,aAAA,IAAI,MAAA,IAAI,CAAC,QAAQ,MAAA,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,KAAA,CAAA,GAAA,EAAA,CAAE,QAAQ,EAAE;AAClC,YAAA,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC;AACnC,SAAA;aAAM,IAAI,IAAI,CAAC,OAAO,IAAI,KAAK,CAAC,OAAO,EAAE,KAAK,YAAY,EAAE;AAC3D,YAAA,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC;AACrC,SAAA;aAAM,IAAI,KAAK,GAAG,CAAC,EAAE;;AAEpB,YAAA,MAAM,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC;YACpD,QAAQ,GAAG,CAAC,QAAQ,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC;AAC3C,SAAA;AAED,QAAA,OAAO,QAAQ,CAAC;KACjB;AAED;;AAEG;IACK,YAAY,CAAC,OAAoB,EAAE,OAAe,EAAA;;QACxD,IAAI,SAAS,GAAG,CAAC,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;AAElD,QAAA,IAAI,OAAO,CAAC,OAAO,CAAC,yBAAyB,CAAC,EAAE;AAC9C,YAAA,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,yBAAyB,CAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AACtE,YAAA,SAAS,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,IAAI,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;AAC7D,SAAA;AAAM,aAAA,IAAI,MAAA,IAAI,CAAC,QAAQ,MAAA,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,KAAA,CAAA,GAAA,EAAA,CAAE,SAAS,EAAE;AACnC,YAAA,SAAS,GAAG,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC;AACrC,SAAA;aAAM,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,YAAY,EAAE;;AAEhD,YAAA,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;AAClB,SAAA;AAED,QAAA,OAAO,SAAS,CAAC;KAClB;AAED;;AAEG;AACK,IAAA,QAAQ,CAAC,OAAoB,EAAA;;QACnC,IAAI,KAAK,GAAkB,IAAI,CAAC;AAEhC,QAAA,IAAI,OAAO,CAAC,OAAO,CAAC,4BAA4B,CAAC,EAAE;YACjD,KAAK,GAAG,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,4BAA4B,CAAE,CAAC,CAAC;AACnE,YAAA,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE;gBACvB,KAAK,GAAG,aAAa,CAAC;AACvB,aAAA;AACF,SAAA;AAAM,aAAA,IAAI,MAAA,IAAI,CAAC,QAAQ,MAAA,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,KAAA,CAAA,GAAA,EAAA,CAAE,KAAK,EAAE;AAC/B,YAAA,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;AAC7B,SAAA;QAED,OAAO,KAAK,IAAI,aAAa,CAAC;KAC/B;AAED;;AAEG;AACH,IAAA,SAAS,CAAC,KAAa,EAAA;AACrB,QAAA,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,EAAE,QAAQ,EAAE,CAAC;KAC/D;AAED;;;;;AAKG;;IAEH,cAAc,GAAA;QACZ,IAAI,CAAC,MAAM,GAAG;AACZ,YAAA,KAAK,EAAE,IAAI,CAAC,QAAQ,EAAE;YACtB,OAAO,EAAE,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC;YAC3C,QAAQ,EAAE,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC;YAC9C,UAAU,EAAE,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC;AACpD,YAAA,OAAO,EAAE,IAAI,CAAC,UAAU,EAAE;AAC1B,YAAA,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK;AACrB,YAAA,WAAW,EAAE,IAAI,CAAC,cAAc,EAAE;AAClC,YAAA,eAAe,EAAE,IAAI,CAAC,kBAAkB,EAAE;AAC1C,YAAA,aAAa,EAAE,IAAI,CAAC,gBAAgB,EAAE;SACvC,CAAC;KACH;;IAGO,QAAQ,GAAA;QACd,QACE,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,EACzE;KACH;AAED;;AAEG;AACK,IAAA,gBAAgB,CAAC,OAAoB,EAAA;QAC3C,MAAM,SAAS,GAAG,MAAM;aACrB,gBAAgB,CAAC,OAAO,CAAC;aACzB,gBAAgB,CAAC,WAAW,CAAC,CAAC;QAEjC,IAAI,MAAM,GAAG,CAAC,CAAC;AACf,QAAA,IAAI,SAAS,IAAI,SAAS,KAAK,MAAM,EAAE;YACrC,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,6BAA6B,CAAC,CAAC;AAC7D,YAAA,IAAI,KAAK,IAAI,KAAK,CAAC,CAAC,CAAC,EAAE;gBACrB,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;gBACpC,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;AAChC,aAAA;AACF,SAAA;AAED,QAAA,OAAO,MAAM,CAAC;KACf;AAED;;AAEG;IACK,QAAQ,GAAA;AACd,QAAA,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,IAAI,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC;AAC7D,QAAA,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC;QAC9D,OAAO,GAAG,IAAI,GAAG,CAAC;KACnB;AAED;;;AAGG;;AAEK,IAAA,UAAU,CAAC,QAAkB,EAAA;QACnC,MAAM,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC;AAC5C,QAAA,OAAO,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,kBAAkB,CAAC,WAAW,CAAC,CAAC,IAAI,QAAQ,CAAC,CAAC,CAAC,CAAC;KAC5E;AAED;;;AAGG;;AAEK,IAAA,WAAW,CAAC,SAAmB,EAAA;QACrC,MAAM,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC;AAC5C,QAAA,QACE,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,kBAAkB,CAAC,WAAW,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC,CAAC,EACrE;KACH;AAED;;AAEG;AACK,IAAA,aAAa,CAAC,KAAkB,EAAA;AACtC,QAAA,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC;AAClE,QAAA,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,GAAG,GAAG,CAAC;QACtD,MAAM,KAAK,GAAG,CAAC,QAAQ,GAAG,KAAK,IAAI,GAAG,CAAC;AAEvC,QAAA,OAAO,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,aAAa,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC;KACjD;AAED;;AAEG;AACK,IAAA,QAAQ,CAAC,KAAa,EAAA;AAC5B,QAAA,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC;AACjC,QAAA,MAAM,WAAW,GAAG,CAAS,MAAA,EAAA,KAAK,GAAG,CAAC;AACtC,QAAA,KAAK,CAAC,eAAe,GAAG,WAAW,CAAC;AACnC,QAAA,KAA6C,CAAC,YAAY,GAAG,WAAW,CAAC;AACzE,QAAA,KAA4C,CAAC,WAAW,GAAG,WAAW,CAAC;AACvE,QAAA,KAA2C,CAAC,UAAU,GAAG,WAAW,CAAC;AACtE,QAAA,KAAK,CAAC,SAAS,GAAG,WAAW,CAAC;KAC/B;AAED;;;;;AAKG;IACH,aAAa,GAAA;QACX,IAAI,OAAO,GAAG,CAAC,CAAC;AAChB,QAAA,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,KAAK,CAAC,CAAC;AACjC,QAAA,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,GAAG,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC;AAC3D,QAAA,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC;AAChC,QAAA,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC;AAEjC,QAAA,IACE,OAAO;AACP,YAAA,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,GAAG,OAAO;AACnC,YAAA,IAAI,CAAC,MAAM,CAAC,eAAe,KAAK,CAAC,CAAC,EAClC;YACA,OAAO,GAAG,CAAC,CAAC;AACb,SAAA;AAAM,aAAA,IACL,MAAM;AACN,YAAA,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,GAAG,QAAQ;AACpC,YAAA,IAAI,CAAC,MAAM,CAAC,aAAa,KAAK,CAAC,CAAC,EAChC;YACA,OAAO,GAAG,CAAC,CAAC;AACb,SAAA;aAAM,IAAI,IAAI,CAAC,SAAS,EAAE,IAAI,IAAI,CAAC,QAAQ,EAAE,EAAE;AAC9C,YAAA,IAAI,eAAe,GACjB,IAAI,CAAC,GAAG,CACN,IAAI,CAAC,GAAG,CACN,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,GAAG,OAAO,EACnC,IAAI,CAAC,MAAM,CAAC,eAAe,CAC5B,EACD,CAAC,CACF,GAAG,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC;AAElC,YAAA,IAAI,IAAI,CAAC,MAAM,CAAC,eAAe,GAAG,CAAC,EAAE;gBACnC,eAAe,GAAG,CAAC,CAAC;AACrB,aAAA;AAED,YAAA,IAAI,gBAAgB,GAClB,IAAI,CAAC,GAAG,CACN,IAAI,CAAC,GAAG,CACN,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,EACpC,IAAI,CAAC,MAAM,CAAC,aAAa,CAC1B,EACD,CAAC,CACF,GAAG,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC;AAEhC,YAAA,IAAI,IAAI,CAAC,MAAM,CAAC,aAAa,GAAG,CAAC,EAAE;gBACjC,gBAAgB,GAAG,CAAC,CAAC;AACtB,aAAA;AAED,YAAA,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW;kBAC7B,IAAI,CAAC,GAAG,CAAC,eAAe,EAAE,gBAAgB,CAAC;kBAC3C,CAAC,CAAC;AACP,SAAA;AAED,QAAA,IAAI,IAAI,CAAC,QAAQ,EAAE,EAAE;YACnB,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC;AACzC,SAAA;QAED,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,OAAO,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;KACnD;AAED;;;;AAIG;IACH,SAAS,GAAA;QACP,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC;QACtC,MAAM,EAAE,eAAe,EAAE,aAAa,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC;QACpE,IAAI,SAAS,GAAG,KAAK,CAAC;QAEtB,IAAI,CAAC,WAAW,EAAE;YAChB,SAAS,GAAG,IAAI,CAAC;AAClB,SAAA;AAAM,aAAA,IAAI,IAAI,CAAC,MAAM,CAAC,OAAO,GAAG,OAAO,IAAI,eAAe,KAAK,CAAC,CAAC,EAAE;YAClE,SAAS,GAAG,IAAI,CAAC;AAClB,SAAA;AAAM,aAAA,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,GAAG,OAAO,IAAI,aAAa,KAAK,CAAC,CAAC,EAAE;YACjE,SAAS,GAAG,IAAI,CAAC;AAClB,SAAA;AAAM,aAAA,IAAI,IAAI,CAAC,QAAQ,EAAE,EAAE;YAC1B,SAAS,GAAG,IAAI,CAAC;AAClB,SAAA;AAED,QAAA,OAAO,SAAS,CAAC;KAClB;AAED;;AAEG;IACH,kBAAkB,GAAA;AAChB,QAAA,IAAI,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,GAAG,EAAE;YAClD,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC;AACvD,SAAA;AAAM,aAAA;YACL,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,MAAM,CAAC,uBAAuB,CAAC,CAAC;AAC1D,SAAA;KACF;AAED;;AAEG;AACH,IAAA,cAAc,CAAC,QAAkB,EAAA;QAC/B,IAAI,WAAW,GAAG,IAAI,CAAC,eAAe,CAAC,aAAa,CAAC,CAAC;QACtD,IAAI,CAAC,WAAW,EAAE;AAChB,YAAA,WAAW,GAAG,QAAQ,CAAC,GAAG,CACxB,OAAO,IAAI,OAAO,GAAG,IAAI,CAAC,kBAAkB,EAAE,CAC/C,CAAC;AACH,SAAA;AACD,QAAA,IAAI,OAAO,WAAW,KAAK,QAAQ,EAAE;AACnC,YAAA,WAAW,GAAG,CAAC,WAAW,CAAC,CAAC;AAC7B,SAAA;AACD,QAAA,OAAO,WAAW,CAAC;KACpB;AAED;;;AAGG;;AAEK,IAAA,aAAa,CAAC,WAAqB,EAAA;QACzC,MAAM,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC;QAC5C,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,kBAAkB,CAAC,WAAW,CAAC,CAAC;QAC9D,OAAO,WAAW,CAAC,UAAU,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC;KAClD;AACF;;ACpbK,MAAO,WAAY,SAAQ,UAAU,CAAA;;AAQzC,IAAA,WAAA,CAAmB,MAAc,EAAA;;AAC/B,QAAA,KAAK,EAAE,CAAC;QADS,IAAM,CAAA,MAAA,GAAN,MAAM,CAAQ;QAG/B,MAAM,OAAO,GAAG,CAAA,EAAA,GAAA,CAAA,EAAA,GAAA,MAAM,CAAC,OAAO,MAAE,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,KAAA,CAAA,GAAA,EAAA,CAAA,KAAK,MAAI,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,EAAA,GAAA,EAAE,CAAC;QAE5C,IAAI,CAAC,QAAQ,GAAG;AACd,YAAA,UAAU,EAAE,OAAO,KAAA,IAAA,IAAP,OAAO,KAAP,KAAA,CAAA,GAAA,KAAA,CAAA,GAAA,OAAO,CAAE,UAAU;YAC/B,WAAW,EAAE,CAAA,OAAO,KAAP,IAAA,IAAA,OAAO,uBAAP,OAAO,CAAE,WAAW,KAAI,EAAE;YACvC,QAAQ,EAAE,CAAA,OAAO,KAAP,IAAA,IAAA,OAAO,uBAAP,OAAO,CAAE,QAAQ,KAAI,EAAE;AACjC,YAAA,WAAW,EAAE,CAAA,EAAA,GAAA,MAAM,CAAC,OAAO,0CAAE,WAAW;YACxC,MAAM,EAAE,CAAA,CAAA,EAAA,GAAA,MAAM,CAAC,OAAO,MAAA,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,KAAA,CAAA,GAAA,EAAA,CAAE,MAAM,KAAI,EAAE;AACpC,YAAA,KAAK,EAAE,OAAO,KAAA,IAAA,IAAP,OAAO,KAAP,KAAA,CAAA,GAAA,KAAA,CAAA,GAAA,OAAO,CAAE,KAAK;AACrB,YAAA,SAAS,EAAE,CAAA,EAAA,GAAA,MAAM,CAAC,OAAO,0CAAE,SAAS;YACpC,MAAM,EAAE,MAAA,IAAI,CAAC,MAAM,CAAC,OAAO,0CAAE,MAAM;SACpC,CAAC;AAEF,QAAA,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;KAClB;;IAGD,KAAK,GAAA;QACH,IAAI,CAAC,YAAY,EAAE,CAAC;QACpB,IAAI,CAAC,YAAY,EAAE,CAAC;QACpB,IAAI,CAAC,aAAa,EAAE,CAAC;QACrB,IAAI,CAAC,eAAe,EAAE,CAAC;QACvB,IAAI,CAAC,cAAc,EAAE,CAAC;QACtB,IAAI,CAAC,cAAc,EAAE,CAAC;QACtB,IAAI,CAAC,SAAS,EAAE,CAAC;KAClB;AAED;;AAEG;IACH,YAAY,GAAA;AACV,QAAA,MAAM,SAAS,GACb,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,aAAa,CAAc,eAAe,CAAC,CAAC;AAEpE,QAAA,IAAI,SAAS,EAAE;AACb,YAAA,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;AAC5B,SAAA;AAAM,aAAA;YACL,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;YAC/C,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;AAE7C,YAAA,IAAI,CAAC,SAAS,CAAC,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,SAAS,CAAC;YAC3D,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,SAAS,GAAG,EAAE,CAAC;YACrC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAClD,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,MAAK;AAC/B,gBAAA,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,SAAS,GAAG,IAAI,CAAC,SAAU,CAAC,SAAS,CAAC;AAC9D,aAAC,CAAC,CAAC;AACJ,SAAA;KACF;AAED;;;;;;;AAOG;IACH,YAAY,GAAA;AACV,QAAA,IAAI,SAAS,IACX,IAAI,CAAC,eAAe,CAAC,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CACzD,CAAC;AACF,QAAA,IAAI,SAAS,KAAK,eAAe,IAAI,SAAS,KAAK,YAAY,EAAE;YAC/D,SAAS,GAAG,QAAQ,CAAC;AACtB,SAAA;AAED,QAAA,IAAI,CAAC,QAAQ,CAAC,SAAS,GAAG,SAAS,CAAC;KACrC;IAED,aAAa,GAAA;;QACX,IAAI,UAAU,GAAG,IAAI,CAAC,eAAe,CAAC,YAAY,CAAC,CAAC;QAEpD,IAAI,CAAC,UAAU,EAAE;;AAEf,YAAA,UAAU,GAAG,IAAI,CAAC,eAAe,CAAC,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;AACpE,SAAA;AAED,QAAA,IAAI,UAAU,IAAI,OAAO,UAAU,KAAK,QAAQ,EAAE;;AAEhD,YAAA,CAAC,UAAU,CAAC,GAAG,UAAU,CAAC;AAC3B,SAAA;AAED,QAAA,IAAI,CAAC,UAAU,GAAG,MAAQ,UAAU,MAAA,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,EAAA,GAAI,2BAA2B,CAAC;KACrE;AAED;;AAEG;IACH,cAAc,GAAA;;AACZ,QAAA,MAAM,kBAAkB,GAAG;AACzB,YAAA,KAAK,EAAE,CAAA,EAAA,GAAA,CAAA,EAAA,GAAA,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,MAAE,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,KAAA,CAAA,GAAA,EAAA,CAAA,KAAK,mCAAI,SAAS;SAC5D,CAAC;QACF,IAAI,CAAC,WAAW,GAAG,IAAI,iBAAiB,CAAC,IAAI,EAAE,kBAAkB,CAAC,CAAC;AACnE,QAAA,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC;KAC1B;AAED;;AAEG;;IAEK,eAAe,GAAA;AACrB,QAAA,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE;YACtB,IAAI,CAAC,SAAU,CAAC,SAAS,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAC;AAC5D,SAAA;QAED,IAAI,IAAI,CAAC,MAAM,CAAC,MAAO,CAAC,YAAY,KAAK,QAAQ,EAAE;YACjD,IAAI,CAAC,SAAU,CAAC,SAAS,CAAC,GAAG,CAAC,oCAAoC,CAAC,CAAC;AACrE,SAAA;KACF;AAED;;AAEG;IACI,MAAM,GAAA;;QACX,IAAI,IAAI,CAAC,MAAM,CAAC,MAAO,CAAC,YAAY,KAAK,QAAQ,EAAE;AACjD,YAAA,MAAM,KAAK,GAAG,CAAA,EAAA,GAAA,IAAI,CAAC,MAAM,CAAC,MAAO,CAAC,SAAS,MAAE,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,KAAA,CAAA,GAAA,EAAA,CAAA,qBAAqB,EAAE,CAAC;AACrE,YAAA,IAAI,KAAK,EAAE;AACT,gBAAA,IAAI,CAAC,SAAU,CAAC,KAAK,CAAC,MAAM,GAAG,CAAA,EAAG,KAAK,CAAC,MAAM,CAAA,EAAA,CAAI,CAAC;AACnD,gBAAA,IAAI,CAAC,SAAU,CAAC,KAAK,CAAC,KAAK,GAAG,CAAA,EAAG,KAAK,CAAC,KAAK,CAAA,EAAA,CAAI,CAAC;AAClD,aAAA;AACF,SAAA;QACD,IAAI,CAAC,eAAe,EAAE,CAAC;KACxB;AAED;;AAEG;IACK,cAAc,GAAA;;QACpB,IACE,EAAC,CAAA,EAAA,GAAA,IAAI,CAAC,QAAQ,MAAA,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,KAAA,CAAA,GAAA,EAAA,CAAE,WAAW,CAAA;YAC3B,CAAA,CAAA,EAAA,GAAA,IAAI,CAAC,QAAQ,MAAA,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,KAAA,CAAA,GAAA,EAAA,CAAE,WAAW,CAAC,MAAM,MAAK,CAAC,EACvC;YACA,IAAI,CAAC,QAAQ,CAAC,WAAW,GAAG,CAAC,GAAG,2BAA2B,CAAC,CAAC;AAC9D,SAAA;QAED,IAAI,IAAI,CAAC,SAAU,CAAC,OAAO,CAAC,kCAAkC,CAAC,EAAE;AAC/D,YAAA,IAAI,CAAC,QAAS,CAAC,WAAW,GAAG,IAAI,CAAC,SAAU,CAAC,OAAO,CAClD,kCAAkC,CAClC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,UAAU,IAAI,QAAQ,CAAC,UAAU,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;AAClE,SAAA;;QAGD,IAAI,CAAC,QAAS,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;KACvC;AAED;;AAEG;IACK,SAAS,GAAA;AACf,QAAA,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CACvB,IAAI,CAAC,SAAU,CAAC,sBAAsB,CAAC,cAAc,CAAC,CACvD,CAAC;AAEF,QAAA,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC;QAEhC,MAAM,CAAC,OAAO,CAAC,CAAC,OAAgB,EAAE,KAAK,KAAI;;YACzC,IAAI,OAAO,GAAiB,EAAE,CAAC;YAC/B,IAAI,CAAA,MAAA,IAAI,KAAA,IAAA,IAAJ,IAAI,KAAJ,KAAA,CAAA,GAAA,KAAA,CAAA,GAAA,IAAI,CAAE,QAAQ,MAAA,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,KAAA,CAAA,GAAA,EAAA,CAAE,MAAM,KAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE;gBACzD,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACvC,aAAA;AACD,YAAA,OAAO,CAAC,OAAO,GAAG,KAAK,KAAK,CAAC,CAAC;YAC9B,OAAO,CAAC,MAAM,GAAG,KAAK,KAAK,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;AAE7C,YAAA,MAAM,KAAK,GAAG,IAAI,WAAW,CAAc,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;AAC1E,YAAA,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAExB,YAAA,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;AACjC,SAAC,CAAC,CAAC;KACJ;AAED;;AAEG;AACH,IAAA,kBAAkB,CAAC,WAAqB,EAAA;;QAEtC,MAAM,WAAW,GAAG,WAAW;AAC5B,aAAA,OAAO,EAAE;aACT,SAAS,CAAC,EAAE,IAAI,EAAE,IAAI,MAAM,CAAC,UAAU,CAAC,CAAC;AAC5C,QAAA,OAAO,WAAW,KAAK,CAAC,CAAC,GAAG,CAAC,GAAG,WAAW,CAAC,MAAM,GAAG,CAAC,GAAG,WAAW,CAAC;KACtE;AAED;;;;;;;AAOG;IACH,MAAM,GAAA;QACJ,OAAO,IAAI,CAAC,SAAU,CAAC,qBAAqB,EAAE,CAAC,GAAG,CAAC;KACpD;AAED;;;;;AAKG;IACH,WAAW,GAAA;;AACT,QAAA,IAAI,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC;QACnB,IAAI,IAAI,CAAC,SAAU,CAAC,OAAO,CAAC,2BAA2B,CAAC,EAAE;AACxD,YAAA,MAAM,UAAU,IACd,IAAI,CAAC,SAAU,CAAC,OAAO,CAAC,2BAA2B,CAAC,CACrD,CAAC;YACF,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,CAAC;AACzC,SAAA;AAAM,aAAA,IAAI,CAAA,CAAA,EAAA,GAAA,IAAI,CAAC,QAAQ,MAAA,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,KAAA,CAAA,GAAA,EAAA,CAAE,QAAQ,KAAI,CAAA,MAAA,IAAI,CAAC,QAAQ,MAAE,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,KAAA,CAAA,GAAA,EAAA,CAAA,QAAQ,CAAC,MAAM,IAAG,CAAC,EAAE;AACxE,YAAA,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC;AACnC,SAAA;aAAM,IAAI,CAAA,MAAA,IAAI,CAAC,QAAQ,MAAE,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,KAAA,CAAA,GAAA,EAAA,CAAA,SAAS,MAAK,eAAe,EAAE;YACvD,QAAQ,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC;AACjD,SAAA;aAAM,IAAI,CAAA,MAAA,IAAI,CAAC,QAAQ,MAAE,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,KAAA,CAAA,GAAA,EAAA,CAAA,SAAS,MAAK,YAAY,EAAE;AACpD,YAAA,QAAQ,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;AAC5B,SAAA;AAED,QAAA,OAAO,QAAQ,CAAC;KACjB;AAED;;;;AAIG;IACH,OAAO,GAAA;AACL,QAAA,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC;KACzB;AAED;;;AAGG;IACH,eAAe,GAAA;AACb,QAAA,IAAI,IAAI,CAAC,OAAO,EAAE,KAAK,YAAY,EAAE;AACnC,YAAA,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,IAAG;gBAC1B,IAAI,KAAK,CAAC,MAAM,CAAC,QAAQ,KAAK,CAAC,CAAC,EAAE;AAChC,oBAAA,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,qBAAqB,EAAE,CAAC;AACjE,oBAAA,KAAK,CAAC,MAAM,CAAC,QAAQ,GAAG,MAAM,CAAC;AAChC,iBAAA;AACH,aAAC,CAAC,CAAC;AACJ,SAAA;KACF;AACF;;ACnQK,MAAO,YAAa,SAAQ,UAAU,CAAA;;IAM1C,WAAmB,CAAA,MAAc,EAAS,QAAwB,EAAA;AAChE,QAAA,KAAK,EAAE,CAAC;QADS,IAAM,CAAA,MAAA,GAAN,MAAM,CAAQ;QAAS,IAAQ,CAAA,QAAA,GAAR,QAAQ,CAAgB;;AAIhE,QAAA,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;AACtB,QAAA,IAAI,CAAC,YAAY,GAAG,QAAQ,CAAC;AAC7B,QAAA,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;KACjB;IAED,KAAK,GAAA;QACH,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACxB,IAAI,CAAC,eAAe,EAAE,CAAC;QACvB,IAAI,CAAC,eAAe,EAAE,CAAC;QACvB,IAAI,CAAC,iBAAiB,EAAE,CAAC;KAC1B;AAED;;;;;AAKG;IACH,gBAAgB,GAAA;QACd,MAAM,KAAK,GAAW,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC;AAErD,QAAA,IAAI,KAAK,EAAE;YACT,MAAM,OAAO,GAA4B,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;AACvE,YAAA,IAAI,OAAO,EAAE;AACX,gBAAA,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC;AAC1B,aAAA;AACF,SAAA;QAED,IACE,IAAI,CAAC,SAAS;YACd,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,aAAa,KAAK,IAAI,CAAC,SAAS,EACtD;;YAEA,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;AACnD,SAAA;KACF;AAED;;;AAGG;IACH,eAAe,GAAA;AACb,QAAA,MAAM,KAAK,GAAG,IAAI,CAAC,eAAe,CAAC,cAAc,EAAE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;AAC1E,QAAA,IAAI,YAAY,CAAC;AAEjB,QAAA,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE;YACtC,IAAI,KAAK,KAAK,QAAQ,EAAE;gBACtB,YAAY,GAAG,KAAK,CAAC;AACtB,aAAA;AAAM,iBAAA;AACL,gBAAA,YAAY,GAA4B,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;AACvE,aAAA;AACF,SAAA;AAED,QAAA,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,SAAS,EAAE;AACnC,YAAA,YAAY,GAAG,IAAI,CAAC,SAAS,CAAC;AAC/B,SAAA;AAED,QAAA,IAAI,YAAY,EAAE;AAChB,YAAA,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;AAClC,SAAA;KACF;AAED;;;;;;AAMG;IACH,eAAe,GAAA;AACb,QAAA,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,WAAW,CAAC;QACjC,IAAI,IAAI,CAAC,SAAS,EAAE;YAClB,MAAM,cAAc,GAAG,gBAAgB,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC;;AAG/D,YAAA,IAAI,CAAC,MAAM,GAAG,CAAC,cAAc,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;AACjD,SAAA;KACF;AAED;;;AAGG;IACK,iBAAiB,GAAA;QACvB,IAAI,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,YAAY,KAAK,QAAQ,EAAE;YACpD,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,SAAS,GAAG,QAAQ,CAAC;YAC1C,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,cAAc,GAAG,QAAQ,CAAC;AAChD,SAAA;KACF;AACF;;ACvGD,MAAM,YAAY,GAAG,CAAA,khCAAA,CAAohC;;ACOziC;;;;;AAKG;AACG,MAAO,MAAO,SAAQ,UAAU,CAAA;;IAWpC,WACS,CAAA,SAAsB,EACtB,OAAA,GAAyB,EAAE,EAAA;;AAElC,QAAA,KAAK,EAAE,CAAC;QAHD,IAAS,CAAA,SAAA,GAAT,SAAS,CAAa;QACtB,IAAO,CAAA,OAAA,GAAP,OAAO,CAAoB;QAT7B,IAAO,CAAA,OAAA,GAAG,CAAC,CAAC;QACX,IAAoB,CAAA,oBAAA,GAAG,CAAC,CAAC,CAAC;QAC3B,IAAU,CAAA,UAAA,GAAmB,EAAE,CAAC;QAWrC,IAAI,CAAC,QAAQ,GAAG;YACd,KAAK,EAAE,MAAA,CAAA,EAAA,GAAA,IAAI,CAAC,OAAO,MAAA,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,KAAA,CAAA,GAAA,EAAA,CAAE,KAAK,MAAA,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,EAAA,GAAI,KAAK;YACnC,MAAM,EAAE,MAAA,CAAA,EAAA,GAAA,IAAI,CAAC,OAAO,MAAA,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,KAAA,CAAA,GAAA,EAAA,CAAE,MAAM,MAAA,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,EAAA,GAAI,SAAS;AACzC,YAAA,YAAY,EAAE,CAAA,EAAA,GAAA,IAAI,CAAC,OAAO,0CAAE,YAAY;YACxC,IAAI,EAAE,MAAA,CAAA,EAAA,GAAA,IAAI,CAAC,OAAO,MAAA,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,KAAA,CAAA,GAAA,EAAA,CAAE,IAAI,MAAA,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,EAAA,GAAI,YAAY;SACzC,CAAC;;AAGF,QAAA,IAAI,CAAC,QAAQ,CAAC,KAAK,GAAG,CAAA,EAAA,GAAA,OAAO,KAAP,IAAA,IAAA,OAAO,uBAAP,OAAO,CAAE,KAAK,MAAA,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,EAAA,GAAI,KAAK,CAAC;KAC/C;AAED;;AAEG;;IAEH,KAAK,GAAA;QACH,IAAI,IAAI,CAAC,SAAS,EAAE;AAClB,YAAA,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE;gBACvB,IAAI,CAAC,YAAY,EAAE,CAAC;AACrB,aAAA;YAED,IAAI,CAAC,OAAO,EAAE,CAAC;YACf,IAAI,CAAC,UAAU,EAAE,CAAC;YAClB,IAAI,CAAC,SAAS,EAAE,CAAC;AACjB,YAAA,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC;YAEjC,IAAI,CAAC,KAAK,GAAG,IAAI,WAAW,CAAC,IAAI,CAAC,CAAC;AACnC,YAAA,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;YAEnB,IAAI,CAAC,eAAe,EAAE,CAAC;AACvB,YAAA,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;AAEpB,YAAA,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE;gBACjC,IAAI,CAAC,UAAU,EAAE,CAAC;AACnB,aAAA;;YAGD,IAAI,CAAC,YAAY,EAAE,CAAC;AACrB,SAAA;AAAM,aAAA;;AAEL,YAAA,OAAO,CAAC,KAAK,CACX,yEAAyE,CAC1E,CAAC;AACH,SAAA;KACF;AAED;;;;AAIG;IACH,OAAO,GAAA;QACL,MAAM,IAAI,GAAW,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;AAClD,QAAA,IAAI,CAAC,IAAI,GAAG,IAAI,KAAK,EAAE,GAAG,IAAI,GAAG,YAAY,CAAC;KAC/C;AAED;;AAEG;IACH,SAAS,GAAA;QACP,MAAM,OAAO,GAAmB,EAAE,CAAC;AAEnC,QAAA,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE;YACxB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;AACvC,SAAA;AAED,QAAA,IAAI,IAAI,CAAC,QAAQ,CAAC,YAAY,EAAE;YAC9B,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC;AACnD,SAAA;QAED,IAAI,CAAC,MAAM,GAAG,IAAI,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;AAC9C,QAAA,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;KACrB;AAED;;AAEG;IACH,OAAO,GAAA;AACL,QAAA,oBAAoB,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;AAEhD,QAAA,OAAO,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE;AAC7B,YAAA,IAAI,CAAC,UAAU,CAAC,GAAG,EAAG,EAAE,CAAC;AAC1B,SAAA;KACF;AAED;;;;;AAKG;IACK,UAAU,GAAA;;QAChB,IAAI,OAAO,GAAG,CAAC,CAAC;QAEhB,IACE,CAAA,MAAA,IAAI,CAAC,MAAM,MAAE,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,KAAA,CAAA,GAAA,EAAA,CAAA,YAAY,MAAK,QAAQ;YACtC,OAAO,MAAM,KAAK,WAAW,EAC7B;AACA,YAAA,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;AAC1B,SAAA;AAAM,aAAA;AACL,YAAA,MAAM,MAAM,GAAgB,IAAI,CAAC,MAAO,CAAC,YAAY,CAAC;AACtD,YAAA,OAAO,GAAW,MAAM,CAAC,SAAS,CAAC;AACpC,SAAA;AAED,QAAA,OAAO,OAAO,CAAC;KAChB;AAED;;AAEG;;IAEH,UAAU,GAAA;QACR,IAAI,YAAY,GAAyB,MAAM,CAAC;AAChD,QAAA,IAAI,IAAI,CAAC,MAAO,CAAC,YAAY,KAAK,QAAQ,EAAE;AAC1C,YAAA,YAAY,GAAgB,IAAI,CAAC,MAAO,CAAC,YAAY,CAAC;AACvD,SAAA;QAED,MAAM,QAAQ,GAAG,MAAK;;AACpB,YAAA,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC;YACjC,IAAI,CAAC,KAAK,EAAE,CAAC;YAEb,IAAI,IAAI,CAAC,WAAW,EAAE;gBACpB,MAAM,OAAO,GAAG,CAAA,EAAA,GAAA,IAAI,CAAC,WAAW,MAAA,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,KAAA,CAAA,GAAA,EAAA,CAAE,aAAa,CAC7C,wBAAwB,CACzB,CAAC;AACF,gBAAA,IAAI,OAAO,EAAE;AACX,oBAAA,OAAQ,CAAC,WAAW,GAAG,CAAA,iBAAA,EAAoB,IAAI,CAAC,KAAK,CACnD,IAAI,CAAC,OAAO,CACb,IAAI,CAAC;AACP,iBAAA;AACF,aAAA;AACH,SAAC,CAAC;AAEF,QAAA,YAAY,CAAC,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;AAClD,QAAA,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,MACnB,YAAY,CAAC,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CACrD,CAAC;AAEF,QAAA,IAAI,aAAqB,CAAC;QAC1B,MAAM,QAAQ,GAAG,MAAK;YACpB,YAAY,CAAC,aAAa,CAAC,CAAC;AAE5B,YAAA,aAAa,GAAG,MAAM,CAAC,UAAU,CAAC,MAAK;AACrC,gBAAA,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,IAAI,KAAK,CAAC,cAAc,EAAE,CAAC,CAAC;gBAC3D,IAAI,CAAC,eAAe,EAAE,CAAC;AACvB,gBAAA,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;gBACpB,IAAI,CAAC,YAAY,EAAE,CAAC;aACrB,EAAE,GAAG,CAAC,CAAC;AACV,SAAC,CAAC;AACF,QAAA,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;AAC5C,QAAA,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC;AAE3E,QAAA,MAAM,OAAO,GAAG,CAAC,KAAiB,KAAI;AACpC,YAAA,MAAM,MAAM,GAAgB,KAAK,CAAC,MAAM,CAAC;YACzC,IACE,cAAc,IAAI,MAAM;AACxB,gBAAA,MAAM,CAAC,YAAY,CAAC,6BAA6B,CAAC,EAClD;AACA,gBAAA,MAAM,KAAK,GAAG,QAAQ,CACZ,MAAO,CAAC,YAAY,CAAC,6BAA6B,CAAC,EAC3D,EAAE,CACH,CAAC;gBAEF,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACvC,gBAAA,IAAI,KAAK,EAAE;AACT,oBAAA,IAAI,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAC,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,eAAe,CAAC;AACnE,oBAAA,IAAI,KAAK,CAAC,MAAM,CAAC,UAAU,EAAE;AAC3B,wBAAA,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC;AACpC,qBAAA;AAED,oBAAA,IAAI,IAAI,CAAC,MAAO,CAAC,YAAY,KAAK,QAAQ,EAAE;AAC1C,wBAAA,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;AAC9B,qBAAA;AAAM,yBAAA;wBACL,MAAM,SAAS,GAAgB,YAAY,CAAC;AAC5C,wBAAA,SAAS,CAAC,SAAS,GAAG,QAAQ,CAAC;AAChC,qBAAA;AACF,iBAAA;AACF,aAAA;AACH,SAAC,CAAC;QACF,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;KACjD;AAED;;AAEG;;IAEK,KAAK,GAAA;AACX,QAAA,oBAAoB,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;AAEhD,QAAA,IAAI,CAAC,oBAAoB,GAAG,qBAAqB,CAAC,MAAK;YACrD,IAAI,CAAC,YAAY,EAAE,CAAC;AACtB,SAAC,CAAC,CAAC;KACJ;AAED;;AAEG;;IAEK,YAAY,GAAA;QAClB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,IAAG;YAChC,KAAK,CAAC,aAAa,EAAE,CAAC;YACtB,KAAK,CAAC,kBAAkB,EAAE,CAAC;AAC7B,SAAC,CAAC,CAAC;KACJ;AAED;;;;;AAKG;IACK,eAAe,GAAA;;;AAErB,QAAA,IAAI,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC;QACxC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,IAAG;AAChC,YAAA,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;AAC7D,SAAC,CAAC,CAAC;;QAGH,MAAM,YAAY,GAAG,CAAA,EAAA,GAAA,CAAA,EAAA,GAAA,IAAI,CAAC,MAAM,MAAE,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,KAAA,CAAA,GAAA,EAAA,CAAA,MAAM,MAAI,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,EAAA,GAAA,CAAC,CAAC;AAC9C,QAAA,MAAM,UAAU,GAAG,WAAW,GAAG,YAAY,CAAC;;QAG9C,MAAM,eAAe,GAAG,UAAU,CAChC,gBAAgB,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAC1D,CAAC;QAEF,IAAI,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,eAAe,EAAE,UAAU,CAAC,CAAC;AAEnD,QAAA,IACE,UAAU,GAAG,MAAM,CAAC,WAAW;YAC/B,IAAI,CAAC,IAAI,KAAK,YAAY;YAC1B,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,SAAS,KAAK,YAAY,EAC9C;AACA,YAAA,MAAM,IAAI,MAAM,CAAC,WAAW,CAAC;AAC9B,SAAA;;QAGD,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,GAAG,CAAA,EAAG,MAAM,CAAA,EAAA,CAAI,CAAC;KAC7C;IAED,UAAU,GAAA;QACR,MAAM,UAAU,GAAG,QAAQ,CAAC,aAAa,CAAC,qBAAqB,CAAC,CAAC;QAEjE,IAAI,CAAC,UAAU,EAAE;YACf,MAAM,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;AAC9C,YAAA,KAAK,CAAC,EAAE,GAAG,eAAe,CAAC;YAE3B,KAAK,CAAC,WAAW,CAAC,QAAQ,CAAC,cAAc,CAAC,YAAY,CAAC,CAAC,CAAC;AACzD,YAAA,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;AAEjC,YAAA,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,MAAK;AACxB,gBAAA,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;AACnC,aAAC,CAAC,CAAC;AACJ,SAAA;KACF;AAED;;;AAGG;;IAEK,YAAY,GAAA;;QAClB,IAAI,CAAC,WAAW,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QACjD,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;QAE/C,MAAM,WAAW,GAAG,QAAQ,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;AAChD,QAAA,WAAW,CAAC,SAAS,GAAG,oBAAoB,CAAC;AAC7C,QAAA,WAAW,CAAC,SAAS,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC;QAEjD,MAAM,eAAe,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;AACtD,QAAA,eAAe,CAAC,SAAS,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC;QACvD,eAAe,CAAC,SAAS,GAAG,CAAA,iBAAA,EAAoB,IAAI,CAAC,OAAO,IAAI,CAAC;AAEjE,QAAA,IAAI,CAAC,WAAW,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC;AAC1C,QAAA,IAAI,CAAC,WAAW,CAAC,WAAW,CAAC,eAAe,CAAC,CAAC;AAE9C,QAAA,MAAM,MAAM,GAAG,CAAA,EAAA,GAAA,CAAA,EAAA,GAAA,IAAI,CAAC,MAAM,MAAA,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,KAAA,CAAA,GAAA,EAAA,CAAE,SAAS,MAAA,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,EAAA,GAAI,QAAQ,CAAC,IAAI,CAAC;AAEvD,QAAA,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;;AAGrC,QAAA,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,EAAA,IAAA,EAAA,CAAA,CAAA,OAAA,MAAA,IAAI,CAAC,WAAW,MAAE,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,KAAA,CAAA,GAAA,EAAA,CAAA,MAAM,EAAE,CAAA,EAAA,CAAC,CAAC;KACxD;AACF;;ACjTD;;;AAGG;AACH,MAAM,WAAW,GAAG,CAAC,OAAuB,KAAc;;IACxD,MAAM,aAAa,GAAG,OAAO,KAAA,IAAA,IAAP,OAAO,KAAP,KAAA,CAAA,GAAA,OAAO,GAAI,EAAE,CAAC;AAEpC,IAAA,MAAM,QAAQ,GAAG,CAAA,EAAA,GAAA,OAAO,KAAP,IAAA,IAAA,OAAO,KAAP,KAAA,CAAA,GAAA,KAAA,CAAA,GAAA,OAAO,CAAE,QAAQ,MAAI,IAAA,IAAA,EAAA,KAAA,KAAA,CAAA,GAAA,EAAA,GAAA,SAAS,CAAC;IAChD,MAAM,QAAQ,GAAG,QAAQ,CAAC,gBAAgB,CAAc,QAAQ,CAAC,CAAC;IAElE,MAAM,SAAS,GAAa,EAAE,CAAC;AAC/B,IAAA,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE;QAC9B,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC;QACpD,QAAQ,CAAC,KAAK,EAAE,CAAC;AAEjB,QAAA,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;AAC1B,KAAA;AAED,IAAA,OAAO,SAAS,CAAC;AACnB,CAAC,CAAC;AAEF,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE;AACjC,IAAA,MAAM,CAAC,WAAW,GAAG,WAAW,CAAC;AAClC;;;;"} -------------------------------------------------------------------------------- /dist/esm/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface LayerOptions { 2 | inpoints: number[]; 3 | outpoints: number[]; 4 | speed: number; 5 | isFirst: boolean; 6 | isLast: boolean; 7 | transitions?: boolean; 8 | transitionStart?: number; 9 | transitionEnd?: number; 10 | tabInpoints?: number[]; 11 | } 12 | export interface LayerSettings { 13 | inpoints: number[]; 14 | outpoints: number[]; 15 | speed: number; 16 | isFirst: boolean; 17 | isLast: boolean; 18 | transitions?: boolean; 19 | transitionStart?: number; 20 | transitionEnd?: number; 21 | } 22 | export interface CompositionOptions { 23 | ratio?: number[]; 24 | } 25 | export interface SceneOptions { 26 | layerDepth?: number; 27 | breakpoints?: number[]; 28 | inpoints?: number[]; 29 | composition?: CompositionOptions; 30 | layers?: LayerOptions[]; 31 | ratio?: number[]; 32 | } 33 | export interface SceneSettings { 34 | breakpoints: number[]; 35 | inpoints: number[]; 36 | layers: LayerOptions[]; 37 | layerDepth?: number; 38 | composition?: CompositionOptions; 39 | ratio?: number[]; 40 | autoStart?: string; 41 | length?: number; 42 | } 43 | export interface PushInOptions { 44 | composition?: CompositionOptions; 45 | debug?: boolean; 46 | layers?: LayerOptions[]; 47 | scene?: SceneOptions; 48 | selector?: string; 49 | target?: string; 50 | scrollTarget?: string; 51 | mode?: string; 52 | autoStart?: string; 53 | length?: number; 54 | } 55 | export interface PushInSettings { 56 | mode: string; 57 | composition?: CompositionOptions; 58 | debug?: boolean; 59 | layers?: LayerOptions[]; 60 | selector?: string; 61 | target?: string; 62 | scrollTarget?: string; 63 | } 64 | export interface TargetSettings { 65 | target?: string; 66 | scrollTarget?: string; 67 | } 68 | export interface PushInLayer { 69 | container: HTMLElement; 70 | index: number; 71 | originalScale: number; 72 | } 73 | export interface LayerRef { 74 | inpoints: number[]; 75 | outpoints: number[]; 76 | speed: number; 77 | tabInpoints: number[]; 78 | } 79 | export interface LayerParams { 80 | depth: number; 81 | inpoint: number; 82 | outpoint: number; 83 | tabInpoint: number; 84 | overlap: number; 85 | speed: number; 86 | transitions: boolean; 87 | transitionStart: number; 88 | transitionEnd: number; 89 | } 90 | -------------------------------------------------------------------------------- /dist/pushin.css: -------------------------------------------------------------------------------- 1 | .pushin { 2 | position: relative; 3 | } 4 | 5 | .pushin-scene { 6 | display: flex; 7 | align-items: center; 8 | 9 | position: fixed; 10 | left: 0; 11 | top: 0; 12 | 13 | width: 100%; 14 | height: 100vh; 15 | } 16 | 17 | .pushin-scene--with-target { 18 | top: 0; 19 | left: auto; 20 | height: auto; 21 | width: auto; 22 | pointer-events: none; 23 | overflow: hidden; 24 | position: sticky; 25 | } 26 | 27 | .pushin-scene--scroll-target-window { 28 | height: 100vh; 29 | } 30 | 31 | .pushin-composition { 32 | flex: 0 0 100%; 33 | padding-top: 201%; 34 | position: relative; 35 | } 36 | 37 | .pushin-layer { 38 | display: flex; 39 | align-items: center; 40 | flex-direction: column; 41 | justify-content: center; 42 | 43 | opacity: 0; 44 | pointer-events: none; 45 | 46 | position: absolute; 47 | top: 0; 48 | right: 0; 49 | bottom: 0; 50 | left: 0; 51 | } 52 | 53 | .pushin-layer--visible * { 54 | pointer-events: auto; 55 | } 56 | 57 | /* Debug */ 58 | .pushin-debug { 59 | background-color: white; 60 | border: 0; 61 | border-bottom: 1px; 62 | box-shadow: -2px 8px 19px 2px rgba(0, 0, 0, 0.26); 63 | padding: 1em; 64 | position: fixed; 65 | top: 0; 66 | width: 100%; 67 | -webkit-box-shadow: -2px 8px 19px 2px rgba(0, 0, 0, 0.26); 68 | z-index: 10; 69 | } 70 | 71 | @media (min-width: 768px) { 72 | .pushin-debug { 73 | border: 1px solid black; 74 | border-radius: 15px 0 0 15px; 75 | border-right: 0; 76 | right: 0; 77 | top: 50px; 78 | width: 250px; 79 | } 80 | } 81 | 82 | .pushin-debug__title { 83 | font-weight: bold; 84 | } 85 | -------------------------------------------------------------------------------- /dist/pushin.min.css: -------------------------------------------------------------------------------- 1 | .pushin{position:relative}.pushin-scene{display:flex;align-items:center;position:fixed;left:0;top:0;width:100%;height:100vh}.pushin-scene--with-target{top:0;left:auto;height:auto;width:auto;pointer-events:none;overflow:hidden;position:sticky}.pushin-scene--scroll-target-window{height:100vh}.pushin-composition{flex:0 0 100%;padding-top:201%;position:relative}.pushin-layer{display:flex;align-items:center;flex-direction:column;justify-content:center;opacity:0;pointer-events:none;position:absolute;top:0;right:0;bottom:0;left:0}.pushin-layer--visible *{pointer-events:auto}.pushin-debug{background-color:#fff;border:0;border-bottom:1px;box-shadow:-2px 8px 19px 2px rgba(0,0,0,.26);padding:1em;position:fixed;top:0;width:100%;-webkit-box-shadow:-2px 8px 19px 2px rgba(0,0,0,.26);z-index:10}@media (min-width:768px){.pushin-debug{border:1px solid #000;border-radius:15px 0 0 15px;border-right:0;right:0;top:50px;width:250px}}.pushin-debug__title{font-weight:700} -------------------------------------------------------------------------------- /dist/umd/pushin.min.js: -------------------------------------------------------------------------------- 1 | !function(t,i){"object"==typeof exports&&"undefined"!=typeof module?i(exports):"function"==typeof define&&define.amd?define(["exports"],i):i((t="undefined"!=typeof globalThis?globalThis:t||self).pushin={})}(this,(function(t){"use strict";const i="pushinBreakpoints",e="pushinSpeed",s="pushinTo",n="pushinFrom",o=[768,1440,1920],r="data-pushin-layer-index";class a{getStringOption(t,i=this.container){let e;const s=this.getAttributeName(t);if(null==i?void 0:i.hasAttribute(s))e=i.getAttribute(s);else if("string"==typeof this.settings[t])e=this.settings[t];else if("number"==typeof this.settings[t])e=this.settings[t].toString();else if(this.settings[t]){"[object Array]"===Object.prototype.toString.call(this.settings[t])&&(e=this.settings[t])}else e="";return"string"==typeof e&&e.includes(",")&&(e=e.split(",")),e}getNumberOption(t,i=this.container){let e=null;const s=this.getAttributeName(t);return(null==i?void 0:i.hasAttribute(s))?e=i.getAttribute(s):this.settings[t]&&(e=this.settings[t]),"string"==typeof e&&(e=e.split(",").map((t=>parseFloat(t))),e=e.length>1?e:e[0]),e}getBoolOption(t,i=this.container){let e=null;const s=this.getAttributeName(t);return(null==i?void 0:i.hasAttribute(s))?e=i.getAttribute(s):this.settings[t]&&(e=this.settings[t]),"string"==typeof e&&(e=e.split(",").map((t=>"false"!==t&&!!t)),e=e.length>1?e:e[0]),e}getAttributeName(t){return`data-pushin-${t.replace(/[A-Z]+(?![a-z])|[A-Z]/g,((t,i)=>(i?"-":"")+t.toLowerCase()))}`}}class h extends a{constructor(t,i){super(),this.scene=t,this.options=i,this.settings=i}start(){this.setContainer(),this.container&&this.setRatio()}setContainer(){var t;const i=this.scene.container.querySelector(".pushin-composition");i?this.container=i:(null===(t=this.settings)||void 0===t?void 0:t.ratio)&&(this.container=document.createElement("div"),this.container.classList.add("pushin-composition"),this.container.innerHTML=this.scene.container.innerHTML,this.scene.container.innerHTML="",this.scene.container.appendChild(this.container),this.scene.pushin.cleanupFns.push((()=>{this.scene.container.innerHTML=this.container.innerHTML})))}setRatio(){let t=this.getNumberOption("ratio");if("number"==typeof t&&(t=[t,t]),t){const i=100*t.reduce(((t,i)=>i/t));this.container.style.paddingTop=`${i.toString()}%`}}}class l extends a{constructor(t,i,e,s){super(),this.container=t,this.index=i,this.scene=e,this.settings=s,this.isFirst=s.isFirst,this.isLast=s.isLast;const n=this.getInpoints(this.container,this.index),o=this.getOutpoints(this.container,n[0]),r=this.getSpeed(this.container),a=this.getTabInpoints(n);this.originalScale=this.getElementScaleX(this.container),this.ref={inpoints:n,outpoints:o,speed:r,tabInpoints:a},this.setA11y(),this.setLayerParams()}setA11y(){this.container.setAttribute("data-pushin-layer-index",this.index.toString()),this.container.setAttribute("tabindex","0"),this.container.setAttribute("aria-role","composite")}getTransitions(){var t,i;let e=null!==(i=null===(t=this.settings)||void 0===t?void 0:t.transitions)&&void 0!==i?i:"sequential"===this.scene.getMode();if(this.container.hasAttribute("data-pushin-transitions")){const t=this.container.dataset.pushinTransitions;t&&(e="false"!==t&&"0"!==t)}return e}getOverlap(){let t=0;if(this.index>0){const i=this.scene.layers[this.index-1],e=Math.max(0,i.params.transitionEnd),s=Math.max(0,this.getTransitionStart()),n=(s+e)/2;t=Math.min(.5*n,s)}return t}getTransitionStart(){const t=this.getTransitions();let i=this.getNumberOption("transitionStart");null!==i&&"number"!=typeof i&&([i]=i);let e=i;return e||t||"continuous"!==this.scene.getMode()?!e&&this.isFirst?e=-1:e||(e=200):e=-1,e}getTransitionEnd(){const t=this.getTransitions();let i=this.getNumberOption("transitionEnd");null!==i&&"number"!=typeof i&&([i]=i);let e=i;return e||t||"continuous"!==this.scene.getMode()?!e&&this.isLast?e=-1:e||(e=200):e=-1,e}getInpoints(t,i){var e;const{scene:s}=this;let o=[0];if(t.dataset[n])o=t.dataset[n].split(",").map((t=>parseInt(t.trim(),10)));else if(null===(e=this.settings)||void 0===e?void 0:e.inpoints)o=this.settings.inpoints;else if(this.isFirst||"continuous"===s.getMode())o=this.scene.getInpoints();else if(i>0){const{outpoint:t}=s.layers[i-1].params;o=[t-this.getOverlap()]}return o}getOutpoints(t,i){var e;let n=[i+this.scene.layerDepth];if(t.dataset[s]){n=t.dataset[s].split(",").map((t=>parseInt(t.trim(),10)))}else(null===(e=this.settings)||void 0===e?void 0:e.outpoints)?n=this.settings.outpoints:"continuous"===this.scene.getMode()&&(n=[-1]);return n}getSpeed(t){var i;let s=null;return t.dataset[e]?(s=parseFloat(t.dataset[e]),Number.isNaN(s)&&(s=8)):(null===(i=this.settings)||void 0===i?void 0:i.speed)&&(s=this.settings.speed),s||8}setZIndex(t){this.container.style.zIndex=(t-this.index).toString()}setLayerParams(){this.params={depth:this.getDepth(),inpoint:this.getInpoint(this.ref.inpoints),outpoint:this.getOutpoint(this.ref.outpoints),tabInpoint:this.getTabInpoint(this.ref.tabInpoints),overlap:this.getOverlap(),speed:this.ref.speed,transitions:this.getTransitions(),transitionStart:this.getTransitionStart(),transitionEnd:this.getTransitionEnd()}}getDepth(){return this.getOutpoint(this.ref.outpoints)-this.getInpoint(this.ref.inpoints)}getElementScaleX(t){const i=window.getComputedStyle(t).getPropertyValue("transform");let e=1;if(i&&"none"!==i){const t=i.match(/[matrix|scale]\(([\d,.\s]+)/);if(t&&t[1]){const i=t[1].split(", ");e=parseFloat(i[0])}}return e}isActive(){const t=this.scene.pushin.scrollY>=this.params.inpoint,i=this.scene.pushin.scrollY<=this.params.outpoint;return t&&i}getInpoint(t){const{breakpoints:i}=this.scene.settings;return t[this.scene.getBreakpointIndex(i)]||t[0]}getOutpoint(t){const{breakpoints:i}=this.scene.settings;return t[this.scene.getBreakpointIndex(i)]||t[0]}getScaleValue(t){const i=(this.scene.pushin.scrollY-t.params.inpoint)*(Math.min(t.params.speed,100)/100)/100;return Math.max(t.originalScale+i,0)}setScale(t){const{style:i}=this.container,e=`scale(${t})`;i.webkitTransform=e,i.mozTransform=e,i.msTransform=e,i.oTransform=e,i.transform=e}setLayerStyle(){let t=0;const i=0===this.index,e=this.index+1===this.scene.layers.length,{inpoint:s}=this.params,{outpoint:n}=this.params;if(i&&this.scene.pushin.scrollYn&&-1===this.params.transitionEnd)t=1;else if(this.isVisible()||this.isActive()){let i=Math.max(Math.min(this.scene.pushin.scrollY-s,this.params.transitionStart),0)/this.params.transitionStart;this.params.transitionStart<0&&(i=1);let e=Math.max(Math.min(n-this.scene.pushin.scrollY,this.params.transitionEnd),0)/this.params.transitionEnd;this.params.transitionEnd<0&&(e=1),t=this.params.transitions?Math.min(i,e):1}this.isActive()&&this.setScale(this.getScaleValue(this)),this.container.style.opacity=t.toString()}isVisible(){const{scrollY:t}=this.scene.pushin,{transitionStart:i,transitionEnd:e,transitions:s}=this.params;let n=!1;return s?(this.params.inpoint>t&&-1===i||this.params.outpoint.1?this.container.classList.add("pushin-layer--visible"):this.container.classList.remove("pushin-layer--visible")}getTabInpoints(t){let i=this.getNumberOption("tabInpoints");return i||(i=t.map((t=>t+this.getTransitionStart()))),"number"==typeof i&&(i=[i]),i}getTabInpoint(t){const{breakpoints:i}=this.scene.settings;return t[this.scene.getBreakpointIndex(i)]||t[0]}}class p extends a{constructor(t){var i,e,s,n,o,r;super(),this.pushin=t;const a=null!==(e=null===(i=t.options)||void 0===i?void 0:i.scene)&&void 0!==e?e:{};this.settings={layerDepth:null==a?void 0:a.layerDepth,breakpoints:(null==a?void 0:a.breakpoints)||[],inpoints:(null==a?void 0:a.inpoints)||[],composition:null===(s=t.options)||void 0===s?void 0:s.composition,layers:(null===(n=t.options)||void 0===n?void 0:n.layers)||[],ratio:null==a?void 0:a.ratio,autoStart:null===(o=t.options)||void 0===o?void 0:o.autoStart,length:null===(r=this.pushin.options)||void 0===r?void 0:r.length},this.layers=[]}start(){this.setContainer(),this.setAutoStart(),this.setLayerDepth(),this.setSceneClasses(),this.setComposition(),this.setBreakpoints(),this.getLayers()}setContainer(){const t=this.pushin.container.querySelector(".pushin-scene");t?this.container=t:(this.container=document.createElement("div"),this.container.classList.add("pushin-scene"),this.container.innerHTML=this.pushin.container.innerHTML,this.pushin.container.innerHTML="",this.pushin.container.appendChild(this.container),this.pushin.cleanupFns.push((()=>{this.pushin.container.innerHTML=this.container.innerHTML})))}setAutoStart(){let t=this.getStringOption("autoStart",this.pushin.container);"screen-bottom"!==t&&"screen-top"!==t&&(t="scroll"),this.settings.autoStart=t}setLayerDepth(){var t;let i=this.getNumberOption("layerDepth");i||(i=this.getNumberOption("length",this.pushin.container)),i&&"number"!=typeof i&&([i]=i),this.layerDepth=null!==(t=i)&&void 0!==t?t:1e3}setComposition(){var t,i;const e={ratio:null!==(i=null===(t=this.pushin.settings.composition)||void 0===t?void 0:t.ratio)&&void 0!==i?i:void 0};this.composition=new h(this,e),this.composition.start()}setSceneClasses(){this.pushin.target&&this.container.classList.add("pushin-scene--with-target"),"window"===this.pushin.target.scrollTarget&&this.container.classList.add("pushin-scene--scroll-target-window")}resize(){var t;if("window"!==this.pushin.target.scrollTarget){const i=null===(t=this.pushin.target.container)||void 0===t?void 0:t.getBoundingClientRect();i&&(this.container.style.height=`${i.height}px`,this.container.style.width=`${i.width}px`)}this.updateOutpoints()}setBreakpoints(){var t,e;(null===(t=this.settings)||void 0===t?void 0:t.breakpoints)&&0!==(null===(e=this.settings)||void 0===e?void 0:e.breakpoints.length)||(this.settings.breakpoints=[...o]),this.container.dataset[i]&&(this.settings.breakpoints=this.container.dataset[i].split(",").map((t=>parseInt(t.trim(),10)))),this.settings.breakpoints.unshift(0)}getLayers(){const t=Array.from(this.container.getElementsByClassName("pushin-layer"));this.layerCount=t.length,t.forEach(((i,e)=>{var s;let n={};(null===(s=null==this?void 0:this.settings)||void 0===s?void 0:s.layers)&&this.settings.layers[e]&&(n=this.settings.layers[e]),n.isFirst=0===e,n.isLast=e===t.length-1;const o=new l(i,e,this,n);this.layers.push(o),o.setZIndex(t.length)}))}getBreakpointIndex(t){const i=t.reverse().findIndex((t=>t<=window.innerWidth));return-1===i?0:t.length-1-i}getTop(){return this.container.getBoundingClientRect().top}getInpoints(){var t,i,e,s;let o=[0];if(this.container.dataset[n]){const t=this.container.dataset[n];o.push(parseInt(t,10))}else(null===(t=this.settings)||void 0===t?void 0:t.inpoints)&&(null===(i=this.settings)||void 0===i?void 0:i.inpoints.length)>0?o=this.settings.inpoints:"screen-bottom"===(null===(e=this.settings)||void 0===e?void 0:e.autoStart)?o=[this.getTop()-window.innerHeight]:"screen-top"===(null===(s=this.settings)||void 0===s?void 0:s.autoStart)&&(o=[this.getTop()]);return o}getMode(){return this.pushin.mode}updateOutpoints(){"continuous"===this.getMode()&&this.layers.forEach((t=>{if(-1===t.params.outpoint){const{bottom:i}=this.pushin.container.getBoundingClientRect();t.params.outpoint=i}}))}}class c extends a{constructor(t,i){super(),this.pushin=t,this.settings=i,this.container=null,this.scrollTarget="window",this.height=0}start(){this.setTargetElement(),this.setScrollTarget(),this.setTargetHeight(),this.setTargetOverflow()}setTargetElement(){const t=this.getStringOption("target");if(t){const i=document.querySelector(t);i&&(this.container=i)}this.container&&this.pushin.container.parentElement!==this.container&&this.container.appendChild(this.pushin.container)}setScrollTarget(){const t=this.getStringOption("scrollTarget",this.pushin.container);let i;t&&"string"==typeof t&&(i="window"===t?t:document.querySelector(t)),!i&&this.container&&(i=this.container),i&&(this.scrollTarget=i)}setTargetHeight(){if(this.height=window.innerHeight,this.container){const t=getComputedStyle(this.container).height;this.height=+t.replace("px","")}}setTargetOverflow(){this.container&&"window"!==this.scrollTarget&&(this.container.style.overflowY="scroll",this.container.style.scrollBehavior="smooth")}}class u extends a{constructor(t,i={}){var e,s,n,o,r,a,h,l;super(),this.container=t,this.options=i,this.scrollY=0,this.lastAnimationFrameId=-1,this.cleanupFns=[],this.settings={debug:null!==(s=null===(e=this.options)||void 0===e?void 0:e.debug)&&void 0!==s&&s,target:null!==(o=null===(n=this.options)||void 0===n?void 0:n.target)&&void 0!==o?o:void 0,scrollTarget:null===(r=this.options)||void 0===r?void 0:r.scrollTarget,mode:null!==(h=null===(a=this.options)||void 0===a?void 0:a.mode)&&void 0!==h?h:"sequential"},this.settings.debug=null!==(l=null==i?void 0:i.debug)&&void 0!==l&&l}start(){this.container?(this.settings.debug&&this.showDebugger(),this.setMode(),this.loadStyles(),this.setTarget(),this.scrollY=this.getScrollY(),this.scene=new p(this),this.scene.start(),this.setScrollLength(),this.scene.resize(),"undefined"!=typeof window&&this.bindEvents(),this.toggleLayers()):console.error("No container element provided to pushIn.js. Effect will not be applied.")}setMode(){const t=this.getStringOption("mode");this.mode=""!==t?t:"sequential"}setTarget(){const t={};this.settings.target&&(t.target=this.settings.target),this.settings.scrollTarget&&(t.scrollTarget=this.settings.scrollTarget),this.target=new c(this,t),this.target.start()}destroy(){for(cancelAnimationFrame(this.lastAnimationFrameId);this.cleanupFns.length;)this.cleanupFns.pop()()}getScrollY(){var t;let i=0;if("window"===(null===(t=this.target)||void 0===t?void 0:t.scrollTarget)&&"undefined"!=typeof window)i=window.scrollY;else{i=this.target.scrollTarget.scrollTop}return i}bindEvents(){let t=window;"window"!==this.target.scrollTarget&&(t=this.target.scrollTarget);const i=()=>{var t;if(this.scrollY=this.getScrollY(),this.dolly(),this.pushinDebug){const i=null===(t=this.pushinDebug)||void 0===t?void 0:t.querySelector(".pushin-debug__content");i&&(i.textContent=`Scroll position: ${Math.round(this.scrollY)}px`)}};let e;t.addEventListener("scroll",i),this.cleanupFns.push((()=>t.removeEventListener("scroll",i)));const s=()=>{clearTimeout(e),e=window.setTimeout((()=>{this.scene.layers.forEach((t=>t.setLayerParams())),this.setScrollLength(),this.scene.resize(),this.toggleLayers()}),300)};window.addEventListener("resize",s),this.cleanupFns.push((()=>window.removeEventListener("resize",s)));window.addEventListener("focus",(i=>{const e=i.target;if("hasAttribute"in e&&e.hasAttribute(r)){const i=parseInt(e.getAttribute(r),10),s=this.scene.layers[i];if(s){let i=s.params.inpoint+s.params.transitionStart;if(s.params.tabInpoint&&(i=s.params.tabInpoint),"window"===this.target.scrollTarget)window.scrollTo(0,i);else{t.scrollTop=i}}}}),!0)}dolly(){cancelAnimationFrame(this.lastAnimationFrameId),this.lastAnimationFrameId=requestAnimationFrame((()=>{this.toggleLayers()}))}toggleLayers(){this.scene.layers.forEach((t=>{t.setLayerStyle(),t.setLayerVisibility()}))}setScrollLength(){var t,i;let e=this.scene.layerDepth;this.scene.layers.forEach((t=>{e=Math.max(e,t.params.outpoint)}));const s=null!==(i=null===(t=this.target)||void 0===t?void 0:t.height)&&void 0!==i?i:0,n=e+s,o=parseFloat(getComputedStyle(this.container).height.replace("px",""));let r=Math.max(o,n);n{document.head.removeChild(t)}))}}showDebugger(){var t,i;this.pushinDebug=document.createElement("div"),this.pushinDebug.classList.add("pushin-debug");const e=document.createElement("p");e.innerText="Pushin.js Debugger",e.classList.add("pushin-debug__title");const s=document.createElement("div");s.classList.add("pushin-debug__content"),s.innerText=`Scroll position: ${this.scrollY}px`,this.pushinDebug.appendChild(e),this.pushinDebug.appendChild(s);(null!==(i=null===(t=this.target)||void 0===t?void 0:t.container)&&void 0!==i?i:document.body).appendChild(this.pushinDebug),this.cleanupFns.push((()=>{var t;return null===(t=this.pushinDebug)||void 0===t?void 0:t.remove()}))}}const d=t=>{var i;const e=null!=t?t:{},s=null!==(i=null==t?void 0:t.selector)&&void 0!==i?i:".pushin",n=document.querySelectorAll(s),o=[];for(const t of n){const i=new u(t,e);i.start(),o.push(i)}return o};"undefined"!=typeof window&&(window.pushInStart=d),t.PushIn=u})); 2 | -------------------------------------------------------------------------------- /docs/images/pushin-logo-banner.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/images/pushin-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Redirecting to https://pushinjs.com/ 4 | 5 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import type { Config } from '@jest/types'; 3 | 4 | const isCI = process.env.CI === 'true'; 5 | 6 | const config: Config.InitialOptions = { 7 | displayName: 'pushin', 8 | rootDir: path.resolve('.'), 9 | preset: 'ts-jest', 10 | 11 | /** 12 | * A set of global variables that need to be available in all test environments. 13 | */ 14 | globals: { 15 | 'ts-jest': { 16 | allowSyntheticDefaultImports: true, 17 | tsconfig: '/tsconfig.spec.json', 18 | }, 19 | }, 20 | 21 | transform: { 22 | '^.+\\.spec.ts$': 'ts-jest', 23 | }, 24 | 25 | /** 26 | * By default, Jest runs all tests and produces all errors into the console upon completion. 27 | * The bail config option can be used here to have Jest stop running tests after n failures. 28 | * Setting bail to true is the same as setting bail to 1 29 | */ 30 | bail: true, 31 | 32 | /** 33 | * Indicates whether the coverage information should be collected while executing the test. 34 | * Because this retrofits all executed files with coverage collection statements, 35 | * it may significantly slow down your tests. 36 | */ 37 | collectCoverage: isCI, 38 | 39 | /** 40 | * An array of glob patterns indicating a set of files for which coverage 41 | * information should be collected. If a file matches the specified glob pattern, 42 | * coverage information will be collected for it even if no tests exist for this file and 43 | * it's never required in the test suite. 44 | */ 45 | collectCoverageFrom: [ 46 | 'src/**/*.ts', 47 | '!src/index.ts|src/constants.ts|src/helpers.ts', 48 | ], 49 | 50 | /** 51 | * A list of reporter names that Jest uses when writing coverage reports. 52 | * Any istanbul reporter can be used. 53 | * https://github.com/istanbuljs/istanbuljs/tree/master/packages/istanbul-reports/lib 54 | */ 55 | coverageReporters: ['json', 'lcovonly', 'lcov', 'text', 'html'], 56 | 57 | /** 58 | * The glob patterns Jest uses to detect test files. 59 | */ 60 | testMatch: ['/test/**/*.spec.ts'], 61 | 62 | /** 63 | * Indicates whether each individual test should be reported during the run. 64 | * All errors will also still be shown on the bottom after execution. 65 | */ 66 | verbose: true, 67 | 68 | /** 69 | * The directory where Jest should store its cached dependency information. 70 | */ 71 | cacheDirectory: '/.cache', 72 | 73 | /** 74 | * By default, each test file gets its own independent module registry. 75 | * Enabling resetModules goes a step further and resets the module registry before running 76 | * each individual test. This is useful to isolate modules for every test so that local 77 | * module state doesn't conflict between tests. This can be done programmatically 78 | * using jest.resetModules(). 79 | */ 80 | resetModules: true, 81 | 82 | /** 83 | * Automatically clear mock calls and instances between every test. 84 | * Equivalent to calling jest.clearAllMocks() between each test. 85 | * This does not remove any mock implementation that may have been provided. 86 | */ 87 | clearMocks: true, 88 | 89 | testEnvironment: 'node', 90 | }; 91 | 92 | export default config; 93 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pushin", 3 | "description": "A javascript plugin to attach a 'dolly' or 'push-in' effect to a div element when user scrolls.", 4 | "version": "6.0.1", 5 | "sideEffects": true, 6 | "module": "dist/esm/pushin.js", 7 | "main": "dist/umd/pushin.js", 8 | "typings": "dist/esm/index.d.ts", 9 | "unpkg": "dist/umd/pushin.min.js", 10 | "jsdelivr": "dist/umd/pushin.min.js", 11 | "files": [ 12 | "dist", 13 | "LICENSE", 14 | "README.md", 15 | "src" 16 | ], 17 | "engines": { 18 | "node": ">= 16.0.0" 19 | }, 20 | "scripts": { 21 | "husky": "husky", 22 | "jest": "jest", 23 | "cross-env": "cross-env", 24 | "lint-staged": "lint-staged", 25 | "lint": "eslint --cache src/*.ts", 26 | "build": "rollup -c --bundleConfigAsCjs", 27 | "start": "rollup -cw --bundleConfigAsCjs", 28 | "test:watch": "jest --watch", 29 | "coverage": "jest --coverage && open coverage/lcov-report/index.html -a 'Google Chrome'", 30 | "test": "jest --run-in-band", 31 | "prepack": "clean-package", 32 | "postpack": "clean-package restore", 33 | "postinstall": "husky install", 34 | "storybook": "storybook dev -p 6006", 35 | "build-storybook": "storybook build" 36 | }, 37 | "clean-package": { 38 | "remove": [ 39 | "scripts", 40 | "devDependencies", 41 | "clean-package", 42 | "prettier", 43 | "lint-staged" 44 | ] 45 | }, 46 | "repository": { 47 | "type": "git", 48 | "url": "git+https://github.com/nateplusplus/pushin.git" 49 | }, 50 | "keywords": [ 51 | "parallax", 52 | "zoom", 53 | "push-in", 54 | "movement", 55 | "motion", 56 | "effect" 57 | ], 58 | "author": "Nathan Blair (https://natehub.net)", 59 | "license": "MIT", 60 | "bugs": { 61 | "url": "https://github.com/nateplusplus/pushin/issues" 62 | }, 63 | "homepage": "https://pushinjs.com", 64 | "devDependencies": { 65 | "@babel/core": "^7.16.12", 66 | "@babel/preset-env": "^7.22.10", 67 | "@babel/preset-react": "^7.22.5", 68 | "@babel/preset-typescript": "^7.22.5", 69 | "@rollup/plugin-babel": "^6.0.3", 70 | "@rollup/plugin-typescript": "^11.0.0", 71 | "@storybook/addon-actions": "^7.2.3", 72 | "@storybook/addon-essentials": "^7.2.3", 73 | "@storybook/addon-interactions": "^7.2.3", 74 | "@storybook/addon-links": "^7.2.3", 75 | "@storybook/addon-mdx-gfm": "^7.2.3", 76 | "@storybook/react": "^7.2.3", 77 | "@storybook/react-webpack5": "^7.2.3", 78 | "@storybook/testing-library": "^0.2.0", 79 | "@types/jest": "^29.5.1", 80 | "@typescript-eslint/eslint-plugin": "^6.2.1", 81 | "@typescript-eslint/parser": "^5.18.0", 82 | "babel-loader": "^9.1.2", 83 | "clean-css": "^5.3.0", 84 | "clean-package": "^2.1.1", 85 | "cross-env": "^7.0.3", 86 | "eslint": "^8.10.0", 87 | "eslint-config-airbnb-base": "^15.0.0", 88 | "eslint-config-prettier": "^8.5.0", 89 | "eslint-plugin-import": "^2.25.4", 90 | "eslint-plugin-prettier": "^4.0.0", 91 | "husky": "^8.0.1", 92 | "jest": "^29.5.0", 93 | "jsdom": "^20.0.0", 94 | "lint-staged": "^13.0.3", 95 | "prettier": "^2.6.1", 96 | "react": "^18.2.0", 97 | "react-docgen-typescript-plugin": "^1.0.5", 98 | "react-dom": "^18.2.0", 99 | "rollup": "3.21.7", 100 | "rollup-plugin-copy": "3.4.0", 101 | "rollup-plugin-terser": "^7.0.2", 102 | "storybook": "^7.2.3", 103 | "ts-jest": "^29.1.0", 104 | "ts-loader": "^9.2.8", 105 | "ts-node": "^10.7.0", 106 | "typescript": "^5.0.2" 107 | }, 108 | "prettier": { 109 | "endOfLine": "lf", 110 | "singleQuote": true, 111 | "arrowParens": "avoid", 112 | "bracketSpacing": true 113 | }, 114 | "lint-staged": { 115 | "*.{js,css,md}": [ 116 | "prettier --write" 117 | ] 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import copy from 'rollup-plugin-copy'; 2 | import typescript from '@rollup/plugin-typescript'; 3 | import { terser } from 'rollup-plugin-terser'; 4 | import getBabelOutputPlugin from '@rollup/plugin-babel'; 5 | import CleanCSS from 'clean-css'; 6 | 7 | // eslint-disable-next-line @typescript-eslint/no-var-requires 8 | const { version, author, license } = require('./package.json'); 9 | 10 | const banner = `Pushin.js - v${version}\nAuthor: ${author}\nLicense: ${license}`; 11 | 12 | function createConfig(format) { 13 | return { 14 | input: 'src/index.ts', 15 | output: { 16 | format, 17 | sourcemap: true, 18 | file: `dist/${format}/pushin.js`, 19 | name: format === 'umd' ? 'pushin' : undefined, 20 | banner: `/* ${banner} */`, 21 | }, 22 | plugins: [ 23 | copy({ 24 | targets: [{ src: 'src/pushin.css', dest: 'dist' }], 25 | }), 26 | typescript({ 27 | tsconfig: './tsconfig.json', 28 | compilerOptions: 29 | format === 'esm' 30 | ? { 31 | sourceMap: true, 32 | declaration: true, 33 | declarationDir: '.', 34 | } 35 | : {}, 36 | }), 37 | ], 38 | }; 39 | } 40 | 41 | /** This outputs a minified JS file which can then be shipped to CDN. */ 42 | function createConfigForCdn() { 43 | return { 44 | input: 'src/index.ts', 45 | output: { 46 | format: 'umd', 47 | sourcemap: false, 48 | file: 'dist/umd/pushin.min.js', 49 | name: 'pushin', 50 | banner: `/* ${banner} */`, 51 | }, 52 | plugins: [ 53 | copy({ 54 | targets: [ 55 | { 56 | src: 'src/pushin.css', 57 | dest: 'dist', 58 | transform: contents => new CleanCSS().minify(contents).styles, 59 | rename: 'pushin.min.css', 60 | }, 61 | ], 62 | }), 63 | typescript({ tsconfig: './tsconfig.json' }), 64 | terser({ 65 | ecma: '5', 66 | mangle: true, 67 | compress: true, 68 | }), 69 | getBabelOutputPlugin({ 70 | babelHelpers: 'bundled', 71 | presets: [ 72 | [ 73 | '@babel/preset-env', 74 | { 75 | targets: 76 | '> 0.25%, last 2 versions, Firefox ESR, not dead, IE 9-11', 77 | }, 78 | ], 79 | ], 80 | }), 81 | ], 82 | }; 83 | } 84 | 85 | export default [createConfig('umd'), createConfig('esm'), createConfigForCdn()]; 86 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_SPEED = 8; 2 | 3 | // The data attribute which may be defined on the elemenet in the following way: 4 | // `
`. 5 | export const PUSH_IN_BREAKPOINTS_DATA_ATTRIBUTE = 'pushinBreakpoints'; 6 | 7 | // The data attribute which may be defined on the elemenet in the following way: 8 | // `
`. 9 | export const PUSH_IN_SPEED_DATA_ATTRIBUTE = 'pushinSpeed'; 10 | 11 | export const PUSH_IN_TO_DATA_ATTRIBUTE = 'pushinTo'; 12 | export const PUSH_IN_FROM_DATA_ATTRIBUTE = 'pushinFrom'; 13 | 14 | export const PUSH_IN_DEFAULT_BREAKPOINTS = [768, 1440, 1920]; 15 | 16 | export const PUSH_IN_LAYER_INDEX_ATTRIBUTE = 'data-pushin-layer-index'; 17 | 18 | export const PUSH_IN_DEFAULT_ASPECT_RATIO = [1, 2]; 19 | 20 | export const PUSH_IN_DEFAULT_TRANSITION_LENGTH = 200; 21 | 22 | export const PUSH_IN_DEFAULT_LAYER_DEPTH = 1000; 23 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | import { PushIn } from './pushin'; 2 | import { PushInOptions } from './types'; 3 | 4 | declare global { 5 | interface Window { 6 | pushInStart(options?: PushInOptions | string): void; 7 | } 8 | } 9 | 10 | /** 11 | * Helper function: Set up and start push-in effect on all elements 12 | * matching the provided selector. 13 | */ 14 | const pushInStart = (options?: PushInOptions): PushIn[] => { 15 | const pushInOptions = options ?? {}; 16 | 17 | const selector = options?.selector ?? '.pushin'; 18 | const elements = document.querySelectorAll(selector); 19 | 20 | const instances: PushIn[] = []; 21 | for (const element of elements) { 22 | const instance = new PushIn(element, pushInOptions); 23 | instance.start(); 24 | 25 | instances.push(instance); 26 | } 27 | 28 | return instances; 29 | }; 30 | 31 | if (typeof window !== 'undefined') { 32 | window.pushInStart = pushInStart; 33 | } 34 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import './helpers'; 2 | 3 | export { PushIn } from './pushin'; 4 | -------------------------------------------------------------------------------- /src/pushInBase.ts: -------------------------------------------------------------------------------- 1 | export default abstract class PushInBase { 2 | public container?: HTMLElement | null; 3 | public settings!: { 4 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 5 | [key: string]: any; 6 | }; 7 | 8 | /** 9 | * Get the value for an option from either HTML markup or the JavaScript API. 10 | * Return a string or array of strings. 11 | */ 12 | getStringOption(name: string, container = this.container): string | string[] { 13 | let option; 14 | const attribute = this.getAttributeName(name); 15 | if (container?.hasAttribute(attribute)) { 16 | option = container.getAttribute(attribute); 17 | } else if (typeof this.settings[name] === 'string') { 18 | option = this.settings[name]; 19 | } else if (typeof this.settings[name] === 'number') { 20 | // fail-safe in case numbers are passed in 21 | option = this.settings[name].toString(); 22 | } else if (this.settings[name]) { 23 | const type = Object.prototype.toString.call(this.settings[name]); 24 | if (type === '[object Array]') { 25 | option = this.settings[name]; 26 | } 27 | } else { 28 | option = ''; 29 | } 30 | 31 | // If the string contains commas, convert it into an array 32 | if (typeof option === 'string' && option.includes(',')) { 33 | option = option.split(','); 34 | } 35 | 36 | return option; 37 | } 38 | 39 | /** 40 | * Get the value for an option from either HTML markup or the JavaScript API. 41 | * Returns a number or array of numbers. 42 | * If nothing found, returns null. 43 | */ 44 | getNumberOption( 45 | name: string, 46 | container = this.container 47 | ): number | number[] | null { 48 | let option = null; 49 | const attribute = this.getAttributeName(name); 50 | if (container?.hasAttribute(attribute)) { 51 | option = container.getAttribute(attribute); 52 | } else if (this.settings[name]) { 53 | option = this.settings[name]; 54 | } 55 | 56 | if (typeof option === 'string') { 57 | option = option.split(',').map(val => parseFloat(val)); 58 | option = option.length > 1 ? option : option[0]; 59 | } 60 | 61 | return option; 62 | } 63 | 64 | /** 65 | * Get the value for an option from either HTML markup or the JavaScript API. 66 | * Returns a boolean or array of booleans. 67 | * If nothing found, returns null. 68 | */ 69 | getBoolOption( 70 | name: string, 71 | container = this.container 72 | ): boolean | boolean[] | null { 73 | let option = null; 74 | const attribute = this.getAttributeName(name); 75 | if (container?.hasAttribute(attribute)) { 76 | option = container.getAttribute(attribute); 77 | } else if (this.settings[name]) { 78 | option = this.settings[name]; 79 | } 80 | 81 | if (typeof option === 'string') { 82 | option = option.split(',').map(val => (val === 'false' ? false : !!val)); 83 | option = option.length > 1 ? option : option[0]; 84 | } 85 | 86 | return option; 87 | } 88 | 89 | getAttributeName(name: string) { 90 | const kebabName = name.replace( 91 | /[A-Z]+(?![a-z])|[A-Z]/g, 92 | (char, idx) => (idx ? '-' : '') + char.toLowerCase() 93 | ); 94 | return `data-pushin-${kebabName}`; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/pushInComposition.ts: -------------------------------------------------------------------------------- 1 | import { PushInScene } from './pushInScene'; 2 | import { CompositionOptions } from './types'; 3 | import PushInBase from './pushInBase'; 4 | 5 | export class PushInComposition extends PushInBase { 6 | /* istanbul ignore next */ 7 | constructor(public scene: PushInScene, public options: CompositionOptions) { 8 | super(); 9 | this.settings = options; 10 | } 11 | 12 | public start(): void { 13 | this.setContainer(); 14 | 15 | if (this.container) { 16 | this.setRatio(); 17 | } 18 | } 19 | 20 | public setContainer(): void { 21 | const container = this.scene.container!.querySelector( 22 | '.pushin-composition' 23 | ); 24 | 25 | if (container) { 26 | this.container = container; 27 | } else if (this.settings?.ratio) { 28 | this.container = document.createElement('div'); 29 | this.container.classList.add('pushin-composition'); 30 | 31 | this.container.innerHTML = this.scene.container!.innerHTML; 32 | this.scene.container!.innerHTML = ''; 33 | this.scene.container!.appendChild(this.container); 34 | this.scene.pushin.cleanupFns.push(() => { 35 | this.scene.container!.innerHTML = this!.container!.innerHTML; 36 | }); 37 | } 38 | } 39 | 40 | /** 41 | * Set the aspect ratio based setting. 42 | */ 43 | private setRatio(): void { 44 | let ratio = this.getNumberOption('ratio'); 45 | 46 | if (typeof ratio === 'number') { 47 | // fail-safe if an array was not provided 48 | ratio = [ratio, ratio]; 49 | } 50 | 51 | if (ratio) { 52 | const paddingTop = ratio.reduce((prev, cur) => cur / prev) * 100; 53 | this.container!.style.paddingTop = `${paddingTop.toString()}%`; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/pushInLayer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DEFAULT_SPEED, 3 | PUSH_IN_TO_DATA_ATTRIBUTE, 4 | PUSH_IN_FROM_DATA_ATTRIBUTE, 5 | PUSH_IN_SPEED_DATA_ATTRIBUTE, 6 | PUSH_IN_DEFAULT_TRANSITION_LENGTH, 7 | } from './constants'; 8 | import { PushInScene } from './pushInScene'; 9 | import PushInBase from './pushInBase'; 10 | 11 | import { LayerOptions, LayerSettings, LayerRef, LayerParams } from './types'; 12 | 13 | export class PushInLayer extends PushInBase { 14 | public params!: LayerParams; 15 | private originalScale: number; 16 | private ref: LayerRef; 17 | public settings: LayerSettings; 18 | public isFirst: boolean; 19 | public isLast: boolean; 20 | 21 | /* istanbul ignore next */ 22 | constructor( 23 | public container: HTMLElement, 24 | private index: number, 25 | public scene: PushInScene, 26 | options: LayerOptions 27 | ) { 28 | super(); 29 | this.settings = options; 30 | 31 | this.isFirst = options.isFirst; 32 | this.isLast = options.isLast; 33 | 34 | const inpoints = this.getInpoints(this.container, this.index); 35 | const outpoints = this.getOutpoints(this.container, inpoints[0]); 36 | const speed = this.getSpeed(this.container); 37 | const tabInpoints = this.getTabInpoints(inpoints); 38 | 39 | this.originalScale = this.getElementScaleX(this.container); 40 | this.ref = { 41 | inpoints, 42 | outpoints, 43 | speed, 44 | tabInpoints, 45 | }; 46 | 47 | this.setA11y(); 48 | this.setLayerParams(); 49 | } 50 | 51 | /** 52 | * Set Accessibility features. 53 | * Ensures layers are tabbable and their role is understood by screenreaders. 54 | */ 55 | private setA11y() { 56 | this.container.setAttribute( 57 | 'data-pushin-layer-index', 58 | this.index.toString() 59 | ); 60 | this.container.setAttribute('tabindex', '0'); 61 | this.container.setAttribute('aria-role', 'composite'); 62 | } 63 | 64 | /** 65 | * Get the transitions setting, either from the API or HTML attributes. 66 | * 67 | * @return {boolean} 68 | */ 69 | private getTransitions(): boolean { 70 | let transitions = 71 | this.settings?.transitions ?? this.scene.getMode() === 'sequential'; 72 | if (this.container.hasAttribute('data-pushin-transitions')) { 73 | const attr = this.container!.dataset!.pushinTransitions; 74 | if (attr) { 75 | transitions = attr !== 'false' && attr !== '0'; 76 | } 77 | } 78 | return transitions; 79 | } 80 | 81 | /** 82 | * Get the amount of overlap between previous and current layer. 83 | * 84 | * @return {number} 85 | */ 86 | private getOverlap(): number { 87 | let overlap = 0; 88 | 89 | if (this.index > 0) { 90 | const prevLayer = this.scene.layers[this.index - 1]; 91 | const prevTranEnd = Math.max(0, prevLayer.params.transitionEnd); 92 | const curTranStart = Math.max(0, this.getTransitionStart()); 93 | 94 | const average = (curTranStart + prevTranEnd) / 2; 95 | 96 | overlap = Math.min(average * 0.5, curTranStart); 97 | } 98 | 99 | return overlap; 100 | } 101 | 102 | /** 103 | * Get the transitionStart setting, either from the API or HTML attributes. 104 | * 105 | * @returns number 106 | */ 107 | private getTransitionStart(): number { 108 | const transitions = this.getTransitions(); 109 | let option = this.getNumberOption('transitionStart'); 110 | 111 | if (option !== null && typeof option !== 'number') { 112 | // not yet compatible with breakpoints. Fall back to first value only. 113 | [option] = option; 114 | } 115 | 116 | let start = option as number | null; 117 | 118 | if (!start && !transitions && this.scene.getMode() === 'continuous') { 119 | start = -1; 120 | } else if (!start && this.isFirst) { 121 | start = -1; 122 | } else if (!start) { 123 | start = PUSH_IN_DEFAULT_TRANSITION_LENGTH; 124 | } 125 | 126 | return start; 127 | } 128 | 129 | /** 130 | * Get the transitionEnd setting, either from the API or HTML attributes. 131 | * 132 | * @returns number 133 | */ 134 | private getTransitionEnd(): number { 135 | const transitions = this.getTransitions(); 136 | let option = this.getNumberOption('transitionEnd'); 137 | 138 | if (option !== null && typeof option !== 'number') { 139 | // not yet compatible with breakpoints. Fall back to first value only. 140 | [option] = option; 141 | } 142 | 143 | let end = option as number | null; 144 | 145 | if (!end && !transitions && this.scene.getMode() === 'continuous') { 146 | end = -1; 147 | } else if (!end && this.isLast) { 148 | end = -1; 149 | } else if (!end) { 150 | end = PUSH_IN_DEFAULT_TRANSITION_LENGTH; 151 | } 152 | 153 | return end; 154 | } 155 | 156 | /** 157 | * Get all inpoints for the layer. 158 | */ 159 | private getInpoints(element: HTMLElement, index: number): number[] { 160 | const { scene } = this; 161 | let inpoints = [0]; 162 | if (element.dataset[PUSH_IN_FROM_DATA_ATTRIBUTE]) { 163 | inpoints = element.dataset[PUSH_IN_FROM_DATA_ATTRIBUTE]!.split(',').map( 164 | inpoint => parseInt(inpoint.trim(), 10) 165 | ); 166 | } else if (this.settings?.inpoints) { 167 | inpoints = this.settings.inpoints; 168 | } else if (this.isFirst || scene.getMode() === 'continuous') { 169 | inpoints = this.scene.getInpoints(); 170 | } else if (index > 0) { 171 | // Set default for middle layers if none provided 172 | const { outpoint } = scene.layers[index - 1].params; 173 | inpoints = [outpoint - this.getOverlap()]; 174 | } 175 | 176 | return inpoints; 177 | } 178 | 179 | /** 180 | * Get all outpoints for the layer. 181 | */ 182 | private getOutpoints(element: HTMLElement, inpoint: number): number[] { 183 | let outpoints = [inpoint + this.scene.layerDepth]; 184 | 185 | if (element.dataset[PUSH_IN_TO_DATA_ATTRIBUTE]) { 186 | const values = element.dataset[PUSH_IN_TO_DATA_ATTRIBUTE]!.split(','); 187 | outpoints = values.map(value => parseInt(value.trim(), 10)); 188 | } else if (this.settings?.outpoints) { 189 | outpoints = this.settings.outpoints; 190 | } else if (this.scene.getMode() === 'continuous') { 191 | // match pushin container height. 192 | outpoints = [-1]; 193 | } 194 | 195 | return outpoints; 196 | } 197 | 198 | /** 199 | * Get the push-in speed for the layer. 200 | */ 201 | private getSpeed(element: HTMLElement): number { 202 | let speed: number | null = null; 203 | 204 | if (element.dataset[PUSH_IN_SPEED_DATA_ATTRIBUTE]) { 205 | speed = parseFloat(element.dataset[PUSH_IN_SPEED_DATA_ATTRIBUTE]!); 206 | if (Number.isNaN(speed)) { 207 | speed = DEFAULT_SPEED; 208 | } 209 | } else if (this.settings?.speed) { 210 | speed = this.settings.speed; 211 | } 212 | 213 | return speed || DEFAULT_SPEED; 214 | } 215 | 216 | /** 217 | * Set the z-index of each layer so they overlap correctly. 218 | */ 219 | setZIndex(total: number): void { 220 | this.container.style.zIndex = (total - this.index).toString(); 221 | } 222 | 223 | /** 224 | * Set all the layer parameters. 225 | * 226 | * This is used during initalization and 227 | * if the window is resized. 228 | */ 229 | /* istanbul ignore next */ 230 | setLayerParams(): void { 231 | this.params = { 232 | depth: this.getDepth(), 233 | inpoint: this.getInpoint(this.ref.inpoints), 234 | outpoint: this.getOutpoint(this.ref.outpoints), 235 | tabInpoint: this.getTabInpoint(this.ref.tabInpoints), 236 | overlap: this.getOverlap(), 237 | speed: this.ref.speed, 238 | transitions: this.getTransitions(), 239 | transitionStart: this.getTransitionStart(), 240 | transitionEnd: this.getTransitionEnd(), 241 | }; 242 | } 243 | 244 | /* istanbul ignore next */ 245 | private getDepth(): number { 246 | return ( 247 | this.getOutpoint(this.ref.outpoints) - this.getInpoint(this.ref.inpoints) 248 | ); 249 | } 250 | 251 | /** 252 | * Get the initial scale of the element at time of DOM load. 253 | */ 254 | private getElementScaleX(element: HTMLElement): number { 255 | const transform = window 256 | .getComputedStyle(element) 257 | .getPropertyValue('transform'); 258 | 259 | let scaleX = 1; 260 | if (transform && transform !== 'none') { 261 | const match = transform.match(/[matrix|scale]\(([\d,.\s]+)/); 262 | if (match && match[1]) { 263 | const matrix = match[1].split(', '); 264 | scaleX = parseFloat(matrix[0]); 265 | } 266 | } 267 | 268 | return scaleX; 269 | } 270 | 271 | /** 272 | * Whether or not a layer should currently be animated. 273 | */ 274 | private isActive(): boolean { 275 | const min = this.scene.pushin.scrollY >= this.params.inpoint; 276 | const max = this.scene.pushin.scrollY <= this.params.outpoint; 277 | return min && max; 278 | } 279 | 280 | /** 281 | * Get the current inpoint for a layer, 282 | * depending on window breakpoint. 283 | */ 284 | /* istanbul ignore next */ 285 | private getInpoint(inpoints: number[]): number { 286 | const { breakpoints } = this.scene.settings; 287 | return inpoints[this.scene.getBreakpointIndex(breakpoints)] || inpoints[0]; 288 | } 289 | 290 | /** 291 | * Get the current outpoint for a layer, 292 | * depending on window breakpoint. 293 | */ 294 | /* istanbul ignore next */ 295 | private getOutpoint(outpoints: number[]): number { 296 | const { breakpoints } = this.scene.settings; 297 | return ( 298 | outpoints[this.scene.getBreakpointIndex(breakpoints)] || outpoints[0] 299 | ); 300 | } 301 | 302 | /** 303 | * Get the scaleX value for the layer. 304 | */ 305 | private getScaleValue(layer: PushInLayer): number { 306 | const distance = this.scene.pushin.scrollY - layer.params.inpoint; 307 | const speed = Math.min(layer.params.speed, 100) / 100; 308 | const delta = (distance * speed) / 100; 309 | 310 | return Math.max(layer.originalScale + delta, 0); 311 | } 312 | 313 | /** 314 | * Set element scale. 315 | */ 316 | private setScale(value: number): void { 317 | const { style } = this.container; 318 | const scaleString = `scale(${value})`; 319 | style.webkitTransform = scaleString; 320 | (style as unknown as { mozTransform: string }).mozTransform = scaleString; 321 | (style as unknown as { msTransform: string }).msTransform = scaleString; 322 | (style as unknown as { oTransform: string }).oTransform = scaleString; 323 | style.transform = scaleString; 324 | } 325 | 326 | /** 327 | * Set CSS styles to control the effect on each layer. 328 | * 329 | * This will control the scale and opacity of the layer 330 | * as the user scrolls. 331 | */ 332 | setLayerStyle(): void { 333 | let opacity = 0; 334 | const isFirst = this.index === 0; 335 | const isLast = this.index + 1 === this.scene.layers.length; 336 | const { inpoint } = this.params; 337 | const { outpoint } = this.params; 338 | 339 | if ( 340 | isFirst && 341 | this.scene.pushin.scrollY < inpoint && 342 | this.params.transitionStart === -1 343 | ) { 344 | opacity = 1; 345 | } else if ( 346 | isLast && 347 | this.scene.pushin.scrollY > outpoint && 348 | this.params.transitionEnd === -1 349 | ) { 350 | opacity = 1; 351 | } else if (this.isVisible() || this.isActive()) { 352 | let inpointDistance = 353 | Math.max( 354 | Math.min( 355 | this.scene.pushin.scrollY - inpoint, 356 | this.params.transitionStart 357 | ), 358 | 0 359 | ) / this.params.transitionStart; 360 | 361 | if (this.params.transitionStart < 0) { 362 | inpointDistance = 1; 363 | } 364 | 365 | let outpointDistance = 366 | Math.max( 367 | Math.min( 368 | outpoint - this.scene.pushin.scrollY, 369 | this.params.transitionEnd 370 | ), 371 | 0 372 | ) / this.params.transitionEnd; 373 | 374 | if (this.params.transitionEnd < 0) { 375 | outpointDistance = 1; 376 | } 377 | 378 | opacity = this.params.transitions 379 | ? Math.min(inpointDistance, outpointDistance) 380 | : 1; 381 | } 382 | 383 | if (this.isActive()) { 384 | this.setScale(this.getScaleValue(this)); 385 | } 386 | 387 | this.container.style.opacity = opacity.toString(); 388 | } 389 | 390 | /** 391 | * Check if the layer should be visible. 392 | * 393 | * @returns boolean 394 | */ 395 | isVisible(): boolean { 396 | const { scrollY } = this.scene.pushin; 397 | const { transitionStart, transitionEnd, transitions } = this.params; 398 | let isVisible = false; 399 | 400 | if (!transitions) { 401 | isVisible = true; 402 | } else if (this.params.inpoint > scrollY && transitionStart === -1) { 403 | isVisible = true; 404 | } else if (this.params.outpoint < scrollY && transitionEnd === -1) { 405 | isVisible = true; 406 | } else if (this.isActive()) { 407 | isVisible = true; 408 | } 409 | 410 | return isVisible; 411 | } 412 | 413 | /** 414 | * Set a css class depending on current opacity. 415 | */ 416 | setLayerVisibility() { 417 | if (parseFloat(this.container.style.opacity) > 0.1) { 418 | this.container.classList.add('pushin-layer--visible'); 419 | } else { 420 | this.container.classList.remove('pushin-layer--visible'); 421 | } 422 | } 423 | 424 | /** 425 | * Set tabInpoints for this layer. 426 | */ 427 | getTabInpoints(inpoints: number[]): number[] { 428 | let tabInpoints = this.getNumberOption('tabInpoints'); 429 | if (!tabInpoints) { 430 | tabInpoints = inpoints.map( 431 | inpoint => inpoint + this.getTransitionStart() 432 | ); 433 | } 434 | if (typeof tabInpoints === 'number') { 435 | tabInpoints = [tabInpoints]; 436 | } 437 | return tabInpoints; 438 | } 439 | 440 | /** 441 | * Get the current tabInpoint for a layer, 442 | * depending on window breakpoint. 443 | */ 444 | /* istanbul ignore next */ 445 | private getTabInpoint(tabInpoints: number[]): number { 446 | const { breakpoints } = this.scene.settings; 447 | const breakpoint = this.scene.getBreakpointIndex(breakpoints); 448 | return tabInpoints[breakpoint] || tabInpoints[0]; 449 | } 450 | } 451 | -------------------------------------------------------------------------------- /src/pushInScene.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PUSH_IN_FROM_DATA_ATTRIBUTE, 3 | PUSH_IN_BREAKPOINTS_DATA_ATTRIBUTE, 4 | PUSH_IN_DEFAULT_BREAKPOINTS, 5 | PUSH_IN_DEFAULT_LAYER_DEPTH, 6 | } from './constants'; 7 | import { PushInComposition } from './pushInComposition'; 8 | import { PushInLayer } from './pushInLayer'; 9 | import { PushIn } from './pushin'; 10 | import PushInBase from './pushInBase'; 11 | 12 | import { LayerOptions, SceneSettings } from './types'; 13 | 14 | export class PushInScene extends PushInBase { 15 | public layers: PushInLayer[]; 16 | public layerDepth!: number; 17 | public settings: SceneSettings; 18 | public composition?: PushInComposition; 19 | public layerCount!: number; 20 | 21 | /* istanbul ignore next */ 22 | constructor(public pushin: PushIn) { 23 | super(); 24 | 25 | const options = pushin.options?.scene ?? {}; 26 | 27 | this.settings = { 28 | layerDepth: options?.layerDepth, 29 | breakpoints: options?.breakpoints || [], 30 | inpoints: options?.inpoints || [], 31 | composition: pushin.options?.composition, 32 | layers: pushin.options?.layers || [], 33 | ratio: options?.ratio, 34 | autoStart: pushin.options?.autoStart, 35 | length: this.pushin.options?.length, 36 | }; 37 | 38 | this.layers = []; 39 | } 40 | 41 | /* istanbul ignore next */ 42 | start(): void { 43 | this.setContainer(); 44 | this.setAutoStart(); 45 | this.setLayerDepth(); 46 | this.setSceneClasses(); 47 | this.setComposition(); 48 | this.setBreakpoints(); 49 | this.getLayers(); 50 | } 51 | 52 | /** 53 | * If there is not a pushin-scene element, create one. 54 | */ 55 | setContainer(): void { 56 | const container = 57 | this.pushin.container.querySelector('.pushin-scene'); 58 | 59 | if (container) { 60 | this.container = container; 61 | } else { 62 | this.container = document.createElement('div'); 63 | this.container.classList.add('pushin-scene'); 64 | 65 | this.container.innerHTML = this.pushin.container.innerHTML; 66 | this.pushin.container.innerHTML = ''; 67 | this.pushin.container.appendChild(this.container); 68 | this.pushin.cleanupFns.push(() => { 69 | this.pushin.container.innerHTML = this.container!.innerHTML; 70 | }); 71 | } 72 | } 73 | 74 | /** 75 | * Get the AutoStart option if provided. 76 | * 77 | * Choices: 78 | * - scroll (default) Start effect on scroll. 79 | * - screen-bottom Start effect when target element top at viewport bottom. 80 | * - screen-top Start effect when target element top at viewport top. 81 | */ 82 | setAutoStart(): void { 83 | let autoStart = ( 84 | this.getStringOption('autoStart', this.pushin.container) 85 | ); 86 | if (autoStart !== 'screen-bottom' && autoStart !== 'screen-top') { 87 | autoStart = 'scroll'; 88 | } 89 | 90 | this.settings.autoStart = autoStart; 91 | } 92 | 93 | setLayerDepth() { 94 | let layerDepth = this.getNumberOption('layerDepth'); 95 | 96 | if (!layerDepth) { 97 | // use pushin 'length' setting as alias for layerDepth 98 | layerDepth = this.getNumberOption('length', this.pushin.container); 99 | } 100 | 101 | if (layerDepth && typeof layerDepth !== 'number') { 102 | // not yet compatible with array - set to first index if array passed in. 103 | [layerDepth] = layerDepth; 104 | } 105 | 106 | this.layerDepth = layerDepth ?? PUSH_IN_DEFAULT_LAYER_DEPTH; 107 | } 108 | 109 | /** 110 | * Setup composition for the scene. 111 | */ 112 | setComposition(): void { 113 | const compositionOptions = { 114 | ratio: this.pushin.settings.composition?.ratio ?? undefined, 115 | }; 116 | this.composition = new PushInComposition(this, compositionOptions); 117 | this.composition.start(); 118 | } 119 | 120 | /** 121 | * Set scene class names. 122 | */ 123 | /* istanbul ignore next */ 124 | private setSceneClasses(): void { 125 | if (this.pushin.target) { 126 | this.container!.classList.add('pushin-scene--with-target'); 127 | } 128 | 129 | if (this.pushin.target!.scrollTarget === 'window') { 130 | this.container!.classList.add('pushin-scene--scroll-target-window'); 131 | } 132 | } 133 | 134 | /** 135 | * Resize the PushIn container if using a target container. 136 | */ 137 | public resize(): void { 138 | if (this.pushin.target!.scrollTarget !== 'window') { 139 | const sizes = this.pushin.target!.container?.getBoundingClientRect(); 140 | if (sizes) { 141 | this.container!.style.height = `${sizes.height}px`; 142 | this.container!.style.width = `${sizes.width}px`; 143 | } 144 | } 145 | this.updateOutpoints(); 146 | } 147 | 148 | /** 149 | * Set breakpoints for responsive design settings. 150 | */ 151 | private setBreakpoints(): void { 152 | if ( 153 | !this.settings?.breakpoints || 154 | this.settings?.breakpoints.length === 0 155 | ) { 156 | this.settings.breakpoints = [...PUSH_IN_DEFAULT_BREAKPOINTS]; 157 | } 158 | 159 | if (this.container!.dataset[PUSH_IN_BREAKPOINTS_DATA_ATTRIBUTE]) { 160 | this.settings!.breakpoints = this.container!.dataset[ 161 | PUSH_IN_BREAKPOINTS_DATA_ATTRIBUTE 162 | ]!.split(',').map(breakpoint => parseInt(breakpoint.trim(), 10)); 163 | } 164 | 165 | // Always include break point 0 for anything under first breakpoint 166 | this.settings!.breakpoints.unshift(0); 167 | } 168 | 169 | /** 170 | * Find all layers on the page and store them with their parameters 171 | */ 172 | private getLayers(): void { 173 | const layers = Array.from( 174 | this.container!.getElementsByClassName('pushin-layer') 175 | ); 176 | 177 | this.layerCount = layers.length; 178 | 179 | layers.forEach((element: Element, index) => { 180 | let options = {}; 181 | if (this?.settings?.layers && this.settings.layers[index]) { 182 | options = this.settings.layers[index]; 183 | } 184 | options.isFirst = index === 0; 185 | options.isLast = index === layers.length - 1; 186 | 187 | const layer = new PushInLayer(element, index, this, options); 188 | this.layers.push(layer); 189 | 190 | layer.setZIndex(layers.length); 191 | }); 192 | } 193 | 194 | /** 195 | * Get the array index of the current window breakpoint. 196 | */ 197 | getBreakpointIndex(breakpoints: number[]): number { 198 | // Find the largest breakpoint that is less-than or equal to the window width. 199 | const searchIndex = breakpoints 200 | .reverse() 201 | .findIndex(bp => bp <= window.innerWidth); 202 | return searchIndex === -1 ? 0 : breakpoints.length - 1 - searchIndex; 203 | } 204 | 205 | /** 206 | * Get the screen-top value of the container. 207 | * 208 | * If using a target, get the top of the 209 | * container relative to the target's top. 210 | * 211 | * @returns {number} 212 | */ 213 | getTop(): number { 214 | return this.container!.getBoundingClientRect().top; 215 | } 216 | 217 | /** 218 | * Get the scene inpoints provided by the JavaScript API 219 | * and/or the HTML data-attributes. 220 | * 221 | * @returns {number[]} 222 | */ 223 | getInpoints(): number[] { 224 | let inpoints = [0]; 225 | if (this.container!.dataset[PUSH_IN_FROM_DATA_ATTRIBUTE]) { 226 | const pushInFrom = ( 227 | this.container!.dataset[PUSH_IN_FROM_DATA_ATTRIBUTE] 228 | ); 229 | inpoints.push(parseInt(pushInFrom, 10)); 230 | } else if (this.settings?.inpoints && this.settings?.inpoints.length > 0) { 231 | inpoints = this.settings.inpoints; 232 | } else if (this.settings?.autoStart === 'screen-bottom') { 233 | inpoints = [this.getTop() - window.innerHeight]; 234 | } else if (this.settings?.autoStart === 'screen-top') { 235 | inpoints = [this.getTop()]; 236 | } 237 | 238 | return inpoints; 239 | } 240 | 241 | /** 242 | * Get the mode setting. 243 | * 244 | * @returns string 245 | */ 246 | getMode(): string { 247 | return this.pushin.mode; 248 | } 249 | 250 | /** 251 | * Update outpoints to match container height 252 | * if using continuous mode and outpoint not specified. 253 | */ 254 | updateOutpoints(): void { 255 | if (this.getMode() === 'continuous') { 256 | this.layers.forEach(layer => { 257 | if (layer.params.outpoint === -1) { 258 | const { bottom } = this.pushin.container.getBoundingClientRect(); 259 | layer.params.outpoint = bottom; 260 | } 261 | }); 262 | } 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/pushInStyles.ts: -------------------------------------------------------------------------------- 1 | const pushInStyles = `.pushin {position: relative;}.pushin-scene {display: flex;align-items: center;position: fixed;left: 0;top: 0;width: 100%;height: 100vh;}.pushin-scene--with-target {top: 0;left: auto;height: auto;width: auto;pointer-events: none;overflow: hidden;position: sticky;}.pushin-scene--scroll-target-window {height: 100vh;}.pushin-composition {flex: 0 0 100%;padding-top: 201%;position: relative;}.pushin-layer {display: flex;align-items: center;flex-direction: column;justify-content: center;opacity: 0;pointer-events: none;position: absolute;top: 0;right: 0;bottom: 0;left: 0;}.pushin-layer--visible * {pointer-events: auto;}.pushin-debug {background-color: white;border: 0;border-bottom: 1px;box-shadow: -2px 8px 19px 2px rgba(0, 0, 0, 0.26);padding: 1em;position: fixed;top: 0;width: 100%;-webkit-box-shadow: -2px 8px 19px 2px rgba(0, 0, 0, 0.26);z-index: 10;}@media (min-width: 768px) {.pushin-debug {border: 1px solid black;border-radius: 15px 0 0 15px;border-right: 0;right: 0;top: 50px;width: 250px;}}.pushin-debug__title {font-weight: bold;}`; 2 | 3 | export default pushInStyles; 4 | -------------------------------------------------------------------------------- /src/pushInTarget.ts: -------------------------------------------------------------------------------- 1 | import PushInBase from './pushInBase'; 2 | import { PushIn } from './pushin'; 3 | import { TargetSettings } from './types'; 4 | 5 | export class PushInTarget extends PushInBase { 6 | public container: HTMLElement | null; 7 | public scrollTarget: HTMLElement | string; 8 | public height: number; 9 | 10 | /* istanbul ignore next */ 11 | constructor(public pushin: PushIn, public settings: TargetSettings) { 12 | super(); 13 | 14 | // set defaults 15 | this.container = null; 16 | this.scrollTarget = 'window'; 17 | this.height = 0; 18 | } 19 | 20 | start(): void { 21 | this.setTargetElement(); 22 | this.setScrollTarget(); 23 | this.setTargetHeight(); 24 | this.setTargetOverflow(); 25 | } 26 | 27 | /** 28 | * Set the target parameter and make sure 29 | * pushin is always a child of that target. 30 | * 31 | * @param options 32 | */ 33 | setTargetElement(): void { 34 | const value = this.getStringOption('target'); 35 | 36 | if (value) { 37 | const element = document.querySelector(value); 38 | if (element) { 39 | this.container = element; 40 | } 41 | } 42 | 43 | if ( 44 | this.container && 45 | this.pushin.container.parentElement !== this.container 46 | ) { 47 | // Move pushin into the target container 48 | this.container.appendChild(this.pushin.container); 49 | } 50 | } 51 | 52 | /** 53 | * Get scrollTarget option from data attribute 54 | * or JavaScript API. 55 | */ 56 | setScrollTarget(): void { 57 | const value = this.getStringOption('scrollTarget', this.pushin.container); 58 | let scrollTarget; 59 | 60 | if (value && typeof value === 'string') { 61 | if (value === 'window') { 62 | scrollTarget = value; 63 | } else { 64 | scrollTarget = document.querySelector(value); 65 | } 66 | } 67 | 68 | if (!scrollTarget && this.container) { 69 | scrollTarget = this.container; 70 | } 71 | 72 | if (scrollTarget) { 73 | this.scrollTarget = scrollTarget; 74 | } 75 | } 76 | 77 | /** 78 | * Set the target height on initialization. 79 | * 80 | * This will be used to calculate scroll length. 81 | * 82 | * @see setScrollLength 83 | */ 84 | setTargetHeight() { 85 | this.height = window.innerHeight; 86 | if (this.container) { 87 | const computedHeight = getComputedStyle(this.container).height; 88 | 89 | // Remove px and convert to number 90 | this.height = +computedHeight.replace('px', ''); 91 | } 92 | } 93 | 94 | /** 95 | * Set overflow-y and scroll-behavior styles 96 | * on the provided target element. 97 | */ 98 | private setTargetOverflow(): void { 99 | if (this.container && this.scrollTarget !== 'window') { 100 | this.container.style.overflowY = 'scroll'; 101 | this.container.style.scrollBehavior = 'smooth'; 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/pushin.css: -------------------------------------------------------------------------------- 1 | .pushin { 2 | position: relative; 3 | } 4 | 5 | .pushin-scene { 6 | display: flex; 7 | align-items: center; 8 | 9 | position: fixed; 10 | left: 0; 11 | top: 0; 12 | 13 | width: 100%; 14 | height: 100vh; 15 | } 16 | 17 | .pushin-scene--with-target { 18 | top: 0; 19 | left: auto; 20 | height: auto; 21 | width: auto; 22 | pointer-events: none; 23 | overflow: hidden; 24 | position: sticky; 25 | } 26 | 27 | .pushin-scene--scroll-target-window { 28 | height: 100vh; 29 | } 30 | 31 | .pushin-composition { 32 | flex: 0 0 100%; 33 | padding-top: 201%; 34 | position: relative; 35 | } 36 | 37 | .pushin-layer { 38 | display: flex; 39 | align-items: center; 40 | flex-direction: column; 41 | justify-content: center; 42 | 43 | opacity: 0; 44 | pointer-events: none; 45 | 46 | position: absolute; 47 | top: 0; 48 | right: 0; 49 | bottom: 0; 50 | left: 0; 51 | } 52 | 53 | .pushin-layer--visible * { 54 | pointer-events: auto; 55 | } 56 | 57 | /* Debug */ 58 | .pushin-debug { 59 | background-color: white; 60 | border: 0; 61 | border-bottom: 1px; 62 | box-shadow: -2px 8px 19px 2px rgba(0, 0, 0, 0.26); 63 | padding: 1em; 64 | position: fixed; 65 | top: 0; 66 | width: 100%; 67 | -webkit-box-shadow: -2px 8px 19px 2px rgba(0, 0, 0, 0.26); 68 | z-index: 10; 69 | } 70 | 71 | @media (min-width: 768px) { 72 | .pushin-debug { 73 | border: 1px solid black; 74 | border-radius: 15px 0 0 15px; 75 | border-right: 0; 76 | right: 0; 77 | top: 50px; 78 | width: 250px; 79 | } 80 | } 81 | 82 | .pushin-debug__title { 83 | font-weight: bold; 84 | } 85 | -------------------------------------------------------------------------------- /src/pushin.ts: -------------------------------------------------------------------------------- 1 | import { PushInScene } from './pushInScene'; 2 | import { PushInTarget } from './pushInTarget'; 3 | import { PushInOptions, PushInSettings, TargetSettings } from './types'; 4 | import { PUSH_IN_LAYER_INDEX_ATTRIBUTE } from './constants'; 5 | import PushInBase from './pushInBase'; 6 | import pushInStyles from './pushInStyles'; 7 | 8 | /** 9 | * PushIn object 10 | * 11 | * Once new object is created, it will initialize itself and 12 | * bind events to begin interacting with dom. 13 | */ 14 | export class PushIn extends PushInBase { 15 | public scene!: PushInScene; 16 | private pushinDebug?: HTMLElement; 17 | public target?: PushInTarget; 18 | public scrollY = 0; 19 | private lastAnimationFrameId = -1; 20 | public cleanupFns: VoidFunction[] = []; 21 | public settings: PushInSettings; 22 | public mode!: string; 23 | 24 | /* istanbul ignore next */ 25 | constructor( 26 | public container: HTMLElement, 27 | public options: PushInOptions = {} 28 | ) { 29 | super(); 30 | 31 | this.settings = { 32 | debug: this.options?.debug ?? false, 33 | target: this.options?.target ?? undefined, 34 | scrollTarget: this.options?.scrollTarget, 35 | mode: this.options?.mode ?? 'sequential', 36 | }; 37 | 38 | // Defaults 39 | this.settings.debug = options?.debug ?? false; 40 | } 41 | 42 | /** 43 | * Initialize the object to start everything up. 44 | */ 45 | /* istanbul ignore next */ 46 | start(): void { 47 | if (this.container) { 48 | if (this.settings.debug) { 49 | this.showDebugger(); 50 | } 51 | 52 | this.setMode(); 53 | this.loadStyles(); 54 | this.setTarget(); 55 | this.scrollY = this.getScrollY(); 56 | 57 | this.scene = new PushInScene(this); 58 | this.scene.start(); 59 | 60 | this.setScrollLength(); 61 | this.scene.resize(); 62 | 63 | if (typeof window !== 'undefined') { 64 | this.bindEvents(); 65 | } 66 | 67 | // Set layer initial state 68 | this.toggleLayers(); 69 | } else { 70 | // eslint-disable-next-line no-console 71 | console.error( 72 | 'No container element provided to pushIn.js. Effect will not be applied.' 73 | ); 74 | } 75 | } 76 | 77 | /** 78 | * Set the mode. 79 | * 80 | * @returns {string} The mode setting, or "sequential" by default. 81 | */ 82 | setMode() { 83 | const mode = this.getStringOption('mode'); 84 | this.mode = mode !== '' ? mode : 'sequential'; 85 | } 86 | 87 | /** 88 | * Set up the target element for this effect, and where to listen for scrolling. 89 | */ 90 | setTarget() { 91 | const options: TargetSettings = {}; 92 | 93 | if (this.settings.target) { 94 | options.target = this.settings.target; 95 | } 96 | 97 | if (this.settings.scrollTarget) { 98 | options.scrollTarget = this.settings.scrollTarget; 99 | } 100 | 101 | this.target = new PushInTarget(this, options); 102 | this.target.start(); 103 | } 104 | 105 | /** 106 | * Does all necessary cleanups by removing event listeners. 107 | */ 108 | destroy(): void { 109 | cancelAnimationFrame(this.lastAnimationFrameId); 110 | 111 | while (this.cleanupFns.length) { 112 | this.cleanupFns.pop()!(); 113 | } 114 | } 115 | 116 | /** 117 | * If there is a window object, 118 | * get the current scroll position. 119 | * 120 | * Otherwise default to 0. 121 | */ 122 | private getScrollY(): number { 123 | let scrollY = 0; 124 | 125 | if ( 126 | this.target?.scrollTarget === 'window' && 127 | typeof window !== 'undefined' 128 | ) { 129 | scrollY = window.scrollY; 130 | } else { 131 | const target = this.target!.scrollTarget; 132 | scrollY = target.scrollTop; 133 | } 134 | 135 | return scrollY; 136 | } 137 | 138 | /** 139 | * Bind event listeners to watch for page load and user interaction. 140 | */ 141 | /* istanbul ignore next */ 142 | bindEvents(): void { 143 | let scrollTarget: Window | HTMLElement = window; 144 | if (this.target!.scrollTarget !== 'window') { 145 | scrollTarget = this.target!.scrollTarget; 146 | } 147 | 148 | const onScroll = () => { 149 | this.scrollY = this.getScrollY(); 150 | this.dolly(); 151 | 152 | if (this.pushinDebug) { 153 | const content = this.pushinDebug?.querySelector( 154 | '.pushin-debug__content' 155 | ); 156 | if (content) { 157 | content!.textContent = `Scroll position: ${Math.round( 158 | this.scrollY 159 | )}px`; 160 | } 161 | } 162 | }; 163 | 164 | scrollTarget.addEventListener('scroll', onScroll); 165 | this.cleanupFns.push(() => 166 | scrollTarget.removeEventListener('scroll', onScroll) 167 | ); 168 | 169 | let resizeTimeout: number; 170 | const onResize = () => { 171 | clearTimeout(resizeTimeout); 172 | 173 | resizeTimeout = window.setTimeout(() => { 174 | this.scene.layers.forEach(layer => layer.setLayerParams()); 175 | this.setScrollLength(); 176 | this.scene.resize(); 177 | this.toggleLayers(); 178 | }, 300); 179 | }; 180 | window.addEventListener('resize', onResize); 181 | this.cleanupFns.push(() => window.removeEventListener('resize', onResize)); 182 | 183 | const onFocus = (event: FocusEvent) => { 184 | const target = event.target; 185 | if ( 186 | 'hasAttribute' in target && 187 | target.hasAttribute(PUSH_IN_LAYER_INDEX_ATTRIBUTE) 188 | ) { 189 | const index = parseInt( 190 | target!.getAttribute(PUSH_IN_LAYER_INDEX_ATTRIBUTE), 191 | 10 192 | ); 193 | 194 | const layer = this.scene.layers[index]; 195 | if (layer) { 196 | let scrollTo = layer.params.inpoint + layer.params.transitionStart; 197 | if (layer.params.tabInpoint) { 198 | scrollTo = layer.params.tabInpoint; 199 | } 200 | 201 | if (this.target!.scrollTarget === 'window') { 202 | window.scrollTo(0, scrollTo); 203 | } else { 204 | const container = scrollTarget; 205 | container.scrollTop = scrollTo; 206 | } 207 | } 208 | } 209 | }; 210 | window.addEventListener('focus', onFocus, true); 211 | } 212 | 213 | /** 214 | * Animation effect, mimicking a camera dolly on the webpage. 215 | */ 216 | /* istanbul ignore next */ 217 | private dolly(): void { 218 | cancelAnimationFrame(this.lastAnimationFrameId); 219 | 220 | this.lastAnimationFrameId = requestAnimationFrame(() => { 221 | this.toggleLayers(); 222 | }); 223 | } 224 | 225 | /** 226 | * Show or hide layers and set their scale, depending on if active. 227 | */ 228 | /* istanbul ignore next */ 229 | private toggleLayers(): void { 230 | this.scene.layers.forEach(layer => { 231 | layer.setLayerStyle(); 232 | layer.setLayerVisibility(); 233 | }); 234 | } 235 | 236 | /** 237 | * Automatically set the container height based on the greatest outpoint. 238 | * 239 | * If the container has a height set already (e.g. if set by CSS), 240 | * the larger of the two numbers will be used. 241 | */ 242 | private setScrollLength(): void { 243 | // Get the largest layer outpoint and add up overlap values. 244 | let maxOutpoint = this.scene.layerDepth; 245 | this.scene.layers.forEach(layer => { 246 | maxOutpoint = Math.max(maxOutpoint, layer.params.outpoint); 247 | }); 248 | 249 | // Calculate height with greatest outpoint + target height. 250 | const targetHeight = this.target?.height ?? 0; 251 | const calculated = maxOutpoint + targetHeight; 252 | 253 | // Get the existing container height. 254 | const containerHeight = parseFloat( 255 | getComputedStyle(this.container).height.replace('px', '') 256 | ); 257 | 258 | let height = Math.max(containerHeight, calculated); 259 | 260 | if ( 261 | calculated < window.innerHeight && 262 | this.mode === 'continuous' && 263 | this.scene.settings.autoStart === 'screen-top' 264 | ) { 265 | height += window.innerHeight; 266 | } 267 | 268 | // Use the largest value between existing container height, largest outpoint or calculated height. 269 | this.container.style.height = `${height}px`; 270 | } 271 | 272 | loadStyles(): void { 273 | const stylesheet = document.querySelector('style#pushin-styles'); 274 | 275 | if (!stylesheet) { 276 | const sheet = document.createElement('style'); 277 | sheet.id = 'pushin-styles'; 278 | 279 | sheet.appendChild(document.createTextNode(pushInStyles)); 280 | document.head.appendChild(sheet); 281 | 282 | this.cleanupFns.push(() => { 283 | document.head.removeChild(sheet); 284 | }); 285 | } 286 | } 287 | 288 | /** 289 | * Show a debugging tool appended to the frontend of the page. 290 | * Can be used to determine best "pushin-from" and "pushin-to" values. 291 | */ 292 | /* istanbul ignore next */ 293 | private showDebugger(): void { 294 | this.pushinDebug = document.createElement('div'); 295 | this.pushinDebug.classList.add('pushin-debug'); 296 | 297 | const scrollTitle = document.createElement('p'); 298 | scrollTitle.innerText = 'Pushin.js Debugger'; 299 | scrollTitle.classList.add('pushin-debug__title'); 300 | 301 | const debuggerContent = document.createElement('div'); 302 | debuggerContent.classList.add('pushin-debug__content'); 303 | debuggerContent.innerText = `Scroll position: ${this.scrollY}px`; 304 | 305 | this.pushinDebug.appendChild(scrollTitle); 306 | this.pushinDebug.appendChild(debuggerContent); 307 | 308 | const target = this.target?.container ?? document.body; 309 | 310 | target.appendChild(this.pushinDebug); 311 | 312 | // Remove debugger when unmounted. 313 | this.cleanupFns.push(() => this.pushinDebug?.remove()); 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface LayerOptions { 2 | inpoints: number[]; 3 | outpoints: number[]; 4 | speed: number; 5 | isFirst: boolean; 6 | isLast: boolean; 7 | transitions?: boolean; 8 | transitionStart?: number; 9 | transitionEnd?: number; 10 | tabInpoints?: number[]; 11 | } 12 | 13 | export interface LayerSettings { 14 | inpoints: number[]; 15 | outpoints: number[]; 16 | speed: number; 17 | isFirst: boolean; 18 | isLast: boolean; 19 | transitions?: boolean; 20 | transitionStart?: number; 21 | transitionEnd?: number; 22 | } 23 | 24 | export interface CompositionOptions { 25 | ratio?: number[]; 26 | } 27 | 28 | export interface SceneOptions { 29 | layerDepth?: number; 30 | breakpoints?: number[]; 31 | inpoints?: number[]; 32 | composition?: CompositionOptions; 33 | layers?: LayerOptions[]; 34 | ratio?: number[]; 35 | } 36 | 37 | export interface SceneSettings { 38 | breakpoints: number[]; 39 | inpoints: number[]; 40 | layers: LayerOptions[]; 41 | layerDepth?: number; 42 | composition?: CompositionOptions; 43 | ratio?: number[]; 44 | autoStart?: string; 45 | length?: number; 46 | } 47 | 48 | export interface PushInOptions { 49 | composition?: CompositionOptions; 50 | debug?: boolean; 51 | layers?: LayerOptions[]; 52 | scene?: SceneOptions; 53 | selector?: string; 54 | target?: string; 55 | scrollTarget?: string; 56 | mode?: string; 57 | autoStart?: string; 58 | length?: number; 59 | } 60 | 61 | export interface PushInSettings { 62 | mode: string; 63 | composition?: CompositionOptions; 64 | debug?: boolean; 65 | layers?: LayerOptions[]; 66 | selector?: string; 67 | target?: string; 68 | scrollTarget?: string; 69 | } 70 | 71 | export interface TargetSettings { 72 | target?: string; 73 | scrollTarget?: string; 74 | } 75 | 76 | export interface PushInLayer { 77 | container: HTMLElement; 78 | index: number; 79 | originalScale: number; 80 | } 81 | 82 | export interface LayerRef { 83 | inpoints: number[]; 84 | outpoints: number[]; 85 | speed: number; 86 | tabInpoints: number[]; 87 | } 88 | 89 | export interface LayerParams { 90 | depth: number; 91 | inpoint: number; 92 | outpoint: number; 93 | tabInpoint: number; 94 | overlap: number; 95 | speed: number; 96 | transitions: boolean; 97 | transitionStart: number; 98 | transitionEnd: number; 99 | } 100 | -------------------------------------------------------------------------------- /stories/A11y.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { A11y } from './A11y'; 4 | 5 | // More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export 6 | const meta = { 7 | title: 'PushIn/A11y', 8 | component: A11y, 9 | // More on argTypes: https://storybook.js.org/docs/react/api/argtypes 10 | argTypes: { 11 | mode: { control: 'select', options: ['sequential', 'continuous'] }, 12 | }, 13 | } as Meta; 14 | 15 | export default meta; 16 | 17 | export const Template: StoryObj = {}; 18 | 19 | export const A11yOptionsSequential: StoryObj = { 20 | args: { 21 | settings: { 22 | mode: 'sequential', 23 | layers: [ 24 | { 25 | tabInpoints: [100], 26 | }, 27 | ], 28 | debug: true, 29 | }, 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /stories/A11y.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useLayoutEffect } from 'react'; 2 | import { PushIn } from '../dist/esm/pushin'; 3 | 4 | export const A11y = ({ settings = {}, attrs = [] }) => { 5 | const pushInContainer = useRef(); 6 | 7 | useLayoutEffect(() => { 8 | const pushIn = new PushIn(pushInContainer.current, settings); 9 | pushIn.start(); 10 | 11 | return () => pushIn.destroy(); 12 | }); 13 | 14 | const layer1Attr = attrs.length > 0 ? attrs[0] : {}; 15 | const layer2Attr = attrs.length > 1 ? attrs[1] : {}; 16 | const layer3Attr = attrs.length > 2 ? attrs[2] : {}; 17 | 18 | return ( 19 |
20 |
21 |
22 | Lorem ipsum dolor sit amet consectetur, adipisicing elit. 23 |
24 |
25 | 26 | Mollitia hic non, at tempora saepe doloremque, voluptatem provident. 27 | 28 |
29 |
30 | 31 | Reprehenderit nisi perspiciatis facilis sint repudiandae totam 32 | praesentium dignissimos consequuntur tenetur. 33 | 34 |
35 |
36 |
37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /stories/AutoStart.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/react'; 2 | 3 | import { AutoStart } from './AutoStart'; 4 | 5 | // More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export 6 | const meta = { 7 | title: 'PushIn/AutoStart', 8 | component: AutoStart, 9 | // More on argTypes: https://storybook.js.org/docs/react/api/argtypes 10 | argTypes: {}, 11 | } as Meta; 12 | 13 | export default meta; 14 | 15 | export const Default = {}; 16 | 17 | export const ScreenTop = { 18 | args: { 19 | settings: { 20 | autoStart: 'screen-top', 21 | }, 22 | }, 23 | }; 24 | 25 | export const ScreenBottom = { 26 | args: { 27 | settings: { 28 | autoStart: 'screen-bottom', 29 | }, 30 | }, 31 | }; 32 | 33 | export const ScreenTopAttr = { 34 | args: { 35 | settings: { 36 | autoStart: 'screen-bottom', 37 | }, 38 | useAttr: 'screen-top', 39 | }, 40 | }; 41 | 42 | export const ScreenTopContinuous = { 43 | args: { 44 | settings: { 45 | autoStart: 'screen-top', 46 | mode: 'continuous', 47 | debug: true, 48 | }, 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /stories/AutoStart.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useLayoutEffect } from 'react'; 2 | import { PushIn } from '../dist/esm/pushin'; 3 | 4 | export const AutoStart = ({ settings, useAttr }) => { 5 | const pushInContainer = useRef(); 6 | 7 | useLayoutEffect(() => { 8 | const pushIn = new PushIn(pushInContainer.current, { 9 | target: '#target', 10 | scrollTarget: 'window', 11 | ...settings, 12 | }); 13 | pushIn.start(); 14 | 15 | return () => pushIn.destroy(); 16 | }); 17 | 18 | const pushInAttr = {}; 19 | if (useAttr) { 20 | pushInAttr['data-pushin-auto-start'] = useAttr; 21 | } 22 | 23 | return ( 24 |
25 |
31 |

AutoStart

32 |

33 | Scroll down to confirm that the animation starts just before the 34 | pushIn effect is visible on the screen. 35 |

36 |

Lorem Ipsum

37 |

38 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae 39 | incidunt et harum dignissimos alias similique tempore! Nemo quisquam 40 | tenetur cumque tempore porro velit libero odit dolorum eligendi 41 | maiores, eius magnam. 42 |

43 |

44 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae 45 | incidunt et harum dignissimos alias similique tempore! Nemo quisquam 46 | tenetur cumque tempore porro velit libero odit dolorum eligendi 47 | maiores, eius magnam. 48 |

49 |

Lorem Ipsum

50 |

51 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae 52 | incidunt et harum dignissimos alias similique tempore! Nemo quisquam 53 | tenetur cumque tempore porro velit libero odit dolorum eligendi 54 | maiores, eius magnam. 55 |

56 |

57 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae 58 | incidunt et harum dignissimos alias similique tempore! Nemo quisquam 59 | tenetur cumque tempore porro velit libero odit dolorum eligendi 60 | maiores, eius magnam. 61 |

62 |

Lorem Ipsum

63 |

64 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae 65 | incidunt et harum dignissimos alias similique tempore! Nemo quisquam 66 | tenetur cumque tempore porro velit libero odit dolorum eligendi 67 | maiores, eius magnam. 68 |

69 |

70 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae 71 | incidunt et harum dignissimos alias similique tempore! Nemo quisquam 72 | tenetur cumque tempore porro velit libero odit dolorum eligendi 73 | maiores, eius magnam. 74 |

75 |
76 |
77 |
78 |
79 |
80 |

Slide 1

81 |
82 |
83 |

Slide 2

84 |
85 |
86 |

Slide 3

87 |
88 |
89 |
90 |
91 |
92 | ); 93 | }; 94 | -------------------------------------------------------------------------------- /stories/Introduction.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/addon-docs'; 2 | 3 | 4 | 5 | 6 | # Welcome to PushIn.js 7 | 8 | Add stories to visually test common use cases of this plugin. 9 | 10 | JavaScript unit tests are also available in the tests directory. 11 | 12 | For more information about Storybook, head over to their comprehensive [documentation](https://storybook.js.org/docs/react/get-started/introduction). 13 | -------------------------------------------------------------------------------- /stories/Modes.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/react'; 2 | 3 | import { Modes } from './Modes'; 4 | 5 | // More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export 6 | const meta = { 7 | title: 'PushIn/Modes', 8 | component: Modes, 9 | // More on argTypes: https://storybook.js.org/docs/react/api/argtypes 10 | argTypes: { 11 | mode: { control: 'select', options: ['sequential', 'continuous'] }, 12 | }, 13 | } as Meta; 14 | 15 | export default meta; 16 | 17 | export const SequentialMode = { 18 | args: { 19 | options: { 20 | mode: 'sequential', 21 | layers: [], 22 | }, 23 | }, 24 | }; 25 | 26 | export const ContinuousMode = { 27 | args: { 28 | options: { 29 | mode: 'continuous', 30 | layers: [], 31 | }, 32 | }, 33 | }; 34 | 35 | export const ContinuousWithTransitions = { 36 | args: { 37 | options: { 38 | mode: 'continuous', 39 | layers: [ 40 | { 41 | transitions: true, 42 | }, 43 | { 44 | transitions: true, 45 | transitionStart: 300, 46 | transitionEnd: 300, 47 | }, 48 | { 49 | transitions: true, 50 | inpoints: [200], 51 | transitionStart: 300, 52 | transitionEnd: 300, 53 | }, 54 | ], 55 | }, 56 | }, 57 | }; 58 | 59 | export const SetModeByAttribute = { 60 | args: { 61 | options: { 62 | mode: '', 63 | layers: [], 64 | }, 65 | modeAttr: 'continuous', 66 | }, 67 | }; 68 | 69 | export const ContinuousWithLayerOutpoint = { 70 | args: { 71 | options: { 72 | mode: 'continuous', 73 | layers: [ 74 | { 75 | outpoints: [3000], 76 | }, 77 | ], 78 | debug: true, 79 | }, 80 | }, 81 | }; 82 | 83 | export const ContinuousWithLayerDepth = { 84 | args: { 85 | options: { 86 | mode: 'continuous', 87 | debug: true, 88 | scene: { 89 | layerDepth: 3500, 90 | }, 91 | }, 92 | }, 93 | }; 94 | 95 | export const ContinuousWithLength = { 96 | args: { 97 | options: { 98 | mode: 'continuous', 99 | debug: true, 100 | length: 4000, 101 | }, 102 | }, 103 | }; 104 | -------------------------------------------------------------------------------- /stories/Modes.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useLayoutEffect } from 'react'; 2 | import { PushIn } from '../dist/esm/pushin'; 3 | 4 | export const Modes = ({ options, modeAttr = '' }) => { 5 | const pushInContainer = useRef(); 6 | 7 | useLayoutEffect(() => { 8 | const pushIn = new PushIn(pushInContainer.current, options); 9 | pushIn.start(); 10 | 11 | return () => pushIn.destroy(); 12 | }); 13 | 14 | const pushInAttr = {}; 15 | if (modeAttr !== '') { 16 | pushInAttr['data-pushin-mode'] = modeAttr; 17 | } 18 | 19 | return ( 20 |
21 |
22 |
23 | 29 | Lorem ipsum dolor sit amet consectetur, adipisicing elit. 30 | 31 |
32 |
33 | 34 | Mollitia hic non, at tempora saepe doloremque, voluptatem provident. 35 | 36 |
37 |
38 | 44 | Reprehenderit nisi perspiciatis facilis sint repudiandae totam 45 | praesentium dignissimos consequuntur tenetur. 46 | 47 |
48 |
49 |
50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /stories/Target.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/react'; 2 | 3 | import { Target } from './Target'; 4 | 5 | // More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export 6 | const meta = { 7 | title: 'PushIn/Target', 8 | component: Target, 9 | argTypes: { 10 | scrollTarget: { control: 'text' }, 11 | }, 12 | } as Meta; 13 | 14 | export default meta; 15 | 16 | export const Default = {}; 17 | 18 | export const WindowScroll = { 19 | args: { 20 | scrollTarget: 'window', 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /stories/Target.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useLayoutEffect } from 'react'; 2 | import { PushIn } from '../dist/esm/pushin'; 3 | 4 | type PushInArgs = { 5 | target: string; 6 | scrollTarget?: string; 7 | }; 8 | 9 | export const Target = ({ scrollTarget }) => { 10 | const pushInContainer = useRef(); 11 | 12 | useLayoutEffect(() => { 13 | const args: PushInArgs = { target: '#target' }; 14 | 15 | if (scrollTarget) { 16 | args.scrollTarget = scrollTarget; 17 | } 18 | 19 | const pushIn = new PushIn(pushInContainer.current, args); 20 | pushIn.start(); 21 | 22 | return () => pushIn.destroy(); 23 | }); 24 | 25 | return ( 26 |
27 |

Target

28 |

29 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Natus esse 30 | ducimus libero tempora officiis, temporibus totam magnam laboriosam 31 | facilis quod dignissimos deleniti unde dolor sunt earum accusantium qui 32 | voluptate mollitia? 33 |

34 |
45 |
46 |
47 |
48 |

Lorem ipsum dolor sit amet consectetur.

49 |
50 |
51 |

52 | Adipisicing elit. Numquam nulla maiores suscipit, odio ad dolor. 53 |

54 |
55 |
56 |

57 | In recusandae culpa error beatae itaque et obcaecati sequi 58 | dolorem voluptates minima. Iure, sit non. 59 |

60 |
61 |
62 |
63 |
64 |

Lorem ipsum, dolor sit amet consectetur adipisicing elit

65 |

66 | Lorem ipsum dolor sit amet consectetur, adipisicing elit. Aspernatur ex 67 | nihil minus odit magnam neque fuga dicta debitis quidem qui hic, autem 68 | eligendi repellendus. Rerum placeat deleniti tempora commodi odio. 69 |

70 |
71 | ); 72 | }; 73 | -------------------------------------------------------------------------------- /stories/Text.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/react'; 2 | 3 | import { Text } from './Text'; 4 | 5 | // More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export 6 | const meta = { 7 | title: 'PushIn/Plain Text', 8 | component: Text, 9 | // More on argTypes: https://storybook.js.org/docs/react/api/argtypes 10 | argTypes: { 11 | layer1: { control: 'text' }, 12 | layer2: { control: 'text' }, 13 | layer3: { control: 'text' }, 14 | layer4: { control: 'text' }, 15 | }, 16 | } as Meta; 17 | 18 | export default meta; 19 | 20 | export const FormattedText = { 21 | args: { 22 | layer1: 23 | 'This example showcases formatted text with hyperlinks.', 24 | layer2: 25 | 'Layers should pass-through click events, allowing users to click on hyperlinks.', 26 | }, 27 | }; 28 | 29 | export const PlainText = { 30 | args: { 31 | layer1: 32 | 'This example demonstrates the simplest, zero-configuration setup for PushIn.js.', 33 | layer2: 34 | 'These layers are added to the HTML without any parameters and minimal CSS.', 35 | layer3: 36 | 'PushIn.js has configured the timing of this effect automatically to adjust for the number of layers.', 37 | layer4: 38 | 'The scroll length increases to accommodate all layers in an evenly-spaced animation.', 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /stories/Text.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useLayoutEffect } from 'react'; 2 | import { PushIn } from '../dist/esm/pushin'; 3 | 4 | export const Text = ({ layer1, layer2, layer3, layer4 }) => { 5 | const pushInContainer = useRef(); 6 | 7 | useLayoutEffect(() => { 8 | const pushIn = new PushIn(pushInContainer.current); 9 | pushIn.start(); 10 | 11 | return () => pushIn.destroy(); 12 | }); 13 | 14 | return ( 15 |
16 |
17 | {layer1 && ( 18 |
19 | 20 |
21 | )} 22 | {layer2 && ( 23 |
24 | 25 |
26 | )} 27 | {layer3 && ( 28 |
29 | 30 |
31 | )} 32 | {layer4 && ( 33 |
34 | 35 |
36 | )} 37 |
38 |
39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /stories/pushin.css: -------------------------------------------------------------------------------- 1 | .pushin { 2 | position: relative; 3 | } 4 | 5 | .pushin-scene { 6 | display: flex; 7 | align-items: center; 8 | 9 | position: fixed; 10 | left: 0; 11 | top: 0; 12 | 13 | width: 100%; 14 | height: 100vh; 15 | } 16 | 17 | .pushin-scene--with-target { 18 | top: 0; 19 | left: auto; 20 | height: auto; 21 | width: auto; 22 | pointer-events: none; 23 | overflow: hidden; 24 | position: sticky; 25 | } 26 | 27 | .pushin-scene--scroll-target-window { 28 | height: 100vh; 29 | } 30 | 31 | .pushin-composition { 32 | flex: 0 0 100%; 33 | padding-top: 201%; 34 | position: relative; 35 | } 36 | 37 | .pushin-layer { 38 | display: flex; 39 | align-items: center; 40 | flex-direction: column; 41 | justify-content: center; 42 | 43 | opacity: 0; 44 | pointer-events: none; 45 | 46 | position: absolute; 47 | top: 0; 48 | right: 0; 49 | bottom: 0; 50 | left: 0; 51 | } 52 | 53 | .pushin-layer--visible * { 54 | pointer-events: auto; 55 | } 56 | 57 | /* Debug */ 58 | .pushin-debug { 59 | background-color: white; 60 | border: 0; 61 | border-bottom: 1px; 62 | box-shadow: -2px 8px 19px 2px rgba(0, 0, 0, 0.26); 63 | padding: 1em; 64 | position: fixed; 65 | top: 0; 66 | width: 100%; 67 | -webkit-box-shadow: -2px 8px 19px 2px rgba(0, 0, 0, 0.26); 68 | z-index: 10; 69 | } 70 | 71 | @media (min-width: 768px) { 72 | .pushin-debug { 73 | border: 1px solid black; 74 | border-radius: 15px 0 0 15px; 75 | border-right: 0; 76 | right: 0; 77 | top: 50px; 78 | width: 250px; 79 | } 80 | } 81 | 82 | .pushin-debug__title { 83 | font-weight: bold; 84 | } 85 | -------------------------------------------------------------------------------- /test/__mocks__/layers.ts: -------------------------------------------------------------------------------- 1 | export const layerOptions = { 2 | inpoints: [100, 200], 3 | outpoints: [150, 250], 4 | speed: 50, 5 | isFirst: false, 6 | isLast: false, 7 | } 8 | 9 | export const layerParams = { 10 | depth: 1000, 11 | inpoint: 200, 12 | outpoint: 300, 13 | overlap: 0, 14 | speed: 8, 15 | transitions: true, 16 | transitionStart: 200, 17 | transitionEnd: 200, 18 | } 19 | -------------------------------------------------------------------------------- /test/__mocks__/scene.ts: -------------------------------------------------------------------------------- 1 | export const sceneOptions = { 2 | layerDepth: 1000, 3 | breakpoints: [375, 768, 1440], 4 | inpoints: [300, 600], 5 | layers: [], 6 | }; 7 | -------------------------------------------------------------------------------- /test/pushIn/destroy.spec.ts: -------------------------------------------------------------------------------- 1 | import { setupJSDOM } from '../setup'; 2 | import { PushIn } from '../../src/pushin'; 3 | 4 | describe('destroy', () => { 5 | let textContent: string; 6 | 7 | beforeEach(() => { 8 | textContent = 'FooBar'; 9 | setupJSDOM(` 10 | 11 | 12 |
13 |
${textContent}
14 |
15 | 16 | `); 17 | }); 18 | 19 | it('should remove event listeners once the `PushIn` is destroyed', () => { 20 | const spy = jest.spyOn(window, 'removeEventListener'); 21 | 22 | const container = document.querySelector('.pushin'); 23 | const pushIn = new PushIn(container); 24 | pushIn.start(); 25 | pushIn.destroy(); 26 | 27 | try { 28 | expect(spy).toHaveBeenCalled(); 29 | } finally { 30 | spy.mockRestore(); 31 | } 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test/pushIn/getScrollY.spec.ts: -------------------------------------------------------------------------------- 1 | import { setupJSDOM } from '../setup'; 2 | import { PushIn } from '../../src/pushin'; 3 | import { PushInTarget } from '../../src/pushInTarget'; 4 | 5 | describe('getScrollY', () => { 6 | let mockPushIn: PushIn; 7 | 8 | beforeEach(() => { 9 | setupJSDOM(` 10 | 11 | 12 | 13 | `); 14 | 15 | const mockPushInTarget = Object.create(PushInTarget.prototype); 16 | 17 | mockPushIn = Object.create(PushIn.prototype); 18 | Object.assign( 19 | mockPushIn, 20 | { 21 | target: mockPushInTarget, 22 | } 23 | ); 24 | }); 25 | 26 | it('Should return scrollTop of scrollTarget', () => { 27 | mockPushIn.target!['scrollTarget'] = { scrollTop: 15 }; 28 | const result = mockPushIn['getScrollY'](); 29 | expect(result).toEqual(15); 30 | }); 31 | 32 | it('Should return scrollY property of window', () => { 33 | mockPushIn.target!['scrollTarget'] = 'window'; 34 | 35 | window.scrollY = 20; 36 | const result = mockPushIn['getScrollY'](); 37 | expect(result).toEqual(20); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/pushIn/setScrollLength.spec.ts: -------------------------------------------------------------------------------- 1 | import { setupJSDOM } from '../setup'; 2 | import { PushIn } from '../../src/pushin'; 3 | import { PushInLayer } from '../../src/pushInLayer'; 4 | import { PushInScene } from '../../src/pushInScene'; 5 | import { PushInTarget } from '../../src/pushInTarget'; 6 | import { layerParams } from '../__mocks__/layers'; 7 | 8 | describe('setScrollLength', () => { 9 | let mockPushIn: PushIn; 10 | let container: HTMLElement; 11 | 12 | beforeEach(() => { 13 | setupJSDOM(` 14 | 15 | 16 |
17 |
18 |
19 | 20 | `); 21 | 22 | container = document.querySelector('.pushin'); 23 | 24 | const mockPushinLayer = Object.create(PushInLayer.prototype); 25 | mockPushinLayer['params'] = Object.create(layerParams); 26 | Object.assign( 27 | mockPushinLayer['params'], 28 | { 29 | depth: 1000, 30 | overlap: 100, 31 | } 32 | ); 33 | 34 | const layer1 = Object.create(mockPushinLayer); 35 | // First layer always has 0 overlap. 36 | layer1['params']['overlap'] = 0; 37 | 38 | const layer2 = Object.create(mockPushinLayer); 39 | const layer3 = Object.create(mockPushinLayer); 40 | layer3.params.outpoint = 2800; 41 | 42 | const mockScene = Object.create(PushInScene.prototype); 43 | Object.assign( 44 | mockScene, 45 | { 46 | layerDepth: 1000, 47 | layers: [ layer1, layer2, layer3 ], 48 | } 49 | ); 50 | 51 | // First layer always has 0 overlap 52 | mockScene.layers[0].params = Object.create(layerParams); 53 | mockScene.layers[0].params.overlap = 0; 54 | 55 | const mockTarget = Object.create(PushInTarget.prototype); 56 | Object.assign( 57 | mockTarget, 58 | { 59 | container: document.querySelector('.target'), 60 | height: 1000 61 | } 62 | ); 63 | 64 | mockPushIn = Object.create(PushIn.prototype); 65 | Object.assign( 66 | mockPushIn, 67 | { 68 | container, 69 | scene: mockScene, 70 | target: mockTarget 71 | } 72 | ); 73 | }); 74 | 75 | it('Should set the container height to its original value if it is higher than calculated value', () => { 76 | mockPushIn['setScrollLength'](); 77 | const result = container.style.height; 78 | expect(result).toEqual('5000px'); 79 | }); 80 | 81 | it('Should calculate container height based on the largest outpoint + target height', () => { 82 | container!.style!.height = '0'; 83 | 84 | mockPushIn['setScrollLength'](); 85 | const result = container.style.height; 86 | 87 | expect(result).toEqual( '3800px' ); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /test/pushInComposition/setRatio.spec.ts: -------------------------------------------------------------------------------- 1 | import { setupJSDOM } from '../setup'; 2 | import { PushInComposition } from '../../src/pushInComposition'; 3 | 4 | describe('setRatio', () => { 5 | let element: HTMLElement; 6 | let mockComposition: PushInComposition; 7 | 8 | beforeEach(() => { 9 | setupJSDOM(` 10 | 11 | 12 |
13 |
14 |
15 | 16 |
17 |
18 |
19 | 20 | `); 21 | 22 | element = document.querySelector('.pushin-composition'); 23 | mockComposition = Object.create(PushInComposition.prototype); 24 | Object.assign(mockComposition, { container: element, settings: {} }); 25 | }); 26 | 27 | it('Should not change padding by default', () => { 28 | mockComposition['setRatio'](); 29 | 30 | const result = element.style.paddingTop; 31 | expect(result).toBeNull; 32 | }); 33 | 34 | it('Should not change padding by default', () => { 35 | mockComposition['settings'].ratio = [16, 9]; 36 | mockComposition['setRatio'](); 37 | 38 | const result = element.style.paddingTop; 39 | expect(result).toEqual('56.25%'); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test/pushInLayer/getElementScaleX.spec.ts: -------------------------------------------------------------------------------- 1 | import { setupJSDOM } from '../setup'; 2 | import { PushInLayer } from '../../src/pushInLayer'; 3 | 4 | describe('getElementScaleX', () => { 5 | let element: HTMLElement; 6 | let mockPushInLayer: PushInLayer; 7 | jest.mock('../../src/pushInLayer', () => jest.fn()); 8 | 9 | beforeEach(() => { 10 | setupJSDOM(` 11 | 12 | 13 |
Hello World!
14 | 15 | `); 16 | 17 | element = document.querySelector('.foo'); 18 | mockPushInLayer = Object.create(PushInLayer.prototype); 19 | mockPushInLayer['container'] = element; 20 | }); 21 | 22 | it('Should return default element scale if never altered', () => { 23 | const result = mockPushInLayer['getElementScaleX'](element); 24 | expect(result).toEqual(1); 25 | }); 26 | 27 | it('Should return element scale if it was previously set', () => { 28 | element.style.transform = 'scale(5)'; 29 | const result = mockPushInLayer['getElementScaleX'](element); 30 | expect(result).toEqual(5); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/pushInLayer/getInpoints.spec.ts: -------------------------------------------------------------------------------- 1 | import { setupJSDOM } from '../setup'; 2 | import { PushInLayer } from '../../src/pushInLayer'; 3 | import { PushInScene } from '../../src/pushInScene'; 4 | import { layerParams } from '../__mocks__/layers'; 5 | 6 | describe('getInpoints', () => { 7 | let mockPushInLayer: PushInLayer; 8 | let mockPushInScene: PushInScene; 9 | 10 | beforeEach(() => { 11 | setupJSDOM(` 12 | 13 | 14 |
15 |
16 |
Layer 0
17 |
Layer 1
18 |
Layer 2
19 |
Layer 3
20 |
21 |
22 | 23 | `); 24 | 25 | const mockLayerParams = Object.create(layerParams); 26 | Object.assign( 27 | mockLayerParams, 28 | { 29 | overlap: 100, 30 | outpoint: 1000, 31 | } 32 | ); 33 | 34 | const mockLayer = Object.create(PushInLayer.prototype); 35 | mockLayer['params'] = mockLayerParams; 36 | 37 | mockPushInScene = Object.create(PushInScene.prototype); 38 | Object.assign( 39 | mockPushInScene, 40 | { 41 | getTop: () => 0, 42 | getInpoints: () => [10], 43 | layers: [ 44 | mockLayer, 45 | mockLayer, 46 | mockLayer, 47 | ], 48 | getMode: () => 'sequential', 49 | } 50 | ); 51 | 52 | mockPushInLayer = Object.create(mockLayer); 53 | mockPushInLayer['scene'] = mockPushInScene; 54 | 55 | // Stub functions 56 | Object.assign( 57 | mockPushInLayer, 58 | { 59 | getOverlap: () => mockLayerParams.overlap, 60 | } 61 | ); 62 | }); 63 | 64 | it('Should return 0 as the default for first layer in sequential mode', () => { 65 | const elem = document.querySelector('#layer-0'); 66 | const result = mockPushInLayer['getInpoints'](elem, 0); 67 | expect(result).toEqual([0]); 68 | }); 69 | 70 | it('Should return scene top value as the default for first layer in continuous mode', () => { 71 | const elem = document.querySelector('#layer-0'); 72 | mockPushInLayer['scene']['getMode'] = () => 'continuous'; 73 | const result = mockPushInLayer['getInpoints'](elem, 0); 74 | expect(result).toEqual([10]); 75 | }); 76 | 77 | it('Should return value provided by data attribute', () => { 78 | const elem = document.querySelector('#layer-1'); 79 | const result = mockPushInLayer['getInpoints'](elem, 1); 80 | expect(result).toEqual([300]); 81 | }); 82 | 83 | it('Should return array of values provided by data attribute', () => { 84 | const elem = document.querySelector('#layer-2'); 85 | const result = mockPushInLayer['getInpoints'](elem, 2); 86 | expect(result).toEqual([300, 500]); 87 | }); 88 | 89 | it('Should return generated value based on previous layer outpoint', () => { 90 | const elem = document.querySelector('#layer-3'); 91 | const result = mockPushInLayer['getInpoints'](elem, 3); 92 | expect(result).toEqual([900]); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /test/pushInLayer/getOutpoints.spec.ts: -------------------------------------------------------------------------------- 1 | import { setupJSDOM } from '../setup'; 2 | import { PushIn } from '../../src/pushin'; 3 | import { PushInScene } from '../../src/pushInScene'; 4 | import { PushInLayer } from '../../src/pushInLayer'; 5 | 6 | describe('getOutpoints', () => { 7 | let mockPushInLayer: PushInLayer; 8 | 9 | beforeEach(() => { 10 | setupJSDOM(` 11 | 12 | 13 |
14 |
15 |
Layer 0
16 |
Layer 1
17 |
Layer 2
18 |
Layer 3
19 |
20 |
21 | 22 | `); 23 | 24 | // Mock layer 25 | mockPushInLayer = Object.create(PushInLayer.prototype); 26 | 27 | // Mock scene 28 | mockPushInLayer['scene'] = Object.create(PushInScene); 29 | Object.assign( 30 | mockPushInLayer['scene'], 31 | { 32 | getMode: () => 'sequential', 33 | } 34 | ); 35 | 36 | // Mock PushIn 37 | mockPushInLayer['scene']['pushin'] = Object.create( PushIn ); 38 | Object.assign( 39 | mockPushInLayer['scene']['pushin'], 40 | { 41 | container: document.querySelector('.pushin'), 42 | } 43 | ); 44 | }); 45 | 46 | it('Should return inpoint + layerDepth by default for first layer', () => { 47 | mockPushInLayer['scene']['layerDepth'] = 300; 48 | 49 | const inpoint = 100; 50 | 51 | const elem = document.querySelector('#layer-0'); 52 | const result = mockPushInLayer['getOutpoints'](elem, inpoint); 53 | 54 | expect(result).toEqual([400]); 55 | }); 56 | 57 | it('Should return data-attribute value if set', () => { 58 | mockPushInLayer['scene']['layerDepth'] = 300; 59 | 60 | const inpoint = 100; 61 | 62 | const elem = document.querySelector('#layer-1'); 63 | const result = mockPushInLayer['getOutpoints'](elem, inpoint); 64 | 65 | expect(result).toEqual([300]); 66 | }); 67 | 68 | it('Should return array of data from data-attribute if set', () => { 69 | mockPushInLayer['scene']['layerDepth'] = 300; 70 | 71 | const inpoint = 100; 72 | 73 | const elem = document.querySelector('#layer-2'); 74 | const result = mockPushInLayer['getOutpoints'](elem, inpoint); 75 | 76 | expect(result).toEqual([300, 500]); 77 | }); 78 | 79 | it('Should generate value based on previous inpoint', () => { 80 | mockPushInLayer['scene']['layerDepth'] = 300; 81 | 82 | const inpoint = 500; 83 | 84 | const elem = document.querySelector('#layer-3'); 85 | const result = mockPushInLayer['getOutpoints'](elem, inpoint); 86 | 87 | expect(result).toEqual([800]); 88 | }); 89 | 90 | it('Should return -1 if outpoint not set and using "continuous" mode', () => { 91 | mockPushInLayer['scene']['getMode'] = () => 'continuous'; 92 | 93 | const elem = document.querySelector('#layer-0'); 94 | const result = mockPushInLayer['getOutpoints'](elem, 0); 95 | 96 | expect(result).toEqual([-1]); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /test/pushInLayer/getOverlap.spec.ts: -------------------------------------------------------------------------------- 1 | import { PushInLayer } from '../../src/pushInLayer'; 2 | import { PushInScene } from '../../src/pushInScene'; 3 | import { layerParams } from '../__mocks__/layers'; 4 | 5 | describe('getOverlap', () => { 6 | let mockPushInLayer: PushInLayer; 7 | 8 | beforeEach(() => { 9 | const mockLayer = Object.create(PushInLayer.prototype); 10 | mockLayer.params = Object.create(layerParams); 11 | 12 | const mockPushInScene = Object.create(PushInScene.prototype); 13 | mockPushInScene.layers = [ 14 | Object.create(mockLayer), 15 | Object.create(mockLayer), 16 | Object.create(mockLayer), 17 | ]; 18 | 19 | mockPushInLayer = Object.create(PushInLayer.prototype); 20 | Object.assign( 21 | mockPushInLayer, 22 | { 23 | index: 0, 24 | scene: mockPushInScene, 25 | params: Object.create(layerParams), 26 | getTransitionStart: layerParams.transitionStart, 27 | } 28 | ); 29 | }); 30 | 31 | it('Should return 0 for first layer', () => { 32 | const result = mockPushInLayer['getOverlap'](); 33 | 34 | expect(result).toEqual(0); 35 | }); 36 | 37 | it('Should calculate based on average transition lengths of previous layer and current layer', () => { 38 | mockPushInLayer['index'] = 1; 39 | mockPushInLayer['getTransitionStart'] = () => 250; 40 | mockPushInLayer.scene.layers[0]['params'].transitionEnd = 200; 41 | const result = mockPushInLayer['getOverlap'](); 42 | 43 | expect(result).toEqual(112.5); 44 | }); 45 | 46 | it('Should not exceed current layer transition start', () => { 47 | mockPushInLayer['index'] = 1; 48 | mockPushInLayer['getTransitionStart'] = () => 20; 49 | mockPushInLayer.scene.layers[0]['params'].transitionEnd = 500; 50 | const result = mockPushInLayer['getOverlap'](); 51 | 52 | expect(result).toEqual(20); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/pushInLayer/getScaleValue.spec.ts: -------------------------------------------------------------------------------- 1 | import { setupJSDOM } from '../setup'; 2 | import { PushInLayer } from '../../src/pushInLayer'; 3 | import { PushInScene } from '../../src/pushInScene'; 4 | import { PushIn } from '../../src/pushin'; 5 | import { LayerParams } from '../../src/types'; 6 | 7 | describe('getScaleValue', () => { 8 | let layerMock: PushInLayer; 9 | let layerMock2: PushInLayer; 10 | let mockPushInLayer: PushInLayer; 11 | 12 | beforeEach(() => { 13 | setupJSDOM(``); 14 | layerMock = Object.create(PushInLayer.prototype); 15 | layerMock['originalScale'] = 2; 16 | layerMock['params'] = { 17 | inpoint: 10, 18 | speed: 2, 19 | }; 20 | 21 | layerMock2 = Object.create(PushInLayer.prototype); 22 | layerMock2['originalScale'] = 1; 23 | layerMock2['params'] = { 24 | inpoint: 150, 25 | speed: 100, 26 | }; 27 | 28 | mockPushInLayer = Object.create(PushInLayer.prototype); 29 | mockPushInLayer['scene'] = Object.create(PushInScene.prototype); 30 | mockPushInLayer['scene']['pushin'] = {scrollY:0}; 31 | }); 32 | 33 | it('should return original scale if scroll position and inpoint are the same', () => { 34 | mockPushInLayer['scene']['pushin']['scrollY'] = 10; 35 | const result = mockPushInLayer['getScaleValue'](layerMock); 36 | 37 | expect(result).toEqual(2); 38 | }); 39 | 40 | it('should reduce scale if scrollY is less than inpoint', () => { 41 | mockPushInLayer['scene']['pushin']['scrollY'] = 6; 42 | const result = mockPushInLayer['getScaleValue'](layerMock); 43 | 44 | expect(result).toEqual(1.9992); 45 | }); 46 | 47 | it('should increase scale if scrollY is greater than inpoint', () => { 48 | mockPushInLayer['scene']['pushin']['scrollY'] = 20; 49 | const result = mockPushInLayer['getScaleValue'](layerMock); 50 | 51 | expect(result).toEqual(2.002); 52 | }); 53 | 54 | it('should not return a negative number', () => { 55 | mockPushInLayer['scene']['pushin']['scrollY'] = 1; 56 | const result = mockPushInLayer['getScaleValue'](layerMock2); 57 | 58 | expect(result).toEqual(0); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /test/pushInLayer/getSpeed.spec.ts: -------------------------------------------------------------------------------- 1 | import { setupJSDOM } from '../setup'; 2 | import { PushInLayer } from '../../src/pushInLayer'; 3 | import { DEFAULT_SPEED } from '../../src/constants'; 4 | import { layerOptions } from '../__mocks__/layers'; 5 | 6 | describe('getSpeed', () => { 7 | let mockPushInLayer: PushInLayer; 8 | 9 | beforeEach(() => { 10 | setupJSDOM(` 11 | 12 | 13 |
14 |
15 |
Layer 0
16 |
Layer 1
17 |
Layer 2
18 |
19 |
20 | 21 | `); 22 | 23 | mockPushInLayer = Object.create(PushInLayer.prototype); 24 | }); 25 | 26 | it('Should return 8 by default', () => { 27 | const elem = document.querySelector('#layer-0'); 28 | const result = mockPushInLayer['getSpeed'](elem); 29 | expect(result).toEqual(DEFAULT_SPEED); 30 | }); 31 | 32 | it('Should return integer value from data-pushin-speed attribute', () => { 33 | const elem = document.querySelector('#layer-1'); 34 | const result = mockPushInLayer['getSpeed'](elem); 35 | expect(result).toEqual(50); 36 | }); 37 | 38 | it('Should return default if NaN', () => { 39 | const elem = document.querySelector('#layer-2'); 40 | const result = mockPushInLayer['getSpeed'](elem); 41 | expect(result).toEqual(DEFAULT_SPEED); 42 | }); 43 | 44 | it('Should use javascript API', () => { 45 | const elem = document.querySelector('#layer-0'); 46 | const settings = Object.apply( 47 | {}, 48 | layerOptions 49 | ); 50 | settings.speed = 10; 51 | mockPushInLayer['settings'] = settings; 52 | const result = mockPushInLayer['getSpeed'](elem); 53 | expect(result).toEqual(10); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /test/pushInLayer/isActive.spec.ts: -------------------------------------------------------------------------------- 1 | import { setupJSDOM } from '../setup'; 2 | import { PushIn } from '../../src/pushin'; 3 | import { PushInLayer } from '../../src/pushInLayer'; 4 | import { PushInScene } from '../../src/pushInScene'; 5 | import { LayerParams } from '../../src/types'; 6 | import { layerParams } from '../__mocks__/layers'; 7 | 8 | describe('isActive', () => { 9 | let mockPushInLayer: PushInLayer; 10 | 11 | beforeEach(() => { 12 | setupJSDOM(``); 13 | const mockLayerParams = Object.create(layerParams); 14 | Object.assign( 15 | mockLayerParams, 16 | { 17 | inpoint: 10, 18 | outpoint: 20 19 | }, 20 | ); 21 | 22 | mockPushInLayer = Object.create(PushInLayer.prototype); 23 | Object.assign( 24 | mockPushInLayer, 25 | { 26 | 'params': mockLayerParams, 27 | 'scene': Object.create(PushInScene.prototype), 28 | }); 29 | 30 | mockPushInLayer['scene']['pushin'] = Object.create(PushIn.prototype); 31 | }); 32 | 33 | it('should be true if screen top is greater than inpoint and less than outpoint', () => { 34 | mockPushInLayer['scene']['pushin']['scrollY'] = 15; 35 | const result = mockPushInLayer['isActive'](); 36 | 37 | expect(result).toEqual(true); 38 | }); 39 | 40 | it('should be true if screen top is equal to inpoint', () => { 41 | mockPushInLayer['scene']['pushin']['scrollY'] = 10; 42 | const result = mockPushInLayer['isActive'](); 43 | 44 | expect(result).toEqual(true); 45 | }); 46 | 47 | it('should be true if screen top is equal to outpoint', () => { 48 | mockPushInLayer['scene']['pushin']['scrollY'] = 20; 49 | const result = mockPushInLayer['isActive'](); 50 | 51 | expect(result).toEqual(true); 52 | }); 53 | 54 | it('should be false if screen top is less than inpoint', () => { 55 | mockPushInLayer['scene']['pushin']['scrollY'] = 5; 56 | const result = mockPushInLayer['isActive'](); 57 | 58 | expect(result).toEqual(false); 59 | }); 60 | 61 | it('should be false if screen top is greater than outpoint', () => { 62 | mockPushInLayer['scene']['pushin']['scrollY'] = 25; 63 | const result = mockPushInLayer['isActive'](); 64 | 65 | expect(result).toEqual(false); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /test/pushInLayer/setLayerStyle.spec.ts: -------------------------------------------------------------------------------- 1 | import { setupJSDOM } from '../setup'; 2 | import { PushInLayer } from '../../src/pushInLayer'; 3 | import { PushIn } from '../../src/pushin'; 4 | import { PushInScene } from '../../src/pushInScene'; 5 | import { LayerParams } from '../../src/types'; 6 | import { layerParams } from '../__mocks__/layers'; 7 | 8 | describe('setLayerStyle', () => { 9 | let mockPushInLayer: PushInLayer; 10 | let element: HTMLElement; 11 | 12 | beforeEach(() => { 13 | setupJSDOM(` 14 | 15 | 16 |
17 | Lorem Ipsum 18 |
19 | 20 | `); 21 | 22 | element = document.querySelector('.pushin-layer'); 23 | 24 | const mockPushinParams = Object.create(layerParams); 25 | Object.assign( 26 | mockPushinParams, 27 | { 28 | inpoint: 200, 29 | outpoint: 500, 30 | transitionStart: 50, 31 | transitionEnd: 50, 32 | } 33 | ); 34 | 35 | const mockScene = Object.create(PushInScene.prototype); 36 | 37 | mockPushInLayer = Object.create(PushInLayer.prototype); 38 | Object.assign( 39 | mockPushInLayer, 40 | { 41 | container: document.querySelector('.pushin-layer'), 42 | index: 0, 43 | 'params': mockPushinParams, 44 | 'scene': mockScene, 45 | }); 46 | 47 | const mockPushIn = Object.create(PushIn.prototype); 48 | Object.assign( 49 | mockPushIn, 50 | { 51 | scrollY: 0, 52 | } 53 | ); 54 | 55 | Object.assign( 56 | mockPushInLayer['scene'], 57 | { 58 | pushin: mockPushIn, 59 | layers: [ 60 | Object.create(PushInLayer.prototype), 61 | Object.create(PushInLayer.prototype), 62 | Object.create(PushInLayer.prototype) 63 | ], 64 | } 65 | ); 66 | 67 | // stub methods 68 | Object.assign( 69 | mockPushInLayer, 70 | { 71 | isActive: () => true, 72 | setScale: () => null, 73 | getScaleValue: () => null, 74 | } 75 | ); 76 | }); 77 | 78 | it('should set opacity to 1 if its the first layer, scroll position is before inpoint, and transitionStart is -1', () => { 79 | mockPushInLayer['scene']['pushin']['scrollY'] = 10; 80 | mockPushInLayer['params']['transitionStart'] = -1; 81 | mockPushInLayer['setLayerStyle'](); 82 | const result = element.style.opacity; 83 | 84 | expect(result).toEqual('1'); 85 | }); 86 | 87 | it('should set opacity to 1 if its the first layer and it is active', () => { 88 | mockPushInLayer['scene']['pushin']['scrollY'] = 251; 89 | mockPushInLayer['setLayerStyle'](); 90 | const result = element.style.opacity; 91 | 92 | expect(result).toEqual('1'); 93 | }); 94 | 95 | it('should set opacity to 1 if its the last layer, scroll position is after outpoint, and transitionEnd is -1', () => { 96 | mockPushInLayer['scene']['pushin']['scrollY'] = 600; 97 | mockPushInLayer['params']['transitionEnd'] = -1; 98 | mockPushInLayer['index'] = 2; 99 | mockPushInLayer['setLayerStyle'](); 100 | 101 | const result = element.style.opacity; 102 | 103 | expect(result).toEqual('1'); 104 | }); 105 | 106 | it('should set opacity to 1 if its the last layer and it is active', () => { 107 | mockPushInLayer['scene']['pushin']['scrollY'] = 440; 108 | mockPushInLayer['index'] = 2; 109 | mockPushInLayer['setLayerStyle'](); 110 | 111 | const result = element.style.opacity; 112 | 113 | expect(result).toEqual('1'); 114 | }); 115 | 116 | it('should set opacity to 0 if its a middle layer and scroll position is exactly equal to its inpoint', () => { 117 | mockPushInLayer['scene']['pushin']['scrollY'] = 200; 118 | mockPushInLayer['index'] = 1; 119 | mockPushInLayer['setLayerStyle'](); 120 | 121 | const result = element.style.opacity; 122 | 123 | expect(result).toEqual('0'); 124 | }); 125 | 126 | it('should set opacity to 0.5 if its a middle layer and scroll position is halfway through transitionStart', () => { 127 | mockPushInLayer['scene']['pushin']['scrollY'] = 225; 128 | mockPushInLayer['index'] = 1; 129 | mockPushInLayer['setLayerStyle'](); 130 | 131 | const result = element.style.opacity; 132 | 133 | expect(result).toEqual('0.5'); 134 | }); 135 | 136 | it('should set opacity to 0 if its a middle layer and scroll position is exactly equal to its outpoint', () => { 137 | mockPushInLayer['scene']['pushin']['scrollY'] = 500; 138 | mockPushInLayer['index'] = 1; 139 | mockPushInLayer['setLayerStyle'](); 140 | 141 | const result = element.style.opacity; 142 | 143 | expect(result).toEqual('0'); 144 | }); 145 | 146 | it('should set opacity to 0.5 if its a middle layer and scroll position is halfway through transitionEnd', () => { 147 | mockPushInLayer['scene']['pushin']['scrollY'] = 475; 148 | mockPushInLayer['index'] = 1; 149 | mockPushInLayer['setLayerStyle'](); 150 | 151 | const result = element.style.opacity; 152 | 153 | expect(result).toEqual('0.5'); 154 | }); 155 | 156 | it('should set opacity to 1 if transitions disabled and scroll point is before inpoint', () => { 157 | mockPushInLayer['scene']['pushin']['scrollY'] = 100; 158 | mockPushInLayer['params'].transitions = false; 159 | mockPushInLayer['index'] = 1; 160 | mockPushInLayer['setLayerStyle'](); 161 | 162 | const result = element.style.opacity; 163 | 164 | expect(result).toEqual('1'); 165 | }); 166 | 167 | it('should set opacity to 1 if transitions disabled and scroll point is after outpoint', () => { 168 | mockPushInLayer['scene']['pushin']['scrollY'] = 600; 169 | mockPushInLayer['params'].transitions = false; 170 | mockPushInLayer['index'] = 1; 171 | mockPushInLayer['setLayerStyle'](); 172 | 173 | const result = element.style.opacity; 174 | 175 | expect(result).toEqual('1'); 176 | }); 177 | 178 | it('should set opacity to 1 if transitionStart is -1 and scroll point is before inpoint', () => { 179 | mockPushInLayer['scene']['pushin']['scrollY'] = 100; 180 | mockPushInLayer['params'].transitionStart = -1; 181 | mockPushInLayer['index'] = 1; 182 | mockPushInLayer['setLayerStyle'](); 183 | 184 | const result = element.style.opacity; 185 | 186 | expect(result).toEqual('1'); 187 | }); 188 | 189 | it('should set opacity to 1 if transitionEnd is -1 and scroll point is after outpoint', () => { 190 | mockPushInLayer['scene']['pushin']['scrollY'] = 600; 191 | mockPushInLayer['params'].transitionEnd = -1; 192 | mockPushInLayer['index'] = 1; 193 | mockPushInLayer['setLayerStyle'](); 194 | 195 | const result = element.style.opacity; 196 | 197 | expect(result).toEqual('1'); 198 | }); 199 | }); 200 | -------------------------------------------------------------------------------- /test/pushInLayer/setScale.spec.ts: -------------------------------------------------------------------------------- 1 | import { setupJSDOM } from '../setup'; 2 | import { PushInLayer } from '../../src/pushInLayer'; 3 | 4 | describe('setScale', () => { 5 | beforeEach(() => { 6 | setupJSDOM(` 7 | 8 | 9 |
Hello World
10 | 11 | `); 12 | }); 13 | 14 | it('should set element scale value', () => { 15 | const mockPushInLayer = Object.create(PushInLayer.prototype); 16 | const element = document.querySelector('.foo'); 17 | mockPushInLayer.container = element; 18 | mockPushInLayer['setScale'](10); 19 | const result = element.style.transform; 20 | expect(result).toEqual('scale(10)'); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/pushInLayer/setZIndex.spec.ts: -------------------------------------------------------------------------------- 1 | import { setupJSDOM } from '../setup'; 2 | import { PushInLayer } from '../../src/pushInLayer'; 3 | 4 | describe('setZIndex', () => { 5 | let mockPushInLayer: PushInLayer; 6 | 7 | beforeEach(() => { 8 | setupJSDOM(` 9 | 10 | 11 |
12 |
13 |
Layer 0
14 |
Layer 1
15 |
Layer 2
16 |
17 |
18 | 19 | `); 20 | 21 | mockPushInLayer = Object.create(PushInLayer.prototype); 22 | mockPushInLayer['container'] = document.querySelector('#layer-1'); 23 | mockPushInLayer['index'] = 1; 24 | }); 25 | 26 | it('Should return the difference between the total number of layers and the current layer index', () => { 27 | mockPushInLayer['setZIndex'](3); 28 | const result = mockPushInLayer['container'].style.zIndex; 29 | expect(result).toEqual('2'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/pushInScene/getBreakpointIndex.spec.ts: -------------------------------------------------------------------------------- 1 | import { setupJSDOM } from '../setup'; 2 | import { PushInScene } from '../../src/pushInScene'; 3 | 4 | describe('getBreakpointIndex', () => { 5 | beforeEach(() => { 6 | setupJSDOM(` 7 | 8 | 9 |
10 |
Text
11 |
12 | 13 | `); 14 | const container = document.querySelector('.pushin'); 15 | }); 16 | 17 | it('Should return 0 by default', () => { 18 | const result = PushInScene.prototype.getBreakpointIndex([]); 19 | expect(result).toEqual(0); 20 | }); 21 | 22 | it('Should return the index of the nearest breakpoint that is less than current window width', () => { 23 | window.innerWidth = 800; 24 | const result = PushInScene.prototype.getBreakpointIndex([0, 768, 1440, 1920]); 25 | expect(result).toEqual(1); 26 | }); 27 | 28 | it('Should return the index of a breakpoint that matches the current window width', () => { 29 | window.innerWidth = 1440; 30 | const result = PushInScene.prototype.getBreakpointIndex([0, 768, 1440, 1920]); 31 | expect(result).toEqual(2); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test/pushInScene/getLayers.spec.ts: -------------------------------------------------------------------------------- 1 | import { setupJSDOM } from '../setup'; 2 | import { PushInScene } from '../../src/pushInScene'; 3 | import { PushInLayer } from '../../src/pushInLayer'; 4 | import { layerOptions } from '../__mocks__/layers'; 5 | import { sceneOptions } from '../__mocks__/scene'; 6 | 7 | jest.mock('../../src/pushInLayer'); 8 | 9 | describe('getLayers', () => { 10 | let mockPushInScene: PushInScene; 11 | let layers: HTMLElement[]; 12 | 13 | beforeEach(() => { 14 | (PushInLayer as jest.Mock).mockClear(); 15 | setupJSDOM(` 16 | 17 | 18 |
19 |
20 |
Layer 0
21 |
Layer 1
22 |
Layer 2
23 |
Layer 3
24 |
25 |
26 | 27 | `); 28 | 29 | mockPushInScene = Object.create(PushInScene.prototype); 30 | mockPushInScene['container'] = document.querySelector('.pushin-scene'); 31 | mockPushInScene['layers'] = []; 32 | 33 | layers = [ ...document.querySelectorAll('.pushin-layer')]; 34 | }); 35 | 36 | it('Should set the layers property to contain all .pushin-layer elements', () => { 37 | mockPushInScene['getLayers'](); 38 | expect(mockPushInScene['layers'].length).toEqual(layers.length); 39 | }); 40 | 41 | it('Should create pushInLayer with correct arguments', () => { 42 | mockPushInScene['getLayers'](); 43 | const mockOptions = { 44 | isFirst: false, 45 | isLast: false, 46 | }; 47 | expect(PushInLayer).toHaveBeenCalledWith(layers[1], 1, mockPushInScene, mockOptions); 48 | }); 49 | 50 | it('Should set layer settings from the JavaScript API', () => { 51 | const testLayerOptions = Object.assign({}, layerOptions); 52 | testLayerOptions.inpoints = [1000, 2000]; 53 | 54 | mockPushInScene['settings'] = sceneOptions; 55 | mockPushInScene['settings'].layers = [ 56 | { ...layerOptions, isFirst: true }, 57 | testLayerOptions, 58 | { ...layerOptions }, 59 | { ...layerOptions, isLast: true }, 60 | ]; 61 | 62 | mockPushInScene['getLayers'](); 63 | 64 | const expectedElement = layers[1]; 65 | const expectedIndex = 1; 66 | const expectedParent = mockPushInScene; 67 | const expectedOptions = testLayerOptions; 68 | 69 | expect(PushInLayer).toHaveBeenNthCalledWith(2, expectedElement, expectedIndex, expectedParent, expectedOptions); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/pushInScene/resize.spec.ts: -------------------------------------------------------------------------------- 1 | import { setupJSDOM } from '../setup'; 2 | import { PushIn } from '../../src/pushin'; 3 | import { PushInScene } from '../../src/pushInScene'; 4 | import { PushInTarget } from '../../src/pushInTarget'; 5 | 6 | describe('resize', () => { 7 | let mockPushInScene; 8 | let sceneContainer; 9 | 10 | beforeEach(() => { 11 | setupJSDOM(` 12 | 13 | 14 |
15 |
16 |
17 |
18 |
19 | 20 | `); 21 | 22 | sceneContainer = document.querySelector('.pushin-scene'); 23 | 24 | const mockPushInTarget = Object.create(PushInTarget.prototype); 25 | Object.assign( 26 | mockPushInTarget, 27 | { 28 | container: document.getElementById('target'), 29 | scrollTarget: 'window' 30 | } 31 | ); 32 | 33 | const mockPushIn = Object.create(PushIn.prototype); 34 | Object.assign( 35 | mockPushIn, 36 | { 37 | container: document.querySelector('.pushin'), 38 | target: mockPushInTarget, 39 | } 40 | ); 41 | 42 | mockPushInScene = Object.create(PushInScene.prototype); 43 | Object.assign( 44 | mockPushInScene, 45 | { 46 | container: sceneContainer, 47 | pushin: mockPushIn, 48 | } 49 | ); 50 | }); 51 | 52 | it('Should not adjust container size by default (scrollTarget = "window"', () => { 53 | mockPushInScene['resize'](); 54 | const result = sceneContainer.style.height; 55 | expect(result).toEqual(''); 56 | }); 57 | 58 | it('Should should set width and height of scene to match the target container', () => { 59 | mockPushInScene['pushin']['target']['scrollTarget'] = document.getElementById('target'); 60 | mockPushInScene['pushin']['target']['container']['getBoundingClientRect'] = () => { 61 | return { 62 | width: 100, 63 | height: 200, 64 | }; 65 | }; 66 | mockPushInScene['pushin']['scrollTarget'] = mockPushInScene['pushin']['target']; 67 | 68 | mockPushInScene['resize'](); 69 | const result = [sceneContainer.style.width, sceneContainer.style.height]; 70 | 71 | expect(result).toEqual(['100px', '200px']); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /test/pushInScene/setBreakpoints.spec.ts: -------------------------------------------------------------------------------- 1 | import { setupJSDOM } from '../setup'; 2 | import { PushInScene } from '../../src/pushInScene'; 3 | import { SceneSettings } from '../../src/types'; 4 | import { PUSH_IN_DEFAULT_BREAKPOINTS } from '../../src/constants'; 5 | 6 | describe('setBreakpoints', () => { 7 | let mockPushInScene: PushInScene; 8 | 9 | beforeEach(() => { 10 | setupJSDOM(` 11 | 12 | 13 |
14 |
15 |
16 | 17 | `); 18 | 19 | mockPushInScene = Object.create(PushInScene.prototype); 20 | mockPushInScene['container'] = document.querySelector('.pushin-scene'); 21 | mockPushInScene['settings'] = { 22 | breakpoints: [], 23 | inpoints: [], 24 | }; 25 | }); 26 | 27 | it('Should set the default breakpoints', () => { 28 | mockPushInScene['setBreakpoints'](); 29 | const result = mockPushInScene['settings'].breakpoints; 30 | const expected = [0, ...PUSH_IN_DEFAULT_BREAKPOINTS]; 31 | expect(result).toEqual(expected); 32 | }); 33 | 34 | it('Should set the breakpoints provided by data-attributes', () => { 35 | mockPushInScene['container'].setAttribute('data-pushin-breakpoints', '1,2,3'); 36 | mockPushInScene['setBreakpoints'](); 37 | const result = mockPushInScene['settings'].breakpoints; 38 | const expected = [0, 1, 2, 3]; 39 | expect(result).toEqual(expected); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test/pushInTarget/setScrollTarget.spec.ts: -------------------------------------------------------------------------------- 1 | import { setupJSDOM } from '../setup'; 2 | import { PushIn } from '../../src/pushin'; 3 | import { PushInTarget } from '../../src/pushInTarget'; 4 | 5 | describe('setScrollTarget', () => { 6 | let mockPushInTarget: PushInTarget; 7 | 8 | beforeEach(() => { 9 | setupJSDOM(` 10 | 11 | 12 |
13 |
14 |
15 |
16 | 17 | `); 18 | 19 | const mockPushIn = Object.create(PushIn.prototype); 20 | Object.assign( 21 | mockPushIn, 22 | { 23 | container: document.querySelector('.pushin'), 24 | } 25 | ); 26 | 27 | mockPushInTarget = Object.create(PushInTarget.prototype); 28 | Object.assign( 29 | mockPushInTarget, 30 | { 31 | container: null, 32 | scrollTarget: 'window', 33 | height: 0, 34 | settings: {}, 35 | pushin: mockPushIn 36 | } 37 | ); 38 | }); 39 | 40 | it('Should return "window" by default', () => { 41 | mockPushInTarget['setScrollTarget'](); 42 | expect(mockPushInTarget['scrollTarget']).toEqual('window'); 43 | }); 44 | 45 | it('Should return scrollTarget from HTML Attribute', () => { 46 | document.querySelector('.pushin')?.setAttribute('data-pushin-scroll-target', '#target'); 47 | mockPushInTarget['setScrollTarget'](); 48 | const expected = document.querySelector('#target'); 49 | expect(mockPushInTarget['scrollTarget']).toEqual(expected); 50 | }); 51 | 52 | it('Should return scrollTarget from JavasScript API', () => { 53 | mockPushInTarget.settings.scrollTarget = '#target'; 54 | mockPushInTarget['setScrollTarget'](); 55 | const expected = document.querySelector('#target'); 56 | expect(mockPushInTarget['scrollTarget']).toEqual(expected); 57 | }); 58 | 59 | it('Should fall back to target element', () => { 60 | const expected = document.querySelector('#target'); 61 | mockPushInTarget['container'] = expected; 62 | mockPushInTarget['setScrollTarget'](); 63 | expect(mockPushInTarget['scrollTarget']).toEqual(expected); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /test/pushInTarget/setTargetHeight.spec.ts: -------------------------------------------------------------------------------- 1 | import { setupJSDOM } from '../setup'; 2 | import { PushInTarget } from '../../src/pushInTarget'; 3 | 4 | describe('setTargetHeight', () => { 5 | let mockPushInTarget: PushInTarget; 6 | let target: HTMLElement; 7 | 8 | beforeEach(() => { 9 | setupJSDOM(` 10 | 11 | 12 |
13 | 14 | `); 15 | 16 | target = document.querySelector('.target'); 17 | 18 | window.innerHeight = 2000; 19 | 20 | mockPushInTarget = Object.create(PushInTarget.prototype); 21 | }); 22 | 23 | it('Should use the window height by default', () => { 24 | mockPushInTarget['setTargetHeight'](); 25 | expect(mockPushInTarget['height']).toEqual(2000); 26 | }); 27 | 28 | it('Should match computed target height', () => { 29 | mockPushInTarget['container'] = target; 30 | mockPushInTarget['setTargetHeight'](); 31 | expect(mockPushInTarget['height']).toEqual(1000); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test/setup.ts: -------------------------------------------------------------------------------- 1 | import { DOMWindow, JSDOM } from 'jsdom'; 2 | 3 | // `globalThis.window` refers to `typeof globalThis`, but `jsdom.DOMWindow` isn't assignable to `global.window`. 4 | const _global = global as unknown as Omit & { 5 | window?: DOMWindow | undefined; 6 | }; 7 | 8 | export function setupJSDOM(html: string) { 9 | const dom = new JSDOM(html); 10 | _global.window = dom.window; 11 | _global.document = window.document; 12 | _global.getComputedStyle = window.getComputedStyle; 13 | _global.cancelAnimationFrame = () => {}; 14 | return dom; 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2015", 4 | "module": "ES2015", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "types": [] 9 | }, 10 | "include": ["src/*.ts"], 11 | "exclude": ["dist"] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "strict": false, 5 | "module": "CommonJS", 6 | "types": ["node", "jest"] 7 | }, 8 | "include": ["test/*.spec.ts"] 9 | } 10 | --------------------------------------------------------------------------------