Repository: inorganik/countUp.js Branch: master Commit: 23bcb00792ef Files: 22 Total size: 101.0 KB Directory structure: gitextract_bsputj3_/ ├── .editorconfig ├── .eslintrc.js ├── .github/ │ ├── ISSUE_TEMPLATE.md │ ├── PULL_REQUEST_TEMPLATE.md │ └── dependabot.yml ├── .gitignore ├── .vscode/ │ └── tasks.json ├── LICENSE.md ├── README.md ├── demo/ │ ├── demo-nomodule.js │ └── demo.js ├── dist/ │ ├── countUp.d.ts │ ├── countUp.js │ ├── countUp.umd.js │ └── requestAnimationFrame.polyfill.js ├── index.html ├── jest.config.js ├── package.json ├── rollup.config.mjs ├── src/ │ ├── countUp.spec.ts │ └── countUp.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # This file is for unifying the coding style for different editors and IDEs # editorconfig.org root = true [*] end_of_line = lf charset = utf-8 insert_final_newline = true trim_trailing_whitespace = true indent_style = space indent_size = 2 ================================================ FILE: .eslintrc.js ================================================ module.exports = { root: true, ignorePatterns: ['*.js', '*.html', 'node_modules/*'], parser: '@typescript-eslint/parser', plugins: [ '@typescript-eslint', ], extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/recommended', ], rules: { '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', 'no-prototype-builtins': 'off', }, env: { browser: true, es6: true, } }; ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ ``` [ ] Bug [ ] Feature request CountUp.js version: ``` ## Description ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ## I'm submitting a... ``` [ ] Bug Fix [ ] Feature [ ] Other (Refactoring, Added tests, Documentation, ...) ``` ## Checklist - [ ] Test your changes - [ ] Followed the build steps ## Description _please describe the changes that you are making_ _for features, please describe how to use the new feature_ _please include a reference to an existing issue, if applicable_ ## Does this PR introduce a breaking change? ``` [ ] Yes [ ] No ``` ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: 'npm' directory: '/' schedule: interval: 'monthly' groups: all: patterns: - '*' ================================================ FILE: .gitignore ================================================ node_modules npm-debug.log .idea .DS_Store ================================================ FILE: .vscode/tasks.json ================================================ { // See https://go.microsoft.com/fwlink/?LinkId=733558 // for the documentation about the tasks.json format "version": "2.0.0", "tasks": [ { "type": "gulp", "task": "build", "group": { "kind": "build", "isDefault": true } }, { "label": "Open in Chrome Mac", "command": "Chrome", "osx": { "command": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" }, "args": [ "${file}" ], "problemMatcher": [] }, { "label": "Open in Chrome Linux", "command": "Chrome", "linux": { "command": "google-chrome" }, "args": [ "${file}" ], "problemMatcher": [] }, { "label": "Open in Chrome Windows", "command": "Chrome", "windows": { "command": "start chrome" }, "args": [ "${file}" ] } ] } ================================================ FILE: LICENSE.md ================================================ The MIT License (MIT) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # CountUp.js CountUp.js is a dependency-free, lightweight Javascript class that animates a numerical value by counting to it. Despite its name, CountUp can count in either direction, depending on the start and end values that you provide. CountUp.js supports all browsers. MIT license. ## [Try the demo](https://inorganik.github.io/countUp.js) Or tinker with CountUp in [Stackblitz](https://stackblitz.com/edit/countup-typescript) ## Jump to: - **[Usage](#usage)** - **[Including CountUp](#including-countup)** - **[Contributing](#contributing)** - **[Creating Animation Plugins](#creating-animation-plugins)** ## Features - **Auto-animate when element becomes visible.** Use option `autoAnimate = true`. - **Highly customizable** with a large range of options, you can even substitute numerals. - **Smart easing**: CountUp intelligently defers easing to make it visually noticeable. Configurable in the [options](#options). - **Plugins** allow for alternate animations like the [Odometer plugin](https://www.npmjs.com/package/odometer_countup) ![Odometer plugin](./demo/images/odometer_plugin.gif) ## Usage: **Use CountUp with:** - [Angular 2+](https://github.com/inorganik/ngx-countUp) - [React](https://gist.github.com/inorganik/2cf776865a4c65c12857027870e9898e) - [Svelte](https://gist.github.com/inorganik/85a66941ab88cc10c5fa5b26aead5f2a) - [Vue](https://github.com/xlsdg/vue-countup-v2) - [WordPress](https://wordpress.org/plugins/countup-js/) - [jQuery](https://gist.github.com/inorganik/b63dbe5b3810ff2c0175aee4670a4732) - [custom element](https://github.com/lekoala/formidable-elements/blob/master/docs/count-up.md) **Use CountUp directly:** On npm as `countup.js`. You can import as a module, or include the UMD script and access CountUp as a global. See [detailed instructions](#including-countup) on including CountUp. **Params**: - `target: string | HTMLElement | HTMLInputElement` - id of html element, input, svg text element, or DOM element reference where counting occurs. - `endVal: number | null` - the value you want to arrive at. Leave null to use the number in the target element. - `options?: CountUpOptions` - optional configuration object for fine-grain control ### Options | Option | Type | Default | Description | | ---------------------- | --------------- | ------------- | ------------------------------------------------------- | | `startVal` | `number` | `0` | Number to start at | | `decimalPlaces` | `number` | `0` | Number of decimal places | | `duration` | `number` | `2` | Animation duration in seconds | | `useGrouping` | `boolean` | `true` | Example: 1,000 vs 1000 | | `useIndianSeparators` | `boolean` | `false` | Example: 1,00,000 vs 100,000 | | `useEasing` | `boolean` | `true` | Ease animation | | `smartEasingThreshold` | `number` | `999` | Smooth easing for large numbers above this if useEasing | | `smartEasingAmount` | `number` | `333` | Amount to be eased for numbers above threshold | | `separator` | `string` | `','` | Grouping separator | | `decimal` | `string` | `'.'` | Decimal character | | `easingFn` | `function` | `easeOutExpo` | Easing function for animation | | `formattingFn` | `function` | — | Custom function to format the result | | `prefix` | `string` | `''` | Text prepended to result | | `suffix` | `string` | `''` | Text appended to result | | `numerals` | `string[]` | — | Numeral glyph substitution | | `onCompleteCallback` | `function` | — | Callback called when animation completes | | `onStartCallback` | `function` | — | Callback called when animation starts | | `plugin` | `CountUpPlugin` | — | Plugin for alternate animations | | `autoAnimate` | `boolean` | `false` | Trigger animation when target becomes visible | | `autoAnimateDelay` | `number` | `200` | Animation delay in ms after auto-animate triggers | | `autoAnimateOnce` | `boolean` | `false` | Run animation only once for auto-animate triggers | | `enableScrollSpy` | `boolean` | — | *(deprecated)* Use `autoAnimate` instead | | `scrollSpyDelay` | `number` | — | *(deprecated)* Use `autoAnimateDelay` instead | | `scrollSpyOnce` | `boolean` | — | *(deprecated)* Use `autoAnimateOnce` instead | **Example usage**: ```js const countUp = new CountUp('targetId', 5234); if (!countUp.error) { countUp.start(); } else { console.error(countUp.error); } ``` Pass options: ```js const countUp = new CountUp('targetId', 5234, options); ``` with optional complete callback: ```js const countUp = new CountUp('targetId', 5234, { onCompleteCallback: someMethod }); // or (passing fn to start will override options.onCompleteCallback) countUp.start(someMethod); // or countUp.start(() => console.log('Complete!')); ``` **Other methods**: Toggle pause/resume: ```js countUp.pauseResume(); ``` Reset the animation: ```js countUp.reset(); ``` Update the end value and animate: ```js countUp.update(989); ``` Destroy the instance (cancels animation, disconnects observers, clears callbacks): ```js countUp.onDestroy(); ``` --- ### **Auto animate when element becomes visible** Use the `autoAnimate` option to animate when the element is scrolled into view or appears on screen. When using autoAnimate, just initialize CountUp but do not call start(). ```js const countUp = new CountUp('targetId', 989, { autoAnimate: true }); ``` **Note** - Auto-animate uses IntersectionObserver which is broadly supported, but if you need to support some very old browsers, v2.9.0 and earlier use a window on-scroll handler when `enableScrollSpy` is set to true. --- ### **Alternate animations with plugins** Currently there's just one plugin, the **[Odometer Plugin](https://github.com/msoler75/odometer_countup.js)**. To use a plugin, you'll need to first install the plugin package. Then you can include it and use the plugin option. See each plugin's docs for more detailed info. ```js const countUp = new CountUp('targetId', 5234, { plugin: new Odometer({ duration: 2.3, lastDigitDelay: 0 }), duration: 3.0 }); ``` If you'd like to make your own plugin, see [the docs](#creating-animation-plugins) below! ### Tabular nums To optimize the styling of counting number animations, you can take advantage of an OpenType feature called tabular nums which stabilizes jitteryness by using equal-width numbers. In my experience, most OpenType fonts already use tabular nums, so this isn't needed. But it may help to add this style if they don't: ```css font-variant-numeric: tabular-nums; ``` --- ## Including CountUp CountUp is distributed as an ES module, though a UMD module is [also included](#umd-module), along with a separate requestAnimationFrame polyfill (see below). For the examples below, first install CountUp: ``` npm i countup.js ``` ### Example with vanilla js This is what is used in the demo. Checkout index.html and demo.js. main.js: ```js import { CountUp } from './js/countUp.min.js'; window.onload = function() { var countUp = new CountUp('target', 2000); countUp.start(); } ``` Include in your html. Notice the `type` attribute: ```html ``` If you prefer not to use modules, use the `nomodule` script tag to include separate scripts: ```html ``` To run module-enabled scripts locally, you'll need a simple local server setup like [this](https://www.npmjs.com/package/http-server) (test the demo locally by running `npm run serve`) because otherwise you may see a CORS error when your browser tries to load the script as a module. ### For Webpack and other build systems Import from the package, instead of the file location: ```js import { CountUp } from 'countup.js'; ``` ### UMD module CountUp is also wrapped as a UMD module in `./dist/countUp.umd.js` and it exposes CountUp as a global variable on the window scope. To use it, include `countUp.umd.js` in a script tag, and invoke it like so: ```js var numAnim = new countUp.CountUp('myTarget', 2000); numAnim.start() ``` --- ## Contributing Before you make a pull request, please be sure to follow these instructions: 1. Do your work on `src/countUp.ts` 1. Lint: `npm run lint` 1. Run tests: `npm t` 1. Build and serve the demo by running `npm start` then check the demo to make sure it counts. --- ## Creating Animation Plugins CountUp supports plugins as of v2.6.0. Plugins implement their own render method to display each frame's formatted value. A class instance or object can be passed to the `plugin` property of CountUpOptions, and the plugin's render method will be called instead of CountUp's. ```ts export declare interface CountUpPlugin { render(elem: HTMLElement, formatted: string): void; } ``` An example of a plugin: ```ts export class SomePlugin implements CountUpPlugin { // ...some properties here constructor(options: SomePluginOptions) { // ...setup code here if you need it } render(elem: HTMLElement, formatted: string): void { // render DOM here } } ``` If you make a plugin, be sure to create a PR to add it to this README! ================================================ FILE: demo/demo-nomodule.js ================================================ // same as demo.js but with a different instantiation of CountUp, // and no lambdas window.onload = function () { var el = function (id) { return document.getElementById(id); }; var code, stars, endVal, options; var demo = new countUp.CountUp('myTargetElement', 100); var codeVisualizer = el('codeVisualizer'); var errorSection = el('errorSection'); el('version').innerHTML = demo.version; var changeEls = document.querySelectorAll('.updateCodeVis'); for (var i = 0, len = changeEls.length; i < len; i++) { changeEls[i].onchange = updateCodeVisualizer; } el('swapValues').onclick = function () { var oldStartVal = el('startVal').value; var oldEndVal = el('endVal').value; el('startVal').value = oldEndVal; el('endVal').value = oldStartVal; updateCodeVisualizer(); }; el('start').onclick = createCountUp; el('apply').onclick = createCountUp; el('pauseResume').onclick = function () { code += '
demo.pauseResume();'; codeVisualizer.innerHTML = code; demo.pauseResume(); }; el('reset').onclick = function () { code += '
demo.reset();'; codeVisualizer.innerHTML = code; demo.reset(); }; el('update').onclick = function () { var updateVal = el('updateVal').value; var num = updateVal ? updateVal : 0; code += "
demo.update(" + num + ");"; codeVisualizer.innerHTML = code; demo.update(num); }; el('updateVal').onchange = function () { var updateVal = el('updateVal').value; var num = updateVal ? updateVal : 0; code += '
demo.update(' + num + ');'; codeVisualizer.innerHTML = code; }; // OPTION VALUES var easingFunctions = { easeOutExpo: function (t, b, c, d) { return c * (-Math.pow(2, -10 * t / d) + 1) * 1024 / 1023 + b; }, outQuintic: function (t, b, c, d) { var ts = (t /= d) * t; var tc = ts * t; return b + c * (tc * ts + -5 * ts * ts + 10 * tc + -10 * ts + 5 * t); }, outCubic: function (t, b, c, d) { var ts = (t /= d) * t; var tc = ts * t; return b + c * (tc + -3 * ts + 3 * t); } }; function getEasingFn() { var fn = el('easingFnsDropdown').value; if (fn === 'easeOutExpo') { return null; } if (typeof easingFunctions[fn] === 'undefined') { return undefined; } return easingFunctions[fn]; } function getEasingFnBody(fn) { fn = typeof fn === 'undefined' ? getEasingFn() : fn; if (typeof fn === 'undefined') { return 'undefined function'; } if (fn !== null) { return fn.toString().replace(/^ {8}/gm, ''); } return ''; } function getNumerals() { var numeralsCode = el('numeralsDropdown').value; // optionally provide alternates for 0-9 switch (numeralsCode) { case 'ea': // Eastern Arabic return ['٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩']; case 'fa': // Farsi return ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹']; default: return null; } } var stringifyArray = function (arr) { return '[\'' + arr.join('\', \'') + '\']'; }; // COUNTUP AND CODE VISUALIZER function createCountUp() { establishOptionsFromInputs(); demo = new countUp.CountUp('myTargetElement', endVal, options); if (!demo.error) { errorSection.style.display = 'none'; demo.start(); updateCodeVisualizer(); } else { errorSection.style.display = 'block'; document.getElementById('error').innerHTML = demo.error; console.error(demo.error); } } function methodToCallOnComplete() { console.log('COMPLETE!'); alert('COMPLETE!'); } function establishOptionsFromInputs() { endVal = Number(el('endVal').value); options = { startVal: el('startVal').value, decimalPlaces: el('decimalPlaces').value, duration: Number(el('duration').value), useEasing: el('useEasing').checked, useGrouping: el('useGrouping').checked, useIndianSeparators: el('useIndianSeparators').checked, easingFn: typeof getEasingFn() === 'undefined' ? null : getEasingFn(), separator: el('separator').value, decimal: el('decimal').value, prefix: el('prefix').value, suffix: el('suffix').value, numerals: getNumerals(), onCompleteCallback: el('useOnComplete').checked ? methodToCallOnComplete : null }; // unset null values so they don't overwrite defaults for (var key in options) { if (options.hasOwnProperty(key)) { if (options[key] === null) { delete options[key]; } } } } function updateCodeVisualizer() { establishOptionsFromInputs(); code = ''; if (options.useEasing && options.easingFn) { code += 'const easingFn = '; var split = getEasingFnBody(options.easingFn).split('\n'); for (var line in split) { if (split.hasOwnProperty(line)) { code += split[line].replace(' ', ' ') + '
'; } } } function indentedLine(keyPair, singleLine) { if (singleLine === void 0) { singleLine = false; } var delimeter = (singleLine) ? ';' : ','; return "  " + keyPair + delimeter + "
"; } var opts = ''; opts += (options.startVal !== '0') ? indentedLine("startVal: " + options.startVal) : ''; opts += (options.decimalPlaces !== '0') ? indentedLine("decimalPlaces: " + options.decimalPlaces) : ''; opts += (options.duration !== 2) ? indentedLine("duration: " + options.duration) : ''; opts += (options.useEasing) ? '' : indentedLine("useEasing: " + options.useEasing); opts += (options.useEasing && options.easingFn) ? indentedLine("easingFn") : ''; opts += (options.useGrouping) ? '' : indentedLine("useGrouping: " + options.useGrouping); opts += (options.useIndianSeparators) ? indentedLine("useIndianSeparators: " + options.useIndianSeparators) : ''; opts += (options.separator !== ',') ? indentedLine("separator: '" + options.separator + "'") : ''; opts += (options.decimal !== '.') ? indentedLine("decimal: '" + options.decimal + "'") : ''; opts += (options.prefix.length) ? indentedLine("prefix: '" + options.prefix + "'") : ''; opts += (options.suffix.length) ? indentedLine("suffix: '" + options.suffix + "'") : ''; opts += (options.numerals && options.numerals.length) ? indentedLine("numerals: " + stringifyArray(options.numerals)) : ''; opts += (options.onCompleteCallback) ? indentedLine("onCompleteCallback: methodToCallOnComplete") : ''; if (opts.length) { code += "const options = {
" + opts + "};
"; code += "let demo = new CountUp('myTargetElement', " + endVal + ", options);
"; } else { code += "let demo = new CountUp('myTargetElement', " + endVal + ");
"; } code += 'if (!demo.error) {
'; code += indentedLine('demo.start()', true); code += '} else {
'; code += indentedLine('console.error(demo.error)', true); code += '}'; codeVisualizer.innerHTML = code; } // get current star count var repoInfoUrl = 'https://api.github.com/repos/inorganik/CountUp.js'; var getStars = new XMLHttpRequest(); getStars.open('GET', repoInfoUrl, true); getStars.timeout = 5000; getStars.onreadystatechange = function () { // 2: received headers, 3: loading, 4: done if (getStars.readyState === 4) { if (getStars.status === 200) { if (getStars.responseText !== 'undefined') { if (getStars.responseText.length > 0) { var data = JSON.parse(getStars.responseText); stars = data.stargazers_count; // change input values el('endVal').value = stars; createCountUp(); } } } } }; getStars.onerror = function () { console.error('error getting stars:', getStars.status); stars = getStars.status; demo.start(); }; getStars.send(); } ================================================ FILE: demo/demo.js ================================================ import { CountUp } from '../dist/countUp.js'; const el = (id) => document.getElementById(id); let code, stars, endVal, options; let demo = new CountUp('myTargetElement', 100); let scrollSpyCountUp, hiddenAtInitCountUp, insideModalCountUp; const codeVisualizer = el('codeVisualizer'); const errorSection = el('errorSection'); let startTime; el('version').textContent = demo.version; document.querySelectorAll('.updateCodeVis').forEach((elem) => { elem.addEventListener('change', updateCodeVisualizer); }); el('swapValues').addEventListener('click', () => { const oldStartVal = el('startVal').value; const oldEndVal = el('endVal').value; el('startVal').value = oldEndVal; el('endVal').value = oldStartVal; updateCodeVisualizer(); }); el('start').addEventListener('click', createCountUp); el('apply').addEventListener('click', createCountUp); el('pauseResume').addEventListener('click', () => { code += '
demo.pauseResume();'; codeVisualizer.innerHTML = code; demo.pauseResume(); }); el('reset').addEventListener('click', () => { code += '
demo.reset();'; codeVisualizer.innerHTML = code; demo.reset(); }); el('update').addEventListener('click', () => { const num = el('updateVal').value || 0; code += `
demo.update(${num});`; codeVisualizer.innerHTML = code; demo.update(num); }); el('updateVal').addEventListener('change', () => { const num = el('updateVal').value || 0; code += `
demo.update(${num});`; codeVisualizer.innerHTML = code; }); const easingFunctions = { easeOutExpo: (t, b, c, d) => c * (-Math.pow(2, -10 * t / d) + 1) * 1024 / 1023 + b, outQuintic: (t, b, c, d) => { const ts = (t /= d) * t; const tc = ts * t; return b + c * (tc * ts + -5 * ts * ts + 10 * tc + -10 * ts + 5 * t); }, outCubic: (t, b, c, d) => { const ts = (t /= d) * t; const tc = ts * t; return b + c * (tc + -3 * ts + 3 * t); } }; function getEasingFn() { const fn = el('easingFnsDropdown').value; if (fn === 'easeOutExpo') return null; if (easingFunctions[fn] === undefined) return undefined; return easingFunctions[fn]; } function getEasingFnBody(fn = getEasingFn()) { if (fn === undefined) return 'undefined function'; if (fn !== null) return fn.toString().replace(/^ {4}/gm, ''); return ''; } function getNumerals() { const numeralsCode = el('numeralsDropdown').value; switch (numeralsCode) { case 'ea': return ['٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩']; case 'fa': return ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹']; default: return null; } } const stringifyArray = (arr) => `['${arr.join("', '")}']`; function createCountUp() { demo.onDestroy(); establishOptionsFromInputs(); demo = new CountUp('myTargetElement', endVal, options); if (!demo.error) { errorSection.style.display = 'none'; startTime = Date.now(); demo.start(); updateCodeVisualizer(); } else { errorSection.style.display = 'block'; el('error').textContent = demo.error; console.error(demo.error); } } function calculateAnimationTime() { const duration = Date.now() - startTime; console.log('actual animation duration (ms):', duration); alert('COMPLETE!'); } function establishOptionsFromInputs() { endVal = Number(el('endVal').value); options = { startVal: el('startVal').value, decimalPlaces: el('decimalPlaces').value, duration: Number(el('duration').value), useGrouping: el('useGrouping').checked, useIndianSeparators: el('useIndianSeparators').checked, easingFn: getEasingFn() ?? null, separator: el('separator').value, decimal: el('decimal').value, prefix: el('prefix').value, numerals: getNumerals(), onCompleteCallback: el('useOnComplete').checked ? calculateAnimationTime : null }; for (const [key, value] of Object.entries(options)) { if (value === null) delete options[key]; } } function updateCodeVisualizer() { establishOptionsFromInputs(); code = ''; if (options.easingFn) { code += 'const easingFn = '; for (const line of getEasingFnBody(options.easingFn).split('\n')) { code += `${line.replace(' ', ' ')}
`; } } const indentedLine = (keyPair, singleLine = false) => { const delimiter = singleLine ? ';' : ','; return `  ${keyPair}${delimiter}
`; }; let opts = ''; opts += (options.startVal !== '0') ? indentedLine(`startVal: ${options.startVal}`) : ''; opts += (options.decimalPlaces !== '0') ? indentedLine(`decimalPlaces: ${options.decimalPlaces}`) : ''; opts += (options.duration !== 2) ? indentedLine(`duration: ${options.duration}`) : ''; opts += options.easingFn ? indentedLine('easingFn') : ''; opts += options.useGrouping ? '' : indentedLine(`useGrouping: ${options.useGrouping}`); opts += options.useIndianSeparators ? indentedLine(`useIndianSeparators: ${options.useIndianSeparators}`) : ''; opts += (options.separator !== ',') ? indentedLine(`separator: '${options.separator}'`) : ''; opts += (options.decimal !== '.') ? indentedLine(`decimal: '${options.decimal}'`) : ''; opts += options.prefix.length ? indentedLine(`prefix: '${options.prefix}'`) : ''; opts += (options.numerals && options.numerals.length) ? indentedLine(`numerals: ${stringifyArray(options.numerals)}`) : ''; opts += options.onCompleteCallback ? indentedLine('onCompleteCallback: methodToCallOnComplete') : ''; if (opts.length) { code += `const options = {
${opts}};
`; code += `const demo = new CountUp('myTargetElement', ${endVal}, options);
`; } else { code += `const demo = new CountUp('myTargetElement', ${endVal});
`; } code += 'if (!demo.error) {
'; code += indentedLine('demo.start()', true); code += '} else {
'; code += indentedLine('console.error(demo.error)', true); code += '}'; codeVisualizer.innerHTML = code; } // auto animate options function getAutoAnimateOptions() { return { autoAnimate: true, autoAnimateOnce: el('autoAnimateOnce').checked, autoAnimateDelay: Number(el('autoAnimateDelay').value), onCompleteCallback: null, }; } function recreateAutoAnimateDemos() { createScrollSpyCountUp(); createHiddenAtInitCountUp(); createInsideModalCountUp(); } el('autoAnimateOnce').addEventListener('change', recreateAutoAnimateDemos); el('autoAnimateDelay').addEventListener('change', recreateAutoAnimateDemos); // scroll spy function createScrollSpyCountUp() { if (scrollSpyCountUp) scrollSpyCountUp.onDestroy(); establishOptionsFromInputs(); const scrollSpyOptions = { ...options, ...getAutoAnimateOptions() }; scrollSpyCountUp = new CountUp('scrollSpyTarget', endVal, scrollSpyOptions); if (scrollSpyCountUp.error) { console.error('scrollSpyCountUp error:', scrollSpyCountUp.error); } } createScrollSpyCountUp(); el('apply').addEventListener('click', createScrollSpyCountUp); el('start').addEventListener('click', createScrollSpyCountUp); // hidden at init function createHiddenAtInitCountUp() { if (hiddenAtInitCountUp) hiddenAtInitCountUp.onDestroy(); establishOptionsFromInputs(); const hiddenOptions = { ...options, ...getAutoAnimateOptions() }; hiddenAtInitCountUp = new CountUp('hiddenAtInitTarget', endVal, hiddenOptions); if (hiddenAtInitCountUp.error) { console.error('hiddenAtInitCountUp error:', hiddenAtInitCountUp.error); } } createHiddenAtInitCountUp(); el('apply').addEventListener('click', createHiddenAtInitCountUp); el('start').addEventListener('click', createHiddenAtInitCountUp); el('toggleVisibility').addEventListener('click', () => { const target = el('hiddenAtInitTarget'); target.style.display = target.style.display === 'none' ? '' : 'none'; }); // inside modal function createInsideModalCountUp() { if (insideModalCountUp) insideModalCountUp.onDestroy(); establishOptionsFromInputs(); const modalOptions = { ...options, ...getAutoAnimateOptions() }; insideModalCountUp = new CountUp('modalTarget', endVal, modalOptions); if (insideModalCountUp.error) { console.error('insideModalCountUp error:', insideModalCountUp.error); } } createInsideModalCountUp(); el('apply').addEventListener('click', createInsideModalCountUp); el('start').addEventListener('click', createInsideModalCountUp); el('openModal').addEventListener('click', () => el('modalDialog').showModal()); el('closeModal').addEventListener('click', () => el('modalDialog').close()); // get current star count try { const response = await fetch('https://api.github.com/repos/inorganik/CountUp.js'); if (response.ok) { const data = await response.json(); stars = data.stargazers_count; el('endVal').value = stars; createCountUp(); createScrollSpyCountUp(); createHiddenAtInitCountUp(); createInsideModalCountUp(); } } catch (error) { console.error('error getting stars:', error); demo.start(); } ================================================ FILE: dist/countUp.d.ts ================================================ export interface CountUpOptions { /** Number to start at @default 0 */ startVal?: number; /** Number of decimal places @default 0 */ decimalPlaces?: number; /** Animation duration in seconds @default 2 */ duration?: number; /** Example: 1,000 vs 1000 @default true */ useGrouping?: boolean; /** Example: 1,00,000 vs 100,000 @default false */ useIndianSeparators?: boolean; /** Ease animation @default true */ useEasing?: boolean; /** Smooth easing for large numbers above this if useEasing @default 999 */ smartEasingThreshold?: number; /** Amount to be eased for numbers above threshold @default 333 */ smartEasingAmount?: number; /** Grouping separator @default ',' */ separator?: string; /** Decimal character @default '.' */ decimal?: string; /** Easing function for animation @default easeOutExpo */ easingFn?: (t: number, b: number, c: number, d: number) => number; /** Custom function to format the result */ formattingFn?: (n: number) => string; /** Text prepended to result */ prefix?: string; /** Text appended to result */ suffix?: string; /** Numeral glyph substitution */ numerals?: string[]; /** Callback called when animation completes */ onCompleteCallback?: () => any; /** Callback called when animation starts */ onStartCallback?: () => any; /** Plugin for alternate animations */ plugin?: CountUpPlugin; /** Trigger animation when target becomes visible @default false */ autoAnimate?: boolean; /** Animation delay in ms after auto-animate triggers @default 200 */ autoAnimateDelay?: number; /** Run animation only once for auto-animate triggers @default false */ autoAnimateOnce?: boolean; /** @deprecated Please use autoAnimate instead */ enableScrollSpy?: boolean; /** @deprecated Please use autoAnimateDelay instead */ scrollSpyDelay?: number; /** @deprecated Please use autoAnimateOnce instead */ scrollSpyOnce?: boolean; } export declare interface CountUpPlugin { render(elem: HTMLElement, formatted: string): void; } /** * Animates a number by counting to it. * playground: stackblitz.com/edit/countup-typescript * * @param target - id of html element, input, svg text element, or DOM element reference where counting occurs. * @param endVal - the value you want to arrive at. * @param options - optional configuration object for fine-grain control */ export declare class CountUp { private endVal?; options?: CountUpOptions; version: string; private static observedElements; private defaults; private rAF; private autoAnimateTimeout; private startTime; private remaining; private finalEndVal; private useEasing; private countDown; private observer; el: HTMLElement | HTMLInputElement; formattingFn: (num: number) => string; easingFn?: (t: number, b: number, c: number, d: number) => number; error: string; startVal: number; duration: number; paused: boolean; frameVal: number; once: boolean; constructor(target: string | HTMLElement | HTMLInputElement, endVal?: number | null, options?: CountUpOptions); /** Set up an IntersectionObserver to auto-animate when the target element appears. */ private setupObserver; /** Disconnect the IntersectionObserver and stop watching this element. */ unobserve(): void; /** Teardown: cancel animation, disconnect observer, clear callbacks. */ onDestroy(): void; /** * Smart easing works by breaking the animation into 2 parts, the second part being the * smartEasingAmount and first part being the total amount minus the smartEasingAmount. It works * by disabling easing for the first part and enabling it on the second part. It is used if * useEasing is true and the total animation amount exceeds the smartEasingThreshold. */ private determineDirectionAndSmartEasing; /** Start the animation. Optionally pass a callback that fires on completion. */ start(callback?: (args?: any) => any): void; /** Toggle pause/resume on the animation. */ pauseResume(): void; /** Reset to startVal so the animation can be run again. */ reset(): void; /** Pass a new endVal and start the animation. */ update(newEndVal: string | number): void; /** Animation frame callback — advances the value each frame. */ count: (timestamp: number) => void; /** Format and render the given value to the target element. */ printValue(val: number): void; /** Return true if the value is a finite number. */ ensureNumber(n: any): boolean; /** Validate and convert a value to a number, setting an error if invalid. */ validateValue(value: string | number): number; /** Reset startTime, duration, and remaining to their initial values. */ private resetDuration; /** Default number formatter with grouping, decimals, prefix/suffix, and numeral substitution. */ formatNumber: (num: number) => string; /** * Default easing function (easeOutExpo). * @param t current time * @param b beginning value * @param c change in value * @param d duration */ easeOutExpo: (t: number, b: number, c: number, d: number) => number; /** Parse a formatted string back to a number using the current separator/decimal options. */ parse(number: string): number; } ================================================ FILE: dist/countUp.js ================================================ var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; /** * Animates a number by counting to it. * playground: stackblitz.com/edit/countup-typescript * * @param target - id of html element, input, svg text element, or DOM element reference where counting occurs. * @param endVal - the value you want to arrive at. * @param options - optional configuration object for fine-grain control */ var CountUp = /** @class */ (function () { function CountUp(target, endVal, options) { var _this = this; this.endVal = endVal; this.options = options; this.version = '2.10.0'; this.defaults = { startVal: 0, decimalPlaces: 0, duration: 2, useEasing: true, useGrouping: true, useIndianSeparators: false, smartEasingThreshold: 999, smartEasingAmount: 333, separator: ',', decimal: '.', prefix: '', suffix: '', autoAnimate: false, autoAnimateDelay: 200, autoAnimateOnce: false, }; this.finalEndVal = null; // for smart easing this.useEasing = true; this.countDown = false; this.error = ''; this.startVal = 0; this.paused = true; this.once = false; /** Animation frame callback — advances the value each frame. */ this.count = function (timestamp) { if (!_this.startTime) { _this.startTime = timestamp; } var progress = timestamp - _this.startTime; _this.remaining = _this.duration - progress; // to ease or not to ease if (_this.useEasing) { if (_this.countDown) { _this.frameVal = _this.startVal - _this.easingFn(progress, 0, _this.startVal - _this.endVal, _this.duration); } else { _this.frameVal = _this.easingFn(progress, _this.startVal, _this.endVal - _this.startVal, _this.duration); } } else { _this.frameVal = _this.startVal + (_this.endVal - _this.startVal) * (progress / _this.duration); } // don't go past endVal since progress can exceed duration in the last frame var wentPast = _this.countDown ? _this.frameVal < _this.endVal : _this.frameVal > _this.endVal; _this.frameVal = wentPast ? _this.endVal : _this.frameVal; // decimal _this.frameVal = Number(_this.frameVal.toFixed(_this.options.decimalPlaces)); // format and print value _this.printValue(_this.frameVal); // whether to continue if (progress < _this.duration) { _this.rAF = requestAnimationFrame(_this.count); } else if (_this.finalEndVal !== null) { // smart easing _this.update(_this.finalEndVal); } else { if (_this.options.onCompleteCallback) { _this.options.onCompleteCallback(); } } }; /** Default number formatter with grouping, decimals, prefix/suffix, and numeral substitution. */ this.formatNumber = function (num) { var neg = (num < 0) ? '-' : ''; var result, x1, x2, x3; result = Math.abs(num).toFixed(_this.options.decimalPlaces); result += ''; var x = result.split('.'); x1 = x[0]; x2 = x.length > 1 ? _this.options.decimal + x[1] : ''; if (_this.options.useGrouping) { x3 = ''; var factor = 3, j = 0; for (var i = 0, len = x1.length; i < len; ++i) { if (_this.options.useIndianSeparators && i === 4) { factor = 2; j = 1; } if (i !== 0 && (j % factor) === 0) { x3 = _this.options.separator + x3; } j++; x3 = x1[len - i - 1] + x3; } x1 = x3; } // optional numeral substitution if (_this.options.numerals && _this.options.numerals.length) { x1 = x1.replace(/[0-9]/g, function (w) { return _this.options.numerals[+w]; }); x2 = x2.replace(/[0-9]/g, function (w) { return _this.options.numerals[+w]; }); } return neg + _this.options.prefix + x1 + x2 + _this.options.suffix; }; /** * Default easing function (easeOutExpo). * @param t current time * @param b beginning value * @param c change in value * @param d duration */ this.easeOutExpo = function (t, b, c, d) { return c * (-Math.pow(2, -10 * t / d) + 1) * 1024 / 1023 + b; }; this.options = __assign(__assign({}, this.defaults), options); if (this.options.enableScrollSpy) { this.options.autoAnimate = true; } if (this.options.scrollSpyDelay !== undefined) { this.options.autoAnimateDelay = this.options.scrollSpyDelay; } if (this.options.scrollSpyOnce) { this.options.autoAnimateOnce = true; } this.formattingFn = (this.options.formattingFn) ? this.options.formattingFn : this.formatNumber; this.easingFn = (this.options.easingFn) ? this.options.easingFn : this.easeOutExpo; this.el = (typeof target === 'string') ? document.getElementById(target) : target; endVal = endVal == null ? this.parse(this.el.innerHTML) : endVal; this.startVal = this.validateValue(this.options.startVal); this.frameVal = this.startVal; this.endVal = this.validateValue(endVal); this.options.decimalPlaces = Math.max(0 || this.options.decimalPlaces); this.resetDuration(); this.options.separator = String(this.options.separator); this.useEasing = this.options.useEasing; if (this.options.separator === '') { this.options.useGrouping = false; } if (this.el) { this.printValue(this.startVal); } else { this.error = '[CountUp] target is null or undefined'; } if (typeof window !== 'undefined' && this.options.autoAnimate) { if (!this.error && typeof IntersectionObserver !== 'undefined') { this.setupObserver(); } else { if (this.error) { console.error(this.error, target); } else { console.error('IntersectionObserver is not supported by this browser'); } } } } /** Set up an IntersectionObserver to auto-animate when the target element appears. */ CountUp.prototype.setupObserver = function () { var _this = this; var existing = CountUp.observedElements.get(this.el); if (existing) { existing.unobserve(); } CountUp.observedElements.set(this.el, this); this.observer = new IntersectionObserver(function (entries) { for (var _i = 0, entries_1 = entries; _i < entries_1.length; _i++) { var entry = entries_1[_i]; if (entry.isIntersecting && _this.paused && !_this.once) { _this.paused = false; _this.autoAnimateTimeout = setTimeout(function () { return _this.start(); }, _this.options.autoAnimateDelay); if (_this.options.autoAnimateOnce) { _this.once = true; _this.observer.disconnect(); } } else if (!entry.isIntersecting && !_this.paused) { clearTimeout(_this.autoAnimateTimeout); _this.reset(); } } }, { threshold: 0 }); this.observer.observe(this.el); }; /** Disconnect the IntersectionObserver and stop watching this element. */ CountUp.prototype.unobserve = function () { var _a; clearTimeout(this.autoAnimateTimeout); (_a = this.observer) === null || _a === void 0 ? void 0 : _a.disconnect(); CountUp.observedElements.delete(this.el); }; /** Teardown: cancel animation, disconnect observer, clear callbacks. */ CountUp.prototype.onDestroy = function () { clearTimeout(this.autoAnimateTimeout); cancelAnimationFrame(this.rAF); this.paused = true; this.unobserve(); this.options.onCompleteCallback = null; this.options.onStartCallback = null; }; /** * Smart easing works by breaking the animation into 2 parts, the second part being the * smartEasingAmount and first part being the total amount minus the smartEasingAmount. It works * by disabling easing for the first part and enabling it on the second part. It is used if * useEasing is true and the total animation amount exceeds the smartEasingThreshold. */ CountUp.prototype.determineDirectionAndSmartEasing = function () { var end = (this.finalEndVal) ? this.finalEndVal : this.endVal; this.countDown = (this.startVal > end); var animateAmount = end - this.startVal; if (Math.abs(animateAmount) > this.options.smartEasingThreshold && this.options.useEasing) { this.finalEndVal = end; var up = (this.countDown) ? 1 : -1; this.endVal = end + (up * this.options.smartEasingAmount); this.duration = this.duration / 2; } else { this.endVal = end; this.finalEndVal = null; } if (this.finalEndVal !== null) { // setting finalEndVal indicates smart easing this.useEasing = false; } else { this.useEasing = this.options.useEasing; } }; /** Start the animation. Optionally pass a callback that fires on completion. */ CountUp.prototype.start = function (callback) { if (this.error) { return; } if (this.options.onStartCallback) { this.options.onStartCallback(); } if (callback) { this.options.onCompleteCallback = callback; } if (this.duration > 0) { this.determineDirectionAndSmartEasing(); this.paused = false; this.rAF = requestAnimationFrame(this.count); } else { this.printValue(this.endVal); } }; /** Toggle pause/resume on the animation. */ CountUp.prototype.pauseResume = function () { if (!this.paused) { cancelAnimationFrame(this.rAF); } else { this.startTime = null; this.duration = this.remaining; this.startVal = this.frameVal; this.determineDirectionAndSmartEasing(); this.rAF = requestAnimationFrame(this.count); } this.paused = !this.paused; }; /** Reset to startVal so the animation can be run again. */ CountUp.prototype.reset = function () { clearTimeout(this.autoAnimateTimeout); cancelAnimationFrame(this.rAF); this.paused = true; this.once = false; this.resetDuration(); this.startVal = this.validateValue(this.options.startVal); this.frameVal = this.startVal; this.printValue(this.startVal); }; /** Pass a new endVal and start the animation. */ CountUp.prototype.update = function (newEndVal) { cancelAnimationFrame(this.rAF); this.startTime = null; this.endVal = this.validateValue(newEndVal); if (this.endVal === this.frameVal) { return; } this.startVal = this.frameVal; if (this.finalEndVal == null) { this.resetDuration(); } this.finalEndVal = null; this.determineDirectionAndSmartEasing(); this.rAF = requestAnimationFrame(this.count); }; /** Format and render the given value to the target element. */ CountUp.prototype.printValue = function (val) { var _a; if (!this.el) return; var result = this.formattingFn(val); if ((_a = this.options.plugin) === null || _a === void 0 ? void 0 : _a.render) { this.options.plugin.render(this.el, result); return; } if (this.el.tagName === 'INPUT') { var input = this.el; input.value = result; } else if (this.el.tagName === 'text' || this.el.tagName === 'tspan') { this.el.textContent = result; } else { this.el.innerHTML = result; } }; /** Return true if the value is a finite number. */ CountUp.prototype.ensureNumber = function (n) { return (typeof n === 'number' && !isNaN(n)); }; /** Validate and convert a value to a number, setting an error if invalid. */ CountUp.prototype.validateValue = function (value) { var newValue = Number(value); if (!this.ensureNumber(newValue)) { this.error = "[CountUp] invalid start or end value: ".concat(value); return null; } else { return newValue; } }; /** Reset startTime, duration, and remaining to their initial values. */ CountUp.prototype.resetDuration = function () { this.startTime = null; this.duration = Number(this.options.duration) * 1000; this.remaining = this.duration; }; /** Parse a formatted string back to a number using the current separator/decimal options. */ CountUp.prototype.parse = function (number) { // eslint-disable-next-line no-irregular-whitespace var escapeRegExp = function (s) { return s.replace(/([.,'  ])/g, '\\$1'); }; var sep = escapeRegExp(this.options.separator); var dec = escapeRegExp(this.options.decimal); var num = number.replace(new RegExp(sep, 'g'), '').replace(new RegExp(dec, 'g'), '.'); return parseFloat(num); }; CountUp.observedElements = new WeakMap(); return CountUp; }()); export { CountUp }; ================================================ FILE: dist/countUp.umd.js ================================================ !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).countUp={})}(this,(function(t){"use strict";var e=function(){return e=Object.assign||function(t){for(var e,i=1,s=arguments.length;in.endVal;n.frameVal=i?n.endVal:n.frameVal,n.frameVal=Number(n.frameVal.toFixed(n.options.decimalPlaces)),n.printValue(n.frameVal),e1?n.options.decimal+r[1]:"",n.options.useGrouping){a="";for(var l=3,u=0,h=0,p=i.length;ht;var e=t-this.startVal;if(Math.abs(e)>this.options.smartEasingThreshold&&this.options.useEasing){this.finalEndVal=t;var i=this.countDown?1:-1;this.endVal=t+i*this.options.smartEasingAmount,this.duration=this.duration/2}else this.endVal=t,this.finalEndVal=null;null!==this.finalEndVal?this.useEasing=!1:this.useEasing=this.options.useEasing},t.prototype.start=function(t){this.error||(this.options.onStartCallback&&this.options.onStartCallback(),t&&(this.options.onCompleteCallback=t),this.duration>0?(this.determineDirectionAndSmartEasing(),this.paused=!1,this.rAF=requestAnimationFrame(this.count)):this.printValue(this.endVal))},t.prototype.pauseResume=function(){this.paused?(this.startTime=null,this.duration=this.remaining,this.startVal=this.frameVal,this.determineDirectionAndSmartEasing(),this.rAF=requestAnimationFrame(this.count)):cancelAnimationFrame(this.rAF),this.paused=!this.paused},t.prototype.reset=function(){clearTimeout(this.autoAnimateTimeout),cancelAnimationFrame(this.rAF),this.paused=!0,this.once=!1,this.resetDuration(),this.startVal=this.validateValue(this.options.startVal),this.frameVal=this.startVal,this.printValue(this.startVal)},t.prototype.update=function(t){cancelAnimationFrame(this.rAF),this.startTime=null,this.endVal=this.validateValue(t),this.endVal!==this.frameVal&&(this.startVal=this.frameVal,null==this.finalEndVal&&this.resetDuration(),this.finalEndVal=null,this.determineDirectionAndSmartEasing(),this.rAF=requestAnimationFrame(this.count))},t.prototype.printValue=function(t){var e;if(this.el){var i=this.formattingFn(t);if(null===(e=this.options.plugin)||void 0===e?void 0:e.render)this.options.plugin.render(this.el,i);else if("INPUT"===this.el.tagName)this.el.value=i;else"text"===this.el.tagName||"tspan"===this.el.tagName?this.el.textContent=i:this.el.innerHTML=i}},t.prototype.ensureNumber=function(t){return"number"==typeof t&&!isNaN(t)},t.prototype.validateValue=function(t){var e=Number(t);return this.ensureNumber(e)?e:(this.error="[CountUp] invalid start or end value: ".concat(t),null)},t.prototype.resetDuration=function(){this.startTime=null,this.duration=1e3*Number(this.options.duration),this.remaining=this.duration},t.prototype.parse=function(t){var e=function(t){return t.replace(/([.,'  ])/g,"\\$1")},i=e(this.options.separator),s=e(this.options.decimal),n=t.replace(new RegExp(i,"g"),"").replace(new RegExp(s,"g"),".");return parseFloat(n)},t.observedElements=new WeakMap,t}();t.CountUp=i})); ================================================ FILE: dist/requestAnimationFrame.polyfill.js ================================================ // make sure requestAnimationFrame and cancelAnimationFrame are defined // polyfill for browsers without native support // by Opera engineer Erik Möller (function () { var lastTime = 0; var vendors = ['webkit', 'moz', 'ms', 'o']; for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame']; window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] || window[vendors[x] + 'CancelRequestAnimationFrame']; } if (!window.requestAnimationFrame) { window.requestAnimationFrame = function (callback) { var currTime = new Date().getTime(); var timeToCall = Math.max(0, 16 - (currTime - lastTime)); var id = window.setTimeout(function () { return callback(currTime + timeToCall); }, timeToCall); lastTime = currTime + timeToCall; return id; }; } if (!window.cancelAnimationFrame) { window.cancelAnimationFrame = function (id) { clearTimeout(id); }; } })(); ================================================ FILE: index.html ================================================ CountUp.js Fork me on GitHub
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

CountUp.js  

CountUp.js is a dependency-free, lightweight JavaScript class that animates a numerical value by counting to it.

Install via npm/yarn using the package name  countup.js.

View on GitHub

Current stars:

0

Params:

Options:

+ many more

Methods:

Custom:

Auto animate demos (scroll down)

Use the autoAnimate option to animate when the target element appears.

0

Hidden at init

0

Inside a modal

0

================================================ FILE: jest.config.js ================================================ module.exports = { roots: [ '/src' ], transform: { '^.+\\.ts?$': 'ts-jest' }, testRegex: '(\\.|/)(test|spec)\\.ts?$', testEnvironment: 'jsdom', moduleFileExtensions: [ 'ts', 'js', 'jsx', 'json', 'node' ] } ================================================ FILE: package.json ================================================ { "name": "countup.js", "description": "Animates a numerical value by counting to it", "version": "2.10.0", "license": "MIT", "author": "Jamie Perkins", "main": "./dist/countUp.umd.js", "module": "./dist/countUp.min.js", "types": "./dist/countUp.d.ts", "repository": { "type": "git", "url": "git+https://github.com/inorganik/countUp.js.git" }, "exports": { "types": "./dist/countUp.d.ts", "import": "./dist/countUp.min.js", "require": "./dist/countUp.umd.js" }, "scripts": { "build": "npm run clean && tsc && rollup -c rollup.config.mjs", "clean": "rimraf dist/countUp.*", "lint": "eslint -c .eslintrc.js --ext .ts ./src", "serve": "http-server -o -c-1 ./", "start": "npm run build && npm run serve", "test": "jest", "test:watch": "jest --watch" }, "devDependencies": { "@eslint/js": "^9.6.0", "@rollup/plugin-terser": "^0.4.4", "@types/eslint__js": "^8.42.3", "@types/jest": "^29.5.12", "eslint": "^8.57.0", "eslint-plugin-import": "^2.29.1", "http-server": "^14.1.1", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "rimraf": "^5.0.9", "rollup": "^4.18.1", "ts-jest": "^29.2.2", "typescript": "^5.5.3", "typescript-eslint": "^7.16.0" } } ================================================ FILE: rollup.config.mjs ================================================ import terser from '@rollup/plugin-terser'; /** * Regarding "(!) `this` has been rewritten to `undefined`" warning: * It occurs because of typescript's Object.assign polyfill, which uses * `this` on the global scope. If you set `context: 'window'` in the rollup * config, it will silence the warning, but it will cause issues if CountUp * is not run in the browser. Allowing rollup to rewrite this to undefined * on just the global scope is harmless and doesn't break anything. */ export default [ // minified build { input: 'dist/countUp.js', output: { file: 'dist/countUp.min.js', }, plugins: [ terser(), // minify the output ], }, // UMD build { input: 'dist/countUp.js', output: { file: 'dist/countUp.umd.js', name: 'countUp', format: 'umd', }, plugins: [ terser(), ], } ]; ================================================ FILE: src/countUp.spec.ts ================================================ import { CountUp, CountUpPlugin } from './countUp'; type IntersectionCallback = (entries: Partial[]) => void; class MockIntersectionObserver { callback: IntersectionCallback; elements: Element[] = []; static instances: MockIntersectionObserver[] = []; constructor(callback: IntersectionCallback) { this.callback = callback; MockIntersectionObserver.instances.push(this); } observe(el: Element) { this.elements.push(el); } unobserve(el: Element) { this.elements = this.elements.filter(e => e !== el); } disconnect() { this.elements = []; } trigger(isIntersecting: boolean) { this.callback(this.elements.map(target => ({ isIntersecting, target } as Partial))); } } describe('CountUp', () => { let countUp; let time; const getTargetHtml = () => document.getElementById('target')?.innerHTML; const resetRAF = () => { time = 0; jest.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => { time += 100; if (time < 2500) { return cb(time) as any; } }); }; beforeEach(() => { document.body.innerHTML = '
' + '

' + '
'; (window as any).IntersectionObserver = MockIntersectionObserver; MockIntersectionObserver.instances = []; countUp = new CountUp('target', 100); resetRAF(); }); describe('constructor', () => { it('should create for a valid target, and print startVal', () => { expect(countUp).toBeTruthy(); expect(countUp.error.length).toBe(0); expect(getTargetHtml()).toEqual('0'); }); it('should set an error for a bad target', () => { countUp = new CountUp('notThere', 100); expect(countUp.error.length).toBeGreaterThan(0); }); it('should set an error for a bad endVal', () => { const endVal = '%' as any; countUp = new CountUp('target', endVal); expect(countUp.error.length).toBeGreaterThan(0); }); it('should set an error for a bad startVal', () => { const startVal = 'oops' as any; countUp = new CountUp('target', 100, { startVal }); expect(countUp.error.length).toBeGreaterThan(0); }); it('should return a value for version', () => { expect(countUp.version).toBeTruthy(); }); it('should support getting endVal from the target element', () => { document.body.innerHTML = '
' + '

1,500

' + '
'; countUp = new CountUp('target'); expect(countUp.endVal).toBe(1500); }); it('should set an error when endVal is omitted and not in target element', () => { document.body.innerHTML = '
' + '

' + '
'; countUp = new CountUp('target'); expect(countUp.error.length).toBeGreaterThan(0); }); it('should not call parse when an endVal is passed to the constructor', () => { const parseSpy = jest.spyOn(CountUp.prototype, 'parse'); countUp = new CountUp('target', 0, { startVal: 100 }); expect(parseSpy).not.toHaveBeenCalled(); parseSpy.mockRestore(); }); }); describe('class methods', () => { describe('# start', () => { it('should count when start method is called', () => { countUp.start(); expect(getTargetHtml()).toEqual('100'); }); it('should use a callback provided to start', () => { const cb = jest.fn(); countUp.start(cb); expect(getTargetHtml()).toEqual('100'); expect(cb).toHaveBeenCalled(); }); }); describe('# pauseResume', () => { it('should pause when pauseResume is called', () => { countUp.start(); countUp.pauseResume(); expect(countUp.paused).toBeTruthy(); }); }); describe('# reset', () => { it('should reset when reset is called', () => { countUp.start(); countUp.reset(); expect(getTargetHtml()).toEqual('0'); expect(countUp.paused).toBeTruthy(); }); }); describe('# update', () => { it('should update when update is called', () => { countUp.start(); expect(getTargetHtml()).toEqual('100'); resetRAF(); countUp.update(200); expect(getTargetHtml()).toEqual('200'); }); }); describe('# onDestroy', () => { it('should cancel a running animation', () => { const cancelSpy = jest.spyOn(window, 'cancelAnimationFrame'); countUp.start(); countUp.onDestroy(); expect(cancelSpy).toHaveBeenCalled(); }); it('should set paused to true', () => { countUp.start(); expect(countUp.paused).toBe(false); countUp.onDestroy(); expect(countUp.paused).toBe(true); }); it('should disconnect the observer', () => { countUp = new CountUp('target', 100, { autoAnimate: true }); const observer = MockIntersectionObserver.instances[MockIntersectionObserver.instances.length - 1]; const disconnectSpy = jest.spyOn(observer, 'disconnect'); countUp.onDestroy(); expect(disconnectSpy).toHaveBeenCalled(); }); it('should clear onCompleteCallback', () => { const cb = jest.fn(); countUp = new CountUp('target', 100, { onCompleteCallback: cb }); countUp.onDestroy(); expect(countUp.options.onCompleteCallback).toBeNull(); }); it('should clear onStartCallback', () => { const cb = jest.fn(); countUp = new CountUp('target', 100, { onStartCallback: cb }); countUp.onDestroy(); expect(countUp.options.onStartCallback).toBeNull(); }); it('should prevent onCompleteCallback from firing after destroy', () => { const cb = jest.fn(); countUp = new CountUp('target', 100, { onCompleteCallback: cb }); countUp.onDestroy(); resetRAF(); countUp.start(); expect(cb).not.toHaveBeenCalled(); }); it('should be safe to call on a fresh instance', () => { countUp = new CountUp('target', 100); expect(() => countUp.onDestroy()).not.toThrow(); expect(countUp.paused).toBe(true); }); }); describe('# parse', () => { it('should properly parse numbers', () => { countUp = new CountUp('target', 0); const result0 = countUp.parse('14,921.00123'); countUp = new CountUp('target', 0, { separator: '.', decimal: ',' }); const result1 = countUp.parse('1.500,0'); countUp = new CountUp('target', 0, { separator: ' ' }); const result2 = countUp.parse('2 800'); expect(result0).toEqual(14921.00123); expect(result1).toEqual(1500); expect(result2).toEqual(2800); }); }); }); describe('various use-cases', () => { it('should handle large numbers', () => { countUp = new CountUp('target', 6000); const spy = jest.spyOn(countUp, 'determineDirectionAndSmartEasing'); countUp.start(); expect(getTargetHtml()).toEqual('6,000'); expect(spy).toHaveBeenCalled(); }); it('should not use easing when specified with a large number (auto-smooth)', () => { countUp = new CountUp('target', 6000, { useEasing: false }); const spy = jest.spyOn(countUp, 'easingFn'); countUp.start(); expect(getTargetHtml()).toEqual('6,000'); expect(spy).toHaveBeenCalledTimes(0); }); it('should count down when endVal is less than startVal', () => { countUp = new CountUp('target', 10, { startVal: 500 }); expect(getTargetHtml()).toEqual('500'); countUp.start(); expect(getTargetHtml()).toEqual('10'); }); it('should handle negative numbers', () => { countUp = new CountUp('target', -500); countUp.start(); expect(getTargetHtml()).toEqual('-500'); }); it('should properly handle a zero duration', () => { countUp = new CountUp('target', 2000, { duration: 0 }); countUp.start(); expect(getTargetHtml()).toEqual('2,000'); }); it('should call the callback when finished if there is one', () => { const cb = jest.fn(); countUp.start(cb); expect(getTargetHtml()).toEqual('100'); expect(cb).toHaveBeenCalled(); }); }); describe('options', () => { it('should respect the decimalPlaces option', () => { countUp = new CountUp('target', 100, { decimalPlaces: 2 }); countUp.start(); expect(getTargetHtml()).toEqual('100.00'); }); it('should respect the duration option', () => { countUp = new CountUp('target', 100, { duration: 1 }); countUp.start(); expect(getTargetHtml()).toEqual('100'); }); it('should respect the useEasing option', () => { countUp = new CountUp('target', 100, { useEasing: false }); countUp.start(); expect(getTargetHtml()).toEqual('100'); }); it('should respect the useGrouping option', () => { countUp = new CountUp('target', 100000, { useGrouping: false }); countUp.start(); expect(getTargetHtml()).toEqual('100000'); resetRAF(); countUp = new CountUp('target', 1000000, { useGrouping: true }); countUp.start(); expect(getTargetHtml()).toEqual('1,000,000'); }); it('should respect the useIndianSeparators option', () => { countUp = new CountUp('target', 100000, { useIndianSeparators: true }); countUp.start(); expect(getTargetHtml()).toEqual('1,00,000'); resetRAF(); countUp = new CountUp('target', 10000000, { useIndianSeparators: true }); countUp.start(); expect(getTargetHtml()).toEqual('1,00,00,000'); }); it('should respect the separator option', () => { countUp = new CountUp('target', 10000, { separator: ':' }); countUp.start(); expect(getTargetHtml()).toEqual('10:000'); }); it('should respect the decimal option', () => { countUp = new CountUp('target', 100, { decimal: ',', decimalPlaces: 1 }); countUp.start(); expect(getTargetHtml()).toEqual('100,0'); }); it('should respect the easingFn option', () => { const easeOutQuintic = jest.fn().mockReturnValue(100); countUp = new CountUp('target', 100, { easingFn: easeOutQuintic }); countUp.start(); expect(easeOutQuintic).toHaveBeenCalled(); expect(getTargetHtml()).toEqual('100'); }); it('should respect the formattingFn option', () => { const formatter = jest.fn().mockReturnValue('~100~'); countUp = new CountUp('target', 100, { formattingFn: formatter }); countUp.start(); expect(formatter).toHaveBeenCalled(); expect(getTargetHtml()).toEqual('~100~'); }); it('should respect the prefix option', () => { countUp = new CountUp('target', 100, { prefix: '$' }); countUp.start(); expect(getTargetHtml()).toEqual('$100'); }); it('should respect the suffix option', () => { countUp = new CountUp('target', 100, { suffix: '!' }); countUp.start(); expect(getTargetHtml()).toEqual('100!'); }); it('should respect the numerals option', () => { const numerals = [')', '!', '@', '#', '$', '%', '^', '&', '*', '(']; countUp = new CountUp('target', 100, { numerals }); countUp.start(); expect(getTargetHtml()).toEqual('!))'); }); it('should respect the onCompleteCallback option', () => { const options = { onCompleteCallback: jest.fn() }; const callbackSpy = jest.spyOn(options, 'onCompleteCallback'); countUp = new CountUp('target', 100, options); countUp.start(); expect(getTargetHtml()).toEqual('100'); expect(callbackSpy).toHaveBeenCalled(); }); it('should respect the onStartCallback option', () => { const options = { onStartCallback: jest.fn() }; const callbackSpy = jest.spyOn(options, 'onStartCallback'); countUp = new CountUp('target', 100, options); countUp.start(); expect(callbackSpy).toHaveBeenCalled(); expect(getTargetHtml()).toEqual('100'); }); it('should respect the plugin option', () => { const plugin: CountUpPlugin = { render: (el, result) => { el.innerHTML = result; } }; countUp = new CountUp('target', 1000, { plugin, useGrouping: true }); countUp.start(); expect(getTargetHtml()).toEqual('1,000'); }); }); describe('autoAnimate (IntersectionObserver)', () => { beforeEach(() => { jest.useFakeTimers({ doNotFake: ['requestAnimationFrame'] }); }); afterEach(() => { jest.useRealTimers(); }); it('should create an IntersectionObserver when autoAnimate is true', () => { countUp = new CountUp('target', 100, { autoAnimate: true }); expect(MockIntersectionObserver.instances.length).toBe(1); expect(MockIntersectionObserver.instances[0].elements).toContain(countUp.el); }); it('should not create an observer when autoAnimate is false', () => { MockIntersectionObserver.instances = []; countUp = new CountUp('target', 100); expect(MockIntersectionObserver.instances.length).toBe(0); }); it('should start animation when element becomes visible', () => { countUp = new CountUp('target', 100, { autoAnimate: true, autoAnimateDelay: 0 }); resetRAF(); const observer = MockIntersectionObserver.instances[0]; observer.trigger(true); jest.advanceTimersByTime(0); expect(getTargetHtml()).toEqual('100'); }); it('should respect autoAnimateDelay before starting', () => { countUp = new CountUp('target', 100, { autoAnimate: true, autoAnimateDelay: 500 }); resetRAF(); const startSpy = jest.spyOn(countUp, 'start'); const observer = MockIntersectionObserver.instances[0]; observer.trigger(true); expect(startSpy).not.toHaveBeenCalled(); jest.advanceTimersByTime(500); expect(startSpy).toHaveBeenCalled(); }); it('should reset when element goes out of view', () => { countUp = new CountUp('target', 100, { autoAnimate: true, autoAnimateDelay: 0 }); resetRAF(); const observer = MockIntersectionObserver.instances[0]; observer.trigger(true); jest.advanceTimersByTime(0); expect(getTargetHtml()).toEqual('100'); observer.trigger(false); expect(countUp.paused).toBe(true); expect(getTargetHtml()).toEqual('0'); }); it('should disconnect observer when autoAnimateOnce is true', () => { countUp = new CountUp('target', 100, { autoAnimate: true, autoAnimateOnce: true, autoAnimateDelay: 0 }); const observer = MockIntersectionObserver.instances[0]; const disconnectSpy = jest.spyOn(observer, 'disconnect'); observer.trigger(true); jest.advanceTimersByTime(0); expect(disconnectSpy).toHaveBeenCalled(); expect(countUp.once).toBe(true); }); it('should not disconnect observer when autoAnimateOnce is false', () => { countUp = new CountUp('target', 100, { autoAnimate: true, autoAnimateOnce: false, autoAnimateDelay: 0 }); const observer = MockIntersectionObserver.instances[0]; const disconnectSpy = jest.spyOn(observer, 'disconnect'); observer.trigger(true); jest.advanceTimersByTime(0); expect(disconnectSpy).not.toHaveBeenCalled(); }); it('should not re-animate after first run when autoAnimateOnce is true', () => { countUp = new CountUp('target', 100, { autoAnimate: true, autoAnimateOnce: true, autoAnimateDelay: 0 }); resetRAF(); const observer = MockIntersectionObserver.instances[MockIntersectionObserver.instances.length - 1]; observer.trigger(true); jest.advanceTimersByTime(0); expect(getTargetHtml()).toEqual('100'); // observer was disconnected so subsequent triggers process no entries observer.trigger(false); expect(getTargetHtml()).toEqual('100'); observer.trigger(true); jest.advanceTimersByTime(0); expect(getTargetHtml()).toEqual('100'); }); it('should allow re-animation after manual reset when autoAnimateOnce is true', () => { countUp = new CountUp('target', 100, { autoAnimate: true, autoAnimateOnce: true, autoAnimateDelay: 0 }); resetRAF(); const observer = MockIntersectionObserver.instances[MockIntersectionObserver.instances.length - 1]; observer.trigger(true); jest.advanceTimersByTime(0); expect(getTargetHtml()).toEqual('100'); expect(countUp.once).toBe(true); // manual reset clears the once flag countUp.reset(); expect(getTargetHtml()).toEqual('0'); expect(countUp.once).toBe(false); // re-observe and trigger — animation should play again observer.observe(countUp.el); resetRAF(); observer.trigger(true); jest.advanceTimersByTime(0); expect(getTargetHtml()).toEqual('100'); }); it('should support multiple independent instances', () => { document.body.innerHTML = '

' + '

'; MockIntersectionObserver.instances = []; const cu1 = new CountUp('target1', 50, { autoAnimate: true, autoAnimateDelay: 0 }); const cu2 = new CountUp('target2', 200, { autoAnimate: true, autoAnimateDelay: 0 }); expect(MockIntersectionObserver.instances.length).toBe(2); const obs1 = MockIntersectionObserver.instances[0]; const obs2 = MockIntersectionObserver.instances[1]; expect(obs1.elements).toContain(cu1.el); expect(obs2.elements).toContain(cu2.el); expect(obs1).not.toBe(obs2); resetRAF(); obs1.trigger(true); jest.advanceTimersByTime(0); expect(document.getElementById('target1')!.innerHTML).toEqual('50'); expect(cu2.paused).toBe(true); }); it('should allow cleanup via unobserve()', () => { countUp = new CountUp('target', 100, { autoAnimate: true }); const observer = MockIntersectionObserver.instances[0]; const disconnectSpy = jest.spyOn(observer, 'disconnect'); countUp.unobserve(); expect(disconnectSpy).toHaveBeenCalled(); }); it('should map deprecated enableScrollSpy to autoAnimate', () => { countUp = new CountUp('target', 100, { enableScrollSpy: true }); expect(countUp.options.autoAnimate).toBe(true); expect(MockIntersectionObserver.instances.length).toBe(1); }); }); }); ================================================ FILE: src/countUp.ts ================================================ export interface CountUpOptions { /** Number to start at @default 0 */ startVal?: number; /** Number of decimal places @default 0 */ decimalPlaces?: number; /** Animation duration in seconds @default 2 */ duration?: number; /** Example: 1,000 vs 1000 @default true */ useGrouping?: boolean; /** Example: 1,00,000 vs 100,000 @default false */ useIndianSeparators?: boolean; /** Ease animation @default true */ useEasing?: boolean; /** Smooth easing for large numbers above this if useEasing @default 999 */ smartEasingThreshold?: number; /** Amount to be eased for numbers above threshold @default 333 */ smartEasingAmount?: number; /** Grouping separator @default ',' */ separator?: string; /** Decimal character @default '.' */ decimal?: string; /** Easing function for animation @default easeOutExpo */ easingFn?: (t: number, b: number, c: number, d: number) => number; /** Custom function to format the result */ formattingFn?: (n: number) => string; /** Text prepended to result */ prefix?: string; /** Text appended to result */ suffix?: string; /** Numeral glyph substitution */ numerals?: string[]; /** Callback called when animation completes */ onCompleteCallback?: () => any; /** Callback called when animation starts */ onStartCallback?: () => any; /** Plugin for alternate animations */ plugin?: CountUpPlugin; /** Trigger animation when target becomes visible @default false */ autoAnimate?: boolean; /** Animation delay in ms after auto-animate triggers @default 200 */ autoAnimateDelay?: number; /** Run animation only once for auto-animate triggers @default false */ autoAnimateOnce?: boolean; /** @deprecated Please use autoAnimate instead */ enableScrollSpy?: boolean; /** @deprecated Please use autoAnimateDelay instead */ scrollSpyDelay?: number; /** @deprecated Please use autoAnimateOnce instead */ scrollSpyOnce?: boolean; } export declare interface CountUpPlugin { render(elem: HTMLElement, formatted: string): void; } /** * Animates a number by counting to it. * playground: stackblitz.com/edit/countup-typescript * * @param target - id of html element, input, svg text element, or DOM element reference where counting occurs. * @param endVal - the value you want to arrive at. * @param options - optional configuration object for fine-grain control */ export class CountUp { version = '2.10.0'; private static observedElements = new WeakMap(); private defaults: CountUpOptions = { startVal: 0, decimalPlaces: 0, duration: 2, useEasing: true, useGrouping: true, useIndianSeparators: false, smartEasingThreshold: 999, smartEasingAmount: 333, separator: ',', decimal: '.', prefix: '', suffix: '', autoAnimate: false, autoAnimateDelay: 200, autoAnimateOnce: false, }; private rAF: any; private autoAnimateTimeout: any; private startTime: number; private remaining: number; private finalEndVal: number = null; // for smart easing private useEasing = true; private countDown = false; private observer: IntersectionObserver; el: HTMLElement | HTMLInputElement; formattingFn: (num: number) => string; easingFn?: (t: number, b: number, c: number, d: number) => number; error = ''; startVal = 0; duration: number; paused = true; frameVal: number; once = false; constructor( target: string | HTMLElement | HTMLInputElement, private endVal?: number | null, public options?: CountUpOptions ) { this.options = { ...this.defaults, ...options }; if (this.options.enableScrollSpy) { this.options.autoAnimate = true; } if (this.options.scrollSpyDelay !== undefined) { this.options.autoAnimateDelay = this.options.scrollSpyDelay; } if (this.options.scrollSpyOnce) { this.options.autoAnimateOnce = true; } this.formattingFn = (this.options.formattingFn) ? this.options.formattingFn : this.formatNumber; this.easingFn = (this.options.easingFn) ? this.options.easingFn : this.easeOutExpo; this.el = (typeof target === 'string') ? document.getElementById(target) : target; endVal = endVal == null ? this.parse(this.el.innerHTML) : endVal; this.startVal = this.validateValue(this.options.startVal); this.frameVal = this.startVal; this.endVal = this.validateValue(endVal); this.options.decimalPlaces = Math.max(0 || this.options.decimalPlaces); this.resetDuration(); this.options.separator = String(this.options.separator); this.useEasing = this.options.useEasing; if (this.options.separator === '') { this.options.useGrouping = false; } if (this.el) { this.printValue(this.startVal); } else { this.error = '[CountUp] target is null or undefined'; } if (typeof window !== 'undefined' && this.options.autoAnimate) { if (!this.error && typeof IntersectionObserver !== 'undefined') { this.setupObserver(); } else { if (this.error) { console.error(this.error, target); } else { console.error('IntersectionObserver is not supported by this browser'); } } } } /** Set up an IntersectionObserver to auto-animate when the target element appears. */ private setupObserver(): void { const existing = CountUp.observedElements.get(this.el as HTMLElement); if (existing) { existing.unobserve(); } CountUp.observedElements.set(this.el as HTMLElement, this); this.observer = new IntersectionObserver((entries) => { for (const entry of entries) { if (entry.isIntersecting && this.paused && !this.once) { this.paused = false; this.autoAnimateTimeout = setTimeout(() => this.start(), this.options.autoAnimateDelay); if (this.options.autoAnimateOnce) { this.once = true; this.observer.disconnect(); } } else if (!entry.isIntersecting && !this.paused) { clearTimeout(this.autoAnimateTimeout); this.reset(); } } }, { threshold: 0 }); this.observer.observe(this.el); } /** Disconnect the IntersectionObserver and stop watching this element. */ unobserve(): void { clearTimeout(this.autoAnimateTimeout); this.observer?.disconnect(); CountUp.observedElements.delete(this.el as HTMLElement); } /** Teardown: cancel animation, disconnect observer, clear callbacks. */ onDestroy(): void { clearTimeout(this.autoAnimateTimeout); cancelAnimationFrame(this.rAF); this.paused = true; this.unobserve(); this.options.onCompleteCallback = null; this.options.onStartCallback = null; } /** * Smart easing works by breaking the animation into 2 parts, the second part being the * smartEasingAmount and first part being the total amount minus the smartEasingAmount. It works * by disabling easing for the first part and enabling it on the second part. It is used if * useEasing is true and the total animation amount exceeds the smartEasingThreshold. */ private determineDirectionAndSmartEasing(): void { const end = (this.finalEndVal) ? this.finalEndVal : this.endVal; this.countDown = (this.startVal > end); const animateAmount = end - this.startVal; if (Math.abs(animateAmount) > this.options.smartEasingThreshold && this.options.useEasing) { this.finalEndVal = end; const up = (this.countDown) ? 1 : -1; this.endVal = end + (up * this.options.smartEasingAmount); this.duration = this.duration / 2; } else { this.endVal = end; this.finalEndVal = null; } if (this.finalEndVal !== null) { // setting finalEndVal indicates smart easing this.useEasing = false; } else { this.useEasing = this.options.useEasing; } } /** Start the animation. Optionally pass a callback that fires on completion. */ start(callback?: (args?: any) => any): void { if (this.error) { return; } if (this.options.onStartCallback) { this.options.onStartCallback(); } if (callback) { this.options.onCompleteCallback = callback; } if (this.duration > 0) { this.determineDirectionAndSmartEasing(); this.paused = false; this.rAF = requestAnimationFrame(this.count); } else { this.printValue(this.endVal); } } /** Toggle pause/resume on the animation. */ pauseResume(): void { if (!this.paused) { cancelAnimationFrame(this.rAF); } else { this.startTime = null; this.duration = this.remaining; this.startVal = this.frameVal; this.determineDirectionAndSmartEasing(); this.rAF = requestAnimationFrame(this.count); } this.paused = !this.paused; } /** Reset to startVal so the animation can be run again. */ reset(): void { clearTimeout(this.autoAnimateTimeout); cancelAnimationFrame(this.rAF); this.paused = true; this.once = false; this.resetDuration(); this.startVal = this.validateValue(this.options.startVal); this.frameVal = this.startVal; this.printValue(this.startVal); } /** Pass a new endVal and start the animation. */ update(newEndVal: string | number): void { cancelAnimationFrame(this.rAF); this.startTime = null; this.endVal = this.validateValue(newEndVal); if (this.endVal === this.frameVal) { return; } this.startVal = this.frameVal; if (this.finalEndVal == null) { this.resetDuration(); } this.finalEndVal = null; this.determineDirectionAndSmartEasing(); this.rAF = requestAnimationFrame(this.count); } /** Animation frame callback — advances the value each frame. */ count = (timestamp: number): void => { if (!this.startTime) { this.startTime = timestamp; } const progress = timestamp - this.startTime; this.remaining = this.duration - progress; // to ease or not to ease if (this.useEasing) { if (this.countDown) { this.frameVal = this.startVal - this.easingFn(progress, 0, this.startVal - this.endVal, this.duration); } else { this.frameVal = this.easingFn(progress, this.startVal, this.endVal - this.startVal, this.duration); } } else { this.frameVal = this.startVal + (this.endVal - this.startVal) * (progress / this.duration); } // don't go past endVal since progress can exceed duration in the last frame const wentPast = this.countDown ? this.frameVal < this.endVal : this.frameVal > this.endVal; this.frameVal = wentPast ? this.endVal : this.frameVal; // decimal this.frameVal = Number(this.frameVal.toFixed(this.options.decimalPlaces)); // format and print value this.printValue(this.frameVal); // whether to continue if (progress < this.duration) { this.rAF = requestAnimationFrame(this.count); } else if (this.finalEndVal !== null) { // smart easing this.update(this.finalEndVal); } else { if (this.options.onCompleteCallback) { this.options.onCompleteCallback(); } } } /** Format and render the given value to the target element. */ printValue(val: number): void { if (!this.el) return; const result = this.formattingFn(val); if (this.options.plugin?.render) { this.options.plugin.render(this.el, result); return; } if (this.el.tagName === 'INPUT') { const input = this.el as HTMLInputElement; input.value = result; } else if (this.el.tagName === 'text' || this.el.tagName === 'tspan') { this.el.textContent = result; } else { this.el.innerHTML = result; } } /** Return true if the value is a finite number. */ ensureNumber(n: any): boolean { return (typeof n === 'number' && !isNaN(n)); } /** Validate and convert a value to a number, setting an error if invalid. */ validateValue(value: string | number): number { const newValue = Number(value); if (!this.ensureNumber(newValue)) { this.error = `[CountUp] invalid start or end value: ${value}`; return null; } else { return newValue; } } /** Reset startTime, duration, and remaining to their initial values. */ private resetDuration(): void { this.startTime = null; this.duration = Number(this.options.duration) * 1000; this.remaining = this.duration; } /** Default number formatter with grouping, decimals, prefix/suffix, and numeral substitution. */ formatNumber = (num: number): string => { const neg = (num < 0) ? '-' : ''; let result: string, x1: string, x2: string, x3: string; result = Math.abs(num).toFixed(this.options.decimalPlaces); result += ''; const x = result.split('.'); x1 = x[0]; x2 = x.length > 1 ? this.options.decimal + x[1] : ''; if (this.options.useGrouping) { x3 = ''; let factor = 3, j = 0; for (let i = 0, len = x1.length; i < len; ++i) { if (this.options.useIndianSeparators && i === 4) { factor = 2; j = 1; } if (i !== 0 && (j % factor) === 0) { x3 = this.options.separator + x3; } j++; x3 = x1[len - i - 1] + x3; } x1 = x3; } // optional numeral substitution if (this.options.numerals && this.options.numerals.length) { x1 = x1.replace(/[0-9]/g, (w) => this.options.numerals[+w]); x2 = x2.replace(/[0-9]/g, (w) => this.options.numerals[+w]); } return neg + this.options.prefix + x1 + x2 + this.options.suffix; } /** * Default easing function (easeOutExpo). * @param t current time * @param b beginning value * @param c change in value * @param d duration */ easeOutExpo = (t: number, b: number, c: number, d: number): number => c * (-Math.pow(2, -10 * t / d) + 1) * 1024 / 1023 + b; /** Parse a formatted string back to a number using the current separator/decimal options. */ parse(number: string): number { // eslint-disable-next-line no-irregular-whitespace const escapeRegExp = (s: string) => s.replace(/([.,'  ])/g, '\\$1'); const sep = escapeRegExp(this.options.separator); const dec = escapeRegExp(this.options.decimal); const num = number.replace(new RegExp(sep, 'g'), '').replace(new RegExp(dec, 'g'), '.'); return parseFloat(num) } } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "lib": ["es2017", "dom"], "module": "esnext", "moduleResolution": "node", "declaration": true, "outDir": "dist", "target": "es5", "pretty": true, "esModuleInterop": true, "skipLibCheck": true, }, "compileOnSave": true, "include": ["src"], "exclude": [ "node_modules", "**/*.spec.ts" ] }