├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── build-js.yml │ └── release.yml ├── .gitignore ├── .gitmessagetpl ├── .gitpod.Dockerfile ├── .gitpod.yml ├── .nvmrc ├── .releaserc.yml ├── .typedoc ├── rehype.ts ├── typedoc.cjs ├── typedoc.css └── typedoc.tsx ├── CHANGELOG.md ├── LICENSE ├── README.md ├── TODO.md ├── docs ├── .nojekyll ├── assets │ ├── custom.css │ ├── highlight.css │ ├── main.js │ ├── search.js │ └── style.css ├── functions │ ├── BatchSpringEasing.html │ ├── CSSSpringEasing.html │ ├── EaseInOut.html │ ├── EaseOut.html │ ├── EaseOutIn.html │ ├── EasingOptions.html │ ├── GenerateSpringFrames.html │ ├── SpringEasing.html │ ├── SpringFrame.html │ ├── SpringInFrame.html │ ├── SpringInOutFrame.html │ ├── SpringOutFrame.html │ ├── SpringOutInFrame.html │ ├── batchInterpolateComplex.html │ ├── batchInterpolateNumber.html │ ├── batchInterpolateSequence.html │ ├── batchInterpolateString.html │ ├── batchInterpolateUsingIndex.html │ ├── getLinearSyntax.html │ ├── getOptimizedPoints.html │ ├── getSpringDuration.html │ ├── getUnit.html │ ├── interpolateComplex.html │ ├── interpolateNumber.html │ ├── interpolateSequence.html │ ├── interpolateString.html │ ├── interpolateUsingIndex.html │ ├── isNumberLike.html │ ├── limit.html │ ├── parseEasingParameters.html │ ├── ramerDouglasPeucker.html │ ├── registerEasingFunction.html │ ├── registerEasingFunctions.html │ ├── scale.html │ ├── squaredSegmentDistance.html │ ├── toAnimationFrames.html │ └── toFixed.html ├── index.html ├── interfaces │ └── IGenericBatchInterpolationFunction.html ├── media │ ├── _headers │ ├── assets │ │ ├── favicon.svg │ │ ├── spring-easing-demo-video.gif │ │ ├── spring-easing-demo-video.mp4 │ │ └── spring-easing-demo-video.webm │ ├── favicon.ico │ ├── fonts │ │ ├── FluentSystemIcons-Regular.ttf │ │ └── FluentSystemIcons-Regular.woff2 │ ├── measure.js │ └── robots.txt ├── modules.html ├── types │ ├── TypeArrayFrameFunctionFormat.html │ ├── TypeCSSEasingOptions.html │ ├── TypeEasingOptions.html │ ├── TypeEasings.html │ ├── TypeFrameFunction.html │ └── TypeInterpolationFunction.html └── variables │ ├── EasingDurationCache.html │ ├── EasingFunctionKeys.html │ ├── EasingFunctions.html │ ├── FramePtsCache.html │ └── INFINITE_LOOP_LIMIT.html ├── dts.tsconfig.json ├── lib ├── batch.d.ts ├── batch.d.ts.map ├── css-linear-easing.d.ts ├── css-linear-easing.d.ts.map ├── index.cjs ├── index.d.ts ├── index.d.ts.map ├── index.js ├── index.mjs ├── mod.d.ts ├── mod.d.ts.map ├── optimize.d.ts ├── optimize.d.ts.map ├── utils.d.ts └── utils.d.ts.map ├── media ├── _headers ├── assets │ ├── favicon.svg │ ├── spring-easing-demo-video.gif │ ├── spring-easing-demo-video.mp4 │ └── spring-easing-demo-video.webm ├── favicon.ico ├── fonts │ ├── FluentSystemIcons-Regular.ttf │ └── FluentSystemIcons-Regular.woff2 ├── measure.js └── robots.txt ├── package.json ├── pnpm-lock.yaml ├── repl.ts ├── src ├── README.md ├── batch.ts ├── css-linear-easing.ts ├── index.ts ├── mod.ts ├── optimize.ts └── utils.ts ├── tests ├── basic.test.ts ├── batch.test.ts ├── css-linear-easing.test.ts ├── fixture │ ├── index.html │ └── main.ts ├── optimize.test.ts └── utils │ ├── color-parse.ts │ ├── color-rgba.ts │ ├── color-space.ts │ ├── colors.ts │ └── interpolate-color.ts ├── tsconfig.json ├── typedoc.json └── vite.config.ts /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.187.0/containers/typescript-node/.devcontainer/base.Dockerfile 2 | 3 | # [Choice] Node.js version: 16, 14, 12 4 | ARG VARIANT="16-buster" 5 | FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT} 6 | 7 | # [Optional] Uncomment this section to install additional OS packages. 8 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 9 | # && apt-get -y install --no-install-recommends 10 | 11 | # [Optional] Uncomment if you want to install an additional version of node using nvm 12 | # ARG EXTRA_NODE_VERSION=10 13 | # RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" 14 | 15 | # [Optional] Uncomment if you want to install more global node packages 16 | # RUN su node -c "npm install -g " 17 | 18 | RUN su node -c "npm install -g pnpm" -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.187.0/containers/typescript-node 3 | { 4 | "name": "Node.js & TypeScript", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | // Update 'VARIANT' to pick a Node version: 12, 14, 16 8 | "args": { 9 | "VARIANT": "16" 10 | } 11 | }, 12 | 13 | // Set *default* container specific settings.json values on container create. 14 | "settings": {}, 15 | 16 | // Add the IDs of extensions you want installed when the container is created. 17 | "extensions": [ 18 | "bierner.jsdoc-markdown-highlighting", 19 | "github.github-vscode-theme", 20 | "shd101wyy.markdown-preview-enhanced" 21 | ], 22 | 23 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 24 | // "forwardPorts": [], 25 | 26 | // Use 'postCreateCommand' to run commands after the container is created. 27 | "postCreateCommand": "pnpm install", 28 | 29 | // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 30 | "remoteUser": "node" 31 | } 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG] An error occurs between pages...on browser..." 5 | labels: bug 6 | assignees: okikio 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | 1. Go to '...' 17 | 2. Click on '....' 18 | 3. Scroll down to '....' 19 | 4. See error 20 | 21 | **Expected behavior** 22 | A clear and concise description of what you expected to happen. 23 | 24 | **Screenshots** 25 | If applicable, add screenshots to help explain your problem. 26 | 27 | **Desktop (please complete the following information):** 28 | 29 | - OS: [e.g. iOS] 30 | - Browser [e.g. chrome, safari] 31 | - Version [e.g. 22] 32 | 33 | **Smartphone (please complete the following information):** 34 | 35 | - Device: [e.g. iPhone6] 36 | - OS: [e.g. iOS8.1] 37 | - Browser [e.g. stock browser, safari] 38 | - Version [e.g. 22] 39 | 40 | **Additional context** 41 | Add any other context about the problem here. 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE] How about ... for the SpringEasing method" 5 | labels: enhancement 6 | assignees: okikio 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/build-js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Build CI 5 | 6 | on: 7 | push: 8 | branches: [main] 9 | paths: 10 | - '.typedoc/**/*' 11 | - '*.ts' 12 | - '*.js' 13 | - '*.json' 14 | - '*.css' 15 | - '*.yaml' 16 | - 'README.md' 17 | - '.github/**/*.yml' 18 | - 'src/**/*' 19 | - 'media/**/*' 20 | - 'tests/**/*' 21 | 22 | jobs: 23 | build: 24 | runs-on: ubuntu-latest 25 | 26 | strategy: 27 | matrix: 28 | node-version: [19.x] 29 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 30 | 31 | steps: 32 | - uses: actions/checkout@v2 33 | with: 34 | persist-credentials: false # otherwise, the token used is the GITHUB_TOKEN, instead of your personal token 35 | fetch-depth: 0 # otherwise, you will failed to push refs to dest repo 36 | 37 | - name: Use Node.js ${{ matrix.node-version }} 38 | uses: actions/setup-node@v2 39 | with: 40 | node-version: ${{ matrix.node-version }} 41 | 42 | - name: Cache .pnpm-store 43 | uses: actions/cache@v1 44 | with: 45 | path: ~/.pnpm-store 46 | key: ${{ runner.os }}-node${{ matrix.node-version }}-${{ hashFiles('**/pnpm-lock.yaml') }} 47 | 48 | - name: Install pnpm 49 | run: curl -f https://get.pnpm.io/v6.js | node - add --global pnpm@6 50 | 51 | - name: Install 52 | run: pnpm install 53 | 54 | - name: Pre-Release 55 | run: pnpm pre-release 56 | 57 | - name: Add lib, types and docs to Git 58 | if: ${{ github.ref == 'refs/heads/main' }} 59 | run: | 60 | git add --force docs/ 61 | git add --force lib/ 62 | 63 | - name: Commit files 64 | if: ${{ github.ref == 'refs/heads/main' }} 65 | run: | 66 | git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" 67 | git config --local user.name "github-actions[bot]" 68 | git commit -m "chore(gh-bot): :rocket: build types, api & library files" -a 69 | 70 | - name: Push changes 71 | if: ${{ github.ref == 'refs/heads/main' }} 72 | uses: ad-m/github-push-action@master 73 | with: 74 | github_token: ${{ secrets.GITHUB_TOKEN }} 75 | branch: ${{ github.ref }} 76 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: workflow_dispatch 3 | 4 | jobs: 5 | release: 6 | name: Release 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [19.x] 12 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | with: 18 | persist-credentials: false # otherwise, the token used is the GITHUB_TOKEN, instead of your personal token 19 | fetch-depth: 0 # otherwise, you will failed to push refs to dest repo 20 | 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v2 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | 26 | - name: Cache .pnpm-store 27 | uses: actions/cache@v1 28 | with: 29 | path: ~/.pnpm-store 30 | key: ${{ runner.os }}-node${{ matrix.node-version }}-${{ hashFiles('**/pnpm-lock.yaml') }} 31 | 32 | - name: Install pnpm 33 | run: curl -f https://get.pnpm.io/v6.js | node - add --global pnpm@6 34 | 35 | - name: Install dependencies 36 | run: pnpm install 37 | 38 | - name: Pre-release 39 | run: pnpm pre-release 40 | 41 | - name: Release 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 44 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 45 | run: pnpm semantic-release 46 | 47 | - name: Add lib, types and docs to Git 48 | if: ${{ github.ref == 'refs/heads/main' }} 49 | run: | 50 | git add --force docs/ 51 | git add --force lib/ 52 | 53 | - name: Commit files 54 | if: ${{ github.ref == 'refs/heads/main' }} 55 | run: | 56 | git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" 57 | git config --local user.name "github-actions[bot]" 58 | git commit -m "chore(gh-bot): :rocket: release" -a 59 | 60 | - name: Push changes 61 | if: ${{ github.ref == 'refs/heads/main' }} 62 | uses: ad-m/github-push-action@master 63 | with: 64 | github_token: ${{ secrets.GH_TOKEN }} 65 | branch: ${{ github.ref }} 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # Project specific files 107 | lib/ 108 | docs/ 109 | @types/ 110 | -------------------------------------------------------------------------------- /.gitmessagetpl: -------------------------------------------------------------------------------- 1 | # Full spec: https://www.conventionalcommits.org/en/v1.0.0 2 | # 3 | # TYPE 4 | # ====================================================================== 5 | # Can be one of: feat, fix, chore, build, ci, docs, tests, etc... 6 | # 7 | # - a commit of the type fix patches a bug in your codebase, 8 | # this correlates with PATCH in Semantic Versioning. 9 | # 10 | # - a commit of the type feat introduces a new feature to the codebase, 11 | # this correlates with MINOR in Semantic Versioning. 12 | # 13 | # - types other than fix and feat are allowed, additional types are 14 | # not mandated by the Conventional Commits specification, and have 15 | # no implicit effect in Semantic Versioning 16 | # 17 | # SCOPE 18 | # ====================================================================== 19 | # A scope MUST consist of a noun describing a section of the codebase 20 | # surrounded by parenthesis, e.g., fix(parser): 21 | # 22 | # FOOTERS 23 | # ====================================================================== 24 | # Footers must also be separated by a blank line 25 | # from the body & be on consecutive lines. 26 | # 27 | # A commit that has a footer BREAKING CHANGE:, or appends a ! after the 28 | # type/scope, introduces a breaking API change (correlating with MAJOR 29 | # in Semantic Versioning). 30 | # 31 | # A BREAKING CHANGE can be part of commits of any type. 32 | # 33 | # Footers other than BREAKING CHANGE may be provided and follow a 34 | # convention similar to the git trailer format. 35 | # see: https://git-scm.com/docs/git-interpret-trailers 36 | # 37 | # LINE LENGTH 38 | # ====================================================================== 39 | # |<----- preferably using up to 50 chars ------>|<- no more than 72 ->| 40 | [(optional scope)][!]: 41 | [optional body] 42 | 43 | [optional footer(s)] -------------------------------------------------------------------------------- /.gitpod.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gitpod/workspace-full:latest 2 | 3 | # Install custom tools, runtime, etc. using apt-get 4 | # For example, the command below would install "bastet" - a command line tetris clone: 5 | # 6 | # RUN sudo apt-get -q update && # sudo apt-get install -yq bastet && # sudo rm -rf /var/lib/apt/lists/* 7 | # 8 | # More information: https://www.gitpod.io/docs/config-docker/ -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | image: 2 | file: .gitpod.Dockerfile 3 | 4 | # List the ports you want to expose and what to do when they are served. See https://www.gitpod.io/docs/43_config_ports/ 5 | ports: 6 | - port: 3000 7 | onOpen: open-preview 8 | - port: 3001 9 | onOpen: ignore 10 | 11 | github: 12 | prebuilds: 13 | # enable for the master/default branch (defaults to true) 14 | master: true 15 | # enable for all branches in this repo (defaults to false) 16 | branches: true 17 | # enable for pull requests coming from this repo (defaults to true) 18 | pullRequests: true 19 | # enable for pull requests coming from forks (defaults to false) 20 | pullRequestsFromForks: true 21 | # add a "Review in Gitpod" button as a comment to pull requests (defaults to true) 22 | addComment: true 23 | # add a "Review in Gitpod" button to pull requests (defaults to false) 24 | addBadge: false 25 | # add a label once the prebuild is ready to pull requests (defaults to false) 26 | addLabel: prebuilt-in-gitpod 27 | 28 | # List the start up tasks. You can start them in parallel in multiple terminals. See https://www.gitpod.io/docs/44_config_start_tasks/ 29 | tasks: 30 | - init: > 31 | npm install -g pnpm && 32 | pnpm install 33 | command: > 34 | npm install -g pnpm && 35 | pnpm build -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 19 -------------------------------------------------------------------------------- /.releaserc.yml: -------------------------------------------------------------------------------- 1 | # spec: https://semantic-release.gitbook.io/semantic-release/usage/configuration 2 | 3 | branches: ['main'] 4 | plugins: 5 | # Determine the type of release by analyzing commits. 6 | # ie: Major, Minor or Patch 7 | - - "@semantic-release/commit-analyzer" 8 | - preset: conventionalcommits 9 | releaseRules: 10 | - { breaking: true, release: major } 11 | - { revert: true, release: patch } 12 | - { type: feat, release: minor } 13 | - { type: fix, release: patch } 14 | - { type: perf, release: patch } 15 | - { type: docs, release: patch } 16 | - { type: refactor, release: patch } 17 | - { type: style, release: patch } 18 | - { type: build, release: patch } 19 | - { type: ci, release: patch } 20 | - { type: test, release: patch } 21 | - { type: update, release: patch } 22 | 23 | # Generate CHANGELOG.md 24 | - - "@semantic-release/release-notes-generator" 25 | - preset: conventionalcommits 26 | presetConfig: 27 | # spec: https://github.com/conventional-changelog/conventional-changelog-config-spec/tree/master/versions/2.1.0 28 | types: 29 | - { type: feat, section: "Features" } 30 | - { type: fix, section: "Bug Fixes" } 31 | - { type: chore, section: "Misc" } 32 | - { type: docs, section: "Misc" } 33 | - { type: style, section: "Improvements" } 34 | - { type: refactor, section: "Improvements" } 35 | - { type: perf, section: "Improvements" } 36 | - { type: test, section: "Automation" } 37 | - { type: ci, section: "Automation" } 38 | - { type: build, section: "Automation" } 39 | - { type: update, section: "Automation" } 40 | 41 | - "@semantic-release/changelog" 42 | 43 | # Commit CHANGELOG.md back to repo 44 | - - "@semantic-release/git" 45 | - assets: ["CHANGELOG.md", "lib/**", "src/**"] 46 | message: "chore(release): update changelog ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" 47 | 48 | # Create new github release 49 | - "@semantic-release/github" 50 | 51 | # Create new npm release 52 | - "@semantic-release/npm" -------------------------------------------------------------------------------- /.typedoc/rehype.ts: -------------------------------------------------------------------------------- 1 | import glob from "fast-glob"; 2 | import { unified } from "unified"; 3 | 4 | import fs from "node:fs/promises"; 5 | import path from "node:path"; 6 | 7 | import rehypeParse from "rehype-parse"; 8 | import rehypeStringify from "rehype-stringify"; 9 | 10 | import { h, s } from "hastscript"; 11 | 12 | export function redirectURLs(url) { 13 | if (/^\/docs/.test(url.path)) { 14 | return url.path.replace(/^\/docs\//, "/").replace(/\.md$/, ""); 15 | } else if (/\.md$/.test(url.path)) { 16 | return url.path.replace(/\.md$/, ""); 17 | } else if (/LICENSE$/i.test(url.path)) { 18 | return "https://github.com/okikio/spring-easing/tree/main/LICENSE"; 19 | } 20 | } 21 | 22 | async function importPlugin(p) { 23 | if (typeof p === "string") 24 | return await import(p); 25 | 26 | return await p; 27 | } 28 | 29 | export function loadPlugins(items) { 30 | return items.map((p) => { 31 | return new Promise((resolve, reject) => { 32 | if (Array.isArray(p)) { 33 | const [plugin, opts] = p; 34 | return importPlugin(plugin) 35 | .then((m) => resolve([m.default, opts])) 36 | .catch((e) => reject(e)); 37 | } 38 | 39 | return importPlugin(p) 40 | .then((m) => resolve([m.default])) 41 | .catch((e) => reject(e)); 42 | }); 43 | }); 44 | } 45 | 46 | const __dirname = path.resolve(path.dirname("")); 47 | (async () => { 48 | const plugins = [ 49 | ["rehype-slug"], 50 | ["rehype-urls", redirectURLs], 51 | ["rehype-accessible-emojis"], 52 | ["rehype-external-links", { 53 | target: "_blank", 54 | rel: ["noopener"], 55 | content: [ 56 | // Based on the external icon from https://www.gitpod.io/blog/workspace-networking 57 | h("span.external-icon", [ 58 | s("svg", { 59 | preserveAspectRatio: "xMidYMid meet", 60 | width: "1.2em", 61 | height: "1.2em", 62 | viewBox: "0 0 24 24", 63 | }, [ 64 | s("path", { 65 | d: "M10.75 3a.75.75 0 0 0 0 1.5h7.67L3.22 19.7a.764.764 0 1 0 1.081 1.081l15.2-15.2v7.669a.75.75 0 0 0 1.5 0v-9.5a.75.75 0 0 0-.75-.75h-9.5Z", 66 | }), 67 | ]), 68 | ]), 69 | // `` 70 | ], 71 | }], 72 | ]; 73 | 74 | let parser = unified() 75 | .use(rehypeParse) 76 | .use(rehypeStringify); 77 | 78 | const loadedRehypePlugins = await Promise.all(loadPlugins(plugins)); 79 | loadedRehypePlugins.forEach(([plugin, opts]) => { 80 | parser.use(plugin, opts); 81 | }); 82 | 83 | const paths = await glob("docs/**/*.html"); 84 | let result; 85 | try { 86 | paths.forEach((p) => { 87 | (async () => { 88 | const currentPath = path.join(__dirname, p); 89 | const content = await fs.readFile(currentPath); 90 | const vfile = await parser 91 | .process(content.toString()); 92 | result = vfile.toString(); 93 | await fs.writeFile(currentPath, result, "utf-8"); 94 | })(); 95 | }); 96 | } catch (err) { 97 | throw err; 98 | } 99 | })(); 100 | -------------------------------------------------------------------------------- /.typedoc/typedoc.cjs: -------------------------------------------------------------------------------- 1 | var __defProp = Object.defineProperty; 2 | var __getOwnPropDesc = Object.getOwnPropertyDescriptor; 3 | var __getOwnPropNames = Object.getOwnPropertyNames; 4 | var __hasOwnProp = Object.prototype.hasOwnProperty; 5 | var __export = (target, all) => { 6 | for (var name in all) 7 | __defProp(target, name, { get: all[name], enumerable: true }); 8 | }; 9 | var __copyProps = (to, from, except, desc) => { 10 | if (from && typeof from === "object" || typeof from === "function") { 11 | for (let key of __getOwnPropNames(from)) 12 | if (!__hasOwnProp.call(to, key) && key !== except) 13 | __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); 14 | } 15 | return to; 16 | }; 17 | var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); 18 | var typedoc_exports = {}; 19 | __export(typedoc_exports, { 20 | load: () => load 21 | }); 22 | module.exports = __toCommonJS(typedoc_exports); 23 | var import_typedoc = require("typedoc"); 24 | function load(app) { 25 | app.options.addDeclaration({ 26 | name: "umami-id", 27 | help: "The id you receive from umami analytics.", 28 | type: import_typedoc.ParameterType.String, 29 | // The default 30 | defaultValue: "" 31 | // The default 32 | }); 33 | app.options.addDeclaration({ 34 | name: "umami-src", 35 | help: "The website source for umami analytics.", 36 | type: import_typedoc.ParameterType.String, 37 | // The default 38 | defaultValue: "/media/measure.js" 39 | // The default 40 | }); 41 | app.options.addDeclaration({ 42 | name: "keywords", 43 | type: import_typedoc.ParameterType.Array, 44 | help: "Website keywords", 45 | defaultValue: [ 46 | "spring easing", 47 | "spring animation", 48 | "custom easing", 49 | "framework agnostic", 50 | "gsap", 51 | "animejs" 52 | ] 53 | }); 54 | app.renderer.hooks.on("head.begin", (ctx) => { 55 | const keywords = ctx.options.getValue("keywords"); 56 | const id = ctx.options.getValue("umami-id"); 57 | const src = ctx.options.getValue("umami-src"); 58 | return /* @__PURE__ */ import_typedoc.JSX.createElement(import_typedoc.JSX.Fragment, null, /* @__PURE__ */ import_typedoc.JSX.createElement("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }), /* @__PURE__ */ import_typedoc.JSX.createElement("link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: true }), /* @__PURE__ */ import_typedoc.JSX.createElement( 59 | "link", 60 | { 61 | href: "https://fonts.googleapis.com/css2?family=Lexend:wght@300;400;500;700&display=swap", 62 | rel: "stylesheet" 63 | } 64 | ), /* @__PURE__ */ import_typedoc.JSX.createElement("meta", { name: "keyword", content: keywords.join(", ") }), /* @__PURE__ */ import_typedoc.JSX.createElement("meta", { name: "color-scheme", content: "dark light" }), /* @__PURE__ */ import_typedoc.JSX.createElement("link", { rel: "shortcut icon", href: "/media/favicon.ico" }), /* @__PURE__ */ import_typedoc.JSX.createElement( 65 | "link", 66 | { 67 | rel: "icon", 68 | type: "image/svg+xml", 69 | href: "/media/assets/favicon.svg" 70 | } 71 | ), /* @__PURE__ */ import_typedoc.JSX.createElement("meta", { name: "web-author", content: "Okiki Ojo" }), /* @__PURE__ */ import_typedoc.JSX.createElement("meta", { name: "robots", content: "index, follow" }), /* @__PURE__ */ import_typedoc.JSX.createElement("meta", { name: "twitter:url", content: "https://spring-easing.okikio.dev/" }), /* @__PURE__ */ import_typedoc.JSX.createElement("meta", { name: "twitter:site", content: "@okikio_dev" }), /* @__PURE__ */ import_typedoc.JSX.createElement("meta", { name: "twitter:creator", content: "@okikio_dev" }), /* @__PURE__ */ import_typedoc.JSX.createElement("link", { href: "https://twitter.com/okikio_dev", rel: "me" }), /* @__PURE__ */ import_typedoc.JSX.createElement( 72 | "link", 73 | { 74 | rel: "webmention", 75 | href: "https://webmention.io/spring-easing.okikio.dev/webmention" 76 | } 77 | ), /* @__PURE__ */ import_typedoc.JSX.createElement( 78 | "link", 79 | { 80 | rel: "pingback", 81 | href: "https://webmention.io/spring-easing.okikio.dev/xmlrpc" 82 | } 83 | ), /* @__PURE__ */ import_typedoc.JSX.createElement( 84 | "link", 85 | { 86 | rel: "pingback", 87 | href: "https://webmention.io/webmention?forward=https://spring-easing.okikio.dev/endpoint" 88 | } 89 | ), ctx.options.isSet("umami-id") && /* @__PURE__ */ import_typedoc.JSX.createElement( 90 | "script", 91 | { 92 | async: true, 93 | defer: true, 94 | type: "module", 95 | "data-host-url": "https://bundlejs.com", 96 | "data-domains": "spring-easing.okikio.dev,okikio.dev", 97 | "data-website-id": id, 98 | src 99 | } 100 | )); 101 | }); 102 | } 103 | -------------------------------------------------------------------------------- /.typedoc/typedoc.css: -------------------------------------------------------------------------------- 1 | /* @import url("https://fonts.googleapis.com/css2?family=Lexend:wght@300;400;500;700&display=swap"); */ 2 | 3 | @font-face { 4 | font-family: "FluentSystemIcons-Regular"; 5 | src: 6 | url("/media/fonts/FluentSystemIcons-Regular.woff2") format("woff2"), 7 | url("/media/fonts/FluentSystemIcons-Regular.ttf") format("truetype"); 8 | } 9 | 10 | :root { 11 | --light-color-background-secondary: #fff; 12 | --dark-color-background: #1b1c1f; 13 | --dark-color-background-secondary: #36393f; 14 | 15 | --light-color-panel-divider: #dadce0; 16 | --dark-color-panel-divider: #47474d; 17 | 18 | --color-panel-divider: var(--light-color-panel-divider); 19 | --dark-color-text-aside: #8486b4; 20 | /* --light-color-background: #f8f9fa; 21 | --light-color-panel-divider: #dadce0; 22 | --light-link-color: #1155cc; 23 | --dark-color-panel-divider: #47474d; 24 | --dark-color-menu-divider-focus: #d73a49; 25 | --dark-color-background: #1b1c1f; 26 | --dark-color-secondary-background: #2d2f34; */ 27 | } 28 | 29 | @media (prefers-color-scheme: light) { 30 | :root { 31 | --light-color-background-secondary: #fff; 32 | --light-color-panel-divider: #dadce0; 33 | --color-panel-divider: var(--light-color-panel-divider); 34 | } 35 | } 36 | 37 | @media (prefers-color-scheme: dark) { 38 | :root { 39 | --dark-color-background: #1b1c1f; 40 | --dark-color-background-secondary: #36393f; 41 | --dark-color-panel-divider: #47474d; 42 | --color-panel-divider: var(--dark-color-panel-divider); 43 | --dark-color-text-aside: #8486b4; 44 | } 45 | } 46 | 47 | html, 48 | body { 49 | font-family: "Lexend", "Manrope", "Century Gothic", sans-serif; 50 | font-weight: 400; 51 | } 52 | 53 | h1 { 54 | font-weight: 300; 55 | padding-bottom: 0.9rem; 56 | } 57 | 58 | h2 { 59 | font-weight: 400; 60 | color: var(--color-ts); 61 | } 62 | 63 | h3 { 64 | font-weight: 500; 65 | } 66 | 67 | h4 { 68 | font-weight: 500; 69 | } 70 | 71 | a :is(h1, h2, h3, h4, h5, h6):after { 72 | font-family: FluentSystemIcons-Regular !important; 73 | font-style: normal; 74 | font-weight: normal !important; 75 | font-variant: normal; 76 | text-transform: none; 77 | line-height: 1; 78 | -webkit-font-smoothing: antialiased; 79 | -moz-osx-font-smoothing: grayscale; 80 | content: "\f4e5"; 81 | 82 | visibility: hidden; 83 | font-weight: 800; 84 | font-size: 1em; 85 | padding-left: 0.25em; 86 | vertical-align: middle; 87 | color: var(--color-link); 88 | } 89 | 90 | a :is(h1, h2, h3, h4, h5, h6):hover:after { 91 | visibility: visible; 92 | } 93 | 94 | a :is(h1, h2, h3, h4, h5, h6):is(:hover, :focus):after { 95 | visibility: visible; 96 | } 97 | 98 | a .external-icon svg { 99 | vertical-align: middle; 100 | display: inline-block; 101 | stroke-width: 2; 102 | opacity: .8; 103 | width: 1em; 104 | height: 1em; 105 | fill: currentColor; 106 | } 107 | 108 | details { 109 | cursor: pointer; 110 | } 111 | 112 | pre { 113 | padding: 14px; 114 | } 115 | 116 | pre, 117 | code { 118 | border-radius: 6px; 119 | border: 1px solid rgb(20 20 20 / 0.25); 120 | } 121 | 122 | pre code { 123 | border: none; 124 | } 125 | 126 | blockquote { 127 | padding: 0.2em 0.8em 0.2em 1em; 128 | border-left: 5px solid gray; 129 | margin-bottom: 2.5rem; 130 | background-color: var(--color-background-secondary); 131 | border-radius: 0.45em; 132 | } 133 | 134 | a[href^="https://bundlejs.com/"] .external-icon { 135 | display: none; 136 | } 137 | 138 | img[src^="https://bundlejs.com/"] { 139 | overflow: hidden; 140 | border-radius: 20em; 141 | border: 1px solid rgb(20 20 20 / 0.25); 142 | } 143 | 144 | .container { 145 | max-width: 1200px; 146 | } 147 | 148 | .container.tsd-generator { 149 | max-width: 100%; 150 | } 151 | 152 | .container.tsd-generator p { 153 | line-height: 28px; 154 | max-width: 1200px; 155 | width: 100%; 156 | margin: auto; 157 | } 158 | 159 | header.tsd-page-toolbar { 160 | border-bottom: 1px solid var(--color-panel-divider); 161 | } 162 | 163 | .tsd-signature { 164 | border-radius: 0.5rem; 165 | } 166 | 167 | .tsd-breadcrumb { 168 | padding-block-start: 0.5rem; 169 | } 170 | 171 | .tsd-typography p, .tsd-typography ul, .tsd-typography ol { 172 | line-height: 1.85em; 173 | font-weight: 300; 174 | } 175 | .tsd-navigation { 176 | padding-inline-end: 0.25rem; 177 | } 178 | 179 | .tsd-accordion-details { 180 | padding-inline-start: 1.35rem; 181 | } 182 | 183 | .tsd-accordion-details > ul > li > ul { 184 | padding-left: 0; 185 | } 186 | 187 | .container-main { 188 | --mask-image: linear-gradient(0deg, transparent 0%, white 2%, white 50%, white 98%, transparent 100%); 189 | } 190 | 191 | @media (min-width: 769px) { 192 | .container-main { 193 | grid-template-columns: minmax(0, 15rem) minmax(0, 2.5fr); 194 | } 195 | 196 | .col-sidebar { 197 | border-right: 1px solid var(--color-panel-divider); 198 | mask-image: var(--mask-image); 199 | -webkit-mask-image: var(--mask-image); 200 | padding-block: 1rem; 201 | } 202 | } 203 | 204 | @media (min-width: 1200px) { 205 | .container-main { 206 | grid-template-columns: minmax(0, 1.15fr) minmax(0, 2.5fr) minmax(0, 15.5rem); 207 | } 208 | 209 | .page-menu { 210 | border-left: 1px solid var(--color-panel-divider); 211 | } 212 | 213 | .site-menu { 214 | border-right: 1px solid var(--color-panel-divider); 215 | mask-image: var(--mask-image); 216 | -webkit-mask-image: var(--mask-image); 217 | padding-block: 1rem; 218 | } 219 | } 220 | 221 | .tsd-accordion-details ul { 222 | padding-left: 1rem; 223 | } 224 | 225 | .tsd-accordion-details > ul { 226 | padding-left: 0; 227 | } 228 | 229 | :is(.tsd-page-navigation, .tsd-navigation) svg { 230 | vertical-align: middle; 231 | } 232 | 233 | .tsd-navigation a.current, .tsd-page-navigation a.current { 234 | border-radius: 0.45rem; 235 | padding-block: 0.3rem; 236 | padding-inline: 0.3rem; 237 | } 238 | 239 | .col-menu { 240 | border-left: 1px solid var(--color-panel-divider); 241 | } 242 | 243 | .tsd-widget:is(.search, .options, .menu) svg { 244 | display: none; 245 | } 246 | 247 | .tsd-widget:is(.search, .options, .menu):after { 248 | font-family: FluentSystemIcons-Regular !important; 249 | font-style: normal; 250 | font-weight: normal !important; 251 | font-variant: normal; 252 | text-transform: none; 253 | line-height: 1; 254 | -webkit-font-smoothing: antialiased; 255 | -moz-osx-font-smoothing: grayscale; 256 | 257 | font-size: 24px; 258 | color: var(--color-toolbar-text); 259 | width: 100%; 260 | height: 100%; 261 | 262 | position: absolute; 263 | text-align: center; 264 | display: flex; 265 | top: 0; 266 | left: 0; 267 | align-items: center; 268 | justify-content: center; 269 | } 270 | 271 | .tsd-widget.search:after { 272 | content: "\f690"; 273 | } 274 | 275 | .tsd-widget.options:after { 276 | content: "\f407"; 277 | } 278 | 279 | .tsd-widget.menu { 280 | position: relative; 281 | height: 40px; 282 | vertical-align: bottom; 283 | } 284 | 285 | .tsd-widget.menu:after { 286 | content: "\f561"; 287 | } -------------------------------------------------------------------------------- /.typedoc/typedoc.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxFactory JSX.createElement */ 2 | /** @jsxFragmentFactory JSX.Fragment */ 3 | import { Application, ParameterType, JSX } from "typedoc"; 4 | 5 | export function load(app: Application) { 6 | app.options.addDeclaration({ 7 | name: "umami-id", 8 | help: "The id you receive from umami analytics.", 9 | type: ParameterType.String, // The default 10 | defaultValue: "", // The default 11 | }); 12 | 13 | app.options.addDeclaration({ 14 | name: "umami-src", 15 | help: "The website source for umami analytics.", 16 | type: ParameterType.String, // The default 17 | defaultValue: "/media/measure.js", // The default 18 | }); 19 | 20 | app.options.addDeclaration({ 21 | name: "keywords", 22 | type: ParameterType.Array, 23 | help: "Website keywords", 24 | defaultValue: [ 25 | "spring easing", 26 | "spring animation", 27 | "custom easing", 28 | "framework agnostic", 29 | "gsap", 30 | "animejs", 31 | ], 32 | }); 33 | 34 | app.renderer.hooks.on("head.begin", (ctx) => { 35 | const keywords = ctx.options.getValue("keywords") as string[]; 36 | const id = ctx.options.getValue("umami-id") as string; 37 | const src = ctx.options.getValue("umami-src") as string; 38 | 39 | return ( 40 | <> 41 | 42 | 43 | 47 | 48 | 49 | 50 | 51 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 69 | 73 | 77 | 78 | {ctx.options.isSet("umami-id") && ( 79 | 88 | )} 89 | 90 | ); 91 | }); 92 | } 93 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.3.3](https://github.com/okikio/spring-easing/compare/v2.3.2...v2.3.3) (2023-05-19) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * **docs:** fix spring-easing deno link ([2d6b527](https://github.com/okikio/spring-easing/commit/2d6b52780bec5d822bf8e4d24f64bcc046ac5fa8)) 7 | 8 | 9 | ### Misc 10 | 11 | * **gh-bot:** :rocket: build types, api & library files ([fe1cdce](https://github.com/okikio/spring-easing/commit/fe1cdce920bdbd91827f94fc9bbe0db9e180beac)) 12 | * **gh-bot:** :rocket: release ([9904633](https://github.com/okikio/spring-easing/commit/99046331759a8b69f4e710523d0047b7289dfd50)) 13 | 14 | ## [2.3.2](https://github.com/okikio/spring-easing/compare/v2.3.1...v2.3.2) (2023-05-19) 15 | 16 | 17 | ### Bug Fixes 18 | 19 | * publish changes ([32395a4](https://github.com/okikio/spring-easing/commit/32395a462f48d359304344087a37c391b30b03ed)) 20 | 21 | 22 | ### Misc 23 | 24 | * add links to deno [skip ci] ([e92c170](https://github.com/okikio/spring-easing/commit/e92c170f6ba099a51e6485f8175d2582af0fe393)) 25 | * fix broken links & add CSSSpringEasing to demo [skip ci] ([02eb310](https://github.com/okikio/spring-easing/commit/02eb3105a22133df7b1a1dc422313fc28669f13b)) 26 | * fix format issues in package.json ([bcd3581](https://github.com/okikio/spring-easing/commit/bcd35817ea0219f2acdbb8bf4f6bf526c3d627b4)) 27 | * **gh-bot:** :rocket: build types, api & library files ([d252bec](https://github.com/okikio/spring-easing/commit/d252bec9790117c151e7043aa02889f06d577610)) 28 | * **gh-bot:** :rocket: build types, api & library files ([9e4713f](https://github.com/okikio/spring-easing/commit/9e4713fe6169b48bb8dacacba01194e2eb367e62)) 29 | * **gh-bot:** :rocket: release ([0af56b5](https://github.com/okikio/spring-easing/commit/0af56b5948e3f850d3b4e0b9832501e47b101482)) 30 | * npm keywords should be lowercase ([b053491](https://github.com/okikio/spring-easing/commit/b05349132d769a96920ed326a6c5f3d4cff01b8d)) 31 | * update deps ([0d31b4f](https://github.com/okikio/spring-easing/commit/0d31b4fc3b333e15bb5588669f21adcb84e5f334)) 32 | 33 | ## [2.3.1](https://github.com/okikio/spring-easing/compare/v2.3.0...v2.3.1) (2023-05-14) 34 | 35 | 36 | ### Bug Fixes 37 | 38 | * deno not showing docs ([6d260c4](https://github.com/okikio/spring-easing/commit/6d260c4b298e49829d2026eb00a1ffc6ce8ca541)) 39 | * git syslink issue with src/README.md [skip ci] ([42df16d](https://github.com/okikio/spring-easing/commit/42df16d8881d914d9403c6a38938a289f932aff9)) 40 | 41 | # [2.3.0](https://github.com/okikio/spring-easing/compare/v2.2.0...v2.3.0) (2023-05-14) 42 | 43 | 44 | ### Features 45 | 46 | * add BatchSpringEasing & it's interpolation functions ([f14b603](https://github.com/okikio/spring-easing/commit/f14b603a7ced20c719c3c52d040b3d55e80aa0ea)) 47 | 48 | # [2.2.0](https://github.com/okikio/spring-easing/compare/v2.1.2...v2.2.0) (2023-05-13) 49 | 50 | 51 | ### Features 52 | 53 | * add css spring easing and linear easing function support ([ac761fe](https://github.com/okikio/spring-easing/commit/ac761fe20c0878f3ba1966f93f816f039ff08a62)) 54 | 55 | ## [2.1.2](https://github.com/okikio/spring-easing/compare/v2.1.1...v2.1.2) (2023-03-29) 56 | 57 | 58 | ### Bug Fixes 59 | 60 | * skypack issues ([5f239d3](https://github.com/okikio/spring-easing/commit/5f239d34b3ada9d44d0db394192ab6b2cf925a17)) 61 | 62 | ## [2.1.1](https://github.com/okikio/spring-easing/compare/v2.1.0...v2.1.1) (2023-03-20) 63 | 64 | 65 | ### Bug Fixes 66 | 67 | * ... ([5429f07](https://github.com/okikio/spring-easing/commit/5429f07d0d0a14a31af287b8e6bd1479249183f1)) 68 | 69 | 70 | ### Performance Improvements 71 | 72 | * optimize perf. w/ help from [@jakearchibald](https://github.com/jakearchibald) ([1f3fc56](https://github.com/okikio/spring-easing/commit/1f3fc56ee45e8d8f9dd39c7f2fa01673c0794750)) 73 | 74 | 75 | ### Reverts 76 | 77 | * back to old interpolation syntax, as the new batch syntax causes confusion ([502bbd0](https://github.com/okikio/spring-easing/commit/502bbd0b3caa4110664a89d8163ad968253c6290)) 78 | 79 | # [2.1.0](https://github.com/okikio/spring-easing/compare/v2.0.0...v2.1.0) (2022-09-18) 80 | 81 | 82 | ### Features 83 | 84 | * re-add instant interpolation functions ([9c52a1b](https://github.com/okikio/spring-easing/commit/9c52a1bbb0dbb1625cd8bfea46cc583d33eba59b)) 85 | 86 | # [2.0.0](https://github.com/okikio/spring-easing/compare/v1.2.0...v2.0.0) (2022-09-09) 87 | 88 | 89 | * feat!: add `toAnimationFrames` function ([517d07e](https://github.com/okikio/spring-easing/commit/517d07efd9c7a519591f59203beae6083291db2d)), closes [#4](https://github.com/okikio/spring-easing/issues/4) 90 | 91 | 92 | ### BREAKING CHANGES 93 | 94 | * new interpolation function syntax will break the current custom interpolation functions 95 | 96 | # [1.2.0](https://github.com/okikio/spring-easing/compare/v1.1.1...v1.2.0) (2022-09-05) 97 | 98 | 99 | ### Bug Fixes 100 | 101 | * bugs ([7c2e24a](https://github.com/okikio/spring-easing/commit/7c2e24a4f16f41b24ed2f201de38b8b64a66d0a7)) 102 | 103 | 104 | ### Features 105 | 106 | * add registerEasingFunction(s) function ([e97cb1d](https://github.com/okikio/spring-easing/commit/e97cb1d838f01678d86ea6bf372eaf75137446b9)) 107 | 108 | 109 | ### Performance Improvements 110 | 111 | * optimize interploation functions ([7787db5](https://github.com/okikio/spring-easing/commit/7787db509b0cf94b25ed611f23b5f0bb3298cb00)) 112 | 113 | ## [1.1.1](https://github.com/okikio/spring-easing/compare/v1.1.0...v1.1.1) (2022-06-02) 114 | 115 | 116 | ### Performance Improvements 117 | 118 | * faster interpolation for t=0 or 1 ([efbdab9](https://github.com/okikio/spring-easing/commit/efbdab9479bff49480b14909c5ff25be67c0cb8a)), closes [#3](https://github.com/okikio/spring-easing/issues/3) 119 | 120 | # [1.1.0](https://github.com/okikio/spring-easing/compare/v1.0.0...v1.1.0) (2022-02-23) 121 | 122 | 123 | ### Bug Fixes 124 | 125 | * broken icons ([c36509d](https://github.com/okikio/spring-easing/commit/c36509d91dec13d34bacfaa01e58ffa6ecbdefbc)) 126 | 127 | 128 | ### Features 129 | 130 | * add string interpolation & custom interpolation functions ([d13f252](https://github.com/okikio/spring-easing/commit/d13f252f9a9e0b699ea762c0f90a2e042a1e57db)), closes [#4f4](https://github.com/okikio/spring-easing/issues/4f4) 131 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Okiki Ojo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # Todo 2 | 3 | - [x] Maintain the project 4 | - [x] Create better documentation 5 | - [x] Research browser compatibility 6 | - [x] Create better tests 7 | - [x] Enhance build tool support 8 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /docs/assets/custom.css: -------------------------------------------------------------------------------- 1 | /* @import url("https://fonts.googleapis.com/css2?family=Lexend:wght@300;400;500;700&display=swap"); */ 2 | 3 | @font-face { 4 | font-family: "FluentSystemIcons-Regular"; 5 | src: 6 | url("/media/fonts/FluentSystemIcons-Regular.woff2") format("woff2"), 7 | url("/media/fonts/FluentSystemIcons-Regular.ttf") format("truetype"); 8 | } 9 | 10 | :root { 11 | --light-color-background-secondary: #fff; 12 | --dark-color-background: #1b1c1f; 13 | --dark-color-background-secondary: #36393f; 14 | 15 | --light-color-panel-divider: #dadce0; 16 | --dark-color-panel-divider: #47474d; 17 | 18 | --color-panel-divider: var(--light-color-panel-divider); 19 | --dark-color-text-aside: #8486b4; 20 | /* --light-color-background: #f8f9fa; 21 | --light-color-panel-divider: #dadce0; 22 | --light-link-color: #1155cc; 23 | --dark-color-panel-divider: #47474d; 24 | --dark-color-menu-divider-focus: #d73a49; 25 | --dark-color-background: #1b1c1f; 26 | --dark-color-secondary-background: #2d2f34; */ 27 | } 28 | 29 | @media (prefers-color-scheme: light) { 30 | :root { 31 | --light-color-background-secondary: #fff; 32 | --light-color-panel-divider: #dadce0; 33 | --color-panel-divider: var(--light-color-panel-divider); 34 | } 35 | } 36 | 37 | @media (prefers-color-scheme: dark) { 38 | :root { 39 | --dark-color-background: #1b1c1f; 40 | --dark-color-background-secondary: #36393f; 41 | --dark-color-panel-divider: #47474d; 42 | --color-panel-divider: var(--dark-color-panel-divider); 43 | --dark-color-text-aside: #8486b4; 44 | } 45 | } 46 | 47 | html, 48 | body { 49 | font-family: "Lexend", "Manrope", "Century Gothic", sans-serif; 50 | font-weight: 400; 51 | } 52 | 53 | h1 { 54 | font-weight: 300; 55 | padding-bottom: 0.9rem; 56 | } 57 | 58 | h2 { 59 | font-weight: 400; 60 | color: var(--color-ts); 61 | } 62 | 63 | h3 { 64 | font-weight: 500; 65 | } 66 | 67 | h4 { 68 | font-weight: 500; 69 | } 70 | 71 | a :is(h1, h2, h3, h4, h5, h6):after { 72 | font-family: FluentSystemIcons-Regular !important; 73 | font-style: normal; 74 | font-weight: normal !important; 75 | font-variant: normal; 76 | text-transform: none; 77 | line-height: 1; 78 | -webkit-font-smoothing: antialiased; 79 | -moz-osx-font-smoothing: grayscale; 80 | content: "\f4e5"; 81 | 82 | visibility: hidden; 83 | font-weight: 800; 84 | font-size: 1em; 85 | padding-left: 0.25em; 86 | vertical-align: middle; 87 | color: var(--color-link); 88 | } 89 | 90 | a :is(h1, h2, h3, h4, h5, h6):hover:after { 91 | visibility: visible; 92 | } 93 | 94 | a :is(h1, h2, h3, h4, h5, h6):is(:hover, :focus):after { 95 | visibility: visible; 96 | } 97 | 98 | a .external-icon svg { 99 | vertical-align: middle; 100 | display: inline-block; 101 | stroke-width: 2; 102 | opacity: .8; 103 | width: 1em; 104 | height: 1em; 105 | fill: currentColor; 106 | } 107 | 108 | details { 109 | cursor: pointer; 110 | } 111 | 112 | pre { 113 | padding: 14px; 114 | } 115 | 116 | pre, 117 | code { 118 | border-radius: 6px; 119 | border: 1px solid rgb(20 20 20 / 0.25); 120 | } 121 | 122 | pre code { 123 | border: none; 124 | } 125 | 126 | blockquote { 127 | padding: 0.2em 0.8em 0.2em 1em; 128 | border-left: 5px solid gray; 129 | margin-bottom: 2.5rem; 130 | background-color: var(--color-background-secondary); 131 | border-radius: 0.45em; 132 | } 133 | 134 | a[href^="https://bundlejs.com/"] .external-icon { 135 | display: none; 136 | } 137 | 138 | img[src^="https://bundlejs.com/"] { 139 | overflow: hidden; 140 | border-radius: 20em; 141 | border: 1px solid rgb(20 20 20 / 0.25); 142 | } 143 | 144 | .container { 145 | max-width: 1200px; 146 | } 147 | 148 | .container.tsd-generator { 149 | max-width: 100%; 150 | } 151 | 152 | .container.tsd-generator p { 153 | line-height: 28px; 154 | max-width: 1200px; 155 | width: 100%; 156 | margin: auto; 157 | } 158 | 159 | header.tsd-page-toolbar { 160 | border-bottom: 1px solid var(--color-panel-divider); 161 | } 162 | 163 | .tsd-signature { 164 | border-radius: 0.5rem; 165 | } 166 | 167 | .tsd-breadcrumb { 168 | padding-block-start: 0.5rem; 169 | } 170 | 171 | .tsd-typography p, .tsd-typography ul, .tsd-typography ol { 172 | line-height: 1.85em; 173 | font-weight: 300; 174 | } 175 | .tsd-navigation { 176 | padding-inline-end: 0.25rem; 177 | } 178 | 179 | .tsd-accordion-details { 180 | padding-inline-start: 1.35rem; 181 | } 182 | 183 | .tsd-accordion-details > ul > li > ul { 184 | padding-left: 0; 185 | } 186 | 187 | .container-main { 188 | --mask-image: linear-gradient(0deg, transparent 0%, white 2%, white 50%, white 98%, transparent 100%); 189 | } 190 | 191 | @media (min-width: 769px) { 192 | .container-main { 193 | grid-template-columns: minmax(0, 15rem) minmax(0, 2.5fr); 194 | } 195 | 196 | .col-sidebar { 197 | border-right: 1px solid var(--color-panel-divider); 198 | mask-image: var(--mask-image); 199 | -webkit-mask-image: var(--mask-image); 200 | padding-block: 1rem; 201 | } 202 | } 203 | 204 | @media (min-width: 1200px) { 205 | .container-main { 206 | grid-template-columns: minmax(0, 1.15fr) minmax(0, 2.5fr) minmax(0, 15.5rem); 207 | } 208 | 209 | .page-menu { 210 | border-left: 1px solid var(--color-panel-divider); 211 | } 212 | 213 | .site-menu { 214 | border-right: 1px solid var(--color-panel-divider); 215 | mask-image: var(--mask-image); 216 | -webkit-mask-image: var(--mask-image); 217 | padding-block: 1rem; 218 | } 219 | } 220 | 221 | .tsd-accordion-details ul { 222 | padding-left: 1rem; 223 | } 224 | 225 | .tsd-accordion-details > ul { 226 | padding-left: 0; 227 | } 228 | 229 | :is(.tsd-page-navigation, .tsd-navigation) svg { 230 | vertical-align: middle; 231 | } 232 | 233 | .tsd-navigation a.current, .tsd-page-navigation a.current { 234 | border-radius: 0.45rem; 235 | padding-block: 0.3rem; 236 | padding-inline: 0.3rem; 237 | } 238 | 239 | .col-menu { 240 | border-left: 1px solid var(--color-panel-divider); 241 | } 242 | 243 | .tsd-widget:is(.search, .options, .menu) svg { 244 | display: none; 245 | } 246 | 247 | .tsd-widget:is(.search, .options, .menu):after { 248 | font-family: FluentSystemIcons-Regular !important; 249 | font-style: normal; 250 | font-weight: normal !important; 251 | font-variant: normal; 252 | text-transform: none; 253 | line-height: 1; 254 | -webkit-font-smoothing: antialiased; 255 | -moz-osx-font-smoothing: grayscale; 256 | 257 | font-size: 24px; 258 | color: var(--color-toolbar-text); 259 | width: 100%; 260 | height: 100%; 261 | 262 | position: absolute; 263 | text-align: center; 264 | display: flex; 265 | top: 0; 266 | left: 0; 267 | align-items: center; 268 | justify-content: center; 269 | } 270 | 271 | .tsd-widget.search:after { 272 | content: "\f690"; 273 | } 274 | 275 | .tsd-widget.options:after { 276 | content: "\f407"; 277 | } 278 | 279 | .tsd-widget.menu { 280 | position: relative; 281 | height: 40px; 282 | vertical-align: bottom; 283 | } 284 | 285 | .tsd-widget.menu:after { 286 | content: "\f561"; 287 | } -------------------------------------------------------------------------------- /docs/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-hl-0: #6F42C1; 3 | --dark-hl-0: #B392F0; 4 | --light-hl-1: #24292E; 5 | --dark-hl-1: #E1E4E8; 6 | --light-hl-2: #032F62; 7 | --dark-hl-2: #9ECBFF; 8 | --light-hl-3: #D73A49; 9 | --dark-hl-3: #F97583; 10 | --light-hl-4: #6A737D; 11 | --dark-hl-4: #6A737D; 12 | --light-hl-5: #22863A; 13 | --dark-hl-5: #85E89D; 14 | --light-hl-6: #005CC5; 15 | --dark-hl-6: #79B8FF; 16 | --light-hl-7: #E36209; 17 | --dark-hl-7: #FFAB70; 18 | --light-code-background: #fff; 19 | --dark-code-background: #24292e; 20 | } 21 | 22 | @media (prefers-color-scheme: light) { :root { 23 | --hl-0: var(--light-hl-0); 24 | --hl-1: var(--light-hl-1); 25 | --hl-2: var(--light-hl-2); 26 | --hl-3: var(--light-hl-3); 27 | --hl-4: var(--light-hl-4); 28 | --hl-5: var(--light-hl-5); 29 | --hl-6: var(--light-hl-6); 30 | --hl-7: var(--light-hl-7); 31 | --code-background: var(--light-code-background); 32 | } } 33 | 34 | @media (prefers-color-scheme: dark) { :root { 35 | --hl-0: var(--dark-hl-0); 36 | --hl-1: var(--dark-hl-1); 37 | --hl-2: var(--dark-hl-2); 38 | --hl-3: var(--dark-hl-3); 39 | --hl-4: var(--dark-hl-4); 40 | --hl-5: var(--dark-hl-5); 41 | --hl-6: var(--dark-hl-6); 42 | --hl-7: var(--dark-hl-7); 43 | --code-background: var(--dark-code-background); 44 | } } 45 | 46 | :root[data-theme='light'] { 47 | --hl-0: var(--light-hl-0); 48 | --hl-1: var(--light-hl-1); 49 | --hl-2: var(--light-hl-2); 50 | --hl-3: var(--light-hl-3); 51 | --hl-4: var(--light-hl-4); 52 | --hl-5: var(--light-hl-5); 53 | --hl-6: var(--light-hl-6); 54 | --hl-7: var(--light-hl-7); 55 | --code-background: var(--light-code-background); 56 | } 57 | 58 | :root[data-theme='dark'] { 59 | --hl-0: var(--dark-hl-0); 60 | --hl-1: var(--dark-hl-1); 61 | --hl-2: var(--dark-hl-2); 62 | --hl-3: var(--dark-hl-3); 63 | --hl-4: var(--dark-hl-4); 64 | --hl-5: var(--dark-hl-5); 65 | --hl-6: var(--dark-hl-6); 66 | --hl-7: var(--dark-hl-7); 67 | --code-background: var(--dark-code-background); 68 | } 69 | 70 | .hl-0 { color: var(--hl-0); } 71 | .hl-1 { color: var(--hl-1); } 72 | .hl-2 { color: var(--hl-2); } 73 | .hl-3 { color: var(--hl-3); } 74 | .hl-4 { color: var(--hl-4); } 75 | .hl-5 { color: var(--hl-5); } 76 | .hl-6 { color: var(--hl-6); } 77 | .hl-7 { color: var(--hl-7); } 78 | pre, code { background: var(--code-background); } 79 | -------------------------------------------------------------------------------- /docs/assets/search.js: -------------------------------------------------------------------------------- 1 | window.searchData = JSON.parse("{\"rows\":[{\"kind\":64,\"name\":\"getSpringDuration\",\"url\":\"functions/getSpringDuration.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"EaseOut\",\"url\":\"functions/EaseOut.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"EaseInOut\",\"url\":\"functions/EaseInOut.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"EaseOutIn\",\"url\":\"functions/EaseOutIn.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"interpolateNumber\",\"url\":\"functions/interpolateNumber.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"interpolateSequence\",\"url\":\"functions/interpolateSequence.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"interpolateString\",\"url\":\"functions/interpolateString.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"interpolateComplex\",\"url\":\"functions/interpolateComplex.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"registerEasingFunction\",\"url\":\"functions/registerEasingFunction.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"registerEasingFunctions\",\"url\":\"functions/registerEasingFunctions.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"parseEasingParameters\",\"url\":\"functions/parseEasingParameters.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"EasingOptions\",\"url\":\"functions/EasingOptions.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"GenerateSpringFrames\",\"url\":\"functions/GenerateSpringFrames.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"SpringEasing\",\"url\":\"functions/SpringEasing.html\",\"classes\":\"\"},{\"kind\":4194304,\"name\":\"TypeFrameFunction\",\"url\":\"types/TypeFrameFunction.html\",\"classes\":\"\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"types/TypeFrameFunction.html#__type\",\"classes\":\"\",\"parent\":\"TypeFrameFunction\"},{\"kind\":4194304,\"name\":\"TypeInterpolationFunction\",\"url\":\"types/TypeInterpolationFunction.html\",\"classes\":\"\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"types/TypeInterpolationFunction.html#__type\",\"classes\":\"\",\"parent\":\"TypeInterpolationFunction\"},{\"kind\":64,\"name\":\"SpringFrame\",\"url\":\"functions/SpringFrame.html\",\"classes\":\"\"},{\"kind\":32,\"name\":\"EasingDurationCache\",\"url\":\"variables/EasingDurationCache.html\",\"classes\":\"\"},{\"kind\":32,\"name\":\"INFINITE_LOOP_LIMIT\",\"url\":\"variables/INFINITE_LOOP_LIMIT.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"SpringInFrame\",\"url\":\"functions/SpringInFrame.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"SpringOutFrame\",\"url\":\"functions/SpringOutFrame.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"SpringInOutFrame\",\"url\":\"functions/SpringInOutFrame.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"SpringOutInFrame\",\"url\":\"functions/SpringOutInFrame.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"interpolateUsingIndex\",\"url\":\"functions/interpolateUsingIndex.html\",\"classes\":\"\"},{\"kind\":4194304,\"name\":\"TypeArrayFrameFunctionFormat\",\"url\":\"types/TypeArrayFrameFunctionFormat.html\",\"classes\":\"\"},{\"kind\":32,\"name\":\"EasingFunctions\",\"url\":\"variables/EasingFunctions.html\",\"classes\":\"\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"variables/EasingFunctions.html#__type\",\"classes\":\"\",\"parent\":\"EasingFunctions\"},{\"kind\":1024,\"name\":\"spring\",\"url\":\"variables/EasingFunctions.html#__type.spring\",\"classes\":\"\",\"parent\":\"EasingFunctions.__type\"},{\"kind\":1024,\"name\":\"spring-in\",\"url\":\"variables/EasingFunctions.html#__type.spring_in\",\"classes\":\"\",\"parent\":\"EasingFunctions.__type\"},{\"kind\":1024,\"name\":\"spring-out\",\"url\":\"variables/EasingFunctions.html#__type.spring_out\",\"classes\":\"\",\"parent\":\"EasingFunctions.__type\"},{\"kind\":1024,\"name\":\"spring-in-out\",\"url\":\"variables/EasingFunctions.html#__type.spring_in_out\",\"classes\":\"\",\"parent\":\"EasingFunctions.__type\"},{\"kind\":1024,\"name\":\"spring-out-in\",\"url\":\"variables/EasingFunctions.html#__type.spring_out_in\",\"classes\":\"\",\"parent\":\"EasingFunctions.__type\"},{\"kind\":32,\"name\":\"EasingFunctionKeys\",\"url\":\"variables/EasingFunctionKeys.html\",\"classes\":\"\"},{\"kind\":4194304,\"name\":\"TypeEasings\",\"url\":\"types/TypeEasings.html\",\"classes\":\"\"},{\"kind\":4194304,\"name\":\"TypeEasingOptions\",\"url\":\"types/TypeEasingOptions.html\",\"classes\":\"\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"types/TypeEasingOptions.html#__type\",\"classes\":\"\",\"parent\":\"TypeEasingOptions\"},{\"kind\":1024,\"name\":\"easing\",\"url\":\"types/TypeEasingOptions.html#__type.easing\",\"classes\":\"\",\"parent\":\"TypeEasingOptions.__type\"},{\"kind\":1024,\"name\":\"numPoints\",\"url\":\"types/TypeEasingOptions.html#__type.numPoints\",\"classes\":\"\",\"parent\":\"TypeEasingOptions.__type\"},{\"kind\":1024,\"name\":\"decimal\",\"url\":\"types/TypeEasingOptions.html#__type.decimal\",\"classes\":\"\",\"parent\":\"TypeEasingOptions.__type\"},{\"kind\":32,\"name\":\"FramePtsCache\",\"url\":\"variables/FramePtsCache.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"isNumberLike\",\"url\":\"functions/isNumberLike.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"limit\",\"url\":\"functions/limit.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"scale\",\"url\":\"functions/scale.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"toFixed\",\"url\":\"functions/toFixed.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"getUnit\",\"url\":\"functions/getUnit.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"batchInterpolateNumber\",\"url\":\"functions/batchInterpolateNumber.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"batchInterpolateSequence\",\"url\":\"functions/batchInterpolateSequence.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"batchInterpolateString\",\"url\":\"functions/batchInterpolateString.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"batchInterpolateComplex\",\"url\":\"functions/batchInterpolateComplex.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"BatchSpringEasing\",\"url\":\"functions/BatchSpringEasing.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"toAnimationFrames\",\"url\":\"functions/toAnimationFrames.html\",\"classes\":\"\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"functions/toAnimationFrames.html#toAnimationFrames.__type\",\"classes\":\"\",\"parent\":\"toAnimationFrames.toAnimationFrames\"},{\"kind\":256,\"name\":\"IGenericBatchInterpolationFunction\",\"url\":\"interfaces/IGenericBatchInterpolationFunction.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"batchInterpolateUsingIndex\",\"url\":\"functions/batchInterpolateUsingIndex.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"squaredSegmentDistance\",\"url\":\"functions/squaredSegmentDistance.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"ramerDouglasPeucker\",\"url\":\"functions/ramerDouglasPeucker.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"getOptimizedPoints\",\"url\":\"functions/getOptimizedPoints.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"getLinearSyntax\",\"url\":\"functions/getLinearSyntax.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"CSSSpringEasing\",\"url\":\"functions/CSSSpringEasing.html\",\"classes\":\"\"},{\"kind\":4194304,\"name\":\"TypeCSSEasingOptions\",\"url\":\"types/TypeCSSEasingOptions.html\",\"classes\":\"\"},{\"kind\":8388608,\"name\":\"default\",\"url\":\"modules.html#default\",\"classes\":\"\"}],\"index\":{\"version\":\"2.3.9\",\"fields\":[\"name\",\"comment\"],\"fieldVectors\":[[\"name/0\",[0,38.919]],[\"comment/0\",[]],[\"name/1\",[1,38.919]],[\"comment/1\",[]],[\"name/2\",[2,38.919]],[\"comment/2\",[]],[\"name/3\",[3,38.919]],[\"comment/3\",[]],[\"name/4\",[4,38.919]],[\"comment/4\",[]],[\"name/5\",[5,38.919]],[\"comment/5\",[]],[\"name/6\",[6,38.919]],[\"comment/6\",[]],[\"name/7\",[7,38.919]],[\"comment/7\",[]],[\"name/8\",[8,38.919]],[\"comment/8\",[]],[\"name/9\",[9,38.919]],[\"comment/9\",[]],[\"name/10\",[10,38.919]],[\"comment/10\",[]],[\"name/11\",[11,38.919]],[\"comment/11\",[]],[\"name/12\",[12,38.919]],[\"comment/12\",[]],[\"name/13\",[13,38.919]],[\"comment/13\",[]],[\"name/14\",[14,38.919]],[\"comment/14\",[]],[\"name/15\",[15,25.447]],[\"comment/15\",[]],[\"name/16\",[16,38.919]],[\"comment/16\",[]],[\"name/17\",[15,25.447]],[\"comment/17\",[]],[\"name/18\",[17,38.919]],[\"comment/18\",[]],[\"name/19\",[18,38.919]],[\"comment/19\",[]],[\"name/20\",[19,38.919]],[\"comment/20\",[]],[\"name/21\",[20,38.919]],[\"comment/21\",[]],[\"name/22\",[21,38.919]],[\"comment/22\",[]],[\"name/23\",[22,38.919]],[\"comment/23\",[]],[\"name/24\",[23,38.919]],[\"comment/24\",[]],[\"name/25\",[24,38.919]],[\"comment/25\",[]],[\"name/26\",[25,38.919]],[\"comment/26\",[]],[\"name/27\",[26,38.919]],[\"comment/27\",[]],[\"name/28\",[15,25.447]],[\"comment/28\",[]],[\"name/29\",[27,25.447]],[\"comment/29\",[]],[\"name/30\",[27,18.343,28,21.721]],[\"comment/30\",[]],[\"name/31\",[27,18.343,29,21.721]],[\"comment/31\",[]],[\"name/32\",[27,14.339,28,16.98,29,16.98]],[\"comment/32\",[]],[\"name/33\",[27,14.339,28,16.98,29,16.98]],[\"comment/33\",[]],[\"name/34\",[30,38.919]],[\"comment/34\",[]],[\"name/35\",[31,38.919]],[\"comment/35\",[]],[\"name/36\",[32,38.919]],[\"comment/36\",[]],[\"name/37\",[15,25.447]],[\"comment/37\",[]],[\"name/38\",[33,38.919]],[\"comment/38\",[]],[\"name/39\",[34,38.919]],[\"comment/39\",[]],[\"name/40\",[35,38.919]],[\"comment/40\",[]],[\"name/41\",[36,38.919]],[\"comment/41\",[]],[\"name/42\",[37,38.919]],[\"comment/42\",[]],[\"name/43\",[38,38.919]],[\"comment/43\",[]],[\"name/44\",[39,38.919]],[\"comment/44\",[]],[\"name/45\",[40,38.919]],[\"comment/45\",[]],[\"name/46\",[41,38.919]],[\"comment/46\",[]],[\"name/47\",[42,38.919]],[\"comment/47\",[]],[\"name/48\",[43,38.919]],[\"comment/48\",[]],[\"name/49\",[44,38.919]],[\"comment/49\",[]],[\"name/50\",[45,38.919]],[\"comment/50\",[]],[\"name/51\",[46,38.919]],[\"comment/51\",[]],[\"name/52\",[47,38.919]],[\"comment/52\",[]],[\"name/53\",[15,25.447]],[\"comment/53\",[]],[\"name/54\",[48,38.919]],[\"comment/54\",[]],[\"name/55\",[49,38.919]],[\"comment/55\",[]],[\"name/56\",[50,38.919]],[\"comment/56\",[]],[\"name/57\",[51,38.919]],[\"comment/57\",[]],[\"name/58\",[52,38.919]],[\"comment/58\",[]],[\"name/59\",[53,38.919]],[\"comment/59\",[]],[\"name/60\",[54,38.919]],[\"comment/60\",[]],[\"name/61\",[55,38.919]],[\"comment/61\",[]],[\"name/62\",[56,38.919]],[\"comment/62\",[]]],\"invertedIndex\":[[\"__type\",{\"_index\":15,\"name\":{\"15\":{},\"17\":{},\"28\":{},\"37\":{},\"53\":{}},\"comment\":{}}],[\"batchinterpolatecomplex\",{\"_index\":45,\"name\":{\"50\":{}},\"comment\":{}}],[\"batchinterpolatenumber\",{\"_index\":42,\"name\":{\"47\":{}},\"comment\":{}}],[\"batchinterpolatesequence\",{\"_index\":43,\"name\":{\"48\":{}},\"comment\":{}}],[\"batchinterpolatestring\",{\"_index\":44,\"name\":{\"49\":{}},\"comment\":{}}],[\"batchinterpolateusingindex\",{\"_index\":49,\"name\":{\"55\":{}},\"comment\":{}}],[\"batchspringeasing\",{\"_index\":46,\"name\":{\"51\":{}},\"comment\":{}}],[\"cssspringeasing\",{\"_index\":54,\"name\":{\"60\":{}},\"comment\":{}}],[\"decimal\",{\"_index\":35,\"name\":{\"40\":{}},\"comment\":{}}],[\"default\",{\"_index\":56,\"name\":{\"62\":{}},\"comment\":{}}],[\"easeinout\",{\"_index\":2,\"name\":{\"2\":{}},\"comment\":{}}],[\"easeout\",{\"_index\":1,\"name\":{\"1\":{}},\"comment\":{}}],[\"easeoutin\",{\"_index\":3,\"name\":{\"3\":{}},\"comment\":{}}],[\"easing\",{\"_index\":33,\"name\":{\"38\":{}},\"comment\":{}}],[\"easingdurationcache\",{\"_index\":18,\"name\":{\"19\":{}},\"comment\":{}}],[\"easingfunctionkeys\",{\"_index\":30,\"name\":{\"34\":{}},\"comment\":{}}],[\"easingfunctions\",{\"_index\":26,\"name\":{\"27\":{}},\"comment\":{}}],[\"easingoptions\",{\"_index\":11,\"name\":{\"11\":{}},\"comment\":{}}],[\"frameptscache\",{\"_index\":36,\"name\":{\"41\":{}},\"comment\":{}}],[\"generatespringframes\",{\"_index\":12,\"name\":{\"12\":{}},\"comment\":{}}],[\"getlinearsyntax\",{\"_index\":53,\"name\":{\"59\":{}},\"comment\":{}}],[\"getoptimizedpoints\",{\"_index\":52,\"name\":{\"58\":{}},\"comment\":{}}],[\"getspringduration\",{\"_index\":0,\"name\":{\"0\":{}},\"comment\":{}}],[\"getunit\",{\"_index\":41,\"name\":{\"46\":{}},\"comment\":{}}],[\"igenericbatchinterpolationfunction\",{\"_index\":48,\"name\":{\"54\":{}},\"comment\":{}}],[\"in\",{\"_index\":28,\"name\":{\"30\":{},\"32\":{},\"33\":{}},\"comment\":{}}],[\"infinite_loop_limit\",{\"_index\":19,\"name\":{\"20\":{}},\"comment\":{}}],[\"interpolatecomplex\",{\"_index\":7,\"name\":{\"7\":{}},\"comment\":{}}],[\"interpolatenumber\",{\"_index\":4,\"name\":{\"4\":{}},\"comment\":{}}],[\"interpolatesequence\",{\"_index\":5,\"name\":{\"5\":{}},\"comment\":{}}],[\"interpolatestring\",{\"_index\":6,\"name\":{\"6\":{}},\"comment\":{}}],[\"interpolateusingindex\",{\"_index\":24,\"name\":{\"25\":{}},\"comment\":{}}],[\"isnumberlike\",{\"_index\":37,\"name\":{\"42\":{}},\"comment\":{}}],[\"limit\",{\"_index\":38,\"name\":{\"43\":{}},\"comment\":{}}],[\"numpoints\",{\"_index\":34,\"name\":{\"39\":{}},\"comment\":{}}],[\"out\",{\"_index\":29,\"name\":{\"31\":{},\"32\":{},\"33\":{}},\"comment\":{}}],[\"parseeasingparameters\",{\"_index\":10,\"name\":{\"10\":{}},\"comment\":{}}],[\"ramerdouglaspeucker\",{\"_index\":51,\"name\":{\"57\":{}},\"comment\":{}}],[\"registereasingfunction\",{\"_index\":8,\"name\":{\"8\":{}},\"comment\":{}}],[\"registereasingfunctions\",{\"_index\":9,\"name\":{\"9\":{}},\"comment\":{}}],[\"scale\",{\"_index\":39,\"name\":{\"44\":{}},\"comment\":{}}],[\"spring\",{\"_index\":27,\"name\":{\"29\":{},\"30\":{},\"31\":{},\"32\":{},\"33\":{}},\"comment\":{}}],[\"springeasing\",{\"_index\":13,\"name\":{\"13\":{}},\"comment\":{}}],[\"springframe\",{\"_index\":17,\"name\":{\"18\":{}},\"comment\":{}}],[\"springinframe\",{\"_index\":20,\"name\":{\"21\":{}},\"comment\":{}}],[\"springinoutframe\",{\"_index\":22,\"name\":{\"23\":{}},\"comment\":{}}],[\"springoutframe\",{\"_index\":21,\"name\":{\"22\":{}},\"comment\":{}}],[\"springoutinframe\",{\"_index\":23,\"name\":{\"24\":{}},\"comment\":{}}],[\"squaredsegmentdistance\",{\"_index\":50,\"name\":{\"56\":{}},\"comment\":{}}],[\"toanimationframes\",{\"_index\":47,\"name\":{\"52\":{}},\"comment\":{}}],[\"tofixed\",{\"_index\":40,\"name\":{\"45\":{}},\"comment\":{}}],[\"typearrayframefunctionformat\",{\"_index\":25,\"name\":{\"26\":{}},\"comment\":{}}],[\"typecsseasingoptions\",{\"_index\":55,\"name\":{\"61\":{}},\"comment\":{}}],[\"typeeasingoptions\",{\"_index\":32,\"name\":{\"36\":{}},\"comment\":{}}],[\"typeeasings\",{\"_index\":31,\"name\":{\"35\":{}},\"comment\":{}}],[\"typeframefunction\",{\"_index\":14,\"name\":{\"14\":{}},\"comment\":{}}],[\"typeinterpolationfunction\",{\"_index\":16,\"name\":{\"16\":{}},\"comment\":{}}]],\"pipeline\":[]}}"); -------------------------------------------------------------------------------- /docs/media/_headers: -------------------------------------------------------------------------------- 1 | /* 2 | X-Frame-Options: SAMEORIGIN 3 | X-Content-Type-Options: nosniff 4 | X-XSS-Protection: 1; mode=block 5 | Referrer-Policy: strict-origin-when-cross-origin 6 | Strict-Transport-Security: max-age=63072000; includeSubDomains; preload 7 | Cache-Control: max-age=1360, stale-while-revalidate=480, public 8 | Accept-CH: DPR, Viewport-Width, Width 9 | X-UA-Compatible: IE=edge 10 | Content-Security-Policy: default-src 'self'; font-src 'self' https://fonts.gstatic.com; style-src 'self' https://fonts.googleapis.com/ 'unsafe-inline'; img-src 'self' https://gitpod.io/ https://bundlejs.com data:; script-src 'self' https://bundlejs.com https://*.bundlejs.com 'unsafe-inline' 'unsafe-eval'; connect-src 'self' https:; block-all-mixed-content; upgrade-insecure-requests; base-uri 'self'; object-src 'none'; worker-src 'self'; manifest-src 'self'; media-src 'self' https://res.cloudinary.com/; form-action 'self'; frame-ancestors 'self' https:; 11 | Permissions-Policy: geolocation=(), microphone=(), usb=(), sync-xhr=(self), camera=(), interest-cohort=() 12 | Link: ; rel=preload; as=font; type=font/ttf; crossorigin=anonymous 13 | Link: ; rel=preload; as=style 14 | Link: ; rel=preload; as=font; type=font/woff2; crossorigin=anonymous 15 | # Link: ; rel=preload; as=style 16 | # Link: ; rel=preload; as=font; type=font/woff2; crossorigin=anonymous 17 | 18 | /*.css 19 | Cache-Control: public, max-age=604800, stale-while-revalidate=480 20 | Content-Type: text/css 21 | 22 | /*.ttf 23 | Cache-Control: public, max-age=31536000, stale-while-revalidate=23480 24 | Content-Type: font/ttf 25 | 26 | /*.woff2 27 | Cache-Control: public, max-age=31536000, stale-while-revalidate=23480 28 | Content-Type: font/woff2 29 | 30 | /*.js 31 | Cache-Control: public, max-age=604800, stale-while-revalidate=480 32 | Content-Type: text/javascript 33 | 34 | /manifest.json 35 | Cache-Control: public, max-age=604800, stale-while-revalidate=480 36 | Content-Type: application/manifest+json 37 | 38 | /assets/* 39 | Cache-Control: public, max-age=31536000, stale-while-revalidate=23480 40 | 41 | /*.svg 42 | Cache-Control: public, max-age=31536000, stale-while-revalidate=23480 43 | Content-Type: image/svg+xml 44 | -------------------------------------------------------------------------------- /docs/media/assets/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/media/assets/spring-easing-demo-video.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/spring-easing/707097383d5dc063092bdb3eef4a237e06640eeb/docs/media/assets/spring-easing-demo-video.gif -------------------------------------------------------------------------------- /docs/media/assets/spring-easing-demo-video.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/spring-easing/707097383d5dc063092bdb3eef4a237e06640eeb/docs/media/assets/spring-easing-demo-video.mp4 -------------------------------------------------------------------------------- /docs/media/assets/spring-easing-demo-video.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/spring-easing/707097383d5dc063092bdb3eef4a237e06640eeb/docs/media/assets/spring-easing-demo-video.webm -------------------------------------------------------------------------------- /docs/media/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/spring-easing/707097383d5dc063092bdb3eef4a237e06640eeb/docs/media/favicon.ico -------------------------------------------------------------------------------- /docs/media/fonts/FluentSystemIcons-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/spring-easing/707097383d5dc063092bdb3eef4a237e06640eeb/docs/media/fonts/FluentSystemIcons-Regular.ttf -------------------------------------------------------------------------------- /docs/media/fonts/FluentSystemIcons-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/spring-easing/707097383d5dc063092bdb3eef4a237e06640eeb/docs/media/fonts/FluentSystemIcons-Regular.woff2 -------------------------------------------------------------------------------- /docs/media/measure.js: -------------------------------------------------------------------------------- 1 | (window => { 2 | const { 3 | screen: { width, height }, 4 | navigator: { language }, 5 | location, 6 | localStorage, 7 | document, 8 | history, 9 | } = window; 10 | const { hostname, pathname, search } = location; 11 | const { currentScript } = document; 12 | 13 | if (!currentScript) return; 14 | 15 | const assign = (a, b) => { 16 | Object.keys(b).forEach(key => { 17 | if (b[key] !== undefined) a[key] = b[key]; 18 | }); 19 | return a; 20 | }; 21 | 22 | const hook = (_this, method, callback) => { 23 | const orig = _this[method]; 24 | 25 | return (...args) => { 26 | callback.apply(null, args); 27 | 28 | return orig.apply(_this, args); 29 | }; 30 | }; 31 | 32 | const doNotTrack = () => { 33 | const { doNotTrack, navigator, external } = window; 34 | 35 | const msTrackProtection = 'msTrackingProtectionEnabled'; 36 | const msTracking = () => { 37 | return external && msTrackProtection in external && external[msTrackProtection](); 38 | }; 39 | 40 | const dnt = doNotTrack || navigator.doNotTrack || navigator.msDoNotTrack || msTracking(); 41 | 42 | return dnt == '1' || dnt === 'yes'; 43 | }; 44 | 45 | const trackingDisabled = () => 46 | (localStorage && localStorage.getItem('umami.disabled')) || 47 | (dnt && doNotTrack()) || 48 | (domain && !domains.includes(hostname)); 49 | 50 | const _data = 'data-'; 51 | const _false = 'false'; 52 | const attr = currentScript.getAttribute.bind(currentScript); 53 | const website = attr(_data + 'website-id'); 54 | const hostUrl = attr(_data + 'host-url'); 55 | const autoTrack = attr(_data + 'auto-track') !== _false; 56 | const dnt = attr(_data + 'do-not-track'); 57 | const cssEvents = attr(_data + 'css-events') !== _false; 58 | const domain = attr(_data + 'domains') || ''; 59 | const domains = domain.split(',').map(n => n.trim()); 60 | const root = hostUrl 61 | ? hostUrl.replace(/\/$/, '') 62 | : currentScript.src.split('/').slice(0, -1).join('/'); 63 | const endpoint = `${root}/take-measurement`; // "/api/collect"; 64 | const screen = `${width}x${height}`; 65 | const eventClass = /^umami--([a-z]+)--([\w]+[\w-]*)$/; 66 | const eventSelect = "[class*='umami--']"; 67 | 68 | let listeners = {}; 69 | let currentUrl = `${pathname}${search}`; 70 | let currentRef = document.referrer; 71 | let cache; 72 | 73 | /* Collect metrics */ 74 | 75 | const getPayload = () => ({ 76 | website, 77 | hostname, 78 | screen, 79 | language, 80 | url: currentUrl, 81 | }); 82 | 83 | const collect = (type, payload) => { 84 | if (trackingDisabled()) return; 85 | 86 | return fetch(endpoint, { 87 | method: 'POST', 88 | body: JSON.stringify({ type, payload }), 89 | headers: assign({ 'Content-Type': 'application/json' }, { ['x-umami-cache']: cache }), 90 | }) 91 | .then(res => res.text()) 92 | .then(text => (cache = text)); 93 | }; 94 | 95 | const trackView = (url = currentUrl, referrer = currentRef, websiteUuid = website) => 96 | collect( 97 | 'pageview', 98 | assign(getPayload(), { 99 | website: websiteUuid, 100 | url, 101 | referrer, 102 | }), 103 | ); 104 | 105 | const trackEvent = (eventName, eventData, url = currentUrl, websiteUuid = website) => 106 | collect( 107 | 'event', 108 | assign(getPayload(), { 109 | website: websiteUuid, 110 | url, 111 | event_name: eventName, 112 | event_data: eventData, 113 | }), 114 | ); 115 | 116 | /* Handle events */ 117 | 118 | const addEvents = node => { 119 | const elements = node.querySelectorAll(eventSelect); 120 | Array.prototype.forEach.call(elements, addEvent); 121 | }; 122 | 123 | const addEvent = element => { 124 | const get = element.getAttribute.bind(element); 125 | (get('class') || '').split(' ').forEach(className => { 126 | if (!eventClass.test(className)) return; 127 | 128 | const [, event, name] = className.split('--'); 129 | 130 | const listener = listeners[className] 131 | ? listeners[className] 132 | : (listeners[className] = e => { 133 | if ( 134 | event === 'click' && 135 | element.tagName === 'A' && 136 | !( 137 | e.ctrlKey || 138 | e.shiftKey || 139 | e.metaKey || 140 | (e.button && e.button === 1) || 141 | get('target') 142 | ) 143 | ) { 144 | e.preventDefault(); 145 | trackEvent(name).then(() => { 146 | const href = get('href'); 147 | if (href) { 148 | location.href = href; 149 | } 150 | }); 151 | } else { 152 | trackEvent(name); 153 | } 154 | }); 155 | 156 | element.addEventListener(event, listener, true); 157 | }); 158 | }; 159 | 160 | /* Handle history changes */ 161 | 162 | const handlePush = (state, title, url) => { 163 | if (!url) return; 164 | 165 | currentRef = currentUrl; 166 | const newUrl = url.toString(); 167 | 168 | if (newUrl.substring(0, 4) === 'http') { 169 | currentUrl = '/' + newUrl.split('/').splice(3).join('/'); 170 | } else { 171 | currentUrl = newUrl; 172 | } 173 | 174 | if (currentUrl !== currentRef) { 175 | trackView(); 176 | } 177 | }; 178 | 179 | const observeDocument = () => { 180 | const monitorMutate = mutations => { 181 | mutations.forEach(mutation => { 182 | const element = mutation.target; 183 | addEvent(element); 184 | addEvents(element); 185 | }); 186 | }; 187 | 188 | const observer = new MutationObserver(monitorMutate); 189 | observer.observe(document, { childList: true, subtree: true }); 190 | }; 191 | 192 | /* Global */ 193 | 194 | if (!window.umami) { 195 | const umami = eventValue => trackEvent(eventValue); 196 | umami.trackView = trackView; 197 | umami.trackEvent = trackEvent; 198 | 199 | window.umami = umami; 200 | } 201 | 202 | /* Start */ 203 | 204 | if (autoTrack && !trackingDisabled()) { 205 | history.pushState = hook(history, 'pushState', handlePush); 206 | history.replaceState = hook(history, 'replaceState', handlePush); 207 | 208 | const update = () => { 209 | if (document.readyState === 'complete') { 210 | trackView(); 211 | 212 | if (cssEvents) { 213 | addEvents(document); 214 | observeDocument(); 215 | } 216 | } 217 | }; 218 | 219 | document.addEventListener('readystatechange', update, true); 220 | 221 | update(); 222 | } 223 | })(window); 224 | 225 | 226 | 227 | // (window => { 228 | // const apiRoute = "/take-measurement"; // "/api/collect"; 229 | // const { screen: { width, height }, navigator: { language }, location: { hostname, pathname, search }, localStorage, document, history, } = window; 230 | // const script = document.querySelector('script[data-website-id]'); 231 | // if (!script) 232 | // return; 233 | // const attr = script.getAttribute.bind(script); 234 | // const website = attr('data-website-id'); 235 | // const hostUrl = attr('data-host-url'); 236 | // const autoTrack = attr('data-auto-track') !== 'false'; 237 | // const dnt = attr('data-do-not-track'); 238 | // const cssEvents = attr('data-css-events') !== 'false'; 239 | // const domain = attr('data-domains') || ''; 240 | // const domains = domain.split(',').map(n => n.trim()); 241 | // const eventClass = /^umami--([a-z]+)--([\w]+[\w-]*)$/; 242 | // const eventSelect = "[class*='umami--']"; 243 | // const trackingDisabled = () => (localStorage && localStorage.getItem('umami.disabled')) || 244 | // (dnt && doNotTrack()) || 245 | // (domain && !domains.includes(hostname)); 246 | // const root = hostUrl 247 | // ? removeTrailingSlash(hostUrl) 248 | // : ""; // script.src.split('/').slice(0, -1).join('/'); 249 | // const screen = `${width}x${height}`; 250 | // const listeners = {}; 251 | // let currentUrl = `${pathname}${search}`; 252 | // let currentRef = document.referrer; 253 | // let cache; 254 | // /* Collect metrics */ 255 | // const post = (url, data, callback) => { 256 | // const req = new XMLHttpRequest(); 257 | // req.open('POST', url, true); 258 | // req.setRequestHeader('Content-Type', 'application/json'); 259 | // if (cache) 260 | // req.setRequestHeader('x-umami-cache', cache); 261 | // req.onreadystatechange = () => { 262 | // if (req.readyState === 4) { 263 | // callback(req.response); 264 | // } 265 | // }; 266 | // req.send(JSON.stringify(data)); 267 | // }; 268 | // const getPayload = () => ({ 269 | // website, 270 | // hostname, 271 | // screen, 272 | // language, 273 | // url: currentUrl, 274 | // }); 275 | // const assign = (a, b) => { 276 | // Object.keys(b).forEach(key => { 277 | // a[key] = b[key]; 278 | // }); 279 | // return a; 280 | // }; 281 | // const collect = (type, payload) => { 282 | // if (trackingDisabled()) 283 | // return; 284 | // post(`${root}${apiRoute}`, { 285 | // type, 286 | // payload, 287 | // }, res => (cache = res)); 288 | // }; 289 | // const trackView = (url = currentUrl, referrer = currentRef, uuid = website) => { 290 | // collect('pageview', assign(getPayload(), { 291 | // website: uuid, 292 | // url, 293 | // referrer, 294 | // })); 295 | // }; 296 | // const trackEvent = (event_value, event_type = 'custom', url = currentUrl, uuid = website) => { 297 | // collect('event', assign(getPayload(), { 298 | // website: uuid, 299 | // url, 300 | // event_type, 301 | // event_value, 302 | // })); 303 | // }; 304 | // /* Handle events */ 305 | // const sendEvent = (value, type) => { 306 | // const payload = getPayload(); 307 | // // @ts-ignore 308 | // payload.event_type = type; 309 | // // @ts-ignore 310 | // payload.event_value = value; 311 | // const data = JSON.stringify({ 312 | // type: 'event', 313 | // payload, 314 | // }); 315 | // navigator.sendBeacon(`${root}${apiRoute}`, data); 316 | // }; 317 | // const addEvents = node => { 318 | // const elements = node.querySelectorAll(eventSelect); 319 | // Array.prototype.forEach.call(elements, addEvent); 320 | // }; 321 | // const addEvent = element => { 322 | // (element.getAttribute('class') || '').split(' ').forEach(className => { 323 | // if (!eventClass.test(className)) 324 | // return; 325 | // const [, type, value] = className.split('--'); 326 | // const listener = listeners[className] 327 | // ? listeners[className] 328 | // : (listeners[className] = () => { 329 | // if (element.tagName === 'A') { 330 | // sendEvent(value, type); 331 | // } 332 | // else { 333 | // trackEvent(value, type); 334 | // } 335 | // }); 336 | // element.addEventListener(type, listener, true); 337 | // }); 338 | // }; 339 | // /* Handle history changes */ 340 | // const handlePush = (state, title, url) => { 341 | // if (!url) 342 | // return; 343 | // currentRef = currentUrl; 344 | // const newUrl = url.toString(); 345 | // if (newUrl.substring(0, 4) === 'http') { 346 | // currentUrl = '/' + newUrl.split('/').splice(3).join('/'); 347 | // } 348 | // else { 349 | // currentUrl = newUrl; 350 | // } 351 | // if (currentUrl !== currentRef) { 352 | // trackView(); 353 | // } 354 | // }; 355 | // const observeDocument = () => { 356 | // const monitorMutate = mutations => { 357 | // mutations.forEach(mutation => { 358 | // const element = mutation.target; 359 | // addEvent(element); 360 | // addEvents(element); 361 | // }); 362 | // }; 363 | // const observer = new MutationObserver(monitorMutate); 364 | // observer.observe(document, { childList: true, subtree: true }); 365 | // }; 366 | // /* Global */ 367 | // // @ts-ignore 368 | // if (!window.umami) { 369 | // const umami = eventValue => trackEvent(eventValue); 370 | // umami.trackView = trackView; 371 | // umami.trackEvent = trackEvent; 372 | // // @ts-ignore 373 | // window.umami = umami; 374 | // } 375 | // /* Start */ 376 | // if (autoTrack && !trackingDisabled()) { 377 | // history.pushState = hook(history, 'pushState', handlePush); 378 | // history.replaceState = hook(history, 'replaceState', handlePush); 379 | // const update = () => { 380 | // if (document.readyState === 'complete') { 381 | // trackView(); 382 | // if (cssEvents) { 383 | // addEvents(document); 384 | // observeDocument(); 385 | // } 386 | // } 387 | // }; 388 | // document.addEventListener('readystatechange', update, true); 389 | // update(); 390 | // } 391 | // })(window); 392 | 393 | -------------------------------------------------------------------------------- /docs/media/robots.txt: -------------------------------------------------------------------------------- 1 | Sitemap: https://spring-easing.okikio.dev/sitemap.xml 2 | User-agent: * 3 | Allow: / 4 | -------------------------------------------------------------------------------- /dts.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib", 5 | "declaration": true, 6 | "declarationMap": true, 7 | "emitDeclarationOnly": true 8 | }, 9 | "include": [ 10 | "./src/**/*" 11 | ], 12 | "exclude": [ 13 | "node_modules", 14 | "./tests/**/*", 15 | "./lib/**/*" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /lib/batch.d.ts: -------------------------------------------------------------------------------- 1 | import type { TypeEasingOptions, TypeInterpolationFunction } from "./mod.ts"; 2 | /** 3 | * The type for interpolation functions which at an instant in the animation, generate the corresponding interpolated frame 4 | */ 5 | export interface IGenericBatchInterpolationFunction { 6 | (arr_t: number[], values: T, decimal?: number): TReturn; 7 | } 8 | /** 9 | * Given an Array of numbers, estimate the resulting number, at a `t` value between 0 to 1 10 | 11 | * Basic interpolation works by scaling `t` from 0 - 1, to some start number and end number, in this case lets use 12 | * 0 as our start number and 100 as our end number, so, basic interpolation would interpolate between 0 to 100. 13 | 14 | * If we use a `t` of 0.5, the interpolated value between 0 to 100, is 50. 15 | * {@link batchInterpolateNumber} takes it further, by allowing you to interpolate with more than 2 values, 16 | * it allows for multiple values. 17 | * E.g. Given an Array of values [0, 100, 0], and a `t` of 0.5, the interpolated value would become 100. 18 | 19 | * Based on d3.interpolateBasis [https://github.com/d3/d3-interpolate#interpolateBasis], 20 | * check out the link above for more detail. 21 | * 22 | * Buliding on-top of {@link batchInterpolateNumber}, `interpolateNumberBatch` interpolates between numbers, but unlike {@link batchInterpolateNumber} 23 | * `interpolateNumberBatch` uses an Array of `t` instances to generate an array of interpolated values 24 | * 25 | * @param arr_t Array of numbers (between 0 to 1) which each represent an instant of the interpolation 26 | * @param values Array of numbers to interpolate between 27 | * @param decimal How many decimals should the interpolated value have 28 | * @return Array of interpolated numbers at different instances 29 | * 30 | * @source Source code of `interpolateNumberBatch` 31 | */ 32 | export declare function batchInterpolateNumber(arr_t: number[], values: number[], decimal?: number): number[]; 33 | /** 34 | * Given an Array of items, find an item using `t` (which goes from 0 to 1), by 35 | * using `t` to estimate the index of said value in the array of `values`, 36 | * then expand that to encompass multiple `t`'s in an Array, 37 | * which returns Array items which each follow the interpolated index value 38 | 39 | * This is meant for interplolating strings that aren't number-like 40 | 41 | * @param arr_t Array of numbers (between 0 to 1) which each represent an instance of the interpolation 42 | * @param values Array of items to choose from 43 | * @return Array of Interpolated input Array items at different instances 44 | 45 | * @source Source code of `interpolateSequenceBatch` 46 | */ 47 | export declare function batchInterpolateSequence(arr_t: number[], values: T[]): T[]; 48 | /** 49 | * Alias of {@link batchInterpolateSequence} 50 | * @deprecated please use {@link batchInterpolateSequence}, it's the same functionality but different name 51 | */ 52 | export declare const batchInterpolateUsingIndex: typeof batchInterpolateSequence; 53 | /** 54 | * Functions the same way {@link batchInterpolateNumber} works. 55 | * Convert strings to numbers, and then interpolates the numbers, 56 | * at the end if there are units on the first value in the `values` array, 57 | * it will use that unit for the interpolated result. 58 | * Make sure to read {@link batchInterpolateNumber}. 59 | * 60 | * @source Source code of `interpolateStringBatch` 61 | */ 62 | export declare function batchInterpolateString(arr_t: number[], values: (string | number)[], decimal?: number): string[]; 63 | /** 64 | * Interpolates all types of values including number, string, etc... values. 65 | * Make sure to read {@link batchInterpolateNumber}, {@link batchInterpolateString} and {@link batchInterpolateSequence}, 66 | * as depending on the values given 67 | * 68 | * @source Source code of `interpolateComplexBatch` 69 | */ 70 | export declare function batchInterpolateComplex(arr_t: number[], values: T[], decimal?: number): number[] | string[] | T[]; 71 | /** 72 | * Generates an Array of values using frame functions which in turn create the effect of spring easing. 73 | * To use this properly make sure to set the easing animation option to "linear". 74 | * Check out a demo of SpringEasing at 75 | * 76 | * SpringEasing has 3 properties they are `easing` (all the easings from {@link EasingFunctions} are supported on top of frame functions like SpringFrame, SpringFrameOut, etc..), `numPoints` (the size of the Array the frame function should create), and `decimal` (the number of decimal places of the values within said Array). 77 | * 78 | * | Properties | Default Value | 79 | * | ----------- | ----------------------- | 80 | * | `easing` | `spring(1, 100, 10, 0)` | 81 | * | `numPoints` | `50` | 82 | * | `decimal` | `3` | 83 | * 84 | * By default, Spring Easing support easings in the form, 85 | * 86 | * | constant | accelerate | decelerate | accelerate-decelerate | decelerate-accelerate | 87 | * | :--------- | :----------------- | :------------- | :-------------------- | :-------------------- | 88 | * | | spring / spring-in | spring-out | spring-in-out | spring-out-in | 89 | * 90 | * All **Spring** easing's can be configured using theses parameters, 91 | * 92 | * `spring-*(mass, stiffness, damping, velocity)` 93 | * 94 | * Each parameter comes with these defaults 95 | * 96 | * | Parameter | Default Value | 97 | * | --------- | ------------- | 98 | * | mass | `1` | 99 | * | stiffness | `100` | 100 | * | damping | `10` | 101 | * | velocity | `0` | 102 | * 103 | * e.g. 104 | * ```ts 105 | * import { SpringEasing, SpringOutFrame } from "spring-easing"; 106 | * import anime from "animejs"; 107 | * 108 | * // Note: this is the return value of {@link SpringEasing} and {@link GenerateSpringFrames}, you don't need the object to get this format 109 | * let [translateX, duration] = SpringEasing([0, 250], { 110 | * easing: "spring-out-in(1, 100, 10, 0)", 111 | * 112 | * // You can change the size of Array for the SpringEasing function to generate 113 | * numPoints: 200, 114 | * 115 | * // The number of decimal places to round, final values in the generated Array 116 | * // This option doesn't exist on {@link GenerateSpringFrames} 117 | * decimal: 5, 118 | * }); 119 | * 120 | * anime({ 121 | * targets: "div", 122 | * 123 | * // Using spring easing animate from [0 to 250] using `spring-out-in` 124 | * translateX, 125 | * 126 | * // You can set the easing without an object 127 | * rotate: SpringEasing(["0turn", 1, 0, 0.5], [SpringOutFrame, 1, 100, 10, 0])[0], 128 | * 129 | * // TIP... Use linear easing for the proper effect 130 | * easing: "linear", 131 | * 132 | * // The optimal duration for this specific spring 133 | * duration 134 | * }) 135 | * ``` 136 | * 137 | * @param values Values to animate between, e.g. `["50px", 60]` 138 | * > _**Note**: You can interpolate with more than 2 values, but it's very confusing, so, it's best to choose 2_ 139 | * @param options Accepts {@link TypeEasingOptions EasingOptions} or {@link TypeEasingOptions.easing array frame functions} 140 | * @param interpolationFunction If you wish to use your own interpolation functions you may 141 | * @return 142 | * ```ts 143 | * // An array of keyframes that represent said spring animation and 144 | * // Total duration (in milliseconds) required to create a smooth spring animation 145 | * [ 146 | * [50, 55, 60, 70, 80, ...], 147 | * 3500 148 | * ] 149 | * ``` 150 | */ 151 | export declare function BatchSpringEasing(values: T, options?: TypeEasingOptions | TypeEasingOptions["easing"], customInterpolate?: IGenericBatchInterpolationFunction): readonly [TReturn, number]; 152 | /** 153 | * Converts interpolation functions written in this style `(t, values, decimal) => { ... }` 154 | * to work in the `BatchSpringEasing` 155 | * 156 | * You use it like so, 157 | * ```ts 158 | * import { BatchSpringEasing, toAnimationFrames, toFixed, scale, limit } from "spring-easing"; 159 | * 160 | * function interpolateNumber(t: number, values: number[], decimal = 3) { 161 | * // nth index 162 | * const n = values.length - 1; 163 | * 164 | * // The current index given t 165 | * const i = limit(Math.floor(t * n), 0, n - 1); 166 | * 167 | * const start = values[i]; 168 | * const end = values[i + 1]; 169 | * const progress = (t - i / n) * n; 170 | * 171 | * return toFixed(scale(progress, start, end), decimal); 172 | * } 173 | * 174 | * function interpolatePixels(t: number, values: number[], decimal = 3) { 175 | * const result = interpolateNumber(t, values, decimal); 176 | * return `${result}px`; 177 | * } 178 | * 179 | * BatchSpringEasing( 180 | * [0, 250], 181 | * 'spring', 182 | * toAnimationFrames(interpolatePixels) 183 | * ); 184 | * ``` 185 | */ 186 | export declare function toAnimationFrames(customInterpolate: TypeInterpolationFunction): (arr_t: number[], values: T, decimal?: number) => TReturn; 187 | //# sourceMappingURL=batch.d.ts.map -------------------------------------------------------------------------------- /lib/batch.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"batch.d.ts","sourceRoot":"","sources":["../../src/batch.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,yBAAyB,EAAE,MAAM,UAAU,CAAC;AAE7E;;GAEG;AACH,MAAM,WAAW,kCAAkC,CAAC,CAAC,SAAS,OAAO,EAAE,EAAE,OAAO,SAAS,OAAO,EAAE,GAAG,CAAC;IACpG,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACzD;AACD;;;;;;;;;;;;;;;;;;;;;;;EAuBE;AACF,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,OAAO,SAAI,YAuBpF;AAED;;;;;;;;;;;;;EAaE;AACF,wBAAgB,wBAAwB,CAAC,CAAC,EACxC,KAAK,EAAE,MAAM,EAAE,EACf,MAAM,EAAE,CAAC,EAAE,OAoBZ;AAED;;;GAGG;AACH,eAAO,MAAM,0BAA0B,iCAA2B,CAAC;AAEnE;;;;;;;;EAQE;AACF,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,MAAM,EAAE,EACf,MAAM,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,EAC3B,OAAO,SAAI,YAaZ;AAED;;;;;;EAME;AACF,wBAAgB,uBAAuB,CAAC,CAAC,EACvC,KAAK,EAAE,MAAM,EAAE,EACf,MAAM,EAAE,CAAC,EAAE,EACX,OAAO,SAAI,6BAuBZ;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+EG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,OAAO,EAAE,GAAG,MAAM,EAAE,EAAE,OAAO,SAAS,OAAO,EAAE,GAAG,CAAC,EAC7F,MAAM,EAAE,CAAC,EACT,OAAO,GAAE,iBAAiB,GAAG,iBAAiB,CAAC,QAAQ,CAAM,EAC7D,iBAAiB,GAAE,kCAAkC,CAAC,CAAC,EAAE,OAAO,CAAwF,8BASzJ;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,iBAAiB,CAAC,iBAAiB,EAAE,yBAAyB,0EACE,MAAM,EAAE,uBAAuB,MAAM,aAGpH"} -------------------------------------------------------------------------------- /lib/css-linear-easing.d.ts: -------------------------------------------------------------------------------- 1 | import type { TypeEasingOptions } from "./mod.ts"; 2 | /*! 3 | * Based off of https://github.com/jakearchibald/linear-easing-generator 4 | * 5 | * Changes: 6 | * - Added comments and docs top explain logic 7 | * - Switched to iterative approach for the `ramerDouglasPeucker` algorithim 8 | * - Renamed functions, parameters and variables to improve readability and to better match a library usecase 9 | * 10 | * Copyright 2023 Jake Archibald [@jakearchibald](https://github.com/jakearchibald) 11 | * 12 | * Licensed under the Apache License, Version 2.0 (the "License"); 13 | * you may not use this file except in compliance with the License. 14 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 15 | * 16 | * Unless required by applicable law or agreed to in writing, software 17 | * distributed under the License is distributed on an "AS IS" BASIS, 18 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 19 | * See the License for the specific language governing permissions and 20 | * limitations under the License. 21 | */ 22 | /** 23 | * Converts a given set of points into an array of strings in this format `["value percent%", ...]` e.g. `["0", "0.25 13.8%", "0.6 45.6%", "0.1", "0.4 60%", ...]`. 24 | * 25 | * @param points - The array of points to be converted. Each point is represented as a pair of numbers: [pos, val]. 26 | * @param round - The number of decimal places to which point values should be rounded. 27 | * 28 | * @returns The formatted points as an array of strings, or an empty array if the input was null. 29 | * 30 | * The function first checks if the input points are null. If they are, it returns an empty array. 31 | * If they are not null, the function does the following: 32 | * 33 | * - It creates a NumberFormat object for formatting the x and y values of the points to the specified number of decimal places. 34 | * The x values are formatted with 2 fewer decimal places than the y values. 35 | * 36 | * - It iterates over the points to find those for which the x value does not need to be stated explicitly. 37 | * The x value of a point does not need to be stated if it is 0 for the first point, 1 for the last point (provided the x value of the point before it is not greater than 1), 38 | * or the average of the x values of the previous and next points. 39 | * 40 | * - It groups the points into subarrays such that all points in a subarray have the same y value. 41 | * 42 | * - It iterates over the groups and, for each group, formats the y value and the x values of the points that need to be stated explicitly. 43 | * The formatted values are concatenated into a string, with the x values followed by the y value and separated by commas. 44 | * If a group contains more than one point, the function also creates a string in which the y value is followed 45 | * by the x values of the first and last points of the group, separated by a space. 46 | * This string is used if its length is shorter than the length of the string with all the x values. 47 | * 48 | * - The function returns the array of formatted strings. 49 | * 50 | * This function makes use of the Intl.NumberFormat object for formatting the numbers. This not only rounds the numbers to the specified 51 | * number of decimal places but also formats them according to the en-US locale. This means that the numbers will be formatted with a period 52 | * as the decimal separator and without thousands separators. 53 | * 54 | * This function also optimizes the representation of the points by omitting the x values where they are not needed and by grouping points with 55 | * the same y value. This can significantly reduce the length of the output strings when many points have the same y value or when the x values 56 | * are close to their expected values based on their position in the array. 57 | */ 58 | export declare function getLinearSyntax(points: [x: number, y: number][] | null, round: number): string[]; 59 | /** 60 | * CSS Spring Easing has 4 properties they are `easing` (all spring frame functions are supported), `numPoints` (the size of the Array the frmae function should create), `decimal` (the number of decimal places of the values within said Array) and `quality` (how detailed/smooth the spring easing should be). 61 | * 62 | * | Properties | Default Value | 63 | * | ----------- | ----------------------- | 64 | * | `easing` | `spring(1, 100, 10, 0)` | 65 | * | `numPoints` | `50` | 66 | * | `decimal` | `3` | 67 | * | `quality` | `0.85` | 68 | */ 69 | export type TypeCSSEasingOptions = { 70 | /** 71 | * Indicates how detailed/smooth the CSS `linear-easing()` function should be, it ranges between 0 - 1 72 | * 73 | * - 0 means it should basically not even bother 74 | * - 1 means it should be a detailed as possible such that human eyes are no longer able to distinguish 75 | */ 76 | quality?: number; 77 | } & TypeEasingOptions; 78 | /** 79 | * Generates a string that represents a set of values used with the linear-easing function to replicate spring animations, 80 | * you can check out the linear-easing playground here https://linear-easing-generator.netlify.app/ 81 | * Or check out a demo on Codepen https://codepen.io/okikio/pen/vYVaEXM 82 | * 83 | * CSS Spring Easing has 4 properties they are `easing` (all spring frame functions are supported), `numPoints` (the size of the Array the frmae function should create), `decimal` (the number of decimal places of the values within said Array) and `quality` (how detailed/smooth the spring easing should be).. 84 | * 85 | * | Properties | Default Value | 86 | * | ----------- | ----------------------- | 87 | * | `easing` | `spring(1, 100, 10, 0)` | 88 | * | `numPoints` | `50` | 89 | * | `decimal` | `3` | 90 | * | `quality` | `0.85` | 91 | * 92 | * By default, CSS Spring Easing support easings in the form, 93 | * 94 | * | constant | accelerate | decelerate | accelerate-decelerate | decelerate-accelerate | 95 | * | :--------- | :----------------- | :------------- | :-------------------- | :-------------------- | 96 | * | | spring / spring-in | spring-out | spring-in-out | spring-out-in | 97 | * 98 | * All **Spring** easing's can be configured using theses parameters, 99 | * 100 | * `spring-*(mass, stiffness, damping, velocity)` 101 | * 102 | * Each parameter comes with these defaults 103 | * 104 | * | Parameter | Default Value | 105 | * | --------- | ------------- | 106 | * | mass | `1` | 107 | * | stiffness | `100` | 108 | * | damping | `10` | 109 | * | velocity | `0` | 110 | * 111 | * e.g. 112 | * ```ts 113 | * import { CSSSpringEasing } from "spring-easing"; 114 | * 115 | * // Note: this is the return value of {@link CSSSpringEasing}, you don't need the object to get this format 116 | * let [easing, duration] = CSSSpringEasing({ 117 | * easing: "spring-out-in(1, 100, 10, 0)", 118 | * 119 | * // You can change the size of Array for the SpringEasing function to generate 120 | * numPoints: 200, 121 | * 122 | * // The number of decimal places to round, final values in the generated Array 123 | * // This option doesn't exist on {@link GenerateSpringFrames} 124 | * decimal: 5, 125 | * 126 | * // How detailed/smooth the spring easing should be 127 | * // 0 means not smooth at all (shorter easing string) 128 | * // 1 means as smooth as possible (this means the resulting easing will be a longer string) 129 | * quality: 0.85 130 | * }); 131 | * 132 | * document.querySelector("div").animate({ 133 | * translate: ["0px", "250px"], 134 | * rotate: ["0turn", "1turn", "0turn", "0.5turn"], 135 | * }, { 136 | * easing: `linear(${easing})`, 137 | * 138 | * // The optimal duration for this specific spring 139 | * duration 140 | * }) 141 | * ``` 142 | * 143 | * > **Note**: You can also use custom easings if you so wish e.g. 144 | * ```ts 145 | * import { CSSSpringEasing, limit, registerEasingFunctions } from "spring-easing"; 146 | * 147 | * registerEasingFunctions({ 148 | * bounce: t => { 149 | * let pow2: number, b = 4; 150 | * while (t < ((pow2 = Math.pow(2, --b)) - 1) / 11) { } 151 | * return 1 / Math.pow(4, 3 - b) - 7.5625 * Math.pow((pow2 * 3 - 2) / 22 - t, 2); 152 | * }, 153 | * elastic: (t, params: number[] = []) => { 154 | * let [amplitude = 1, period = 0.5] = params; 155 | * const a = limit(amplitude, 1, 10); 156 | * const p = limit(period, 0.1, 2); 157 | * if (t === 0 || t === 1) return t; 158 | * return -a * 159 | * Math.pow(2, 10 * (t - 1)) * 160 | * Math.sin( 161 | * ((t - 1 - (p / (Math.PI * 2)) * Math.asin(1 / a)) * (Math.PI * 2)) / p 162 | * ); 163 | * } 164 | * }); 165 | * 166 | * CSSSpringEasing("bounce") // ["0, 0.013, 0.015, 0.006 8.1%, 0.046 13.5%, 0.06, 0.062, 0.054, 0.034, 0.003 27%, 0.122, 0.206 37.8%, 0.232, 0.246, 0.25, 0.242, 0.224, 0.194, 0.153 56.8%, 0.039 62.2%, 0.066 64.9%, 0.448 73%, 0.646, 0.801 83.8%, 0.862 86.5%, 0.95 91.9%, 0.978, 0.994, 1", ...] 167 | * CSSSpringEasing("elastic(1, 0.5)") // ["0, -0.005 32.4%, 0.006 40.5%, 0.034 51.4%, 0.033 56.8%, 0.022, 0.003, -0.026 64.9%, -0.185 75.7%, -0.204, -0.195, -0.146, -0.05, 0.1 89.2%, 1", ...] 168 | * ``` 169 | * 170 | * @param options Accepts {@link TypeCSSEasingOptions EasingOptions} or {@link TypeCSSEasingOptions["easing"] array frame functions} 171 | * @return 172 | * ```ts 173 | * // A string with the values that represent said spring animation using the linear-easing function 174 | * // Total duration (in milliseconds) required to create a smooth spring animation 175 | * [ 176 | * "0, 0.583 5.4%, 0.669, 0.601, 0.502, 0.451, 0.457 18.9%, 0.512 24.3%, 0.516 27%, ...0.417 94.6%, 1", 177 | * 3500 178 | * ] 179 | * ``` 180 | */ 181 | export declare function CSSSpringEasing(options?: TypeCSSEasingOptions | TypeCSSEasingOptions["easing"]): readonly [string, number]; 182 | //# sourceMappingURL=css-linear-easing.d.ts.map -------------------------------------------------------------------------------- /lib/css-linear-easing.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"css-linear-easing.d.ts","sourceRoot":"","sources":["../../src/css-linear-easing.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAElD;;;;;;;;;;;;;;;;;;;GAmBG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,CAAC,EAAE,GAAG,IAAI,EACvC,KAAK,EAAE,MAAM,GACZ,MAAM,EAAE,CAkFV;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,oBAAoB,GAAG;IACjC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,GAAG,iBAAiB,CAAC;AAEtB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsGG;AACH,wBAAgB,eAAe,CAC7B,OAAO,GAAE,oBAAoB,GAAG,oBAAoB,CAAC,QAAQ,CAAM,6BAgBpE"} -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./mod.ts"; 2 | export { default } from "./mod.ts"; 3 | //# sourceMappingURL=index.d.ts.map -------------------------------------------------------------------------------- /lib/index.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC"} -------------------------------------------------------------------------------- /lib/mod.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../../src/mod.ts"],"names":[],"mappings":"AAIA,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,wBAAwB,CAAC;AAEvC;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAC9B,CAAC,EAAE,MAAM,EACT,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,EAC/C,QAAQ,CAAC,EAAE,MAAM,KACd,MAAM,CAAC;AAEZ;;GAEG;AACH,MAAM,MAAM,yBAAyB,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,MAAM,GAAG,MAAM,GAAG,GAAG,CAAC;AAE9G;;;;;;;;;;;;;;;;;;;;;;;EAuBE;AACF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,WAAW,EAAE,iBAwBzB,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,mBAAmB,EAAE,GAAG,CACnC,MAAM,EACN;IAAC,MAAM;IAAE,MAAM;CAAC,CACL,CAAC;AAEd;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,SAAU,CAAC;AAE3C;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,CAAC,GAAE,MAAM,EAAO,YAgDpF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,iBAAiB,GAAG,iBAAiB,CAEnE;AAED;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,iBAAiB,GAAG,iBAAiB,CAMrE;AAED;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,iBAAiB,GAAG,iBAAiB,CAMrE;AAED;;;GAGG;AACH,eAAO,MAAM,aAAa,mBAAc,CAAC;AAEzC;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,mBAAuB,CAAC;AAEnD;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,mBAAyB,CAAC;AAEvD;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,mBAAyB,CAAC;AAEvD;;;;;;;;;;;;;;;;;;;;EAoBE;AACF,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,OAAO,SAAI,UAYzE;AAED;;;;;;;;;;;EAWE;AACF,wBAAgB,mBAAmB,CAAC,CAAC,EACnC,CAAC,EAAE,MAAM,EACT,MAAM,EAAE,CAAC,EAAE,KAWZ;AAED;;;GAGG;AACH,eAAO,MAAM,qBAAqB,4BAAsB,CAAC;AAEzD;;;;;;;;EAQE;AACF,wBAAgB,iBAAiB,CAC/B,CAAC,EAAE,MAAM,EACT,MAAM,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,EAC3B,OAAO,SAAI,UAaZ;AAED;;;;;EAKE;AACF,wBAAgB,kBAAkB,CAAC,CAAC,EAClC,CAAC,EAAE,MAAM,EACT,MAAM,EAAE,CAAC,EAAE,EACX,OAAO,SAAI,uBAaZ;AAED;;;;GAIG;AACH,MAAM,MAAM,4BAA4B,GAAG,CAAC,iBAAiB,EAAE,GAAG,MAAM,EAAE,CAAC,CAAC;AAE5E;;GAEG;AACH,eAAO,IAAI,eAAe;;;;;;CAMzB,CAAC;AAEF,eAAO,IAAI,kBAAkB,UAA+B,CAAC;AAE7D;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,SAAS,MAAM,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,iBAAiB,QAGtF;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,EAAE,GAAG,EAAE,CAAC,QAG1F;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,WAAW,GAAG,GAAG,MAAM,OAAO,eAAe,EAAE,GAAG,GAAG,MAAM,OAAO,eAAe,IAAI,MAAM,GAAG,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,GAAG,4BAA4B,CAAC;AAE1J;;;;;;;;GAQG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,uBAQhD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,iBAAiB,EACvD,OAAO,GAAE,CAAC,GAAG,iBAAiB,CAAC,QAAQ,CAAW;;;;gDAsBnD;AAED;;GAEG;AACH,eAAO,MAAM,aAAa,oDAA2D,CAAC;AAEtF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,GAAE,iBAAsB,GAAG,CAAC,MAAM,EAAE,EAAE,MAAM,CAAC,CAkDxF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+EG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAC5B,MAAM,EAAE,CAAC,EAAE,EACX,OAAO,GAAE,iBAAiB,GAAG,iBAAiB,CAAC,QAAQ,CAAM,EAC7D,iBAAiB,GAAE,yBAA8C,4BASlE;AAED,eAAe,YAAY,CAAC"} -------------------------------------------------------------------------------- /lib/optimize.d.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Based off of https://github.com/jakearchibald/linear-easing-generator 3 | * 4 | * Changes: 5 | * - Added comments and docs top explain logic 6 | * - Switched to iterative approach for the `ramerDouglasPeucker` algorithim 7 | * - Renamed functions, parameters and variables to improve readability and to better match a library usecase 8 | * 9 | * Copyright 2023 Jake Archibald [@jakearchibald](https://github.com/jakearchibald) 10 | * 11 | * Licensed under the Apache License, Version 2.0 (the "License"); 12 | * you may not use this file except in compliance with the License. 13 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | */ 21 | /** 22 | * The function calculates the squared distance from a point to a line segment. 23 | * Using squared distances avoids costly square root operations and doesn't 24 | * affect the result because we're only interested in relative distances. 25 | * 26 | * @param point The point from which distance is to be measured 27 | * @param lineStart The start point of the line segment 28 | * @param lineEnd The end point of the line segment 29 | * @returns The squared distance from the point to the line segment 30 | */ 31 | export declare function squaredSegmentDistance(point: [number, number], lineStart: [number, number], lineEnd: [number, number]): number; 32 | /** 33 | * Simplify a line given an array of points and a tolerance using the Ramer-Douglas-Peucker algorithm. 34 | * The tolerance determines the maximum allowed perpendicular distance from a point to the line segment 35 | * connecting its neighboring points. Points with a greater distance are included in the simplified line, 36 | * while points with a smaller distance are excluded. 37 | * 38 | * This version of the function uses an iterative approach with a stack instead of recursion. 39 | * 40 | * The iterative approach using a stack doesn't guarantee that the points will be processed in the same 41 | * order as the recursive approach. Because of the way points are pushed onto the stack, 42 | * the algorithm could sometimes process points out of order. 43 | * 44 | * To fix this before returning the final result we sort the 45 | * simplified points in increasing order of x values before returning them. 46 | * 47 | * @param points The array of points to be simplified 48 | * @param tolerance The maximum allowed perpendicular distance from a point to the line segment 49 | * connecting its neighboring points 50 | * @returns The simplified line as an array of points, it sorts the simplified points in increasing order of x values before returning them. 51 | */ 52 | export declare function ramerDouglasPeucker(points: [number, number][], tolerance: number): [number, number][]; 53 | /** 54 | * Simplifies a given set of points using the Ramer-Douglas-Peucker algorithm and 55 | * rounds the x and y values of the resulting points. 56 | * 57 | * @param fullPoints - The array of points to be simplified. Each point is represented as a pair of numbers: [pos, val]. 58 | * @param simplify - The maximum allowed perpendicular distance from a point to the line segment. 59 | * @param round - The number of decimal places to which point values should be rounded. 60 | * 61 | * @returns The simplified and rounded points, or null if the input was null. 62 | * 63 | * The function first checks if the input points are null. If they are, it returns null. 64 | * If they are not null, the function applies the Ramer-Douglas-Peucker algorithm to the points using the specified tolerance. 65 | * Then it rounds the x and y values of the resulting points to the specified number of decimal places. 66 | * The x values are always rounded to at least 2 decimal places because they are represented as a percentage. 67 | */ 68 | export declare function getOptimizedPoints(fullPoints: [x: number, y: number][] | null, simplify: number, round: number): [x: number, y: number][] | null; 69 | //# sourceMappingURL=optimize.d.ts.map -------------------------------------------------------------------------------- /lib/optimize.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"optimize.d.ts","sourceRoot":"","sources":["../../src/optimize.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAyB9H;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CA2CrG;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,CAAC,EAAE,GAAG,IAAI,EAC3C,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,GACZ,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,CAAC,EAAE,GAAG,IAAI,CAWjC"} -------------------------------------------------------------------------------- /lib/utils.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * If a value can be converted to a valid number, then it's most likely a number 3 | * 4 | * @source Source code of `isNumberLike` 5 | */ 6 | export declare function isNumberLike(num: string | number): boolean; 7 | /** 8 | * Limit a number to a minimum of `min` and a maximum of `max` 9 | * 10 | * @source Source code of `limit` 11 | * 12 | * @param value number to limit 13 | * @param min minimum limit 14 | * @param max maximum limit 15 | * @returns limited/constrained number 16 | */ 17 | export declare function limit(value: number, min: number, max: number): number; 18 | /** 19 | * map `t` from 0 to 1, to `start` to `end` 20 | * 21 | * @source Source code of `scale` 22 | */ 23 | export declare function scale(t: number, start: number, end: number): number; 24 | /** 25 | * Rounds numbers to a fixed decimal place 26 | * 27 | * @source Source code of `toFixed` 28 | */ 29 | export declare function toFixed(value: number, decimal: number): number; 30 | /** 31 | * Returns the unit of a string, it does this by removing the number in the string 32 | * 33 | * @source Source code of `getUnit` 34 | */ 35 | export declare function getUnit(str: string | number): string; 36 | //# sourceMappingURL=utils.d.ts.map -------------------------------------------------------------------------------- /lib/utils.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/utils.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,WAGhD;AAED;;;;;;;;;GASG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,UAE5D;AAED;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,UAE1D;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,UAErD;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,UAG3C"} -------------------------------------------------------------------------------- /media/_headers: -------------------------------------------------------------------------------- 1 | /* 2 | X-Frame-Options: SAMEORIGIN 3 | X-Content-Type-Options: nosniff 4 | X-XSS-Protection: 1; mode=block 5 | Referrer-Policy: strict-origin-when-cross-origin 6 | Strict-Transport-Security: max-age=63072000; includeSubDomains; preload 7 | Cache-Control: max-age=1360, stale-while-revalidate=480, public 8 | Accept-CH: DPR, Viewport-Width, Width 9 | X-UA-Compatible: IE=edge 10 | Content-Security-Policy: default-src 'self'; font-src 'self' https://fonts.gstatic.com; style-src 'self' https://fonts.googleapis.com/ 'unsafe-inline'; img-src 'self' https://gitpod.io/ https://bundlejs.com data:; script-src 'self' https://bundlejs.com https://*.bundlejs.com 'unsafe-inline' 'unsafe-eval'; connect-src 'self' https:; block-all-mixed-content; upgrade-insecure-requests; base-uri 'self'; object-src 'none'; worker-src 'self'; manifest-src 'self'; media-src 'self' https://res.cloudinary.com/; form-action 'self'; frame-ancestors 'self' https:; 11 | Permissions-Policy: geolocation=(), microphone=(), usb=(), sync-xhr=(self), camera=(), interest-cohort=() 12 | Link: ; rel=preload; as=font; type=font/ttf; crossorigin=anonymous 13 | Link: ; rel=preload; as=style 14 | Link: ; rel=preload; as=font; type=font/woff2; crossorigin=anonymous 15 | # Link: ; rel=preload; as=style 16 | # Link: ; rel=preload; as=font; type=font/woff2; crossorigin=anonymous 17 | 18 | /*.css 19 | Cache-Control: public, max-age=604800, stale-while-revalidate=480 20 | Content-Type: text/css 21 | 22 | /*.ttf 23 | Cache-Control: public, max-age=31536000, stale-while-revalidate=23480 24 | Content-Type: font/ttf 25 | 26 | /*.woff2 27 | Cache-Control: public, max-age=31536000, stale-while-revalidate=23480 28 | Content-Type: font/woff2 29 | 30 | /*.js 31 | Cache-Control: public, max-age=604800, stale-while-revalidate=480 32 | Content-Type: text/javascript 33 | 34 | /manifest.json 35 | Cache-Control: public, max-age=604800, stale-while-revalidate=480 36 | Content-Type: application/manifest+json 37 | 38 | /assets/* 39 | Cache-Control: public, max-age=31536000, stale-while-revalidate=23480 40 | 41 | /*.svg 42 | Cache-Control: public, max-age=31536000, stale-while-revalidate=23480 43 | Content-Type: image/svg+xml 44 | -------------------------------------------------------------------------------- /media/assets/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /media/assets/spring-easing-demo-video.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/spring-easing/707097383d5dc063092bdb3eef4a237e06640eeb/media/assets/spring-easing-demo-video.gif -------------------------------------------------------------------------------- /media/assets/spring-easing-demo-video.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/spring-easing/707097383d5dc063092bdb3eef4a237e06640eeb/media/assets/spring-easing-demo-video.mp4 -------------------------------------------------------------------------------- /media/assets/spring-easing-demo-video.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/spring-easing/707097383d5dc063092bdb3eef4a237e06640eeb/media/assets/spring-easing-demo-video.webm -------------------------------------------------------------------------------- /media/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/spring-easing/707097383d5dc063092bdb3eef4a237e06640eeb/media/favicon.ico -------------------------------------------------------------------------------- /media/fonts/FluentSystemIcons-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/spring-easing/707097383d5dc063092bdb3eef4a237e06640eeb/media/fonts/FluentSystemIcons-Regular.ttf -------------------------------------------------------------------------------- /media/fonts/FluentSystemIcons-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/spring-easing/707097383d5dc063092bdb3eef4a237e06640eeb/media/fonts/FluentSystemIcons-Regular.woff2 -------------------------------------------------------------------------------- /media/measure.js: -------------------------------------------------------------------------------- 1 | (window => { 2 | const { 3 | screen: { width, height }, 4 | navigator: { language }, 5 | location, 6 | localStorage, 7 | document, 8 | history, 9 | } = window; 10 | const { hostname, pathname, search } = location; 11 | const { currentScript } = document; 12 | 13 | if (!currentScript) return; 14 | 15 | const assign = (a, b) => { 16 | Object.keys(b).forEach(key => { 17 | if (b[key] !== undefined) a[key] = b[key]; 18 | }); 19 | return a; 20 | }; 21 | 22 | const hook = (_this, method, callback) => { 23 | const orig = _this[method]; 24 | 25 | return (...args) => { 26 | callback.apply(null, args); 27 | 28 | return orig.apply(_this, args); 29 | }; 30 | }; 31 | 32 | const doNotTrack = () => { 33 | const { doNotTrack, navigator, external } = window; 34 | 35 | const msTrackProtection = 'msTrackingProtectionEnabled'; 36 | const msTracking = () => { 37 | return external && msTrackProtection in external && external[msTrackProtection](); 38 | }; 39 | 40 | const dnt = doNotTrack || navigator.doNotTrack || navigator.msDoNotTrack || msTracking(); 41 | 42 | return dnt == '1' || dnt === 'yes'; 43 | }; 44 | 45 | const trackingDisabled = () => 46 | (localStorage && localStorage.getItem('umami.disabled')) || 47 | (dnt && doNotTrack()) || 48 | (domain && !domains.includes(hostname)); 49 | 50 | const _data = 'data-'; 51 | const _false = 'false'; 52 | const attr = currentScript.getAttribute.bind(currentScript); 53 | const website = attr(_data + 'website-id'); 54 | const hostUrl = attr(_data + 'host-url'); 55 | const autoTrack = attr(_data + 'auto-track') !== _false; 56 | const dnt = attr(_data + 'do-not-track'); 57 | const cssEvents = attr(_data + 'css-events') !== _false; 58 | const domain = attr(_data + 'domains') || ''; 59 | const domains = domain.split(',').map(n => n.trim()); 60 | const root = hostUrl 61 | ? hostUrl.replace(/\/$/, '') 62 | : currentScript.src.split('/').slice(0, -1).join('/'); 63 | const endpoint = `${root}/take-measurement`; // "/api/collect"; 64 | const screen = `${width}x${height}`; 65 | const eventClass = /^umami--([a-z]+)--([\w]+[\w-]*)$/; 66 | const eventSelect = "[class*='umami--']"; 67 | 68 | let listeners = {}; 69 | let currentUrl = `${pathname}${search}`; 70 | let currentRef = document.referrer; 71 | let cache; 72 | 73 | /* Collect metrics */ 74 | 75 | const getPayload = () => ({ 76 | website, 77 | hostname, 78 | screen, 79 | language, 80 | url: currentUrl, 81 | }); 82 | 83 | const collect = (type, payload) => { 84 | if (trackingDisabled()) return; 85 | 86 | return fetch(endpoint, { 87 | method: 'POST', 88 | body: JSON.stringify({ type, payload }), 89 | headers: assign({ 'Content-Type': 'application/json' }, { ['x-umami-cache']: cache }), 90 | }) 91 | .then(res => res.text()) 92 | .then(text => (cache = text)); 93 | }; 94 | 95 | const trackView = (url = currentUrl, referrer = currentRef, websiteUuid = website) => 96 | collect( 97 | 'pageview', 98 | assign(getPayload(), { 99 | website: websiteUuid, 100 | url, 101 | referrer, 102 | }), 103 | ); 104 | 105 | const trackEvent = (eventName, eventData, url = currentUrl, websiteUuid = website) => 106 | collect( 107 | 'event', 108 | assign(getPayload(), { 109 | website: websiteUuid, 110 | url, 111 | event_name: eventName, 112 | event_data: eventData, 113 | }), 114 | ); 115 | 116 | /* Handle events */ 117 | 118 | const addEvents = node => { 119 | const elements = node.querySelectorAll(eventSelect); 120 | Array.prototype.forEach.call(elements, addEvent); 121 | }; 122 | 123 | const addEvent = element => { 124 | const get = element.getAttribute.bind(element); 125 | (get('class') || '').split(' ').forEach(className => { 126 | if (!eventClass.test(className)) return; 127 | 128 | const [, event, name] = className.split('--'); 129 | 130 | const listener = listeners[className] 131 | ? listeners[className] 132 | : (listeners[className] = e => { 133 | if ( 134 | event === 'click' && 135 | element.tagName === 'A' && 136 | !( 137 | e.ctrlKey || 138 | e.shiftKey || 139 | e.metaKey || 140 | (e.button && e.button === 1) || 141 | get('target') 142 | ) 143 | ) { 144 | e.preventDefault(); 145 | trackEvent(name).then(() => { 146 | const href = get('href'); 147 | if (href) { 148 | location.href = href; 149 | } 150 | }); 151 | } else { 152 | trackEvent(name); 153 | } 154 | }); 155 | 156 | element.addEventListener(event, listener, true); 157 | }); 158 | }; 159 | 160 | /* Handle history changes */ 161 | 162 | const handlePush = (state, title, url) => { 163 | if (!url) return; 164 | 165 | currentRef = currentUrl; 166 | const newUrl = url.toString(); 167 | 168 | if (newUrl.substring(0, 4) === 'http') { 169 | currentUrl = '/' + newUrl.split('/').splice(3).join('/'); 170 | } else { 171 | currentUrl = newUrl; 172 | } 173 | 174 | if (currentUrl !== currentRef) { 175 | trackView(); 176 | } 177 | }; 178 | 179 | const observeDocument = () => { 180 | const monitorMutate = mutations => { 181 | mutations.forEach(mutation => { 182 | const element = mutation.target; 183 | addEvent(element); 184 | addEvents(element); 185 | }); 186 | }; 187 | 188 | const observer = new MutationObserver(monitorMutate); 189 | observer.observe(document, { childList: true, subtree: true }); 190 | }; 191 | 192 | /* Global */ 193 | 194 | if (!window.umami) { 195 | const umami = eventValue => trackEvent(eventValue); 196 | umami.trackView = trackView; 197 | umami.trackEvent = trackEvent; 198 | 199 | window.umami = umami; 200 | } 201 | 202 | /* Start */ 203 | 204 | if (autoTrack && !trackingDisabled()) { 205 | history.pushState = hook(history, 'pushState', handlePush); 206 | history.replaceState = hook(history, 'replaceState', handlePush); 207 | 208 | const update = () => { 209 | if (document.readyState === 'complete') { 210 | trackView(); 211 | 212 | if (cssEvents) { 213 | addEvents(document); 214 | observeDocument(); 215 | } 216 | } 217 | }; 218 | 219 | document.addEventListener('readystatechange', update, true); 220 | 221 | update(); 222 | } 223 | })(window); 224 | 225 | 226 | 227 | // (window => { 228 | // const apiRoute = "/take-measurement"; // "/api/collect"; 229 | // const { screen: { width, height }, navigator: { language }, location: { hostname, pathname, search }, localStorage, document, history, } = window; 230 | // const script = document.querySelector('script[data-website-id]'); 231 | // if (!script) 232 | // return; 233 | // const attr = script.getAttribute.bind(script); 234 | // const website = attr('data-website-id'); 235 | // const hostUrl = attr('data-host-url'); 236 | // const autoTrack = attr('data-auto-track') !== 'false'; 237 | // const dnt = attr('data-do-not-track'); 238 | // const cssEvents = attr('data-css-events') !== 'false'; 239 | // const domain = attr('data-domains') || ''; 240 | // const domains = domain.split(',').map(n => n.trim()); 241 | // const eventClass = /^umami--([a-z]+)--([\w]+[\w-]*)$/; 242 | // const eventSelect = "[class*='umami--']"; 243 | // const trackingDisabled = () => (localStorage && localStorage.getItem('umami.disabled')) || 244 | // (dnt && doNotTrack()) || 245 | // (domain && !domains.includes(hostname)); 246 | // const root = hostUrl 247 | // ? removeTrailingSlash(hostUrl) 248 | // : ""; // script.src.split('/').slice(0, -1).join('/'); 249 | // const screen = `${width}x${height}`; 250 | // const listeners = {}; 251 | // let currentUrl = `${pathname}${search}`; 252 | // let currentRef = document.referrer; 253 | // let cache; 254 | // /* Collect metrics */ 255 | // const post = (url, data, callback) => { 256 | // const req = new XMLHttpRequest(); 257 | // req.open('POST', url, true); 258 | // req.setRequestHeader('Content-Type', 'application/json'); 259 | // if (cache) 260 | // req.setRequestHeader('x-umami-cache', cache); 261 | // req.onreadystatechange = () => { 262 | // if (req.readyState === 4) { 263 | // callback(req.response); 264 | // } 265 | // }; 266 | // req.send(JSON.stringify(data)); 267 | // }; 268 | // const getPayload = () => ({ 269 | // website, 270 | // hostname, 271 | // screen, 272 | // language, 273 | // url: currentUrl, 274 | // }); 275 | // const assign = (a, b) => { 276 | // Object.keys(b).forEach(key => { 277 | // a[key] = b[key]; 278 | // }); 279 | // return a; 280 | // }; 281 | // const collect = (type, payload) => { 282 | // if (trackingDisabled()) 283 | // return; 284 | // post(`${root}${apiRoute}`, { 285 | // type, 286 | // payload, 287 | // }, res => (cache = res)); 288 | // }; 289 | // const trackView = (url = currentUrl, referrer = currentRef, uuid = website) => { 290 | // collect('pageview', assign(getPayload(), { 291 | // website: uuid, 292 | // url, 293 | // referrer, 294 | // })); 295 | // }; 296 | // const trackEvent = (event_value, event_type = 'custom', url = currentUrl, uuid = website) => { 297 | // collect('event', assign(getPayload(), { 298 | // website: uuid, 299 | // url, 300 | // event_type, 301 | // event_value, 302 | // })); 303 | // }; 304 | // /* Handle events */ 305 | // const sendEvent = (value, type) => { 306 | // const payload = getPayload(); 307 | // // @ts-ignore 308 | // payload.event_type = type; 309 | // // @ts-ignore 310 | // payload.event_value = value; 311 | // const data = JSON.stringify({ 312 | // type: 'event', 313 | // payload, 314 | // }); 315 | // navigator.sendBeacon(`${root}${apiRoute}`, data); 316 | // }; 317 | // const addEvents = node => { 318 | // const elements = node.querySelectorAll(eventSelect); 319 | // Array.prototype.forEach.call(elements, addEvent); 320 | // }; 321 | // const addEvent = element => { 322 | // (element.getAttribute('class') || '').split(' ').forEach(className => { 323 | // if (!eventClass.test(className)) 324 | // return; 325 | // const [, type, value] = className.split('--'); 326 | // const listener = listeners[className] 327 | // ? listeners[className] 328 | // : (listeners[className] = () => { 329 | // if (element.tagName === 'A') { 330 | // sendEvent(value, type); 331 | // } 332 | // else { 333 | // trackEvent(value, type); 334 | // } 335 | // }); 336 | // element.addEventListener(type, listener, true); 337 | // }); 338 | // }; 339 | // /* Handle history changes */ 340 | // const handlePush = (state, title, url) => { 341 | // if (!url) 342 | // return; 343 | // currentRef = currentUrl; 344 | // const newUrl = url.toString(); 345 | // if (newUrl.substring(0, 4) === 'http') { 346 | // currentUrl = '/' + newUrl.split('/').splice(3).join('/'); 347 | // } 348 | // else { 349 | // currentUrl = newUrl; 350 | // } 351 | // if (currentUrl !== currentRef) { 352 | // trackView(); 353 | // } 354 | // }; 355 | // const observeDocument = () => { 356 | // const monitorMutate = mutations => { 357 | // mutations.forEach(mutation => { 358 | // const element = mutation.target; 359 | // addEvent(element); 360 | // addEvents(element); 361 | // }); 362 | // }; 363 | // const observer = new MutationObserver(monitorMutate); 364 | // observer.observe(document, { childList: true, subtree: true }); 365 | // }; 366 | // /* Global */ 367 | // // @ts-ignore 368 | // if (!window.umami) { 369 | // const umami = eventValue => trackEvent(eventValue); 370 | // umami.trackView = trackView; 371 | // umami.trackEvent = trackEvent; 372 | // // @ts-ignore 373 | // window.umami = umami; 374 | // } 375 | // /* Start */ 376 | // if (autoTrack && !trackingDisabled()) { 377 | // history.pushState = hook(history, 'pushState', handlePush); 378 | // history.replaceState = hook(history, 'replaceState', handlePush); 379 | // const update = () => { 380 | // if (document.readyState === 'complete') { 381 | // trackView(); 382 | // if (cssEvents) { 383 | // addEvents(document); 384 | // observeDocument(); 385 | // } 386 | // } 387 | // }; 388 | // document.addEventListener('readystatechange', update, true); 389 | // update(); 390 | // } 391 | // })(window); 392 | 393 | -------------------------------------------------------------------------------- /media/robots.txt: -------------------------------------------------------------------------------- 1 | Sitemap: https://spring-easing.okikio.dev/sitemap.xml 2 | User-agent: * 3 | Allow: / 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spring-easing", 3 | "version": "2.3.3", 4 | "type": "module", 5 | "sideEffects": false, 6 | "description": "Quick and easy spring animations. Works with other animation libraries (animejs, framer motion, motion one, @okikio/animate, etc...) or the Web Animation API (WAAPI).", 7 | "umd": "SpringEasing", 8 | "access": "public", 9 | "legacy": "lib/index.js", 10 | "main": "lib/index.cjs", 11 | "types": "lib/index.d.ts", 12 | "browser": "lib/index.mjs", 13 | "module": "lib/index.mjs", 14 | "exports": { 15 | ".": { 16 | "types": "./lib/index.d.ts", 17 | "require": "./lib/index.cjs", 18 | "import": "./lib/index.mjs", 19 | "browser": "./lib/index.mjs", 20 | "unpkg": "./lib/index.mjs", 21 | "legacy": "./lib/index.js", 22 | "default": "./lib/index.mjs" 23 | }, 24 | "./lib/*": "./lib/*", 25 | "./src/*": "./src/*", 26 | "./package.json": "./package.json" 27 | }, 28 | "directories": { 29 | "lib": "./lib", 30 | "src": "./src" 31 | }, 32 | "files": [ 33 | "lib", 34 | "src" 35 | ], 36 | "scripts": { 37 | "typedoc": "esbuild ./.typedoc/typedoc.tsx --format=cjs --outfile=./.typedoc/typedoc.cjs && typedoc && esno ./.typedoc/rehype.ts", 38 | "preview": "vite docs", 39 | "start": "vite docs", 40 | "fixtures": "vite tests/fixture", 41 | "test": "vitest", 42 | "test:run": "vitest run", 43 | "repl": "esno repl.ts", 44 | "build": "vite build", 45 | "pre-release": "pnpm test:run && pnpm build && pnpm typedoc", 46 | "semantic-release": "semantic-release" 47 | }, 48 | "changelog": { 49 | "repo": "spring-easing", 50 | "labels": { 51 | "breaking": ":boom: Breaking Change", 52 | "enhancement": ":rocket: Enhancement", 53 | "bug": ":bug: Bug Fix", 54 | "documentation": ":memo: Documentation", 55 | "internal": ":house: Internal", 56 | "revert": ":rewind: Revert" 57 | }, 58 | "cacheDir": ".changelog" 59 | }, 60 | "husky": { 61 | "hooks": { 62 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS --verbose" 63 | } 64 | }, 65 | "repository": { 66 | "type": "git", 67 | "url": "https://github.com/okikio/spring-easing.git" 68 | }, 69 | "keywords": [ 70 | "spring-easing", 71 | "typescript", 72 | "animation", 73 | "easing", 74 | "spring", 75 | "framer-motion", 76 | "motion-one", 77 | "@okikio/animate", 78 | "web-animation-api", 79 | "waapi", 80 | "linear-easing", 81 | "css-spring-easing", 82 | "custom-easing" 83 | ], 84 | "author": { 85 | "name": "Okiki Ojo", 86 | "email": "hey@okikio.dev", 87 | "url": "https://okikio.dev" 88 | }, 89 | "license": "MIT", 90 | "bugs": { 91 | "url": "https://github.com/okikio/spring-easing/issues" 92 | }, 93 | "homepage": "https://spring-easing.okikio.dev", 94 | "devDependencies": { 95 | "@commitlint/cli": "^17.6.3", 96 | "@commitlint/config-conventional": "^17.6.3", 97 | "@semantic-release/changelog": "^6.0.3", 98 | "@semantic-release/git": "^10.0.1", 99 | "@types/node": "^20.2.1", 100 | "@types/web": "^0.0.99", 101 | "esbuild": "^0.17.19", 102 | "esno": "^0.16.3", 103 | "fast-glob": "^3.2.12", 104 | "hastscript": "^7.2.0", 105 | "husky": "^8.0.3", 106 | "pnpm": "^8.5.1", 107 | "rehype-accessible-emojis": "^0.3.2", 108 | "rehype-external-links": "^2.1.0", 109 | "rehype-parse": "^8.0.4", 110 | "rehype-slug": "^5.1.0", 111 | "rehype-stringify": "^9.0.3", 112 | "rehype-urls": "^1.1.1", 113 | "semantic-release": "^21.0.2", 114 | "typedoc": "^0.24.7", 115 | "typedoc-plugin-extras": "^2.3.3", 116 | "typedoc-plugin-inline-sources": "^1.0.1", 117 | "typedoc-plugin-mdn-links": "^3.0.3", 118 | "typescript": "^5.0.4", 119 | "unified": "^10.1.2", 120 | "vite": "^4.3.8", 121 | "vite-plugin-dts": "^2.3.0", 122 | "vitest": "^0.31.1" 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /repl.ts: -------------------------------------------------------------------------------- 1 | import SpringEasing, { SpringFrame, SpringOutFrame, SpringInFrame } from "./src/index"; 2 | import { interpolateColor } from "./tests/utils/interpolate-color"; 3 | 4 | // let [keyframes] = SpringEasing(["red", "black"], { 5 | // easing: "spring", 6 | // numPoints: 100, 7 | // decimal: 2 8 | // }, interpolateColor); 9 | 10 | let [keyframes] = SpringEasing(["red", "blue", "#4f4", "rgb(0, 0, 0)"], { 11 | // Enforce a linear easing frame function 12 | // Not really necessary but it show what you can do if you really need other kinds of easings 13 | easing: [(t) => t], 14 | numPoints: 8, 15 | decimal: 2 16 | }, interpolateColor); 17 | 18 | console.log(keyframes) -------------------------------------------------------------------------------- /src/batch.ts: -------------------------------------------------------------------------------- 1 | import { EasingOptions, GenerateSpringFrames } from "./mod.ts"; 2 | import { limit, toFixed, scale, isNumberLike, getUnit } from "./utils.ts"; 3 | 4 | import type { TypeEasingOptions, TypeInterpolationFunction } from "./mod.ts"; 5 | 6 | /** 7 | * The type for interpolation functions which at an instant in the animation, generate the corresponding interpolated frame 8 | */ 9 | export interface IGenericBatchInterpolationFunction { 10 | (arr_t: number[], values: T, decimal?: number): TReturn; 11 | } 12 | /** 13 | * Given an Array of numbers, estimate the resulting number, at a `t` value between 0 to 1 14 | 15 | * Basic interpolation works by scaling `t` from 0 - 1, to some start number and end number, in this case lets use 16 | * 0 as our start number and 100 as our end number, so, basic interpolation would interpolate between 0 to 100. 17 | 18 | * If we use a `t` of 0.5, the interpolated value between 0 to 100, is 50. 19 | * {@link batchInterpolateNumber} takes it further, by allowing you to interpolate with more than 2 values, 20 | * it allows for multiple values. 21 | * E.g. Given an Array of values [0, 100, 0], and a `t` of 0.5, the interpolated value would become 100. 22 | 23 | * Based on d3.interpolateBasis [https://github.com/d3/d3-interpolate#interpolateBasis], 24 | * check out the link above for more detail. 25 | * 26 | * Buliding on-top of {@link batchInterpolateNumber}, `interpolateNumberBatch` interpolates between numbers, but unlike {@link batchInterpolateNumber} 27 | * `interpolateNumberBatch` uses an Array of `t` instances to generate an array of interpolated values 28 | * 29 | * @param arr_t Array of numbers (between 0 to 1) which each represent an instant of the interpolation 30 | * @param values Array of numbers to interpolate between 31 | * @param decimal How many decimals should the interpolated value have 32 | * @return Array of interpolated numbers at different instances 33 | * 34 | * @source Source code of `interpolateNumberBatch` 35 | */ 36 | export function batchInterpolateNumber(arr_t: number[], values: number[], decimal = 3) { 37 | // nth index 38 | const n = values.length - 1; 39 | const results: number[] = []; 40 | 41 | let t = 0; 42 | let t_index = 0; 43 | let len = arr_t.length; 44 | 45 | for (; t_index < len; t_index++) { 46 | t = arr_t[t_index]; 47 | 48 | // The current index given t 49 | const i = limit(Math.floor(t * n), 0, n - 1); 50 | 51 | const start = values[i]; 52 | const end = values[i + 1]; 53 | const progress = (t - i / n) * n; 54 | 55 | results.push(toFixed(scale(progress, start, end), decimal)); 56 | } 57 | 58 | return results; 59 | } 60 | 61 | /** 62 | * Given an Array of items, find an item using `t` (which goes from 0 to 1), by 63 | * using `t` to estimate the index of said value in the array of `values`, 64 | * then expand that to encompass multiple `t`'s in an Array, 65 | * which returns Array items which each follow the interpolated index value 66 | 67 | * This is meant for interplolating strings that aren't number-like 68 | 69 | * @param arr_t Array of numbers (between 0 to 1) which each represent an instance of the interpolation 70 | * @param values Array of items to choose from 71 | * @return Array of Interpolated input Array items at different instances 72 | 73 | * @source Source code of `interpolateSequenceBatch` 74 | */ 75 | export function batchInterpolateSequence( 76 | arr_t: number[], 77 | values: T[] 78 | ) { 79 | // nth index 80 | const n = values.length - 1; 81 | const results: T[] = []; 82 | 83 | let t = 0; 84 | let t_index = 0; 85 | let len = arr_t.length; 86 | 87 | for (; t_index < len; t_index++) { 88 | // limit `t`, to a min of 0 and a max of 1 89 | t = limit(arr_t[t_index], 0, 1); 90 | 91 | // The current index given t 92 | const i = Math.round(t * n); 93 | results.push(values[i]); 94 | } 95 | 96 | return results; 97 | } 98 | 99 | /** 100 | * Alias of {@link batchInterpolateSequence} 101 | * @deprecated please use {@link batchInterpolateSequence}, it's the same functionality but different name 102 | */ 103 | export const batchInterpolateUsingIndex = batchInterpolateSequence; 104 | 105 | /** 106 | * Functions the same way {@link batchInterpolateNumber} works. 107 | * Convert strings to numbers, and then interpolates the numbers, 108 | * at the end if there are units on the first value in the `values` array, 109 | * it will use that unit for the interpolated result. 110 | * Make sure to read {@link batchInterpolateNumber}. 111 | * 112 | * @source Source code of `interpolateStringBatch` 113 | */ 114 | export function batchInterpolateString( 115 | arr_t: number[], 116 | values: (string | number)[], 117 | decimal = 3 118 | ) { 119 | let units = ""; 120 | 121 | // If the first value looks like a number with a unit 122 | if (isNumberLike(values[0])) units = getUnit(values[0]); 123 | return ( 124 | batchInterpolateNumber( 125 | arr_t, 126 | values.map((v) => (typeof v === "number" ? v : parseFloat(v))), 127 | decimal 128 | ).map(value => value + units) 129 | ); 130 | } 131 | 132 | /** 133 | * Interpolates all types of values including number, string, etc... values. 134 | * Make sure to read {@link batchInterpolateNumber}, {@link batchInterpolateString} and {@link batchInterpolateSequence}, 135 | * as depending on the values given 136 | * 137 | * @source Source code of `interpolateComplexBatch` 138 | */ 139 | export function batchInterpolateComplex( 140 | arr_t: number[], 141 | values: T[], 142 | decimal = 3 143 | ) { 144 | let isNumber = true; 145 | let isLikeNumber = true; 146 | 147 | let i = 0; 148 | let v: T; 149 | const len = values.length; 150 | for (; i < len; i++) { 151 | v = values[i]; 152 | if (isNumber) isNumber = typeof v === "number"; 153 | if (isLikeNumber) isLikeNumber = isNumberLike(v as string); 154 | } 155 | 156 | // Interpolate numbers 157 | if (isNumber) return batchInterpolateNumber(arr_t, values as number[], decimal); 158 | 159 | // Interpolate strings with numbers, e.g. "5px" 160 | if (isLikeNumber) 161 | return batchInterpolateString(arr_t, values as (number | string)[], decimal); 162 | 163 | // Interpolate pure strings and/or other type of values, e.g. "inherit", "solid", etc... 164 | return batchInterpolateSequence(arr_t, values); 165 | } 166 | 167 | /** 168 | * Generates an Array of values using frame functions which in turn create the effect of spring easing. 169 | * To use this properly make sure to set the easing animation option to "linear". 170 | * Check out a demo of SpringEasing at 171 | * 172 | * SpringEasing has 3 properties they are `easing` (all the easings from {@link EasingFunctions} are supported on top of frame functions like SpringFrame, SpringFrameOut, etc..), `numPoints` (the size of the Array the frame function should create), and `decimal` (the number of decimal places of the values within said Array). 173 | * 174 | * | Properties | Default Value | 175 | * | ----------- | ----------------------- | 176 | * | `easing` | `spring(1, 100, 10, 0)` | 177 | * | `numPoints` | `50` | 178 | * | `decimal` | `3` | 179 | * 180 | * By default, Spring Easing support easings in the form, 181 | * 182 | * | constant | accelerate | decelerate | accelerate-decelerate | decelerate-accelerate | 183 | * | :--------- | :----------------- | :------------- | :-------------------- | :-------------------- | 184 | * | | spring / spring-in | spring-out | spring-in-out | spring-out-in | 185 | * 186 | * All **Spring** easing's can be configured using theses parameters, 187 | * 188 | * `spring-*(mass, stiffness, damping, velocity)` 189 | * 190 | * Each parameter comes with these defaults 191 | * 192 | * | Parameter | Default Value | 193 | * | --------- | ------------- | 194 | * | mass | `1` | 195 | * | stiffness | `100` | 196 | * | damping | `10` | 197 | * | velocity | `0` | 198 | * 199 | * e.g. 200 | * ```ts 201 | * import { SpringEasing, SpringOutFrame } from "spring-easing"; 202 | * import anime from "animejs"; 203 | * 204 | * // Note: this is the return value of {@link SpringEasing} and {@link GenerateSpringFrames}, you don't need the object to get this format 205 | * let [translateX, duration] = SpringEasing([0, 250], { 206 | * easing: "spring-out-in(1, 100, 10, 0)", 207 | * 208 | * // You can change the size of Array for the SpringEasing function to generate 209 | * numPoints: 200, 210 | * 211 | * // The number of decimal places to round, final values in the generated Array 212 | * // This option doesn't exist on {@link GenerateSpringFrames} 213 | * decimal: 5, 214 | * }); 215 | * 216 | * anime({ 217 | * targets: "div", 218 | * 219 | * // Using spring easing animate from [0 to 250] using `spring-out-in` 220 | * translateX, 221 | * 222 | * // You can set the easing without an object 223 | * rotate: SpringEasing(["0turn", 1, 0, 0.5], [SpringOutFrame, 1, 100, 10, 0])[0], 224 | * 225 | * // TIP... Use linear easing for the proper effect 226 | * easing: "linear", 227 | * 228 | * // The optimal duration for this specific spring 229 | * duration 230 | * }) 231 | * ``` 232 | * 233 | * @param values Values to animate between, e.g. `["50px", 60]` 234 | * > _**Note**: You can interpolate with more than 2 values, but it's very confusing, so, it's best to choose 2_ 235 | * @param options Accepts {@link TypeEasingOptions EasingOptions} or {@link TypeEasingOptions.easing array frame functions} 236 | * @param interpolationFunction If you wish to use your own interpolation functions you may 237 | * @return 238 | * ```ts 239 | * // An array of keyframes that represent said spring animation and 240 | * // Total duration (in milliseconds) required to create a smooth spring animation 241 | * [ 242 | * [50, 55, 60, 70, 80, ...], 243 | * 3500 244 | * ] 245 | * ``` 246 | */ 247 | export function BatchSpringEasing( 248 | values: T, 249 | options: TypeEasingOptions | TypeEasingOptions["easing"] = {}, 250 | customInterpolate: IGenericBatchInterpolationFunction = batchInterpolateComplex as unknown as IGenericBatchInterpolationFunction 251 | ) { 252 | const optionsObj = EasingOptions(options); 253 | const [frames, duration] = GenerateSpringFrames(optionsObj); 254 | 255 | return [ 256 | customInterpolate?.(frames, values, optionsObj.decimal), 257 | duration 258 | ] as const; 259 | } 260 | 261 | /** 262 | * Converts interpolation functions written in this style `(t, values, decimal) => { ... }` 263 | * to work in the `BatchSpringEasing` 264 | * 265 | * You use it like so, 266 | * ```ts 267 | * import { BatchSpringEasing, toAnimationFrames, toFixed, scale, limit } from "spring-easing"; 268 | * 269 | * function interpolateNumber(t: number, values: number[], decimal = 3) { 270 | * // nth index 271 | * const n = values.length - 1; 272 | * 273 | * // The current index given t 274 | * const i = limit(Math.floor(t * n), 0, n - 1); 275 | * 276 | * const start = values[i]; 277 | * const end = values[i + 1]; 278 | * const progress = (t - i / n) * n; 279 | * 280 | * return toFixed(scale(progress, start, end), decimal); 281 | * } 282 | * 283 | * function interpolatePixels(t: number, values: number[], decimal = 3) { 284 | * const result = interpolateNumber(t, values, decimal); 285 | * return `${result}px`; 286 | * } 287 | * 288 | * BatchSpringEasing( 289 | * [0, 250], 290 | * 'spring', 291 | * toAnimationFrames(interpolatePixels) 292 | * ); 293 | * ``` 294 | */ 295 | export function toAnimationFrames(customInterpolate: TypeInterpolationFunction) { 296 | return (arr_t: number[], values: T, decimal?: number) => { 297 | return arr_t.map(t => customInterpolate(t, values, decimal)) as TReturn; 298 | } 299 | } -------------------------------------------------------------------------------- /src/css-linear-easing.ts: -------------------------------------------------------------------------------- 1 | import { EasingOptions, GenerateSpringFrames } from "./mod.ts"; 2 | import { getOptimizedPoints } from "./optimize.ts"; 3 | import { limit, scale } from "./utils.ts"; 4 | 5 | import type { TypeEasingOptions } from "./mod.ts"; 6 | 7 | /*! 8 | * Based off of https://github.com/jakearchibald/linear-easing-generator 9 | * 10 | * Changes: 11 | * - Added comments and docs top explain logic 12 | * - Switched to iterative approach for the `ramerDouglasPeucker` algorithim 13 | * - Renamed functions, parameters and variables to improve readability and to better match a library usecase 14 | * 15 | * Copyright 2023 Jake Archibald [@jakearchibald](https://github.com/jakearchibald) 16 | * 17 | * Licensed under the Apache License, Version 2.0 (the "License"); 18 | * you may not use this file except in compliance with the License. 19 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 20 | * 21 | * Unless required by applicable law or agreed to in writing, software 22 | * distributed under the License is distributed on an "AS IS" BASIS, 23 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 24 | * See the License for the specific language governing permissions and 25 | * limitations under the License. 26 | */ 27 | 28 | /** 29 | * Converts a given set of points into an array of strings in this format `["value percent%", ...]` e.g. `["0", "0.25 13.8%", "0.6 45.6%", "0.1", "0.4 60%", ...]`. 30 | * 31 | * @param points - The array of points to be converted. Each point is represented as a pair of numbers: [pos, val]. 32 | * @param round - The number of decimal places to which point values should be rounded. 33 | * 34 | * @returns The formatted points as an array of strings, or an empty array if the input was null. 35 | * 36 | * The function first checks if the input points are null. If they are, it returns an empty array. 37 | * If they are not null, the function does the following: 38 | * 39 | * - It creates a NumberFormat object for formatting the x and y values of the points to the specified number of decimal places. 40 | * The x values are formatted with 2 fewer decimal places than the y values. 41 | * 42 | * - It iterates over the points to find those for which the x value does not need to be stated explicitly. 43 | * The x value of a point does not need to be stated if it is 0 for the first point, 1 for the last point (provided the x value of the point before it is not greater than 1), 44 | * or the average of the x values of the previous and next points. 45 | * 46 | * - It groups the points into subarrays such that all points in a subarray have the same y value. 47 | * 48 | * - It iterates over the groups and, for each group, formats the y value and the x values of the points that need to be stated explicitly. 49 | * The formatted values are concatenated into a string, with the x values followed by the y value and separated by commas. 50 | * If a group contains more than one point, the function also creates a string in which the y value is followed 51 | * by the x values of the first and last points of the group, separated by a space. 52 | * This string is used if its length is shorter than the length of the string with all the x values. 53 | * 54 | * - The function returns the array of formatted strings. 55 | * 56 | * This function makes use of the Intl.NumberFormat object for formatting the numbers. This not only rounds the numbers to the specified 57 | * number of decimal places but also formats them according to the en-US locale. This means that the numbers will be formatted with a period 58 | * as the decimal separator and without thousands separators. 59 | * 60 | * This function also optimizes the representation of the points by omitting the x values where they are not needed and by grouping points with 61 | * the same y value. This can significantly reduce the length of the output strings when many points have the same y value or when the x values 62 | * are close to their expected values based on their position in the array. 63 | */ 64 | export function getLinearSyntax( 65 | points: [x: number, y: number][] | null, 66 | round: number, 67 | ): string[] { 68 | if (!points) return []; 69 | 70 | // Prepare the number formatters with the required number of fractional digits. 71 | const xFormat = new Intl.NumberFormat('en-US', { 72 | maximumFractionDigits: Math.max(round - 2, 0), 73 | }); 74 | const yFormat = new Intl.NumberFormat('en-US', { 75 | maximumFractionDigits: round, 76 | }); 77 | 78 | const pointsValue = points; 79 | const valuesWithRedundantX = new Set<[number, number]>(); 80 | const maxDelta = 1 / 10 ** round; 81 | 82 | // Determine which points don't need an explicit x value. 83 | for (const [i, value] of pointsValue.entries()) { 84 | const [x] = value; 85 | // If the first item's position is 0, then we don't need to state the position 86 | if (i === 0) { 87 | if (x === 0) valuesWithRedundantX.add(value); 88 | continue; 89 | } 90 | // If the last entry's position is 1, and the item before it is less than 1, then we don't need to state the position 91 | if (i === pointsValue.length - 1) { 92 | const previous = pointsValue[i - 1][0]; 93 | if (x === 1 && previous <= 1) valuesWithRedundantX.add(value); 94 | continue; 95 | } 96 | 97 | // If the position is the average of the previous and next positions, then we don't need to state the position 98 | const previous = pointsValue[i - 1][0]; 99 | const next = pointsValue[i + 1][0]; 100 | 101 | const averagePos = (next - previous) / 2 + previous; 102 | const delta = Math.abs(x - averagePos); 103 | 104 | if (delta < maxDelta) valuesWithRedundantX.add(value); 105 | } 106 | 107 | // Group points with the same y value. 108 | const groupedValues: [x: number, y: number][][] = [[pointsValue[0]]]; 109 | 110 | for (const value of pointsValue.slice(1)) { 111 | if (value[1] === groupedValues.at(-1)![0][1]) { 112 | groupedValues.at(-1)!.push(value); 113 | } else { 114 | groupedValues.push([value]); 115 | } 116 | } 117 | 118 | // Convert each group to a formatted string. 119 | const outputValues = groupedValues.map((group) => { 120 | const yValue = yFormat.format(group[0][1]); 121 | 122 | const regularValue = group 123 | .map((value) => { 124 | const [x] = value; 125 | let output = yValue; 126 | 127 | if (!valuesWithRedundantX.has(value)) { 128 | output += ' ' + xFormat.format(x * 100) + '%'; 129 | } 130 | 131 | return output; 132 | }) 133 | .join(', '); 134 | 135 | if (group.length === 1) return regularValue; 136 | 137 | // Check if it's shorter to omit some x values. 138 | const xVals = [group[0][0], group.at(-1)![0]]; 139 | const positionalValues = xVals 140 | .map((x) => xFormat.format(x * 100) + '%') 141 | .join(' '); 142 | 143 | const skipValue = `${yValue} ${positionalValues}`; 144 | 145 | return skipValue.length > regularValue.length ? regularValue : skipValue; 146 | }); 147 | 148 | return outputValues; 149 | } 150 | 151 | /** 152 | * CSS Spring Easing has 4 properties they are `easing` (all spring frame functions are supported), `numPoints` (the size of the Array the frmae function should create), `decimal` (the number of decimal places of the values within said Array) and `quality` (how detailed/smooth the spring easing should be). 153 | * 154 | * | Properties | Default Value | 155 | * | ----------- | ----------------------- | 156 | * | `easing` | `spring(1, 100, 10, 0)` | 157 | * | `numPoints` | `50` | 158 | * | `decimal` | `3` | 159 | * | `quality` | `0.85` | 160 | */ 161 | export type TypeCSSEasingOptions = { 162 | /** 163 | * Indicates how detailed/smooth the CSS `linear-easing()` function should be, it ranges between 0 - 1 164 | * 165 | * - 0 means it should basically not even bother 166 | * - 1 means it should be a detailed as possible such that human eyes are no longer able to distinguish 167 | */ 168 | quality?: number 169 | } & TypeEasingOptions; 170 | 171 | /** 172 | * Generates a string that represents a set of values used with the linear-easing function to replicate spring animations, 173 | * you can check out the linear-easing playground here https://linear-easing-generator.netlify.app/ 174 | * Or check out a demo on Codepen https://codepen.io/okikio/pen/vYVaEXM 175 | * 176 | * CSS Spring Easing has 4 properties they are `easing` (all spring frame functions are supported), `numPoints` (the size of the Array the frmae function should create), `decimal` (the number of decimal places of the values within said Array) and `quality` (how detailed/smooth the spring easing should be).. 177 | * 178 | * | Properties | Default Value | 179 | * | ----------- | ----------------------- | 180 | * | `easing` | `spring(1, 100, 10, 0)` | 181 | * | `numPoints` | `50` | 182 | * | `decimal` | `3` | 183 | * | `quality` | `0.85` | 184 | * 185 | * By default, CSS Spring Easing support easings in the form, 186 | * 187 | * | constant | accelerate | decelerate | accelerate-decelerate | decelerate-accelerate | 188 | * | :--------- | :----------------- | :------------- | :-------------------- | :-------------------- | 189 | * | | spring / spring-in | spring-out | spring-in-out | spring-out-in | 190 | * 191 | * All **Spring** easing's can be configured using theses parameters, 192 | * 193 | * `spring-*(mass, stiffness, damping, velocity)` 194 | * 195 | * Each parameter comes with these defaults 196 | * 197 | * | Parameter | Default Value | 198 | * | --------- | ------------- | 199 | * | mass | `1` | 200 | * | stiffness | `100` | 201 | * | damping | `10` | 202 | * | velocity | `0` | 203 | * 204 | * e.g. 205 | * ```ts 206 | * import { CSSSpringEasing } from "spring-easing"; 207 | * 208 | * // Note: this is the return value of {@link CSSSpringEasing}, you don't need the object to get this format 209 | * let [easing, duration] = CSSSpringEasing({ 210 | * easing: "spring-out-in(1, 100, 10, 0)", 211 | * 212 | * // You can change the size of Array for the SpringEasing function to generate 213 | * numPoints: 200, 214 | * 215 | * // The number of decimal places to round, final values in the generated Array 216 | * // This option doesn't exist on {@link GenerateSpringFrames} 217 | * decimal: 5, 218 | * 219 | * // How detailed/smooth the spring easing should be 220 | * // 0 means not smooth at all (shorter easing string) 221 | * // 1 means as smooth as possible (this means the resulting easing will be a longer string) 222 | * quality: 0.85 223 | * }); 224 | * 225 | * document.querySelector("div").animate({ 226 | * translate: ["0px", "250px"], 227 | * rotate: ["0turn", "1turn", "0turn", "0.5turn"], 228 | * }, { 229 | * easing: `linear(${easing})`, 230 | * 231 | * // The optimal duration for this specific spring 232 | * duration 233 | * }) 234 | * ``` 235 | * 236 | * > **Note**: You can also use custom easings if you so wish e.g. 237 | * ```ts 238 | * import { CSSSpringEasing, limit, registerEasingFunctions } from "spring-easing"; 239 | * 240 | * registerEasingFunctions({ 241 | * bounce: t => { 242 | * let pow2: number, b = 4; 243 | * while (t < ((pow2 = Math.pow(2, --b)) - 1) / 11) { } 244 | * return 1 / Math.pow(4, 3 - b) - 7.5625 * Math.pow((pow2 * 3 - 2) / 22 - t, 2); 245 | * }, 246 | * elastic: (t, params: number[] = []) => { 247 | * let [amplitude = 1, period = 0.5] = params; 248 | * const a = limit(amplitude, 1, 10); 249 | * const p = limit(period, 0.1, 2); 250 | * if (t === 0 || t === 1) return t; 251 | * return -a * 252 | * Math.pow(2, 10 * (t - 1)) * 253 | * Math.sin( 254 | * ((t - 1 - (p / (Math.PI * 2)) * Math.asin(1 / a)) * (Math.PI * 2)) / p 255 | * ); 256 | * } 257 | * }); 258 | * 259 | * CSSSpringEasing("bounce") // ["0, 0.013, 0.015, 0.006 8.1%, 0.046 13.5%, 0.06, 0.062, 0.054, 0.034, 0.003 27%, 0.122, 0.206 37.8%, 0.232, 0.246, 0.25, 0.242, 0.224, 0.194, 0.153 56.8%, 0.039 62.2%, 0.066 64.9%, 0.448 73%, 0.646, 0.801 83.8%, 0.862 86.5%, 0.95 91.9%, 0.978, 0.994, 1", ...] 260 | * CSSSpringEasing("elastic(1, 0.5)") // ["0, -0.005 32.4%, 0.006 40.5%, 0.034 51.4%, 0.033 56.8%, 0.022, 0.003, -0.026 64.9%, -0.185 75.7%, -0.204, -0.195, -0.146, -0.05, 0.1 89.2%, 1", ...] 261 | * ``` 262 | * 263 | * @param options Accepts {@link TypeCSSEasingOptions EasingOptions} or {@link TypeCSSEasingOptions["easing"] array frame functions} 264 | * @return 265 | * ```ts 266 | * // A string with the values that represent said spring animation using the linear-easing function 267 | * // Total duration (in milliseconds) required to create a smooth spring animation 268 | * [ 269 | * "0, 0.583 5.4%, 0.669, 0.601, 0.502, 0.451, 0.457 18.9%, 0.512 24.3%, 0.516 27%, ...0.417 94.6%, 1", 270 | * 3500 271 | * ] 272 | * ``` 273 | */ 274 | export function CSSSpringEasing( 275 | options: TypeCSSEasingOptions | TypeCSSEasingOptions["easing"] = {} 276 | ) { 277 | const optionsObj = EasingOptions(options); 278 | const [frames, duration] = GenerateSpringFrames(optionsObj); 279 | 280 | const quality = limit((optionsObj.quality ?? 0.85), 0, 1); 281 | const simplify = scale(1 - quality, 0, 0.025); 282 | 283 | const len = frames.length; 284 | const pts = frames.map((x, i) => [i / (len - 1), x]) as [x: number, y: number][]; 285 | const optimizedPoints = getOptimizedPoints(pts, simplify, optionsObj.decimal); 286 | 287 | return [ 288 | getLinearSyntax(optimizedPoints, optionsObj.decimal).join(", "), 289 | duration 290 | ] as const; 291 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./mod.ts"; 2 | export { default } from "./mod.ts"; -------------------------------------------------------------------------------- /src/optimize.ts: -------------------------------------------------------------------------------- 1 | import { toFixed } from "./utils.ts"; 2 | 3 | /*! 4 | * Based off of https://github.com/jakearchibald/linear-easing-generator 5 | * 6 | * Changes: 7 | * - Added comments and docs top explain logic 8 | * - Switched to iterative approach for the `ramerDouglasPeucker` algorithim 9 | * - Renamed functions, parameters and variables to improve readability and to better match a library usecase 10 | * 11 | * Copyright 2023 Jake Archibald [@jakearchibald](https://github.com/jakearchibald) 12 | * 13 | * Licensed under the Apache License, Version 2.0 (the "License"); 14 | * you may not use this file except in compliance with the License. 15 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 16 | * 17 | * Unless required by applicable law or agreed to in writing, software 18 | * distributed under the License is distributed on an "AS IS" BASIS, 19 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 20 | * See the License for the specific language governing permissions and 21 | * limitations under the License. 22 | */ 23 | 24 | /** 25 | * The function calculates the squared distance from a point to a line segment. 26 | * Using squared distances avoids costly square root operations and doesn't 27 | * affect the result because we're only interested in relative distances. 28 | * 29 | * @param point The point from which distance is to be measured 30 | * @param lineStart The start point of the line segment 31 | * @param lineEnd The end point of the line segment 32 | * @returns The squared distance from the point to the line segment 33 | */ 34 | export function squaredSegmentDistance(point: [number, number], lineStart: [number, number], lineEnd: [number, number]): number { 35 | let [x, y] = lineStart; // Start with the coordinates of lineStart 36 | let dx = lineEnd[0] - x; // Change in x coordinates 37 | let dy = lineEnd[1] - y; // Change in y coordinates 38 | 39 | // If the line segment is not a point 40 | if (dx !== 0 || dy !== 0) { 41 | // Calculate t, the fractional distance along the line from lineStart to the point 42 | let t = ((point[0] - x) * dx + (point[1] - y) * dy) / (dx * dx + dy * dy); 43 | 44 | // If t is beyond 1, the closest point is lineEnd 45 | if (t > 1) { 46 | x = lineEnd[0]; 47 | y = lineEnd[1]; 48 | } else if (t > 0) { // If t is between 0 and 1, the closest point is on the segment 49 | x += dx * t; 50 | y += dy * t; 51 | } 52 | // If t is less than 0, the closest point is lineStart which we've already accounted for 53 | } 54 | 55 | dx = point[0] - x; // x distance from point to closest point on segment 56 | dy = point[1] - y; // y distance from point to closest point on segment 57 | 58 | return dx * dx + dy * dy; // Return squared distance from the point to the segment 59 | } 60 | 61 | /** 62 | * Simplify a line given an array of points and a tolerance using the Ramer-Douglas-Peucker algorithm. 63 | * The tolerance determines the maximum allowed perpendicular distance from a point to the line segment 64 | * connecting its neighboring points. Points with a greater distance are included in the simplified line, 65 | * while points with a smaller distance are excluded. 66 | * 67 | * This version of the function uses an iterative approach with a stack instead of recursion. 68 | * 69 | * The iterative approach using a stack doesn't guarantee that the points will be processed in the same 70 | * order as the recursive approach. Because of the way points are pushed onto the stack, 71 | * the algorithm could sometimes process points out of order. 72 | * 73 | * To fix this before returning the final result we sort the 74 | * simplified points in increasing order of x values before returning them. 75 | * 76 | * @param points The array of points to be simplified 77 | * @param tolerance The maximum allowed perpendicular distance from a point to the line segment 78 | * connecting its neighboring points 79 | * @returns The simplified line as an array of points, it sorts the simplified points in increasing order of x values before returning them. 80 | */ 81 | export function ramerDouglasPeucker(points: [number, number][], tolerance: number): [number, number][] { 82 | // Calculate the square tolerance 83 | const sqTolerance = tolerance * tolerance; 84 | 85 | // If there are less than 3 points, no simplification is possible, return the points as is 86 | if (points.length < 3) return points; 87 | 88 | // Start and end points of the line will always be part of the result 89 | let result: [number, number][] = [points[0]]; 90 | 91 | // Define stack to handle line segments 92 | let stack: [number, number][] = [[0, points.length - 1]]; 93 | 94 | while (stack.length > 0) { 95 | // Pop a line segment defined by start and end indices from stack 96 | let [start, end] = stack.pop() as [number, number]; 97 | 98 | let maxSqDist = 0; // Maximum squared distance 99 | let index = 0; // Index of the point with maximum squared distance 100 | 101 | // Find the point with the maximum squared distance from the line segment 102 | for (let i = start + 1; i < end; i++) { 103 | const sqDist = squaredSegmentDistance(points[i], points[start], points[end]); 104 | if (sqDist > maxSqDist) { 105 | index = i; 106 | maxSqDist = sqDist; 107 | } 108 | } 109 | 110 | // If the maximum squared distance is greater than the tolerance, split the line segment 111 | if (maxSqDist > sqTolerance) { 112 | // Push the line segments onto the stack 113 | stack.push([start, index]); 114 | stack.push([index, end]); 115 | } else { 116 | // If the maximum squared distance is less than the tolerance, the line segment is straight enough 117 | // Add the end point to the result 118 | result.push(points[end]); 119 | } 120 | } 121 | 122 | // Sort the simplified points in increasing order of x values. 123 | return result.sort((a, b) => a[0] - b[0]); 124 | } 125 | 126 | /** 127 | * Simplifies a given set of points using the Ramer-Douglas-Peucker algorithm and 128 | * rounds the x and y values of the resulting points. 129 | * 130 | * @param fullPoints - The array of points to be simplified. Each point is represented as a pair of numbers: [pos, val]. 131 | * @param simplify - The maximum allowed perpendicular distance from a point to the line segment. 132 | * @param round - The number of decimal places to which point values should be rounded. 133 | * 134 | * @returns The simplified and rounded points, or null if the input was null. 135 | * 136 | * The function first checks if the input points are null. If they are, it returns null. 137 | * If they are not null, the function applies the Ramer-Douglas-Peucker algorithm to the points using the specified tolerance. 138 | * Then it rounds the x and y values of the resulting points to the specified number of decimal places. 139 | * The x values are always rounded to at least 2 decimal places because they are represented as a percentage. 140 | */ 141 | export function getOptimizedPoints( 142 | fullPoints: [x: number, y: number][] | null, 143 | simplify: number, 144 | round: number, 145 | ): [x: number, y: number][] | null { 146 | if (!fullPoints) return null; 147 | 148 | const xRounding = Math.max(round, 2); // Ensure x values are rounded to at least 2 decimal places as they represent percentages. 149 | 150 | return ramerDouglasPeucker(fullPoints, simplify).map( 151 | ([x, y]) => [ 152 | toFixed(x, xRounding), // Round x values 153 | toFixed(y, round), // Round y values 154 | ], 155 | ); 156 | } -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * If a value can be converted to a valid number, then it's most likely a number 3 | * 4 | * @source Source code of `isNumberLike` 5 | */ 6 | export function isNumberLike(num: string | number) { 7 | const value = parseFloat(num as string); 8 | return typeof value == "number" && !Number.isNaN(value); 9 | } 10 | 11 | /** 12 | * Limit a number to a minimum of `min` and a maximum of `max` 13 | * 14 | * @source Source code of `limit` 15 | * 16 | * @param value number to limit 17 | * @param min minimum limit 18 | * @param max maximum limit 19 | * @returns limited/constrained number 20 | */ 21 | export function limit(value: number, min: number, max: number) { 22 | return Math.min(Math.max(value, min), max); 23 | } 24 | 25 | /** 26 | * map `t` from 0 to 1, to `start` to `end` 27 | * 28 | * @source Source code of `scale` 29 | */ 30 | export function scale(t: number, start: number, end: number) { 31 | return start + (end - start) * t; 32 | } 33 | 34 | /** 35 | * Rounds numbers to a fixed decimal place 36 | * 37 | * @source Source code of `toFixed` 38 | */ 39 | export function toFixed(value: number, decimal: number) { 40 | return Math.round(value * 10 ** decimal) / 10 ** decimal; 41 | } 42 | 43 | /** 44 | * Returns the unit of a string, it does this by removing the number in the string 45 | * 46 | * @source Source code of `getUnit` 47 | */ 48 | export function getUnit(str: string | number) { 49 | const num = parseFloat(str as string); 50 | return (str.toString()).replace(num.toString(), ""); 51 | } 52 | 53 | -------------------------------------------------------------------------------- /tests/css-linear-easing.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest"; 2 | import { CSSSpringEasing, SpringFrame, SpringInOutFrame, SpringOutFrame, SpringOutInFrame, registerEasingFunction, registerEasingFunctions } from "../src/mod"; 3 | 4 | describe("CSS Easing", () => { 5 | it('With default values', () => { 6 | expect(CSSSpringEasing()) 7 | .toEqual([ 8 | '0, 0.057, 0.199 5.4%, 0.761 13.5%, 0.914, 1.029, 1.107, 1.149, 1.163, 1.155, 1.132 32.4%, 1.016 43.2%, 0.996 45.9%, 0.976, 0.975 56.8%, 1.003 75.7%, 1', 9 | 1333.3333333333333 10 | ]); 11 | 12 | }) 13 | 14 | it('Easing using Array Frame Format (only the frame function is specified) [SpringOutFrame]', () => { 15 | let [frames, duration] = CSSSpringEasing([SpringOutFrame]); 16 | expect([frames, duration]) 17 | .toEqual([ 18 | '0, -0.003 24.3%, 0.025 43.2%, 0.024, 0.004 54.1%, -0.016 56.8%, -0.132 67.6%, -0.155, -0.163, -0.149, -0.107, -0.029, 0.086, 0.239 86.5%, 0.801 94.6%, 0.943, 1', 19 | 1333.3333333333333 20 | ]); 21 | }) 22 | 23 | // Even though I'd prefer if people didn't only set some of the spring parameters 24 | // I predict people will, so, `spring-easing` will warn about doing things like this 25 | it('Easing using string format (partially filled spring parameters) `spring-out(1, 100)`', () => { 26 | let [frames, duration] = CSSSpringEasing(`spring-out(1, 100)`); 27 | expect([frames, duration]) 28 | .toEqual([ 29 | '0, -0.003 24.3%, 0.025 43.2%, 0.024, 0.004 54.1%, -0.016 56.8%, -0.132 67.6%, -0.155, -0.163, -0.149, -0.107, -0.029, 0.086, 0.239 86.5%, 0.801 94.6%, 0.943, 1', 30 | 1333.3333333333333 31 | ]); 32 | }) 33 | 34 | // Even though I'd prefer if people didn't only set some of the spring parameters 35 | // I predict people will, so, `spring-easing` will warn about doing things like this 36 | it('Easing using string format (completely filled spring parameters) `spring-out(1, 100, 10, 0)`', () => { 37 | let [frames, duration] = CSSSpringEasing(`spring-out(1, 100, 10, 0)`); 38 | expect([frames, duration]) 39 | .toEqual([ 40 | '0, -0.003 24.3%, 0.025 43.2%, 0.024, 0.004 54.1%, -0.016 56.8%, -0.132 67.6%, -0.155, -0.163, -0.149, -0.107, -0.029, 0.086, 0.239 86.5%, 0.801 94.6%, 0.943, 1', 41 | 1333.3333333333333 42 | ]); 43 | }) 44 | 45 | it('Easing using Array Frame format (partially & completely filled spring parameters)', () => { 46 | let solution = [ 47 | '0, 0.099 2.7%, 0.457 8.1%, 0.553, 0.581, 0.566 16.2%, 0.508 21.6%, 0.492, 0.487 27%, 0.502 40.5%, 0.499 62.2%, 0.513 73%, 0.508, 0.492 78.4%, 0.434 83.8%, 0.419, 0.447, 0.543 91.9%, 0.901 97.3%, 1', 48 | 1333.3333333333333 49 | ]; 50 | 51 | // Partial 52 | let partial = CSSSpringEasing([SpringInOutFrame, 1, 100]); 53 | expect(partial) 54 | .toEqual(solution); 55 | 56 | // Complete 57 | let complete = CSSSpringEasing([SpringInOutFrame, 1, 100, 10, 0]); 58 | expect(complete) 59 | .toEqual(solution); 60 | }) 61 | 62 | it('Easing using both formats (maximums & minimums spring parameters)', () => { 63 | let solutionMin = [ 64 | '0, 0.093 2.7%, 0.441 8.1%, 0.544, 0.58, 0.572 16.2%, 0.515 21.6%, 0.496, 0.487 27%, 0.502 40.5%, 0.498 59.5%, 0.513 73%, 0.504, 0.485 78.4%, 0.428 83.8%, 0.42, 0.456, 0.559 91.9%, 0.907 97.3%, 1', 65 | 12833.333333333321 66 | ]; 67 | 68 | let solutionMax = [ 69 | '0, 262.711, 66.026, -41.397, -24.507, 3.568, 6.82, 1.395, -0.681 21.6%, 0.639 27%, 0.644, 0.505, 0.469 35.1%, 0.505 40.5%, 0.495 59.5%, 0.531 64.9%, 0.495, 0.356, 0.361 73%, 1.681 78.4%, -0.395, -5.82, -2.568, 25.507, 42.397, -65.026, -261.711, 1', 70 | 27833.333333333394 71 | ]; 72 | 73 | // Minimums for Spring Parameter 74 | let minimum = CSSSpringEasing([SpringInOutFrame, -5000, -5000, -5000, -5000]); 75 | expect(minimum) 76 | .toEqual(solutionMin); 77 | 78 | // Maximums for Spring Parameter 79 | let maximum = CSSSpringEasing([SpringInOutFrame, 5000, 5000, 5000, 5000]); 80 | expect(maximum) 81 | .toEqual(solutionMax); 82 | 83 | // Minimums for Spring Parameter (string format) 84 | let minimumStr = CSSSpringEasing(`spring-in-out(-5000, -5000, -5000, -5000)`); 85 | expect(minimumStr) 86 | .toEqual(solutionMin); 87 | 88 | // Maximums for Spring Parameter (string format) 89 | let maximumStr = CSSSpringEasing(`spring-in-out(5000, 5000, 5000, 5000)`); 90 | expect(maximumStr) 91 | .toEqual(solutionMax); 92 | }) 93 | 94 | it('Other easing options', () => { 95 | let solutionMin = [ 96 | '0, 0.0213858, 0.0760052 2.40481%, 0.4660091 8.61723%, 0.5389152, 0.5746306 12.62525%, 0.5814869, 0.5774941 15.43086%, 0.5063437 22.64529%, 0.4871971 27.25451%, 0.5015188 39.67936%, 0.4980748 59.31864%, 0.5125414 70.34068%, 0.5116291 73.54709%, 0.4920844 77.55511%, 0.425114 84.16834%, 0.4187121 85.57114%, 0.4208612 86.77355%, 0.4511391, 0.5249359 91.18236%, 0.9348226 97.79559%, 0.9848663 98.998%, 1', 97 | 12833.333333333321 98 | ]; 99 | 100 | let solutionMax = [ 101 | '0, 272.6, 171.44, 20.42, -41.88, -32.5, -6.6, 6.64, 6.63, 2.38, -0.29, -0.6 22%, 0.58 27%, 0.69 29%, 0.5 33%, 0.47 35%, 0.5 39% 61%, 0.53 65%, 0.5, 0.31 71%, 0.42 73%, 1.6 78%, 1.29, -1.38, -5.63, -5.64, 7.6, 33.5, 42.88, -19.42, -170.44, -271.6, 1', 102 | 27833.333333333394 103 | ]; 104 | 105 | // Minimums for Spring Parameter 106 | let minimum = CSSSpringEasing({ 107 | easing: [SpringInOutFrame, -5000, -5000, -5000, -5000], 108 | numPoints: 500, 109 | decimal: 7 110 | }); 111 | expect(minimum) 112 | .toEqual(solutionMin); 113 | 114 | // Maximums for Spring Parameter 115 | let maximum = CSSSpringEasing({ 116 | easing: `spring-in-out(5000, 5000, 5000, 5000)`, 117 | numPoints: 50, 118 | decimal: 2 119 | }); 120 | expect(maximum) 121 | .toEqual(solutionMax); 122 | }) 123 | 124 | it('All frames function', () => { 125 | let easeIn = CSSSpringEasing({ 126 | easing: [SpringFrame, 1, 100, 10, 0], 127 | numPoints: 50, 128 | decimal: 2 129 | }); 130 | expect(easeIn) 131 | .toEqual([ 132 | '0, 0.03, 0.12 4%, 0.68 12%, 0.92, 1.08 20%, 1.12, 1.15, 1.16, 1.16, 1.15 31%, 1.02 43%, 0.99, 0.98 51%, 0.97 57%, 1 69% 100%', 133 | 1333.3333333333333 134 | ]); 135 | 136 | let easeOut = CSSSpringEasing({ 137 | easing: [SpringOutFrame, 1, 100, 10, 0], 138 | numPoints: 50, 139 | decimal: 2 140 | }); 141 | expect(easeOut) 142 | .toEqual([ 143 | '0 0% 31%, 0.03 43%, 0.02 49%, 0.01, -0.02 57%, -0.15 69%, -0.16, -0.16, -0.15, -0.12, -0.08 80%, 0.08, 0.32 88%, 0.88 96%, 0.97, 1', 144 | 1333.3333333333333 145 | ]); 146 | 147 | let easeInOut = CSSSpringEasing({ 148 | easing: [SpringInOutFrame, 1, 100, 10, 0], 149 | numPoints: 50, 150 | decimal: 2 151 | }); 152 | expect(easeInOut) 153 | .toEqual([ 154 | '0, 0.06 2%, 0.46 8%, 0.54, 0.58 12% 14%, 0.5 22%, 0.49 27%, 0.5 39% 61%, 0.51 71% 76%, 0.48 80%, 0.42 86% 88%, 0.46, 0.54 92%, 0.94 98%, 1', 155 | 1333.3333333333333 156 | ]); 157 | 158 | let easeOutIn = CSSSpringEasing({ 159 | easing: [SpringOutInFrame, 1, 100, 10, 0], 160 | numPoints: 50, 161 | decimal: 2 162 | }); 163 | expect(easeOutIn) 164 | .toEqual([ 165 | '0, 0, 0.01 24%, -0.01, -0.07 35%, -0.08, -0.06, -0, 0.1, 0.38 47%, 0.48, 0.52 51%, 0.9 57%, 1, 1.06, 1.08, 1.07 65%, 1.01, 0.99 76%, 1, 1', 166 | 1333.3333333333333 167 | ]); 168 | }) 169 | 170 | it('Different quality settings', () => { 171 | const results: string[] = [ 172 | /* quality = 0 */ '0, 0.91 16%, 1.15 24% 30%, 0.98 51%, 1', 173 | /* quality = 0.2 */ '0, 0.91 16%, 1.15 24% 30%, 1.02 43%, 0.98 51%, 1', 174 | /* quality = 0.3 */ '0, 0.2 5%, 0.91 16%, 1.15 24% 30%, 1.02 43%, 0.98 51%, 1', 175 | /* quality = 0.35555 */ '0, 0.2 5%, 0.91 16%, 1.15 24% 30%, 1.02 43%, 0.98 51%, 1', 176 | /* quality = 0.6 */ '0, 0.06, 0.2 5%, 0.91 16%, 1.03 19%, 1.15, 1.16, 1.15 30%, 1.02 43%, 0.98 51%, 1, 1', 177 | /* quality = -0.3 */ '0, 0.91 16%, 1.15 24% 30%, 0.98 51%, 1', 178 | /* quality = 0.9 */ '0, 0.06, 0.2 5%, 0.76 14%, 0.91, 1.03, 1.11, 1.15, 1.16, 1.15, 1.13 32%, 1.04 41%, 1.02, 1, 0.98 49% 51%, 0.97 57%, 1 70% 100%', 179 | // (default) quality 180 | /* quality = 0.85 */ '0, 0.06, 0.2 5%, 0.76 14%, 0.91, 1.03, 1.11, 1.15, 1.16, 1.15, 1.13 32%, 1.02 43%, 1, 0.98, 0.97 57%, 1 76%, 1', 181 | /* quality = 1 */ '0, 0.06, 0.2, 0.38, 0.58, 0.76, 0.91, 1.03, 1.11, 1.15, 1.16, 1.15, 1.13, 1.1, 1.07, 1.04, 1.02, 1, 0.98, 0.98, 0.97, 0.97, 0.98, 0.98, 0.99, 0.99, 1 70% 100%', 182 | /* quality = 1.2 */ '0, 0.06, 0.2, 0.38, 0.58, 0.76, 0.91, 1.03, 1.11, 1.15, 1.16, 1.15, 1.13, 1.1, 1.07, 1.04, 1.02, 1, 0.98, 0.98, 0.97, 0.97, 0.98, 0.98, 0.99, 0.99, 1 70% 100%', 183 | /* quality = 1000 */ '0, 0.06, 0.2, 0.38, 0.58, 0.76, 0.91, 1.03, 1.11, 1.15, 1.16, 1.15, 1.13, 1.1, 1.07, 1.04, 1.02, 1, 0.98, 0.98, 0.97, 0.97, 0.98, 0.98, 0.99, 0.99, 1 70% 100%', 184 | /* quality = 90 */ '0, 0.06, 0.2, 0.38, 0.58, 0.76, 0.91, 1.03, 1.11, 1.15, 1.16, 1.15, 1.13, 1.1, 1.07, 1.04, 1.02, 1, 0.98, 0.98, 0.97, 0.97, 0.98, 0.98, 0.99, 0.99, 1 70% 100%', 185 | /* quality = '64' */ '0, 0.06, 0.2, 0.38, 0.58, 0.76, 0.91, 1.03, 1.11, 1.15, 1.16, 1.15, 1.13, 1.1, 1.07, 1.04, 1.02, 1, 0.98, 0.98, 0.97, 0.97, 0.98, 0.98, 0.99, 0.99, 1 70% 100%', 186 | /* quality = NaN */ '0, 1', 187 | /* quality = null */ '0, 0.06, 0.2 5%, 0.76 14%, 0.91, 1.03, 1.11, 1.15, 1.16, 1.15, 1.13 32%, 1.02 43%, 1, 0.98, 0.97 57%, 1 76%, 1', 188 | /* quality = undefined */ '0, 0.06, 0.2 5%, 0.76 14%, 0.91, 1.03, 1.11, 1.15, 1.16, 1.15, 1.13 32%, 1.02 43%, 1, 0.98, 0.97 57%, 1 76%, 1', 189 | /* quality = true */ '0, 0.06, 0.2, 0.38, 0.58, 0.76, 0.91, 1.03, 1.11, 1.15, 1.16, 1.15, 1.13, 1.1, 1.07, 1.04, 1.02, 1, 0.98, 0.98, 0.97, 0.97, 0.98, 0.98, 0.99, 0.99, 1 70% 100%', 190 | /* quality = false */ '0, 0.91 16%, 1.15 24% 30%, 0.98 51%, 1', 191 | ] 192 | // string interpolation 193 | expect([0, 0.2, 0.3, 0.35555, 0.6, -0.3, 0.9, 0.85, 1, 1.2, 1000, 90, '64', NaN, null, undefined, true, false].map(q => CSSSpringEasing({ 194 | easing: "spring", 195 | decimal: 2, 196 | quality: q as number 197 | })[0])).toEqual(results) 198 | }) 199 | 200 | it('Register custom easing functions', () => { 201 | registerEasingFunction("linear", (t) => t); 202 | registerEasingFunctions({ 203 | quad: (t) => Math.pow(t, 2), 204 | cubic: (t) => Math.pow(t, 3), 205 | }); 206 | 207 | let [easings] = CSSSpringEasing({ 208 | easing: "linear", 209 | numPoints: 100, 210 | decimal: 2 211 | }); 212 | 213 | expect(easings).toEqual('0, 1') 214 | 215 | let [easings2] = CSSSpringEasing({ 216 | easing: "quad", 217 | numPoints: 100, 218 | decimal: 2 219 | }); 220 | expect(easings2).toEqual('0, 0.01, 0.06, 0.13, 0.24, 0.38, 0.56, 0.77, 1') 221 | 222 | let [easings3] = CSSSpringEasing({ 223 | easing: "quad", 224 | numPoints: 100, 225 | decimal: 2 226 | }); 227 | expect(easings3).toEqual('0, 0.01, 0.06, 0.13, 0.24, 0.38, 0.56, 0.77, 1') 228 | }) 229 | }); 230 | 231 | 232 | describe('CSSSpringEasing 2', () => { 233 | it('should return an easing string and a duration with default parameters', () => { 234 | const result = CSSSpringEasing({}); 235 | expect(typeof result[0]).toBe('string'); 236 | expect(typeof result[1]).toBe('number'); 237 | }); 238 | 239 | it('should handle larger numPoints correctly', () => { 240 | const result = CSSSpringEasing({ numPoints: 100 }); 241 | const easingString = result[0]; 242 | const keyframes = easingString.split(', '); 243 | 244 | // Test that the number of x-value,y-value pairs is equal to numPoints 245 | expect(keyframes.length / 2).toEqual(17 / 2); 246 | }); 247 | 248 | it('should format values to correct number of decimal places', () => { 249 | const result = CSSSpringEasing({ decimal: 2 }); 250 | const easingString = result[0]; 251 | const keyframes = easingString.split(', '); 252 | 253 | // Check that values are formatted to 2 decimal places 254 | keyframes.forEach((keyframe) => { 255 | const yValue = keyframe.split(" "); 256 | const decimalPlaces = yValue[0].split('.')[1]?.length || 0; 257 | expect(decimalPlaces).toBeLessThanOrEqual(2); 258 | }); 259 | }); 260 | 261 | it('should correctly use the quality parameter', () => { 262 | const highQualityResult = CSSSpringEasing({ quality: 1 }); 263 | const lowQualityResult = CSSSpringEasing({ quality: 0 }); 264 | 265 | // High quality result should have more keyframes than low quality result 266 | const highQualityKeyframes = highQualityResult[0].split(', '); 267 | const lowQualityKeyframes = lowQualityResult[0].split(', '); 268 | expect(highQualityKeyframes.length).toBeGreaterThan(lowQualityKeyframes.length); 269 | }); 270 | 271 | it('should accept a custom easing function and use it correctly', () => { 272 | const customEasing = 'spring-out-in(2, 200, 20, 0)'; 273 | const result = CSSSpringEasing({ easing: customEasing }); 274 | const easingString = result[0]; 275 | 276 | // Due to the nature of the function, it's hard to test the exact output. 277 | // We can, however, test that the output is different from the default easing. 278 | const defaultResult = CSSSpringEasing({}); 279 | const defaultEasingString = defaultResult[0]; 280 | 281 | expect(easingString).not.toEqual(defaultEasingString); 282 | }); 283 | 284 | it('should throw an error if an invalid easing function is provided', () => { 285 | const invalidEasing = 'not-a-real-easing-function()'; 286 | expect(() => CSSSpringEasing({ easing: invalidEasing })).toThrow(); 287 | }); 288 | }); 289 | -------------------------------------------------------------------------------- /tests/fixture/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /tests/fixture/main.ts: -------------------------------------------------------------------------------- 1 | import { SpringFrame, SpringInFrame, SpringOutFrame, SpringInOutFrame, SpringOutInFrame } from "../../src/mod"; 2 | import { SpringEasing, CSSSpringEasing, limit, registerEasingFunctions } from "../../src/mod"; 3 | 4 | const frameFunctions = [ 5 | ["SpringFrame", SpringFrame], 6 | ["SpringInFrame", SpringInFrame], 7 | ["SpringOutFrame", SpringOutFrame], 8 | ["SpringInOutFrame", SpringInOutFrame], 9 | ["SpringOutInFrame", SpringOutInFrame] 10 | ] as const; 11 | 12 | const div = document.createElement("div"); 13 | div.classList.add("div"); 14 | 15 | frameFunctions.forEach(([name], i) => { 16 | const newDiv = div.cloneNode() as HTMLElement; 17 | newDiv.classList.add(`div${i + 1}`); 18 | newDiv.textContent = name; 19 | document.body.append(newDiv); 20 | 21 | const newLinearEasingDiv = div.cloneNode() as HTMLElement; 22 | newLinearEasingDiv.classList.add(`linear-easing-${i + 1}`); 23 | newLinearEasingDiv.textContent = `CSS Linear Easing - ${name}`; 24 | document.body.append(newLinearEasingDiv); 25 | }) 26 | 27 | frameFunctions.forEach(([_, frameFn], i) => { 28 | const el: HTMLElement = document.querySelector(`.div${i + 1}`)!; 29 | console.log({ 30 | el 31 | }) 32 | 33 | let [translateX, duration] = SpringEasing([25, 250], { 34 | easing: [frameFn, 1, 100, 7, 4], 35 | decimal: 3 36 | }); 37 | 38 | el.animate({ 39 | translate: translateX.map(x => `${x}px`) // ["25px", "250px"], 40 | }, { 41 | duration, 42 | iterations: Infinity, 43 | direction: "alternate", 44 | easing: "linear" 45 | }) 46 | }); 47 | 48 | frameFunctions.forEach(([_, frameFn], i) => { 49 | const el: HTMLElement = document.querySelector(`.linear-easing-${i + 1}`)!; 50 | 51 | let [easing, duration] = CSSSpringEasing({ 52 | easing: [frameFn, 1, 100, 7, 4], 53 | decimal: 3 54 | }); 55 | 56 | console.log({ 57 | el, 58 | easing 59 | }) 60 | 61 | el.animate({ 62 | translate: ["25px", "250px"], 63 | }, { 64 | duration, 65 | iterations: Infinity, 66 | direction: "alternate", 67 | easing: `linear(${easing})` 68 | }) 69 | }); 70 | 71 | registerEasingFunctions({ 72 | bounce: t => { 73 | let pow2: number, b = 4; 74 | while (t < ((pow2 = Math.pow(2, --b)) - 1) / 11) { } 75 | return 1 / Math.pow(4, 3 - b) - 7.5625 * Math.pow((pow2 * 3 - 2) / 22 - t, 2); 76 | }, 77 | elastic: (t, params: number[] = []) => { 78 | let [amplitude = 1, period = 0.5] = params; 79 | const a = limit(amplitude, 1, 10); 80 | const p = limit(period, 0.1, 2); 81 | if (t === 0 || t === 1) return t; 82 | return -a * 83 | Math.pow(2, 10 * (t - 1)) * 84 | Math.sin( 85 | ((t - 1 - (p / (Math.PI * 2)) * Math.asin(1 / a)) * (Math.PI * 2)) / p 86 | ); 87 | } 88 | }); 89 | 90 | CSSSpringEasing("bounce") // 91 | CSSSpringEasing("elastic(1, 0.5)") // 92 | 93 | const customFrameFunctions = [ 94 | ['Bounce', 'bounce'], 95 | ['Elastic', 'elastic(1, 0.5)'] 96 | ]; 97 | 98 | customFrameFunctions.forEach(([name], i) => { 99 | const newDiv = div.cloneNode() as HTMLElement; 100 | newDiv.classList.add(`div${i + 1}`); 101 | newDiv.classList.add(`custom-easing${i + 1}`); 102 | newDiv.textContent = name; 103 | document.body.append(newDiv); 104 | 105 | const newLinearEasingDiv = div.cloneNode() as HTMLElement; 106 | newLinearEasingDiv.classList.add(`linear-easing-${i + 1}`); 107 | newLinearEasingDiv.classList.add(`linear-custom-easing${i + 1}`); 108 | newLinearEasingDiv.textContent = `CSS Linear Easing - ${name}`; 109 | document.body.append(newLinearEasingDiv); 110 | }) 111 | 112 | customFrameFunctions.forEach(([_, frameFn], i) => { 113 | const el: HTMLElement = document.querySelector(`.custom-easing${i + 1}`)!; 114 | console.log({ 115 | el 116 | }) 117 | 118 | let [translateX] = SpringEasing([25, 250], { 119 | easing: frameFn, 120 | decimal: 3 121 | }); 122 | 123 | el.animate({ 124 | translate: translateX.map(x => `${x}px`) // ["25px", "250px"], 125 | }, { 126 | duration: 1330, 127 | iterations: Infinity, 128 | direction: "alternate", 129 | easing: "linear" 130 | }) 131 | }); 132 | 133 | customFrameFunctions.forEach(([_, frameFn], i) => { 134 | const el: HTMLElement = document.querySelector(`.linear-custom-easing${i + 1}`)!; 135 | 136 | let [easing] = CSSSpringEasing({ 137 | easing: frameFn, 138 | decimal: 3 139 | }); 140 | 141 | console.log({ 142 | el, 143 | easing 144 | }) 145 | 146 | el.animate({ 147 | translate: ["25px", "250px"], 148 | }, { 149 | duration: 1330, 150 | iterations: Infinity, 151 | direction: "alternate", 152 | easing: `linear(${easing})` 153 | }) 154 | }); -------------------------------------------------------------------------------- /tests/optimize.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest"; 2 | import { getLinearSyntax, getOptimizedPoints, toFixed } from "../src/mod"; 3 | 4 | describe("getOptimizedPoints", () => { 5 | it('test getOptimizedPoints readme example', () => { 6 | const testData: [number, number][] = [[0, 0], [0.1, 0.2], [0.5, 1], [0.9, 0.2], [1, 0]]; 7 | const expectedResult = [[0, 0], [0.5, 1], [1, 0]]; 8 | 9 | const result = getOptimizedPoints(testData, 0.1, 2); 10 | expect(result).toEqual(expectedResult) 11 | }); 12 | it('optimize basic points', () => { 13 | const testData: [number, number][] = [[0, 0], [0.5, 1], [1, 0]]; 14 | const expectedResult = [[0, 0], [0.5, 1], [1, 0]]; 15 | 16 | const result = getOptimizedPoints(testData, 0.1, 2); 17 | expect(result).toEqual(expectedResult) 18 | }); 19 | it('returns an empty array when given an empty array', () => { 20 | const testData: [number, number][] = []; 21 | const result = getOptimizedPoints(testData, 0.1, 2); 22 | expect(result).toEqual([]); 23 | }); 24 | 25 | it('returns single point array when given a single point array', () => { 26 | const testData: [number, number][] = [[0, 0]]; 27 | const result = getOptimizedPoints(testData, 0.1, 2); 28 | expect(result).toEqual([[0, 0]]); 29 | }); 30 | 31 | it('returns the same array when points are already simplified', () => { 32 | const testData: [number, number][] = [[0, 0], [1, 1], [2, 0]]; 33 | const result = getOptimizedPoints(testData, 0.1, 2); 34 | expect(result).toEqual([[0, 0], [1, 1], [2, 0]]); 35 | }); 36 | 37 | it('simplifies the points correctly', () => { 38 | const testData: [number, number][] = [[0, 0], [0.5, 0.001], [1, 0]]; 39 | const result = getOptimizedPoints(testData, 0.01, 2); 40 | expect(result).toEqual([[0, 0], [1, 0]]); 41 | }); 42 | 43 | it('rounds the point values correctly', () => { 44 | const testData: [number, number][] = [[0, 0], [0.333333333, 1], [0.666666666, 0], [1, 0]]; 45 | const result = getOptimizedPoints(testData, 0.01, 2); 46 | expect(result).toEqual([[0, 0], [0.33, 1], [0.67, 0], [1, 0]]); 47 | }); 48 | 49 | it('handles large datasets correctly', () => { 50 | // Generate a large dataset of points 51 | const largeData: [number, number][] = Array.from({ length: 10000 }, (_, i) => [i * 0.0001, Math.sin(i * 0.0001)]); 52 | 53 | // Perform simplification and rounding 54 | const result = getOptimizedPoints(largeData, 0.01, 2)!; 55 | 56 | // Expect the resulting data to be significantly smaller, but not empty 57 | expect(result.length).toBeLessThan(largeData.length); 58 | expect(result.length).toBeGreaterThan(0); 59 | 60 | // Check that the first and last points are preserved 61 | expect(result[0]).toEqual([0, 0]); 62 | expect(result[result.length - 1]).toEqual([1, toFixed(Math.sin(1), 2)]); 63 | }); 64 | 65 | it('preserves points when tolerance is set to 0', () => { 66 | const testData: [number, number][] = [[0, 0], [0.5, 0.2], [1, 0]]; 67 | const result = getOptimizedPoints(testData, 0, 2); 68 | expect(result).toEqual([[0, 0], [0.5, 0.2], [1, 0]]); 69 | }); 70 | 71 | it('handles null values gracefully', () => { 72 | const result = getOptimizedPoints(null, 0.1, 2); 73 | expect(result).toBeNull(); 74 | }); 75 | 76 | it('does not round x values to less than 2 decimal places', () => { 77 | const testData: [number, number][] = [[0, 0], [0.333333333, 1], [0.666666666, 0], [1, 0]]; 78 | const result = getOptimizedPoints(testData, 0.01, 1); 79 | expect(result).toEqual([[0, 0], [0.33, 1], [0.67, 0], [1, 0]]); 80 | }); 81 | }) 82 | 83 | 84 | 85 | /** 86 | * 87 | * 88 | * getLinearSyntax 89 | * 90 | * 91 | * 92 | */ 93 | describe('getLinearSyntax', () => { 94 | it('returns an empty array when input is null', () => { 95 | const result = getLinearSyntax(null, 2); 96 | expect(result).toEqual([]); 97 | }); 98 | 99 | it('test example from readme', () => { 100 | const result = getLinearSyntax([[0, 0], [0.1, 0.2], [0.5, 1], [0.9, 0.2], [1, 0]], 2); 101 | expect(result).toEqual([ 102 | '0', 103 | '0.2 10%', 104 | '1', 105 | '0.2 90%', 106 | '0', 107 | ]); 108 | }); 109 | 110 | it('formats single point correctly', () => { 111 | const singlePoint: [number, number][] = [[0.5, 0.2]]; 112 | const result = getLinearSyntax(singlePoint, 2); 113 | expect(result).toEqual(["0.2 50%"]); 114 | }); 115 | 116 | it('formats multiple points correctly', () => { 117 | const multiplePoints: [number, number][] = [[0, 0], [0.5, 1], [1, 0.5]]; 118 | const result = getLinearSyntax(multiplePoints, 2); 119 | expect(result).toEqual(["0", "1", "0.5"]); 120 | }); 121 | 122 | it('preserves order of points with distinct y values', () => { 123 | const testData: [number, number][] = [[0, 0], [0.33, 0.2], [0.67, 0.8], [1, 0.5]]; 124 | const result = getLinearSyntax(testData, 2); 125 | expect(result).toEqual(["0", "0.2", "0.8", "0.5"]); 126 | }); 127 | 128 | it('preserves order of points with same y values', () => { 129 | const testData: [number, number][] = [[0, 0.2], [0.33, 0.2], [0.67, 0.2], [1, 0.2]]; 130 | const result = getLinearSyntax(testData, 2); 131 | expect(result).toEqual(["0.2 0% 100%"]); 132 | }); 133 | 134 | it('handles datasets with random y values correctly', () => { 135 | // Generate a large dataset of points with random y values 136 | const largeData: [number, number][] = Array.from({ length: 10000 }, (_, i) => [i * 0.0001, Math.random()]); 137 | 138 | // Perform conversion to linear syntax 139 | const result = getLinearSyntax(largeData, 2); 140 | 141 | // Expect the resulting string to contain all points 142 | expect(result.length).toBe(largeData.length); 143 | 144 | // Check that the first and last points are preserved and correctly formatted 145 | expect(result[0]).toEqual(toFixed(largeData[0][1], 2).toString()); 146 | expect(result[largeData.length - 1]).toEqual(toFixed(largeData[largeData.length - 1][1], 2) + " 100%"); 147 | }); 148 | 149 | it('preserves order of points when rounding is applied', () => { 150 | const testData: [number, number][] = [[0, 0.333333333], [0.33, 0.666666666], [0.67, 0.111111111], [1, 0.999999999]]; 151 | const result = getLinearSyntax(testData, 2); 152 | expect(result).toEqual(["0.33", "0.67", "0.11", "1"]); 153 | }); 154 | }); 155 | -------------------------------------------------------------------------------- /tests/utils/color-parse.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | /** 3 | * Based on (color-parse)[https://github.com/colorjs/color-parse] by [colorjs](https://github.com/colorjs) under an MIT License 4 | */ 5 | import names from './colors'; 6 | 7 | /** 8 | * Base hues 9 | * http://dev.w3.org/csswg/css-color/#typedef-named-hue 10 | */ 11 | //FIXME: use external hue detector 12 | var baseHues = { 13 | red: 0, 14 | orange: 60, 15 | yellow: 120, 16 | green: 180, 17 | blue: 240, 18 | purple: 300 19 | } 20 | 21 | /** 22 | * Parse color from the string passed 23 | * 24 | * @return {Object} A space indicator `space`, an array `values` and `alpha` 25 | */ 26 | export const parse = (cstr) => { 27 | var m, parts = [], alpha = 1, space 28 | if (typeof cstr === 'string') { 29 | //keyword 30 | if (names[cstr]) { 31 | parts = names[cstr].slice() 32 | space = 'rgb' 33 | } 34 | 35 | //reserved words 36 | else if (cstr === 'transparent') { 37 | alpha = 0 38 | space = 'rgb' 39 | parts = [0, 0, 0] 40 | } 41 | 42 | //hex 43 | else if (/^#[A-Fa-f0-9]+$/.test(cstr)) { 44 | var base = cstr.slice(1) 45 | var size = base.length 46 | var isShort = size <= 4 47 | alpha = 1 48 | 49 | if (isShort) { 50 | parts = [ 51 | parseInt(base[0] + base[0], 16), 52 | parseInt(base[1] + base[1], 16), 53 | parseInt(base[2] + base[2], 16) 54 | ] 55 | if (size === 4) { 56 | alpha = parseInt(base[3] + base[3], 16) / 255 57 | } 58 | } 59 | else { 60 | parts = [ 61 | parseInt(base[0] + base[1], 16), 62 | parseInt(base[2] + base[3], 16), 63 | parseInt(base[4] + base[5], 16) 64 | ] 65 | if (size === 8) { 66 | alpha = parseInt(base[6] + base[7], 16) / 255 67 | } 68 | } 69 | 70 | if (!parts[0]) parts[0] = 0 71 | if (!parts[1]) parts[1] = 0 72 | if (!parts[2]) parts[2] = 0 73 | 74 | space = 'rgb' 75 | } 76 | 77 | //color space 78 | else if (m = /^((?:rgb|hs[lvb]|hwb|cmyk?|xy[zy]|gray|lab|lchu?v?|[ly]uv|lms)a?)\s*\(([^\)]*)\)/.exec(cstr)) { 79 | var name = m[1] 80 | var isRGB = name === 'rgb' 81 | var base: string = name.replace(/a$/, ''); 82 | space = base 83 | var size = base === 'cmyk' ? 4 : base === 'gray' ? 1 : 3 84 | parts = m[2].trim() 85 | .split(/\s*[,\/]\s*|\s+/) 86 | .map(function (x, i) { 87 | // 88 | if (/%$/.test(x)) { 89 | //alpha 90 | if (i === size) return parseFloat(x) / 100 91 | //rgb 92 | if (base === 'rgb') return parseFloat(x) * 255 / 100 93 | return parseFloat(x) 94 | } 95 | //hue 96 | else if (base[i] === 'h') { 97 | // 98 | if (/deg$/.test(x)) { 99 | return parseFloat(x) 100 | } 101 | // 102 | else if (baseHues[x] !== undefined) { 103 | return baseHues[x] 104 | } 105 | } 106 | return parseFloat(x) 107 | }) 108 | 109 | if (name === base) parts.push(1) 110 | alpha = (isRGB) ? 1 : (parts[size] === undefined) ? 1 : parts[size] 111 | parts = parts.slice(0, size) 112 | } 113 | 114 | //named channels case 115 | else if (cstr.length > 10 && /[0-9](?:\s|\/)/.test(cstr)) { 116 | parts = cstr.match(/([0-9]+)/g).map(function (value) { 117 | return parseFloat(value) 118 | }) 119 | 120 | space = cstr.match(/([a-z])/ig).join('').toLowerCase() 121 | } 122 | } 123 | 124 | //numeric case 125 | else if (!Number.isNaN(cstr)) { 126 | space = 'rgb' 127 | parts = [cstr >>> 16, (cstr & 0x00ff00) >>> 8, cstr & 0x0000ff] 128 | } 129 | 130 | //array-like 131 | else if (Array.isArray(cstr) || cstr.length) { 132 | parts = [cstr[0], cstr[1], cstr[2]] 133 | space = 'rgb' 134 | alpha = cstr.length === 4 ? cstr[3] : 1 135 | } 136 | 137 | //object case - detects css cases of rgb and hsl 138 | else if (cstr instanceof Object) { 139 | if (cstr.r != null || cstr.red != null || cstr.R != null) { 140 | space = 'rgb' 141 | parts = [ 142 | cstr.r || cstr.red || cstr.R || 0, 143 | cstr.g || cstr.green || cstr.G || 0, 144 | cstr.b || cstr.blue || cstr.B || 0 145 | ] 146 | } 147 | else { 148 | space = 'hsl' 149 | parts = [ 150 | cstr.h || cstr.hue || cstr.H || 0, 151 | cstr.s || cstr.saturation || cstr.S || 0, 152 | cstr.l || cstr.lightness || cstr.L || cstr.b || cstr.brightness 153 | ] 154 | } 155 | 156 | alpha = cstr.a || cstr.alpha || cstr.opacity || 1 157 | 158 | if (cstr.opacity != null) alpha /= 100 159 | } 160 | 161 | return { 162 | space: space, 163 | values: parts, 164 | alpha: alpha 165 | } 166 | } -------------------------------------------------------------------------------- /tests/utils/color-rgba.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Based on (color-rgba)[https://github.com/colorjs/color-rgba] by [Dima Yv.](https://github.com/dy) under an MIT License 3 | */ 4 | import { parse } from "./color-parse" 5 | import { hsl_rgb } from "./color-space" 6 | 7 | export const rgba = (color) => { 8 | // template literals 9 | // @ts-ignore 10 | if (Array.isArray(color) && color.raw) color = String.raw(...arguments) 11 | 12 | var values, i, l 13 | 14 | //attempt to parse non-array arguments 15 | var parsed = parse(color) 16 | 17 | if (!parsed.space) return [] 18 | 19 | values = Array(3) 20 | values[0] = Math.min(Math.max(parsed.values[0], 0), 255) 21 | values[1] = Math.min(Math.max(parsed.values[1], 0), 255) 22 | values[2] = Math.min(Math.max(parsed.values[2], 0), 255) 23 | 24 | if (parsed.space[0] === 'h') { 25 | values = hsl_rgb(values) 26 | } 27 | 28 | values.push(Math.min(Math.max(parsed.alpha, 0), 1)) 29 | 30 | return values 31 | } 32 | 33 | export default rgba; -------------------------------------------------------------------------------- /tests/utils/color-space.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Based on (color-space/hsl)[https://github.com/colorjs/color-space/blob/master/hsl.js] by [colorjs](https://github.com/colorjs) under an MIT License 3 | */ 4 | 5 | /** 6 | * @module color-space/hsl 7 | */ 8 | export function hsl_rgb(hsl) { 9 | var h = hsl[0] / 360, s = hsl[1] / 100, l = hsl[2] / 100, t1, t2, t3, rgb, val, i = 0; 10 | 11 | if (s === 0) return val = l * 255, [val, val, val]; 12 | 13 | t2 = l < 0.5 ? l * (1 + s) : l + s - l * s; 14 | t1 = 2 * l - t2; 15 | 16 | rgb = [0, 0, 0]; 17 | for (; i < 3;) { 18 | t3 = h + 1 / 3 * - (i - 1); 19 | t3 < 0 ? t3++ : t3 > 1 && t3--; 20 | val = 6 * t3 < 1 ? t1 + (t2 - t1) * 6 * t3 : 21 | 2 * t3 < 1 ? t2 : 22 | 3 * t3 < 2 ? t1 + (t2 - t1) * (2 / 3 - t3) * 6 : 23 | t1; 24 | rgb[i++] = val * 255; 25 | } 26 | 27 | return rgb; 28 | } 29 | -------------------------------------------------------------------------------- /tests/utils/colors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Based on (color-name)[https://github.com/colorjs/color-name] by [colorjs](https://github.com/colorjs) under an MIT License 3 | */ 4 | export default { 5 | "aliceblue": [240, 248, 255], 6 | "antiquewhite": [250, 235, 215], 7 | "aqua": [0, 255, 255], 8 | "aquamarine": [127, 255, 212], 9 | "azure": [240, 255, 255], 10 | "beige": [245, 245, 220], 11 | "bisque": [255, 228, 196], 12 | "black": [0, 0, 0], 13 | "blanchedalmond": [255, 235, 205], 14 | "blue": [0, 0, 255], 15 | "blueviolet": [138, 43, 226], 16 | "brown": [165, 42, 42], 17 | "burlywood": [222, 184, 135], 18 | "cadetblue": [95, 158, 160], 19 | "chartreuse": [127, 255, 0], 20 | "chocolate": [210, 105, 30], 21 | "coral": [255, 127, 80], 22 | "cornflowerblue": [100, 149, 237], 23 | "cornsilk": [255, 248, 220], 24 | "crimson": [220, 20, 60], 25 | "cyan": [0, 255, 255], 26 | "darkblue": [0, 0, 139], 27 | "darkcyan": [0, 139, 139], 28 | "darkgoldenrod": [184, 134, 11], 29 | "darkgray": [169, 169, 169], 30 | "darkgreen": [0, 100, 0], 31 | "darkgrey": [169, 169, 169], 32 | "darkkhaki": [189, 183, 107], 33 | "darkmagenta": [139, 0, 139], 34 | "darkolivegreen": [85, 107, 47], 35 | "darkorange": [255, 140, 0], 36 | "darkorchid": [153, 50, 204], 37 | "darkred": [139, 0, 0], 38 | "darksalmon": [233, 150, 122], 39 | "darkseagreen": [143, 188, 143], 40 | "darkslateblue": [72, 61, 139], 41 | "darkslategray": [47, 79, 79], 42 | "darkslategrey": [47, 79, 79], 43 | "darkturquoise": [0, 206, 209], 44 | "darkviolet": [148, 0, 211], 45 | "deeppink": [255, 20, 147], 46 | "deepskyblue": [0, 191, 255], 47 | "dimgray": [105, 105, 105], 48 | "dimgrey": [105, 105, 105], 49 | "dodgerblue": [30, 144, 255], 50 | "firebrick": [178, 34, 34], 51 | "floralwhite": [255, 250, 240], 52 | "forestgreen": [34, 139, 34], 53 | "fuchsia": [255, 0, 255], 54 | "gainsboro": [220, 220, 220], 55 | "ghostwhite": [248, 248, 255], 56 | "gold": [255, 215, 0], 57 | "goldenrod": [218, 165, 32], 58 | "gray": [128, 128, 128], 59 | "green": [0, 128, 0], 60 | "greenyellow": [173, 255, 47], 61 | "grey": [128, 128, 128], 62 | "honeydew": [240, 255, 240], 63 | "hotpink": [255, 105, 180], 64 | "indianred": [205, 92, 92], 65 | "indigo": [75, 0, 130], 66 | "ivory": [255, 255, 240], 67 | "khaki": [240, 230, 140], 68 | "lavender": [230, 230, 250], 69 | "lavenderblush": [255, 240, 245], 70 | "lawngreen": [124, 252, 0], 71 | "lemonchiffon": [255, 250, 205], 72 | "lightblue": [173, 216, 230], 73 | "lightcoral": [240, 128, 128], 74 | "lightcyan": [224, 255, 255], 75 | "lightgoldenrodyellow": [250, 250, 210], 76 | "lightgray": [211, 211, 211], 77 | "lightgreen": [144, 238, 144], 78 | "lightgrey": [211, 211, 211], 79 | "lightpink": [255, 182, 193], 80 | "lightsalmon": [255, 160, 122], 81 | "lightseagreen": [32, 178, 170], 82 | "lightskyblue": [135, 206, 250], 83 | "lightslategray": [119, 136, 153], 84 | "lightslategrey": [119, 136, 153], 85 | "lightsteelblue": [176, 196, 222], 86 | "lightyellow": [255, 255, 224], 87 | "lime": [0, 255, 0], 88 | "limegreen": [50, 205, 50], 89 | "linen": [250, 240, 230], 90 | "magenta": [255, 0, 255], 91 | "maroon": [128, 0, 0], 92 | "mediumaquamarine": [102, 205, 170], 93 | "mediumblue": [0, 0, 205], 94 | "mediumorchid": [186, 85, 211], 95 | "mediumpurple": [147, 112, 219], 96 | "mediumseagreen": [60, 179, 113], 97 | "mediumslateblue": [123, 104, 238], 98 | "mediumspringgreen": [0, 250, 154], 99 | "mediumturquoise": [72, 209, 204], 100 | "mediumvioletred": [199, 21, 133], 101 | "midnightblue": [25, 25, 112], 102 | "mintcream": [245, 255, 250], 103 | "mistyrose": [255, 228, 225], 104 | "moccasin": [255, 228, 181], 105 | "navajowhite": [255, 222, 173], 106 | "navy": [0, 0, 128], 107 | "oldlace": [253, 245, 230], 108 | "olive": [128, 128, 0], 109 | "olivedrab": [107, 142, 35], 110 | "orange": [255, 165, 0], 111 | "orangered": [255, 69, 0], 112 | "orchid": [218, 112, 214], 113 | "palegoldenrod": [238, 232, 170], 114 | "palegreen": [152, 251, 152], 115 | "paleturquoise": [175, 238, 238], 116 | "palevioletred": [219, 112, 147], 117 | "papayawhip": [255, 239, 213], 118 | "peachpuff": [255, 218, 185], 119 | "peru": [205, 133, 63], 120 | "pink": [255, 192, 203], 121 | "plum": [221, 160, 221], 122 | "powderblue": [176, 224, 230], 123 | "purple": [128, 0, 128], 124 | "rebeccapurple": [102, 51, 153], 125 | "red": [255, 0, 0], 126 | "rosybrown": [188, 143, 143], 127 | "royalblue": [65, 105, 225], 128 | "saddlebrown": [139, 69, 19], 129 | "salmon": [250, 128, 114], 130 | "sandybrown": [244, 164, 96], 131 | "seagreen": [46, 139, 87], 132 | "seashell": [255, 245, 238], 133 | "sienna": [160, 82, 45], 134 | "silver": [192, 192, 192], 135 | "skyblue": [135, 206, 235], 136 | "slateblue": [106, 90, 205], 137 | "slategray": [112, 128, 144], 138 | "slategrey": [112, 128, 144], 139 | "snow": [255, 250, 250], 140 | "springgreen": [0, 255, 127], 141 | "steelblue": [70, 130, 180], 142 | "tan": [210, 180, 140], 143 | "teal": [0, 128, 128], 144 | "thistle": [216, 191, 216], 145 | "tomato": [255, 99, 71], 146 | "turquoise": [64, 224, 208], 147 | "violet": [238, 130, 238], 148 | "wheat": [245, 222, 179], 149 | "white": [255, 255, 255], 150 | "whitesmoke": [245, 245, 245], 151 | "yellow": [255, 255, 0], 152 | "yellowgreen": [154, 205, 50] 153 | }; -------------------------------------------------------------------------------- /tests/utils/interpolate-color.ts: -------------------------------------------------------------------------------- 1 | import { interpolateNumber } from "../../src/index"; 2 | import { toFixed } from "../../src/utils"; 3 | import { parse } from "./color-parse"; 4 | 5 | import rgba from "./color-rgba"; 6 | import colors from "./colors"; 7 | 8 | /** 9 | * Convert value to string, then trim any extra white space and line terminator characters from the string. 10 | */ 11 | export function trim(str: T) { return (`` + str).trim() } 12 | 13 | /** 14 | * Determines if an object is empty 15 | */ 16 | export function isEmpty(obj: any) { 17 | for (let _ in obj) return false; 18 | return true; 19 | } 20 | 21 | /** 22 | * Checks if a value is valid/truthy; it counts empty arrays and strings as falsey, 23 | * as well as null, undefined, and NaN, everything else is valid 24 | * 25 | * _**Note:** 0 counts as valid_ 26 | * 27 | * @param value - anything 28 | * @returns true or false 29 | */ 30 | export function isValid(value: T) { 31 | if (Array.isArray(value) || typeof value == "string") 32 | return Boolean(value.length); 33 | return value != null && value != undefined && !Number.isNaN(value); 34 | } 35 | 36 | /** 37 | * Check if input value is a valid color 38 | */ 39 | export function isColor(value: T): boolean { 40 | if (typeof value == "string") { 41 | const input = trim(value).toLowerCase(); 42 | if (input == "transparent") return true; 43 | if (input in colors) return true; 44 | if (/^#[A-Fa-f0-9]+$/.test(input)) 45 | return rgba(input).length > 0; 46 | if (/^((?:rgb|hs[lvb]|hwb|cmyk?|xy[zy]|gray|lab|lchu?v?|[ly]uv|lms)a?)\s*\(([^\)]*)\)/.exec(input)) 47 | return rgba(input).length > 0; 48 | } 49 | 50 | const parsed = parse(value) 51 | if (!parsed.space) return false; 52 | 53 | return false; 54 | } 55 | 56 | /** 57 | * Convert the input to an array 58 | * For strings if type == "split", split the string at spaces, if type == "wrap" wrap the string in an array 59 | * For array do nothing 60 | * For everything else wrap the input in an array 61 | */ 62 | export function toArr(input: T) { 63 | if (Array.isArray(input) || typeof input == "string") { 64 | if (typeof input == "string") return input.split(/\s+/); 65 | return input; 66 | } 67 | 68 | return [input] as const; 69 | } 70 | 71 | // (TypeCSSGenericPropertyKeyframes | TypeCSSGenericPropertyKeyframes[])[] 72 | /** 73 | * Flips the rows and columns of 2-dimensional arrays 74 | * 75 | * Read more on [underscorejs.org](https://underscorejs.org/#zip) & [lodash.com](https://lodash.com/docs/4.17.15#zip) 76 | * 77 | * @example 78 | * ```ts 79 | * transpose( 80 | * ['moe', 'larry', 'curly'], 81 | * [30, 40, 50], 82 | * [true, false, false] 83 | * ); 84 | * // [ 85 | * // ["moe", 30, true], 86 | * // ["larry", 40, false], 87 | * // ["curly", 50, false] 88 | * // ] 89 | * ``` 90 | * @param [...args] - the arrays to process as a set of arguments 91 | * @returns 92 | * returns the new array of grouped elements 93 | */ 94 | export function transpose(...args: (T | T[])[]) { 95 | let largestArrLen = 0; 96 | const newargs = args.map((arr) => { 97 | // Convert all values in arrays to an array 98 | // This ensures that `arrays` is an array of arrays 99 | const result = toArr(arr); 100 | 101 | // Finds the largest array 102 | const len = result.length; 103 | if (len > largestArrLen) 104 | largestArrLen = len; 105 | return result; 106 | }); 107 | 108 | // Flip the rows and columns of arrays 109 | let result: T[][] = []; 110 | const len = newargs.length; 111 | for (let col = 0; col < largestArrLen; col++) { 112 | result[col] = []; 113 | 114 | for (let row = 0; row < len; row++) { 115 | const val = newargs[row][col]; 116 | if (isValid(val)) result[col][row] = val; 117 | } 118 | } 119 | 120 | return result; 121 | } 122 | 123 | /** 124 | * Use the `color-rgba` npm package, to convert all color formats to an Array of rgba values, 125 | * e.g. `[red, green, blue, alpha]`. Then, use the {@link interpolateNumber} functions to interpolate over the array 126 | * 127 | * _**Note**: the red, green, and blue colors are rounded to intergers with no decimal places, 128 | * while the alpha color gets rounded to a specific decimal place_ 129 | * Make sure to read {@link interpolateNumber}. 130 | */ 131 | export function interpolateColor(t: number, values: string[], decimal = 3) { 132 | const color = transpose(...values.map((v) => rgba(v))) 133 | .map((colors: number[], i) => { 134 | const result = interpolateNumber(t, colors); 135 | return i < 3 ? Math.round(result) : toFixed(result, decimal); 136 | }); 137 | 138 | return `rgba(${color.join()})`; 139 | } 140 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "ES2022", 5 | "module": "ES2022", 6 | "lib": [ 7 | "ES2022", 8 | "DOM", 9 | ], 10 | "sourceMap": true, 11 | "outDir": "lib", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "esModuleInterop": true, 15 | "skipLibCheck": true, 16 | "emitDeclarationOnly": true, 17 | "declaration": true, 18 | "declarationMap": true, 19 | "jsxFactory": "JSX.createElement", 20 | "jsxFragmentFactory": "JSX.Fragment", 21 | "allowImportingTsExtensions": true 22 | }, 23 | "include": [ 24 | "src", 25 | "tests/utils/color-parse.ts" 26 | ], 27 | "exclude": [ 28 | "node_modules" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://typedoc.org/schema.json", 3 | "exclude": [ 4 | "lib/**/*", 5 | "tests/**/*", 6 | "**/test.ts", 7 | "**/@types/*.d.ts" 8 | ], 9 | "entryPoints": ["src/index.ts"], 10 | "name": "spring-easing", 11 | "out": "docs", 12 | "media": "media", 13 | "theme": "default", 14 | "lightHighlightTheme": "github-light", 15 | "darkHighlightTheme": "github-dark", 16 | "includeVersion": true, 17 | "readme": "./README.md", 18 | "customCss": "./.typedoc/typedoc.css", 19 | "tsconfig": "./tsconfig.json", 20 | "umami-id": "4921ec19-210c-45b0-b5b8-cb9aaae292e9", 21 | "plugin": [ 22 | "./.typedoc/typedoc.cjs", 23 | "typedoc-plugin-extras", 24 | "typedoc-plugin-mdn-links", 25 | "typedoc-plugin-inline-sources" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // Configure Vitest (https://vitest.dev/config) 3 | import { defineConfig } from "vite"; 4 | import { umd as name } from "./package.json"; 5 | import dts from 'vite-plugin-dts'; 6 | 7 | export default defineConfig({ 8 | plugins: [ 9 | dts() 10 | ], 11 | build: { 12 | outDir: "lib", 13 | minify: false, 14 | lib: { 15 | entry: "src/index.ts", 16 | name, 17 | formats: ["es", "cjs", "umd"], 18 | fileName(format) { 19 | switch (format) { 20 | case "es": 21 | return "index.mjs"; 22 | case "cjs": 23 | return "index.cjs"; 24 | default: 25 | return "index.js"; 26 | } 27 | } 28 | }, 29 | }, 30 | test: { 31 | /* for example, use global to avoid globals imports (describe, test, expect): */ 32 | // globals: true, 33 | } 34 | }); 35 | --------------------------------------------------------------------------------