├── .changeset ├── README.md └── config.json ├── .codesandbox └── ci.json ├── .github └── workflows │ ├── release.yml │ ├── size-limit.yml │ └── tests.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── .prettierignore ├── README.md ├── examples ├── interpol-basic │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── src │ │ ├── index.css │ │ ├── main.ts │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── interpol-colors │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── src │ │ ├── index.css │ │ ├── main.ts │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── interpol-dom-ondrag │ ├── .gitignore │ ├── index.html │ ├── libs │ │ └── ratio.less │ ├── package.json │ ├── src │ │ ├── index.css │ │ ├── main.ts │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── interpol-ease │ ├── .gitignore │ ├── index.html │ ├── libs │ │ ├── ratio.less │ │ └── useWindowSize.ts │ ├── package.json │ ├── src │ │ ├── App.module.less │ │ ├── App.tsx │ │ ├── Controls.module.less │ │ ├── Controls.tsx │ │ ├── Params.module.less │ │ ├── Params.tsx │ │ ├── index.less │ │ ├── main.tsx │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── interpol-graphic │ ├── .gitignore │ ├── index.html │ ├── libs │ │ ├── ratio.less │ │ └── useWindowSize.ts │ ├── package.json │ ├── src │ │ ├── components │ │ │ ├── app │ │ │ │ ├── App.module.less │ │ │ │ └── App.tsx │ │ │ ├── controls │ │ │ │ ├── Controls.module.less │ │ │ │ └── Controls.tsx │ │ │ ├── graph │ │ │ │ ├── Graph.module.less │ │ │ │ └── Graph.tsx │ │ │ └── params │ │ │ │ ├── Params.module.less │ │ │ │ └── Params.tsx │ │ ├── index.less │ │ ├── main.tsx │ │ ├── utils │ │ │ ├── calcCoords.ts │ │ │ ├── clamp.ts │ │ │ ├── randomRange.ts │ │ │ ├── ratio.less │ │ │ └── styles.ts │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── interpol-menu │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── src │ │ ├── components │ │ │ ├── app │ │ │ │ ├── App.module.less │ │ │ │ └── App.tsx │ │ │ └── menu │ │ │ │ ├── Menu.module.less │ │ │ │ └── Menu.tsx │ │ ├── index.less │ │ ├── main.tsx │ │ ├── utils │ │ │ ├── ratio.less │ │ │ └── useWindowSize.ts │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── interpol-object-el │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── src │ │ ├── index.css │ │ ├── index.ts │ │ ├── shaders │ │ │ ├── fragment.glsl │ │ │ └── vertex.glsl │ │ └── vite-env.d.ts │ └── tsconfig.json ├── interpol-offsets │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── src │ │ ├── components │ │ │ ├── app │ │ │ │ ├── App.module.less │ │ │ │ └── App.tsx │ │ │ └── controls │ │ │ │ ├── Controls.module.less │ │ │ │ └── Controls.tsx │ │ ├── index.less │ │ ├── main.tsx │ │ ├── utils │ │ │ ├── ratio.less │ │ │ └── useWindowSize.ts │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── interpol-particles │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── src │ │ ├── main.less │ │ ├── main.tsx │ │ ├── utils │ │ │ ├── ratio.less │ │ │ └── useWindowSize.ts │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── interpol-seek-reset-wall │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── public │ │ └── vite.svg │ ├── src │ │ ├── main.ts │ │ ├── style.css │ │ ├── typescript.svg │ │ └── vite-env.d.ts │ └── tsconfig.json ├── interpol-timeline-refresh │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── src │ │ ├── index.less │ │ ├── main.ts │ │ └── vite-env.d.ts │ ├── tsconfig.json │ └── tsconfig.node.json └── interpol-timeline │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── src │ ├── index.less │ ├── main.ts │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── package.json ├── packages └── interpol │ ├── CHANGELOG.md │ ├── README.md │ ├── interpol.png │ ├── package.json │ ├── src │ ├── Interpol.ts │ ├── Timeline.ts │ ├── core │ │ ├── Ticker.ts │ │ ├── clamp.ts │ │ ├── compute.ts │ │ ├── deferredPromise.ts │ │ ├── ease.ts │ │ ├── env.ts │ │ ├── noop.ts │ │ ├── round.ts │ │ ├── styles.ts │ │ └── types.ts │ ├── index.ts │ ├── itp.ts │ ├── options.ts │ └── utils │ │ ├── clamp.ts │ │ ├── compute.ts │ │ ├── deferredPromise.ts │ │ ├── env.ts │ │ ├── noop.ts │ │ └── round.ts │ ├── tests │ ├── Interpol.basic.test.ts │ ├── Interpol.callbacks.test.ts │ ├── Interpol.delay.test.ts │ ├── Interpol.duration.test.ts │ ├── Interpol.pause.test.ts │ ├── Interpol.props.test.ts │ ├── Interpol.refresh.test.ts │ ├── Interpol.reverse.test.ts │ ├── Interpol.seek.test.ts │ ├── Interpol.stress.test.ts │ ├── Ticker.test.ts │ ├── Timeline.callbacks.test.ts │ ├── Timeline.offset.test.ts │ ├── Timeline.play.test.ts │ ├── Timeline.refresh.test.ts │ ├── Timeline.reverse.test.ts │ ├── Timeline.seek.test.ts │ ├── Timeline.stop.test.ts │ ├── Timeline.stress.test.ts │ ├── _old │ │ ├── Interpol.el.test.ts │ │ └── Interpol.units.test.ts │ ├── _setup.ts │ ├── ease.test.ts │ ├── options.test.ts │ ├── styles.test.ts │ └── utils │ │ ├── getDocument.ts │ │ ├── interpolParamsGenerator.ts │ │ ├── randomRange.ts │ │ └── wait.ts │ ├── tsconfig.json │ ├── tsup.config.ts │ └── vitest.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tsconfig.json └── turbo.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [["@wbe/interpol"]], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": ["interpol-*"] 11 | } 12 | -------------------------------------------------------------------------------- /.codesandbox/ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "buildCommand": "build", 3 | "packages": ["./packages/*"], 4 | "sandboxes": [ 5 | "/examples/interpol-basic", 6 | "/examples/interpol-colors", 7 | "/examples/interpol-dom-ondrag", 8 | "/examples/interpol-ease", 9 | "/examples/interpol-graphic", 10 | "/examples/interpol-menu", 11 | "/examples/interpol-object-el", 12 | "/examples/interpol-particles", 13 | "/examples/interpol-timeline", 14 | "/examples/interpol-offsets" 15 | ], 16 | "node": "18" 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - '**/package.json' 8 | - '.changeset/**' 9 | - '.github/workflows/release.yml' 10 | env: 11 | # Bypass husky commit hook for CI 12 | HUSKY: 0 13 | jobs: 14 | version: 15 | timeout-minutes: 8 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: checkout code repository 19 | uses: actions/checkout@v3 20 | with: 21 | fetch-depth: 0 22 | 23 | - name: Use Node 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: '20' 27 | 28 | - name: Cache pnpm modules 29 | uses: actions/cache@v3 30 | with: 31 | path: ~/.pnpm-store 32 | key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} 33 | restore- keys: | 34 | ${{ runner.os }}- 35 | 36 | - uses: pnpm/action-setup@v2 37 | with: 38 | version: 8.3.1 39 | run_install: | 40 | args: [--filter "@wbe/*"] 41 | 42 | - name: Copy README file to interpol package 43 | uses: canastro/copy-file-action@master 44 | with: 45 | source: 'README.md' 46 | target: 'packages/interpol/README.md' 47 | 48 | - name: Set up NPM credentials 49 | run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc 50 | env: 51 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 52 | 53 | - name: Create versions PR & prepare publish 54 | id: changesets 55 | uses: changesets/action@v1 56 | with: 57 | version: pnpm ci:version 58 | publish: pnpm ci:publish 59 | # Messages 60 | commit: 'chore(deploy): Release' 61 | title: 'chore(deploy): Release' 62 | env: 63 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 64 | GITHUB_TOKEN: ${{ secrets.REPO_TOKEN }} 65 | -------------------------------------------------------------------------------- /.github/workflows/size-limit.yml: -------------------------------------------------------------------------------- 1 | name: Size limit 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | jobs: 8 | size: 9 | runs-on: ubuntu-latest 10 | env: 11 | CI_JOB_NUMBER: 1 12 | steps: 13 | - uses: actions/checkout@v1 14 | - name: Install pnpm 15 | uses: pnpm/action-setup@v2 16 | with: 17 | version: 8 18 | - name: Use Size limit 19 | uses: andresz1/size-limit-action@dd31dce7dcc72a041fd3e49abf0502b13fc4ce05 # support for pnpm 20 | with: 21 | github_token: ${{ secrets.REPO_TOKEN }} 22 | package_manager: pnpm 23 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | on: [push] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | matrix: 8 | node-version: [20.x] 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Use Node.js ${{ matrix.node-version }} 12 | uses: actions/setup-node@v1 13 | with: 14 | node-version: ${{ matrix.node-version }} 15 | 16 | - name: Checkout 17 | uses: actions/checkout@v3 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Install pnpm 22 | uses: pnpm/action-setup@v2 23 | with: 24 | version: 8 25 | 26 | - name: Install dependencies 27 | run: pnpm install 28 | 29 | - name: build 30 | run: pnpm run build 31 | 32 | - name: test 33 | run: pnpm run test 34 | 35 | - name: size 36 | run: pnpm run size 37 | -------------------------------------------------------------------------------- /.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 | stats.html 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | 27 | .turbo 28 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .cache 3 | .github 4 | node_modules 5 | src 6 | src/* 7 | src/** 8 | test 9 | example 10 | examples 11 | example-node 12 | .babelrc 13 | .prettierignore 14 | .prettierrc 15 | jest.config.ts 16 | tsconfig.json 17 | vitest.config.ts 18 | vite.config.ts 19 | .turbo 20 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | strict-peer-dependencies=false 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | README.md 2 | -------------------------------------------------------------------------------- /examples/interpol-basic/.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 | -------------------------------------------------------------------------------- /examples/interpol-basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | interpol 8 | 9 | 10 |
11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 |
25 |
26 |
27 |
28 |
29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /examples/interpol-basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "interpol-basic", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "main": "src/index.ts", 7 | "scripts": { 8 | "dev": "vite --host" 9 | }, 10 | "dependencies": { 11 | "@wbe/interpol": "^0.20.2" 12 | }, 13 | "devDependencies": { 14 | "typescript": "^5.7.2", 15 | "vite": "^6.0.6" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/interpol-basic/src/index.css: -------------------------------------------------------------------------------- 1 | 2 | :root { 3 | --color-gray: #454545; 4 | --color-black-1: #313131; 5 | --color-black: #171717; 6 | --color-blue: #646cff; 7 | --color-white: #fff; 8 | } 9 | 10 | html { 11 | font-size: var(--font-size); 12 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 13 | font-weight: 400; 14 | color-scheme: dark; 15 | font-synthesis: none; 16 | text-rendering: optimizeLegibility; 17 | -webkit-font-smoothing: antialiased; 18 | -moz-osx-font-smoothing: grayscale; 19 | -webkit-text-size-adjust: 100%; 20 | } 21 | 22 | body { 23 | margin: 0; 24 | font-size: 16px; 25 | min-width: 100vw; 26 | min-height: 100vh; 27 | overflow: hidden; 28 | position: fixed; 29 | } 30 | 31 | .ball { 32 | position: absolute; 33 | width: 50px; 34 | height: 50px; 35 | border-radius: 50%; 36 | background-color: red; 37 | } 38 | -------------------------------------------------------------------------------- /examples/interpol-basic/src/main.ts: -------------------------------------------------------------------------------- 1 | import "./index.css" 2 | import { Interpol, styles } from "@wbe/interpol" 3 | 4 | /** 5 | * Query 6 | */ 7 | const element = document.querySelector(".ball") 8 | const seek0 = document.querySelector(".seek-0") 9 | const seek05 = document.querySelector(".seek-05") 10 | const seek1 = document.querySelector(".seek-1") 11 | const inputProgress = document.querySelector(".progress") 12 | const inputSlider = document.querySelector(".slider") 13 | 14 | /** 15 | * Events 16 | */ 17 | ;["play", "reverse", "pause", "stop", "resume"].forEach( 18 | (name: any) => 19 | // @ts-ignore 20 | (document.querySelector(`.${name}`)!.onclick = () => itp[name]()), 21 | ) 22 | 23 | seek0!.onclick = () => itp.seek(0, false) 24 | seek05!.onclick = () => itp.seek(0.5, false) 25 | seek1!.onclick = () => itp.seek(1, false) 26 | inputProgress!.onchange = () => itp.seek(parseFloat(inputProgress!.value) / 100, false) 27 | inputSlider!.oninput = () => itp.seek(parseFloat(inputSlider!.value) / 100, false) 28 | 29 | const itp = new Interpol({ 30 | x: 100, 31 | y: { from: 0, to: 300 }, 32 | opacity: [0.5, 1], 33 | z: [100, 0], 34 | b: "dldl", 35 | 36 | onUpdate: ({ x, y, opacity, z, b, s }) => { 37 | styles(element!, { x, y, opacity }) 38 | }, 39 | onComplete: (props) => { 40 | console.log("itp onComplete", props) 41 | }, 42 | }) 43 | -------------------------------------------------------------------------------- /examples/interpol-basic/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/interpol-basic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | 19 | /* Linting */ 20 | "strict": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": false, 23 | "noFallthroughCasesInSwitch": true 24 | }, 25 | 26 | "include": ["src"], 27 | "references": [{ "path": "./tsconfig.node.json" }] 28 | } 29 | -------------------------------------------------------------------------------- /examples/interpol-basic/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /examples/interpol-basic/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite" 2 | 3 | // https://vitejs.dev/config/ 4 | export default defineConfig({}) 5 | -------------------------------------------------------------------------------- /examples/interpol-colors/.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 | -------------------------------------------------------------------------------- /examples/interpol-colors/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | interpol 8 | 9 | 10 |
11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 |
25 |
26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /examples/interpol-colors/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "interpol-colors", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "main": "src/index.ts", 7 | "scripts": { 8 | "dev": "vite --host" 9 | }, 10 | "dependencies": { 11 | "@wbe/interpol": "workspace:*" 12 | }, 13 | "devDependencies": { 14 | "typescript": "^5.7.2", 15 | "vite": "^6.0.6" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/interpol-colors/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-gray: #454545; 3 | --color-black-1: #313131; 4 | --color-black: #171717; 5 | --color-blue: #646cff; 6 | --color-white: #fff; 7 | } 8 | 9 | html { 10 | .propertyViewport(--font-size, 1, 2); 11 | font-size: var(--font-size); 12 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 13 | font-weight: 400; 14 | color-scheme: dark; 15 | font-synthesis: none; 16 | text-rendering: optimizeLegibility; 17 | -webkit-font-smoothing: antialiased; 18 | -moz-osx-font-smoothing: grayscale; 19 | -webkit-text-size-adjust: 100%; 20 | } 21 | 22 | body { 23 | margin: 0; 24 | font-size: 16px; 25 | min-width: 100vw; 26 | min-height: 100vh; 27 | overflow: hidden; 28 | position: fixed; 29 | } 30 | -------------------------------------------------------------------------------- /examples/interpol-colors/src/main.ts: -------------------------------------------------------------------------------- 1 | import "./index.css" 2 | import { Interpol } from "@wbe/interpol" 3 | 4 | /** 5 | * Query 6 | */ 7 | const seek0 = document.querySelector(".seek-0") 8 | const seek05 = document.querySelector(".seek-05") 9 | const seek1 = document.querySelector(".seek-1") 10 | const inputProgress = document.querySelector(".progress") 11 | const inputSlider = document.querySelector(".slider") 12 | 13 | /** 14 | * Events 15 | */ 16 | ;["play", "reverse", "pause", "stop", "resume"].forEach( 17 | (name: any) => 18 | (document.querySelector(`.${name}`)!.onclick = () => { 19 | // @ts-ignore 20 | itp[name]() 21 | }), 22 | ) 23 | 24 | seek0!.onclick = () => itp.seek(0, false) 25 | seek05!.onclick = () => itp.seek(0.5, false) 26 | seek1!.onclick = () => itp.seek(1, false) 27 | 28 | inputProgress!.onchange = () => itp.seek(parseFloat(inputProgress!.value) / 100, false) 29 | inputSlider!.oninput = () => itp.seek(parseFloat(inputSlider!.value) / 100, false) 30 | 31 | const itp = new Interpol({ 32 | v: [0, 1], 33 | duration: 2000, 34 | onUpdate: ({ v }) => { 35 | document.body.style.background = interpolateColor("rgb(0, 10, 0)", "#DDDDDD", v) 36 | }, 37 | }) 38 | 39 | /** 40 | * Interpolate color helper generate by phind 41 | * @param color1 42 | * @param color2 43 | * @param percent 44 | */ 45 | function interpolateColor(color1: string, color2: string, percent: number) { 46 | // Helper function to convert hex to RGB 47 | const hexToRgb = (hex) => { 48 | const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) 49 | return result 50 | ? { 51 | r: parseInt(result[1], 16), 52 | g: parseInt(result[2], 16), 53 | b: parseInt(result[3], 16), 54 | } 55 | : null 56 | } 57 | // Helper function to convert RGB to hex 58 | const rgbToHex = (rgb) => { 59 | const { r, g, b } = rgb 60 | return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1) 61 | } 62 | // Helper function to convert RGB string to object 63 | const rgbStringToObject = (rgbString) => { 64 | const [r, g, b] = rgbString.match(/\d+/g) 65 | return { r: parseInt(r), g: parseInt(g), b: parseInt(b) } 66 | } 67 | // Helper function to convert RGB object to string 68 | const rgbObjectToString = (rgb) => `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})` 69 | // Check if the input is in hex format 70 | const isHex = (color) => color.startsWith("#") 71 | // Convert the input colors to RGB objects 72 | const rgb1 = isHex(color1) ? hexToRgb(color1) : rgbStringToObject(color1) 73 | const rgb2 = isHex(color2) ? hexToRgb(color2) : rgbStringToObject(color2) 74 | // Interpolate the RGB values 75 | const r = Math.round(rgb1.r + (rgb2.r - rgb1.r) * percent) 76 | const g = Math.round(rgb1.g + (rgb2.g - rgb1.g) * percent) 77 | const b = Math.round(rgb1.b + (rgb2.b - rgb1.b) * percent) 78 | // Convert the interpolated RGB values back to the original format 79 | return isHex(color1) ? rgbToHex({ r, g, b }) : rgbObjectToString({ r, g, b }) 80 | } 81 | -------------------------------------------------------------------------------- /examples/interpol-colors/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/interpol-colors/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | 19 | /* Linting */ 20 | "strict": false, 21 | "noUnusedLocals": false, 22 | "noUnusedParameters": false, 23 | "noFallthroughCasesInSwitch": false 24 | }, 25 | 26 | "include": ["src"], 27 | "references": [{ "path": "./tsconfig.node.json" }] 28 | } 29 | -------------------------------------------------------------------------------- /examples/interpol-colors/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /examples/interpol-colors/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite" 2 | 3 | // https://vitejs.dev/config/ 4 | export default defineConfig({}) 5 | -------------------------------------------------------------------------------- /examples/interpol-dom-ondrag/.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 | -------------------------------------------------------------------------------- /examples/interpol-dom-ondrag/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | interpol 8 | 9 | 10 |
11 |
12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/interpol-dom-ondrag/libs/ratio.less: -------------------------------------------------------------------------------- 1 | @viewport-reference-width: 375; 2 | @viewport-reference-height: 667; 3 | @viewport-reference-desktop-width: 1400; 4 | @viewport-reference-desktop-height: 900; 5 | @breakpoint-mobile: 320px; 6 | @breakpoint-tablet: 768px; 7 | @breakpoint-laptop: 1024px; 8 | @breakpoint-bigLaptop: 1440px; 9 | @breakpoint-desktop: 1680px; 10 | 11 | /** 12 | Get VH value from ratio 13 | */ 14 | .ratioVW(@n1, @n2 : @viewport-reference-width) { 15 | @isnumber: isnumber(@n1); 16 | @returns: if((@isnumber), (@n1 / @n2) * 100vw, @n1); 17 | } 18 | 19 | /** 20 | Get VH value from ratio 21 | */ 22 | .ratioVH(@n1, @n2 : @viewport-reference-height) { 23 | @isnumber: isnumber(@n1); 24 | 25 | // fallback for old browser who don't support css --var 26 | @returns: if((@isnumber), (@n1 / @n2) * 100vh, @n1); 27 | // vh value relative to css --vh variable 28 | @var: var(--vh, 1vh); 29 | @returns: if((@isnumber), ~"calc( (@{n1} / @{n2}) * (@{var} * 100) )", @n1); 30 | } 31 | 32 | /** 33 | Get VW value for property according to value in pixels 34 | */ 35 | .propertyVW(@property, @n1, @n2 : @viewport-reference-width) { 36 | @value-length-mobile: length(@n1); 37 | 38 | & when (@value-length-mobile = 1) { 39 | @{property}: .ratioVW(@n1, @n2) [ @returns]; 40 | } 41 | & when (@value-length-mobile = 2) { 42 | @{property}: .ratioVW(extract(@n1, 1), @n2) [ @returns] .ratioVW(extract(@n1, 2), @n2) [ 43 | @returns]; 44 | } 45 | & when (@value-length-mobile = 3) { 46 | @{property}: .ratioVW(extract(@n1, 1), @n2) [ @returns] .ratioVW(extract(@n1, 2), @n2) [ 47 | @returns] .ratioVW(extract(@n1, 3), @n2) [ @returns]; 48 | } 49 | & when (@value-length-mobile = 4) { 50 | @{property}: .ratioVW(extract(@n1, 1), @n2) [ @returns] .ratioVW(extract(@n1, 2), @n2) [ 51 | @returns] .ratioVW(extract(@n1, 3), @n2) [ @returns] .ratioVW(extract(@n1, 4), @n2) [ 52 | @returns]; 53 | } 54 | } 55 | 56 | /** 57 | Get VH value for property according to value in pixels 58 | */ 59 | .propertyVH(@property, @n1, @n2 : @viewport-reference-height, @aspect-ratio: true) { 60 | @value-length-mobile: length(@n1); 61 | 62 | & when (@value-length-mobile = 1) { 63 | @{property}: .ratioVH(@n1, @n2) [ @returns]; 64 | } 65 | & when (@value-length-mobile = 2) { 66 | @{property}: .ratioVH(extract(@n1, 1), @n2) [ @returns] .ratioVH(extract(@n1, 2), @n2) [ 67 | @returns]; 68 | } 69 | & when (@value-length-mobile = 3) { 70 | @{property}: .ratioVH(extract(@n1, 1), @n2) [ @returns] .ratioVH(extract(@n1, 2), @n2) [ 71 | @returns] .ratioVH(extract(@n1, 3), @n2) [ @returns]; 72 | } 73 | & when (@value-length-mobile = 4) { 74 | @{property}: .ratioVH(extract(@n1, 1), @n2) [ @returns] .ratioVH(extract(@n1, 2), @n2) [ 75 | @returns] .ratioVH(extract(@n1, 3), @n2) [ @returns] .ratioVH(extract(@n1, 4), @n2) [ 76 | @returns]; 77 | } 78 | 79 | & when (@aspect-ratio = true) { 80 | // calc viewport height 81 | @local-viewport-height: @viewport-reference-desktop-height + 1; 82 | 83 | @media (max-aspect-ratio: ~"@{viewport-reference-desktop-width} / @{local-viewport-height}") { 84 | .propertyVW(@property, @n1, @n2 : @viewport-reference-desktop-width); 85 | } 86 | } 87 | } 88 | 89 | /** 90 | Responsive Property 91 | */ 92 | .propertyViewport( 93 | @property, 94 | @value1, 95 | @value2: @value1, 96 | @breakpoint: @breakpoint-tablet, 97 | @capValue: false, 98 | @aspect-ratio: true 99 | ) { 100 | .propertyVW(@property, @value1, @n2 : @viewport-reference-width); 101 | @media (min-width: @breakpoint) { 102 | .propertyVH(@property, @value2, @n2 : @viewport-reference-desktop-height, @aspect-ratio); 103 | } 104 | 105 | & when(@capValue) { 106 | @media (min-width: @breakpoint-desktop) { 107 | @value2-length: length(@value2); 108 | 109 | & when (@value2-length = 1) { 110 | @{property}: .toPx(@value2) [ @returns]; 111 | } 112 | & when (@value2-length = 2) { 113 | @{property}: .toPx(extract(@value2, 1)) [ @returns] .toPx(extract(@value2, 2)) [ @returns]; 114 | } 115 | & when (@value2-length = 3) { 116 | @{property}: .toPx(extract(@value2, 1)) [ @returns] .toPx(extract(@value2, 2)) [ @returns] 117 | .toPx(extract(@value2, 3)) [ @returns]; 118 | } 119 | & when (@value2-length = 4) { 120 | @{property}: .toPx(extract(@value2, 1)) [ @returns] .toPx(extract(@value2, 2)) [ @returns] 121 | .toPx(extract(@value2, 3)) [ @returns] .toPx(extract(@value2, 4)) [ @returns]; 122 | } 123 | } 124 | } 125 | } 126 | 127 | /** 128 | Pixel to Rem property, mobile & desktop 129 | */ 130 | .rem(@property, @value-mobile, @value-desktop: false, @breakpoint: @breakpoint-tablet) { 131 | @value-length-mobile: length(@value-mobile); 132 | 133 | & when (@value-length-mobile = 1) { 134 | @{property}: .toRem(@value-mobile) [ @returns]; 135 | } 136 | & when (@value-length-mobile = 2) { 137 | @{property}: .toRem(extract(@value-mobile, 1)) [ @returns] .toRem(extract(@value-mobile, 2)) [ 138 | @returns]; 139 | } 140 | & when (@value-length-mobile = 3) { 141 | @{property}: .toRem(extract(@value-mobile, 1)) [ @returns] .toRem(extract(@value-mobile, 2)) [ 142 | @returns] .toRem(extract(@value-mobile, 3)) [ @returns]; 143 | } 144 | & when (@value-length-mobile = 4) { 145 | @{property}: .toRem(extract(@value-mobile, 1)) [ @returns] .toRem(extract(@value-mobile, 2)) [ 146 | @returns] .toRem(extract(@value-mobile, 3)) [ @returns] .toRem(extract(@value-mobile, 4)) [ 147 | @returns]; 148 | } 149 | 150 | & when (not (@value-desktop = false)) { 151 | @value-length-desktop: length(@value-desktop); 152 | 153 | @media (min-width: @breakpoint) { 154 | & when (@value-length-desktop = 1) { 155 | @{property}: .toRem(@value-desktop) [ @returns]; 156 | } 157 | & when (@value-length-desktop = 2) { 158 | @{property}: .toRem(extract(@value-desktop, 1)) [ @returns] 159 | .toRem(extract(@value-desktop, 2)) [ @returns]; 160 | } 161 | & when (@value-length-desktop = 3) { 162 | @{property}: .toRem(extract(@value-desktop, 1)) [ @returns] 163 | .toRem(extract(@value-desktop, 2)) [ @returns] .toRem(extract(@value-desktop, 3)) [ 164 | @returns]; 165 | } 166 | & when (@value-length-desktop = 4) { 167 | @{property}: .toRem(extract(@value-desktop, 1)) [ @returns] 168 | .toRem(extract(@value-desktop, 2)) [ @returns] .toRem(extract(@value-desktop, 3)) [ 169 | @returns] .toRem(extract(@value-desktop, 4)) [ @returns]; 170 | } 171 | } 172 | } 173 | } 174 | 175 | /** 176 | * Set a property size to rem from px size 177 | */ 178 | .toRem(@value) { 179 | @isnumber: isnumber(@value); 180 | @returns: if(isnumber(@value), unit(@value, rem), @value); 181 | } 182 | 183 | /** 184 | * Set a property size to rem from px size 185 | */ 186 | .toPx(@value) { 187 | @isnumber: isnumber(@value); 188 | @returns: if(isnumber(@value), unit(@value, px), @value); 189 | } 190 | -------------------------------------------------------------------------------- /examples/interpol-dom-ondrag/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "interpol-dom-ondrag", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "main": "src/index.ts", 7 | "scripts": { 8 | "dev": "vite --host" 9 | }, 10 | "dependencies": { 11 | "@wbe/interpol": "workspace:*", 12 | "@use-gesture/vanilla": "latest", 13 | "gsap": "latest" 14 | }, 15 | "devDependencies": { 16 | "typescript": "^5.7.2", 17 | "vite": "^6.0.6" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/interpol-dom-ondrag/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-gray: #454545; 3 | --color-black-1: #313131; 4 | --color-black: #171717; 5 | --color-blue: #646cff; 6 | --color-white: #fff; 7 | } 8 | 9 | html { 10 | font-size: var(--font-size); 11 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 12 | font-weight: 400; 13 | color-scheme: dark; 14 | font-synthesis: none; 15 | text-rendering: optimizeLegibility; 16 | -webkit-font-smoothing: antialiased; 17 | -moz-osx-font-smoothing: grayscale; 18 | -webkit-text-size-adjust: 100%; 19 | } 20 | 21 | body { 22 | margin: 0; 23 | font-size: 16px; 24 | min-width: 100vw; 25 | min-height: 100vh; 26 | overflow: hidden; 27 | position: fixed; 28 | } 29 | 30 | .ball { 31 | position: absolute; 32 | width: 50px; 33 | height: 50px; 34 | border-radius: 50%; 35 | background-color: red; 36 | touch-action: none; 37 | cursor: grab; 38 | &:active { 39 | cursor: grabbing; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/interpol-dom-ondrag/src/main.ts: -------------------------------------------------------------------------------- 1 | import "./index.css" 2 | import { Interpol, styles } from "@wbe/interpol" 3 | import { DragGesture } from "@use-gesture/vanilla" 4 | import gsap from "gsap" 5 | 6 | const ball = document.querySelector(".ball") 7 | 8 | let state = { 9 | current: { 10 | x: 0, 11 | y: 0, 12 | }, 13 | target: { 14 | x: 0, 15 | y: 0, 16 | }, 17 | } 18 | 19 | // ------------------------------------------------------------------------------------------------- 20 | 21 | /** 22 | * Using interpol 23 | * 24 | * Not really perf, because we have to recreate an object on each dragging pixel 25 | */ 26 | new DragGesture(ball, ({ active, delta: [dx, dy] }) => { 27 | // set the new target 28 | state.target.x += dx 29 | state.target.y += dy 30 | 31 | new Interpol({ 32 | ease: "expo.out", 33 | duration: 2000, 34 | x: { from: state.current.x, to: state.target.x }, 35 | y: { from: state.current.y, to: state.target.y }, 36 | 37 | onUpdate: ({ x, y }) => { 38 | // anim 39 | styles(ball, { x: x + "px", y: y + "px" }) 40 | 41 | // update the current values 42 | state.current.x = x 43 | state.current.y = y 44 | }, 45 | }) 46 | }) 47 | 48 | // ------------------------------------------------------------------------------------------------- 49 | 50 | /** 51 | * Using simple lerp & raf 52 | 53 | const lerp = (a: number, b: number, t: number) => a + (b - a) * t 54 | 55 | new DragGesture(ball, ({ delta: [dx, dy] }) => { 56 | state.target.x += dx 57 | state.target.y += dy 58 | }) 59 | 60 | InterpolOptions.ticker.add(()=> { 61 | state.current.x = lerp(state.current.x, state.target.x, 0.04) 62 | state.current.y = lerp(state.current.y, state.target.y, 0.04) 63 | ball.style.transform = `translate3d(${state.current.x}px, ${state.current.y}px, ${0}px)` 64 | }) 65 | 66 | */ 67 | 68 | // ------------------------------------------------------------------------------------------------- 69 | 70 | /** 71 | * Using GSAP 72 | 73 | new DragGesture(ball, ({ delta: [dx, dy] }) => { 74 | state.current.x += dx 75 | state.current.y += dy 76 | gsap.to(ball, { 77 | duration: 2, 78 | x: state.current.x, 79 | y: state.current.y, 80 | ease: "expo.out" 81 | }) 82 | }) 83 | 84 | */ 85 | -------------------------------------------------------------------------------- /examples/interpol-dom-ondrag/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/interpol-dom-ondrag/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | 19 | /* Linting */ 20 | "strict": false, 21 | "noUnusedLocals": false, 22 | "noUnusedParameters": false, 23 | "noFallthroughCasesInSwitch": false 24 | }, 25 | 26 | "include": ["src"], 27 | "references": [{ "path": "./tsconfig.node.json" }] 28 | } 29 | -------------------------------------------------------------------------------- /examples/interpol-dom-ondrag/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /examples/interpol-dom-ondrag/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | 3 | // https://vitejs.dev/config/ 4 | export default defineConfig({}) 5 | -------------------------------------------------------------------------------- /examples/interpol-ease/.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 | -------------------------------------------------------------------------------- /examples/interpol-ease/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | interpol 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/interpol-ease/libs/ratio.less: -------------------------------------------------------------------------------- 1 | @viewport-reference-width: 375; 2 | @viewport-reference-height: 667; 3 | @viewport-reference-desktop-width: 1400; 4 | @viewport-reference-desktop-height: 900; 5 | @breakpoint-mobile: 320px; 6 | @breakpoint-tablet: 768px; 7 | @breakpoint-laptop: 1024px; 8 | @breakpoint-bigLaptop: 1440px; 9 | @breakpoint-desktop: 1680px; 10 | 11 | /** 12 | Get VH value from ratio 13 | */ 14 | .ratioVW(@n1, @n2 : @viewport-reference-width) { 15 | @isnumber: isnumber(@n1); 16 | @returns: if((@isnumber), (@n1 / @n2) * 100vw, @n1); 17 | } 18 | 19 | /** 20 | Get VH value from ratio 21 | */ 22 | .ratioVH(@n1, @n2 : @viewport-reference-height) { 23 | @isnumber: isnumber(@n1); 24 | 25 | // fallback for old browser who don't support css --var 26 | @returns: if((@isnumber), (@n1 / @n2) * 100vh, @n1); 27 | // vh value relative to css --vh variable 28 | @var: var(--vh, 1vh); 29 | @returns: if((@isnumber), ~"calc( (@{n1} / @{n2}) * (@{var} * 100) )", @n1); 30 | } 31 | 32 | /** 33 | Get VW value for property according to value in pixels 34 | */ 35 | .propertyVW(@property, @n1, @n2 : @viewport-reference-width) { 36 | @value-length-mobile: length(@n1); 37 | 38 | & when (@value-length-mobile = 1) { 39 | @{property}: .ratioVW(@n1, @n2) [ @returns]; 40 | } 41 | & when (@value-length-mobile = 2) { 42 | @{property}: .ratioVW(extract(@n1, 1), @n2) [ @returns] .ratioVW(extract(@n1, 2), @n2) [ 43 | @returns]; 44 | } 45 | & when (@value-length-mobile = 3) { 46 | @{property}: .ratioVW(extract(@n1, 1), @n2) [ @returns] .ratioVW(extract(@n1, 2), @n2) [ 47 | @returns] .ratioVW(extract(@n1, 3), @n2) [ @returns]; 48 | } 49 | & when (@value-length-mobile = 4) { 50 | @{property}: .ratioVW(extract(@n1, 1), @n2) [ @returns] .ratioVW(extract(@n1, 2), @n2) [ 51 | @returns] .ratioVW(extract(@n1, 3), @n2) [ @returns] .ratioVW(extract(@n1, 4), @n2) [ 52 | @returns]; 53 | } 54 | } 55 | 56 | /** 57 | Get VH value for property according to value in pixels 58 | */ 59 | .propertyVH(@property, @n1, @n2 : @viewport-reference-height, @aspect-ratio: true) { 60 | @value-length-mobile: length(@n1); 61 | 62 | & when (@value-length-mobile = 1) { 63 | @{property}: .ratioVH(@n1, @n2) [ @returns]; 64 | } 65 | & when (@value-length-mobile = 2) { 66 | @{property}: .ratioVH(extract(@n1, 1), @n2) [ @returns] .ratioVH(extract(@n1, 2), @n2) [ 67 | @returns]; 68 | } 69 | & when (@value-length-mobile = 3) { 70 | @{property}: .ratioVH(extract(@n1, 1), @n2) [ @returns] .ratioVH(extract(@n1, 2), @n2) [ 71 | @returns] .ratioVH(extract(@n1, 3), @n2) [ @returns]; 72 | } 73 | & when (@value-length-mobile = 4) { 74 | @{property}: .ratioVH(extract(@n1, 1), @n2) [ @returns] .ratioVH(extract(@n1, 2), @n2) [ 75 | @returns] .ratioVH(extract(@n1, 3), @n2) [ @returns] .ratioVH(extract(@n1, 4), @n2) [ 76 | @returns]; 77 | } 78 | 79 | & when (@aspect-ratio = true) { 80 | // calc viewport height 81 | @local-viewport-height: @viewport-reference-desktop-height + 1; 82 | 83 | @media (max-aspect-ratio: ~"@{viewport-reference-desktop-width} / @{local-viewport-height}") { 84 | .propertyVW(@property, @n1, @n2 : @viewport-reference-desktop-width); 85 | } 86 | } 87 | } 88 | 89 | /** 90 | Responsive Property 91 | */ 92 | .propertyViewport( 93 | @property, 94 | @value1, 95 | @value2: @value1, 96 | @breakpoint: @breakpoint-tablet, 97 | @capValue: false, 98 | @aspect-ratio: true 99 | ) { 100 | .propertyVW(@property, @value1, @n2 : @viewport-reference-width); 101 | @media (min-width: @breakpoint) { 102 | .propertyVH(@property, @value2, @n2 : @viewport-reference-desktop-height, @aspect-ratio); 103 | } 104 | 105 | & when(@capValue) { 106 | @media (min-width: @breakpoint-desktop) { 107 | @value2-length: length(@value2); 108 | 109 | & when (@value2-length = 1) { 110 | @{property}: .toPx(@value2) [ @returns]; 111 | } 112 | & when (@value2-length = 2) { 113 | @{property}: .toPx(extract(@value2, 1)) [ @returns] .toPx(extract(@value2, 2)) [ @returns]; 114 | } 115 | & when (@value2-length = 3) { 116 | @{property}: .toPx(extract(@value2, 1)) [ @returns] .toPx(extract(@value2, 2)) [ @returns] 117 | .toPx(extract(@value2, 3)) [ @returns]; 118 | } 119 | & when (@value2-length = 4) { 120 | @{property}: .toPx(extract(@value2, 1)) [ @returns] .toPx(extract(@value2, 2)) [ @returns] 121 | .toPx(extract(@value2, 3)) [ @returns] .toPx(extract(@value2, 4)) [ @returns]; 122 | } 123 | } 124 | } 125 | } 126 | 127 | /** 128 | Pixel to Rem property, mobile & desktop 129 | */ 130 | .rem(@property, @value-mobile, @value-desktop: false, @breakpoint: @breakpoint-tablet) { 131 | @value-length-mobile: length(@value-mobile); 132 | 133 | & when (@value-length-mobile = 1) { 134 | @{property}: .toRem(@value-mobile) [ @returns]; 135 | } 136 | & when (@value-length-mobile = 2) { 137 | @{property}: .toRem(extract(@value-mobile, 1)) [ @returns] .toRem(extract(@value-mobile, 2)) [ 138 | @returns]; 139 | } 140 | & when (@value-length-mobile = 3) { 141 | @{property}: .toRem(extract(@value-mobile, 1)) [ @returns] .toRem(extract(@value-mobile, 2)) [ 142 | @returns] .toRem(extract(@value-mobile, 3)) [ @returns]; 143 | } 144 | & when (@value-length-mobile = 4) { 145 | @{property}: .toRem(extract(@value-mobile, 1)) [ @returns] .toRem(extract(@value-mobile, 2)) [ 146 | @returns] .toRem(extract(@value-mobile, 3)) [ @returns] .toRem(extract(@value-mobile, 4)) [ 147 | @returns]; 148 | } 149 | 150 | & when (not (@value-desktop = false)) { 151 | @value-length-desktop: length(@value-desktop); 152 | 153 | @media (min-width: @breakpoint) { 154 | & when (@value-length-desktop = 1) { 155 | @{property}: .toRem(@value-desktop) [ @returns]; 156 | } 157 | & when (@value-length-desktop = 2) { 158 | @{property}: .toRem(extract(@value-desktop, 1)) [ @returns] 159 | .toRem(extract(@value-desktop, 2)) [ @returns]; 160 | } 161 | & when (@value-length-desktop = 3) { 162 | @{property}: .toRem(extract(@value-desktop, 1)) [ @returns] 163 | .toRem(extract(@value-desktop, 2)) [ @returns] .toRem(extract(@value-desktop, 3)) [ 164 | @returns]; 165 | } 166 | & when (@value-length-desktop = 4) { 167 | @{property}: .toRem(extract(@value-desktop, 1)) [ @returns] 168 | .toRem(extract(@value-desktop, 2)) [ @returns] .toRem(extract(@value-desktop, 3)) [ 169 | @returns] .toRem(extract(@value-desktop, 4)) [ @returns]; 170 | } 171 | } 172 | } 173 | } 174 | 175 | /** 176 | * Set a property size to rem from px size 177 | */ 178 | .toRem(@value) { 179 | @isnumber: isnumber(@value); 180 | @returns: if(isnumber(@value), unit(@value, rem), @value); 181 | } 182 | 183 | /** 184 | * Set a property size to rem from px size 185 | */ 186 | .toPx(@value) { 187 | @isnumber: isnumber(@value); 188 | @returns: if(isnumber(@value), unit(@value, px), @value); 189 | } 190 | -------------------------------------------------------------------------------- /examples/interpol-ease/libs/useWindowSize.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react" 2 | 3 | export const useWindowSize = (): { width: number; height: number } => { 4 | const [s, setS] = useState({ width: window.innerWidth, height: window.innerHeight }) 5 | useEffect(() => { 6 | const handler = () => { 7 | setS({ width: window.innerWidth, height: window.innerHeight }) 8 | } 9 | window.addEventListener("resie", handler) 10 | return () => { 11 | window.removeEventListener("resie", handler) 12 | } 13 | }, [s]) 14 | 15 | return s 16 | } 17 | -------------------------------------------------------------------------------- /examples/interpol-ease/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "interpol-ease", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "main": "src/index.ts", 7 | "scripts": { 8 | "dev": "vite --host" 9 | }, 10 | "dependencies": { 11 | "@wbe/interpol": "^0.20.2", 12 | "react": "^19.0.0", 13 | "react-dom": "^19.0.0" 14 | }, 15 | "devDependencies": { 16 | "@types/react": "^19.0.2", 17 | "@types/react-dom": "^19.0.2", 18 | "@vitejs/plugin-react": "^4.3.4", 19 | "less": "^4.2.1", 20 | "typescript": "^5.7.2", 21 | "vite": "^6.0.6" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/interpol-ease/src/App.module.less: -------------------------------------------------------------------------------- 1 | @import (reference) "../libs/ratio.less"; 2 | 3 | .root { 4 | width: 100vw; 5 | height: 100vh; 6 | } 7 | 8 | .wrapper { 9 | padding: 30rem; 10 | @media (min-width: @breakpoint-tablet) { 11 | padding: 70rem; 12 | } 13 | } 14 | 15 | .controls { 16 | } 17 | 18 | .params { 19 | margin-top: 20rem; 20 | } 21 | 22 | .ball { 23 | will-change: transform; 24 | position: absolute; 25 | @size: 40rem; 26 | width: @size; 27 | height: @size; 28 | bottom: 0; 29 | left: 0; 30 | background: #c59f00; 31 | border-radius: 50%; 32 | } 33 | -------------------------------------------------------------------------------- /examples/interpol-ease/src/App.tsx: -------------------------------------------------------------------------------- 1 | import css from "./App.module.less" 2 | import React, { useEffect, useRef, useState } from "react" 3 | import { Interpol } from "@wbe/interpol" 4 | import { Controls } from "./Controls" 5 | import { Params } from "./Params" 6 | import { useWindowSize } from "../libs/useWindowSize" 7 | 8 | export function App() { 9 | const $ball = useRef(null) 10 | const { width } = useWindowSize() 11 | 12 | const [instance, setInstance] = useState(null) 13 | const [ease, setEase] = useState(null) 14 | const [params, setParams] = useState<{ value; time; progress }>({ 15 | value: 0, 16 | time: 0, 17 | progress: 0, 18 | }) 19 | 20 | const firstMount = useRef(true) 21 | useEffect(() => { 22 | if (!$ball.current) return 23 | const ballSize = $ball.current?.offsetWidth 24 | 25 | const itp = new Interpol({ 26 | value: [0, () => window.innerHeight - ballSize], 27 | duration: 1000, 28 | ease: Ease[ease], 29 | debug: true, 30 | paused: true, 31 | onUpdate: ({ value }, time, progress) => { 32 | setParams({ value: value, time, progress }) 33 | const x = progress * (window.innerWidth - ballSize) 34 | const y = -value 35 | $ball.current.style.transform = ` 36 | translateX(${x}px) 37 | translateY(${y}px) 38 | translateZ(0) 39 | ` 40 | }, 41 | }) 42 | setInstance(itp) 43 | 44 | if (firstMount.current) { 45 | firstMount.current = false 46 | return 47 | } else { 48 | itp.play() 49 | } 50 | }, [ease, width]) 51 | 52 | return ( 53 |
54 |
55 | setEase(ease)} 59 | /> 60 | 61 |
62 |
63 |
64 | ) 65 | } 66 | 67 | // prettier-ignore 68 | export const Ease = { 69 | linear: t => t, 70 | 71 | inQuad: t => t*t, 72 | outQuad: t => t*(2-t), 73 | inOutQuad: t => t<.5 ? 2*t*t : -1+(4-2*t)*t, 74 | 75 | inCubic: t => t*t*t, 76 | outCubic: t => (--t)*t*t+1, 77 | inOutCubic: t => t<.5 ? 4*t*t*t : (t-1)*(2*t-2)*(2*t-2)+1, 78 | 79 | inQuart: t => t*t*t*t, 80 | outQuart: t => 1-(--t)*t*t*t, 81 | inOutQuart: t => t<.5 ? 8*t*t*t*t : 1-8*(--t)*t*t*t, 82 | 83 | inQuint: t => t*t*t*t*t, 84 | outQuint: t => 1+(--t)*t*t*t*t, 85 | inOutQuint: t => t<.5 ? 16*t*t*t*t*t : 1+16*(--t)*t*t*t*t, 86 | 87 | inSine:t => -Math.cos(t * (Math.PI/2)) + 1, 88 | outSine:t => Math.sin(t * (Math.PI/2)), 89 | inOutSine:t => (-0.5 * (Math.cos(Math.PI*t) -1)), 90 | 91 | inExpo:t => (t===0) ? 0 : Math.pow(2, 10 * (t - 1)), 92 | outExpo:t => (t===1) ? 1 : -Math.pow(2, -10 * t) + 1, 93 | inOutExpo: t => { 94 | if (t===0) return 0; 95 | if (t===1) return 1; 96 | if ((t/=0.5) < 1) return 0.5 * Math.pow(2,10 * (t-1)); 97 | return 0.5 * (-Math.pow(2, -10 * --t) + 2); 98 | }, 99 | 100 | } 101 | -------------------------------------------------------------------------------- /examples/interpol-ease/src/Controls.module.less: -------------------------------------------------------------------------------- 1 | @import (reference) "../libs/ratio.less"; 2 | 3 | .root { 4 | } 5 | 6 | .wrapper { 7 | gap: 10rem; 8 | } 9 | 10 | .buttons { 11 | display: flex; 12 | flex-direction: row; 13 | flex-wrap: wrap; 14 | justify-content: left; 15 | gap: 10rem; 16 | } 17 | 18 | .button, 19 | .easeSelect, 20 | .easeOption { 21 | display: block; 22 | position: relative; 23 | border-radius: 8rem; 24 | border: 1px solid transparent; 25 | padding: 8rem 10rem; 26 | font-size: 100%; 27 | background-color: var(--color-black-1); 28 | cursor: pointer; 29 | } 30 | .button:hover { 31 | border-color: var(--color-blue); 32 | } 33 | .button:active, 34 | .button:focus, 35 | .button:focus-visible { 36 | border-color: var(--color-blue); 37 | } 38 | 39 | .easeSelect { 40 | margin-top: 20rem; 41 | max-width: 100%; 42 | } 43 | -------------------------------------------------------------------------------- /examples/interpol-ease/src/Controls.tsx: -------------------------------------------------------------------------------- 1 | import css from "./Controls.module.less" 2 | import React, { useEffect, useState } from "react" 3 | import { Interpol } from "@wbe/interpol" 4 | import { Ease } from "./App" 5 | export const Controls = ({ 6 | className, 7 | instance, 8 | dispatchEase, 9 | }: { 10 | className: string 11 | instance: Interpol 12 | dispatchEase: (ease) => void 13 | }) => { 14 | const [progress, setProgress] = useState("0") 15 | 16 | useEffect(() => { 17 | instance?.seek(parseFloat(progress) / 100) 18 | }, [progress]) 19 | 20 | return ( 21 |
22 |
23 | {/* prettier-ignore */} 24 |
25 |
33 | 34 | 45 |
46 | setProgress(e.target.value || "0")} 50 | /> 51 |
52 |
53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /examples/interpol-ease/src/Params.module.less: -------------------------------------------------------------------------------- 1 | @import (reference) "../libs/ratio.less"; 2 | 3 | .root { 4 | width: 100%; 5 | } 6 | 7 | .wrapper { 8 | } 9 | 10 | .list { 11 | list-style: none; 12 | text-align: left; 13 | padding: 0; 14 | margin: 0; 15 | } 16 | 17 | .item { 18 | color: var(--color-gray); 19 | font-size: 20rem; 20 | @media (min-width: @breakpoint-tablet) { 21 | font-size: 24rem; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/interpol-ease/src/Params.tsx: -------------------------------------------------------------------------------- 1 | import css from "./Params.module.less" 2 | import React from "react" 3 | 4 | export function Params({ params, className }) { 5 | return ( 6 |
7 |
8 |
    9 | {Object.keys(params).map((e, i) => ( 10 |
  • 11 | {e}:{" "} 12 | {params[e]} 13 |
  • 14 | ))} 15 |
16 |
17 |
18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /examples/interpol-ease/src/index.less: -------------------------------------------------------------------------------- 1 | @import (reference) "../libs/ratio.less"; 2 | 3 | :root { 4 | --color-gray: #454545; 5 | --color-black-1: #313131; 6 | --color-black: #171717; 7 | --color-blue: #646cff; 8 | --color-white: #fff; 9 | } 10 | 11 | html { 12 | .propertyViewport(--font-size, 1, 1.3); 13 | font-size: var(--font-size); 14 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 15 | font-weight: 400; 16 | color-scheme: dark; 17 | font-synthesis: none; 18 | text-rendering: optimizeLegibility; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-osx-font-smoothing: grayscale; 21 | -webkit-text-size-adjust: 100%; 22 | } 23 | 24 | body { 25 | margin: 0; 26 | font-size: 16rem; 27 | min-width: 100vw; 28 | min-height: 100vh; 29 | overflow: hidden; 30 | position: fixed; 31 | } 32 | -------------------------------------------------------------------------------- /examples/interpol-ease/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom/client" 3 | import "./index.less" 4 | import { App } from "./App" 5 | 6 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render() 7 | -------------------------------------------------------------------------------- /examples/interpol-ease/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/interpol-ease/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": false, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /examples/interpol-ease/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /examples/interpol-ease/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | -------------------------------------------------------------------------------- /examples/interpol-graphic/.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 | -------------------------------------------------------------------------------- /examples/interpol-graphic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | interpol 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/interpol-graphic/libs/ratio.less: -------------------------------------------------------------------------------- 1 | @viewport-reference-width: 375; 2 | @viewport-reference-height: 667; 3 | @viewport-reference-desktop-width: 1400; 4 | @viewport-reference-desktop-height: 900; 5 | @breakpoint-mobile: 320px; 6 | @breakpoint-tablet: 768px; 7 | @breakpoint-laptop: 1024px; 8 | @breakpoint-bigLaptop: 1440px; 9 | @breakpoint-desktop: 1680px; 10 | 11 | /** 12 | Get VH value from ratio 13 | */ 14 | .ratioVW(@n1, @n2 : @viewport-reference-width) { 15 | @isnumber: isnumber(@n1); 16 | @returns: if((@isnumber), (@n1 / @n2) * 100vw, @n1); 17 | } 18 | 19 | /** 20 | Get VH value from ratio 21 | */ 22 | .ratioVH(@n1, @n2 : @viewport-reference-height) { 23 | @isnumber: isnumber(@n1); 24 | 25 | // fallback for old browser who don't support css --var 26 | @returns: if((@isnumber), (@n1 / @n2) * 100vh, @n1); 27 | // vh value relative to css --vh variable 28 | @var: var(--vh, 1vh); 29 | @returns: if((@isnumber), ~"calc( (@{n1} / @{n2}) * (@{var} * 100) )", @n1); 30 | } 31 | 32 | /** 33 | Get VW value for property according to value in pixels 34 | */ 35 | .propertyVW(@property, @n1, @n2 : @viewport-reference-width) { 36 | @value-length-mobile: length(@n1); 37 | 38 | & when (@value-length-mobile = 1) { 39 | @{property}: .ratioVW(@n1, @n2) [ @returns]; 40 | } 41 | & when (@value-length-mobile = 2) { 42 | @{property}: .ratioVW(extract(@n1, 1), @n2) [ @returns] .ratioVW(extract(@n1, 2), @n2) [ 43 | @returns]; 44 | } 45 | & when (@value-length-mobile = 3) { 46 | @{property}: .ratioVW(extract(@n1, 1), @n2) [ @returns] .ratioVW(extract(@n1, 2), @n2) [ 47 | @returns] .ratioVW(extract(@n1, 3), @n2) [ @returns]; 48 | } 49 | & when (@value-length-mobile = 4) { 50 | @{property}: .ratioVW(extract(@n1, 1), @n2) [ @returns] .ratioVW(extract(@n1, 2), @n2) [ 51 | @returns] .ratioVW(extract(@n1, 3), @n2) [ @returns] .ratioVW(extract(@n1, 4), @n2) [ 52 | @returns]; 53 | } 54 | } 55 | 56 | /** 57 | Get VH value for property according to value in pixels 58 | */ 59 | .propertyVH(@property, @n1, @n2 : @viewport-reference-height, @aspect-ratio: true) { 60 | @value-length-mobile: length(@n1); 61 | 62 | & when (@value-length-mobile = 1) { 63 | @{property}: .ratioVH(@n1, @n2) [ @returns]; 64 | } 65 | & when (@value-length-mobile = 2) { 66 | @{property}: .ratioVH(extract(@n1, 1), @n2) [ @returns] .ratioVH(extract(@n1, 2), @n2) [ 67 | @returns]; 68 | } 69 | & when (@value-length-mobile = 3) { 70 | @{property}: .ratioVH(extract(@n1, 1), @n2) [ @returns] .ratioVH(extract(@n1, 2), @n2) [ 71 | @returns] .ratioVH(extract(@n1, 3), @n2) [ @returns]; 72 | } 73 | & when (@value-length-mobile = 4) { 74 | @{property}: .ratioVH(extract(@n1, 1), @n2) [ @returns] .ratioVH(extract(@n1, 2), @n2) [ 75 | @returns] .ratioVH(extract(@n1, 3), @n2) [ @returns] .ratioVH(extract(@n1, 4), @n2) [ 76 | @returns]; 77 | } 78 | 79 | & when (@aspect-ratio = true) { 80 | // calc viewport height 81 | @local-viewport-height: @viewport-reference-desktop-height + 1; 82 | 83 | @media (max-aspect-ratio: ~"@{viewport-reference-desktop-width} / @{local-viewport-height}") { 84 | .propertyVW(@property, @n1, @n2 : @viewport-reference-desktop-width); 85 | } 86 | } 87 | } 88 | 89 | /** 90 | Responsive Property 91 | */ 92 | .propertyViewport( 93 | @property, 94 | @value1, 95 | @value2: @value1, 96 | @breakpoint: @breakpoint-tablet, 97 | @capValue: false, 98 | @aspect-ratio: true 99 | ) { 100 | .propertyVW(@property, @value1, @n2 : @viewport-reference-width); 101 | @media (min-width: @breakpoint) { 102 | .propertyVH(@property, @value2, @n2 : @viewport-reference-desktop-height, @aspect-ratio); 103 | } 104 | 105 | & when(@capValue) { 106 | @media (min-width: @breakpoint-desktop) { 107 | @value2-length: length(@value2); 108 | 109 | & when (@value2-length = 1) { 110 | @{property}: .toPx(@value2) [ @returns]; 111 | } 112 | & when (@value2-length = 2) { 113 | @{property}: .toPx(extract(@value2, 1)) [ @returns] .toPx(extract(@value2, 2)) [ @returns]; 114 | } 115 | & when (@value2-length = 3) { 116 | @{property}: .toPx(extract(@value2, 1)) [ @returns] .toPx(extract(@value2, 2)) [ @returns] 117 | .toPx(extract(@value2, 3)) [ @returns]; 118 | } 119 | & when (@value2-length = 4) { 120 | @{property}: .toPx(extract(@value2, 1)) [ @returns] .toPx(extract(@value2, 2)) [ @returns] 121 | .toPx(extract(@value2, 3)) [ @returns] .toPx(extract(@value2, 4)) [ @returns]; 122 | } 123 | } 124 | } 125 | } 126 | 127 | /** 128 | Pixel to Rem property, mobile & desktop 129 | */ 130 | .rem(@property, @value-mobile, @value-desktop: false, @breakpoint: @breakpoint-tablet) { 131 | @value-length-mobile: length(@value-mobile); 132 | 133 | & when (@value-length-mobile = 1) { 134 | @{property}: .toRem(@value-mobile) [ @returns]; 135 | } 136 | & when (@value-length-mobile = 2) { 137 | @{property}: .toRem(extract(@value-mobile, 1)) [ @returns] .toRem(extract(@value-mobile, 2)) [ 138 | @returns]; 139 | } 140 | & when (@value-length-mobile = 3) { 141 | @{property}: .toRem(extract(@value-mobile, 1)) [ @returns] .toRem(extract(@value-mobile, 2)) [ 142 | @returns] .toRem(extract(@value-mobile, 3)) [ @returns]; 143 | } 144 | & when (@value-length-mobile = 4) { 145 | @{property}: .toRem(extract(@value-mobile, 1)) [ @returns] .toRem(extract(@value-mobile, 2)) [ 146 | @returns] .toRem(extract(@value-mobile, 3)) [ @returns] .toRem(extract(@value-mobile, 4)) [ 147 | @returns]; 148 | } 149 | 150 | & when (not (@value-desktop = false)) { 151 | @value-length-desktop: length(@value-desktop); 152 | 153 | @media (min-width: @breakpoint) { 154 | & when (@value-length-desktop = 1) { 155 | @{property}: .toRem(@value-desktop) [ @returns]; 156 | } 157 | & when (@value-length-desktop = 2) { 158 | @{property}: .toRem(extract(@value-desktop, 1)) [ @returns] 159 | .toRem(extract(@value-desktop, 2)) [ @returns]; 160 | } 161 | & when (@value-length-desktop = 3) { 162 | @{property}: .toRem(extract(@value-desktop, 1)) [ @returns] 163 | .toRem(extract(@value-desktop, 2)) [ @returns] .toRem(extract(@value-desktop, 3)) [ 164 | @returns]; 165 | } 166 | & when (@value-length-desktop = 4) { 167 | @{property}: .toRem(extract(@value-desktop, 1)) [ @returns] 168 | .toRem(extract(@value-desktop, 2)) [ @returns] .toRem(extract(@value-desktop, 3)) [ 169 | @returns] .toRem(extract(@value-desktop, 4)) [ @returns]; 170 | } 171 | } 172 | } 173 | } 174 | 175 | /** 176 | * Set a property size to rem from px size 177 | */ 178 | .toRem(@value) { 179 | @isnumber: isnumber(@value); 180 | @returns: if(isnumber(@value), unit(@value, rem), @value); 181 | } 182 | 183 | /** 184 | * Set a property size to rem from px size 185 | */ 186 | .toPx(@value) { 187 | @isnumber: isnumber(@value); 188 | @returns: if(isnumber(@value), unit(@value, px), @value); 189 | } 190 | -------------------------------------------------------------------------------- /examples/interpol-graphic/libs/useWindowSize.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react" 2 | 3 | export const useWindowSize = (): { width: number; height: number } => { 4 | const [s, setS] = useState({ width: window.innerWidth, height: window.innerHeight }) 5 | useEffect(() => { 6 | const handler = () => { 7 | setS({ width: window.innerWidth, height: window.innerHeight }) 8 | } 9 | window.addEventListener("resie", handler) 10 | return () => { 11 | window.removeEventListener("resie", handler) 12 | } 13 | }, [s]) 14 | 15 | return s 16 | } 17 | -------------------------------------------------------------------------------- /examples/interpol-graphic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "interpol-graphic", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "main": "src/index.ts", 7 | "scripts": { 8 | "dev": "vite --host" 9 | }, 10 | "dependencies": { 11 | "@wbe/interpol": "^0.20.2", 12 | "react": "^19.0.0", 13 | "react-dom": "^19.0.0" 14 | }, 15 | "devDependencies": { 16 | "@types/react": "^19.0.2", 17 | "@types/react-dom": "^19.0.2", 18 | "@vitejs/plugin-react": "^4.3.4", 19 | "less": "^4.2.1", 20 | "typescript": "^5.7.2", 21 | "vite": "^6.0.6" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/interpol-graphic/src/components/app/App.module.less: -------------------------------------------------------------------------------- 1 | @import (reference) "../../../libs/ratio.less"; 2 | 3 | .root { 4 | } 5 | 6 | .controls { 7 | position: relative; 8 | top: 0; 9 | // position: fixed; 10 | } 11 | 12 | .params { 13 | position: relative; 14 | margin-top: 10rem; 15 | } 16 | 17 | .ball { 18 | position: absolute; 19 | left: 0; 20 | bottom: 0; 21 | width: var(--ball-size); 22 | height: var(--ball-size); 23 | border-radius: 50%; 24 | background-color: red; 25 | } 26 | -------------------------------------------------------------------------------- /examples/interpol-graphic/src/components/app/App.tsx: -------------------------------------------------------------------------------- 1 | import css from "./App.module.less" 2 | import React, { useEffect, useMemo, useRef, useState } from "react" 3 | import { useWindowSize } from "../../../libs/useWindowSize" 4 | import { Graph } from "../graph/Graph" 5 | import { calcCoords } from "../../utils/calcCoords" 6 | import { Timeline } from "@wbe/interpol" 7 | import { Controls } from "../controls/Controls" 8 | import { Params } from "../params/Params" 9 | import { randomRange } from "../../utils/randomRange" 10 | 11 | export function App() { 12 | const ball = useRef(null) 13 | const { width } = useWindowSize() 14 | 15 | const [counterRefresh, setCounterRefresh] = useState(0) 16 | const [instance, setInstance] = useState(null) 17 | const [params, setParams] = useState<{ time: number; progress: number }>({ 18 | time: 0, 19 | progress: 0, 20 | }) 21 | const [pointsNumber, setPointNumber] = useState(10) 22 | const tl = useRef(null) 23 | 24 | // calc coords 25 | const coords = useMemo(() => calcCoords(pointsNumber), [counterRefresh, pointsNumber]) 26 | 27 | const firstMount = useRef(true) 28 | useEffect(() => { 29 | tl.current = new Timeline({ 30 | onUpdate: (time: number, progress: number) => setParams({ time, progress }), 31 | }) 32 | 33 | const eases = [ 34 | "power1.in", 35 | "power1.out", 36 | "power1.inOut", 37 | "power3.in", 38 | "power3.out", 39 | "power3.inOut", 40 | ] 41 | 42 | for (let props of coords) { 43 | tl.current.add({ 44 | ...props, 45 | delay: 100, 46 | duration: () => randomRange(100, 600), 47 | //ease: eases[randomRange(0, eases.length - 1)] as any, 48 | ease: "power1.out", 49 | onUpdate: ({ x, y }) => { 50 | ball.current.style.transform = `translate3d(${x}px, ${y}px, 0)` 51 | }, 52 | }) 53 | } 54 | 55 | setInstance(tl.current) 56 | if (firstMount.current) { 57 | firstMount.current = false 58 | return 59 | } else { 60 | tl.current.stop() 61 | //itp.play() 62 | } 63 | }, [width, coords, counterRefresh, pointsNumber]) 64 | 65 | return ( 66 |
67 | { 72 | setCounterRefresh((prevState) => prevState + 1) 73 | }} 74 | onPointNumberChange={(v) => setPointNumber(v)} 75 | /> 76 |
77 | 78 |
79 | 80 |
81 |
82 | ) 83 | } 84 | -------------------------------------------------------------------------------- /examples/interpol-graphic/src/components/controls/Controls.module.less: -------------------------------------------------------------------------------- 1 | @import (reference) "../../../libs/ratio.less"; 2 | 3 | .root { 4 | } 5 | 6 | .wrapper { 7 | gap: 10rem; 8 | } 9 | 10 | .buttons { 11 | display: flex; 12 | flex-direction: row; 13 | flex-wrap: wrap; 14 | justify-content: left; 15 | gap: 3rem; 16 | } 17 | 18 | .button, 19 | .easeSelect, 20 | .easeOption { 21 | cursor: pointer; 22 | } 23 | 24 | .button:hover { 25 | border-color: var(--color-blue); 26 | } 27 | .button:active, 28 | .button:focus, 29 | .button:focus-visible { 30 | border-color: var(--color-blue); 31 | } 32 | 33 | .pointNumberInput { 34 | // width: 40rem; 35 | } 36 | 37 | .slider { 38 | width: 300px; 39 | @media (min-width: @breakpoint-tablet) { 40 | // margin: 10rem auto; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/interpol-graphic/src/components/controls/Controls.tsx: -------------------------------------------------------------------------------- 1 | import css from "./Controls.module.less" 2 | import React, { useEffect, useState } from "react" 3 | import { Timeline } from "@wbe/interpol" 4 | 5 | /** 6 | * Controls 7 | */ 8 | export const Controls = ({ 9 | className, 10 | instance, 11 | onRefreshClick, 12 | pointsNumber, 13 | onPointNumberChange, 14 | }: { 15 | className: string 16 | instance: Timeline 17 | onRefreshClick: () => void 18 | pointsNumber: number 19 | onPointNumberChange: (v: number) => void 20 | }) => { 21 | const [progress, setProgress] = useState("0") 22 | 23 | useEffect(() => { 24 | instance?.seek(parseFloat(progress) / 100) 25 | }, [progress]) 26 | 27 | return ( 28 |
29 |
30 | {/* prettier-ignore */} 31 |
32 | 33 |
54 |
55 |
56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /examples/interpol-graphic/src/components/graph/Graph.module.less: -------------------------------------------------------------------------------- 1 | .point { 2 | position: absolute; 3 | left: 0; 4 | bottom: 0; 5 | width: var(--ball-size); 6 | height: var(--ball-size); 7 | border-radius: 50%; 8 | background-color: rgba(255, 255, 255, 0.1); 9 | } 10 | 11 | .svg { 12 | z-index: -1; 13 | position: absolute; 14 | left: calc(var(--ball-size) / 2); 15 | top: calc(var(--ball-size) / 2 * -1); 16 | width: 100%; 17 | height: 100%; 18 | stroke: hotpink; 19 | stroke-width: 2; 20 | fill: none; 21 | } 22 | 23 | -------------------------------------------------------------------------------- /examples/interpol-graphic/src/components/graph/Graph.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useRef } from "react" 2 | import css from "./Graph.module.less" 3 | import { Coords } from "../../utils/calcCoords" 4 | 5 | export const Graph = ({ coords }: { coords: Coords }) => { 6 | const svgRef = useRef(null) 7 | /** 8 | * Create SVG path 9 | */ 10 | const createSVGPath = useCallback( 11 | (coords: Coords): string => { 12 | let path = "" 13 | for (let i = 0; i < coords.length; i++) { 14 | const curr = coords[i] 15 | const [xFrom, xTo] = curr.x 16 | const [yFrom, yTo] = curr.y 17 | path += `M${xFrom},${yFrom + innerHeight} L${xTo},${yTo + innerHeight} ` 18 | } 19 | return path 20 | }, 21 | [coords] 22 | ) 23 | 24 | /** 25 | * Prepare points coords for render 26 | * @param coords 27 | */ 28 | const getPointsCoords = (coords: Coords): { x: number; y: number }[] => 29 | coords.reduce( 30 | (prev, curr, i) => [ 31 | ...prev, 32 | { x: curr.x[0], y: curr.y[0] }, 33 | ...(i === coords.length - 1 ? [{ x: curr.x[1], y: curr.y[1] }] : []), 34 | ], 35 | [] 36 | ) 37 | 38 | return ( 39 |
40 | {getPointsCoords(coords).map((e, i) => ( 41 |
46 | ))} 47 | 48 | 49 | 50 |
51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /examples/interpol-graphic/src/components/params/Params.module.less: -------------------------------------------------------------------------------- 1 | @import (reference) "../../../libs/ratio.less"; 2 | 3 | .root { 4 | width: 100%; 5 | } 6 | 7 | .wrapper { 8 | } 9 | 10 | .list { 11 | list-style: none; 12 | text-align: left; 13 | padding: 0; 14 | margin: 0; 15 | } 16 | 17 | .item { 18 | color: var(--color-gray); 19 | } 20 | -------------------------------------------------------------------------------- /examples/interpol-graphic/src/components/params/Params.tsx: -------------------------------------------------------------------------------- 1 | import css from "./Params.module.less" 2 | 3 | export function Params({ params, className }: {params, className?}) { 4 | return ( 5 |
6 |
7 |
    8 | {Object.keys(params).map((e, i) => ( 9 |
  • 10 | {e}:{" "} 11 | {params[e]} 12 |
  • 13 | ))} 14 |
15 |
16 |
17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /examples/interpol-graphic/src/index.less: -------------------------------------------------------------------------------- 1 | @import (reference) "../libs/ratio.less"; 2 | 3 | :root { 4 | --color-gray: #454545; 5 | --color-black-1: #313131; 6 | --color-black: #001011; 7 | --color-blue: #284a56; 8 | --color-white: #fff; 9 | --ball-size: 30rem; 10 | } 11 | 12 | html { 13 | .propertyViewport(--font-size, 1, 1); 14 | font-size: var(--font-size); 15 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 16 | font-weight: 400; 17 | color-scheme: dark; 18 | background-color: var(--color-black); 19 | font-synthesis: none; 20 | text-rendering: optimizeLegibility; 21 | -webkit-font-smoothing: antialiased; 22 | -moz-osx-font-smoothing: grayscale; 23 | -webkit-text-size-adjust: 100%; 24 | } 25 | 26 | body { 27 | margin: 0; 28 | font-size: 16rem; 29 | min-width: 100vw; 30 | min-height: 100vh; 31 | overflow: hidden; 32 | position: fixed; 33 | } 34 | 35 | button { 36 | background-color: var(--color-blue); 37 | border: none; 38 | } 39 | -------------------------------------------------------------------------------- /examples/interpol-graphic/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom/client" 3 | import "./index.less" 4 | import { App } from "./components/app/App" 5 | 6 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render() 7 | -------------------------------------------------------------------------------- /examples/interpol-graphic/src/utils/calcCoords.ts: -------------------------------------------------------------------------------- 1 | import { clamp } from "./clamp" 2 | import { randomRange } from "./randomRange" 3 | 4 | export type Coords = Array> 5 | 6 | /** 7 | * Calc Coordinates array 8 | * @param pointsNumber 9 | */ 10 | export const calcCoords = (pointsNumber = 12): Coords => { 11 | const ballSize: number = parseInt(getComputedStyle(document.body).getPropertyValue("--ball-size")) 12 | const win = { w: innerWidth, h: innerHeight } 13 | const coords: Coords = [] 14 | const ballRectP = ballSize / pointsNumber 15 | 16 | for (let i = 0; i < pointsNumber; i++) { 17 | const odd = i % 2 === 0 18 | const last = coords?.[coords.length - 1] 19 | if (!last) { 20 | coords.push({ 21 | x: [0, win.w / pointsNumber - ballRectP], 22 | y: [0, clamp(-win.h, -randomRange(0, win.h), 0)], 23 | }) 24 | } else { 25 | const x = last.x[1] 26 | const y = last.y[1] 27 | coords.push({ 28 | x: [x, x + win.w / pointsNumber - ballRectP], 29 | y: [ 30 | y, 31 | odd ? clamp(-win.h, randomRange(0, -win.h), 0) : clamp(-win.h, -randomRange(0, win.h), 0), 32 | ], 33 | }) 34 | } 35 | } 36 | return coords 37 | } 38 | -------------------------------------------------------------------------------- /examples/interpol-graphic/src/utils/clamp.ts: -------------------------------------------------------------------------------- 1 | export function clamp(min: number, value: number, max: number): number { 2 | return Math.max(min, Math.min(value, max)) 3 | } 4 | -------------------------------------------------------------------------------- /examples/interpol-graphic/src/utils/randomRange.ts: -------------------------------------------------------------------------------- 1 | export function randomRange(min: number, max: number, decimal = 0): number { 2 | let rand 3 | 4 | // except the value 0 5 | do rand = Math.random() * (max - min + 1) + min 6 | while (rand === 0) 7 | 8 | const power = Math.pow(10, decimal) 9 | return Math.floor(rand * power) / power 10 | } 11 | 12 | -------------------------------------------------------------------------------- /examples/interpol-graphic/src/utils/styles.ts: -------------------------------------------------------------------------------- 1 | export const styles = (el: HTMLElement | null, s: Record) => { 2 | for (let key in s) if (s.hasOwnProperty(key)) el.style[key] = s[key] 3 | } 4 | -------------------------------------------------------------------------------- /examples/interpol-graphic/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/interpol-graphic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": false, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /examples/interpol-graphic/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /examples/interpol-graphic/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | -------------------------------------------------------------------------------- /examples/interpol-menu/.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 | -------------------------------------------------------------------------------- /examples/interpol-menu/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | interpol 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/interpol-menu/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "interpol-menu", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "main": "src/index.ts", 7 | "scripts": { 8 | "dev": "vite --host" 9 | }, 10 | "dependencies": { 11 | "@wbe/interpol": "^0.20.2", 12 | "react": "^19.0.0", 13 | "react-dom": "^19.0.0" 14 | }, 15 | "devDependencies": { 16 | "@types/react": "^19.0.2", 17 | "@types/react-dom": "^19.0.2", 18 | "@vitejs/plugin-react": "^4.3.4", 19 | "less": "^4.2.1", 20 | "typescript": "^5.7.2", 21 | "vite": "^6.0.6" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/interpol-menu/src/components/app/App.module.less: -------------------------------------------------------------------------------- 1 | .root { 2 | } 3 | 4 | .button { 5 | position: relative; 6 | padding: 30rem; 7 | cursor: pointer; 8 | &:hover { 9 | background: var(--color-black-1); 10 | } 11 | } 12 | 13 | .menu { 14 | } 15 | -------------------------------------------------------------------------------- /examples/interpol-menu/src/components/app/App.tsx: -------------------------------------------------------------------------------- 1 | import css from "./App.module.less" 2 | import React from "react" 3 | import { Menu } from "../menu/Menu" 4 | 5 | export function App() { 6 | const [menuOpen, setMenuOpen] = React.useState(false) 7 | return ( 8 |
9 | 10 | 13 |
14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /examples/interpol-menu/src/components/menu/Menu.module.less: -------------------------------------------------------------------------------- 1 | .root { 2 | position: fixed; 3 | top: 0; 4 | width: 100%; 5 | height: 100vh; 6 | background: #284a56; 7 | } 8 | 9 | .items { 10 | padding: 0; 11 | display: flex; 12 | flex-direction: column; 13 | align-items: center; 14 | justify-content: center; 15 | height: 100%; 16 | list-style: none; 17 | } 18 | 19 | .item { 20 | font-size: 50rem; 21 | @media (min-width: 768px) { 22 | font-size: 70rem; 23 | } 24 | cursor: pointer; 25 | &:hover { 26 | color: var(--color-black-1); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/interpol-menu/src/components/menu/Menu.tsx: -------------------------------------------------------------------------------- 1 | import css from "./Menu.module.less" 2 | import React, { useEffect, useRef } from "react" 3 | import { Timeline, styles } from "@wbe/interpol" 4 | 5 | export function Menu({ isOpen }: { isOpen: boolean }) { 6 | const rootRef = useRef(null) 7 | const itemsRef = useRef([]) 8 | 9 | const getTl = () => { 10 | const tl = new Timeline({ paused: true }) 11 | 12 | const wallDuration = 700 13 | // Background wall 14 | tl.add({ 15 | debug: true, 16 | ease: "expo.out", 17 | duration: wallDuration, 18 | x: [-100, 0], 19 | opacity: [0, 1], 20 | 21 | // Use the styles function to update the DOM element 22 | beforeStart: ({ x, opacity }) => { 23 | styles(rootRef.current, { 24 | x: `${x}%`, 25 | opacity, 26 | }) 27 | }, 28 | 29 | onUpdate: ({ x, opacity }) => { 30 | styles(rootRef.current, { 31 | x: `${x}%`, 32 | opacity, 33 | }) 34 | }, 35 | }) 36 | 37 | // Create a stagger effect on items 38 | const itemDuration = wallDuration 39 | const itemDelay = 100 40 | // Loop on each item 41 | // and add an Interpol instance for each one 42 | for (let item of itemsRef.current) { 43 | tl.add( 44 | { 45 | duration: itemDuration, 46 | ease: "expo.out", 47 | y: [100, 0], 48 | opacity: [0, 1], 49 | 50 | // Equivalent to copy the onUpdate function on beforeStart 51 | // "immediateRender" allows to execute "onUpdate" callback just before "beforeStart" 52 | // Useful in this case, onUpdate will be called once, if the timeline is paused 53 | // in order to give a position to DOM element 54 | immediateRender: true, 55 | onUpdate: ({ y, opacity }) => { 56 | styles(item, { 57 | y: `${y}%`, 58 | opacity, 59 | }) 60 | }, 61 | }, 62 | // Use the offset to create a delay between each item 63 | // delay is not available on Interpol instance when using Timeline 64 | // It could be complicated to implement it since we use the Interpol.seek 65 | // method to move the timeline 66 | `-=${itemDuration - itemDelay}`, 67 | ) 68 | } 69 | 70 | tl.add( 71 | { 72 | duration: wallDuration, 73 | ease: "expo.out", 74 | scale: [1, 0.8], 75 | onUpdate: ({ scale }) => { 76 | styles(rootRef.current, { scale }) 77 | }, 78 | }, 79 | "-=800", 80 | ) 81 | 82 | return tl 83 | } 84 | 85 | /** 86 | * Init and register the timeline in a ref 87 | */ 88 | const tl = useRef(null) 89 | useEffect(() => { 90 | tl.current = getTl() 91 | }, []) 92 | 93 | /** 94 | * Play / Reverse 95 | * (not on first mount) 96 | */ 97 | const isFirstMount = useRef(true) 98 | useEffect(() => { 99 | // flag the first mount 100 | if (isFirstMount.current) { 101 | isFirstMount.current = false 102 | return 103 | } 104 | 105 | // Here we go, play or reverse the timeline 106 | // when the isOpen prop change 107 | if (isOpen) tl.current.play() 108 | else tl.current.reverse() 109 | }, [isOpen]) 110 | 111 | return ( 112 |
113 |
    114 | {["Home", "About", "Contact"].map((item, index) => ( 115 |
  • (itemsRef.current[index] = r)} 120 | /> 121 | ))} 122 |
123 |
124 | ) 125 | } 126 | -------------------------------------------------------------------------------- /examples/interpol-menu/src/index.less: -------------------------------------------------------------------------------- 1 | @import (reference) "utils/ratio.less"; 2 | 3 | :root { 4 | --color-gray: #454545; 5 | --color-black-1: #313131; 6 | --color-black: #001011; 7 | --color-blue: #284a56; 8 | --color-white: #fff; 9 | --ball-size: 30rem; 10 | } 11 | 12 | html { 13 | .propertyViewport(--font-size, 1, 1); 14 | font-size: var(--font-size); 15 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 16 | font-weight: 400; 17 | color-scheme: dark; 18 | background-color: var(--color-black); 19 | font-synthesis: none; 20 | text-rendering: optimizeLegibility; 21 | -webkit-font-smoothing: antialiased; 22 | -moz-osx-font-smoothing: grayscale; 23 | -webkit-text-size-adjust: 100%; 24 | } 25 | 26 | body { 27 | margin: 0; 28 | font-size: 16rem; 29 | min-width: 100vw; 30 | min-height: 100vh; 31 | overflow: hidden; 32 | position: fixed; 33 | } 34 | 35 | button { 36 | background-color: var(--color-blue); 37 | border: none; 38 | } 39 | -------------------------------------------------------------------------------- /examples/interpol-menu/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom/client" 3 | import "./index.less" 4 | import { App } from "./components/app/App" 5 | 6 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render() 7 | -------------------------------------------------------------------------------- /examples/interpol-menu/src/utils/useWindowSize.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react" 2 | 3 | export const useWindowSize = (): { width: number; height: number } => { 4 | const [s, setS] = useState({ width: window.innerWidth, height: window.innerHeight }) 5 | useEffect(() => { 6 | const handler = () => { 7 | setS({ width: window.innerWidth, height: window.innerHeight }) 8 | } 9 | window.addEventListener("resie", handler) 10 | return () => { 11 | window.removeEventListener("resie", handler) 12 | } 13 | }, [s]) 14 | 15 | return s 16 | } 17 | -------------------------------------------------------------------------------- /examples/interpol-menu/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/interpol-menu/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": false, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /examples/interpol-menu/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /examples/interpol-menu/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | -------------------------------------------------------------------------------- /examples/interpol-object-el/.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 | -------------------------------------------------------------------------------- /examples/interpol-object-el/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | interpol object el 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/interpol-object-el/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "interpol-object-el", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "main": "src/index.ts", 7 | "scripts": { 8 | "dev": "vite --host" 9 | }, 10 | "dependencies": { 11 | "@wbe/interpol": "^0.20.2", 12 | "ogl": "^1.0.9", 13 | "prettier": "^3.4.2" 14 | }, 15 | "devDependencies": { 16 | "typescript": "^5.7.2", 17 | "vite": "^6.0.6" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/interpol-object-el/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | text-rendering: optimizeLegibility; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | -webkit-text-size-adjust: 100%; 9 | } 10 | 11 | body { 12 | margin: 0; 13 | display: flex; 14 | place-items: center; 15 | min-width: 320px; 16 | min-height: 100vh; 17 | } 18 | 19 | -------------------------------------------------------------------------------- /examples/interpol-object-el/src/index.ts: -------------------------------------------------------------------------------- 1 | import "./index.css" 2 | import { Renderer, Program, Mesh, Transform, Geometry } from "ogl" 3 | import fragment from "./shaders/fragment.glsl?raw" 4 | import vertex from "./shaders/vertex.glsl?raw" 5 | import { Timeline } from "@wbe/interpol" 6 | 7 | async function main() { 8 | const renderer = new Renderer({ 9 | width: window.innerWidth, 10 | height: window.innerHeight, 11 | }) 12 | 13 | const gl = renderer.gl 14 | document.body.appendChild(gl.canvas) 15 | 16 | function resize() { 17 | renderer.setSize(window.innerWidth, window.innerHeight) 18 | } 19 | window.addEventListener("resize", resize, false) 20 | resize() 21 | 22 | const scene = new Transform() 23 | 24 | const program = new Program(gl, { 25 | vertex, 26 | fragment, 27 | transparent: true, 28 | uniforms: { 29 | uMove: { value: 0 }, 30 | }, 31 | }) 32 | 33 | const geometry = new Geometry(gl, { 34 | position: { size: 2, data: new Float32Array([-1, -1, 3, -1, -1, 3]) }, 35 | uv: { size: 2, data: new Float32Array([0, 0, 2, 0, 0, 2]) }, 36 | }) 37 | 38 | const mesh = new Mesh(gl, { geometry, program }) 39 | mesh.setParent(scene) 40 | 41 | requestAnimationFrame(update) 42 | function update() { 43 | requestAnimationFrame(update) 44 | renderer.render({ scene }) 45 | } 46 | 47 | /** 48 | * Animate 49 | */ 50 | const tl = new Timeline({ paused: true }) 51 | 52 | tl.add({ 53 | value: [0, 1], 54 | ease: "expo.out", 55 | onUpdate: ({ value }) => { 56 | program.uniforms.uMove.value = value 57 | }, 58 | }) 59 | 60 | const loop = async () => { 61 | await tl.play() 62 | await tl.reverse() 63 | loop() 64 | } 65 | loop() 66 | } 67 | 68 | main() 69 | -------------------------------------------------------------------------------- /examples/interpol-object-el/src/shaders/fragment.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | varying vec2 vUv; 3 | uniform float uMove; 4 | 5 | void main() { 6 | float strength = floor(vUv.y * 10. * uMove) / 10.; 7 | gl_FragColor = vec4(vec3(strength), 1.); 8 | } 9 | -------------------------------------------------------------------------------- /examples/interpol-object-el/src/shaders/vertex.glsl: -------------------------------------------------------------------------------- 1 | attribute vec2 uv; 2 | attribute vec2 position; 3 | 4 | varying vec2 vUv; 5 | 6 | void main() { 7 | vUv = uv; 8 | gl_Position = vec4(position, 0, 1); 9 | } 10 | -------------------------------------------------------------------------------- /examples/interpol-object-el/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/interpol-object-el/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true 21 | }, 22 | "include": ["src"] 23 | } 24 | -------------------------------------------------------------------------------- /examples/interpol-offsets/.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 | -------------------------------------------------------------------------------- /examples/interpol-offsets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | interpol 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/interpol-offsets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "interpol-offsets", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "main": "src/index.ts", 7 | "scripts": { 8 | "dev": "vite --host" 9 | }, 10 | "dependencies": { 11 | "@wbe/interpol": "^0.20.2", 12 | "react": "^19.0.0", 13 | "react-dom": "^19.0.0" 14 | }, 15 | "devDependencies": { 16 | "@types/react": "^19.0.2", 17 | "@types/react-dom": "^19.0.2", 18 | "@vitejs/plugin-react": "^4.3.4", 19 | "less": "^4.2.1", 20 | "typescript": "^5.7.2", 21 | "vite": "^6.0.6" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/interpol-offsets/src/components/app/App.module.less: -------------------------------------------------------------------------------- 1 | .root { 2 | width: 100vw; 3 | height: 100vh; 4 | position: relative; 5 | } 6 | 7 | 8 | .typeOffsetContainer { 9 | display: flex; 10 | gap: 20rem; 11 | } 12 | 13 | .container { 14 | width: 70vw; 15 | position: absolute; 16 | top: 50%; 17 | left: 50%; 18 | transform: translate(-50%, -50%); 19 | } 20 | 21 | .ball { 22 | display: block; 23 | background: hotpink; 24 | width: 30rem; 25 | height: 30rem; 26 | @media(min-width: 768px) 27 | { 28 | width: 60rem; 29 | height: 60rem; 30 | } 31 | border-radius: 50%; 32 | } 33 | -------------------------------------------------------------------------------- /examples/interpol-offsets/src/components/app/App.tsx: -------------------------------------------------------------------------------- 1 | import css from "./App.module.less" 2 | import { useEffect, useRef, useState } from "react" 3 | import { styles, Timeline } from "@wbe/interpol" 4 | import { Controls } from "../controls/Controls" 5 | import { useWindowSize } from "../../utils/useWindowSize" 6 | 7 | export function App() { 8 | const refs = useRef([]) 9 | const containerRef = useRef(null) 10 | const [instance, setInstance] = useState(null) 11 | const windowSize = useWindowSize() 12 | 13 | const [customOffset, setCustomOffset] = useState("0") 14 | const [type, setType] = useState("relative") 15 | 16 | useEffect(() => { 17 | const tl = new Timeline({ debug: true }) 18 | 19 | for (let i = 0; i < refs.current.length; i++) { 20 | const curr = refs.current[i] 21 | tl.add( 22 | { 23 | duration: 1000, 24 | immediateRender: true, 25 | ease: "power1.inOut", 26 | x: [0, containerRef.current.offsetWidth - curr.offsetWidth], 27 | onUpdate: ({ x }) => { 28 | styles(curr, { x: x + "px" }) 29 | }, 30 | }, 31 | 32 | // that's the trick 33 | // in order to test the offset interpolation 34 | // ball 1 is relative to its position in the timeline 35 | // other balls are absolute (relative to the beginning of the timeline) 36 | i === 1 ? customOffset : i * 40, 37 | ) 38 | } 39 | 40 | setInstance(tl) 41 | }, [windowSize, customOffset]) 42 | 43 | const handleValue = (v): void => { 44 | setCustomOffset(type === "relative" ? `${v}` : parseInt(v)) 45 | } 46 | 47 | useEffect(() => { 48 | handleValue(customOffset) 49 | }, [type]) 50 | 51 | return ( 52 |
53 | 54 |
55 | 56 |
57 |
58 |
type
59 | {" "} 63 |
64 |
65 |
66 |
{type} offset
67 | handleValue(e.target?.value)} 72 | /> 73 |
74 |
75 |
76 | 77 |
78 | {new Array(10).fill(null).map((e, i) => ( 79 |
(refs.current[i] = r)} 84 | >
85 | ))} 86 |
87 |
88 | ) 89 | } 90 | -------------------------------------------------------------------------------- /examples/interpol-offsets/src/components/controls/Controls.module.less: -------------------------------------------------------------------------------- 1 | @import (reference) "../../utils/ratio.less"; 2 | 3 | .root { 4 | } 5 | 6 | .wrapper { 7 | gap: 10rem; 8 | } 9 | 10 | .buttons { 11 | display: flex; 12 | flex-direction: row; 13 | flex-wrap: wrap; 14 | justify-content: left; 15 | gap: 3rem; 16 | } 17 | 18 | .button, 19 | .easeSelect, 20 | .easeOption { 21 | cursor: pointer; 22 | } 23 | 24 | .button:hover { 25 | border-color: var(--color-blue); 26 | } 27 | .button:active, 28 | .button:focus, 29 | .button:focus-visible { 30 | border-color: var(--color-blue); 31 | } 32 | 33 | .pointNumberInput { 34 | // width: 40rem; 35 | } 36 | 37 | .slider { 38 | width: 300px; 39 | @media (min-width: @breakpoint-tablet) { 40 | // margin: 10rem auto; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/interpol-offsets/src/components/controls/Controls.tsx: -------------------------------------------------------------------------------- 1 | import css from "./Controls.module.less" 2 | import React, { useEffect, useState } from "react" 3 | import { Timeline } from "@wbe/interpol" 4 | 5 | /** 6 | * Controls 7 | */ 8 | export const Controls = ({ 9 | className, 10 | instance, 11 | }: { 12 | className: string 13 | instance: Timeline 14 | }) => { 15 | const [progress, setProgress] = useState("0") 16 | 17 | useEffect(() => { 18 | instance?.seek(parseFloat(progress) / 100) 19 | }, [progress]) 20 | 21 | return ( 22 |
23 |
24 | {/* prettier-ignore */} 25 |
26 |
40 |
41 |
42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /examples/interpol-offsets/src/index.less: -------------------------------------------------------------------------------- 1 | @import (reference) "utils/ratio.less"; 2 | 3 | :root { 4 | --color-gray: #454545; 5 | --color-black-1: #313131; 6 | --color-black: #001011; 7 | --color-blue: #284a56; 8 | --color-white: #fff; 9 | --ball-size: 30rem; 10 | } 11 | 12 | html { 13 | .propertyViewport(--font-size, 1, 1); 14 | font-size: var(--font-size); 15 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 16 | font-weight: 400; 17 | color-scheme: dark; 18 | background-color: var(--color-black); 19 | font-synthesis: none; 20 | text-rendering: optimizeLegibility; 21 | -webkit-font-smoothing: antialiased; 22 | -moz-osx-font-smoothing: grayscale; 23 | -webkit-text-size-adjust: 100%; 24 | } 25 | 26 | body { 27 | margin: 0; 28 | font-size: 16rem; 29 | min-width: 100vw; 30 | min-height: 100vh; 31 | overflow: hidden; 32 | position: fixed; 33 | } 34 | 35 | button { 36 | background-color: var(--color-blue); 37 | border: none; 38 | } 39 | -------------------------------------------------------------------------------- /examples/interpol-offsets/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom/client" 3 | import "./index.less" 4 | import { App } from "./components/app/App" 5 | 6 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render() 7 | -------------------------------------------------------------------------------- /examples/interpol-offsets/src/utils/useWindowSize.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react" 2 | 3 | export const useWindowSize = (): { width: number; height: number } => { 4 | const [s, setS] = useState({ width: window.innerWidth, height: window.innerHeight }) 5 | useEffect(() => { 6 | const handler = () => { 7 | setS({ width: window.innerWidth, height: window.innerHeight }) 8 | } 9 | window.addEventListener("resize", handler) 10 | return () => { 11 | window.removeEventListener("resize", handler) 12 | } 13 | }, [s]) 14 | return s 15 | } 16 | -------------------------------------------------------------------------------- /examples/interpol-offsets/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/interpol-offsets/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": false, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /examples/interpol-offsets/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /examples/interpol-offsets/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | -------------------------------------------------------------------------------- /examples/interpol-particles/.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 | -------------------------------------------------------------------------------- /examples/interpol-particles/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | interpol 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/interpol-particles/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "interpol-particles", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "main": "src/index.ts", 7 | "scripts": { 8 | "dev": "vite --host" 9 | }, 10 | "dependencies": { 11 | "@wbe/interpol": "^0.20.2", 12 | "react": "^19.0.0", 13 | "react-dom": "^19.0.0" 14 | }, 15 | "devDependencies": { 16 | "@types/react": "^19.0.2", 17 | "@types/react-dom": "^19.0.2", 18 | "@vitejs/plugin-react": "^4.3.4", 19 | "less": "^4.2.1", 20 | "typescript": "^5.7.2", 21 | "vite": "^6.0.6" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/interpol-particles/src/main.less: -------------------------------------------------------------------------------- 1 | @import (reference) "utils/ratio.less"; 2 | 3 | :root { 4 | --color-gray: #454545; 5 | --color-black-1: #313131; 6 | --color-black: #001011; 7 | --color-blue: #284a56; 8 | --color-white: #fff; 9 | --ball-size: 30rem; 10 | } 11 | 12 | html { 13 | .propertyViewport(--font-size, 1, 1); 14 | font-size: var(--font-size); 15 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 16 | font-weight: 400; 17 | color-scheme: dark; 18 | background-color: var(--color-black); 19 | font-synthesis: none; 20 | text-rendering: optimizeLegibility; 21 | -webkit-font-smoothing: antialiased; 22 | -moz-osx-font-smoothing: grayscale; 23 | -webkit-text-size-adjust: 100%; 24 | } 25 | 26 | body { 27 | margin: 0; 28 | font-size: 16rem; 29 | min-width: 100vw; 30 | min-height: 100vh; 31 | overflow: hidden; 32 | position: fixed; 33 | } 34 | 35 | button { 36 | background-color: var(--color-blue); 37 | border: none; 38 | } 39 | 40 | .particle { 41 | position: absolute; 42 | width: 15rem; 43 | height: 15rem; 44 | border-radius: 50%; 45 | @media (min-width: 768px) { 46 | width: 25rem; 47 | height: 25rem; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/interpol-particles/src/main.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom/client" 2 | import "./main.less" 3 | import React, { useEffect, useRef, useState } from "react" 4 | import { Interpol, styles } from "@wbe/interpol" 5 | import { useWindowSize } from "./utils/useWindowSize" 6 | 7 | /** 8 | * Prepare 9 | */ 10 | function random(min: number, max: number, decimal = 0): number { 11 | let rand 12 | do rand = Math.random() * (max - min + 1) + min 13 | while (rand === 0) 14 | const power = Math.pow(10, decimal) 15 | return Math.floor(rand * power) / power 16 | } 17 | const randomRGB = () => `rgb(${random(0, 255)}, ${random(0, 255)}, ${random(0, 255)})` 18 | 19 | const getEases = () => 20 | ["power1", "power2", "power3", "expo"].reduce( 21 | (a, b) => [...a, ...["in", "out", "inOut"].map((d) => `${b}.${d}`)], 22 | [], 23 | ) 24 | 25 | const eases = getEases() 26 | const randomEase = eases[random(0, eases.length - 1)] 27 | 28 | /** 29 | * App 30 | */ 31 | export function App() { 32 | const els = useRef([]) 33 | const [pointsNumber, setPointsNumber] = useState(150) 34 | const windowSize = useWindowSize() 35 | 36 | /** 37 | * Animate each particle 38 | */ 39 | useEffect(() => { 40 | let itps = [] 41 | for (let el of els.current) { 42 | const itp = new Interpol({ 43 | paused: true, 44 | duration: () => random(1000, 3000), 45 | ease: randomEase, 46 | x: [random(0, innerWidth), () => random(0, innerWidth)], 47 | y: [random(0, innerHeight), () => random(0, innerHeight)], 48 | onUpdate: ({ x, y }) => { 49 | styles(el, { 50 | x: x + "px", 51 | y: y + "px", 52 | }) 53 | }, 54 | }) 55 | itps.push(itp) 56 | const yoyo = () => { 57 | itp.refreshComputedValues() 58 | itp.play().then(() => itp.reverse().then(yoyo)) 59 | } 60 | yoyo() 61 | } 62 | return () => { 63 | itps.forEach((e) => e.stop()) 64 | } 65 | }, [pointsNumber, windowSize]) 66 | 67 | return ( 68 |
69 | setPointsNumber(parseInt(e.target.value))} 74 | /> 75 | {pointsNumber > 0 && 76 | new Array(pointsNumber) 77 | .fill(0) 78 | .map((_, i) => ( 79 |
(els.current[i] = r)} 84 | /> 85 | ))} 86 |
87 | ) 88 | } 89 | 90 | /** 91 | * Render 92 | */ 93 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render() 94 | -------------------------------------------------------------------------------- /examples/interpol-particles/src/utils/useWindowSize.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react" 2 | 3 | export const useWindowSize = (): { width: number; height: number } => { 4 | const [s, setS] = useState({ width: window.innerWidth, height: window.innerHeight }) 5 | useEffect(() => { 6 | const handler = () => setS({ width: window.innerWidth, height: window.innerHeight }) 7 | window.addEventListener("resize", handler) 8 | return () => { 9 | window.removeEventListener("resize", handler) 10 | } 11 | }, []) 12 | return s 13 | } 14 | -------------------------------------------------------------------------------- /examples/interpol-particles/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/interpol-particles/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": false, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /examples/interpol-particles/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /examples/interpol-particles/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | -------------------------------------------------------------------------------- /examples/interpol-seek-reset-wall/.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 | -------------------------------------------------------------------------------- /examples/interpol-seek-reset-wall/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | interpol 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/interpol-seek-reset-wall/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "interpol-seek-reset-wall", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite --host" 8 | }, 9 | "devDependencies": { 10 | "typescript": "^5.7.2", 11 | "vite": "^6.0.6" 12 | }, 13 | "dependencies": { 14 | "@wbe/interpol": "workspace:*" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/interpol-seek-reset-wall/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/interpol-seek-reset-wall/src/main.ts: -------------------------------------------------------------------------------- 1 | import { Timeline, Interpol, styles } from "@wbe/interpol" 2 | import "./style.css" 3 | 4 | const wall = document.querySelector(".wall") 5 | const button = document.querySelector(".button") 6 | 7 | /** 8 | * Test wall seek played on click & seeked on resize 9 | * Goal: the wall should seek properly and computedValues refreshed when the window is resized 10 | */ 11 | 12 | /** 13 | * Test with Interpol 14 | */ 15 | const testWithInterpol = () => { 16 | const itp = new Interpol({ 17 | immediateRender: true, 18 | paused: true, 19 | debug: true, 20 | el: wall, 21 | duration: 1000, 22 | ease: "linear", 23 | x: [() => -innerWidth * 0.9, 0, "px"], 24 | onUpdate: ({ x }) => { 25 | styles(wall, { x: x + "px" }) 26 | }, 27 | }) 28 | 29 | let isVisible = false 30 | const openClose = () => { 31 | isVisible = !isVisible 32 | if (isVisible) itp.play() 33 | else itp.reverse() 34 | } 35 | openClose() 36 | button?.addEventListener("click", () => { 37 | openClose() 38 | }) 39 | 40 | window.addEventListener("resize", () => { 41 | itp.refreshComputedValues() 42 | itp.seek(0) 43 | isVisible = false 44 | }) 45 | } 46 | 47 | testWithInterpol() 48 | 49 | /** 50 | * Test with Timeline 51 | */ 52 | const testWithTimeline = () => { 53 | const tl = new Timeline({ paused: true }) 54 | tl.add({ 55 | immediateRender: true, 56 | paused: true, 57 | debug: true, 58 | duration: 1000, 59 | ease: "linear", 60 | x: [() => -innerWidth * 0.9, 0], 61 | onUpdate: ({ x }) => { 62 | styles(wall, { x: x + "px" }) 63 | }, 64 | }) 65 | tl.add({ 66 | debug: true, 67 | duration: 1000, 68 | ease: "linear", 69 | x: [0, () => -innerWidth * 0.5], 70 | onUpdate: ({ x }) => { 71 | styles(wall, { x: x + "px" }) 72 | }, 73 | }) 74 | 75 | let isVisible = false 76 | const openClose = () => { 77 | isVisible = !isVisible 78 | if (isVisible) tl.play() 79 | else tl.reverse() 80 | } 81 | openClose() 82 | button?.addEventListener("click", () => { 83 | openClose() 84 | }) 85 | window.addEventListener("resize", () => { 86 | tl.refreshComputedValues() 87 | tl.seek(0) 88 | isVisible = false 89 | }) 90 | } 91 | 92 | // testWithTimeline() 93 | -------------------------------------------------------------------------------- /examples/interpol-seek-reset-wall/src/style.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | html { 4 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 5 | font-weight: 400; 6 | color-scheme: dark; 7 | font-synthesis: none; 8 | text-rendering: optimizeLegibility; 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | -webkit-text-size-adjust: 100%; 12 | } 13 | 14 | body { 15 | margin: 0; 16 | font-size: 16rem; 17 | width: 100vw; 18 | height: 100vh; 19 | overflow: hidden; 20 | position: fixed; 21 | } 22 | 23 | .wall { 24 | position: absolute; 25 | width: 100vw; 26 | height: 100vh; 27 | background-color: red; 28 | } 29 | 30 | .button { 31 | position: fixed; 32 | background-color: white; 33 | padding: 1rem; 34 | margin: 1rem; 35 | font-size: 2rem; 36 | color: black; 37 | } 38 | -------------------------------------------------------------------------------- /examples/interpol-seek-reset-wall/src/typescript.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/interpol-seek-reset-wall/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/interpol-seek-reset-wall/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true 21 | }, 22 | "include": ["src"] 23 | } 24 | -------------------------------------------------------------------------------- /examples/interpol-timeline-refresh/.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 | -------------------------------------------------------------------------------- /examples/interpol-timeline-refresh/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | interpol 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/interpol-timeline-refresh/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "interpol-timeline-refresh", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "main": "src/index.ts", 7 | "scripts": { 8 | "dev": "vite --host" 9 | }, 10 | "dependencies": { 11 | "@wbe/interpol": "workspace:*" 12 | }, 13 | "devDependencies": { 14 | "less": "^4.2.1", 15 | "typescript": "^5.7.2", 16 | "vite": "^6.0.6" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/interpol-timeline-refresh/src/index.less: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-gray: #454545; 3 | --color-black-1: #313131; 4 | --color-black: #171717; 5 | --color-blue: #646cff; 6 | --color-white: #fff; 7 | } 8 | 9 | html { 10 | font-size: var(--font-size); 11 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 12 | font-weight: 400; 13 | color-scheme: dark; 14 | font-synthesis: none; 15 | text-rendering: optimizeLegibility; 16 | -webkit-font-smoothing: antialiased; 17 | -moz-osx-font-smoothing: grayscale; 18 | -webkit-text-size-adjust: 100%; 19 | } 20 | 21 | body { 22 | margin: 0; 23 | font-size: 16rem; 24 | width: 100vw; 25 | height: 100vh; 26 | overflow: hidden; 27 | position: fixed; 28 | } 29 | 30 | .ball { 31 | position: absolute; 32 | width: 5rem; 33 | height: 5rem; 34 | border-radius: 50%; 35 | background-color: red; 36 | } 37 | -------------------------------------------------------------------------------- /examples/interpol-timeline-refresh/src/main.ts: -------------------------------------------------------------------------------- 1 | import { styles, Timeline } from "@wbe/interpol" 2 | import "./index.less" 3 | 4 | const ball = document.querySelector(".ball") 5 | const tl: Timeline = new Timeline({ debug: false, paused: true }) 6 | 7 | /** 8 | * The goal of this example is to use an external value, muted on onUpdate callbacks 9 | * of each interpol instance. 10 | * 11 | * This value is used as "from" computed value of the next interpol instance. 12 | * 13 | */ 14 | let EXTERNAL_X = 0 15 | 16 | tl.add({ 17 | ease: "power3.in", 18 | x: [0, 70], 19 | onUpdate: ({ x }) => { 20 | EXTERNAL_X = x 21 | styles(ball, { x: `${x}vw` }) 22 | console.log("1 - x", x) 23 | }, 24 | }) 25 | tl.add({ 26 | x: [() => EXTERNAL_X, 20], 27 | onUpdate: ({ x }) => { 28 | styles(ball, { x: `${x}vw` }) 29 | EXTERNAL_X = x 30 | console.log("2 - x", x) 31 | }, 32 | }) 33 | tl.add({ 34 | x: [() => EXTERNAL_X, 50], 35 | onUpdate: ({ x }) => { 36 | styles(ball, { x: `${x}vw` }) 37 | console.log("3 - x", x) 38 | }, 39 | }) 40 | 41 | tl.play() 42 | -------------------------------------------------------------------------------- /examples/interpol-timeline-refresh/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/interpol-timeline-refresh/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": false, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /examples/interpol-timeline-refresh/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /examples/interpol-timeline/.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 | -------------------------------------------------------------------------------- /examples/interpol-timeline/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | interpol 8 | 9 | 10 |
11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /examples/interpol-timeline/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "interpol-timeline", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "main": "src/index.ts", 7 | "scripts": { 8 | "dev": "vite --host" 9 | }, 10 | "dependencies": { 11 | "@wbe/interpol": "workspace:*", 12 | "react": "^19.0.0", 13 | "react-dom": "^19.0.0" 14 | }, 15 | "devDependencies": { 16 | "@types/react": "^19.0.2", 17 | "@types/react-dom": "^19.0.2", 18 | "@vitejs/plugin-react": "^4.3.4", 19 | "less": "^4.2.1", 20 | "typescript": "^5.7.2", 21 | "vite": "^6.0.6" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/interpol-timeline/src/index.less: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-gray: #454545; 3 | --color-black-1: #313131; 4 | --color-black: #171717; 5 | --color-blue: #646cff; 6 | --color-white: #fff; 7 | } 8 | 9 | html { 10 | font-size: var(--font-size); 11 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 12 | font-weight: 400; 13 | color-scheme: dark; 14 | font-synthesis: none; 15 | text-rendering: optimizeLegibility; 16 | -webkit-font-smoothing: antialiased; 17 | -moz-osx-font-smoothing: grayscale; 18 | -webkit-text-size-adjust: 100%; 19 | } 20 | 21 | body { 22 | margin: 0; 23 | font-size: 1rem; 24 | min-width: 100vw; 25 | min-height: 100vh; 26 | overflow: hidden; 27 | position: fixed; 28 | } 29 | 30 | .ball { 31 | position: absolute; 32 | width: 5rem; 33 | height: 5rem; 34 | border-radius: 50%; 35 | background-color: red; 36 | } 37 | -------------------------------------------------------------------------------- /examples/interpol-timeline/src/main.ts: -------------------------------------------------------------------------------- 1 | import { Power1, Timeline, Interpol, styles } from "@wbe/interpol" 2 | import "./index.less" 3 | 4 | /** 5 | * Query 6 | */ 7 | const ball = document.querySelector(".ball") 8 | const ball2 = document.querySelector(".ball-2") 9 | const seek0 = document.querySelector(".seek-0") 10 | const seek05 = document.querySelector(".seek-05") 11 | const seek1 = document.querySelector(".seek-1") 12 | const inputProgress = document.querySelector(".progress") 13 | const inputSlider = document.querySelector(".slider") 14 | 15 | /** 16 | * Events 17 | */ 18 | ;["play", "reverse", "pause", "stop", "refresh", "resume"].forEach( 19 | (name) => (document.querySelector(`.${name}`).onclick = () => tl[name]()), 20 | ) 21 | seek0.onclick = () => tl.seek(0, false, false) 22 | seek05.onclick = () => tl.seek(0.5, false, false) 23 | seek1.onclick = () => tl.seek(1, false, false) 24 | inputProgress.onchange = () => tl.seek(parseFloat(inputProgress.value) / 100, false, false) 25 | inputSlider.oninput = () => tl.seek(parseFloat(inputSlider.value) / 100, false, false) 26 | window.addEventListener("resize", () => tl.seek(1)) 27 | 28 | /** 29 | * Timeline 30 | */ 31 | const tl: Timeline = new Timeline({ 32 | debug: true, 33 | onComplete: (time, progress) => console.log(`tl onComplete!`), 34 | }) 35 | 36 | const itp = new Interpol({ 37 | x: [0, 200], 38 | y: [0, 200], 39 | ease: Power1.in, 40 | onUpdate: ({ x, y }) => { 41 | styles(ball, { x: x + "px", y: y + "px" }) 42 | }, 43 | onComplete: (e) => { 44 | console.log("itp 1 onComplete", e) 45 | }, 46 | }) 47 | tl.add(itp) 48 | 49 | tl.add({ 50 | x: [200, 100], 51 | y: [200, 300], 52 | ease: Power1.out, 53 | onUpdate: ({ x, y }) => { 54 | styles(ball, { x: x + "px", y: y + "px" }) 55 | }, 56 | onComplete: (e) => { 57 | console.log("itp 2 onComplete", e) 58 | }, 59 | }) 60 | 61 | tl.add({ 62 | x: [0, 100], 63 | y: [0, 400], 64 | ease: Power1.out, 65 | onUpdate: ({ x, y }) => { 66 | styles(ball2, { x: x + "px", y: y + "px" }) 67 | }, 68 | onComplete: (e) => { 69 | console.log("itp 3 onComplete", e) 70 | }, 71 | }) 72 | -------------------------------------------------------------------------------- /examples/interpol-timeline/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/interpol-timeline/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": false, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /examples/interpol-timeline/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /examples/interpol-timeline/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "interpol", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/willybrauner/interpol.git" 9 | }, 10 | "keywords": [ 11 | "interpol", 12 | "interpolation", 13 | "animation", 14 | "timeline", 15 | "dom" 16 | ], 17 | "scripts": { 18 | "clean": "rm -rf dist", 19 | "build": "FORCE_COLOR=1 turbo run build", 20 | "build:watch": "FORCE_COLOR=1 turbo run build -- --watch", 21 | "dev": "FORCE_COLOR=1 turbo run dev --concurrency 20", 22 | "test:watch": "vitest --reporter verbose", 23 | "test": "vitest run", 24 | "size": "size-limit", 25 | "ncu": "find . -name 'node_modules' -prune -o -name 'package.json' -execdir ncu -u ';'", 26 | "pre-publish": "npm run build && npm run test", 27 | "ci:version": "pnpm changeset version && pnpm --filter \"@wbe/*\" install --lockfile-only", 28 | "ci:publish": "pnpm build && pnpm changeset publish" 29 | }, 30 | "devDependencies": { 31 | "@changesets/cli": "^2.27.11", 32 | "@size-limit/preset-small-lib": "^11.1.6", 33 | "@types/node": "^22.10.2", 34 | "jsdom": "^25.0.1", 35 | "prettier": "^3.4.2", 36 | "size-limit": "^11.1.6", 37 | "turbo": "^2.3.3", 38 | "typescript": "^5.7.2", 39 | "vite": "^6.0.6", 40 | "vitest": "^2.1.8" 41 | }, 42 | "prettier": { 43 | "semi": false, 44 | "printWidth": 100 45 | }, 46 | "packageManager": "pnpm@8.15.4", 47 | "size-limit": [ 48 | { 49 | "name": "@wbe/interpol", 50 | "path": "packages/interpol/dist/interpol.js", 51 | "limit": "3.5 KB" 52 | } 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /packages/interpol/interpol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willybrauner/interpol/c70ed119f85de98422268ae2e9d83a8712534b40/packages/interpol/interpol.png -------------------------------------------------------------------------------- /packages/interpol/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wbe/interpol", 3 | "description": "Interpolates values with a GSAP-like API ~ 3kB", 4 | "author": { 5 | "name": "Willy Brauner", 6 | "url": "https://willybrauner.com" 7 | }, 8 | "version": "0.20.2", 9 | "type": "module", 10 | "sideEffects": false, 11 | "files": [ 12 | "dist" 13 | ], 14 | "exports": { 15 | ".": { 16 | "import": "./dist/interpol.js", 17 | "require": "./dist/interpol.cjs" 18 | } 19 | }, 20 | "main": "./dist/interpol.cjs", 21 | "module": "./dist/interpol.js", 22 | "types": "./dist/interpol.d.ts", 23 | "repository": { 24 | "type": "git", 25 | "url": "git://github.com/willybrauner/interpol.git" 26 | }, 27 | "keywords": [ 28 | "interpol", 29 | "interpolation", 30 | "animation", 31 | "anim", 32 | "timeline", 33 | "motion" 34 | ], 35 | "scripts": { 36 | "clean": "rm -rf dist", 37 | "build": "tsup", 38 | "build:watch": "tsup --watch --sourcemap" 39 | }, 40 | "devDependencies": { 41 | "terser": "^5.37.0", 42 | "tsup": "^8.3.5", 43 | "typescript": "^5.7.2", 44 | "vite": "^6.0.6", 45 | "vitest": "^2.1.8" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/interpol/src/core/Ticker.ts: -------------------------------------------------------------------------------- 1 | import { isClient } from "./env" 2 | 3 | type TickParams = { 4 | delta: number 5 | time: number 6 | elapsed: number 7 | } 8 | 9 | type Handler = (e: TickParams) => void 10 | 11 | /** 12 | * Ticker 13 | */ 14 | export class Ticker { 15 | #isRunning = false 16 | #handlers: { handler: Handler; rank: number }[] 17 | #onUpdateObj: TickParams 18 | #start: number 19 | #time: number 20 | #elapsed: number 21 | #keepElapsed: number 22 | #delta: number 23 | #rafId: number 24 | #isClient: boolean 25 | #enable: boolean 26 | 27 | constructor() { 28 | this.#handlers = [] 29 | this.#onUpdateObj = { delta: null, time: null, elapsed: null } 30 | this.#keepElapsed = 0 31 | this.#enable = true 32 | this.#isClient = isClient() 33 | this.#initEvents() 34 | // wait a frame in case disableRaf is set to true 35 | setTimeout(() => this.play(), 0) 36 | } 37 | 38 | public disable(): void { 39 | this.#enable = false 40 | } 41 | 42 | public add(handler: Handler, rank: number = 0): () => void { 43 | this.#handlers.push({ handler, rank }) 44 | this.#handlers.sort((a, b) => a.rank - b.rank) 45 | return () => this.remove(handler) 46 | } 47 | 48 | public remove(handler: Handler): void { 49 | this.#handlers = this.#handlers.filter((obj) => obj.handler !== handler) 50 | } 51 | 52 | public play(): void { 53 | this.#isRunning = true 54 | this.#start = performance.now() 55 | this.#time = this.#start 56 | this.#elapsed = this.#keepElapsed + (this.#time - this.#start) 57 | this.#delta = 16 58 | if (this.#enable && this.#isClient) { 59 | this.#rafId = requestAnimationFrame(this.#update) 60 | } 61 | } 62 | 63 | public pause(): void { 64 | this.#isRunning = false 65 | this.#keepElapsed = this.#elapsed 66 | } 67 | 68 | public stop(): void { 69 | this.#isRunning = false 70 | this.#keepElapsed = 0 71 | this.#elapsed = 0 72 | this.#removeEvents() 73 | if (this.#enable && this.#isClient && this.#rafId) { 74 | cancelAnimationFrame(this.#rafId) 75 | this.#rafId = null 76 | } 77 | } 78 | 79 | public raf(t: number): void { 80 | this.#delta = t - (this.#time || t) 81 | this.#time = t 82 | this.#elapsed = this.#keepElapsed + (this.#time - this.#start) 83 | this.#onUpdateObj.delta = this.#delta 84 | this.#onUpdateObj.time = this.#time 85 | this.#onUpdateObj.elapsed = this.#elapsed 86 | for (const { handler } of this.#handlers) handler(this.#onUpdateObj) 87 | } 88 | 89 | #initEvents(): void { 90 | if (this.#isClient) { 91 | document.addEventListener("visibilitychange", this.#handleVisibility) 92 | } 93 | } 94 | 95 | #removeEvents(): void { 96 | if (this.#isClient) { 97 | document.removeEventListener("visibilitychange", this.#handleVisibility) 98 | } 99 | } 100 | 101 | #handleVisibility = (): void => { 102 | document.hidden ? this.pause() : this.play() 103 | } 104 | 105 | #update = (t = performance.now()): void => { 106 | if (!this.#isRunning) return 107 | if (this.#enable && this.#isClient) { 108 | this.#rafId = requestAnimationFrame(this.#update) 109 | } 110 | this.raf(t) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /packages/interpol/src/core/clamp.ts: -------------------------------------------------------------------------------- 1 | export function clamp(min: number, value: number, max: number): number { 2 | return Math.max(min, Math.min(value, max)) 3 | } 4 | -------------------------------------------------------------------------------- /packages/interpol/src/core/compute.ts: -------------------------------------------------------------------------------- 1 | export const compute = (p) => (typeof p === "function" ? p() : p) 2 | -------------------------------------------------------------------------------- /packages/interpol/src/core/deferredPromise.ts: -------------------------------------------------------------------------------- 1 | export type TDeferredPromise = { 2 | promise: Promise 3 | resolve: (resolve?: T) => void 4 | } 5 | 6 | /** 7 | * @name deferredPromise 8 | * @return TDeferredPromise 9 | */ 10 | export function deferredPromise(): TDeferredPromise { 11 | const deferred: TDeferredPromise | any = {} 12 | deferred.promise = new Promise((resolve) => { 13 | deferred.resolve = resolve 14 | }) 15 | return deferred 16 | } 17 | -------------------------------------------------------------------------------- /packages/interpol/src/core/ease.ts: -------------------------------------------------------------------------------- 1 | // Power1: Quad 2 | export const Power1: Power = { 3 | in: (t) => t * t, 4 | out: (t) => t * (2 - t), 5 | inOut: (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t), 6 | } 7 | 8 | // Power2: Cubic 9 | export const Power2: Power = { 10 | in: (t) => t * t * t, 11 | out: (t) => 1 - Math.pow(1 - t, 3), 12 | inOut: (t) => (t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2), 13 | } 14 | 15 | // Power3: Quart 16 | export const Power3: Power = { 17 | in: (t) => t * t * t * t, 18 | out: (t) => 1 - Math.pow(1 - t, 4), 19 | inOut: (t) => (t < 0.5 ? 8 * t * t * t * t : 1 - Math.pow(-2 * t + 2, 4) / 2), 20 | } 21 | 22 | // Power4: Quint 23 | export const Power4: Power = { 24 | in: (t) => t * t * t * t * t, 25 | out: (t) => 1 - Math.pow(1 - t, 5), 26 | inOut: (t) => (t < 0.5 ? 16 * t * t * t * t * t : 1 - Math.pow(-2 * t + 2, 5) / 2), 27 | } 28 | 29 | // Expo 30 | export const Expo: Power = { 31 | in: (t) => (t === 0 ? 0 : Math.pow(2, 10 * (t - 1))), 32 | out: (t) => (t === 1 ? 1 : -Math.pow(2, -10 * t) + 1), 33 | inOut: (t) => { 34 | if (t === 0) return 0 35 | if (t === 1) return 1 36 | if ((t /= 0.5) < 1) return 0.5 * Math.pow(2, 10 * (t - 1)) 37 | return 0.5 * (-Math.pow(2, -10 * --t) + 2) 38 | }, 39 | } 40 | 41 | export const Linear: EaseFn = (t) => t 42 | 43 | /** 44 | * Adaptor for gsap ease functions as string 45 | */ 46 | // prettier-ignore 47 | export type EaseType = "power1" | "power2" | "power3" | "power4" | "expo" 48 | export type EaseDirection = "in" | "out" | "inOut" 49 | export type EaseName = `${EaseType}.${EaseDirection}` | "linear" | "none" 50 | export type EaseFn = (t: number) => number 51 | export type Ease = EaseName | EaseFn 52 | export type Power = Record 53 | 54 | export const easeAdapter = (ease: EaseName): EaseFn => { 55 | let [type, direction] = ease.split(".") as [EaseType, EaseDirection] 56 | // if first letter is lowercase, capitalize it 57 | if (type[0] === type[0].toLowerCase()) { 58 | type = (type[0].toUpperCase() + type.slice(1)) as EaseType 59 | } 60 | const e = { Linear, Power1, Power2, Power3, Power4, Expo } 61 | return e?.[type]?.[direction] ?? Linear 62 | } 63 | -------------------------------------------------------------------------------- /packages/interpol/src/core/env.ts: -------------------------------------------------------------------------------- 1 | export const isClient = () => typeof window < "u" 2 | -------------------------------------------------------------------------------- /packages/interpol/src/core/noop.ts: -------------------------------------------------------------------------------- 1 | export const noop = () => {} 2 | -------------------------------------------------------------------------------- /packages/interpol/src/core/round.ts: -------------------------------------------------------------------------------- 1 | export const round = (v: number, decimal = 1000): number => 2 | Math.round(v * decimal) / decimal 3 | -------------------------------------------------------------------------------- /packages/interpol/src/core/styles.ts: -------------------------------------------------------------------------------- 1 | import { El, CallbackProps } from "./types" 2 | 3 | const CACHE = new Map>() 4 | const COORDS = new Set(["x", "y", "z"]) 5 | const NO_PX = new Set([ 6 | "opacity", 7 | "scale", 8 | "scaleX", 9 | "scaleY", 10 | "scaleZ", 11 | "perspective", 12 | "transformOrigin", 13 | ]) 14 | const DEG_PROPERTIES = new Set([ 15 | "rotate", 16 | "rotateX", 17 | "rotateY", 18 | "rotateZ", 19 | "skew", 20 | "skewX", 21 | "skewY", 22 | ]) 23 | 24 | function formatValue(key: string, val: number | string, format = true): string | number { 25 | if (!format || typeof val !== "number") return val 26 | if (NO_PX.has(key)) return val 27 | if (DEG_PROPERTIES.has(key)) return `${val}deg` 28 | return `${val}px` 29 | } 30 | 31 | /** 32 | * Styles function 33 | * @description Set CSS properties on DOM element(s) or object properties 34 | * @param element HTMLElement or array of HTMLElement or object 35 | * @param props Object of css properties to set 36 | * @param autoUnits Auto add "px" & "deg" units to number values, string values are not affected 37 | * @returns 38 | */ 39 | export const styles = ( 40 | element: El, 41 | props: CallbackProps, 42 | autoUnits = true, 43 | ): void => { 44 | if (!element) return 45 | if (!Array.isArray(element)) element = [element as HTMLElement] 46 | 47 | // for each element 48 | for (const el of element) { 49 | const cache = CACHE.get(el) || {} 50 | 51 | // for each key 52 | for (let key in props) { 53 | const v = formatValue(key, props[key], autoUnits) 54 | // Specific case for "translate3d" 55 | // if x, y, z are keys 56 | if (COORDS.has(key)) { 57 | const val = (c) => formatValue(c, props?.[c] ?? cache?.[c] ?? "0px", autoUnits) 58 | cache.translate3d = `translate3d(${val("x")}, ${val("y")}, ${val("z")})` 59 | cache[key] = `${v}` 60 | } 61 | // Other transform properties 62 | else if (key.match(/^(translate|rotate|scale|skew)/)) { 63 | cache[key] = `${key}(${v})` 64 | } 65 | 66 | // All other properties, applying directly 67 | else { 68 | // case this is a style property 69 | if (el.style) el.style[key] = v && `${v}` 70 | // case this is a simple object 71 | else el[key] = v 72 | } 73 | } 74 | 75 | // Get the string of transform properties without COORDS (x, y and z values) 76 | // ex: translate3d(0px, 11px, 0px) scale(1) rotate(1deg) 77 | const transformString = Object.keys(cache) 78 | .reduce((a, b) => (COORDS.has(b) ? a : a + cache[b] + " "), "") 79 | .trim() 80 | 81 | // Finally Apply the join transform string properties with values of COORDS 82 | if (transformString !== "") el.style.transform = transformString 83 | 84 | // Cache the transform properties object 85 | CACHE.set(el, cache) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /packages/interpol/src/core/types.ts: -------------------------------------------------------------------------------- 1 | import { Interpol } from "../Interpol" 2 | import { Ticker } from "./Ticker" 3 | import { Ease } from "./ease" 4 | 5 | /** 6 | * Common 7 | * 8 | * 9 | */ 10 | export type El = HTMLElement | HTMLElement[] | Record | null 11 | 12 | // Value can be a number or a function that return a number 13 | export type Value = number | (() => number) 14 | 15 | // Props params 16 | export type PropsValues = 17 | | Value 18 | | [Value, Value] 19 | | Partial<{ from: Value; to: Value; ease: Ease; reverseEase: Ease }> 20 | 21 | // props 22 | export type Props = Record 23 | export type PropKeys = keyof InterpolConstructBase | (string & {}) 24 | export type ExtraProps = Record< 25 | Exclude>, 26 | PropsValues 27 | > 28 | 29 | // Final Props Object returned by callbacks 30 | export type CallbackProps = Record 31 | 32 | // Props object formatted in Map 33 | export type FormattedProp = { 34 | from: Value 35 | to: Value 36 | _from: number 37 | _to: number 38 | value: number 39 | ease: Ease 40 | reverseEase: Ease 41 | } 42 | 43 | /** 44 | * Interpol 45 | * 46 | * 47 | */ 48 | export type CallBack = ( 49 | props: CallbackProps>>, 50 | time: number, 51 | progress: number, 52 | instance: Interpol, 53 | ) => void 54 | 55 | export type InterpolConstructBase = { 56 | duration?: Value 57 | ease?: Ease 58 | reverseEase?: Ease 59 | paused?: boolean 60 | immediateRender?: boolean 61 | delay?: number 62 | debug?: boolean 63 | beforeStart?: CallBack 64 | onUpdate?: CallBack 65 | onComplete?: CallBack 66 | } 67 | 68 | // @credit Philippe Elsass 69 | export type InterpolConstruct> = InterpolConstructBase & { 70 | [key in T]: key extends keyof InterpolConstructBase 71 | ? InterpolConstructBase[key] 72 | : PropsValues 73 | } 74 | 75 | /** 76 | * Timeline 77 | * 78 | */ 79 | export type TimelineCallback = (time: number, progress: number) => void 80 | 81 | export interface TimelineConstruct { 82 | paused?: boolean 83 | debug?: boolean 84 | onUpdate?: TimelineCallback 85 | onComplete?: TimelineCallback 86 | ticker?: Ticker 87 | } 88 | -------------------------------------------------------------------------------- /packages/interpol/src/index.ts: -------------------------------------------------------------------------------- 1 | export type { InterpolConstruct, Props, TimelineConstruct, Value } from "./core/types" 2 | 3 | export { InterpolOptions } from "./options" 4 | export { Interpol } from "./Interpol" 5 | export { Timeline } from "./Timeline" 6 | export { Ticker } from "./core/Ticker" 7 | export { Power1, Power2, Power3, Power4, Expo, easeAdapter } from "./core/ease" 8 | export { styles } from "./core/styles" 9 | -------------------------------------------------------------------------------- /packages/interpol/src/itp.ts: -------------------------------------------------------------------------------- 1 | import { InterpolConstruct, Props } from "./core/types" 2 | import { Interpol } from "./index" 3 | 4 | export function itp(options: InterpolConstruct): Interpol { 5 | return new Interpol(options) 6 | } 7 | -------------------------------------------------------------------------------- /packages/interpol/src/options.ts: -------------------------------------------------------------------------------- 1 | import { Ticker } from "./core/Ticker" 2 | import { Value } from "./core/types" 3 | import { Ease } from "./core/ease" 4 | 5 | /** 6 | * global options in window object 7 | */ 8 | interface InterpolOptions { 9 | ticker: Ticker 10 | durationFactor: number 11 | duration: Value 12 | ease: Ease 13 | } 14 | 15 | export const InterpolOptions: InterpolOptions = { 16 | ticker: new Ticker(), 17 | durationFactor: 1, 18 | duration: 1000, 19 | ease: "linear", 20 | } 21 | -------------------------------------------------------------------------------- /packages/interpol/src/utils/clamp.ts: -------------------------------------------------------------------------------- 1 | export function clamp(min: number, value: number, max: number): number { 2 | return Math.max(min, Math.min(value, max)) 3 | } 4 | -------------------------------------------------------------------------------- /packages/interpol/src/utils/compute.ts: -------------------------------------------------------------------------------- 1 | export const compute = (p) => (typeof p === "function" ? p() : p) 2 | -------------------------------------------------------------------------------- /packages/interpol/src/utils/deferredPromise.ts: -------------------------------------------------------------------------------- 1 | export type TDeferredPromise = { 2 | promise: Promise 3 | resolve: (resolve?: T) => void 4 | } 5 | 6 | /** 7 | * @name deferredPromise 8 | * @return TDeferredPromise 9 | */ 10 | export function deferredPromise(): TDeferredPromise { 11 | const deferred: TDeferredPromise | any = {} 12 | deferred.promise = new Promise((resolve) => { 13 | deferred.resolve = resolve 14 | }) 15 | return deferred 16 | } 17 | -------------------------------------------------------------------------------- /packages/interpol/src/utils/env.ts: -------------------------------------------------------------------------------- 1 | export const isClient = () => typeof window < "u" 2 | -------------------------------------------------------------------------------- /packages/interpol/src/utils/noop.ts: -------------------------------------------------------------------------------- 1 | export const noop = () => {} 2 | -------------------------------------------------------------------------------- /packages/interpol/src/utils/round.ts: -------------------------------------------------------------------------------- 1 | export const round = (v: number, decimal = 1000): number => 2 | Math.round(v * decimal) / decimal 3 | -------------------------------------------------------------------------------- /packages/interpol/tests/Interpol.basic.test.ts: -------------------------------------------------------------------------------- 1 | import { it, expect, describe, vi } from "vitest" 2 | import { Interpol } from "../src" 3 | import { randomRange } from "./utils/randomRange" 4 | import "./_setup" 5 | 6 | describe.concurrent("Interpol basic", () => { 7 | it("should return the right time", async () => { 8 | const test = (duration: number) => { 9 | return new Interpol({ 10 | v: [5, 100], 11 | duration, 12 | onComplete: (props, time) => { 13 | expect(time).toBe(duration) 14 | }, 15 | }).play() 16 | } 17 | const tests = new Array(30).fill(0).map((_, i) => test(randomRange(0, 1000))) 18 | await Promise.all(tests) 19 | }) 20 | 21 | it("should not auto play if paused is set", async () => { 22 | const mock = vi.fn() 23 | const itp = new Interpol({ 24 | v: [5, 100], 25 | duration: 100, 26 | paused: true, 27 | onUpdate: () => mock(), 28 | onComplete: () => mock(), 29 | }) 30 | expect(itp.isPlaying).toBe(false) 31 | setTimeout(() => { 32 | expect(itp.progress).toBe(0) 33 | expect(mock).toHaveBeenCalledTimes(0) 34 | }, itp.duration) 35 | }) 36 | 37 | it("play should play with duration 0", async () => { 38 | const mock = vi.fn() 39 | return new Promise((resolve: any) => { 40 | new Interpol({ 41 | v: [0, 1000], 42 | duration: 0, 43 | onUpdate: () => { 44 | mock() 45 | expect(mock).toBeCalledTimes(1) 46 | }, 47 | onComplete: (props, time) => { 48 | mock() 49 | expect(mock).toBeCalledTimes(2) 50 | expect(time).toBe(0) 51 | resolve() 52 | }, 53 | }) 54 | }) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /packages/interpol/tests/Interpol.callbacks.test.ts: -------------------------------------------------------------------------------- 1 | import { it, expect, describe, vi } from "vitest" 2 | import { Interpol } from "../src" 3 | import "./_setup" 4 | 5 | describe.concurrent("Interpol callbacks", () => { 6 | it("should execute beforeStart before the play", async () => { 7 | const pms = (paused: boolean) => 8 | new Promise(async (resolve: any) => { 9 | const beforeStart = vi.fn() 10 | const itp = new Interpol({ 11 | x: [0, 100], 12 | duration: 500, 13 | paused, 14 | beforeStart, 15 | }) 16 | expect(beforeStart).toHaveBeenCalledTimes(1) 17 | await itp.play() 18 | expect(beforeStart).toHaveBeenCalledTimes(1) 19 | resolve() 20 | }) 21 | 22 | // play with paused = true 23 | // play with paused = false 24 | return Promise.all([pms(true), pms(false)]) 25 | }) 26 | 27 | it("should return a resolved promise when complete", async () => { 28 | return new Promise(async (resolve: any) => { 29 | const mock = vi.fn() 30 | const itp = new Interpol({ 31 | v: [0, 100], 32 | duration: 100, 33 | paused: true, 34 | onComplete: () => mock(), 35 | }) 36 | await itp.play() 37 | expect(itp.isPlaying).toBe(false) 38 | expect(mock).toBeCalledTimes(1) 39 | resolve() 40 | }) 41 | }) 42 | 43 | it("Call onUpdate once on beforeStart if immediateRender is true", () => { 44 | const test = (immediateRender: boolean) => 45 | new Promise(async (resolve: any) => { 46 | const onUpdate = vi.fn() 47 | new Interpol({ 48 | paused: true, 49 | v: [0, 100], 50 | duration: 100, 51 | immediateRender, 52 | onUpdate, 53 | }) 54 | expect(onUpdate).toHaveBeenCalledTimes(immediateRender ? 1 : 0) 55 | resolve() 56 | }) 57 | return Promise.all([test(true), test(false)]) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /packages/interpol/tests/Interpol.delay.test.ts: -------------------------------------------------------------------------------- 1 | import { it, expect, describe, vi, afterEach } from "vitest" 2 | import { wait } from "./utils/wait" 3 | import { Interpol, InterpolOptions } from "../src" 4 | import "./_setup" 5 | 6 | describe.concurrent("Interpol delay", () => { 7 | it("play with delay", () => { 8 | return new Promise(async (resolve: any) => { 9 | const delay = 200 10 | const mock = vi.fn() 11 | const itp = new Interpol({ 12 | delay, 13 | onComplete: () => mock(), 14 | }) 15 | // juste before play 16 | await wait(delay).then(() => { 17 | expect(itp.isPlaying).toBe(true) 18 | expect(itp.time).toBe(0) 19 | expect(itp.progress).toBe(0) 20 | }) 21 | // wait just after play 22 | await wait(100) 23 | expect(itp.time).toBeGreaterThan(0) 24 | expect(itp.progress).toBeGreaterThan(0) 25 | resolve() 26 | }) 27 | }) 28 | 29 | afterEach(() => { 30 | InterpolOptions.durationFactor = 1 31 | InterpolOptions.duration = 1000 32 | }) 33 | 34 | it("play with delay when a custom Duration factor is set", () => { 35 | return new Promise(async (resolve: any) => { 36 | InterpolOptions.durationFactor = 1000 37 | InterpolOptions.duration = 1 38 | const delay = 0.2 39 | const mock = vi.fn() 40 | const itp = new Interpol({ 41 | delay, 42 | onComplete: () => mock(), 43 | }) 44 | // juste before play 45 | await wait(delay * InterpolOptions.durationFactor).then(() => { 46 | expect(itp.isPlaying).toBe(true) 47 | expect(itp.time).toBe(0) 48 | expect(itp.progress).toBe(0) 49 | }) 50 | // wait just after play 51 | await wait(100) 52 | expect(itp.time).toBeGreaterThan(0) 53 | expect(itp.progress).toBeGreaterThan(0) 54 | resolve() 55 | }) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /packages/interpol/tests/Interpol.duration.test.ts: -------------------------------------------------------------------------------- 1 | import { it, expect, describe, afterEach } from "vitest" 2 | import { Interpol, InterpolOptions } from "../src" 3 | import "./_setup" 4 | import { Value } from "../src/core/types" 5 | 6 | describe.concurrent("Interpol duration", () => { 7 | afterEach(() => { 8 | InterpolOptions.durationFactor = 1 9 | }) 10 | 11 | it("should have 1 as durationFactor by default", async () => { 12 | // use the default durationFactor (1) 13 | return new Interpol({ 14 | duration: 200, 15 | onComplete: (_, time) => { 16 | expect(time).toBe(200) 17 | }, 18 | }).play() 19 | }) 20 | 21 | it("should use duration in second for global interpol instances", async () => { 22 | // use the default durationFactor (1) 23 | InterpolOptions.durationFactor = 1000 24 | InterpolOptions.duration = 0.2 25 | return new Interpol({ 26 | onComplete: (_, time) => { 27 | expect(time).toBe(200) 28 | }, 29 | }).play() 30 | }) 31 | 32 | it("should accept custom durationFactor", async () => { 33 | const test = (durationFactor: number, duration: Value) => { 34 | // set the custom durationFactor 35 | InterpolOptions.durationFactor = durationFactor 36 | return new Interpol({ 37 | duration, 38 | onComplete: (_, time) => { 39 | expect(time).toBe( 40 | (typeof duration === "function" ? duration() : duration) * durationFactor, 41 | ) 42 | }, 43 | }).play() 44 | } 45 | 46 | // prettier-ignore 47 | return Promise.all([ 48 | test(0.5, 400), 49 | test(1, 200), 50 | test(1000, 0.2), 51 | ]) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /packages/interpol/tests/Interpol.pause.test.ts: -------------------------------------------------------------------------------- 1 | import { it, expect, describe, vi } from "vitest" 2 | import { wait } from "./utils/wait" 3 | import { Interpol } from "../src" 4 | import "./_setup" 5 | 6 | describe.concurrent("Interpol pause", () => { 7 | it("should play, pause and play again (resume)", async () => { 8 | const mock = vi.fn() 9 | let savedTime = vi.fn(() => 0) 10 | return new Promise(async (resolve: any) => { 11 | const itp = new Interpol({ 12 | duration: 1000, 13 | paused: true, 14 | onUpdate: mock, 15 | }) 16 | expect(mock).toHaveBeenCalledTimes(0) 17 | itp.play() 18 | expect(itp.isPlaying).toBe(true) 19 | await wait(500) 20 | itp.pause() 21 | expect(mock).toHaveBeenCalled() 22 | expect(itp.isPlaying).toBe(false) 23 | // save time before restart (should be around 500) 24 | savedTime.mockReturnValue(itp.time) 25 | // and play again (resume) 26 | itp.play() 27 | // We are sure that time is not reset on play() after pause() 28 | await wait(100) 29 | expect(itp.progress - savedTime()).toBeLessThan(150) 30 | expect(itp.isPlaying).toBe(true) 31 | resolve() 32 | }) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /packages/interpol/tests/Interpol.props.test.ts: -------------------------------------------------------------------------------- 1 | import { it, expect, describe } from "vitest" 2 | import { Interpol } from "../src" 3 | import "./_setup" 4 | 5 | describe.concurrent("Interpol props", () => { 6 | it("should accept array props", async () => { 7 | const test = (from, to, onCompleteProp) => 8 | new Interpol({ 9 | duration: 100, 10 | x: [from, to], 11 | y: [from, to], 12 | onUpdate: ({ x, y }) => { 13 | expect(x).toBeTypeOf("number") 14 | expect(y).toBeTypeOf("number") 15 | }, 16 | onComplete: ({ x, y }) => { 17 | expect(x).toBe(onCompleteProp) 18 | expect(x).toBeTypeOf("number") 19 | expect(y).toBeTypeOf("number") 20 | }, 21 | }).play() 22 | 23 | return Promise.all([ 24 | test(0, 1000, 1000), 25 | test(-100, 100, 100), 26 | test(0, 1000, 1000), 27 | test(null, 1000, 1000), 28 | test(null, null, NaN), 29 | ]) 30 | }) 31 | 32 | it("should accept object props", async () => { 33 | const test = (from, to, onCompleteProp) => 34 | new Interpol({ 35 | x: { from, to, ease: "power3.out", reverseEase: "power2.in" }, 36 | y: { from, to }, 37 | duration: 100, 38 | onUpdate: ({ x }) => { 39 | expect(x).toBeTypeOf("number") 40 | }, 41 | onComplete: ({ x }) => { 42 | expect(x).toBe(onCompleteProp) 43 | expect(x).toBeTypeOf("number") 44 | }, 45 | }).play() 46 | 47 | return Promise.all([ 48 | test(0, 1000, 1000), 49 | test(-100, 100, 100), 50 | test(0, 1000, 1000), 51 | test(null, 1000, 1000), 52 | test(null, null, NaN), 53 | ]) 54 | }) 55 | 56 | it("should accept a single number props 'to', implicit 'from'", async () => { 57 | const test = (to, onCompleteProp) => 58 | new Interpol({ 59 | x: to, 60 | duration: 100, 61 | onUpdate: ({ x }) => { 62 | expect(x).toBeTypeOf("number") 63 | }, 64 | onComplete: ({ x }) => { 65 | expect(x).toBe(onCompleteProp) 66 | expect(x).toBeTypeOf("number") 67 | }, 68 | }).play() 69 | 70 | return Promise.all([test(0, 0), test(1000, 1000), test(10, 10), test(null, 0)]) 71 | }) 72 | 73 | it("should accept inline props", async () => { 74 | return new Interpol({ 75 | duration: 100, 76 | x: 100, 77 | y: -100, 78 | top: [0, 100], 79 | left: [-100, 100], 80 | onComplete: ({ x, y, top, left }) => { 81 | expect(x).toBe(100) 82 | expect(y).toBe(-100) 83 | expect(top).toBe(100) 84 | expect(left).toBe(100) 85 | }, 86 | }).play() 87 | }) 88 | 89 | it("Should works without props object and without inline props", async () => { 90 | return new Interpol({ 91 | duration: 100, 92 | onUpdate: (props, time, progress) => { 93 | expect(props).toEqual({}) 94 | }, 95 | onComplete: (props, time, progress) => { 96 | expect(props).toEqual({}) 97 | }, 98 | }).play() 99 | }) 100 | }) 101 | -------------------------------------------------------------------------------- /packages/interpol/tests/Interpol.refresh.test.ts: -------------------------------------------------------------------------------- 1 | import { it, expect, describe, vi } from "vitest" 2 | import { randomRange } from "./utils/randomRange" 3 | import { Interpol } from "../src" 4 | import { wait } from "./utils/wait" 5 | import "./_setup" 6 | 7 | describe.concurrent("Interpol refresh", () => { 8 | it("should compute 'from' 'to' and 'duration' if there are functions", async () => { 9 | return new Promise(async (resolve: any) => { 10 | const itp = new Interpol({ 11 | v: [() => randomRange(-100, 100), () => randomRange(-100, 100)], 12 | duration: () => randomRange(-100, 100), 13 | }) 14 | expect(typeof itp.props.v._to).toBe("number") 15 | expect(typeof itp.props.v._from).toBe("number") 16 | expect(typeof itp.duration).toBe("number") 17 | resolve() 18 | }) 19 | }) 20 | 21 | it("should re compute if refreshComputedValues() is called", async () => { 22 | return new Promise(async (resolve: any) => { 23 | const mockTo = vi.fn() 24 | const mockFrom = vi.fn() 25 | const itp = new Interpol({ 26 | v: [ 27 | () => { 28 | mockFrom() 29 | return randomRange(-100, 100) 30 | }, 31 | () => { 32 | mockTo() 33 | return randomRange(-100, 100) 34 | }, 35 | ], 36 | 37 | duration: () => 66, 38 | }) 39 | 40 | expect(mockFrom).toHaveBeenCalledTimes(1) 41 | expect(mockTo).toHaveBeenCalledTimes(1) 42 | expect(itp.duration).toBe(66) 43 | await wait(itp.duration) 44 | itp.refreshComputedValues() 45 | await wait(500) 46 | expect(mockFrom).toHaveBeenCalledTimes(2) 47 | expect(mockTo).toHaveBeenCalledTimes(2) 48 | expect(itp.duration).toBe(66) 49 | resolve() 50 | }) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /packages/interpol/tests/Interpol.reverse.test.ts: -------------------------------------------------------------------------------- 1 | import { it, expect, describe, vi } from "vitest" 2 | import { Interpol } from "../src" 3 | import { wait } from "./utils/wait" 4 | import "./_setup" 5 | 6 | describe.concurrent("Interpol reverse", () => { 7 | it("should update 'isRevered' state", async () => { 8 | return new Promise(async (resolve: any) => { 9 | const duration = 500 10 | const itp = new Interpol({ 11 | paused: true, 12 | duration, 13 | }) 14 | expect(itp.isReversed).toBe(false) 15 | await wait(100) 16 | itp.reverse() 17 | expect(itp.isReversed).toBe(true) 18 | resolve() 19 | }) 20 | }) 21 | 22 | it("should not call onComplete if play is not resolved", async () => { 23 | const onComplete = vi.fn() 24 | return new Promise(async (resolve: any) => { 25 | const duration = 300 26 | const itp = new Interpol({ 27 | paused: true, 28 | duration, 29 | onComplete, 30 | }) 31 | 32 | itp.play() 33 | await wait(duration / 2) 34 | itp.reverse() 35 | await wait(duration) 36 | expect(onComplete).toHaveBeenCalledTimes(0) 37 | resolve() 38 | }) 39 | }) 40 | 41 | it("should resolve reverse() promise when reverse is complete", async () => { 42 | const test = async ({ duration, waitBetweenPlayAndReverse }) => { 43 | const reverseComplete = vi.fn() 44 | const itp = new Interpol({ duration, paused: true }) 45 | // play and wait half duration 46 | itp.play() 47 | await wait(waitBetweenPlayAndReverse) 48 | // reverse during the play 49 | await itp.reverse().then(() => reverseComplete()) 50 | // the reverse() promise should resolve when the reverse is complete 51 | expect(reverseComplete).toHaveBeenCalledTimes(1) 52 | } 53 | 54 | return Promise.all([ 55 | // wait long time between play and reverse 56 | test({ 57 | duration: 100, 58 | waitBetweenPlayAndReverse: 100 * 2, 59 | }), 60 | 61 | // wait only the duration of the play before reverse 62 | test({ 63 | duration: 100, 64 | waitBetweenPlayAndReverse: 100, 65 | }), 66 | 67 | // wait half the duration of the play before reverse 68 | // Start the reverse during the play 69 | test({ 70 | duration: 100, 71 | waitBetweenPlayAndReverse: 100 / 2, 72 | }), 73 | ]) 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /packages/interpol/tests/Interpol.seek.test.ts: -------------------------------------------------------------------------------- 1 | import { it, expect, vi, describe } from "vitest" 2 | import { Interpol } from "../src" 3 | import "./_setup" 4 | import { wait } from "./utils/wait" 5 | 6 | describe.concurrent("Interpol seek", () => { 7 | it("Interpol should be seekable to specific progress", () => { 8 | return new Promise(async (resolve: any) => { 9 | const mock = vi.fn() 10 | const itp = new Interpol({ 11 | v: [0, 100], 12 | duration: 1000, 13 | onUpdate: ({ v }) => mock(v), 14 | }) 15 | for (let v of [0.25, 0.5, 0.75, 1, 1, 0.2, 0.2, 0, 0]) { 16 | // seek will pause the interpol, that's why the test is instant 17 | itp.seek(v) 18 | expect(mock).toHaveBeenCalledWith(100 * v) 19 | } 20 | resolve() 21 | }) 22 | }) 23 | 24 | it("Interpol should be seekable to the same progress several times in a row", () => { 25 | /** 26 | * Goal is to test if the onUpdate callback is called each time we seek to the same progress value 27 | */ 28 | return new Promise(async (resolve: any) => { 29 | const mock = vi.fn() 30 | const itp = new Interpol({ 31 | v: [0, 1000], 32 | duration: 1000, 33 | onUpdate: ({ v }) => mock(v), 34 | }) 35 | 36 | // stop it during the play 37 | await wait(100) 38 | 39 | // clear the mock value, because it will be called before the first seek 40 | mock.mockClear() 41 | // seek multiple times on the same progress value 42 | const SEEK_REPEAT_NUMBER = 30 43 | 44 | for (let i = 0; i < SEEK_REPEAT_NUMBER; i++) itp.seek(0.5) 45 | // itp onUpdate should be called 50 times 46 | expect(mock).toHaveBeenCalledTimes(SEEK_REPEAT_NUMBER) 47 | expect(mock).toHaveBeenCalledWith(500) 48 | 49 | // clear the mock value before seek in orde to have a clean count 50 | mock.mockClear() 51 | for (let i = 0; i < SEEK_REPEAT_NUMBER; i++) itp.seek(0) 52 | // itp onUpdate should be called 50 times 53 | expect(mock).toHaveBeenCalledTimes(SEEK_REPEAT_NUMBER) 54 | expect(mock).toHaveBeenCalledWith(0) 55 | 56 | resolve() 57 | }) 58 | }) 59 | 60 | it("Should execute Interpol events callbacks on seek if suppressEvents is false", () => { 61 | return new Promise(async (resolve: any) => { 62 | const onComplete = vi.fn() 63 | const itp = new Interpol({ onComplete }) 64 | // onComplete is called each time the interpol reach the end (progress 1) 65 | itp.seek(0.5, false) 66 | expect(onComplete).toHaveBeenCalledTimes(0) 67 | itp.seek(1, false) // will call onComplete 68 | expect(onComplete).toHaveBeenCalledTimes(1) 69 | itp.seek(0.25, false) 70 | expect(onComplete).toHaveBeenCalledTimes(1) 71 | itp.seek(1, false) // will call onComplete again 72 | expect(onComplete).toHaveBeenCalledTimes(2) 73 | itp.seek(0, false) 74 | expect(onComplete).toHaveBeenCalledTimes(2) 75 | resolve() 76 | }) 77 | }) 78 | 79 | it("Shouldn't execute Interpol events callbacks on seek if suppressEvents is true", () => { 80 | return new Promise(async (resolve: any) => { 81 | const onComplete = vi.fn() 82 | const itp = new Interpol({ onComplete }) 83 | itp.seek(0.5) 84 | itp.seek(1) 85 | itp.seek(0.25) 86 | itp.seek(1) 87 | itp.seek(0) 88 | expect(onComplete).toHaveBeenCalledTimes(0) 89 | resolve() 90 | }) 91 | }) 92 | }) 93 | -------------------------------------------------------------------------------- /packages/interpol/tests/Interpol.stress.test.ts: -------------------------------------------------------------------------------- 1 | import { it, expect, describe } from "vitest" 2 | import { Interpol } from "../src" 3 | import { interpolParamsGenerator } from "./utils/interpolParamsGenerator" 4 | import { randomRange } from "./utils/randomRange" 5 | import "./_setup" 6 | 7 | /** 8 | * Create generic interpol tester 9 | */ 10 | const interpolTest = (from, to, duration, resolve, isLast) => { 11 | const inter = new Interpol({ 12 | v: [from, to], 13 | duration, 14 | onUpdate: ({ v }) => { 15 | if (inter.props.v.from < inter.props.v.to) { 16 | expect(v).toBeGreaterThanOrEqual(inter.props.v._from) 17 | } else if (inter.props.v.from > inter.props.v.to) { 18 | expect(v).toBeLessThanOrEqual(inter.props.v._from) 19 | } else if (inter.props.v.from === inter.props.v.to) { 20 | expect(v).toBe(inter.props.v.to) 21 | expect(v).toBe(inter.props.v.from) 22 | } 23 | }, 24 | onComplete: (props, time, progress) => { 25 | expect(props.v).toBe(inter.props.v.to) 26 | expect(progress).toBe(1) 27 | if (isLast) resolve() 28 | }, 29 | }) 30 | } 31 | 32 | /** 33 | * Stress test 34 | * w/ from to and duration 35 | */ 36 | describe.concurrent("Interpol stress test", () => { 37 | it("should interpol value between two points", async () => { 38 | let inputs = new Array(50) 39 | .fill(null) 40 | .map((_) => interpolParamsGenerator()) 41 | .sort((a, b) => a.duration - b.duration) 42 | return new Promise((resolve: any) => { 43 | inputs.forEach(async ({ from, to, duration }, i) => { 44 | interpolTest(from, to, duration, resolve, i === inputs.length - 1) 45 | }) 46 | }) 47 | }) 48 | 49 | it("should work if 'from' and 'to' are equals", () => { 50 | let inputs = new Array(50) 51 | .fill(null) 52 | .map((_) => { 53 | return interpolParamsGenerator({ 54 | to: randomRange(-10000, 10000, 2), 55 | from: randomRange(-10000, 10000, 2), 56 | }) 57 | }) 58 | .sort((a, b) => a.duration - b.duration) 59 | return new Promise((resolve: any) => { 60 | inputs.forEach(async ({ from, to, duration }, i) => { 61 | interpolTest(from, to, duration, resolve, i === inputs.length - 1) 62 | }) 63 | }) 64 | }) 65 | 66 | it("should be onComplete immediately if duration is <= 0", () => { 67 | let inputs = new Array(50) 68 | .fill(null) 69 | .map((_) => interpolParamsGenerator({ duration: randomRange(-2000, 0, 2) })) 70 | return new Promise((resolve: any) => { 71 | inputs.forEach(async ({ from, to, duration }, i) => { 72 | interpolTest(from, to, duration, resolve, i === inputs.length - 1) 73 | }) 74 | }) 75 | }) 76 | 77 | it("should work even if the developer does anything :)", () => 78 | new Promise((resolve: any) => interpolTest(0, 0, 0, resolve, true))) 79 | }) 80 | -------------------------------------------------------------------------------- /packages/interpol/tests/Ticker.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vi } from "vitest" 2 | import { Interpol, InterpolOptions, Ticker } from "../src" 3 | import { wait } from "./utils/wait" 4 | import "./_setup" 5 | 6 | describe.concurrent("Ticker", () => { 7 | it("should be disable from options ", () => { 8 | // disable ticker 9 | InterpolOptions.ticker.disable() 10 | 11 | const mock = vi.fn() 12 | return new Promise(async (resolve: any) => { 13 | new Interpol({ 14 | props: { v: [-100, 100] }, 15 | duration: 100, 16 | onComplete: mock, 17 | }) 18 | await wait(110) 19 | // onComplete should not be called after itp is completed 20 | expect(mock).toHaveBeenCalledTimes(0) 21 | resolve() 22 | }) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /packages/interpol/tests/Timeline.callbacks.test.ts: -------------------------------------------------------------------------------- 1 | import { it, expect, vi, describe } from "vitest" 2 | import { Timeline } from "../src" 3 | import "./_setup" 4 | 5 | describe.concurrent("Timeline callbacks", () => { 6 | it("Timeline should execute Timeline events callback once & on play only", () => { 7 | return new Promise(async (resolve: any) => { 8 | const onComplete = vi.fn() 9 | const tl = new Timeline({ paused: true, onComplete }) 10 | tl.add({ 11 | v: [0, 100], 12 | duration: 100, 13 | }) 14 | tl.add({ 15 | v: [0, 100], 16 | duration: 100, 17 | }) 18 | await tl.play() 19 | expect(onComplete).toHaveBeenCalledTimes(1) 20 | await tl.reverse() 21 | expect(onComplete).toHaveBeenCalledTimes(1) 22 | resolve() 23 | }) 24 | }) 25 | 26 | it("Timeline should execute interpol's onComplete once", () => { 27 | return new Promise(async (resolve: any) => { 28 | const onComplete1 = vi.fn() 29 | const onComplete2 = vi.fn() 30 | const tl = new Timeline({ paused: true }) 31 | tl.add({ 32 | v: [0, 100], 33 | duration: 100, 34 | onComplete: () => onComplete1(), 35 | }) 36 | tl.add({ 37 | v: [0, 100], 38 | duration: 100, 39 | onComplete: () => onComplete2(), 40 | }) 41 | await tl.play() 42 | expect(onComplete1).toHaveBeenCalledTimes(1) 43 | await tl.reverse() 44 | expect(onComplete2).toHaveBeenCalledTimes(1) 45 | resolve() 46 | }) 47 | }) 48 | 49 | it("Call onUpdate once on beforeStart if immediateRender is true", () => { 50 | return new Promise(async (resolve: any) => { 51 | const onUpdate = vi.fn() 52 | const onUpdate2 = vi.fn() 53 | 54 | const tl = new Timeline({ paused: true }) 55 | tl.add({ 56 | v: [0, 100], 57 | duration: 100, 58 | immediateRender: true, 59 | onUpdate, 60 | }) 61 | tl.add({ 62 | v: [0, 100], 63 | duration: 100, 64 | onUpdate: onUpdate2, 65 | }) 66 | 67 | expect(onUpdate).toHaveBeenCalledTimes(1) 68 | expect(onUpdate2).toHaveBeenCalledTimes(0) 69 | 70 | resolve() 71 | }) 72 | }) 73 | }) 74 | -------------------------------------------------------------------------------- /packages/interpol/tests/Timeline.offset.test.ts: -------------------------------------------------------------------------------- 1 | import { it, expect, describe } from "vitest" 2 | import { InterpolOptions, Timeline } from "../src" 3 | import "./_setup" 4 | import { afterEach } from "node:test" 5 | 6 | /** 7 | * Template for testing offset 8 | * @param itps 9 | * @param tlDuration 10 | */ 11 | const testTemplate = (itps: [number, (number | string)?][], tlDuration: number) => 12 | new Promise(async (resolve: any) => { 13 | const tl = new Timeline({ 14 | onComplete: (time) => { 15 | // We are testing the final time / final tlDuration 16 | // It depends on itps duration and offset 17 | expect(time).toBe(tlDuration) 18 | resolve() 19 | }, 20 | }) 21 | for (let [duration, offset] of itps) { 22 | tl.add({ duration, v: [0, 100] }, offset) 23 | } 24 | }) 25 | 26 | /** 27 | * Tests 28 | */ 29 | // prettier-ignore 30 | describe.concurrent("Timeline.add() offset", () => { 31 | it("relative offset should work with `0` (string)", () => { 32 | return Promise.all([ 33 | testTemplate([[100], [100], [100]], 300), 34 | testTemplate([[100], [100, "0"], [100, "0"]], 300), 35 | ]) 36 | }) 37 | 38 | it("relative offset should work with string -= or -", () => { 39 | return Promise.all([ 40 | /** 41 | 0 100 200 300 42 | [- itp1 (100) -] 43 | [- itp2 (100) -] 44 | ^ 45 | offset start at relative "-50" (string) 46 | ^ 47 | total duration is 150 48 | */ 49 | testTemplate([[100], [100, "-=50"]], 150), 50 | 51 | testTemplate([[100], [100, "-50"]], 150), 52 | testTemplate([[100], [100, "-=50"], [100, "-=10"]], 240), 53 | testTemplate([[100], [100, "-=50"], [100, "0"]], 250), 54 | ]) 55 | }) 56 | 57 | it("relative offset should work with string += or +", () => { 58 | return Promise.all([ 59 | /** 60 | 0 100 200 300 61 | [- itp1 (100) -] 62 | [- itp2 (100) -] 63 | ^ 64 | offset start at relative "+=50" (string) 65 | ^ 66 | total duration is 250 67 | */ 68 | testTemplate([[100], [100, "+=50"]], 250), 69 | testTemplate([[100], [100, "+50"]], 250), 70 | testTemplate([[100], [100, "50"]], 250), 71 | testTemplate([[100], [100, "10"], [100, "50"]], 360), 72 | testTemplate([[500], [100, "10"], [100, "50"], [100]], 860), 73 | ]) 74 | }) 75 | 76 | it("relative offset should work with negative value", () => { 77 | return Promise.all([ 78 | testTemplate([[50, "-50"]], 0), 79 | testTemplate([[50, "-=50"]], 0), 80 | testTemplate([[50, "-=50"],[100, "-=50"]], 50), 81 | testTemplate([[50, "-=50"],[100, "-100"]], 0), 82 | 83 | /** 84 | -100 0 100 200 300 85 | [--- itp1 (150) ----] 86 | ^ offset start at relative "0" (string) 87 | 88 | < - - - - - - - - - - - -| (itp2 negative offset "-200") 89 | [--- itp2 (150) ----] 90 | ^ total TL duration is 150 91 | */ 92 | testTemplate([[150, "0"],[150, "-200"]], 150), 93 | ]) 94 | }) 95 | 96 | it("absolute offset should work with number", () => { 97 | // prettier-ignore 98 | return Promise.all([ 99 | 100 | // when absolute offset of the second add is 0 101 | /** 102 | 0 100 200 300 103 | [- itp1 (100) -] 104 | [ ------- itp2 (200) -------- ] 105 | ^ 106 | offset start at absolute 0 (number) 107 | ^ 108 | total duration is 200 109 | */ 110 | testTemplate([[100], [200, 0]], 200), 111 | 112 | 113 | // when absolute offset is greater than the second add duration 114 | /** 115 | 0 100 200 300 400 116 | [- itp1 (100) -] 117 | ^ 118 | offset start at absolute 300 (number) 119 | [ ------------- itp2 (300) -------------- ] 120 | ^ 121 | offset start at absolute 0 (number) 122 | ^ 123 | total duration is 400 124 | */ 125 | testTemplate([[100, 300], [300, 0]], 400), 126 | testTemplate([[100, 0], [100]], 200), 127 | testTemplate([[100], [100, 0]], 100), 128 | testTemplate([[100, 0], [100, 50]], 150), 129 | testTemplate([[100], [200, 0], [200, 0], [200, 0], [200, 0], [200, 0]], 200), 130 | testTemplate([[100, 200], [400, 0]], 400), 131 | ]) 132 | }) 133 | 134 | it("absolute offset should work with negative number", () => { 135 | return Promise.all([ 136 | /** 137 | 0 100 200 300 138 | [- itp1 (100) -] 139 | ^ 140 | offset start at absolute -50 (number) 141 | ^ 142 | total duration is 50 143 | */ 144 | testTemplate([[100, -50]], 50), 145 | testTemplate([[50, -50]], 0), 146 | testTemplate([[0, 0]], 0), 147 | testTemplate([[150, -50]], 100), 148 | ]) 149 | }) 150 | 151 | afterEach(()=> { 152 | InterpolOptions.durationFactor = 1 153 | InterpolOptions.duration = 1000 154 | }) 155 | 156 | it('should work with duration factor on relative offset', async() => { 157 | InterpolOptions.durationFactor = 1000 158 | InterpolOptions.duration = 1 159 | const tl = new Timeline({ 160 | paused: true, 161 | onComplete: (time) => { 162 | expect(time).toBe(300) 163 | }, 164 | }) 165 | tl.add({ duration: .2 }) 166 | // start .1 in advance before the first add finishes 167 | tl.add({ duration: .2 }, '-=.1') 168 | return tl.play() 169 | }) 170 | it('should work with duration factor on absolute offset', async() => { 171 | InterpolOptions.durationFactor = 1000 172 | InterpolOptions.duration = 1 173 | const tl = new Timeline({ 174 | paused: true, 175 | onComplete: (time) => { 176 | expect(time).toBe(300) 177 | }, 178 | }) 179 | tl.add({ duration: .2 }) 180 | // start .1 after the first add 181 | tl.add({ duration: .2 }, .1) 182 | return tl.play() 183 | }) 184 | }) 185 | -------------------------------------------------------------------------------- /packages/interpol/tests/Timeline.play.test.ts: -------------------------------------------------------------------------------- 1 | import { it, expect, vi, describe } from "vitest" 2 | import { Timeline, Interpol } from "../src" 3 | import "./_setup" 4 | 5 | describe.concurrent("Timeline play", () => { 6 | it("Timeline should add Interpol's and play properly", () => { 7 | return new Promise(async (resolve: any) => { 8 | const onComplete = vi.fn() 9 | const tl = new Timeline({ onComplete, paused: true }) 10 | // accept instance 11 | tl.add(new Interpol({ duration: 100 })) 12 | // accept object 13 | tl.add({ duration: 100 }) 14 | await tl.play() 15 | expect(onComplete).toBeCalledTimes(1) 16 | resolve() 17 | }) 18 | }) 19 | 20 | it("play should return a promise resolve once, even if play is exe during playing", () => { 21 | return new Promise(async (resolve: any) => { 22 | const onComplete = vi.fn() 23 | const promiseResolve = vi.fn() 24 | const tl = new Timeline({ onComplete, paused: true }) 25 | for (let i = 0; i < 3; i++) { 26 | tl.add({ duration: 100 }) 27 | } 28 | for (let i = 0; i < 3; i++) tl.play() 29 | await tl.play() 30 | expect(onComplete).toBeCalledTimes(1) 31 | promiseResolve() 32 | expect(promiseResolve).toBeCalledTimes(1) 33 | resolve() 34 | }) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /packages/interpol/tests/Timeline.refresh.test.ts: -------------------------------------------------------------------------------- 1 | import { it, expect, vi, describe } from "vitest" 2 | import { Timeline } from "../src" 3 | import "./_setup" 4 | 5 | describe.concurrent("Timeline auto refresh computed values", () => { 6 | it("adds computed values should be re-calc before add stars", () => { 7 | /** 8 | * Goal is to update EXTERNAL_X on the first add() onUpdate and reused the updated EXTERNAL_X 9 | * as "from" of the second add(). 10 | * 11 | * It will work if "from" of the second add() is a computed value 12 | * Behind the scene, we re-execute refreshComputedValues() juste before the add() starts 13 | */ 14 | const tl = new Timeline({ paused: true }) 15 | let EXTERNAL_X = 0 16 | const firstAddTo = 200 17 | const secondAddTo = 30 18 | let firstOnUpdate = true 19 | 20 | tl.add({ 21 | duration: 100, 22 | x: [0, firstAddTo], 23 | onUpdate: ({ x }) => { 24 | // register the external value 25 | EXTERNAL_X = x 26 | }, 27 | }) 28 | tl.add({ 29 | duration: 100, 30 | x: [() => EXTERNAL_X, secondAddTo], 31 | onUpdate: ({ x }, t, p, instance) => { 32 | if (firstOnUpdate) { 33 | expect(EXTERNAL_X).toBe(firstAddTo) 34 | expect(instance.props.x._from).toBe(firstAddTo) 35 | firstOnUpdate = false 36 | } 37 | // register the external value 38 | EXTERNAL_X = x 39 | }, 40 | onComplete: () => { 41 | expect(EXTERNAL_X).toBe(secondAddTo) 42 | }, 43 | }) 44 | return tl.play() 45 | }) 46 | 47 | it("adds values should NOT be refresh before add stars", () => { 48 | const tl = new Timeline({ paused: true }) 49 | let EXTERNAL_X = 0 50 | let firstOnUpdate = true 51 | 52 | tl.add({ 53 | duration: 100, 54 | x: [0, 200], 55 | onUpdate: ({ x }) => { 56 | // register the external value 57 | EXTERNAL_X = x 58 | }, 59 | }) 60 | tl.add({ 61 | duration: 100, 62 | x: [EXTERNAL_X, 30], 63 | onUpdate: ({ x }, time, progress, instance) => { 64 | if (firstOnUpdate) { 65 | // _from as not been computed before the 1st add() starts 66 | expect(instance.props.x._from).toBe(0) 67 | firstOnUpdate = false 68 | } 69 | }, 70 | }) 71 | return tl.play() 72 | }) 73 | }) 74 | -------------------------------------------------------------------------------- /packages/interpol/tests/Timeline.reverse.test.ts: -------------------------------------------------------------------------------- 1 | import { it, expect, vi, describe } from "vitest" 2 | import { Timeline } from "../src" 3 | import "./_setup" 4 | import { wait } from "./utils/wait" 5 | 6 | describe.concurrent("Timeline reverse", () => { 7 | it("should reverse timeline properly", () => { 8 | const timeMock = vi.fn(() => 0) 9 | const progressMock = vi.fn(() => 0) 10 | const onCompleteMock = vi.fn() 11 | const reverseCompleteMock = vi.fn() 12 | 13 | return new Promise(async (resolve: any) => { 14 | const tl = new Timeline({ 15 | paused: true, 16 | onUpdate: (time, progress) => { 17 | timeMock.mockReturnValue(time) 18 | progressMock.mockReturnValue(progress) 19 | }, 20 | onComplete: () => { 21 | onCompleteMock() 22 | 23 | if (!tl.isReversed) { 24 | expect(timeMock()).toBe(200) 25 | expect(progressMock()).toBe(1) 26 | } else { 27 | expect(timeMock()).toBe(0) 28 | expect(progressMock()).toBe(0) 29 | } 30 | }, 31 | }) 32 | 33 | tl.add({ duration: 100 }) 34 | tl.add({ duration: 100 }) 35 | 36 | await tl.play() 37 | await tl.reverse() 38 | await tl.play() 39 | tl.reverse().then(() => { 40 | reverseCompleteMock() 41 | }) 42 | // reverse is not complete yet 43 | expect(reverseCompleteMock).toBeCalledTimes(0) 44 | await wait(300) 45 | // reverse is complete 46 | expect(reverseCompleteMock).toBeCalledTimes(1) 47 | 48 | // onComplete is called 2 times 49 | expect(onCompleteMock).toBeCalledTimes(2) 50 | resolve() 51 | }) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /packages/interpol/tests/Timeline.seek.test.ts: -------------------------------------------------------------------------------- 1 | import { it, expect, vi, describe } from "vitest" 2 | import { Timeline } from "../src" 3 | import "./_setup" 4 | import { wait } from "./utils/wait" 5 | 6 | describe.concurrent("Timeline seek", () => { 7 | it("Timeline should be seekable to specific tl progress", () => { 8 | return new Promise(async (resolve: any) => { 9 | const mock = vi.fn() 10 | const tl = new Timeline({ paused: true }) 11 | tl.add({ 12 | v: [0, 100], 13 | duration: 200, 14 | onUpdate: ({ v }) => mock(v), 15 | }) 16 | for (let v of [0.25, 0.5, 0.75, 1]) { 17 | tl.seek(v) 18 | expect(mock).toHaveBeenCalledWith(100 * v) 19 | } 20 | resolve() 21 | }) 22 | }) 23 | 24 | it("Timeline should be seekable to the same progress several times in a row", () => { 25 | /** 26 | * Goal is to test if the onUpdate callback is called each time we seek to the same progress value 27 | */ 28 | return new Promise(async (resolve: any) => { 29 | const mockAdd1 = vi.fn() 30 | const mockAdd2 = vi.fn() 31 | const tl = new Timeline() 32 | tl.add({ 33 | v: [0, 1000], 34 | duration: 1000, 35 | onUpdate: ({ v }) => mockAdd1(v), 36 | }) 37 | tl.add({ 38 | v: [1000, 2000], 39 | duration: 1000, 40 | onUpdate: ({ v }) => mockAdd2(v), 41 | }) 42 | 43 | // stop it during the play 44 | await wait(100) 45 | 46 | // clear the mock value, because it will be called before the first seek 47 | mockAdd1.mockClear() 48 | mockAdd2.mockClear() 49 | 50 | // seek multiple times on the same progress value 51 | const SEEK_REPEAT_NUMBER = 30 52 | 53 | for (let i = 0; i < SEEK_REPEAT_NUMBER; i++) tl.seek(0.6) 54 | 55 | // when we seek to 0.6, the first interpol should be at v = 1000 only called ONCE 56 | expect(mockAdd1).toHaveBeenCalledTimes(1) 57 | expect(mockAdd1).toHaveBeenCalledWith(1000) 58 | 59 | // and the second interpol should be at v = 1200, SEEK_REPEAT_NUMBER times 60 | expect(mockAdd2).toHaveBeenCalledTimes(SEEK_REPEAT_NUMBER) 61 | expect(mockAdd2).toHaveBeenCalledWith(1200) 62 | 63 | // clear the mock value before seek in orde to have a clean count 64 | mockAdd1.mockClear() 65 | mockAdd2.mockClear() 66 | 67 | // seek to 0 68 | for (let i = 0; i < SEEK_REPEAT_NUMBER; i++) tl.seek(0) 69 | 70 | // same logic as above, the 2de interpol should be at v = 1000 only called ONCE 71 | expect(mockAdd2).toHaveBeenCalledTimes(1) 72 | expect(mockAdd2).toHaveBeenCalledWith(1000) 73 | 74 | // and the first interpol should be at v = 0, SEEK_REPEAT_NUMBER times 75 | expect(mockAdd1).toHaveBeenCalledTimes(SEEK_REPEAT_NUMBER) 76 | expect(mockAdd1).toHaveBeenCalledWith(0) 77 | 78 | resolve() 79 | }) 80 | }) 81 | 82 | it("Timeline should execute interpol's events callbacks on seek if suppressEvents is false", () => { 83 | return new Promise(async (resolve: any) => { 84 | const onComplete1 = vi.fn() 85 | const onComplete2 = vi.fn() 86 | const onTlComplete = vi.fn() 87 | const tl = new Timeline({ paused: true, onComplete: onTlComplete }) 88 | tl.add({ 89 | v: [0, 100], 90 | duration: 100, 91 | onComplete: onComplete1, 92 | }) 93 | tl.add({ 94 | v: [0, 100], 95 | duration: 100, 96 | onComplete: onComplete2, 97 | }) 98 | 99 | tl.seek(0.5, false, false) 100 | expect(onComplete1).toHaveBeenCalledTimes(1) 101 | expect(onComplete2).toHaveBeenCalledTimes(0) 102 | tl.seek(1, false, false) 103 | expect(onComplete1).toHaveBeenCalledTimes(1) 104 | expect(onComplete2).toHaveBeenCalledTimes(1) 105 | tl.seek(0.5, false, false) 106 | expect(onComplete1).toHaveBeenCalledTimes(1) 107 | expect(onComplete2).toHaveBeenCalledTimes(1) 108 | tl.seek(1, false, false) 109 | expect(onComplete1).toHaveBeenCalledTimes(1) 110 | expect(onComplete2).toHaveBeenCalledTimes(2) 111 | 112 | // because 3th argument suppressTlEvents is "false" 113 | expect(onTlComplete).toHaveBeenCalledTimes(2) 114 | 115 | resolve() 116 | }) 117 | }) 118 | 119 | it("Timeline should execute interpol's events callbacks on seek if suppressEvents is true", () => { 120 | return new Promise(async (resolve: any) => { 121 | const onComplete1 = vi.fn() 122 | const onComplete2 = vi.fn() 123 | const onTlComplete = vi.fn() 124 | const tl = new Timeline({ paused: true, onComplete: onTlComplete }) 125 | tl.add({ 126 | duration: 100, 127 | onComplete: onComplete1, 128 | }) 129 | tl.add({ 130 | duration: 100, 131 | onComplete: onComplete2, 132 | }) 133 | 134 | tl.seek(0.5) 135 | expect(onComplete1).toHaveBeenCalledTimes(0) 136 | expect(onComplete2).toHaveBeenCalledTimes(0) 137 | tl.seek(1, false, false) 138 | expect(onComplete1).toHaveBeenCalledTimes(1) 139 | expect(onComplete2).toHaveBeenCalledTimes(1) 140 | 141 | // because 3th argument suppressTlEvents is "true" by default 142 | expect(onTlComplete).toHaveBeenCalledTimes(1) 143 | 144 | resolve() 145 | }) 146 | }) 147 | }) 148 | -------------------------------------------------------------------------------- /packages/interpol/tests/Timeline.stop.test.ts: -------------------------------------------------------------------------------- 1 | import { it, expect, vi, describe } from "vitest" 2 | import { Interpol, Timeline } from "../src" 3 | import { randomRange } from "./utils/randomRange" 4 | import { wait } from "./utils/wait" 5 | import "./_setup" 6 | 7 | describe.concurrent("Timeline stop", () => { 8 | it("Timeline should stop and play properly", () => { 9 | const oneTl = ({ itpNumber = 3, itpDuration = 50 }) => 10 | new Promise(async (resolve: any) => { 11 | const timelineDuration = itpNumber * itpDuration 12 | const onCompleteMock = vi.fn() 13 | 14 | const tl = new Timeline({ 15 | onUpdate: (time, progress) => { 16 | expect(time).toBeGreaterThanOrEqual(0) 17 | expect(progress).toBeGreaterThanOrEqual(0) 18 | }, 19 | onComplete: (time, progress) => { 20 | expect(time).toBe(timelineDuration) 21 | expect(progress).toBe(1) 22 | onCompleteMock() 23 | onCompleteMock.mockClear() 24 | }, 25 | }) 26 | 27 | for (let i = 0; i < itpNumber; i++) { 28 | tl.add( 29 | new Interpol({ 30 | v: [randomRange(-10000, 10000), randomRange(-10000, 10000)], 31 | duration: itpDuration, 32 | }), 33 | ) 34 | } 35 | 36 | // play and stop at 50% of the timeline 37 | tl.play() 38 | await wait(timelineDuration * 0.5) 39 | tl.stop() 40 | 41 | // have been reset after stop 42 | expect(tl.time).toBe(0) 43 | expect(tl.progress).toBe(0) 44 | 45 | // OnComplete should not have been called 46 | expect(onCompleteMock).toHaveBeenCalledTimes(0) 47 | 48 | resolve() 49 | }) 50 | 51 | const TESTS_NUMBER = 1 52 | 53 | const tls = new Array(TESTS_NUMBER).fill(null).map((_) => { 54 | const itpNumber = randomRange(1, 10) 55 | const itpDuration = randomRange(10, 400) 56 | return oneTl({ itpNumber, itpDuration }) 57 | }) 58 | 59 | return Promise.all(tls) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /packages/interpol/tests/Timeline.stress.test.ts: -------------------------------------------------------------------------------- 1 | import { it, expect, vi, describe } from "vitest" 2 | import { Timeline } from "../src" 3 | import { randomRange } from "./utils/randomRange" 4 | import "./_setup" 5 | 6 | describe.concurrent("Timeline stress test", () => { 7 | it("should play multiple timelines properly", () => { 8 | const oneTl = ({ itpNumber, itpDuration }) => 9 | new Promise(async (resolve: any) => { 10 | let timeMock = vi.fn(() => 0) 11 | let progressMock = vi.fn(() => 0) 12 | 13 | // Create TL 14 | const tl = new Timeline({ 15 | paused: true, 16 | onUpdate: (time, progress) => { 17 | timeMock.mockReturnValue(time) 18 | progressMock.mockReturnValue(progress) 19 | }, 20 | onComplete: (time, progress) => { 21 | const t = timeMock() 22 | expect(time).toEqual(t) 23 | 24 | const p = progressMock() 25 | expect(p).toBe(1) 26 | expect(progress).toEqual(p) 27 | 28 | timeMock.mockClear() 29 | progressMock.mockClear() 30 | }, 31 | }) 32 | 33 | // Add interpol to the TL 34 | for (let i = 0; i < itpNumber; i++) { 35 | tl.add({ duration: itpDuration }) 36 | } 37 | 38 | tl.play().then(resolve) 39 | }) 40 | 41 | const TESTS_NUMBER = 50 42 | 43 | const tls = new Array(TESTS_NUMBER).fill(null).map((_) => { 44 | return oneTl({ 45 | itpNumber: randomRange(1, 20), 46 | itpDuration: randomRange(1, 50), 47 | }) 48 | }) 49 | 50 | return Promise.all(tls) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /packages/interpol/tests/_old/Interpol.el.test.ts: -------------------------------------------------------------------------------- 1 | import { it, expect, describe } from "vitest" 2 | import { Interpol } from "../../src" 3 | import { getDocument } from "../utils/getDocument" 4 | import "../_setup" 5 | 6 | describe.concurrent.skip("Interpol DOM el", () => { 7 | // it("should set prop key and value on DOM element", async () => { 8 | // return new Promise(async (resolve: any) => { 9 | // const { el } = getDocument() 10 | // // Props have been automatically set on div as style 11 | // const callback = ({ opacity, y }) => { 12 | // expect(opacity).toBeTypeOf("number") 13 | // expect(y).toBeTypeOf("string") 14 | // expect(el.style.opacity).toBe(`${opacity}`) 15 | // expect(el.style.transform).toBe(`translate3d(0px, ${y}, 0px)`) 16 | // } 17 | // const itp = new Interpol({ 18 | // el, 19 | // paused: true, 20 | // props: { 21 | // opacity: [5, 100], 22 | // y: [-200, 100, "px"], 23 | // }, 24 | // duration: 100, 25 | // immediateRender: true, 26 | // // so beforeStart opacity is already set on div 27 | // beforeStart: callback, 28 | // onUpdate: callback, 29 | // onComplete: callback, 30 | // }) 31 | // await itp.play() 32 | // resolve() 33 | // }) 34 | // }) 35 | // it("should set prop key and value on Object element", async () => { 36 | // const testElObj1 = () => 37 | // new Promise(async (resolve: any) => { 38 | // const program = { uniform: { uProgress: { value: -100 } } } 39 | // const callback = ({ value }) => { 40 | // expect(program.uniform.uProgress.value).toBe(value) 41 | // } 42 | // const itp = new Interpol({ 43 | // el: program.uniform.uProgress, 44 | // duration: 100, 45 | // props: { 46 | // value: [program.uniform.uProgress.value, 100], 47 | // }, 48 | // beforeStart: callback, 49 | // onUpdate: callback, 50 | // onComplete: callback, 51 | // }) 52 | // await itp.play() 53 | // resolve() 54 | // }) 55 | // const testElObj2 = () => 56 | // new Promise(async (resolve: any) => { 57 | // const program = { v: 0 } 58 | // const callback = ({ v }) => { 59 | // expect(program.v).toBe(v) 60 | // } 61 | // const itp = new Interpol({ 62 | // el: program, 63 | // duration: 300, 64 | // props: { 65 | // v: [program.v, 1000], 66 | // }, 67 | // beforeStart: callback, 68 | // onUpdate: callback, 69 | // onComplete: callback, 70 | // }) 71 | // await itp.play() 72 | // resolve() 73 | // }) 74 | // return Promise.all([testElObj1(), testElObj2()]) 75 | // }) 76 | }) 77 | -------------------------------------------------------------------------------- /packages/interpol/tests/_old/Interpol.units.test.ts: -------------------------------------------------------------------------------- 1 | import { it, expect, describe, vi } from "vitest" 2 | import { Interpol } from "../../src" 3 | import "../_setup" 4 | 5 | describe.concurrent.skip("Interpol units", () => { 6 | // it("should return a string value with unit", async () => { 7 | // const test = (unit) => 8 | // new Promise((resolve: any) => { 9 | // const callback = ({ v }) => { 10 | // expect(v).toBeTypeOf("string") 11 | // expect(v).toContain(unit) 12 | // expect(v.slice(-unit.length)).toBe(unit) 13 | // } 14 | // new Interpol({ 15 | // props: { v: [5, 100, unit] }, 16 | // duration: 100, 17 | // beforeStart: ({ v }) => { 18 | // callback({ v }) 19 | // expect(v).toBe(5 + unit) 20 | // }, 21 | // onUpdate: callback, 22 | // onComplete: ({ v }) => { 23 | // callback({ v }) 24 | // expect(v).toBe(100 + unit) 25 | // resolve() 26 | // }, 27 | // }) 28 | // }) 29 | // return Promise.all( 30 | // ["px", "rem", "svh", "foo", "bar", "whatever-unit-string-we-want"].map((e) => test(e)) 31 | // ) 32 | // }) 33 | // it("should return a number value if unit is not defined", async () => { 34 | // return new Promise(async (resolve: any) => { 35 | // const callback = ({ v }) => expect(v).toBeTypeOf("number") 36 | // new Interpol({ 37 | // props: { v: [5, 100] }, 38 | // duration: 100, 39 | // beforeStart: callback, 40 | // onUpdate: callback, 41 | // onComplete: resolve, 42 | // }) 43 | // }) 44 | // }) 45 | }) 46 | -------------------------------------------------------------------------------- /packages/interpol/tests/_setup.ts: -------------------------------------------------------------------------------- 1 | import { InterpolOptions } from "../src" 2 | 3 | /** 4 | * Disable the internal ticker and replace it by a setInterval for nodejs tests. 5 | * Rate to 16ms is ~= to 60fps (1/60). It's a kind of rounded value 6 | * of the real requestAnimationFrame painting rate. 7 | */ 8 | InterpolOptions.ticker.disable() 9 | const runFakeRaf = (rate = 16) => { 10 | let count = 0 11 | setInterval(() => { 12 | InterpolOptions.ticker.raf((count += rate)) 13 | }, rate) 14 | } 15 | runFakeRaf() 16 | -------------------------------------------------------------------------------- /packages/interpol/tests/ease.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest" 2 | import "./_setup" 3 | 4 | import { 5 | easeAdapter, 6 | EaseName, 7 | Expo, 8 | Linear, 9 | Power1, 10 | Power2, 11 | Power3, 12 | Power4, 13 | } from "../src/core/ease" 14 | 15 | const eases = { Power1, Power2, Power3, Power4, Expo } 16 | const types = ["power1", "power2", "power3", "power4", "expo"] 17 | 18 | describe.concurrent("Ease", () => { 19 | // prettier-ignore 20 | it("adaptor should return easing function", () => { 21 | const directions = ["in", "out", "inOut"] 22 | const capitalizeFirstLetter = (s) => { 23 | if (typeof s !== "string" || s.length === 0) return s 24 | return s.charAt(0).toUpperCase() + s.slice(1) 25 | } 26 | 27 | // all other eases 28 | for (const type of types) { 29 | for (const direction of directions) { 30 | const adaptor = easeAdapter(`${type}.${direction}` as EaseName) 31 | expect(adaptor).toBe(eases[capitalizeFirstLetter(type)]?.[direction]) 32 | } 33 | } 34 | 35 | // linear 36 | const adaptor = easeAdapter(`linear` as EaseName) 37 | expect(adaptor).toBe(Linear) 38 | 39 | }) 40 | 41 | it("adaptor should return linear easing function if name doesnt exist", () => { 42 | expect(easeAdapter("power2.oit" as any)).toBe(Linear) 43 | expect(easeAdapter("coucou" as any)).toBe(Linear) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /packages/interpol/tests/options.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest" 2 | import { InterpolOptions, Ticker } from "../src" 3 | 4 | describe.concurrent("options", () => { 5 | it("options should expose Ticker instance", () => { 6 | expect(InterpolOptions.ticker).toBeInstanceOf(Ticker) 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /packages/interpol/tests/styles.test.ts: -------------------------------------------------------------------------------- 1 | import { it, expect, describe } from "vitest" 2 | import { styles } from "../src" 3 | import { getDocument } from "./utils/getDocument" 4 | import "./_setup" 5 | 6 | describe.concurrent("styles DOM helpers", () => { 7 | it("should set props of basic CSS properties on DOM element", async () => { 8 | const { el, doc } = getDocument() 9 | 10 | styles(el, { 11 | opacity: 1, 12 | top: "10px", 13 | left: "30rem", 14 | position: "absolute", 15 | }) 16 | 17 | expect(el.style.opacity).toBe("1") 18 | expect(el.style.top).toBe("10px") 19 | expect(el.style.left).toBe("30rem") 20 | expect(el.style.position).toBe("absolute") 21 | }) 22 | 23 | it("should set props of transform CSS properties on DOM element", async () => { 24 | const { el, doc } = getDocument() 25 | const el2 = doc.createElement("div") 26 | const el3 = doc.createElement("div") 27 | 28 | // styles function will add px on some properties automatically 29 | // can be disabled by passing false as third argument 30 | styles(el, { x: 1 }, true) 31 | expect(el.style.transform).toBe("translate3d(1px, 0px, 0px)") 32 | 33 | styles(el, { y: 11 }) 34 | expect(el.style.transform).toBe("translate3d(1px, 11px, 0px)") 35 | 36 | styles(el, { z: "111px" }) 37 | expect(el.style.transform).toBe("translate3d(1px, 11px, 111px)") 38 | 39 | styles(el, { scale: 1, rotate: "1deg" }) 40 | expect(el.style.transform).toBe("translate3d(1px, 11px, 111px) scale(1) rotate(1deg)") 41 | 42 | styles(el, { scale: 1, rotate: 10 }) 43 | expect(el.style.transform).toBe("translate3d(1px, 11px, 111px) scale(1) rotate(10deg)") 44 | 45 | // the second element should not be affected by the first element 46 | // false as third argument to disable auto add px 47 | styles(el2, { x: 2 }, false) 48 | expect(el2.style.transform).toBe("translate3d(2, 0px, 0px)") 49 | 50 | // the third too should not be affected by the others 51 | styles(el3, { x: "10rem", y: "40%", skewX: 0.5 }) 52 | expect(el3.style.transform).toBe("translate3d(10rem, 40%, 0px) skewX(0.5deg)") 53 | }) 54 | 55 | it("should set props of transform CSS properties on DOM element with array", async () => { 56 | const { el } = getDocument() 57 | // translate3d & translateX can't be used together 58 | // This is not right as CSS declaration 59 | // But we should not prevent user to do it 60 | // Use x y z for translate3d and translateX for translateX property 61 | styles(el, { x: 2, translateX: "222px" }) 62 | expect(el.style.transform).toBe("translate3d(2px, 0px, 0px) translateX(222px)") 63 | }) 64 | 65 | it("null should return '' as value", async () => { 66 | const { el } = getDocument() 67 | styles(el, { transformOrigin: "left" }) 68 | expect(el.style.transformOrigin).toBe("left") 69 | styles(el, { transformOrigin: null }) 70 | expect(el.style.transformOrigin).toBe("") 71 | }) 72 | 73 | it("should accept a DOM element array", async () => { 74 | const { el, doc } = getDocument() 75 | const el2 = doc.createElement("div") 76 | const el3 = doc.createElement("div") 77 | const arr = [el, el2, el3] 78 | // this is wrong to set a number without unit on transform, but it's just for testing 79 | styles(arr, { transformOrigin: "center" }) 80 | for (let el of arr) expect(el.style.transformOrigin).toBe("center") 81 | }) 82 | 83 | it("should work with translateX translateY and translateZ", async () => { 84 | const { el, doc } = getDocument() 85 | styles(el, { translateX: 1 }, true) 86 | expect(el.style.transform).toBe("translateX(1px)") 87 | styles(el, { translateY: 1 }, true) 88 | expect(el.style.transform).toBe("translateX(1px) translateY(1px)") 89 | styles(el, { translateZ: 1 }, false) 90 | expect(el.style.transform).toBe("translateX(1px) translateY(1px) translateZ(1)") 91 | 92 | // special case 93 | styles(el, { translateX: "10" }, true) 94 | expect(el.style.transform).toBe("translateX(10) translateY(1px) translateZ(1)") 95 | styles(el, { translateX: 10 }, true) 96 | expect(el.style.transform).toBe("translateX(10px) translateY(1px) translateZ(1)") 97 | }) 98 | }) 99 | -------------------------------------------------------------------------------- /packages/interpol/tests/utils/getDocument.ts: -------------------------------------------------------------------------------- 1 | import { JSDOM } from "jsdom" 2 | 3 | export const getDocument = () => { 4 | const dom = new JSDOM() 5 | const win = dom.window 6 | const doc = win.document 7 | const proxy = { proxyWindow: win, proxyDocument: doc } 8 | const el = doc.createElement("div") 9 | doc.body.append(el) 10 | return { dom, win, doc, proxy, el } 11 | } 12 | -------------------------------------------------------------------------------- /packages/interpol/tests/utils/interpolParamsGenerator.ts: -------------------------------------------------------------------------------- 1 | import { randomRange } from "./randomRange" 2 | 3 | export const interpolParamsGenerator = ({ 4 | from = randomRange(-10000, 10000, 2), 5 | to = randomRange(-10000, 10000, 2), 6 | duration = randomRange(0, 200, 2), 7 | repeat = randomRange(1, 10, 0), 8 | } = {}) => ({ from, to, duration, repeat }) 9 | -------------------------------------------------------------------------------- /packages/interpol/tests/utils/randomRange.ts: -------------------------------------------------------------------------------- 1 | export function randomRange(min: number, max: number, decimal = 0): number { 2 | let rand 3 | 4 | // except the value 0 5 | do rand = Math.random() * (max - min + 1) + min 6 | while (rand === 0) 7 | 8 | const power = Math.pow(10, decimal) 9 | return Math.floor(rand * power) / power 10 | } 11 | -------------------------------------------------------------------------------- /packages/interpol/tests/utils/wait.ts: -------------------------------------------------------------------------------- 1 | export const wait = async (t) => new Promise((r) => setTimeout(r, t)) 2 | -------------------------------------------------------------------------------- /packages/interpol/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "module": "esnext", 6 | "declaration": true, 7 | "outDir": "dist", 8 | }, 9 | "include": ["src/**/*"], 10 | "exclude": ["node_modules", "dist"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/interpol/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup" 2 | import { spawn } from "child_process" 3 | 4 | export default defineConfig({ 5 | entry: { interpol: "src/index.ts" }, 6 | splitting: false, 7 | clean: true, 8 | minify: "terser", 9 | dts: true, 10 | format: ["cjs", "esm"], 11 | name: "interpol", 12 | sourcemap: true, 13 | terserOptions: { 14 | compress: true, 15 | mangle: { 16 | properties: { 17 | regex: /^(#.+)$/, 18 | }, 19 | }, 20 | }, 21 | async onSuccess() { 22 | const process = spawn("npx", ["size-limit"], { shell: true }) 23 | process.stdout.on("data", (data) => console.log(data.toString())) 24 | }, 25 | }) 26 | -------------------------------------------------------------------------------- /packages/interpol/vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from "vite" 3 | import { resolve } from "path" 4 | 5 | export default defineConfig({ 6 | resolve: { 7 | alias: { 8 | "~": resolve(__dirname, "src"), 9 | }, 10 | }, 11 | test: { 12 | testTimeout: 5000, 13 | }, 14 | }) 15 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "examples/*" 3 | - "packages/*" 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "importHelpers": true, 7 | "outDir": "dist", 8 | "strict": false, 9 | "jsx": "preserve", 10 | "declaration": true, 11 | "sourceMap": true, 12 | "resolveJsonModule": true, 13 | "esModuleInterop": true, 14 | "skipLibCheck": true, 15 | "experimentalDecorators": true, 16 | "baseUrl": ".", 17 | "types": ["vite/client"], 18 | "paths": { 19 | "~/*": ["src/*"] 20 | }, 21 | "lib": ["esnext", "dom"] 22 | }, 23 | "exclude": ["node_modules", "dist"], 24 | "include": ["packages", "*.d.ts"] 25 | } 26 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "tasks": { 4 | "dev": { 5 | "cache": false, 6 | "persistent": true 7 | }, 8 | "build": { 9 | "cache": false, 10 | "persistent": true 11 | } 12 | } 13 | } 14 | --------------------------------------------------------------------------------