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 |
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 |
--------------------------------------------------------------------------------