├── .ci ├── .env.example └── Taskfile.yaml ├── .dev ├── .env.example ├── Taskfile.yaml ├── UnitTestUi.Dockerfile └── docker-compose.yaml ├── .github ├── README.md ├── images │ └── performance.png └── workflows │ ├── deploy.yml │ └── test-and-build.yml ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── docs ├── .gitignore ├── bartholomeow.html ├── e2e.html ├── e2e │ ├── ScreenShot.test.ts │ └── ScreenShot.test.ts-snapshots │ │ ├── 0-1-chromium-linux.png │ │ ├── 0-1-firefox-linux.png │ │ ├── 0-1-webkit-linux.png │ │ ├── 1000-1-chromium-linux.png │ │ ├── 1000-1-firefox-linux.png │ │ ├── 1000-1-webkit-linux.png │ │ ├── 3000-1-chromium-linux.png │ │ ├── 3000-1-firefox-linux.png │ │ ├── 3000-1-webkit-linux.png │ │ ├── 500-1-chromium-linux.png │ │ ├── 500-1-firefox-linux.png │ │ └── 500-1-webkit-linux.png ├── env.d.ts ├── gsap.html ├── index.html ├── package.json ├── playwright.config.ts ├── postcss.config.cjs ├── public │ ├── bartholomeow.png │ ├── example.glb │ ├── vite.svg │ ├── worm-ball.png │ └── worm-bg.png ├── src │ ├── bartholomeow.css │ ├── bartholomeow.ts │ ├── e2e.ts │ ├── gsap.css │ └── gsap.ts ├── tests.html ├── tsconfig.json ├── types.html ├── vite.config.ts └── yarn.lock ├── eslint.config.js ├── lib ├── .gitignore ├── dist │ ├── Animation.d.ts │ ├── Animation.types.d.ts │ ├── Driver.d.ts │ ├── Driver.types.d.ts │ ├── Observer.d.ts │ ├── Observer.types.d.ts │ ├── TheSupersonicPlugin.d.ts │ ├── TheSupersonicPlugin.types.d.ts │ ├── index.d.ts │ ├── the-supersonic-plugin-for-scroll-based-animation.iife.js │ └── the-supersonic-plugin-for-scroll-based-animation.js ├── env.d.ts ├── package.json ├── src │ ├── Animation.test.ts │ ├── Animation.ts │ ├── Animation.types.ts │ ├── Driver.test.ts │ ├── Driver.ts │ ├── Driver.types.ts │ ├── Observer.ts │ ├── Observer.types.ts │ ├── TheSupersonicPlugin.test.ts │ ├── TheSupersonicPlugin.ts │ ├── TheSupersonicPlugin.types.ts │ ├── index.ts │ └── utils.ts ├── tsconfig.json ├── vite.config.ts └── yarn.lock ├── package.json └── yarn.lock /.ci/.env.example: -------------------------------------------------------------------------------- 1 | NODE_IMAGE="node:20" 2 | PLAYWRIGHT_IMAGE="mcr.microsoft.com/playwright:v1.43.0-jammy" 3 | CLOUDFLARE_ACCOUNT_ID="example" 4 | CLOUDFLARE_API_TOKEN="example" -------------------------------------------------------------------------------- /.ci/Taskfile.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | dotenv: [.env] 4 | 5 | tasks: 6 | env: 7 | desc: makes .env from .env.example 8 | cmds: 9 | - cp .env.example .env 10 | 11 | root:yarn: 12 | desc: runs 'yarn' in root directory 13 | cmds: 14 | - | 15 | docker run --rm \ 16 | -w /app \ 17 | -v $(pwd)/..:/app \ 18 | $NODE_IMAGE yarn 19 | 20 | docs:yarn: 21 | desc: runs 'yarn' in 'docs' directory 22 | cmds: 23 | - | 24 | docker run --rm \ 25 | -w /app \ 26 | -v $(pwd)/../docs:/app \ 27 | $NODE_IMAGE yarn 28 | 29 | lib:yarn: 30 | desc: runs 'yarn' in 'lib' directory 31 | cmds: 32 | - | 33 | docker run --rm \ 34 | -w /app \ 35 | -v $(pwd)/../lib:/app \ 36 | $NODE_IMAGE yarn 37 | 38 | yarn: 39 | cmds: 40 | - task root:yarn 41 | - task docs:yarn 42 | - task lib:yarn 43 | 44 | lint: 45 | cmds: 46 | - | 47 | docker run --rm \ 48 | -w /app \ 49 | -v $(pwd)/..:/app \ 50 | $NODE_IMAGE /bin/bash -c "yarn lint" 51 | 52 | build:docs: 53 | desc: creates production build of docs 54 | cmds: 55 | - task lint 56 | - | 57 | docker run --rm \ 58 | -w /app/docs \ 59 | -v $(pwd)/../docs:/app/docs \ 60 | -v $(pwd)/../lib:/app/lib \ 61 | $NODE_IMAGE /bin/bash -c "yarn build" 62 | 63 | build:lib: 64 | desc: creates production build of lib 65 | cmds: 66 | - task lint 67 | - | 68 | docker run --rm \ 69 | -w /app \ 70 | -v $(pwd)/../lib:/app \ 71 | $NODE_IMAGE /bin/bash -c "yarn build" 72 | 73 | build: 74 | cmds: 75 | - task build:docs 76 | - task build:lib 77 | 78 | up: 79 | cmds: 80 | - | 81 | docker run \ 82 | --rm --network="host" \ 83 | -v $(pwd)/../docs:/app/docs \ 84 | -v $(pwd)/../lib:/app/lib \ 85 | -w /app/docs \ 86 | --name ci-preview \ 87 | -d $NODE_IMAGE \ 88 | yarn preview 89 | 90 | down: 91 | cmds: 92 | - docker container stop ci-preview 93 | 94 | test:unit: 95 | desc: runs unit tests in 'lib' directory 96 | cmds: 97 | - | 98 | docker run --rm \ 99 | -v $(pwd)/../lib:/app \ 100 | -w /app \ 101 | $NODE_IMAGE \ 102 | yarn test:unit 103 | 104 | test:e2e: 105 | desc: runs Playwright 106 | cmds: 107 | - | 108 | docker run --rm \ 109 | --network="host" \ 110 | -v $(pwd)/../docs:/app \ 111 | -w /app \ 112 | $PLAYWRIGHT_IMAGE \ 113 | yarn test:e2e 114 | 115 | test: 116 | cmds: 117 | - task test:unit 118 | - task test:e2e 119 | 120 | deploy:pages: 121 | desc: deploy to CloudFlare pages 122 | cmds: 123 | - | 124 | docker run \ 125 | --rm \ 126 | -w /app \ 127 | -v $(pwd)/../docs:/app \ 128 | -e CLOUDFLARE_ACCOUNT_ID \ 129 | -e CLOUDFLARE_API_TOKEN \ 130 | $NODE_IMAGE yarn deploy 131 | 132 | deploy: 133 | desc: Lints, tests, builds and deploys to CloudFlare pages 134 | cmds: 135 | - task build 136 | - task up 137 | - task test 138 | - task deploy:pages 139 | -------------------------------------------------------------------------------- /.dev/.env.example: -------------------------------------------------------------------------------- 1 | NODE_IMAGE="node:20" 2 | PLAYWRIGHT_IMAGE="mcr.microsoft.com/playwright:v1.43.0-jammy" -------------------------------------------------------------------------------- /.dev/Taskfile.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | dotenv: [.env] 4 | 5 | vars: 6 | MY_UID: $(id -u) # TODO: Change this line if you are not on Linux 7 | MY_GID: $(id -g) # TODO: Change this line if you are not on Linux 8 | 9 | tasks: 10 | env: 11 | desc: makes .env from .env.example 12 | cmds: 13 | - cp .env.example .env 14 | 15 | init: 16 | desc: prepare everything for development 17 | cmds: 18 | - task root:yarn 19 | - task lib:yarn 20 | - task docs:yarn 21 | - docker pull $PLAYWRIGHT_IMAGE 22 | - docker build -t unit-tests-ui - < ./UnitTestUi.Dockerfile --build-arg NODE_IMAGE 23 | 24 | up: 25 | desc: start services 26 | cmds: 27 | - MY_UID="{{.MY_UID}}" MY_GID="{{.MY_GID}}" docker compose up -d 28 | 29 | down: 30 | desc: stops services 31 | cmds: 32 | - MY_UID="{{.MY_UID}}" MY_GID="{{.MY_GID}}" docker compose down 33 | 34 | restart: 35 | desc: restart services 36 | cmds: 37 | - task down 38 | - task up 39 | 40 | logs: 41 | desc: logs the development compose 42 | cmds: 43 | - docker compose logs -f 44 | 45 | # ROOT 46 | root: 47 | desc: launch node with mounted root directory 48 | cmds: 49 | - | 50 | docker run --user {{.MY_UID}}:{{.MY_GID}} \ 51 | -it --rm \ 52 | -w /app \ 53 | -v $(pwd)/..:/app \ 54 | $NODE_IMAGE /bin/bash 55 | 56 | root:yarn: 57 | desc: runs 'yarn' in root directory 58 | cmds: 59 | - | 60 | docker run --user {{.MY_UID}}:{{.MY_GID}} \ 61 | --rm \ 62 | -w /app \ 63 | -v $(pwd)/..:/app \ 64 | $NODE_IMAGE /bin/bash -c "yarn {{.CLI_ARGS }}" 65 | 66 | root:lint: 67 | desc: runs 'lint' in root directory 68 | cmds: 69 | - | 70 | docker run --user {{.MY_UID}}:{{.MY_GID}} \ 71 | --rm \ 72 | -w /app \ 73 | -v $(pwd)/..:/app \ 74 | $NODE_IMAGE /bin/bash -c "yarn lint" 75 | 76 | root:lint:fix: 77 | desc: runs 'lint:fix' in root directory 78 | cmds: 79 | - | 80 | docker run --user {{.MY_UID}}:{{.MY_GID}} \ 81 | --rm \ 82 | -w /app \ 83 | -v $(pwd)/..:/app \ 84 | $NODE_IMAGE /bin/bash -c "yarn lint:fix" 85 | 86 | # LIB 87 | lib: 88 | desc: launch node with mounted 'lib' directory 89 | cmds: 90 | - | 91 | docker run --user {{.MY_UID}}:{{.MY_GID}} \ 92 | -it --rm \ 93 | -w /app \ 94 | -v $(pwd)/../lib:/app \ 95 | $NODE_IMAGE /bin/bash 96 | 97 | lib:yarn: 98 | desc: runs 'yarn' in 'lib' directory 99 | cmds: 100 | - | 101 | docker run --user {{.MY_UID}}:{{.MY_GID}} \ 102 | --rm \ 103 | -w /app \ 104 | -v $(pwd)/../lib:/app \ 105 | $NODE_IMAGE /bin/bash -c "yarn {{.CLI_ARGS }}" 106 | 107 | lib:build: 108 | desc: runs 'yarn build' in 'lib' directory 109 | cmds: 110 | - | 111 | docker run --user {{.MY_UID}}:{{.MY_GID}} \ 112 | --rm \ 113 | -w /app \ 114 | -v $(pwd)/../lib:/app \ 115 | $NODE_IMAGE /bin/bash -c "yarn build && yarn minify" 116 | - rm ../lib/dist/the-supersonic-plugin-for-scroll-based-animation.js 117 | - mv ../lib/dist/the-supersonic-plugin-for-scroll-based-animation.min.js ../lib/dist/the-supersonic-plugin-for-scroll-based-animation.js 118 | 119 | # DOCS 120 | docs: 121 | desc: launch node with mounted 'docs' directory 122 | cmds: 123 | - | 124 | docker run --user {{.MY_UID}}:{{.MY_GID}} \ 125 | -it --rm \ 126 | -w /app \ 127 | -v $(pwd)/../docs:/app \ 128 | $NODE_IMAGE /bin/bash 129 | 130 | docs:yarn: 131 | desc: runs 'yarn' in 'docs' directory 132 | cmds: 133 | - | 134 | docker run --user {{.MY_UID}}:{{.MY_GID}} \ 135 | --rm \ 136 | -w /app \ 137 | -v $(pwd)/../docs:/app \ 138 | $NODE_IMAGE /bin/bash -c "yarn {{.CLI_ARGS }}" 139 | 140 | docs:build: 141 | desc: runs 'yarn build' in 'docs' directory 142 | cmds: 143 | - | 144 | docker run --user {{.MY_UID}}:{{.MY_GID}} \ 145 | --rm \ 146 | -w /app/docs \ 147 | -v $(pwd)/../docs:/app/docs \ 148 | -v $(pwd)/../lib:/app/lib \ 149 | $NODE_IMAGE /bin/bash -c "yarn build" 150 | 151 | # TESTS 152 | test:unit: 153 | desc: runs unit tests in 'lib' directory 154 | cmds: 155 | - | 156 | docker run --user {{.MY_UID}}:{{.MY_GID}} \ 157 | -v $(pwd)/../lib:/app \ 158 | -w /app \ 159 | $NODE_IMAGE \ 160 | yarn test:unit 161 | 162 | test:e2e: 163 | desc: runs Playwright 164 | cmds: 165 | - | 166 | docker run --user {{.MY_UID}}:{{.MY_GID}} \ 167 | --rm --network="host" --ipc=host \ 168 | -v $(pwd)/../docs:/app \ 169 | -w /app \ 170 | $PLAYWRIGHT_IMAGE \ 171 | yarn test:e2e 172 | 173 | test:e2e:ui: 174 | desc: runs Playwright in UI mode (works only in Linux) 175 | # TODO: Please refer to https://www.oddbird.net/2022/11/30/headed-playwright-in-docker/ if you want to make it work on Windows or macOS 176 | cmds: 177 | - | 178 | docker run --user {{.MY_UID}}:{{.MY_GID}} \ 179 | --rm --network="host" --ipc=host \ 180 | -e DISPLAY=${DISPLAY} \ 181 | -v /tmp/.X11-unix:/tmp/.X11-unix \ 182 | -v $(pwd)/../docs:/app \ 183 | -w /app \ 184 | $PLAYWRIGHT_IMAGE \ 185 | yarn test:e2e:ui 186 | 187 | test: 188 | cmds: 189 | - task test:unit 190 | - task test:e2e 191 | -------------------------------------------------------------------------------- /.dev/UnitTestUi.Dockerfile: -------------------------------------------------------------------------------- 1 | ARG NODE_IMAGE 2 | 3 | FROM ${NODE_IMAGE} 4 | 5 | RUN apt update && apt install -y xdg-utils 6 | 7 | # @vitest/ui needs xdg-utils to be able to spawn UI server -------------------------------------------------------------------------------- /.dev/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | docs: 3 | image: ${NODE_IMAGE} 4 | working_dir: /app/docs 5 | command: yarn dev 6 | user: ${MY_UID}:${MY_GID} 7 | ports: 8 | - 80:80 9 | volumes: 10 | - ../docs:/app/docs 11 | - ../lib:/app/lib 12 | 13 | docs-preview: 14 | image: ${NODE_IMAGE} 15 | working_dir: /app/docs 16 | command: yarn preview 17 | user: ${MY_UID}:${MY_GID} 18 | ports: 19 | - 9000:80 20 | volumes: 21 | - ../docs:/app/docs 22 | - ../lib:/app/lib 23 | 24 | lib-tests-unit: 25 | image: unit-tests-ui 26 | user: ${MY_UID}:${MY_GID} 27 | working_dir: /app 28 | command: yarn test:unit:ui 29 | ports: 30 | - 9010:8000 31 | volumes: 32 | - ../lib:/app 33 | -------------------------------------------------------------------------------- /.github/README.md: -------------------------------------------------------------------------------- 1 | # The Supersonic Plugin For Scroll Based Animation 2 | 3 | ![gzip size](https://img.shields.io/bundlejs/size/the-supersonic-plugin-for-scroll-based-animation) 4 | ![test-and-build](https://github.com/the-illarionov/the-supersonic-plugin-for-scroll-based-animation/actions/workflows/test-and-build.yml/badge.svg) 5 | 6 | - [Main features](#main-features) 7 | - [Installation](#installation) 8 | - [Examples](#examples) 9 | - [Configuration](#configuration) 10 | - [Possible issues](#possible-issues) 11 | 12 | --- 13 | 14 | Once upon a time, I suddenly realized that I need to animate 1000 pictures of my cat Bartholomew with scrolling. Moreover, it had to work on mobiles as is, without any additional rules. And work fast without killing the browser. And be highly customizable to fit any needs. 15 | 16 | Other solutions did not go well, so I decided to write my own (of course, I did). 17 | 18 | [Go check the final result and see how 1000 Bartholomews are animated!](https://the-illarionov.com/the-supersonic-plugin-for-scroll-based-animation/bartholomeow) 19 | 20 | # Main features 21 | 22 | 1. ## 2.5kb gzipped 23 | - 0 dependencies 24 | - 100% TypeScript 25 | - ESM and IIFE formats 26 | 27 | 2. ## Use native CSS animations 28 | ```javascript 29 | const plugin = new TheSupersonicPlugin([ 30 | { 31 | start: '.start', // Animation starts when this element appears on the screen 32 | end: '.end', // Animation ends when this element appears on the screen 33 | elements: ['.animatable'] // List of elements with CSS animations 34 | }, 35 | ]); 36 | ``` 37 | ```css 38 | .animatable { 39 | animation-name: hooray; 40 | animation-duration: 10s; /* It has to be 10s, more details in examples */ 41 | animation-play-state: paused; /* It will be controlled by script */ 42 | } 43 | ``` 44 | No more struggling with calculating animations, dealing with orchestrating them etc. 45 | 46 | Write your CSS animations as usual and let the plugin to control them. You can even have CSS animations that are being played as usual together with plugin controlled ones! [Check examples](https://the-illarionov.com/the-supersonic-plugin-for-scroll-based-animation/examples). 47 | 48 | As a bonus, you can have different animations for different screens with 0 lines of javascript code! 49 | 50 | Sure, we have `scroll-timeline` upcoming, but it has some disavantages: 51 | - Still experimental at the time of April 2024. 52 | - You can't bind start of an animation to one element and end of it to completely another. 53 | 54 | 3. ## Use DOM elements as drivers of your animation 55 | 56 | Instead of manually setting the start and end values, you use HTML elements (**_drivers_**). Their appearance and position on the screen will control the animation. The Plugin uses the driver's top offset as a value, [check examples](https://the-illarionov.com/the-supersonic-plugin-for-scroll-based-animation/examples). 57 | 58 | And again, as a bonus of using DOM elements as drivers you don't have to think about responsivity, it comes out of the box (you can use `media queries` to reposition your drivers). 59 | 60 | 4. ## Customization and crazy flexibility 61 | Every internal stuff has a hook. Actually, everything that plugin does it does through it's API so you create as complex stuff as you want. 62 | 63 | Look at the [type declarations](https://the-illarionov.com/the-supersonic-plugin-for-scroll-based-animation/types) to discover all of the customization possibilities. 64 | 65 | You can even use plugin to animate whatever you like, not only CSS animations. Check [last example here](https://the-illarionov.com/the-supersonic-plugin-for-scroll-based-animation/examples), you will be surprised ;) 66 | 67 | 5. ## Speed 68 | 69 | The Plugin uses IntersectionObserver, so only visible elements will be processed. It means you can have lots of elements! 70 | 71 | Also, there are small tricks for tiny optimizations. 72 | 73 | For example, float number rounding after the decimal point is used heavily, and instead of ```parseFloat(float.toFixed(2))``` The Plugin uses bitwise rounding: 74 | ```javascript 75 | function supersonicToFixed(number: number, precision: number) { 76 | precision = 10 ** precision 77 | return ~~(number * precision) / precision 78 | } 79 | ``` 80 | It is 90% more performant. 81 | 82 | As a result, [1000 Bartholomews](https://the-illarionov.com/the-supersonic-plugin-for-scroll-based-animation/bartholomeow) perform like this on 10 years old PC (i7-4770, 8Gb RAM): 83 | 84 | ![image](./images/performance.png) 85 | 86 | Yeah, it lags a little, but check the Heap size and total Scripting time! 87 | 88 | The Plugin itself still works extremely fast, it takes about ***0.5ms*** per frame to make all of the calculations, the rest is rendering. 89 | 90 | It is one goddamn thousand HTML elements animating over here! 91 | 92 | 6. ## SPA-ready 93 | 94 | Plugin has `uninit()` method, which clears all of the stuff. Don't forget to call it when you unmount your component. 95 | 96 | ```javascript 97 | onMounted() { 98 | const plugin = new TheSupersonicPlugin([ 99 | // config... 100 | ]) 101 | } 102 | 103 | onBeforeUnmount() { 104 | plugin.uninit() 105 | } 106 | ``` 107 | 108 | And all of this at a price of 2.5Kb! 109 | 110 | So what are you waiting for? 111 | 112 | # Installation 113 | 114 | ## NPM 115 | 116 | ``` 117 | npm install the-supersonic-plugin-for-scroll-based-animation --save-dev 118 | ``` 119 | 120 | ```javascript 121 | import { TheSupersonicPlugin } from "the-supersonic-plugin-for-scroll-based-animation" 122 | 123 | new TheSupersonicPlugin([ 124 | // ...configuration 125 | ]) 126 | ``` 127 | 128 | ## CDN 129 | ### IIFE 130 | ```html 131 | 132 | 133 | 138 | ``` 139 | ### ESM 140 | ```javascript 141 | import { TheSupersonicPlugin } from "https://esm.sh/the-supersonic-plugin-for-scroll-based-animation" 142 | 143 | new TheSupersonicPlugin([ 144 | // ...configuration 145 | ]) 146 | ``` 147 | 148 | # Examples 149 | 150 | You can [find them here](https://the-illarionov.com/the-supersonic-plugin-for-scroll-based-animation/examples). 151 | 152 | # Configuration 153 | Check [types declarations](https://the-illarionov.com/the-supersonic-plugin-for-scroll-based-animation/types) to see all of the configuration options. 154 | 155 | The Plugin consists of 3 classes: 156 | 1. `TheSupersonicPlugin`, which acts like main entry point. 157 | 2. `Driver`, which calculates it's progress from 0 to 1 according to current scroll and position of `start` and `end` elements. 158 | 3. `Animation`, which is responsible for storing and setting CSS Animation `currentTime` property. 159 | 160 | All of them are provided too, so you can manually create your own instances: 161 | ```javascript 162 | import { 163 | TheSupersonicPlugin, 164 | Driver, 165 | Animation 166 | } from "https://esm.sh/the-supersonic-plugin-for-scroll-based-animation" 167 | 168 | const plugin = new TheSupersonicPlugin([ 169 | // ...configuration 170 | ]) 171 | 172 | const animation = new Animation({ 173 | driver: plugin.driverInstances.get('some-driver'), 174 | // ...another configuration 175 | }) 176 | ``` 177 | 178 | ## Possible issues 179 | 1. When you have **lots** of animations (more 500), plugin can start to lag a little on start. 180 | 181 | It's because `domElement.getAnimations()` doesn't scale too well. 182 | 183 | If you really need to have lots of animations, implement lazy initialization of animation at `onActivation` driver hook. Or you can manually updating properties like [i did with 1000 Bartholomeows](https://the-illarionov.com/the-supersonic-plugin-for-scroll-based-animation/bartholomeow). 184 | 185 | 2. If you have **lots** of drivers (more 1000), plugin can start to lag a little on start. 186 | 187 | It's because plugin fires `updateLimits()` on start to set proper top distances to all elements, which causes `reflow`. 188 | 189 | If it's really a problem, consider cancelling default rendering via `onBeforeRender: () => false` and batching all of `updateLimits` call at one call. 190 | 191 | --- 192 | 193 | Cheers from Bartholomew :3 194 | -------------------------------------------------------------------------------- /.github/images/performance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-illarionov/the-supersonic-plugin-for-scroll-based-animation/35223418e8ef91cc97dbbb206f96d8143c40a7a0/.github/images/performance.png -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: deploy 2 | 3 | run-name: Deploying 4 | 5 | concurrency: 6 | group: '${{ github.workflow }}' 7 | cancel-in-progress: true 8 | 9 | on: 10 | workflow_dispatch: 11 | push: 12 | branches: 13 | master 14 | 15 | jobs: 16 | deploy: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - name: Download artifacts 22 | uses: actions/github-script@v7 23 | with: 24 | script: | 25 | const { data } = await github.rest.actions.listWorkflowRuns({ 26 | owner: context.repo.owner, 27 | repo: context.repo.repo, 28 | workflow_id: 'test-and-build.yml', 29 | status: 'completed', 30 | }); 31 | 32 | const successfulRunId = data.workflow_runs.find(run => run.conclusion === 'success').id; 33 | 34 | const allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ 35 | owner: context.repo.owner, 36 | repo: context.repo.repo, 37 | run_id: successfulRunId, 38 | }); 39 | 40 | const artifactId = allArtifacts.data.artifacts.find((artifact) => artifact.name == "docs-dist").id; 41 | 42 | const downloadArtifact = await github.rest.actions.downloadArtifact({ 43 | owner: context.repo.owner, 44 | repo: context.repo.repo, 45 | artifact_id: artifactId, 46 | archive_format: 'zip', 47 | }); 48 | 49 | let fs = require('fs'); 50 | fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/docs.zip`, Buffer.from(downloadArtifact.data)); 51 | 52 | - name: Unzip artifacts 53 | run: | 54 | unzip docs.zip -d dist 55 | 56 | - name: Deploy to Cloudflare Pages 57 | uses: cloudflare/pages-action@v1 58 | with: 59 | apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} 60 | accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} 61 | projectName: the-supersonic-plugin-for-scroll-based-animation 62 | directory: dist 63 | branch: main 64 | -------------------------------------------------------------------------------- /.github/workflows/test-and-build.yml: -------------------------------------------------------------------------------- 1 | name: test-and-build 2 | 3 | run-name: Tests and prepares build for deploying 4 | 5 | concurrency: 6 | group: '${{ github.workflow }}' 7 | cancel-in-progress: true 8 | 9 | on: 10 | workflow_dispatch: 11 | pull_request: 12 | branches: 13 | master 14 | 15 | jobs: 16 | test: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - uses: arduino/setup-task@v2 22 | 23 | - name: Prepare env 24 | run: task -d .ci env 25 | 26 | - name: 'Cache node_modules: check' 27 | uses: actions/cache/restore@v4 28 | id: cache-node-modules 29 | with: 30 | path: | 31 | node_modules 32 | docs/node_modules 33 | lib/node_modules 34 | key: node-modules-${{ hashFiles('yarn.lock', 'docs/yarn.lock', 'lib/yarn.lock') }} 35 | 36 | - if: steps.cache-node-modules.outputs.cache-hit != 'true' 37 | name: 'Cache node_modules doesn''t exist: install' 38 | run: task -d .ci yarn 39 | 40 | - if: steps.cache-node-modules.outputs.cache-hit != 'true' 41 | name: 'Cache node_modules doesn''t exist: save node_modules to cache' 42 | uses: actions/cache/save@v4 43 | with: 44 | path: | 45 | node_modules 46 | docs/node_modules 47 | lib/node_modules 48 | key: node-modules-${{ hashFiles('yarn.lock', 'docs/yarn.lock', 'lib/yarn.lock') }} 49 | 50 | - name: Build 51 | run: task -d .ci build 52 | 53 | - name: Runs server 54 | run: task -d .ci up 55 | 56 | - name: Tests 57 | run: task -d .ci test 58 | 59 | - name: 'Uploads ''docs/dist'' artifact' 60 | uses: actions/upload-artifact@v4 61 | with: 62 | path: docs/dist 63 | name: docs-dist 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules 3 | _old 4 | notes -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-playwright.playwright", 4 | "dbaeumer.vscode-eslint", 5 | "stylelint.vscode-stylelint" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Enable the ESlint flat config support 3 | "eslint.experimental.useFlatConfig": true, 4 | 5 | "editor.formatOnSave": false, 6 | 7 | // Auto fix 8 | "editor.codeActionsOnSave": { 9 | "source.fixAll": "explicit", 10 | "source.organizeImports": "never" 11 | }, 12 | 13 | // Silent the stylistic rules in you IDE, but still auto fix them 14 | "eslint.rules.customizations": [ 15 | { "rule": "style/*", "severity": "off" }, 16 | { "rule": "format/*", "severity": "off" }, 17 | { "rule": "*-indent", "severity": "off" }, 18 | { "rule": "*-spacing", "severity": "off" }, 19 | { "rule": "*-spaces", "severity": "off" }, 20 | { "rule": "*-order", "severity": "off" }, 21 | { "rule": "*-dangle", "severity": "off" }, 22 | { "rule": "*-newline", "severity": "off" }, 23 | { "rule": "*quotes", "severity": "off" }, 24 | { "rule": "*semi", "severity": "off" } 25 | ], 26 | 27 | // Enable eslint for all supported languages 28 | "eslint.validate": [ 29 | "javascript", 30 | "javascriptreact", 31 | "typescript", 32 | "typescriptreact", 33 | "html", 34 | "markdown", 35 | "json", 36 | "jsonc", 37 | "yaml", 38 | "toml" 39 | ], 40 | 41 | // My customs 42 | "css.validate": false, 43 | "less.validate": false, 44 | "scss.validate": false, 45 | "stylelint.validate": ["css"] 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Alex Illarionov 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 | # The Supersonic Plugin For Scroll Based Animation 2 | 3 | Once upon a time, I suddenly realized that I need to animate 1000 pictures of my cat Bartholomew with scrolling. Moreover, it had to work on mobiles as is, without any additional rules. And work fast without killing the browser. And be highly customizable to fit any needs. 4 | 5 | Other solutions did not go well, so I decided to write my own (of course, I did). 6 | 7 | [Go check the final result and see how 1000 Bartholomews are animated!](https://the-illarionov.com/the-supersonic-plugin-for-scroll-based-animation/bartholomeow) 8 | 9 | [Read the full documentation on GitHub](https://github.com/the-illarionov/the-supersonic-plugin-for-scroll-based-animation#readme). 10 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | test-results/ 27 | playwright-report/ 28 | .wrangler -------------------------------------------------------------------------------- /docs/bartholomeow.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 13 | 14 | The Supersonic Plugin For Scroll Based Animation: 1000 Bartholomeows! 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /docs/e2e.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 85 | 86 | 87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | 96 |
97 | 98 |
102 | 103 |
107 | 108 |
109 | 110 |
114 | 115 |
116 | 117 |
118 | 119 |
123 | 124 |
125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /docs/e2e/ScreenShot.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test' 2 | 3 | test('0', async ({ page }) => { 4 | await page.goto('/e2e.html') 5 | await sleep() 6 | await expect(page).toHaveScreenshot({ 7 | animations: 'allow', 8 | }) 9 | }) 10 | 11 | test('500', async ({ page }) => { 12 | await page.goto('/e2e.html') 13 | await sleep() 14 | await page.evaluate(() => window.scrollTo(0, 500)) 15 | await sleep() 16 | await expect(page).toHaveScreenshot({ 17 | animations: 'allow', 18 | }) 19 | }) 20 | 21 | test('1000', async ({ page }) => { 22 | await page.goto('/e2e.html') 23 | await sleep() 24 | await page.evaluate(() => window.scrollTo(0, 1000)) 25 | await sleep() 26 | await expect(page).toHaveScreenshot({ 27 | animations: 'allow', 28 | }) 29 | }) 30 | 31 | test('3000', async ({ page }) => { 32 | await page.goto('/e2e.html') 33 | await sleep() 34 | await page.evaluate(() => window.scrollTo(0, 3000)) 35 | await sleep() 36 | await expect(page).toHaveScreenshot({ 37 | animations: 'allow', 38 | }) 39 | }) 40 | 41 | function sleep(time = 500) { 42 | return new Promise((res) => { 43 | setTimeout(res, time) 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /docs/e2e/ScreenShot.test.ts-snapshots/0-1-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-illarionov/the-supersonic-plugin-for-scroll-based-animation/35223418e8ef91cc97dbbb206f96d8143c40a7a0/docs/e2e/ScreenShot.test.ts-snapshots/0-1-chromium-linux.png -------------------------------------------------------------------------------- /docs/e2e/ScreenShot.test.ts-snapshots/0-1-firefox-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-illarionov/the-supersonic-plugin-for-scroll-based-animation/35223418e8ef91cc97dbbb206f96d8143c40a7a0/docs/e2e/ScreenShot.test.ts-snapshots/0-1-firefox-linux.png -------------------------------------------------------------------------------- /docs/e2e/ScreenShot.test.ts-snapshots/0-1-webkit-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-illarionov/the-supersonic-plugin-for-scroll-based-animation/35223418e8ef91cc97dbbb206f96d8143c40a7a0/docs/e2e/ScreenShot.test.ts-snapshots/0-1-webkit-linux.png -------------------------------------------------------------------------------- /docs/e2e/ScreenShot.test.ts-snapshots/1000-1-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-illarionov/the-supersonic-plugin-for-scroll-based-animation/35223418e8ef91cc97dbbb206f96d8143c40a7a0/docs/e2e/ScreenShot.test.ts-snapshots/1000-1-chromium-linux.png -------------------------------------------------------------------------------- /docs/e2e/ScreenShot.test.ts-snapshots/1000-1-firefox-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-illarionov/the-supersonic-plugin-for-scroll-based-animation/35223418e8ef91cc97dbbb206f96d8143c40a7a0/docs/e2e/ScreenShot.test.ts-snapshots/1000-1-firefox-linux.png -------------------------------------------------------------------------------- /docs/e2e/ScreenShot.test.ts-snapshots/1000-1-webkit-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-illarionov/the-supersonic-plugin-for-scroll-based-animation/35223418e8ef91cc97dbbb206f96d8143c40a7a0/docs/e2e/ScreenShot.test.ts-snapshots/1000-1-webkit-linux.png -------------------------------------------------------------------------------- /docs/e2e/ScreenShot.test.ts-snapshots/3000-1-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-illarionov/the-supersonic-plugin-for-scroll-based-animation/35223418e8ef91cc97dbbb206f96d8143c40a7a0/docs/e2e/ScreenShot.test.ts-snapshots/3000-1-chromium-linux.png -------------------------------------------------------------------------------- /docs/e2e/ScreenShot.test.ts-snapshots/3000-1-firefox-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-illarionov/the-supersonic-plugin-for-scroll-based-animation/35223418e8ef91cc97dbbb206f96d8143c40a7a0/docs/e2e/ScreenShot.test.ts-snapshots/3000-1-firefox-linux.png -------------------------------------------------------------------------------- /docs/e2e/ScreenShot.test.ts-snapshots/3000-1-webkit-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-illarionov/the-supersonic-plugin-for-scroll-based-animation/35223418e8ef91cc97dbbb206f96d8143c40a7a0/docs/e2e/ScreenShot.test.ts-snapshots/3000-1-webkit-linux.png -------------------------------------------------------------------------------- /docs/e2e/ScreenShot.test.ts-snapshots/500-1-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-illarionov/the-supersonic-plugin-for-scroll-based-animation/35223418e8ef91cc97dbbb206f96d8143c40a7a0/docs/e2e/ScreenShot.test.ts-snapshots/500-1-chromium-linux.png -------------------------------------------------------------------------------- /docs/e2e/ScreenShot.test.ts-snapshots/500-1-firefox-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-illarionov/the-supersonic-plugin-for-scroll-based-animation/35223418e8ef91cc97dbbb206f96d8143c40a7a0/docs/e2e/ScreenShot.test.ts-snapshots/500-1-firefox-linux.png -------------------------------------------------------------------------------- /docs/e2e/ScreenShot.test.ts-snapshots/500-1-webkit-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-illarionov/the-supersonic-plugin-for-scroll-based-animation/35223418e8ef91cc97dbbb206f96d8143c40a7a0/docs/e2e/ScreenShot.test.ts-snapshots/500-1-webkit-linux.png -------------------------------------------------------------------------------- /docs/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /docs/gsap.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 |
12 |
13 |
14 |
15 | 16 |
17 |
18 | 19 | 20 |
21 |
22 | 23 |
24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 16 | The Supersonic Plugin For Scroll Based Animation 17 | 30 | 31 | 32 |

1. Basic example

33 |
34 | 51 |
52 | 53 |

2. Customizing CSS animation properties

54 |
55 | 74 |
75 | 76 |

3. Using driver hooks to implement custom transition

77 |
78 | 96 |
97 | 98 |

4. Specifying animations

99 |
100 | 119 |
120 | 121 |

5. Using plugin hooks to stop rendering at all

122 |
123 | 142 |
143 | 144 |

6. Using plugin 'uninit' to stop rendering at all

145 |
146 | 167 |
168 | 169 |

7. Using driver hooks to stop rendering

170 |
171 | 190 |
191 | 192 |

8. Using animation hooks to stop rendering

193 |
194 | 213 |
214 | 215 |

9. Using animation hooks to manually set currentTime

216 |
217 | 236 |
237 | 238 |

10. Using driver hooks to animate custom element

239 |
240 | 260 |
261 | 262 |

11. Replicating GSAP examples: Airpods

263 |
264 | 283 |
284 | 285 | 286 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "type": "module", 4 | "version": "0.0.0", 5 | "private": true, 6 | "scripts": { 7 | "dev": "vite --port=80 --host", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview --port=80 --host", 10 | "test:e2e": "playwright test", 11 | "test:e2e:ui": "playwright test --ui", 12 | "deploy": "wrangler pages deploy dist --project-name=the-supersonic-plugin-for-scroll-based-animation --branch=main" 13 | }, 14 | "devDependencies": { 15 | "@playwright/test": "^1.43.0", 16 | "@types/node": "^20.12.7", 17 | "postcss": "^8.4.38", 18 | "typescript": "^5.2.2", 19 | "vite": "^5.2.0", 20 | "wrangler": "^3.50.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /docs/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import { defineConfig, devices } from '@playwright/test' 3 | 4 | /** 5 | * Read environment variables from file. 6 | * https://github.com/motdotla/dotenv 7 | */ 8 | // require('dotenv').config(); 9 | 10 | /** 11 | * See https://playwright.dev/docs/test-configuration. 12 | */ 13 | export default defineConfig({ 14 | testDir: './e2e', 15 | /* Maximum time one test can run for. */ 16 | timeout: 30 * 1000, 17 | expect: { 18 | /** 19 | * Maximum time expect() should wait for the condition to be met. 20 | * For example in `await expect(locator).toHaveText();` 21 | */ 22 | timeout: 5000, 23 | }, 24 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 25 | forbidOnly: !!process.env.CI, 26 | /* Retry on CI only */ 27 | retries: process.env.CI ? 2 : 0, 28 | /* Opt out of parallel tests on CI. */ 29 | workers: process.env.CI ? 1 : undefined, 30 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 31 | reporter: [['html', { open: 'never' }]], 32 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 33 | use: { 34 | /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ 35 | actionTimeout: 0, 36 | /* Base URL to use in actions like `await page.goto('/')`. */ 37 | baseURL: 'http://localhost', 38 | 39 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 40 | trace: 'on-first-retry', 41 | 42 | headless: true, 43 | 44 | testIdAttribute: 'data-test', 45 | }, 46 | 47 | /* Configure projects for major browsers */ 48 | projects: [ 49 | { 50 | name: 'chromium', 51 | use: { 52 | ...devices['Desktop Chrome'], 53 | viewport: { width: 1000, height: 1000 }, 54 | }, 55 | }, 56 | { 57 | name: 'firefox', 58 | use: { 59 | ...devices['Desktop Firefox'], 60 | viewport: { width: 1000, height: 1000 }, 61 | }, 62 | }, 63 | { 64 | name: 'webkit', 65 | use: { 66 | ...devices['Desktop Safari'], 67 | viewport: { width: 1000, height: 1000 }, 68 | }, 69 | }, 70 | 71 | /* Test against mobile viewports. */ 72 | /* { 73 | name: 'Mobile Chrome', 74 | use: { 75 | ...devices['Pixel 5'], 76 | }, 77 | }, */ 78 | /* { 79 | name: 'Mobile Safari', 80 | use: { 81 | ...devices['iPhone 12'], 82 | }, 83 | }, */ 84 | 85 | /* Test against branded browsers. */ 86 | // { 87 | // name: 'Microsoft Edge', 88 | // use: { 89 | // channel: 'msedge', 90 | // }, 91 | // }, 92 | // { 93 | // name: 'Google Chrome', 94 | // use: { 95 | // channel: 'chrome', 96 | // }, 97 | // }, 98 | ], 99 | 100 | /* Folder for test artifacts such as screenshots, videos, traces, etc. */ 101 | // outputDir: 'test-results/', 102 | }) 103 | -------------------------------------------------------------------------------- /docs/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | function bartholomeow() { 2 | return { 3 | postcssPlugin: 'bartholomeow', 4 | AtRule: { 5 | barth: (atRule) => { 6 | const size = Number.parseInt(atRule.params) 7 | let resultCss = '' 8 | let row = 0 9 | for (let i = 0; i < 49; i++) { 10 | if (i % 7 === 0 && i > 0) 11 | row++ 12 | 13 | resultCss += ` 14 | .barth-${i + 1} { 15 | background-position: ${(i % 7) * size}px ${row * size}px 16 | } 17 | ` 18 | } 19 | atRule.after(resultCss) 20 | atRule.remove() 21 | }, 22 | }, 23 | } 24 | } 25 | 26 | bartholomeow.postcss = true 27 | 28 | module.exports = { 29 | plugins: [ 30 | bartholomeow, 31 | ], 32 | } 33 | -------------------------------------------------------------------------------- /docs/public/bartholomeow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-illarionov/the-supersonic-plugin-for-scroll-based-animation/35223418e8ef91cc97dbbb206f96d8143c40a7a0/docs/public/bartholomeow.png -------------------------------------------------------------------------------- /docs/public/example.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-illarionov/the-supersonic-plugin-for-scroll-based-animation/35223418e8ef91cc97dbbb206f96d8143c40a7a0/docs/public/example.glb -------------------------------------------------------------------------------- /docs/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/public/worm-ball.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-illarionov/the-supersonic-plugin-for-scroll-based-animation/35223418e8ef91cc97dbbb206f96d8143c40a7a0/docs/public/worm-ball.png -------------------------------------------------------------------------------- /docs/public/worm-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-illarionov/the-supersonic-plugin-for-scroll-based-animation/35223418e8ef91cc97dbbb206f96d8143c40a7a0/docs/public/worm-bg.png -------------------------------------------------------------------------------- /docs/src/bartholomeow.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | * { 8 | box-sizing: border-box; 9 | } 10 | 11 | body { 12 | position: relative; 13 | } 14 | 15 | body { 16 | overflow-x: hidden; 17 | background: #fff; 18 | } 19 | 20 | .driver { 21 | position: absolute; 22 | } 23 | 24 | .barth { 25 | position: absolute; 26 | 27 | width: 170px; 28 | height: 170px; 29 | 30 | background-image: url("/bartholomeow.png"); 31 | border: 15px solid rgb(32 32 32); 32 | border-radius: 100%; 33 | } 34 | 35 | @barth 150; 36 | 37 | @media (width <= 1024px) { 38 | .barth { 39 | width: 100px; 40 | height: 100px; 41 | background-size: 700px 700px; 42 | border-width: 10px; 43 | } 44 | 45 | @barth 100; 46 | } -------------------------------------------------------------------------------- /docs/src/bartholomeow.ts: -------------------------------------------------------------------------------- 1 | import { TheSupersonicPlugin } from '../../lib/src' 2 | import type { DriverConfiguration } from '../../lib/src/Driver.types' 3 | import { toFixed } from '../../lib/src/utils' 4 | 5 | const isMobile = matchMedia('(max-width: 1024px)').matches 6 | const elementSize: number = isMobile ? 150 : 190 7 | const elementsAmount = 1000 8 | const maxHorizontalElements = Math.ceil(window.innerWidth / elementSize) 9 | const maxRows = Math.ceil(window.innerHeight / elementSize) 10 | const elementsOnScreen = maxRows * maxHorizontalElements 11 | const drivers: DriverConfiguration[] = [] 12 | 13 | const props = [ 14 | { 15 | name: 'translateX', 16 | minStart: 0, 17 | maxStart: 0, 18 | minEnd: -(window.innerWidth / 5), 19 | maxEnd: window.innerWidth / 5, 20 | }, 21 | 22 | { 23 | name: 'translateY', 24 | minStart: 0, 25 | maxStart: 0, 26 | minEnd: -(window.innerHeight / 5), 27 | maxEnd: window.innerHeight / 5, 28 | }, 29 | 30 | { 31 | name: 'rotate', 32 | minStart: 0, 33 | maxStart: 0, 34 | minEnd: -90, 35 | maxEnd: 90, 36 | }, 37 | { 38 | name: 'scale', 39 | minStart: 1, 40 | maxStart: 1, 41 | minEnd: 3, 42 | maxEnd: 3.5, 43 | }, 44 | ] 45 | 46 | let row = 0 47 | const last10sprites: any = [] 48 | function gen() { 49 | return Math.floor(Math.random() * 49) + 1 50 | } 51 | 52 | function randomSprite() { 53 | const index = gen() 54 | if (last10sprites.includes(index)) 55 | return randomSprite() 56 | if (last10sprites.length === 10) 57 | last10sprites.shift() 58 | last10sprites.push(index) 59 | return index 60 | } 61 | for (let i = 0; i < elementsAmount; i++) { 62 | const sprite = randomSprite() 63 | const image = document.createElement('span') 64 | image.classList.add('barth') 65 | image.classList.add(`barth-${sprite}`) 66 | 67 | if (i % maxHorizontalElements === 0 && i > 0) 68 | row++ 69 | 70 | const elementTop = row * elementSize 71 | 72 | image.style.top = `${elementTop}px` 73 | image.style.left = `${(i % maxHorizontalElements) * elementSize}px` 74 | // @ts-expect-error foo 75 | image.style.zIndex = i + 100 76 | image.id = `barth-${i}` 77 | 78 | document.body.appendChild(image) 79 | } 80 | 81 | const height = Math.ceil(row * elementSize) 82 | 83 | const driverHeight = elementSize * 2 84 | const driverAmount = Math.ceil((height - window.innerHeight) / driverHeight) 85 | const driversOnScreen = Math.ceil(window.innerHeight / driverHeight) 86 | 87 | for (let i = 0; i < driverAmount; i++) { 88 | const driverId = `driver-${i}` 89 | const start = document.createElement('i') 90 | start.classList.add('driver') 91 | const driverStart = i * driverHeight + window.innerHeight 92 | start.style.top = `${driverStart}px` 93 | 94 | document.body.appendChild(start) 95 | 96 | const end = document.createElement('i') 97 | end.classList.add('driver') 98 | end.style.top = `${driverStart + driverHeight + window.innerHeight}px` 99 | document.body.appendChild(end) 100 | 101 | const elementsPerDriver = elementsOnScreen / driversOnScreen 102 | 103 | const driverRow = Math.round(driverStart / elementSize) - driversOnScreen 104 | const driverStartFromElement = driverRow * maxHorizontalElements 105 | 106 | drivers.push({ 107 | id: driverId, 108 | start, 109 | end, 110 | hooks: { 111 | onAfterInit(driver) { 112 | driver.data.elements = [] 113 | 114 | for (let index = 0; index < elementsPerDriver; index++) { 115 | const element: any = {} 116 | element.domElement = document.querySelector(`#barth-${index + driverStartFromElement}`)! 117 | 118 | element.properties = {} 119 | 120 | for (let i = 0; i < props.length; i++) { 121 | const sourceProp = props[i] 122 | 123 | element.properties[sourceProp.name] = {} 124 | 125 | const targetProp = element.properties[sourceProp.name] 126 | 127 | targetProp.start = Math.random() * (sourceProp.maxStart - sourceProp.minStart) + sourceProp.minStart 128 | if (sourceProp.name === 'opacity' || sourceProp.name === 'scale') 129 | targetProp.start = Number.parseFloat(targetProp.start.toFixed(2)) 130 | else targetProp.start = Math.ceil(targetProp.start) 131 | 132 | targetProp.end = Math.random() * (sourceProp.maxEnd - sourceProp.minEnd) + sourceProp.minEnd 133 | if (sourceProp.name === 'opacity' || sourceProp.name === 'scale') 134 | targetProp.end = Number.parseFloat(targetProp.end.toFixed(2)) 135 | else targetProp.end = Math.ceil(targetProp.end) 136 | 137 | targetProp.distance = toFixed(targetProp.end - targetProp.start, 2) 138 | } 139 | 140 | driver.data.elements.push(element) 141 | } 142 | }, 143 | onAfterRender(driver) { 144 | driver.data.elements.forEach((element: any) => { 145 | const translateX = element.properties.translateX.distance * driver.progress + element.properties.translateX.start 146 | const translateY = element.properties.translateY.distance * driver.progress + element.properties.translateY.start 147 | const scale = element.properties.scale.distance * driver.progress + element.properties.scale.start 148 | const rotate = element.properties.rotate.distance * driver.progress + element.properties.rotate.start 149 | 150 | element.domElement.style.setProperty('transform', 151 | // eslint-disable-next-line prefer-template 152 | 'translate3d(' + translateX + 'px, ' + translateY + 'px, 0) ' 153 | + 'scale(' + scale + ') ' 154 | + 'rotate(' + rotate + 'deg') 155 | }) 156 | }, 157 | }, 158 | }) 159 | } 160 | 161 | const plugin = new TheSupersonicPlugin( 162 | drivers, 163 | ) 164 | 165 | // @ts-expect-error foo 166 | window.plugin = plugin 167 | -------------------------------------------------------------------------------- /docs/src/e2e.ts: -------------------------------------------------------------------------------- 1 | import { TheSupersonicPlugin } from '../../lib/src/index' 2 | 3 | const scrollLog = document.querySelector('#scroll-log') 4 | 5 | const plugin = new TheSupersonicPlugin([ 6 | { 7 | start: '.top-100', 8 | end: '.top-200', 9 | elements: [ 10 | '.basic', 11 | { 12 | selector: '.cancel-render-animation', 13 | animations: [ 14 | { 15 | name: 'basic', 16 | hooks: { 17 | onBeforeRender() { 18 | return false 19 | }, 20 | }, 21 | }, 22 | ], 23 | }, 24 | { 25 | selector: '.changes-bg-on-render', 26 | animations: [ 27 | { 28 | name: 'basic', 29 | hooks: { 30 | onBeforeRender(animation) { 31 | animation.domElement.style.background = `#${animation.driver.plugin.scroll.toString(16)}` 32 | }, 33 | }, 34 | }, 35 | ], 36 | }, 37 | ], 38 | }, 39 | { 40 | start: '.top-100', 41 | end: '.top-200', 42 | elements: ['.cancel-render-driver'], 43 | hooks: { 44 | onBeforeRender() { 45 | return false 46 | }, 47 | }, 48 | }, 49 | { 50 | start: '.top-250', 51 | end: '.top-400', 52 | id: 'bottom', 53 | elements: ['.bottom-driver'], 54 | }, 55 | ], { 56 | debug: true, 57 | hooks: { 58 | onBeforeRender(plugin) { 59 | scrollLog!.innerHTML = plugin.scroll.toString() 60 | }, 61 | }, 62 | }) 63 | 64 | console.log(plugin) 65 | 66 | // @ts-expect-error window 67 | window.plugin = plugin 68 | -------------------------------------------------------------------------------- /docs/src/gsap.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0; 4 | background: #0e100f; 5 | } 6 | 7 | .worm-wrapper { 8 | width: 584px; 9 | padding: 80px 0; 10 | position: relative; 11 | } 12 | 13 | .worm-container { 14 | 15 | height: 500px; 16 | } 17 | 18 | .worm-container__path { 19 | width: 100%; 20 | } 21 | 22 | .worm-container__ball { 23 | position: absolute; 24 | left: 322px; 25 | width: 93px; 26 | top: -93px; 27 | } 28 | 29 | .worm-container__ball-y { 30 | /* animation-name: ball-y; */ 31 | animation-duration: 1s; 32 | animation-timing-function: linear; 33 | 34 | animation-iteration-count: infinite; 35 | } 36 | 37 | @keyframes ball-y { 38 | 0% { 39 | translate: 0 0 0; 40 | } 41 | 10% { 42 | translate: 0 247px 0; 43 | } 44 | 20% { 45 | translate: 0 247px 0; 46 | } 47 | 30% { 48 | translate: 0 350px 0; 49 | } 50 | 40% { 51 | translate: 0 350px 0; 52 | } 53 | 50% { 54 | translate: 0 464px 0; 55 | } 56 | 60% { 57 | translate: 0 464px 0; 58 | } 59 | 100% { 60 | translate: 0 740px 0; 61 | } 62 | } 63 | 64 | .worm-container__ball-x { 65 | /* animation-name: ball-x; */ 66 | animation-duration: 1s; 67 | animation-timing-function: linear; 68 | 69 | animation-iteration-count: infinite; 70 | } 71 | 72 | @keyframes ball-x { 73 | 0% { 74 | translate: 0 0 0; 75 | } 76 | 10% { 77 | translate: -322px 0 0; 78 | } 79 | 20% { 80 | translate: -322px 0 0; 81 | } 82 | 30% { 83 | translate: 166px 0 0; 84 | } 85 | 40% { 86 | translate: 166px 0 0; 87 | } 88 | 100% { 89 | translate: -174px 0 0; 90 | } 91 | } -------------------------------------------------------------------------------- /docs/src/gsap.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-illarionov/the-supersonic-plugin-for-scroll-based-animation/35223418e8ef91cc97dbbb206f96d8143c40a7a0/docs/src/gsap.ts -------------------------------------------------------------------------------- /docs/tests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | TESTS 8 | 9 | 60 | 61 |
62 |
63 |

Lib Unit

64 | 68 |
69 |
70 | 71 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | 11 | "paths": { 12 | "@lib/*": ["../lib/src/*"] 13 | }, 14 | "resolveJsonModule": true, 15 | "allowImportingTsExtensions": true, 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noFallthroughCasesInSwitch": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noEmit": true, 23 | "isolatedModules": true, 24 | "skipLibCheck": true 25 | }, 26 | "include": ["src", "vite.config.ts", "playwright.config.ts", "env.d.ts"] 27 | } 28 | -------------------------------------------------------------------------------- /docs/types.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 13 | The Supersonic Plugin For Scroll Based Animation: Types 14 | 23 | 24 | 25 |
26 |

TheSupersonicPlugin.d.ts

27 | 28 |
29 | 30 |
31 |

Driver.d.ts

32 | 33 |
34 | 35 |
36 |

Animation.d.ts

37 | 38 |
39 | 40 | 41 | -------------------------------------------------------------------------------- /docs/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path' 2 | 3 | import { defineConfig } from 'vite' 4 | 5 | export default defineConfig(() => { 6 | return { 7 | build: { 8 | rollupOptions: { 9 | input: { 10 | main: resolve(__dirname, 'index.html'), 11 | barth: resolve(__dirname, 'bartholomeow.html'), 12 | types: resolve(__dirname, 'types.html'), 13 | e2e: resolve(__dirname, 'e2e.html'), 14 | }, 15 | }, 16 | }, 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /docs/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@cloudflare/kv-asset-handler@0.3.1": 6 | version "0.3.1" 7 | resolved "https://registry.yarnpkg.com/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.3.1.tgz#9b86167e58dbc419943c8d3ddcd8e2823f5db300" 8 | integrity sha512-lKN2XCfKCmpKb86a1tl4GIwsJYDy9TGuwjhDELLmpKygQhw8X2xR4dusgpC5Tg7q1pB96Eb0rBo81kxSILQMwA== 9 | dependencies: 10 | mime "^3.0.0" 11 | 12 | "@cloudflare/workerd-darwin-64@1.20240405.0": 13 | version "1.20240405.0" 14 | resolved "https://registry.yarnpkg.com/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20240405.0.tgz#07637ec31e2b6272c8924c5d6b499ec022378fa5" 15 | integrity sha512-ut8kwpHmlz9dNSjoov6v1b6jS50J46Mj9QcMA0t1Hne36InaQk/qqPSd12fN5p2GesZ9OOBJvBdDsTblVdyJ1w== 16 | 17 | "@cloudflare/workerd-darwin-arm64@1.20240405.0": 18 | version "1.20240405.0" 19 | resolved "https://registry.yarnpkg.com/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20240405.0.tgz#5fba9fbfdb6906f5c216ab1e9451a3360daa90fd" 20 | integrity sha512-x3A3Ym+J2DH1uYnw0aedeKOTnUebEo312+Aladv7bFri97pjRJcqVbYhMtOHLkHjwYn7bpKSY2eL5iM+0XT29A== 21 | 22 | "@cloudflare/workerd-linux-64@1.20240405.0": 23 | version "1.20240405.0" 24 | resolved "https://registry.yarnpkg.com/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20240405.0.tgz#9f455b9484921b4ef0b30f2f13747abfc73bf86c" 25 | integrity sha512-3tYpfjtxEQ0R30Pna7OF3Bz0CTx30hc0QNtH61KnkvXtaeYMkWutSKQKXIuVlPa/7v1MHp+8ViBXMflmS7HquA== 26 | 27 | "@cloudflare/workerd-linux-arm64@1.20240405.0": 28 | version "1.20240405.0" 29 | resolved "https://registry.yarnpkg.com/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20240405.0.tgz#50220db83d4e9f423bb59623a37b67fd4251680f" 30 | integrity sha512-NpKZlvmdgcX/m4tP5zM91AfJpZrue2/GRA+Sl3szxAivu2uE5jDVf5SS9dzqzCVfPrdhylqH7yeL4U/cafFNOg== 31 | 32 | "@cloudflare/workerd-windows-64@1.20240405.0": 33 | version "1.20240405.0" 34 | resolved "https://registry.yarnpkg.com/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20240405.0.tgz#4cb542cf8003fcd230bcf3b767f0d697a6cd799d" 35 | integrity sha512-REBeJMxvUCjwuEVzSSIBtzAyM69QjToab8qBst0S9vdih+9DObym4dw8CevdBQhDbFrHiyL9E6pAZpLPNHVgCw== 36 | 37 | "@cspotcode/source-map-support@0.8.1": 38 | version "0.8.1" 39 | resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" 40 | integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== 41 | dependencies: 42 | "@jridgewell/trace-mapping" "0.3.9" 43 | 44 | "@esbuild-plugins/node-globals-polyfill@^0.2.3": 45 | version "0.2.3" 46 | resolved "https://registry.yarnpkg.com/@esbuild-plugins/node-globals-polyfill/-/node-globals-polyfill-0.2.3.tgz#0e4497a2b53c9e9485e149bc92ddb228438d6bcf" 47 | integrity sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw== 48 | 49 | "@esbuild-plugins/node-modules-polyfill@^0.2.2": 50 | version "0.2.2" 51 | resolved "https://registry.yarnpkg.com/@esbuild-plugins/node-modules-polyfill/-/node-modules-polyfill-0.2.2.tgz#cefa3dc0bd1c16277a8338b52833420c94987327" 52 | integrity sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA== 53 | dependencies: 54 | escape-string-regexp "^4.0.0" 55 | rollup-plugin-node-polyfills "^0.2.1" 56 | 57 | "@esbuild/aix-ppc64@0.20.2": 58 | version "0.20.2" 59 | resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537" 60 | integrity sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g== 61 | 62 | "@esbuild/android-arm64@0.17.19": 63 | version "0.17.19" 64 | resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz#bafb75234a5d3d1b690e7c2956a599345e84a2fd" 65 | integrity sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA== 66 | 67 | "@esbuild/android-arm64@0.20.2": 68 | version "0.20.2" 69 | resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz#db1c9202a5bc92ea04c7b6840f1bbe09ebf9e6b9" 70 | integrity sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg== 71 | 72 | "@esbuild/android-arm@0.17.19": 73 | version "0.17.19" 74 | resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.17.19.tgz#5898f7832c2298bc7d0ab53701c57beb74d78b4d" 75 | integrity sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A== 76 | 77 | "@esbuild/android-arm@0.20.2": 78 | version "0.20.2" 79 | resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.2.tgz#3b488c49aee9d491c2c8f98a909b785870d6e995" 80 | integrity sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w== 81 | 82 | "@esbuild/android-x64@0.17.19": 83 | version "0.17.19" 84 | resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.17.19.tgz#658368ef92067866d95fb268719f98f363d13ae1" 85 | integrity sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww== 86 | 87 | "@esbuild/android-x64@0.20.2": 88 | version "0.20.2" 89 | resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.2.tgz#3b1628029e5576249d2b2d766696e50768449f98" 90 | integrity sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg== 91 | 92 | "@esbuild/darwin-arm64@0.17.19": 93 | version "0.17.19" 94 | resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz#584c34c5991b95d4d48d333300b1a4e2ff7be276" 95 | integrity sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg== 96 | 97 | "@esbuild/darwin-arm64@0.20.2": 98 | version "0.20.2" 99 | resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz#6e8517a045ddd86ae30c6608c8475ebc0c4000bb" 100 | integrity sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA== 101 | 102 | "@esbuild/darwin-x64@0.17.19": 103 | version "0.17.19" 104 | resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz#7751d236dfe6ce136cce343dce69f52d76b7f6cb" 105 | integrity sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw== 106 | 107 | "@esbuild/darwin-x64@0.20.2": 108 | version "0.20.2" 109 | resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz#90ed098e1f9dd8a9381695b207e1cff45540a0d0" 110 | integrity sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA== 111 | 112 | "@esbuild/freebsd-arm64@0.17.19": 113 | version "0.17.19" 114 | resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz#cacd171665dd1d500f45c167d50c6b7e539d5fd2" 115 | integrity sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ== 116 | 117 | "@esbuild/freebsd-arm64@0.20.2": 118 | version "0.20.2" 119 | resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz#d71502d1ee89a1130327e890364666c760a2a911" 120 | integrity sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw== 121 | 122 | "@esbuild/freebsd-x64@0.17.19": 123 | version "0.17.19" 124 | resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz#0769456eee2a08b8d925d7c00b79e861cb3162e4" 125 | integrity sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ== 126 | 127 | "@esbuild/freebsd-x64@0.20.2": 128 | version "0.20.2" 129 | resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz#aa5ea58d9c1dd9af688b8b6f63ef0d3d60cea53c" 130 | integrity sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw== 131 | 132 | "@esbuild/linux-arm64@0.17.19": 133 | version "0.17.19" 134 | resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz#38e162ecb723862c6be1c27d6389f48960b68edb" 135 | integrity sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg== 136 | 137 | "@esbuild/linux-arm64@0.20.2": 138 | version "0.20.2" 139 | resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz#055b63725df678379b0f6db9d0fa85463755b2e5" 140 | integrity sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A== 141 | 142 | "@esbuild/linux-arm@0.17.19": 143 | version "0.17.19" 144 | resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz#1a2cd399c50040184a805174a6d89097d9d1559a" 145 | integrity sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA== 146 | 147 | "@esbuild/linux-arm@0.20.2": 148 | version "0.20.2" 149 | resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz#76b3b98cb1f87936fbc37f073efabad49dcd889c" 150 | integrity sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg== 151 | 152 | "@esbuild/linux-ia32@0.17.19": 153 | version "0.17.19" 154 | resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz#e28c25266b036ce1cabca3c30155222841dc035a" 155 | integrity sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ== 156 | 157 | "@esbuild/linux-ia32@0.20.2": 158 | version "0.20.2" 159 | resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz#c0e5e787c285264e5dfc7a79f04b8b4eefdad7fa" 160 | integrity sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig== 161 | 162 | "@esbuild/linux-loong64@0.17.19": 163 | version "0.17.19" 164 | resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz#0f887b8bb3f90658d1a0117283e55dbd4c9dcf72" 165 | integrity sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ== 166 | 167 | "@esbuild/linux-loong64@0.20.2": 168 | version "0.20.2" 169 | resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz#a6184e62bd7cdc63e0c0448b83801001653219c5" 170 | integrity sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ== 171 | 172 | "@esbuild/linux-mips64el@0.17.19": 173 | version "0.17.19" 174 | resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz#f5d2a0b8047ea9a5d9f592a178ea054053a70289" 175 | integrity sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A== 176 | 177 | "@esbuild/linux-mips64el@0.20.2": 178 | version "0.20.2" 179 | resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz#d08e39ce86f45ef8fc88549d29c62b8acf5649aa" 180 | integrity sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA== 181 | 182 | "@esbuild/linux-ppc64@0.17.19": 183 | version "0.17.19" 184 | resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz#876590e3acbd9fa7f57a2c7d86f83717dbbac8c7" 185 | integrity sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg== 186 | 187 | "@esbuild/linux-ppc64@0.20.2": 188 | version "0.20.2" 189 | resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz#8d252f0b7756ffd6d1cbde5ea67ff8fd20437f20" 190 | integrity sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg== 191 | 192 | "@esbuild/linux-riscv64@0.17.19": 193 | version "0.17.19" 194 | resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz#7f49373df463cd9f41dc34f9b2262d771688bf09" 195 | integrity sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA== 196 | 197 | "@esbuild/linux-riscv64@0.20.2": 198 | version "0.20.2" 199 | resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz#19f6dcdb14409dae607f66ca1181dd4e9db81300" 200 | integrity sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg== 201 | 202 | "@esbuild/linux-s390x@0.17.19": 203 | version "0.17.19" 204 | resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz#e2afd1afcaf63afe2c7d9ceacd28ec57c77f8829" 205 | integrity sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q== 206 | 207 | "@esbuild/linux-s390x@0.20.2": 208 | version "0.20.2" 209 | resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz#3c830c90f1a5d7dd1473d5595ea4ebb920988685" 210 | integrity sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ== 211 | 212 | "@esbuild/linux-x64@0.17.19": 213 | version "0.17.19" 214 | resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz#8a0e9738b1635f0c53389e515ae83826dec22aa4" 215 | integrity sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw== 216 | 217 | "@esbuild/linux-x64@0.20.2": 218 | version "0.20.2" 219 | resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz#86eca35203afc0d9de0694c64ec0ab0a378f6fff" 220 | integrity sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw== 221 | 222 | "@esbuild/netbsd-x64@0.17.19": 223 | version "0.17.19" 224 | resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz#c29fb2453c6b7ddef9a35e2c18b37bda1ae5c462" 225 | integrity sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q== 226 | 227 | "@esbuild/netbsd-x64@0.20.2": 228 | version "0.20.2" 229 | resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz#e771c8eb0e0f6e1877ffd4220036b98aed5915e6" 230 | integrity sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ== 231 | 232 | "@esbuild/openbsd-x64@0.17.19": 233 | version "0.17.19" 234 | resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz#95e75a391403cb10297280d524d66ce04c920691" 235 | integrity sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g== 236 | 237 | "@esbuild/openbsd-x64@0.20.2": 238 | version "0.20.2" 239 | resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz#9a795ae4b4e37e674f0f4d716f3e226dd7c39baf" 240 | integrity sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ== 241 | 242 | "@esbuild/sunos-x64@0.17.19": 243 | version "0.17.19" 244 | resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz#722eaf057b83c2575937d3ffe5aeb16540da7273" 245 | integrity sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg== 246 | 247 | "@esbuild/sunos-x64@0.20.2": 248 | version "0.20.2" 249 | resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz#7df23b61a497b8ac189def6e25a95673caedb03f" 250 | integrity sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w== 251 | 252 | "@esbuild/win32-arm64@0.17.19": 253 | version "0.17.19" 254 | resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz#9aa9dc074399288bdcdd283443e9aeb6b9552b6f" 255 | integrity sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag== 256 | 257 | "@esbuild/win32-arm64@0.20.2": 258 | version "0.20.2" 259 | resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz#f1ae5abf9ca052ae11c1bc806fb4c0f519bacf90" 260 | integrity sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ== 261 | 262 | "@esbuild/win32-ia32@0.17.19": 263 | version "0.17.19" 264 | resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz#95ad43c62ad62485e210f6299c7b2571e48d2b03" 265 | integrity sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw== 266 | 267 | "@esbuild/win32-ia32@0.20.2": 268 | version "0.20.2" 269 | resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz#241fe62c34d8e8461cd708277813e1d0ba55ce23" 270 | integrity sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ== 271 | 272 | "@esbuild/win32-x64@0.17.19": 273 | version "0.17.19" 274 | resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz#8cfaf2ff603e9aabb910e9c0558c26cf32744061" 275 | integrity sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA== 276 | 277 | "@esbuild/win32-x64@0.20.2": 278 | version "0.20.2" 279 | resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc" 280 | integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ== 281 | 282 | "@fastify/busboy@^2.0.0": 283 | version "2.1.1" 284 | resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.1.tgz#b9da6a878a371829a0502c9b6c1c143ef6663f4d" 285 | integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA== 286 | 287 | "@jridgewell/resolve-uri@^3.0.3": 288 | version "3.1.2" 289 | resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" 290 | integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== 291 | 292 | "@jridgewell/sourcemap-codec@^1.4.10": 293 | version "1.4.15" 294 | resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" 295 | integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== 296 | 297 | "@jridgewell/trace-mapping@0.3.9": 298 | version "0.3.9" 299 | resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" 300 | integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== 301 | dependencies: 302 | "@jridgewell/resolve-uri" "^3.0.3" 303 | "@jridgewell/sourcemap-codec" "^1.4.10" 304 | 305 | "@playwright/test@^1.43.1": 306 | version "1.43.1" 307 | resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.43.1.tgz#16728a59eb8ce0f60472f98d8886d6cab0fa3e42" 308 | integrity sha512-HgtQzFgNEEo4TE22K/X7sYTYNqEMMTZmFS8kTq6m8hXj+m1D8TgwgIbumHddJa9h4yl4GkKb8/bgAl2+g7eDgA== 309 | dependencies: 310 | playwright "1.43.1" 311 | 312 | "@rollup/rollup-android-arm-eabi@4.14.1": 313 | version "4.14.1" 314 | resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.1.tgz#ca0501dd836894216cb9572848c5dde4bfca3bec" 315 | integrity sha512-fH8/o8nSUek8ceQnT7K4EQbSiV7jgkHq81m9lWZFIXjJ7lJzpWXbQFpT/Zh6OZYnpFykvzC3fbEvEAFZu03dPA== 316 | 317 | "@rollup/rollup-android-arm64@4.14.1": 318 | version "4.14.1" 319 | resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.14.1.tgz#154ca7e4f815d2e442ffc62ee7f64aee8b2547b0" 320 | integrity sha512-Y/9OHLjzkunF+KGEoJr3heiD5X9OLa8sbT1lm0NYeKyaM3oMhhQFvPB0bNZYJwlq93j8Z6wSxh9+cyKQaxS7PQ== 321 | 322 | "@rollup/rollup-darwin-arm64@4.14.1": 323 | version "4.14.1" 324 | resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.1.tgz#02b522ab6ccc2c504634651985ff8e657b42c055" 325 | integrity sha512-+kecg3FY84WadgcuSVm6llrABOdQAEbNdnpi5X3UwWiFVhZIZvKgGrF7kmLguvxHNQy+UuRV66cLVl3S+Rkt+Q== 326 | 327 | "@rollup/rollup-darwin-x64@4.14.1": 328 | version "4.14.1" 329 | resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.14.1.tgz#217737f9f73de729fdfd7d529afebb6c8283f554" 330 | integrity sha512-2pYRzEjVqq2TB/UNv47BV/8vQiXkFGVmPFwJb+1E0IFFZbIX8/jo1olxqqMbo6xCXf8kabANhp5bzCij2tFLUA== 331 | 332 | "@rollup/rollup-linux-arm-gnueabihf@4.14.1": 333 | version "4.14.1" 334 | resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.14.1.tgz#a87e478ab3f697c7f4e74c8b1cac1e0667f8f4be" 335 | integrity sha512-mS6wQ6Do6/wmrF9aTFVpIJ3/IDXhg1EZcQFYHZLHqw6AzMBjTHWnCG35HxSqUNphh0EHqSM6wRTT8HsL1C0x5g== 336 | 337 | "@rollup/rollup-linux-arm64-gnu@4.14.1": 338 | version "4.14.1" 339 | resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.1.tgz#4da6830eca27e5f4ca15f9197e5660952ca185c6" 340 | integrity sha512-p9rGKYkHdFMzhckOTFubfxgyIO1vw//7IIjBBRVzyZebWlzRLeNhqxuSaZ7kCEKVkm/kuC9fVRW9HkC/zNRG2w== 341 | 342 | "@rollup/rollup-linux-arm64-musl@4.14.1": 343 | version "4.14.1" 344 | resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.14.1.tgz#0b0ed35720aebc8f5e501d370a9ea0f686ead1e0" 345 | integrity sha512-nDY6Yz5xS/Y4M2i9JLQd3Rofh5OR8Bn8qe3Mv/qCVpHFlwtZSBYSPaU4mrGazWkXrdQ98GB//H0BirGR/SKFSw== 346 | 347 | "@rollup/rollup-linux-powerpc64le-gnu@4.14.1": 348 | version "4.14.1" 349 | resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.14.1.tgz#024ad04d162726f25e62915851f7df69a9677c17" 350 | integrity sha512-im7HE4VBL+aDswvcmfx88Mp1soqL9OBsdDBU8NqDEYtkri0qV0THhQsvZtZeNNlLeCUQ16PZyv7cqutjDF35qw== 351 | 352 | "@rollup/rollup-linux-riscv64-gnu@4.14.1": 353 | version "4.14.1" 354 | resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.14.1.tgz#180694d1cd069ddbe22022bb5b1bead3b7de581c" 355 | integrity sha512-RWdiHuAxWmzPJgaHJdpvUUlDz8sdQz4P2uv367T2JocdDa98iRw2UjIJ4QxSyt077mXZT2X6pKfT2iYtVEvOFw== 356 | 357 | "@rollup/rollup-linux-s390x-gnu@4.14.1": 358 | version "4.14.1" 359 | resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.14.1.tgz#f7b4e2b0ca49be4e34f9ef0b548c926d94edee87" 360 | integrity sha512-VMgaGQ5zRX6ZqV/fas65/sUGc9cPmsntq2FiGmayW9KMNfWVG/j0BAqImvU4KTeOOgYSf1F+k6at1UfNONuNjA== 361 | 362 | "@rollup/rollup-linux-x64-gnu@4.14.1": 363 | version "4.14.1" 364 | resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.1.tgz#0aaf79e5b9ccf7db3084fe6c3f2d2873a27d5af4" 365 | integrity sha512-9Q7DGjZN+hTdJomaQ3Iub4m6VPu1r94bmK2z3UeWP3dGUecRC54tmVu9vKHTm1bOt3ASoYtEz6JSRLFzrysKlA== 366 | 367 | "@rollup/rollup-linux-x64-musl@4.14.1": 368 | version "4.14.1" 369 | resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.14.1.tgz#38f0a37ca5015eb07dff86a1b6f94279c179f4ed" 370 | integrity sha512-JNEG/Ti55413SsreTguSx0LOVKX902OfXIKVg+TCXO6Gjans/k9O6ww9q3oLGjNDaTLxM+IHFMeXy/0RXL5R/g== 371 | 372 | "@rollup/rollup-win32-arm64-msvc@4.14.1": 373 | version "4.14.1" 374 | resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.14.1.tgz#84d48c55740ede42c77373f76e85f368633a0cc3" 375 | integrity sha512-ryS22I9y0mumlLNwDFYZRDFLwWh3aKaC72CWjFcFvxK0U6v/mOkM5Up1bTbCRAhv3kEIwW2ajROegCIQViUCeA== 376 | 377 | "@rollup/rollup-win32-ia32-msvc@4.14.1": 378 | version "4.14.1" 379 | resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.14.1.tgz#c1e0bc39e20e760f0a526ddf14ae0543af796605" 380 | integrity sha512-TdloItiGk+T0mTxKx7Hp279xy30LspMso+GzQvV2maYePMAWdmrzqSNZhUpPj3CGw12aGj57I026PgLCTu8CGg== 381 | 382 | "@rollup/rollup-win32-x64-msvc@4.14.1": 383 | version "4.14.1" 384 | resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.1.tgz#299eee74b7d87e116083ac5b1ce8dd9434668294" 385 | integrity sha512-wQGI+LY/Py20zdUPq+XCem7JcPOyzIJBm3dli+56DJsQOHbnXZFEwgmnC6el1TPAfC8lBT3m+z69RmLykNUbew== 386 | 387 | "@types/estree@1.0.5": 388 | version "1.0.5" 389 | resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" 390 | integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== 391 | 392 | "@types/json-schema@^7.0.15": 393 | version "7.0.15" 394 | resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" 395 | integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== 396 | 397 | "@types/node-forge@^1.3.0": 398 | version "1.3.11" 399 | resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.11.tgz#0972ea538ddb0f4d9c2fa0ec5db5724773a604da" 400 | integrity sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ== 401 | dependencies: 402 | "@types/node" "*" 403 | 404 | "@types/node@*", "@types/node@^20.12.7": 405 | version "20.12.7" 406 | resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.7.tgz#04080362fa3dd6c5822061aa3124f5c152cff384" 407 | integrity sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg== 408 | dependencies: 409 | undici-types "~5.26.4" 410 | 411 | acorn-walk@^8.2.0: 412 | version "8.3.2" 413 | resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.2.tgz#7703af9415f1b6db9315d6895503862e231d34aa" 414 | integrity sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A== 415 | 416 | acorn@^8.8.0: 417 | version "8.11.3" 418 | resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" 419 | integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== 420 | 421 | anymatch@~3.1.2: 422 | version "3.1.3" 423 | resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" 424 | integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== 425 | dependencies: 426 | normalize-path "^3.0.0" 427 | picomatch "^2.0.4" 428 | 429 | as-table@^1.0.36: 430 | version "1.0.55" 431 | resolved "https://registry.yarnpkg.com/as-table/-/as-table-1.0.55.tgz#dc984da3937745de902cea1d45843c01bdbbec4f" 432 | integrity sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ== 433 | dependencies: 434 | printable-characters "^1.0.42" 435 | 436 | balanced-match@^1.0.0: 437 | version "1.0.2" 438 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" 439 | integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== 440 | 441 | binary-extensions@^2.0.0: 442 | version "2.3.0" 443 | resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" 444 | integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== 445 | 446 | blake3-wasm@^2.1.5: 447 | version "2.1.5" 448 | resolved "https://registry.yarnpkg.com/blake3-wasm/-/blake3-wasm-2.1.5.tgz#b22dbb84bc9419ed0159caa76af4b1b132e6ba52" 449 | integrity sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g== 450 | 451 | brace-expansion@^2.0.1: 452 | version "2.0.1" 453 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" 454 | integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== 455 | dependencies: 456 | balanced-match "^1.0.0" 457 | 458 | braces@~3.0.2: 459 | version "3.0.2" 460 | resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" 461 | integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== 462 | dependencies: 463 | fill-range "^7.0.1" 464 | 465 | capnp-ts@^0.7.0: 466 | version "0.7.0" 467 | resolved "https://registry.yarnpkg.com/capnp-ts/-/capnp-ts-0.7.0.tgz#16fd8e76b667d002af8fcf4bf92bf15d1a7b54a9" 468 | integrity sha512-XKxXAC3HVPv7r674zP0VC3RTXz+/JKhfyw94ljvF80yynK6VkTnqE3jMuN8b3dUVmmc43TjyxjW4KTsmB3c86g== 469 | dependencies: 470 | debug "^4.3.1" 471 | tslib "^2.2.0" 472 | 473 | chokidar@^3.5.3: 474 | version "3.6.0" 475 | resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" 476 | integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== 477 | dependencies: 478 | anymatch "~3.1.2" 479 | braces "~3.0.2" 480 | glob-parent "~5.1.2" 481 | is-binary-path "~2.1.0" 482 | is-glob "~4.0.1" 483 | normalize-path "~3.0.0" 484 | readdirp "~3.6.0" 485 | optionalDependencies: 486 | fsevents "~2.3.2" 487 | 488 | commander@^12.0.0: 489 | version "12.0.0" 490 | resolved "https://registry.yarnpkg.com/commander/-/commander-12.0.0.tgz#b929db6df8546080adfd004ab215ed48cf6f2592" 491 | integrity sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA== 492 | 493 | cookie@^0.5.0: 494 | version "0.5.0" 495 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" 496 | integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== 497 | 498 | data-uri-to-buffer@^2.0.0: 499 | version "2.0.2" 500 | resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz#d296973d5a4897a5dbe31716d118211921f04770" 501 | integrity sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA== 502 | 503 | debug@^4.3.1: 504 | version "4.3.4" 505 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" 506 | integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== 507 | dependencies: 508 | ms "2.1.2" 509 | 510 | esbuild@0.17.19: 511 | version "0.17.19" 512 | resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.17.19.tgz#087a727e98299f0462a3d0bcdd9cd7ff100bd955" 513 | integrity sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw== 514 | optionalDependencies: 515 | "@esbuild/android-arm" "0.17.19" 516 | "@esbuild/android-arm64" "0.17.19" 517 | "@esbuild/android-x64" "0.17.19" 518 | "@esbuild/darwin-arm64" "0.17.19" 519 | "@esbuild/darwin-x64" "0.17.19" 520 | "@esbuild/freebsd-arm64" "0.17.19" 521 | "@esbuild/freebsd-x64" "0.17.19" 522 | "@esbuild/linux-arm" "0.17.19" 523 | "@esbuild/linux-arm64" "0.17.19" 524 | "@esbuild/linux-ia32" "0.17.19" 525 | "@esbuild/linux-loong64" "0.17.19" 526 | "@esbuild/linux-mips64el" "0.17.19" 527 | "@esbuild/linux-ppc64" "0.17.19" 528 | "@esbuild/linux-riscv64" "0.17.19" 529 | "@esbuild/linux-s390x" "0.17.19" 530 | "@esbuild/linux-x64" "0.17.19" 531 | "@esbuild/netbsd-x64" "0.17.19" 532 | "@esbuild/openbsd-x64" "0.17.19" 533 | "@esbuild/sunos-x64" "0.17.19" 534 | "@esbuild/win32-arm64" "0.17.19" 535 | "@esbuild/win32-ia32" "0.17.19" 536 | "@esbuild/win32-x64" "0.17.19" 537 | 538 | esbuild@^0.20.1: 539 | version "0.20.2" 540 | resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.20.2.tgz#9d6b2386561766ee6b5a55196c6d766d28c87ea1" 541 | integrity sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g== 542 | optionalDependencies: 543 | "@esbuild/aix-ppc64" "0.20.2" 544 | "@esbuild/android-arm" "0.20.2" 545 | "@esbuild/android-arm64" "0.20.2" 546 | "@esbuild/android-x64" "0.20.2" 547 | "@esbuild/darwin-arm64" "0.20.2" 548 | "@esbuild/darwin-x64" "0.20.2" 549 | "@esbuild/freebsd-arm64" "0.20.2" 550 | "@esbuild/freebsd-x64" "0.20.2" 551 | "@esbuild/linux-arm" "0.20.2" 552 | "@esbuild/linux-arm64" "0.20.2" 553 | "@esbuild/linux-ia32" "0.20.2" 554 | "@esbuild/linux-loong64" "0.20.2" 555 | "@esbuild/linux-mips64el" "0.20.2" 556 | "@esbuild/linux-ppc64" "0.20.2" 557 | "@esbuild/linux-riscv64" "0.20.2" 558 | "@esbuild/linux-s390x" "0.20.2" 559 | "@esbuild/linux-x64" "0.20.2" 560 | "@esbuild/netbsd-x64" "0.20.2" 561 | "@esbuild/openbsd-x64" "0.20.2" 562 | "@esbuild/sunos-x64" "0.20.2" 563 | "@esbuild/win32-arm64" "0.20.2" 564 | "@esbuild/win32-ia32" "0.20.2" 565 | "@esbuild/win32-x64" "0.20.2" 566 | 567 | escape-string-regexp@^4.0.0: 568 | version "4.0.0" 569 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" 570 | integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== 571 | 572 | estree-walker@^0.6.1: 573 | version "0.6.1" 574 | resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.6.1.tgz#53049143f40c6eb918b23671d1fe3219f3a1b362" 575 | integrity sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w== 576 | 577 | exit-hook@^2.2.1: 578 | version "2.2.1" 579 | resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-2.2.1.tgz#007b2d92c6428eda2b76e7016a34351586934593" 580 | integrity sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw== 581 | 582 | fill-range@^7.0.1: 583 | version "7.0.1" 584 | resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" 585 | integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== 586 | dependencies: 587 | to-regex-range "^5.0.1" 588 | 589 | fs.realpath@^1.0.0: 590 | version "1.0.0" 591 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 592 | integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== 593 | 594 | fsevents@2.3.2: 595 | version "2.3.2" 596 | resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" 597 | integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== 598 | 599 | fsevents@~2.3.2, fsevents@~2.3.3: 600 | version "2.3.3" 601 | resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" 602 | integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== 603 | 604 | function-bind@^1.1.2: 605 | version "1.1.2" 606 | resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" 607 | integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== 608 | 609 | get-source@^2.0.12: 610 | version "2.0.12" 611 | resolved "https://registry.yarnpkg.com/get-source/-/get-source-2.0.12.tgz#0b47d57ea1e53ce0d3a69f4f3d277eb8047da944" 612 | integrity sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w== 613 | dependencies: 614 | data-uri-to-buffer "^2.0.0" 615 | source-map "^0.6.1" 616 | 617 | glob-parent@~5.1.2: 618 | version "5.1.2" 619 | resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" 620 | integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== 621 | dependencies: 622 | is-glob "^4.0.1" 623 | 624 | glob-to-regexp@^0.4.1: 625 | version "0.4.1" 626 | resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" 627 | integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== 628 | 629 | glob@^8.0.3: 630 | version "8.1.0" 631 | resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" 632 | integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== 633 | dependencies: 634 | fs.realpath "^1.0.0" 635 | inflight "^1.0.4" 636 | inherits "2" 637 | minimatch "^5.0.1" 638 | once "^1.3.0" 639 | 640 | hasown@^2.0.0: 641 | version "2.0.2" 642 | resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" 643 | integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== 644 | dependencies: 645 | function-bind "^1.1.2" 646 | 647 | inflight@^1.0.4: 648 | version "1.0.6" 649 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 650 | integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== 651 | dependencies: 652 | once "^1.3.0" 653 | wrappy "1" 654 | 655 | inherits@2: 656 | version "2.0.4" 657 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 658 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 659 | 660 | is-binary-path@~2.1.0: 661 | version "2.1.0" 662 | resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" 663 | integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== 664 | dependencies: 665 | binary-extensions "^2.0.0" 666 | 667 | is-core-module@^2.13.0: 668 | version "2.13.1" 669 | resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" 670 | integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== 671 | dependencies: 672 | hasown "^2.0.0" 673 | 674 | is-extglob@^2.1.1: 675 | version "2.1.1" 676 | resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" 677 | integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== 678 | 679 | is-glob@^4.0.1, is-glob@~4.0.1: 680 | version "4.0.3" 681 | resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" 682 | integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== 683 | dependencies: 684 | is-extglob "^2.1.1" 685 | 686 | is-number@^7.0.0: 687 | version "7.0.0" 688 | resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" 689 | integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== 690 | 691 | json5@^2.2.3: 692 | version "2.2.3" 693 | resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" 694 | integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== 695 | 696 | magic-string@^0.25.3: 697 | version "0.25.9" 698 | resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c" 699 | integrity sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ== 700 | dependencies: 701 | sourcemap-codec "^1.4.8" 702 | 703 | mime@^3.0.0: 704 | version "3.0.0" 705 | resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" 706 | integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== 707 | 708 | miniflare@3.20240405.1: 709 | version "3.20240405.1" 710 | resolved "https://registry.yarnpkg.com/miniflare/-/miniflare-3.20240405.1.tgz#4a6e335c0f2fbccb4cd94a9abf40e8af4a767254" 711 | integrity sha512-oShOR/ckr9JTO1bkPQH0nXvuSgJjoE+E5+M1tvP01Q8Z+Q0GJnzU2+FDYUH8yIK/atHv7snU8yy0X6KWVn1YdQ== 712 | dependencies: 713 | "@cspotcode/source-map-support" "0.8.1" 714 | acorn "^8.8.0" 715 | acorn-walk "^8.2.0" 716 | capnp-ts "^0.7.0" 717 | exit-hook "^2.2.1" 718 | glob-to-regexp "^0.4.1" 719 | stoppable "^1.1.0" 720 | undici "^5.28.2" 721 | workerd "1.20240405.0" 722 | ws "^8.11.0" 723 | youch "^3.2.2" 724 | zod "^3.20.6" 725 | 726 | minimatch@^5.0.1: 727 | version "5.1.6" 728 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" 729 | integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== 730 | dependencies: 731 | brace-expansion "^2.0.1" 732 | 733 | ms@2.1.2: 734 | version "2.1.2" 735 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 736 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== 737 | 738 | mustache@^4.2.0: 739 | version "4.2.0" 740 | resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64" 741 | integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ== 742 | 743 | nanoid@^3.3.3, nanoid@^3.3.7: 744 | version "3.3.7" 745 | resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" 746 | integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== 747 | 748 | node-forge@^1: 749 | version "1.3.1" 750 | resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" 751 | integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== 752 | 753 | normalize-path@^3.0.0, normalize-path@~3.0.0: 754 | version "3.0.0" 755 | resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" 756 | integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== 757 | 758 | once@^1.3.0: 759 | version "1.4.0" 760 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 761 | integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== 762 | dependencies: 763 | wrappy "1" 764 | 765 | path-parse@^1.0.7: 766 | version "1.0.7" 767 | resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" 768 | integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== 769 | 770 | path-to-regexp@^6.2.0: 771 | version "6.2.2" 772 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.2.tgz#324377a83e5049cbecadc5554d6a63a9a4866b36" 773 | integrity sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw== 774 | 775 | picocolors@^1.0.0: 776 | version "1.0.0" 777 | resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" 778 | integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== 779 | 780 | picomatch@^2.0.4, picomatch@^2.2.1: 781 | version "2.3.1" 782 | resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" 783 | integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== 784 | 785 | playwright-core@1.43.1: 786 | version "1.43.1" 787 | resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.43.1.tgz#0eafef9994c69c02a1a3825a4343e56c99c03b02" 788 | integrity sha512-EI36Mto2Vrx6VF7rm708qSnesVQKbxEWvPrfA1IPY6HgczBplDx7ENtx+K2n4kJ41sLLkuGfmb0ZLSSXlDhqPg== 789 | 790 | playwright@1.43.1: 791 | version "1.43.1" 792 | resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.43.1.tgz#8ad08984ac66c9ef3d0db035be54dd7ec9f1c7d9" 793 | integrity sha512-V7SoH0ai2kNt1Md9E3Gwas5B9m8KR2GVvwZnAI6Pg0m3sh7UvgiYhRrhsziCmqMJNouPckiOhk8T+9bSAK0VIA== 794 | dependencies: 795 | playwright-core "1.43.1" 796 | optionalDependencies: 797 | fsevents "2.3.2" 798 | 799 | postcss@^8.4.38: 800 | version "8.4.38" 801 | resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" 802 | integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== 803 | dependencies: 804 | nanoid "^3.3.7" 805 | picocolors "^1.0.0" 806 | source-map-js "^1.2.0" 807 | 808 | printable-characters@^1.0.42: 809 | version "1.0.42" 810 | resolved "https://registry.yarnpkg.com/printable-characters/-/printable-characters-1.0.42.tgz#3f18e977a9bd8eb37fcc4ff5659d7be90868b3d8" 811 | integrity sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ== 812 | 813 | readdirp@~3.6.0: 814 | version "3.6.0" 815 | resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" 816 | integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== 817 | dependencies: 818 | picomatch "^2.2.1" 819 | 820 | resolve.exports@^2.0.2: 821 | version "2.0.2" 822 | resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800" 823 | integrity sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg== 824 | 825 | resolve@^1.22.8: 826 | version "1.22.8" 827 | resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" 828 | integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== 829 | dependencies: 830 | is-core-module "^2.13.0" 831 | path-parse "^1.0.7" 832 | supports-preserve-symlinks-flag "^1.0.0" 833 | 834 | rollup-plugin-inject@^3.0.0: 835 | version "3.0.2" 836 | resolved "https://registry.yarnpkg.com/rollup-plugin-inject/-/rollup-plugin-inject-3.0.2.tgz#e4233855bfba6c0c12a312fd6649dff9a13ee9f4" 837 | integrity sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w== 838 | dependencies: 839 | estree-walker "^0.6.1" 840 | magic-string "^0.25.3" 841 | rollup-pluginutils "^2.8.1" 842 | 843 | rollup-plugin-node-polyfills@^0.2.1: 844 | version "0.2.1" 845 | resolved "https://registry.yarnpkg.com/rollup-plugin-node-polyfills/-/rollup-plugin-node-polyfills-0.2.1.tgz#53092a2744837164d5b8a28812ba5f3ff61109fd" 846 | integrity sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA== 847 | dependencies: 848 | rollup-plugin-inject "^3.0.0" 849 | 850 | rollup-pluginutils@^2.8.1: 851 | version "2.8.2" 852 | resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz#72f2af0748b592364dbd3389e600e5a9444a351e" 853 | integrity sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ== 854 | dependencies: 855 | estree-walker "^0.6.1" 856 | 857 | rollup@^4.13.0: 858 | version "4.14.1" 859 | resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.14.1.tgz#228d5159c3f4d8745bd24819d734bc6c6ca87c09" 860 | integrity sha512-4LnHSdd3QK2pa1J6dFbfm1HN0D7vSK/ZuZTsdyUAlA6Rr1yTouUTL13HaDOGJVgby461AhrNGBS7sCGXXtT+SA== 861 | dependencies: 862 | "@types/estree" "1.0.5" 863 | optionalDependencies: 864 | "@rollup/rollup-android-arm-eabi" "4.14.1" 865 | "@rollup/rollup-android-arm64" "4.14.1" 866 | "@rollup/rollup-darwin-arm64" "4.14.1" 867 | "@rollup/rollup-darwin-x64" "4.14.1" 868 | "@rollup/rollup-linux-arm-gnueabihf" "4.14.1" 869 | "@rollup/rollup-linux-arm64-gnu" "4.14.1" 870 | "@rollup/rollup-linux-arm64-musl" "4.14.1" 871 | "@rollup/rollup-linux-powerpc64le-gnu" "4.14.1" 872 | "@rollup/rollup-linux-riscv64-gnu" "4.14.1" 873 | "@rollup/rollup-linux-s390x-gnu" "4.14.1" 874 | "@rollup/rollup-linux-x64-gnu" "4.14.1" 875 | "@rollup/rollup-linux-x64-musl" "4.14.1" 876 | "@rollup/rollup-win32-arm64-msvc" "4.14.1" 877 | "@rollup/rollup-win32-ia32-msvc" "4.14.1" 878 | "@rollup/rollup-win32-x64-msvc" "4.14.1" 879 | fsevents "~2.3.2" 880 | 881 | safe-stable-stringify@^2.4.3: 882 | version "2.4.3" 883 | resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz#138c84b6f6edb3db5f8ef3ef7115b8f55ccbf886" 884 | integrity sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g== 885 | 886 | selfsigned@^2.0.1: 887 | version "2.4.1" 888 | resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.4.1.tgz#560d90565442a3ed35b674034cec4e95dceb4ae0" 889 | integrity sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q== 890 | dependencies: 891 | "@types/node-forge" "^1.3.0" 892 | node-forge "^1" 893 | 894 | source-map-js@^1.2.0: 895 | version "1.2.0" 896 | resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" 897 | integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== 898 | 899 | source-map@0.6.1, source-map@^0.6.1: 900 | version "0.6.1" 901 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" 902 | integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== 903 | 904 | sourcemap-codec@^1.4.8: 905 | version "1.4.8" 906 | resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" 907 | integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== 908 | 909 | stacktracey@^2.1.8: 910 | version "2.1.8" 911 | resolved "https://registry.yarnpkg.com/stacktracey/-/stacktracey-2.1.8.tgz#bf9916020738ce3700d1323b32bd2c91ea71199d" 912 | integrity sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw== 913 | dependencies: 914 | as-table "^1.0.36" 915 | get-source "^2.0.12" 916 | 917 | stoppable@^1.1.0: 918 | version "1.1.0" 919 | resolved "https://registry.yarnpkg.com/stoppable/-/stoppable-1.1.0.tgz#32da568e83ea488b08e4d7ea2c3bcc9d75015d5b" 920 | integrity sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw== 921 | 922 | supports-preserve-symlinks-flag@^1.0.0: 923 | version "1.0.0" 924 | resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" 925 | integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== 926 | 927 | to-regex-range@^5.0.1: 928 | version "5.0.1" 929 | resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" 930 | integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== 931 | dependencies: 932 | is-number "^7.0.0" 933 | 934 | ts-json-schema-generator@^1.5.0: 935 | version "1.5.1" 936 | resolved "https://registry.yarnpkg.com/ts-json-schema-generator/-/ts-json-schema-generator-1.5.1.tgz#7759c421240be86d393a884ad186f926b22332db" 937 | integrity sha512-apX5qG2+NA66j7b4AJm8q/DpdTeOsjfh7A3LpKsUiil0FepkNwtN28zYgjrsiiya2/OPhsr/PSjX5FUYg79rCg== 938 | dependencies: 939 | "@types/json-schema" "^7.0.15" 940 | commander "^12.0.0" 941 | glob "^8.0.3" 942 | json5 "^2.2.3" 943 | normalize-path "^3.0.0" 944 | safe-stable-stringify "^2.4.3" 945 | typescript "~5.4.2" 946 | 947 | tslib@^2.2.0: 948 | version "2.6.2" 949 | resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" 950 | integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== 951 | 952 | typescript@^5.2.2, typescript@~5.4.2: 953 | version "5.4.5" 954 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" 955 | integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== 956 | 957 | undici-types@~5.26.4: 958 | version "5.26.5" 959 | resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" 960 | integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== 961 | 962 | undici@^5.28.2: 963 | version "5.28.4" 964 | resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.4.tgz#6b280408edb6a1a604a9b20340f45b422e373068" 965 | integrity sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g== 966 | dependencies: 967 | "@fastify/busboy" "^2.0.0" 968 | 969 | vite@^5.2.0: 970 | version "5.2.8" 971 | resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.8.tgz#a99e09939f1a502992381395ce93efa40a2844aa" 972 | integrity sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA== 973 | dependencies: 974 | esbuild "^0.20.1" 975 | postcss "^8.4.38" 976 | rollup "^4.13.0" 977 | optionalDependencies: 978 | fsevents "~2.3.3" 979 | 980 | workerd@1.20240405.0: 981 | version "1.20240405.0" 982 | resolved "https://registry.yarnpkg.com/workerd/-/workerd-1.20240405.0.tgz#c5cb6563b0a9b16dc981f5138f4b71fa2f976683" 983 | integrity sha512-AWrOSBh4Ll7sBWHuh0aywm8hDkKqsZmcwnDB0PVGszWZM5mndNBI5iJ/8haXVpdoyqkJQEVdhET9JDi4yU8tRg== 984 | optionalDependencies: 985 | "@cloudflare/workerd-darwin-64" "1.20240405.0" 986 | "@cloudflare/workerd-darwin-arm64" "1.20240405.0" 987 | "@cloudflare/workerd-linux-64" "1.20240405.0" 988 | "@cloudflare/workerd-linux-arm64" "1.20240405.0" 989 | "@cloudflare/workerd-windows-64" "1.20240405.0" 990 | 991 | wrangler@^3.50.0: 992 | version "3.50.0" 993 | resolved "https://registry.yarnpkg.com/wrangler/-/wrangler-3.50.0.tgz#012e5dd9162e59b717a5a239a7e2b99b7270990a" 994 | integrity sha512-JlLuch+6DtaC5HGp8YD9Au++XvMv34g3ySdlB5SyPbaObELi8P9ZID5vgyf9AA75djzxL7cuNOk1YdKCJEuq0w== 995 | dependencies: 996 | "@cloudflare/kv-asset-handler" "0.3.1" 997 | "@esbuild-plugins/node-globals-polyfill" "^0.2.3" 998 | "@esbuild-plugins/node-modules-polyfill" "^0.2.2" 999 | blake3-wasm "^2.1.5" 1000 | chokidar "^3.5.3" 1001 | esbuild "0.17.19" 1002 | miniflare "3.20240405.1" 1003 | nanoid "^3.3.3" 1004 | path-to-regexp "^6.2.0" 1005 | resolve "^1.22.8" 1006 | resolve.exports "^2.0.2" 1007 | selfsigned "^2.0.1" 1008 | source-map "0.6.1" 1009 | ts-json-schema-generator "^1.5.0" 1010 | xxhash-wasm "^1.0.1" 1011 | optionalDependencies: 1012 | fsevents "~2.3.2" 1013 | 1014 | wrappy@1: 1015 | version "1.0.2" 1016 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 1017 | integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== 1018 | 1019 | ws@^8.11.0: 1020 | version "8.16.0" 1021 | resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4" 1022 | integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ== 1023 | 1024 | xxhash-wasm@^1.0.1: 1025 | version "1.0.2" 1026 | resolved "https://registry.yarnpkg.com/xxhash-wasm/-/xxhash-wasm-1.0.2.tgz#ecc0f813219b727af4d5f3958ca6becee2f2f1ff" 1027 | integrity sha512-ibF0Or+FivM9lNrg+HGJfVX8WJqgo+kCLDc4vx6xMeTce7Aj+DLttKbxxRR/gNLSAelRc1omAPlJ77N/Jem07A== 1028 | 1029 | youch@^3.2.2: 1030 | version "3.3.3" 1031 | resolved "https://registry.yarnpkg.com/youch/-/youch-3.3.3.tgz#50cfdf5bc395ce664a5073e31b712ff4a859d928" 1032 | integrity sha512-qSFXUk3UZBLfggAW3dJKg0BMblG5biqSF8M34E06o5CSsZtH92u9Hqmj2RzGiHDi64fhe83+4tENFP2DB6t6ZA== 1033 | dependencies: 1034 | cookie "^0.5.0" 1035 | mustache "^4.2.0" 1036 | stacktracey "^2.1.8" 1037 | 1038 | zod@^3.20.6: 1039 | version "3.22.4" 1040 | resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff" 1041 | integrity sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg== 1042 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | 3 | export default antfu({ 4 | formatters: { 5 | html: true, 6 | }, 7 | ignores: [ 8 | '**/playwright-report/', 9 | '**/test-results/', 10 | '**/dist/', 11 | ], 12 | }, { 13 | rules: { 14 | 'array-bracket-newline': [ 15 | 'error', 16 | 'consistent', 17 | ], 18 | 'array-element-newline': [ 19 | 'error', 20 | 'consistent', 21 | ], 22 | 'object-curly-newline': [ 23 | 'error', 24 | { consistent: true }, 25 | ], 26 | 'object-property-newline': [ 27 | 'error', 28 | { 29 | allowAllPropertiesOnSameLine: true, 30 | }, 31 | ], 32 | 33 | 'no-console': 'off', 34 | 'node/prefer-global/process': 'off', 35 | 'ts/consistent-type-definitions': 'off', 36 | }, 37 | }) 38 | -------------------------------------------------------------------------------- /lib/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | # dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /lib/dist/Animation.d.ts: -------------------------------------------------------------------------------- 1 | import { Driver } from './Driver'; 2 | import { AnimationConstructor, AnimationHooks, AnimationRender } from './Animation.types'; 3 | 4 | export declare class Animation { 5 | id: string; 6 | cssAnimation: CSSAnimation; 7 | /** You can store your custom data here to use between hooks */ 8 | data: any; 9 | /** Reference to linked `Driver` instance */ 10 | driver: Driver; 11 | /** You can access domElement this animation is belongs to */ 12 | domElement: HTMLElement; 13 | hooks: AnimationHooks; 14 | constructor({ id, cssAnimation, hooks, driver, domElement }: AnimationConstructor); 15 | render({ driverProgress }: AnimationRender): false | undefined; 16 | } 17 | -------------------------------------------------------------------------------- /lib/dist/Animation.types.d.ts: -------------------------------------------------------------------------------- 1 | import { Driver } from './Driver'; 2 | import { Animation } from './Animation'; 3 | 4 | export type AnimationConstructor = { 5 | id: string; 6 | hooks: AnimationHooks; 7 | cssAnimation: CSSAnimation; 8 | driver: Driver; 9 | domElement: HTMLElement; 10 | }; 11 | export type AnimationHooks = { 12 | onInit?: (animation: Animation) => void; 13 | /** You can `return false` inside your hook, it will cancel rendering. Or you can return a number, it will be an animation currentTime */ 14 | onBeforeRender?: (animation: Animation) => void | undefined | boolean | number; 15 | onAfterRender?: (animation: Animation) => void; 16 | }; 17 | type AnimationConfiguration = string | { 18 | name: string; 19 | hooks: AnimationHooks; 20 | }; 21 | export type ElementSelector = string | { 22 | selector: string; 23 | animations: AnimationConfiguration[]; 24 | }; 25 | export type AnimationRender = { 26 | driverProgress: number; 27 | }; 28 | export {}; 29 | -------------------------------------------------------------------------------- /lib/dist/Driver.d.ts: -------------------------------------------------------------------------------- 1 | import { DriverBorderConstructor, DriverBorderUpdateLimits, DriverCalculateProgress, DriverConstructor, DriverHelperConstructor, DriverHelperUpdateLimits, DriverHooks, DriverRender, DriverUpdateLimits } from './Driver.types'; 2 | import { TheSupersonicPlugin } from './TheSupersonicPlugin'; 3 | import { Animation } from './Animation'; 4 | 5 | /** 6 | * The main purpose of Driver is to calculate current progress from 0 to 1 depending on current scroll and position of 'start' and 'end' elements 7 | */ 8 | export declare class Driver { 9 | id: string; 10 | /** Progress is generated by script and means how much of the scroll covered right now. Minimum value: 0, maximum value: 1, float number with 4 numbers after decimal point precision */ 11 | progress: number; 12 | /** You can store your custom data here to use between hooks */ 13 | data: any; 14 | /** Start is HTML element. When it appears on the screen, driver will start an animation */ 15 | start: DriverBorder; 16 | /** End is HTML element. When it appears on the screen, driver will stop an animation */ 17 | end: DriverBorder; 18 | /** Link to plugin instance to be able to access global variables like 'scroll', 'screenHeight' */ 19 | plugin: TheSupersonicPlugin; 20 | animations: Map; 21 | /** Helper is an element which need for IntersectionObserver to activate or deactive driver */ 22 | helper: DriverHelper; 23 | hooks: DriverHooks; 24 | constructor({ id, start, end, plugin, elements, hooks }: DriverConstructor); 25 | /** Driver calculates its progress and then renders all of it's properties with progress value */ 26 | render({ scroll, renderedInitially, consoleColor }: DriverRender): false | undefined; 27 | /** Calculates current driver progress, depending on current scroll and top offset of DOM elements */ 28 | calculateProgress({ scroll, start, end }: DriverCalculateProgress): number; 29 | /** Recalculates DOM elements top offset */ 30 | updateLimits({ scroll, screenHeight }: DriverUpdateLimits): void; 31 | /** Activates driver when it becomes visible on the screen */ 32 | activate(): void; 33 | /** Deactivates driver when it's progress becomes 0 or 1' */ 34 | deactivate(): void; 35 | } 36 | /** An HTML Element. It's top offset serves as indicator of where driver starts and where it ends */ 37 | declare class DriverBorder { 38 | /** Associated DOM element */ 39 | domElement: HTMLElement; 40 | /** Top means amount of scroll needed to border activate or deactivate driver */ 41 | top: number; 42 | constructor({ domElement, type, driver }: DriverBorderConstructor); 43 | /** Recalculates top offset */ 44 | updateLimits({ scroll, screenHeight }: DriverBorderUpdateLimits): void; 45 | } 46 | /** A helper HTML element, which connects to Border instances and starts being tracked by IntersectionObserver */ 47 | declare class DriverHelper { 48 | /** DOM element which is dynamically generated by plugin */ 49 | domElement: HTMLElement; 50 | pluginId: string; 51 | debug: boolean; 52 | constructor({ id, pluginId, debug }: DriverHelperConstructor); 53 | /** Sets position of helper */ 54 | updateLimits({ top, height }: DriverHelperUpdateLimits): void; 55 | /** Deletes helper DOM element */ 56 | uninit(): void; 57 | } 58 | export {}; 59 | -------------------------------------------------------------------------------- /lib/dist/Driver.types.d.ts: -------------------------------------------------------------------------------- 1 | import { TheSupersonicPlugin } from './TheSupersonicPlugin'; 2 | import { ElementSelector } from './Animation.types'; 3 | import { Driver } from './Driver'; 4 | 5 | export type DriverConfiguration = { 6 | id?: string; 7 | start: DriverBorderConfiguration; 8 | end: DriverBorderConfiguration; 9 | elements?: ElementSelector[]; 10 | hooks?: DriverHooks; 11 | }; 12 | export type DriverConstructor = Omit & { 13 | id: string; 14 | plugin: TheSupersonicPlugin; 15 | }; 16 | export type DriverRender = { 17 | scroll: number; 18 | renderedInitially: boolean; 19 | consoleColor?: string; 20 | }; 21 | export type DriverCalculateProgress = { 22 | scroll: number; 23 | start: number; 24 | end: number; 25 | }; 26 | export type DriverHooks = { 27 | onBeforeInit?: (driver: Driver) => void; 28 | onAfterInit?: (driver: Driver) => void; 29 | /** You can `return false` inside your hook, it will cancel rendering */ 30 | onBeforeRender?: (driver: Driver) => void | undefined | boolean; 31 | onAfterRender?: (driver: Driver) => void; 32 | onActivation?: (driver: Driver) => void; 33 | onDeactivation?: (driver: Driver) => void; 34 | onUpdateLimits?: (driver: Driver) => void; 35 | }; 36 | type DriverBorderConfiguration = HTMLElement | string | null; 37 | export type DriverBorderConstructor = { 38 | domElement: DriverBorderConfiguration; 39 | driver: Driver; 40 | type: 'start' | 'end'; 41 | }; 42 | export type DriverHelperConstructor = { 43 | id: string; 44 | pluginId: string; 45 | debug?: boolean; 46 | }; 47 | export type DriverUpdateLimits = { 48 | scroll: number; 49 | screenHeight: number; 50 | }; 51 | export type DriverBorderUpdateLimits = { 52 | scroll: number; 53 | screenHeight: number; 54 | }; 55 | export type DriverHelperUpdateLimits = { 56 | top: number; 57 | height: number; 58 | }; 59 | export {}; 60 | -------------------------------------------------------------------------------- /lib/dist/Observer.d.ts: -------------------------------------------------------------------------------- 1 | import { Constructor } from './Observer.types'; 2 | 3 | export declare class Observer { 4 | instance: IntersectionObserver; 5 | constructor({ observables, driverInstances }: Constructor); 6 | uninit(): void; 7 | } 8 | -------------------------------------------------------------------------------- /lib/dist/Observer.types.d.ts: -------------------------------------------------------------------------------- 1 | import { Driver } from './Driver'; 2 | 3 | export type Constructor = { 4 | observables: HTMLElement[]; 5 | driverInstances: Map; 6 | }; 7 | -------------------------------------------------------------------------------- /lib/dist/TheSupersonicPlugin.d.ts: -------------------------------------------------------------------------------- 1 | import { DriverConfiguration } from './Driver.types'; 2 | import { PluginConfiguration, PluginHooks, PluginRender } from './TheSupersonicPlugin.types'; 3 | import { Observer } from './Observer'; 4 | import { Driver } from './Driver'; 5 | 6 | /** 7 | * 8 | * Main class handling all of the logic. To initialize the plugin, you create a new instance of this class 9 | * 10 | * @example 11 | * const plugin = new TheSupersonicPlugin([ 12 | * { 13 | * start: '.start', 14 | * end: '.end', 15 | * elements: ['.animatable-element'] 16 | * } 17 | * ]); 18 | * 19 | */ 20 | export declare class TheSupersonicPlugin { 21 | /** Unique id of this running instance. You explicitly define it or let plugin auto generate it */ 22 | id: string; 23 | /** Current window scrollY */ 24 | scroll: number; 25 | /** Current screen height */ 26 | screenHeight: number; 27 | /** Required to get all of the drivers render at once to stand on their first frame */ 28 | renderedInitially: boolean; 29 | /** Used to cancelAnimationFrame on 'uninit()' */ 30 | rafId: number; 31 | /** Color of console messages in dev mode. It changes each frame to make it more convenient to visually separate frames */ 32 | consoleColor: string; 33 | /** IntersectionObserver instance */ 34 | observer: Observer | null; 35 | /** Debounced resize listener */ 36 | onResize: EventListener | null; 37 | /** You can store your custom data here to use between hooks */ 38 | data: any; 39 | /** Make helper visible */ 40 | debug: boolean; 41 | hooks: PluginHooks; 42 | driverInstances: Map; 43 | driverActiveInstances: Set; 44 | constructor(drivers: DriverConfiguration[], configuration?: PluginConfiguration); 45 | /** Removes all of the plugin stuff (useful for SPA) */ 46 | uninit(): void; 47 | /** Main rendering cycle. Active drivers are visible ones */ 48 | render({ useActiveDrivers }: PluginRender): false | undefined; 49 | /** Updates global scroll and driver DOM elements top offset. Called once on page load and each time after window.resize */ 50 | updateLimits(): void; 51 | updateScroll(): void; 52 | /** Dirty hack for calculating screen height. We can't just use "window.innerHeight" because it "jumps" on mobile phones when you scroll and toolbar collapses */ 53 | updateScreenHeight(): void; 54 | } 55 | -------------------------------------------------------------------------------- /lib/dist/TheSupersonicPlugin.types.d.ts: -------------------------------------------------------------------------------- 1 | import { TheSupersonicPlugin } from './TheSupersonicPlugin'; 2 | 3 | export type PluginConfiguration = { 4 | id?: string; 5 | hooks?: PluginHooks; 6 | debug?: boolean; 7 | }; 8 | export type PluginHooks = { 9 | onBeforeInit?: (plugin: TheSupersonicPlugin) => void; 10 | onAfterInit?: (plugin: TheSupersonicPlugin) => void; 11 | /** You can `return false` inside your hook, it will cancel rendering */ 12 | onBeforeRender?: (plugin: TheSupersonicPlugin) => void | undefined | boolean; 13 | onAfterRender?: (plugin: TheSupersonicPlugin) => void; 14 | onBeforeResize?: (plugin: TheSupersonicPlugin) => void; 15 | onAfterResize?: (plugin: TheSupersonicPlugin) => void; 16 | }; 17 | export type PluginRender = { 18 | useActiveDrivers: boolean; 19 | }; 20 | -------------------------------------------------------------------------------- /lib/dist/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Animation } from './Animation'; 2 | import { Driver } from './Driver'; 3 | import { TheSupersonicPlugin } from './TheSupersonicPlugin'; 4 | 5 | export { TheSupersonicPlugin, Driver, Animation, }; 6 | -------------------------------------------------------------------------------- /lib/dist/the-supersonic-plugin-for-scroll-based-animation.iife.js: -------------------------------------------------------------------------------- 1 | var TheSupersonicPluginWrapper=function(h){"use strict";var $=Object.defineProperty;var P=(h,l,c)=>l in h?$(h,l,{enumerable:!0,configurable:!0,writable:!0,value:c}):h[l]=c;var s=(h,l,c)=>(P(h,typeof l!="symbol"?l+"":l,c),c);function l(a,t){return t=10**t,~~(a*t)/t}function c(a,t){let e;return function(){clearTimeout(e),e=setTimeout(()=>a.apply(this),t)}}function A(){return Math.random().toString(16).substring(2)}class E{constructor({id:t,cssAnimation:e,hooks:i,driver:o,domElement:r}){s(this,"id");s(this,"cssAnimation");s(this,"data",{});s(this,"driver");s(this,"domElement");s(this,"hooks");this.id=t,this.driver=o,this.cssAnimation=e,this.hooks=i,this.domElement=r,this.hooks.onInit&&this.hooks.onInit(this)}render({driverProgress:t}){let e=t*1e4;if(this.hooks.onBeforeRender){const i=this.hooks.onBeforeRender(this);if(typeof i=="number")e=i;else if(typeof i=="boolean"&&!i)return!1}this.cssAnimation.currentTime=e,this.hooks.onAfterRender&&this.hooks.onAfterRender(this)}}const u={colors:["red","blue","orange","yellow"],colorIndex:0};class g{constructor({id:t,start:e,end:i,plugin:o,elements:r=[],hooks:m={}}){s(this,"id");s(this,"progress",0);s(this,"data",{});s(this,"start");s(this,"end");s(this,"plugin");s(this,"animations",new Map);s(this,"helper");s(this,"hooks");this.id=t,this.plugin=o,this.hooks=m,this.hooks.onBeforeInit&&this.hooks.onBeforeInit(this),this.start=new b({domElement:e,type:"start",driver:this}),this.end=new b({domElement:i,type:"end",driver:this}),this.helper=new S({id:t,pluginId:this.plugin.id,debug:o.debug}),r.forEach(n=>{const f=typeof n=="string"?n:n.selector,v=Array.from(document.querySelectorAll(f));if(v.length===0)throw new Error(`Can't find element "${f}"`);v.forEach((R,x)=>{const k=R.getAnimations(),I=[];typeof n=="string"?(k.length,k.forEach(d=>{I.push({cssAnimation:d,hooks:{}})})):typeof n=="object"&&n.animations.forEach(d=>{let p="",y={};typeof d=="string"?p=d:typeof d=="object"&&(p=d.name,y=d.hooks);const w=k.find(z=>z.animationName===p);w&&I.push({cssAnimation:w,hooks:y})}),I.forEach(d=>{const p=`${this.id}---${f}---${x}---${d.cssAnimation.animationName}`,y=new E({id:p,driver:this,cssAnimation:d.cssAnimation,hooks:d.hooks,domElement:R});this.animations.set(p,y)})})}),this.hooks.onAfterInit&&this.hooks.onAfterInit(this)}render({scroll:t,renderedInitially:e,consoleColor:i="#000000"}){const o=this.progress;if(this.progress=this.calculateProgress({scroll:t,start:this.start.top,end:this.end.top}),this.hooks.onBeforeRender){const r=this.hooks.onBeforeRender(this);if(typeof r=="boolean"&&!r)return!1}if(o!==this.progress||!e){for(const r of this.animations.values())r.render({driverProgress:this.progress});this.hooks.onAfterRender&&this.hooks.onAfterRender(this)}}calculateProgress({scroll:t,start:e,end:i}){let o=(t-e)/(i-e);return o<0?o=0:o>1?o=1:o=l(o,4),o}updateLimits({scroll:t,screenHeight:e}){this.start.updateLimits({scroll:t,screenHeight:e}),this.end.updateLimits({scroll:t,screenHeight:e});const i=this.start.top+e;this.helper.updateLimits({top:i,height:this.end.top-i}),this.hooks.onUpdateLimits&&this.hooks.onUpdateLimits(this)}activate(){this.plugin.driverActiveInstances.add(this),this.hooks.onActivation&&this.hooks.onActivation(this)}deactivate(){this.plugin.driverActiveInstances.delete(this),this.hooks.onDeactivation&&this.hooks.onDeactivation(this)}}class b{constructor({domElement:t,type:e,driver:i}){s(this,"domElement");s(this,"top",0);if(typeof t=="string"&&(t=document.querySelector(t)),!t)throw new Error(`Can't find "${e}" HTMLElement for driver "${i.id}"`);this.domElement=t}updateLimits({scroll:t,screenHeight:e}){this.top=~~this.domElement.getBoundingClientRect().top+t-e}}class S{constructor({id:t,pluginId:e,debug:i=!1}){s(this,"domElement");s(this,"pluginId");s(this,"debug");if(this.pluginId=e,this.debug=i,this.domElement=document.createElement("i"),this.domElement.style.position="absolute",this.domElement.style.left="0",this.domElement.style.width="1px",this.domElement.setAttribute("data-supersonic-driver",t),this.domElement.setAttribute("data-supersonic-type","helper"),this.domElement.setAttribute("data-supersonic-plugin-id",this.pluginId),this.domElement.classList.add("supersonic-helper"),document.body.appendChild(this.domElement),i){const r=Array.from(document.querySelectorAll("[data-supersonic-type='helper']")).indexOf(this.domElement);this.domElement.style.left=`${r*10}px`,this.domElement.style.width="10px",this.domElement.style.minHeight="50px",this.domElement.style.background=u.colors[u.colorIndex],this.domElement.style.zIndex="100000",u.colorIndex===3?u.colorIndex=0:u.colorIndex++,this.domElement.style.opacity="0.75"}}updateLimits({top:t,height:e}){e<=0&&(e=1),this.domElement.style.setProperty("top",`${t}px`),this.domElement.style.setProperty("height",`${e}px`)}uninit(){this.domElement.remove()}}class B{constructor({observables:t,driverInstances:e}){s(this,"instance");this.instance=new IntersectionObserver(i=>{i.forEach(o=>{const m=o.target.dataset.supersonicDriver,n=e.get(m);if(!n)throw new Error(`Observer can't find driver "${m}"`);o.isIntersecting?n.activate():n.deactivate()})}),t.forEach(i=>{this.instance.observe(i)})}uninit(){this.instance.disconnect()}}class L{constructor(t,e){s(this,"id");s(this,"scroll",0);s(this,"screenHeight",0);s(this,"renderedInitially",!1);s(this,"rafId",0);s(this,"consoleColor","#ffffff");s(this,"observer",null);s(this,"onResize",null);s(this,"data",{});s(this,"debug");s(this,"hooks",{});s(this,"driverInstances",new Map);s(this,"driverActiveInstances",new Set);var r,m;this.id=(e==null?void 0:e.id)??A(),this.hooks=(e==null?void 0:e.hooks)??{},this.debug=(e==null?void 0:e.debug)??!1,(r=this.hooks)!=null&&r.onBeforeInit&&this.hooks.onBeforeInit(this),t.forEach(n=>{const f=n.id??A(),v=new g({id:f,hooks:n.hooks,start:n.start,end:n.end,elements:n.elements,plugin:this});this.driverInstances.set(f,v)}),this.updateLimits();const i=Array.from(document.querySelectorAll(`[data-supersonic-type="helper"][data-supersonic-plugin-id="${this.id}"]`));this.observer=new B({observables:i,driverInstances:this.driverInstances});const o=()=>{var n;(n=this.hooks)!=null&&n.onBeforeResize&&this.hooks.onBeforeResize(this),this.updateLimits(),this.render({useActiveDrivers:!1}),this.hooks.onAfterResize&&this.hooks.onAfterResize(this)};this.onResize=c(o,250),window.addEventListener("resize",this.onResize),this.render({useActiveDrivers:!1}),this.renderedInitially=!0,(m=this.hooks)!=null&&m.onAfterInit&&this.hooks.onAfterInit(this)}uninit(){for(const t of this.driverInstances.values())t.helper.uninit();this.driverInstances.clear(),this.driverActiveInstances.clear(),this.observer.uninit(),cancelAnimationFrame(this.rafId),window.removeEventListener("resize",this.onResize)}render({useActiveDrivers:t}){if(this.updateScroll(),this.hooks.onBeforeRender){const i=this.hooks.onBeforeRender(this);if(typeof i=="boolean"&&!i)return!1}const e=t?this.driverActiveInstances.values():this.driverInstances.values();for(const i of e)i.render({scroll:this.scroll,renderedInitially:this.renderedInitially,consoleColor:this.consoleColor});this.rafId=requestAnimationFrame(()=>{this.render({useActiveDrivers:!0})}),this.hooks.onAfterRender&&this.hooks.onAfterRender(this)}updateLimits(){this.updateScreenHeight(),this.updateScroll();for(const t of this.driverInstances.values())t.updateLimits({scroll:this.scroll,screenHeight:this.screenHeight})}updateScroll(){this.scroll=window.scrollY||document.documentElement.scrollTop}updateScreenHeight(){const t={position:"absolute",left:"0",top:"0",height:"100vh",width:"1px",zIndex:"-1",visibility:"hidden"},e=document.createElement("div");for(const i in t)e.style.setProperty(i,t[i]);document.body.appendChild(e),this.screenHeight=e.clientHeight,e.remove()}}return h.Animation=E,h.Driver=g,h.TheSupersonicPlugin=L,Object.defineProperty(h,Symbol.toStringTag,{value:"Module"}),h}({}); 2 | -------------------------------------------------------------------------------- /lib/dist/the-supersonic-plugin-for-scroll-based-animation.js: -------------------------------------------------------------------------------- 1 | var g = Object.defineProperty; 2 | var R = (r, e, t) => e in r ? g(r, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : r[e] = t; 3 | var s = (r, e, t) => (R(r, typeof e != "symbol" ? e + "" : e, t), t); 4 | function w(r, e) { 5 | return e = 10 ** e, ~~(r * e) / e; 6 | } 7 | function x(r, e) { 8 | let t; 9 | return function() { 10 | clearTimeout(t), t = setTimeout(() => r.apply(this), e); 11 | }; 12 | } 13 | function I() { 14 | return Math.random().toString(16).substring(2); 15 | } 16 | class B { 17 | constructor({ id: e, cssAnimation: t, hooks: i, driver: o, domElement: h }) { 18 | s(this, "id"); 19 | s(this, "cssAnimation"); 20 | /** You can store your custom data here to use between hooks */ 21 | s(this, "data", {}); 22 | /** Reference to linked `Driver` instance */ 23 | s(this, "driver"); 24 | /** You can access domElement this animation is belongs to */ 25 | s(this, "domElement"); 26 | s(this, "hooks"); 27 | this.id = e, this.driver = o, this.cssAnimation = t, this.hooks = i, this.domElement = h, this.hooks.onInit && this.hooks.onInit(this); 28 | } 29 | render({ driverProgress: e }) { 30 | let t = e * 1e4; 31 | if (this.hooks.onBeforeRender) { 32 | const i = this.hooks.onBeforeRender(this); 33 | if (typeof i == "number") 34 | t = i; 35 | else if (typeof i == "boolean" && !i) 36 | return !1; 37 | } 38 | this.cssAnimation.currentTime = t, this.hooks.onAfterRender && this.hooks.onAfterRender(this); 39 | } 40 | } 41 | const m = { 42 | colors: ["red", "blue", "orange", "yellow"], 43 | colorIndex: 0 44 | }; 45 | class L { 46 | constructor({ id: e, start: t, end: i, plugin: o, elements: h = [], hooks: l = {} }) { 47 | s(this, "id"); 48 | /** Progress is generated by script and means how much of the scroll covered right now. Minimum value: 0, maximum value: 1, float number with 4 numbers after decimal point precision */ 49 | s(this, "progress", 0); 50 | /** You can store your custom data here to use between hooks */ 51 | s(this, "data", {}); 52 | /** Start is HTML element. When it appears on the screen, driver will start an animation */ 53 | s(this, "start"); 54 | /** End is HTML element. When it appears on the screen, driver will stop an animation */ 55 | s(this, "end"); 56 | /** Link to plugin instance to be able to access global variables like 'scroll', 'screenHeight' */ 57 | s(this, "plugin"); 58 | s(this, "animations", /* @__PURE__ */ new Map()); 59 | /** Helper is an element which need for IntersectionObserver to activate or deactive driver */ 60 | s(this, "helper"); 61 | s(this, "hooks"); 62 | this.id = e, this.plugin = o, this.hooks = l, this.hooks.onBeforeInit && this.hooks.onBeforeInit(this), this.start = new A({ 63 | domElement: t, 64 | type: "start", 65 | driver: this 66 | }), this.end = new A({ 67 | domElement: i, 68 | type: "end", 69 | driver: this 70 | }), this.helper = new S({ 71 | id: e, 72 | pluginId: this.plugin.id, 73 | debug: o.debug 74 | }), h.forEach((n) => { 75 | const a = typeof n == "string" ? n : n.selector, p = Array.from(document.querySelectorAll(a)); 76 | if (p.length === 0) 77 | throw new Error(`Can't find element "${a}"`); 78 | p.forEach((k, E) => { 79 | const u = k.getAnimations(), v = []; 80 | typeof n == "string" ? (u.length, u.forEach((d) => { 81 | v.push({ 82 | cssAnimation: d, 83 | hooks: {} 84 | }); 85 | })) : typeof n == "object" && n.animations.forEach((d) => { 86 | let c = "", f = {}; 87 | typeof d == "string" ? c = d : typeof d == "object" && (c = d.name, f = d.hooks); 88 | const y = u.find((b) => b.animationName === c); 89 | y && v.push({ 90 | cssAnimation: y, 91 | hooks: f 92 | }); 93 | }), v.forEach((d) => { 94 | const c = `${this.id}---${a}---${E}---${d.cssAnimation.animationName}`, f = new B({ 95 | id: c, 96 | driver: this, 97 | cssAnimation: d.cssAnimation, 98 | hooks: d.hooks, 99 | domElement: k 100 | }); 101 | this.animations.set(c, f); 102 | }); 103 | }); 104 | }), this.hooks.onAfterInit && this.hooks.onAfterInit(this); 105 | } 106 | /** Driver calculates its progress and then renders all of it's properties with progress value */ 107 | render({ scroll: e, renderedInitially: t, consoleColor: i = "#000000" }) { 108 | const o = this.progress; 109 | if (this.progress = this.calculateProgress({ 110 | scroll: e, 111 | start: this.start.top, 112 | end: this.end.top 113 | }), this.hooks.onBeforeRender) { 114 | const h = this.hooks.onBeforeRender(this); 115 | if (typeof h == "boolean" && !h) 116 | return !1; 117 | } 118 | if (o !== this.progress || !t) { 119 | for (const h of this.animations.values()) 120 | h.render({ driverProgress: this.progress }); 121 | this.hooks.onAfterRender && this.hooks.onAfterRender(this); 122 | } 123 | } 124 | /** Calculates current driver progress, depending on current scroll and top offset of DOM elements */ 125 | calculateProgress({ scroll: e, start: t, end: i }) { 126 | let o = (e - t) / (i - t); 127 | return o < 0 ? o = 0 : o > 1 ? o = 1 : o = w(o, 4), o; 128 | } 129 | /** Recalculates DOM elements top offset */ 130 | updateLimits({ scroll: e, screenHeight: t }) { 131 | this.start.updateLimits({ scroll: e, screenHeight: t }), this.end.updateLimits({ scroll: e, screenHeight: t }); 132 | const i = this.start.top + t; 133 | this.helper.updateLimits({ 134 | top: i, 135 | height: this.end.top - i 136 | }), this.hooks.onUpdateLimits && this.hooks.onUpdateLimits(this); 137 | } 138 | /** Activates driver when it becomes visible on the screen */ 139 | activate() { 140 | this.plugin.driverActiveInstances.add(this), this.hooks.onActivation && this.hooks.onActivation(this); 141 | } 142 | /** Deactivates driver when it's progress becomes 0 or 1' */ 143 | deactivate() { 144 | this.plugin.driverActiveInstances.delete(this), this.hooks.onDeactivation && this.hooks.onDeactivation(this); 145 | } 146 | } 147 | class A { 148 | constructor({ domElement: e, type: t, driver: i }) { 149 | /** Associated DOM element */ 150 | s(this, "domElement"); 151 | /** Top means amount of scroll needed to border activate or deactivate driver */ 152 | s(this, "top", 0); 153 | if (typeof e == "string" && (e = document.querySelector(e)), !e) 154 | throw new Error(`Can't find "${t}" HTMLElement for driver "${i.id}"`); 155 | this.domElement = e; 156 | } 157 | /** Recalculates top offset */ 158 | updateLimits({ scroll: e, screenHeight: t }) { 159 | this.top = ~~this.domElement.getBoundingClientRect().top + e - t; 160 | } 161 | } 162 | class S { 163 | constructor({ id: e, pluginId: t, debug: i = !1 }) { 164 | /** DOM element which is dynamically generated by plugin */ 165 | s(this, "domElement"); 166 | s(this, "pluginId"); 167 | s(this, "debug"); 168 | if (this.pluginId = t, this.debug = i, this.domElement = document.createElement("i"), this.domElement.style.position = "absolute", this.domElement.style.left = "0", this.domElement.style.width = "1px", this.domElement.setAttribute("data-supersonic-driver", e), this.domElement.setAttribute("data-supersonic-type", "helper"), this.domElement.setAttribute("data-supersonic-plugin-id", this.pluginId), this.domElement.classList.add("supersonic-helper"), document.body.appendChild(this.domElement), i) { 169 | const h = Array.from(document.querySelectorAll("[data-supersonic-type='helper']")).indexOf(this.domElement); 170 | this.domElement.style.left = `${h * 10}px`, this.domElement.style.width = "10px", this.domElement.style.minHeight = "50px", this.domElement.style.background = m.colors[m.colorIndex], this.domElement.style.zIndex = "100000", m.colorIndex === 3 ? m.colorIndex = 0 : m.colorIndex++, this.domElement.style.opacity = "0.75"; 171 | } 172 | } 173 | /** Sets position of helper */ 174 | updateLimits({ top: e, height: t }) { 175 | t <= 0 && (t = 1), this.domElement.style.setProperty("top", `${e}px`), this.domElement.style.setProperty("height", `${t}px`); 176 | } 177 | /** Deletes helper DOM element */ 178 | uninit() { 179 | this.domElement.remove(); 180 | } 181 | } 182 | class z { 183 | constructor({ observables: e, driverInstances: t }) { 184 | s(this, "instance"); 185 | this.instance = new IntersectionObserver( 186 | (i) => { 187 | i.forEach((o) => { 188 | const l = o.target.dataset.supersonicDriver, n = t.get(l); 189 | if (!n) 190 | throw new Error(`Observer can't find driver "${l}"`); 191 | o.isIntersecting ? n.activate() : n.deactivate(); 192 | }); 193 | } 194 | ), e.forEach((i) => { 195 | this.instance.observe(i); 196 | }); 197 | } 198 | uninit() { 199 | this.instance.disconnect(); 200 | } 201 | } 202 | class D { 203 | constructor(e, t) { 204 | /** Unique id of this running instance. You explicitly define it or let plugin auto generate it */ 205 | s(this, "id"); 206 | /** Current window scrollY */ 207 | s(this, "scroll", 0); 208 | /** Current screen height */ 209 | s(this, "screenHeight", 0); 210 | /** Required to get all of the drivers render at once to stand on their first frame */ 211 | s(this, "renderedInitially", !1); 212 | /** Used to cancelAnimationFrame on 'uninit()' */ 213 | s(this, "rafId", 0); 214 | /** Color of console messages in dev mode. It changes each frame to make it more convenient to visually separate frames */ 215 | s(this, "consoleColor", "#ffffff"); 216 | /** IntersectionObserver instance */ 217 | s(this, "observer", null); 218 | /** Debounced resize listener */ 219 | s(this, "onResize", null); 220 | /** You can store your custom data here to use between hooks */ 221 | s(this, "data", {}); 222 | /** Make helper visible */ 223 | s(this, "debug"); 224 | s(this, "hooks", {}); 225 | s(this, "driverInstances", /* @__PURE__ */ new Map()); 226 | s(this, "driverActiveInstances", /* @__PURE__ */ new Set()); 227 | var h, l; 228 | this.id = (t == null ? void 0 : t.id) ?? I(), this.hooks = (t == null ? void 0 : t.hooks) ?? {}, this.debug = (t == null ? void 0 : t.debug) ?? !1, (h = this.hooks) != null && h.onBeforeInit && this.hooks.onBeforeInit(this), e.forEach((n) => { 229 | const a = n.id ?? I(), p = new L({ 230 | id: a, 231 | hooks: n.hooks, 232 | start: n.start, 233 | end: n.end, 234 | elements: n.elements, 235 | plugin: this 236 | }); 237 | this.driverInstances.set(a, p); 238 | }), this.updateLimits(); 239 | const i = Array.from(document.querySelectorAll(`[data-supersonic-type="helper"][data-supersonic-plugin-id="${this.id}"]`)); 240 | this.observer = new z({ 241 | observables: i, 242 | driverInstances: this.driverInstances 243 | }); 244 | const o = () => { 245 | var n; 246 | (n = this.hooks) != null && n.onBeforeResize && this.hooks.onBeforeResize(this), this.updateLimits(), this.render({ useActiveDrivers: !1 }), this.hooks.onAfterResize && this.hooks.onAfterResize(this); 247 | }; 248 | this.onResize = x(o, 250), window.addEventListener("resize", this.onResize), this.render({ useActiveDrivers: !1 }), this.renderedInitially = !0, (l = this.hooks) != null && l.onAfterInit && this.hooks.onAfterInit(this); 249 | } 250 | /** Removes all of the plugin stuff (useful for SPA) */ 251 | uninit() { 252 | for (const e of this.driverInstances.values()) 253 | e.helper.uninit(); 254 | this.driverInstances.clear(), this.driverActiveInstances.clear(), this.observer.uninit(), cancelAnimationFrame(this.rafId), window.removeEventListener("resize", this.onResize); 255 | } 256 | /** Main rendering cycle. Active drivers are visible ones */ 257 | render({ useActiveDrivers: e }) { 258 | if (this.updateScroll(), this.hooks.onBeforeRender) { 259 | const i = this.hooks.onBeforeRender(this); 260 | if (typeof i == "boolean" && !i) 261 | return !1; 262 | } 263 | const t = e ? this.driverActiveInstances.values() : this.driverInstances.values(); 264 | for (const i of t) 265 | i.render({ 266 | scroll: this.scroll, 267 | renderedInitially: this.renderedInitially, 268 | consoleColor: this.consoleColor 269 | }); 270 | this.rafId = requestAnimationFrame(() => { 271 | this.render({ useActiveDrivers: !0 }); 272 | }), this.hooks.onAfterRender && this.hooks.onAfterRender(this); 273 | } 274 | /** Updates global scroll and driver DOM elements top offset. Called once on page load and each time after window.resize */ 275 | updateLimits() { 276 | this.updateScreenHeight(), this.updateScroll(); 277 | for (const e of this.driverInstances.values()) 278 | e.updateLimits({ 279 | scroll: this.scroll, 280 | screenHeight: this.screenHeight 281 | }); 282 | } 283 | updateScroll() { 284 | this.scroll = window.scrollY || document.documentElement.scrollTop; 285 | } 286 | /** Dirty hack for calculating screen height. We can't just use "window.innerHeight" because it "jumps" on mobile phones when you scroll and toolbar collapses */ 287 | updateScreenHeight() { 288 | const e = { 289 | position: "absolute", 290 | left: "0", 291 | top: "0", 292 | height: "100vh", 293 | width: "1px", 294 | zIndex: "-1", 295 | visibility: "hidden" 296 | }, t = document.createElement("div"); 297 | for (const i in e) 298 | t.style.setProperty(i, e[i]); 299 | document.body.appendChild(t), this.screenHeight = t.clientHeight, t.remove(); 300 | } 301 | } 302 | export { 303 | B as Animation, 304 | L as Driver, 305 | D as TheSupersonicPlugin 306 | }; 307 | -------------------------------------------------------------------------------- /lib/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lib", 3 | "type": "module", 4 | "version": "0.0.0", 5 | "private": true, 6 | "scripts": { 7 | "build": "tsc && vite build", 8 | "minify": "terser -c -m --module dist/the-supersonic-plugin-for-scroll-based-animation.js > dist/the-supersonic-plugin-for-scroll-based-animation.min.js", 9 | "test:unit": "vitest run", 10 | "test:unit:ui": "vitest --api.host 0.0.0.0 --api.port 8000 --ui" 11 | }, 12 | "devDependencies": { 13 | "@types/node": "^20.12.7", 14 | "@vitest/ui": "^1.5.0", 15 | "jsdom": "^24.0.0", 16 | "terser": "^5.30.3", 17 | "typescript": "^5.2.2", 18 | "vite": "^5.2.0", 19 | "vite-plugin-dts": "^3.8.3", 20 | "vitest": "^1.5.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/src/Animation.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | 3 | import { Animation } from './Animation' 4 | import type { Driver } from './Driver' 5 | 6 | it('adds onInit hook', () => { 7 | const animation = new Animation({ 8 | id: 'animation-id', 9 | driver: {} as Driver, 10 | hooks: { 11 | onInit(animation) { 12 | animation.data.foo = 'bar' 13 | }, 14 | }, 15 | cssAnimation: {} as CSSAnimation, 16 | domElement: {} as HTMLElement, 17 | }) 18 | 19 | expect(animation.data.foo).toBe('bar') 20 | }) 21 | 22 | it('renders', () => { 23 | const animation = new Animation({ 24 | id: 'animation-id', 25 | driver: {} as Driver, 26 | hooks: {}, 27 | cssAnimation: { currentTime: 0 } as CSSAnimation, 28 | domElement: {} as HTMLElement, 29 | }) 30 | 31 | animation.render({ driverProgress: 0.1234 }) 32 | 33 | expect(animation.cssAnimation.currentTime).toBe(1234) 34 | }) 35 | 36 | it('adds onBeforeRender hook', () => { 37 | const animation = new Animation({ 38 | id: 'animation-id', 39 | driver: {} as Driver, 40 | hooks: { 41 | onBeforeRender(animation) { 42 | animation.data.foo = 'bar' 43 | return false 44 | }, 45 | }, 46 | cssAnimation: { currentTime: 0 } as CSSAnimation, 47 | domElement: {} as HTMLElement, 48 | }) 49 | 50 | animation.render({ driverProgress: 0.1234 }) 51 | 52 | expect(animation.data.foo).toBe('bar') 53 | expect(animation.cssAnimation.currentTime).toBe(0) 54 | }) 55 | 56 | it('manually sets animation.currentTime', () => { 57 | const animation = new Animation({ 58 | id: 'animation-id', 59 | driver: {} as Driver, 60 | hooks: { 61 | onBeforeRender() { 62 | return 100 63 | }, 64 | }, 65 | cssAnimation: { currentTime: 0 } as CSSAnimation, 66 | domElement: {} as HTMLElement, 67 | }) 68 | 69 | animation.render({ driverProgress: 0.1234 }) 70 | 71 | expect(animation.cssAnimation.currentTime).toBe(100) 72 | }) 73 | 74 | it('adds onAfterRender hook', () => { 75 | const animation = new Animation({ 76 | id: 'animation-id', 77 | driver: {} as Driver, 78 | hooks: { 79 | onAfterRender(animation) { 80 | animation.data.foo = 'bar' 81 | }, 82 | }, 83 | cssAnimation: {} as CSSAnimation, 84 | domElement: {} as HTMLElement, 85 | }) 86 | 87 | animation.render({ driverProgress: 0.1234 }) 88 | 89 | expect(animation.data.foo).toBe('bar') 90 | }) 91 | -------------------------------------------------------------------------------- /lib/src/Animation.ts: -------------------------------------------------------------------------------- 1 | import type { AnimationConstructor, AnimationHooks, AnimationRender } from './Animation.types' 2 | import type { Driver } from './Driver' 3 | 4 | export class Animation { 5 | id: string 6 | 7 | cssAnimation: CSSAnimation 8 | 9 | /** You can store your custom data here to use between hooks */ 10 | data: any = {} 11 | 12 | /** Reference to linked `Driver` instance */ 13 | driver: Driver 14 | 15 | /** You can access domElement this animation is belongs to */ 16 | domElement: HTMLElement 17 | 18 | hooks: AnimationHooks 19 | 20 | constructor({ id, cssAnimation, hooks, driver, domElement }: AnimationConstructor) { 21 | this.id = id 22 | this.driver = driver 23 | this.cssAnimation = cssAnimation 24 | this.hooks = hooks 25 | this.domElement = domElement 26 | 27 | if (this.hooks.onInit) 28 | this.hooks.onInit(this) 29 | } 30 | 31 | render({ driverProgress }: AnimationRender) { 32 | let currentTime = driverProgress * 10000 33 | 34 | if (this.hooks.onBeforeRender) { 35 | const onBeforeRenderReturn = this.hooks.onBeforeRender(this) 36 | 37 | if (typeof onBeforeRenderReturn === 'number') 38 | currentTime = onBeforeRenderReturn 39 | 40 | else if (typeof onBeforeRenderReturn === 'boolean' && !onBeforeRenderReturn) 41 | return false 42 | } 43 | 44 | this.cssAnimation.currentTime = currentTime 45 | 46 | if (this.hooks.onAfterRender) 47 | this.hooks.onAfterRender(this) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/src/Animation.types.ts: -------------------------------------------------------------------------------- 1 | import type { Animation } from './Animation' 2 | import type { Driver } from './Driver' 3 | 4 | export type AnimationConstructor = { 5 | id: string 6 | hooks: AnimationHooks 7 | cssAnimation: CSSAnimation 8 | driver: Driver 9 | domElement: HTMLElement 10 | } 11 | 12 | export type AnimationHooks = { 13 | onInit?: (animation: Animation) => void 14 | /** You can `return false` inside your hook, it will cancel rendering. Or you can return a number, it will be an animation currentTime */ 15 | onBeforeRender?: (animation: Animation) => void | undefined | boolean | number 16 | onAfterRender?: (animation: Animation) => void 17 | } 18 | 19 | type AnimationConfiguration = | 20 | string | 21 | { 22 | name: string 23 | hooks: AnimationHooks 24 | } 25 | 26 | export type ElementSelector = | 27 | string | 28 | { 29 | selector: string 30 | animations: AnimationConfiguration[] 31 | } 32 | 33 | export type AnimationRender = { 34 | driverProgress: number 35 | } 36 | -------------------------------------------------------------------------------- /lib/src/Driver.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, expect, it, vi } from 'vitest' 2 | import { Driver } from './Driver' 3 | import type { TheSupersonicPlugin } from './TheSupersonicPlugin' 4 | 5 | it('adds onBeforeInit hook', () => { 6 | const driver = new Driver({ 7 | ...driverSetup(), 8 | hooks: { 9 | onBeforeInit(driver) { 10 | driver.data.foo = 'bar' 11 | }, 12 | }, 13 | }) 14 | 15 | expect(driver.data.foo).toBe('bar') 16 | }) 17 | 18 | it('adds onAfterInit hook', () => { 19 | const driver = new Driver({ 20 | ...driverSetup(), 21 | hooks: { 22 | onAfterInit(driver) { 23 | driver.data.foo = 'bar' 24 | }, 25 | }, 26 | }) 27 | 28 | expect(driver.data.foo).toBe('bar') 29 | }) 30 | 31 | it('adds onBeforeRender hook', () => { 32 | const driver = new Driver({ 33 | ...driverSetup(), 34 | hooks: { 35 | onBeforeRender(driver) { 36 | driver.data.foo = 'bar' 37 | return false 38 | }, 39 | onAfterRender(driver) { 40 | driver.data.foo = 'foo' 41 | }, 42 | }, 43 | }) 44 | 45 | driver.render({ scroll: 0, renderedInitially: false }) 46 | 47 | expect(driver.data.foo).toBe('bar') 48 | }) 49 | 50 | it('adds onAfterRender hook', () => { 51 | const driver = new Driver({ 52 | ...driverSetup(), 53 | hooks: { 54 | onAfterRender(driver) { 55 | driver.data.foo = 'foo' 56 | }, 57 | }, 58 | }) 59 | 60 | driver.render({ scroll: 0, renderedInitially: false }) 61 | 62 | expect(driver.data.foo).toBe('foo') 63 | }) 64 | 65 | it('adds onUpdateLimits hook', () => { 66 | const driver = new Driver({ 67 | ...driverSetup(), 68 | hooks: { 69 | onUpdateLimits(driver) { 70 | driver.data.foo = 'foo' 71 | }, 72 | }, 73 | }) 74 | 75 | driver.updateLimits({ scroll: 0, screenHeight: 0 }) 76 | 77 | expect(driver.data.foo).toBe('foo') 78 | }) 79 | 80 | it('creates borders and tests "updateLimits"', () => { 81 | const driver = new Driver({ 82 | ...driverSetup(), 83 | }) 84 | 85 | driver.updateLimits({ scroll: 100, screenHeight: 1000 }) 86 | 87 | expect(driver.start.top).toBe(100) 88 | expect(driver.end.top).toBe(2100) 89 | }) 90 | 91 | it('creates helper and tests "updateLimits"', () => { 92 | const driver = new Driver({ 93 | ...driverSetup(), 94 | }) 95 | 96 | driver.helper.updateLimits({ top: 500, height: 800 }) 97 | 98 | expect(driver.helper.domElement.style.top).toBe('500px') 99 | expect(driver.helper.domElement.style.height).toBe('800px') 100 | }) 101 | 102 | it('renders and calculates progress', () => { 103 | const driver = new Driver({ 104 | ...driverSetup(), 105 | }) 106 | 107 | driver.updateLimits({ scroll: 0, screenHeight: 1000 }) 108 | 109 | driver.render({ 110 | scroll: 1000, 111 | renderedInitially: false, 112 | }) 113 | 114 | expect(driver.progress).toBe(0.5) 115 | }) 116 | 117 | it('reports an error if element is not found', () => { 118 | expect(() => { 119 | // eslint-disable-next-line no-new 120 | new Driver({ 121 | ...driverSetup(), 122 | elements: ['.foo'], 123 | }) 124 | }).toThrowError('Can\'t find element ".foo"') 125 | }) 126 | 127 | it('warns in console if element hasn\'t animations', () => { 128 | const { animatableElement } = createAnimatableElement() 129 | animatableElement.getAnimations = vi.fn(() => ([])) 130 | 131 | const consoleSpy = vi.spyOn(console, 'warn') 132 | 133 | // eslint-disable-next-line no-new 134 | new Driver({ 135 | ...driverSetup(), 136 | elements: ['.animatable-element'], 137 | }) 138 | 139 | expect(consoleSpy).toHaveBeenCalledWith('Element \".animatable-element\" hasn\'t animations') 140 | }) 141 | 142 | it('creates animations from simple config', () => { 143 | const { firstAnimation, secondAnimation } = createAnimatableElement(true) 144 | 145 | const driver = new Driver({ 146 | ...driverSetup(), 147 | elements: ['.animatable-element'], 148 | }) 149 | 150 | expect(driver.animations.get(firstAnimation)).toBeTruthy() 151 | expect(driver.animations.get(secondAnimation)).toBeTruthy() 152 | }) 153 | 154 | it('creates only specified animations', () => { 155 | const { firstAnimation, secondAnimation } = createAnimatableElement(true) 156 | 157 | const driver = new Driver({ 158 | ...driverSetup(), 159 | elements: [{ 160 | selector: '.animatable-element', 161 | animations: ['first-animation'], 162 | }], 163 | }) 164 | 165 | expect(driver.animations.get(firstAnimation)).toBeTruthy() 166 | expect(driver.animations.get(secondAnimation)).toBeFalsy() 167 | }) 168 | 169 | it('adds onInit hook to animation', () => { 170 | const { firstAnimation } = createAnimatableElement(true) 171 | 172 | const driver = new Driver({ 173 | ...driverSetup(), 174 | elements: [{ 175 | selector: '.animatable-element', 176 | animations: [{ 177 | name: 'first-animation', 178 | hooks: { 179 | onInit(animation) { 180 | animation.data.foo = 'bar' 181 | }, 182 | }, 183 | }], 184 | }], 185 | }) 186 | 187 | expect(driver.animations.get(firstAnimation)?.data.foo).toBe('bar') 188 | }) 189 | 190 | it('renders animation', () => { 191 | const { firstAnimation } = createAnimatableElement(true) 192 | 193 | const driver = new Driver({ 194 | ...driverSetup(), 195 | elements: ['.animatable-element'], 196 | }) 197 | 198 | driver.updateLimits({ scroll: 0, screenHeight: 1000 }) 199 | 200 | driver.render({ scroll: 1000, renderedInitially: false }) 201 | 202 | expect(driver.animations.get(firstAnimation)?.cssAnimation.currentTime).toBe(5000) 203 | }) 204 | 205 | it('adds onBeforeRender hook to animation', () => { 206 | const { firstAnimation } = createAnimatableElement(true) 207 | 208 | const driver = new Driver({ 209 | ...driverSetup(), 210 | elements: [{ 211 | selector: '.animatable-element', 212 | animations: [{ 213 | name: 'first-animation', 214 | hooks: { 215 | onBeforeRender(animation) { 216 | animation.data.foo = 'bar' 217 | 218 | return false 219 | }, 220 | onAfterRender(animation) { 221 | animation.data.foo = 'foo' 222 | }, 223 | }, 224 | }], 225 | }], 226 | }) 227 | 228 | driver.updateLimits({ scroll: 0, screenHeight: 1000 }) 229 | 230 | driver.render({ scroll: 550, renderedInitially: false }) 231 | 232 | expect(driver.animations.get(firstAnimation)?.data.foo).toBe('bar') 233 | }) 234 | 235 | afterEach(() => { 236 | document.body.innerHTML = '' 237 | }) 238 | 239 | function driverSetup() { 240 | const start = document.createElement('div') 241 | const end = document.createElement('div') 242 | 243 | // @ts-expect-error not full implementation of DOMRect 244 | start.getBoundingClientRect = vi.fn(() => ({ 245 | top: 1000, 246 | })) 247 | 248 | // @ts-expect-error not full implementation of DOMRect 249 | end.getBoundingClientRect = vi.fn(() => ({ 250 | top: 3000, 251 | })) 252 | 253 | return { 254 | id: 'driver-id', 255 | plugin: {} as TheSupersonicPlugin, 256 | start, 257 | end, 258 | } 259 | } 260 | 261 | function createAnimatableElement(withAnimations = false) { 262 | const animatableElement = document.createElement('div') 263 | animatableElement.classList.add('animatable-element') 264 | document.body.appendChild(animatableElement) 265 | 266 | if (withAnimations) { 267 | // @ts-expect-error not full implementation of Animation 268 | animatableElement.getAnimations = vi.fn(() => ([ 269 | { 270 | animationName: 'first-animation', 271 | currentTime: 0, 272 | }, 273 | { 274 | animationName: 'second-animation', 275 | currentTime: 0, 276 | }, 277 | ])) 278 | } 279 | 280 | const firstAnimation = 'driver-id---.animatable-element---0---first-animation' 281 | const secondAnimation = 'driver-id---.animatable-element---0---second-animation' 282 | 283 | return { 284 | animatableElement, 285 | firstAnimation, 286 | secondAnimation, 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /lib/src/Driver.ts: -------------------------------------------------------------------------------- 1 | import { toFixed } from './utils' 2 | 3 | import { Animation } from './Animation' 4 | import type { AnimationHooks } from './Animation.types' 5 | 6 | import type { TheSupersonicPlugin } from './TheSupersonicPlugin' 7 | 8 | import type { DriverBorderConstructor, DriverBorderUpdateLimits, DriverCalculateProgress, DriverConstructor, DriverHelperConstructor, DriverHelperUpdateLimits, DriverHooks, DriverRender, DriverUpdateLimits } from './Driver.types' 9 | 10 | const debugColors = { 11 | colors: ['red', 'blue', 'orange', 'yellow'], 12 | colorIndex: 0, 13 | } 14 | 15 | /** 16 | * The main purpose of Driver is to calculate current progress from 0 to 1 depending on current scroll and position of 'start' and 'end' elements 17 | */ 18 | export class Driver { 19 | id: string 20 | /** Progress is generated by script and means how much of the scroll covered right now. Minimum value: 0, maximum value: 1, float number with 4 numbers after decimal point precision */ 21 | progress = 0 22 | /** You can store your custom data here to use between hooks */ 23 | data: any = {} 24 | /** Start is HTML element. When it appears on the screen, driver will start an animation */ 25 | start: DriverBorder 26 | /** End is HTML element. When it appears on the screen, driver will stop an animation */ 27 | end: DriverBorder 28 | /** Link to plugin instance to be able to access global variables like 'scroll', 'screenHeight' */ 29 | plugin: TheSupersonicPlugin 30 | 31 | animations: Map = new Map() 32 | 33 | /** Helper is an element which need for IntersectionObserver to activate or deactive driver */ 34 | helper: DriverHelper 35 | 36 | hooks: DriverHooks 37 | 38 | constructor({ id, start, end, plugin, elements = [], hooks = {} }: DriverConstructor) { 39 | this.id = id 40 | this.plugin = plugin 41 | this.hooks = hooks 42 | 43 | if (this.hooks.onBeforeInit) 44 | this.hooks.onBeforeInit(this) 45 | 46 | // Initializing borders 47 | this.start = new DriverBorder({ 48 | domElement: start, 49 | type: 'start', 50 | driver: this, 51 | }) 52 | this.end = new DriverBorder({ 53 | domElement: end, 54 | type: 'end', 55 | driver: this, 56 | }) 57 | 58 | this.helper = new DriverHelper({ 59 | id, 60 | pluginId: this.plugin.id, 61 | debug: plugin.debug, 62 | }) 63 | 64 | // Initializing animations 65 | elements.forEach((selector) => { 66 | const actualSelector = typeof selector === 'string' ? selector : selector.selector 67 | const domElements = Array.from(document.querySelectorAll(actualSelector)) 68 | 69 | if (domElements.length === 0) 70 | throw new Error(`Can't find element "${actualSelector}"`) 71 | 72 | domElements.forEach((domElement, domElementIndex) => { 73 | const elementCssAnimations = domElement.getAnimations() as unknown as CSSAnimation[] 74 | 75 | const animationConfigurations: { 76 | cssAnimation: CSSAnimation 77 | hooks: AnimationHooks 78 | }[] = [] 79 | 80 | if (typeof selector === 'string') { 81 | if (elementCssAnimations.length === 0) 82 | console.warn(`Element "${actualSelector}" hasn't animations`) 83 | 84 | elementCssAnimations.forEach((cssAnimation) => { 85 | animationConfigurations.push({ 86 | cssAnimation, 87 | hooks: {}, 88 | }) 89 | }) 90 | } 91 | else if (typeof selector === 'object') { 92 | selector.animations.forEach((animationConfiguration) => { 93 | let animationName = '' 94 | let hooks: AnimationHooks = {} 95 | 96 | if (typeof animationConfiguration === 'string') { 97 | animationName = animationConfiguration 98 | } 99 | else if (typeof animationConfiguration === 'object') { 100 | animationName = animationConfiguration.name 101 | hooks = animationConfiguration.hooks 102 | } 103 | 104 | const cssAnimation = elementCssAnimations.find(animation => animation.animationName === animationName) 105 | 106 | if (cssAnimation) { 107 | animationConfigurations.push({ 108 | cssAnimation, 109 | hooks, 110 | }) 111 | } 112 | else { 113 | console.warn(`Element "${actualSelector}" hasn't animation: "${animationName}"`) 114 | } 115 | }) 116 | } 117 | 118 | animationConfigurations.forEach((animationConfiguration) => { 119 | const id = `${this.id}---${actualSelector}---${domElementIndex}---${animationConfiguration.cssAnimation.animationName}` 120 | const animation = new Animation({ 121 | id, 122 | driver: this, 123 | cssAnimation: animationConfiguration.cssAnimation, 124 | hooks: animationConfiguration.hooks, 125 | domElement, 126 | }) 127 | 128 | this.animations.set(id, animation) 129 | }) 130 | }) 131 | }) 132 | 133 | if (this.hooks.onAfterInit) 134 | this.hooks.onAfterInit(this) 135 | } 136 | 137 | /** Driver calculates its progress and then renders all of it's properties with progress value */ 138 | render({ scroll, renderedInitially, consoleColor = '#000000' }: DriverRender) { 139 | const oldProgress = this.progress 140 | this.progress = this.calculateProgress({ 141 | scroll, 142 | start: this.start.top, 143 | end: this.end.top, 144 | }) 145 | 146 | if (this.hooks.onBeforeRender) { 147 | const onBeforeRenderReturn = this.hooks.onBeforeRender(this) 148 | 149 | if (typeof onBeforeRenderReturn === 'boolean' && !onBeforeRenderReturn) 150 | return false 151 | } 152 | 153 | if (oldProgress !== this.progress || !renderedInitially) { 154 | console.groupCollapsed( 155 | `%cDriver "${this.id}" starts rendering, progress is ${this.progress}, scroll is ${scroll}`, 156 | `color: ${consoleColor}`, 157 | ) 158 | 159 | for (const animation of this.animations.values()) { 160 | console.log(`Animation "${animation.id}" starts rendering`) 161 | animation.render({ driverProgress: this.progress }) 162 | console.log(`Animation "${animation.id}" finished rendering, currentTime: ${animation.cssAnimation.currentTime}`) 163 | } 164 | 165 | if (this.hooks.onAfterRender) 166 | this.hooks.onAfterRender(this) 167 | 168 | console.log(`Driver "${this.id}" finished rendering`) 169 | console.groupEnd() 170 | } 171 | } 172 | 173 | /** Calculates current driver progress, depending on current scroll and top offset of DOM elements */ 174 | calculateProgress({ scroll, start, end }: DriverCalculateProgress): number { 175 | let progress = (scroll - start) / (end - start) 176 | if (progress < 0) 177 | progress = 0 178 | else if (progress > 1) 179 | progress = 1 180 | else progress = toFixed(progress, 4) 181 | 182 | return progress 183 | } 184 | 185 | /** Recalculates DOM elements top offset */ 186 | updateLimits({ scroll, screenHeight }: DriverUpdateLimits) { 187 | this.start.updateLimits({ scroll, screenHeight }) 188 | this.end.updateLimits({ scroll, screenHeight }) 189 | 190 | const top = this.start.top + screenHeight 191 | this.helper.updateLimits({ 192 | top, 193 | height: this.end.top - top, 194 | }) 195 | 196 | if (this.hooks.onUpdateLimits) 197 | this.hooks.onUpdateLimits(this) 198 | } 199 | 200 | /** Activates driver when it becomes visible on the screen */ 201 | activate() { 202 | this.plugin.driverActiveInstances.add(this) 203 | 204 | if (this.hooks.onActivation) 205 | this.hooks.onActivation(this) 206 | 207 | console.log(`Driver "${this.id}" activated`) 208 | } 209 | 210 | /** Deactivates driver when it's progress becomes 0 or 1' */ 211 | deactivate() { 212 | this.plugin.driverActiveInstances.delete(this) 213 | 214 | if (this.hooks.onDeactivation) 215 | this.hooks.onDeactivation(this) 216 | 217 | console.log(`Driver "${this.id}" deactivated`) 218 | } 219 | } 220 | 221 | /** An HTML Element. It's top offset serves as indicator of where driver starts and where it ends */ 222 | class DriverBorder { 223 | /** Associated DOM element */ 224 | domElement: HTMLElement 225 | /** Top means amount of scroll needed to border activate or deactivate driver */ 226 | top: number = 0 227 | 228 | constructor({ domElement, type, driver }: DriverBorderConstructor) { 229 | if (typeof domElement === 'string') 230 | domElement = document.querySelector(domElement) 231 | 232 | if (!domElement) 233 | throw new Error(`Can't find "${type}" HTMLElement for driver "${driver.id}"`) 234 | 235 | this.domElement = domElement 236 | } 237 | 238 | /** Recalculates top offset */ 239 | updateLimits({ scroll, screenHeight }: DriverBorderUpdateLimits) { 240 | this.top = ~~this.domElement.getBoundingClientRect().top + scroll - screenHeight 241 | } 242 | } 243 | 244 | /** A helper HTML element, which connects to Border instances and starts being tracked by IntersectionObserver */ 245 | class DriverHelper { 246 | /** DOM element which is dynamically generated by plugin */ 247 | domElement: HTMLElement 248 | 249 | pluginId: string 250 | 251 | debug: boolean 252 | 253 | constructor({ id, pluginId, debug = false }: DriverHelperConstructor) { 254 | this.pluginId = pluginId 255 | this.debug = debug 256 | this.domElement = document.createElement('i') 257 | this.domElement.style.position = 'absolute' 258 | this.domElement.style.left = '0' 259 | this.domElement.style.width = '1px' 260 | 261 | this.domElement.setAttribute('data-supersonic-driver', id) 262 | this.domElement.setAttribute('data-supersonic-type', 'helper') 263 | this.domElement.setAttribute('data-supersonic-plugin-id', this.pluginId) 264 | this.domElement.classList.add('supersonic-helper') 265 | 266 | document.body.appendChild(this.domElement) 267 | 268 | if (debug) { 269 | const elements = Array.from(document.querySelectorAll('[data-supersonic-type=\'helper\']')) 270 | const index = elements.indexOf(this.domElement) 271 | this.domElement.style.left 272 | = `${index * 10}px` 273 | this.domElement.style.width = '10px' 274 | this.domElement.style.minHeight = '50px' 275 | this.domElement.style.background = debugColors.colors[debugColors.colorIndex] 276 | this.domElement.style.zIndex = (100000).toString() 277 | if (debugColors.colorIndex === 3) 278 | debugColors.colorIndex = 0 279 | else debugColors.colorIndex++ 280 | this.domElement.style.opacity = '0.75' 281 | } 282 | } 283 | 284 | /** Sets position of helper */ 285 | updateLimits({ top, height }: DriverHelperUpdateLimits) { 286 | if (height <= 0) 287 | height = 1 288 | this.domElement.style.setProperty('top', `${top}px`) 289 | this.domElement.style.setProperty('height', `${height}px`) 290 | } 291 | 292 | /** Deletes helper DOM element */ 293 | uninit() { 294 | this.domElement.remove() 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /lib/src/Driver.types.ts: -------------------------------------------------------------------------------- 1 | import type { Driver } from './Driver' 2 | import type { ElementSelector } from './Animation.types' 3 | import type { TheSupersonicPlugin } from './TheSupersonicPlugin' 4 | 5 | export type DriverConfiguration = { 6 | id?: string 7 | start: DriverBorderConfiguration 8 | end: DriverBorderConfiguration 9 | elements?: ElementSelector[] 10 | hooks?: DriverHooks 11 | } 12 | 13 | export type DriverConstructor = Omit & { 14 | id: string 15 | plugin: TheSupersonicPlugin 16 | } 17 | 18 | export type DriverRender = { 19 | scroll: number 20 | renderedInitially: boolean 21 | consoleColor?: string 22 | } 23 | 24 | export type DriverCalculateProgress = { 25 | scroll: number 26 | start: number 27 | end: number 28 | } 29 | 30 | export type DriverHooks = { 31 | onBeforeInit?: (driver: Driver) => void 32 | onAfterInit?: (driver: Driver) => void 33 | /** You can `return false` inside your hook, it will cancel rendering */ 34 | onBeforeRender?: (driver: Driver) => void | undefined | boolean 35 | onAfterRender?: (driver: Driver) => void 36 | onActivation?: (driver: Driver) => void 37 | onDeactivation?: (driver: Driver) => void 38 | onUpdateLimits?: (driver: Driver) => void 39 | } 40 | 41 | type DriverBorderConfiguration = HTMLElement | string | null 42 | 43 | export type DriverBorderConstructor = { 44 | domElement: DriverBorderConfiguration 45 | driver: Driver 46 | type: 'start' | 'end' 47 | } 48 | 49 | export type DriverHelperConstructor = { 50 | id: string 51 | pluginId: string 52 | debug?: boolean 53 | } 54 | 55 | export type DriverUpdateLimits = { 56 | scroll: number 57 | screenHeight: number 58 | } 59 | 60 | export type DriverBorderUpdateLimits = { 61 | scroll: number 62 | screenHeight: number 63 | } 64 | 65 | export type DriverHelperUpdateLimits = { 66 | top: number 67 | height: number 68 | } 69 | -------------------------------------------------------------------------------- /lib/src/Observer.ts: -------------------------------------------------------------------------------- 1 | import type { Constructor } from './Observer.types' 2 | 3 | export class Observer { 4 | instance: IntersectionObserver 5 | 6 | constructor({ observables, driverInstances }: Constructor) { 7 | this.instance = new IntersectionObserver( 8 | (entries) => { 9 | entries.forEach((entry) => { 10 | const target = entry.target as HTMLElement 11 | const driverId = target.dataset.supersonicDriver! 12 | const driver = driverInstances.get(driverId) 13 | 14 | if (!driver) 15 | throw new Error(`Observer can't find driver "${driverId}"`) 16 | 17 | if (entry.isIntersecting) 18 | driver.activate() 19 | 20 | else 21 | driver.deactivate() 22 | }) 23 | }, 24 | ) 25 | 26 | observables.forEach((observable) => { 27 | this.instance.observe(observable) 28 | }) 29 | } 30 | 31 | uninit() { 32 | this.instance.disconnect() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/src/Observer.types.ts: -------------------------------------------------------------------------------- 1 | import type { Driver } from './Driver' 2 | 3 | export type Constructor = { 4 | observables: HTMLElement[] 5 | driverInstances: Map 6 | } 7 | -------------------------------------------------------------------------------- /lib/src/TheSupersonicPlugin.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, expect, it, vi } from 'vitest' 2 | import { TheSupersonicPlugin } from './TheSupersonicPlugin' 3 | 4 | const IOObserver = vi.fn() 5 | const IODisconnect = vi.fn() 6 | 7 | it('adds onBeforeInit hook', () => { 8 | const plugin = new TheSupersonicPlugin( 9 | [{ ...driverSetup() }], 10 | { 11 | hooks: { 12 | onBeforeInit(plugin) { 13 | plugin.data.foo = 'bar' 14 | }, 15 | }, 16 | }, 17 | ) 18 | 19 | expect(plugin.data.foo).toBe('bar') 20 | }) 21 | 22 | it('creates driver instance onInit', () => { 23 | const plugin = new TheSupersonicPlugin([{ ...driverSetup(), id: 'foo' }]) 24 | 25 | expect(plugin.driverInstances.has('foo')).toBeTruthy() 26 | }) 27 | 28 | it('uninits', () => { 29 | const plugin = new TheSupersonicPlugin([{ ...driverSetup() }]) 30 | 31 | plugin.uninit() 32 | 33 | expect(document.body.innerHTML).toBe('') 34 | expect(plugin.driverInstances.size).toBe(0) 35 | expect(plugin.driverActiveInstances.size).toBe(0) 36 | expect(IODisconnect).toHaveBeenCalledOnce() 37 | }) 38 | 39 | it('adds onBeforeRender hook', () => { 40 | const plugin = new TheSupersonicPlugin([{ ...driverSetup() }], { 41 | hooks: { 42 | onBeforeRender(plugin) { 43 | plugin.data.foo = 'bar' 44 | return false 45 | }, 46 | onAfterRender(plugin) { 47 | plugin.data.foo = 'foo' 48 | }, 49 | }, 50 | }) 51 | 52 | expect(plugin.data.foo).toBe('bar') 53 | }) 54 | 55 | it('renders all drivers', () => { 56 | const plugin = new TheSupersonicPlugin([ 57 | { 58 | ...driverSetup(), 59 | id: 'foo', 60 | }, 61 | { 62 | ...driverSetup(), 63 | id: 'bar', 64 | }, 65 | ]) 66 | 67 | plugin.screenHeight = 1000 68 | 69 | plugin.updateLimits() 70 | 71 | vi.stubGlobal('scrollY', 2000) 72 | 73 | plugin.render({ useActiveDrivers: false }) 74 | 75 | expect(plugin.driverInstances.get('foo')?.progress).toBe(0.5) 76 | expect(plugin.driverInstances.get('bar')?.progress).toBe(0.5) 77 | }) 78 | 79 | it('renders only active drivers', () => { 80 | const plugin = new TheSupersonicPlugin([ 81 | { 82 | ...driverSetup(), 83 | id: 'foo', 84 | }, 85 | { 86 | ...driverSetup(), 87 | id: 'bar', 88 | }, 89 | ]) 90 | 91 | plugin.screenHeight = 1000 92 | 93 | plugin.updateLimits() 94 | 95 | vi.stubGlobal('scrollY', 2000) 96 | 97 | plugin.driverInstances.get('foo')?.activate() 98 | 99 | plugin.render({ useActiveDrivers: true }) 100 | 101 | expect(plugin.driverInstances.get('foo')?.progress).toBe(0.5) 102 | expect(plugin.driverInstances.get('bar')?.progress).toBe(0) 103 | }) 104 | 105 | afterEach(() => { 106 | document.body.innerHTML = '' 107 | vi.stubGlobal('scrollY', 0) 108 | }) 109 | 110 | vi.stubGlobal('IntersectionObserver', vi.fn(() => ({ 111 | observe: IOObserver, 112 | disconnect: IODisconnect, 113 | }))) 114 | 115 | function driverSetup() { 116 | const start = document.createElement('div') 117 | const end = document.createElement('div') 118 | 119 | // @ts-expect-error not full implementation of DOMRect 120 | start.getBoundingClientRect = vi.fn(() => ({ 121 | top: 1000, 122 | })) 123 | 124 | // @ts-expect-error not full implementation of DOMRect 125 | end.getBoundingClientRect = vi.fn(() => ({ 126 | top: 3000, 127 | })) 128 | 129 | return { 130 | start, 131 | end, 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /lib/src/TheSupersonicPlugin.ts: -------------------------------------------------------------------------------- 1 | import { Driver } from './Driver' 2 | import { Observer } from './Observer' 3 | import { debounce, generateId } from './utils' 4 | 5 | import type { PluginConfiguration, PluginHooks, PluginRender } from './TheSupersonicPlugin.types' 6 | import type { DriverConfiguration } from './Driver.types' 7 | 8 | /** 9 | * 10 | * Main class handling all of the logic. To initialize the plugin, you create a new instance of this class 11 | * 12 | * @example 13 | * const plugin = new TheSupersonicPlugin([ 14 | * { 15 | * start: '.start', 16 | * end: '.end', 17 | * elements: ['.animatable-element'] 18 | * } 19 | * ]); 20 | * 21 | */ 22 | export class TheSupersonicPlugin { 23 | /** Unique id of this running instance. You explicitly define it or let plugin auto generate it */ 24 | id: string 25 | /** Current window scrollY */ 26 | scroll = 0 27 | 28 | /** Current screen height */ 29 | screenHeight = 0 30 | 31 | /** Required to get all of the drivers render at once to stand on their first frame */ 32 | renderedInitially: boolean = false 33 | 34 | /** Used to cancelAnimationFrame on 'uninit()' */ 35 | rafId = 0 36 | 37 | /** Color of console messages in dev mode. It changes each frame to make it more convenient to visually separate frames */ 38 | consoleColor = '#ffffff' 39 | 40 | /** IntersectionObserver instance */ 41 | observer: Observer | null = null 42 | 43 | /** Debounced resize listener */ 44 | onResize: EventListener | null = null 45 | /** You can store your custom data here to use between hooks */ 46 | data: any = {} 47 | 48 | /** Make helper visible */ 49 | debug: boolean 50 | 51 | hooks: PluginHooks = {} 52 | 53 | driverInstances: Map = new Map() 54 | driverActiveInstances: Set = new Set() 55 | 56 | constructor(drivers: DriverConfiguration[], configuration?: PluginConfiguration) { 57 | this.id = configuration?.id ?? generateId() 58 | this.hooks = configuration?.hooks ?? {} 59 | this.debug = configuration?.debug ?? false 60 | 61 | if (this.hooks?.onBeforeInit) 62 | this.hooks.onBeforeInit(this) 63 | 64 | // Initializing driver instances 65 | drivers.forEach((driverConfiguration) => { 66 | const id = driverConfiguration.id ?? generateId() 67 | 68 | const driver = new Driver({ 69 | id, 70 | hooks: driverConfiguration.hooks, 71 | start: driverConfiguration.start, 72 | end: driverConfiguration.end, 73 | elements: driverConfiguration.elements, 74 | plugin: this, 75 | }) 76 | this.driverInstances.set(id, driver) 77 | }) 78 | 79 | this.updateLimits() 80 | 81 | // Creating IntersectionObserver, which handles "active" state on Driver instances 82 | const observables = Array.from(document.querySelectorAll(`[data-supersonic-type="helper"][data-supersonic-plugin-id="${this.id}"]`)) 83 | this.observer = new Observer({ 84 | observables, 85 | driverInstances: this.driverInstances, 86 | }) 87 | 88 | // Adding event listener for resize 89 | const resize = () => { 90 | if (this.hooks?.onBeforeResize) 91 | this.hooks.onBeforeResize(this) 92 | 93 | this.updateLimits() 94 | this.render({ useActiveDrivers: false }) 95 | 96 | if (this.hooks.onAfterResize) 97 | this.hooks.onAfterResize(this) 98 | } 99 | this.onResize = debounce(resize, 250) 100 | window.addEventListener('resize', this.onResize) 101 | 102 | this.render({ useActiveDrivers: false }) 103 | this.renderedInitially = true 104 | 105 | if (this.hooks?.onAfterInit) 106 | this.hooks.onAfterInit(this) 107 | 108 | console.log('Driver instances:', this.driverInstances) 109 | } 110 | 111 | /** Removes all of the plugin stuff (useful for SPA) */ 112 | uninit() { 113 | for (const driver of this.driverInstances.values()) 114 | driver.helper.uninit() 115 | 116 | this.driverInstances.clear() 117 | this.driverActiveInstances.clear() 118 | 119 | this.observer!.uninit() 120 | cancelAnimationFrame(this.rafId) 121 | 122 | window.removeEventListener('resize', this.onResize!) 123 | } 124 | 125 | /** Main rendering cycle. Active drivers are visible ones */ 126 | render({ useActiveDrivers }: PluginRender) { 127 | this.updateScroll() 128 | 129 | if (this.hooks.onBeforeRender) { 130 | const onBeforeRenderReturn = this.hooks.onBeforeRender(this) 131 | 132 | if (typeof onBeforeRenderReturn === 'boolean' && !onBeforeRenderReturn) 133 | return false 134 | } 135 | 136 | const drivers = useActiveDrivers ? this.driverActiveInstances.values() : this.driverInstances.values() 137 | 138 | for (const driver of drivers) { 139 | driver.render({ 140 | scroll: this.scroll, 141 | renderedInitially: this.renderedInitially, 142 | consoleColor: this.consoleColor, 143 | }) 144 | } 145 | 146 | this.rafId = requestAnimationFrame(() => { 147 | this.render({ useActiveDrivers: true }) 148 | }) 149 | 150 | if (this.hooks.onAfterRender) 151 | this.hooks.onAfterRender(this) 152 | 153 | if (import.meta.env.DEV) { 154 | const randomInt = ~~(Math.random() * 100000) 155 | this.consoleColor = `#${randomInt.toString(16).padStart(6, 'f')}` 156 | } 157 | } 158 | 159 | /** Updates global scroll and driver DOM elements top offset. Called once on page load and each time after window.resize */ 160 | updateLimits() { 161 | this.updateScreenHeight() 162 | this.updateScroll() 163 | 164 | for (const driver of this.driverInstances.values()) { 165 | driver.updateLimits({ 166 | scroll: this.scroll, 167 | screenHeight: this.screenHeight, 168 | }) 169 | } 170 | } 171 | 172 | updateScroll() { 173 | this.scroll = window.scrollY || document.documentElement.scrollTop 174 | } 175 | 176 | /** Dirty hack for calculating screen height. We can't just use "window.innerHeight" because it "jumps" on mobile phones when you scroll and toolbar collapses */ 177 | updateScreenHeight() { 178 | const styles: { [key: string]: string } = { 179 | position: 'absolute', 180 | left: '0', 181 | top: '0', 182 | height: '100vh', 183 | width: '1px', 184 | zIndex: '-1', 185 | visibility: 'hidden', 186 | } 187 | const helper = document.createElement('div') 188 | for (const property in styles) 189 | helper.style.setProperty(property, styles[property]) 190 | 191 | document.body.appendChild(helper) 192 | this.screenHeight = helper.clientHeight 193 | helper.remove() 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /lib/src/TheSupersonicPlugin.types.ts: -------------------------------------------------------------------------------- 1 | import type { TheSupersonicPlugin } from './TheSupersonicPlugin' 2 | 3 | export type PluginConfiguration = { 4 | id?: string 5 | hooks?: PluginHooks 6 | debug?: boolean 7 | } 8 | 9 | export type PluginHooks = { 10 | onBeforeInit?: (plugin: TheSupersonicPlugin) => void 11 | onAfterInit?: (plugin: TheSupersonicPlugin) => void 12 | 13 | /** You can `return false` inside your hook, it will cancel rendering */ 14 | onBeforeRender?: (plugin: TheSupersonicPlugin) => void | undefined | boolean 15 | onAfterRender?: (plugin: TheSupersonicPlugin) => void 16 | 17 | onBeforeResize?: (plugin: TheSupersonicPlugin) => void 18 | onAfterResize?: (plugin: TheSupersonicPlugin) => void 19 | } 20 | 21 | export type PluginRender = { 22 | useActiveDrivers: boolean 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/index.ts: -------------------------------------------------------------------------------- 1 | import { TheSupersonicPlugin } from './TheSupersonicPlugin' 2 | import { Driver } from './Driver' 3 | import { Animation } from './Animation' 4 | 5 | export { 6 | TheSupersonicPlugin, 7 | Driver, 8 | Animation, 9 | } 10 | -------------------------------------------------------------------------------- /lib/src/utils.ts: -------------------------------------------------------------------------------- 1 | function toFixed(number: number, precision: number) { 2 | precision = 10 ** precision 3 | return ~~(number * precision) / precision 4 | } 5 | 6 | function debounce(func: Function, time: number) { 7 | let timeout: any 8 | return function () { 9 | clearTimeout(timeout) 10 | // @ts-expect-error this 11 | timeout = setTimeout(() => func.apply(this), time) 12 | } 13 | } 14 | 15 | function generateId(): string { 16 | return Math.random().toString(16).substring(2) 17 | } 18 | 19 | export { 20 | toFixed, 21 | debounce, 22 | generateId, 23 | } 24 | -------------------------------------------------------------------------------- /lib/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | "resolveJsonModule": true, 11 | "allowImportingTsExtensions": true, 12 | 13 | /* Linting */ 14 | "strict": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "noEmit": true, 19 | "isolatedModules": true, 20 | "skipLibCheck": true 21 | }, 22 | "include": ["src", "env.d.ts"] 23 | } 24 | -------------------------------------------------------------------------------- /lib/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { resolve } from 'node:path' 4 | import { defineConfig } from 'vite' 5 | import dts from 'vite-plugin-dts' 6 | 7 | export default defineConfig(({ mode }) => { 8 | return { 9 | build: { 10 | lib: { 11 | entry: resolve(__dirname, 'src/index.ts'), 12 | fileName: 'the-supersonic-plugin-for-scroll-based-animation', 13 | name: 'TheSupersonicPluginWrapper', 14 | formats: ['es', 'iife'], 15 | }, 16 | }, 17 | esbuild: { 18 | drop: mode === 'production' 19 | ? ['console'] 20 | : [], 21 | }, 22 | test: { 23 | environment: 'jsdom', 24 | }, 25 | plugins: [dts({ 26 | exclude: ['**/*.test.ts', '**/utils.ts'], 27 | })], 28 | } 29 | }) 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "the-supersonic-plugin-for-scroll-based-animation", 3 | "type": "module", 4 | "version": "2.1.0", 5 | "description": "The Supersonic Plugin For Scroll Based Animation", 6 | "author": { 7 | "name": "Alex Illarionov", 8 | "email": "the.illarionov@gmail.com", 9 | "url": "https://github.com/the-illarionov" 10 | }, 11 | "license": "MIT", 12 | "homepage": "https://the-illarionov.com/the-supersonic-plugin-for-scroll-based-animation/examples", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/the-illarionov/the-supersonic-plugin-for-scroll-based-animation.git" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/the-illarionov/the-supersonic-plugin-for-scroll-based-animation/issues", 19 | "email": "the.illarionov@gmail.com" 20 | }, 21 | "keywords": [ 22 | "javascript", 23 | "scroll", 24 | "animation", 25 | "fast", 26 | "custom", 27 | "parallax", 28 | "supersonic", 29 | "plugin", 30 | "css" 31 | ], 32 | "browser": "./lib/dist/the-supersonic-plugin-for-scroll-based-animation.js", 33 | "unpkg": "./lib/dist/the-supersonic-plugin-for-scroll-based-animation.iife.js", 34 | "types": "./lib/dist/index.d.ts", 35 | "files": [ 36 | "./lib/dist" 37 | ], 38 | "scripts": { 39 | "lint": "eslint .", 40 | "lint:fix": "eslint . --fix" 41 | }, 42 | "devDependencies": { 43 | "@antfu/eslint-config": "^2.12.2", 44 | "eslint": "^8.56.0", 45 | "eslint-plugin-format": "^0.1.0", 46 | "typescript": "~5.4.0" 47 | } 48 | } 49 | --------------------------------------------------------------------------------