├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── custom.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── codacy.yml │ └── static.yml ├── .gitignore ├── .npmignore ├── .prettierrc.json ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── babel.config.json ├── dist ├── Textify.min.css ├── Textify.min.js └── Textify.min.js.LICENSE.txt ├── docs └── main-site │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── .prettierrc.json │ ├── .vscode │ └── extensions.json │ ├── README.md │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ └── site.webmanifest │ ├── src │ ├── App.vue │ ├── assets │ │ ├── code.svg │ │ ├── copy.svg │ │ ├── default.svg │ │ ├── restart.svg │ │ └── styles │ │ │ ├── base │ │ │ ├── _base.scss │ │ │ ├── _reset.scss │ │ │ └── _typography.scss │ │ │ ├── components │ │ │ ├── _codebox.scss │ │ │ ├── _footer.scss │ │ │ ├── _navigation.scss │ │ │ └── _sidebar.scss │ │ │ ├── index.scss │ │ │ ├── layouts │ │ │ ├── _flexbox.scss │ │ │ └── _scroll.scss │ │ │ ├── pages │ │ │ ├── _docs.scss │ │ │ ├── _example.scss │ │ │ ├── _home.scss │ │ │ ├── _notfound.scss │ │ │ └── _tutorial.scss │ │ │ ├── shared │ │ │ ├── _links.scss │ │ │ ├── _sections.scss │ │ │ └── _texts.scss │ │ │ └── utils │ │ │ └── _variables.scss │ ├── components │ │ ├── AnimationBox.vue │ │ ├── CodeBox.vue │ │ ├── FooterBox.vue │ │ ├── LargeAnimationBox.vue │ │ ├── Navigation.vue │ │ └── SideBar.vue │ ├── layouts │ │ └── MainLayout.vue │ ├── library │ │ ├── Textify.min.css │ │ └── Textify.min.js │ ├── main.js │ ├── router │ │ └── index.js │ └── views │ │ ├── Documentation.vue │ │ ├── Example.vue │ │ ├── Home.vue │ │ ├── NotFound.vue │ │ └── Tutorial.vue │ ├── vercel.json │ └── vite.config.js ├── package-lock.json ├── package.json ├── src ├── Utils │ ├── Break.js │ ├── Config.js │ ├── Text.js │ ├── dom.js │ └── isBrowser.js ├── index.js └── types │ └── Texts.js ├── style └── Textify.css └── webpack.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env":{ 4 | "browser":true, 5 | "node":true , 6 | "es6":true 7 | }, 8 | "extends":["eslint:recommended","prettier"], 9 | "parser": "@babel/eslint-parser", 10 | "parserOptions":{ 11 | "sourceType":"module" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 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 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 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/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/codacy.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | # This workflow checks out code, performs a Codacy security scan 7 | # and integrates the results with the 8 | # GitHub Advanced Security code scanning feature. For more information on 9 | # the Codacy security scan action usage and parameters, see 10 | # https://github.com/codacy/codacy-analysis-cli-action. 11 | # For more information on Codacy Analysis CLI in general, see 12 | # https://github.com/codacy/codacy-analysis-cli. 13 | 14 | name: Codacy Security Scan 15 | 16 | on: 17 | push: 18 | branches: [ "master" ] 19 | pull_request: 20 | # The branches below must be a subset of the branches above 21 | branches: [ "master" ] 22 | schedule: 23 | - cron: '23 13 * * 2' 24 | 25 | permissions: 26 | contents: read 27 | 28 | jobs: 29 | codacy-security-scan: 30 | permissions: 31 | contents: read # for actions/checkout to fetch code 32 | security-events: write # for github/codeql-action/upload-sarif to upload SARIF results 33 | name: Codacy Security Scan 34 | runs-on: ubuntu-latest 35 | steps: 36 | # Checkout the repository to the GitHub Actions runner 37 | - name: Checkout code 38 | uses: actions/checkout@v3 39 | 40 | # Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis 41 | - name: Run Codacy Analysis CLI 42 | uses: codacy/codacy-analysis-cli-action@d840f886c4bd4edc059706d09c6a1586111c540b 43 | with: 44 | # Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository 45 | # You can also omit the token and run the tools that support default configurations 46 | project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} 47 | verbose: true 48 | output: results.sarif 49 | format: sarif 50 | # Adjust severity of non-security issues 51 | gh-code-scanning-compat: true 52 | # Force 0 exit code to allow SARIF file generation 53 | # This will handover control about PR rejection to the GitHub side 54 | max-allowed-issues: 2147483647 55 | 56 | # Upload the SARIF file generated in the previous step 57 | - name: Upload SARIF results file 58 | uses: github/codeql-action/upload-sarif@v2 59 | with: 60 | sarif_file: results.sarif 61 | -------------------------------------------------------------------------------- /.github/workflows/static.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["master"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow one concurrent deployment 19 | concurrency: 20 | group: "pages" 21 | cancel-in-progress: true 22 | 23 | jobs: 24 | # Single deploy job since we're just deploying 25 | deploy: 26 | environment: 27 | name: github-pages 28 | url: ${{ steps.deployment.outputs.page_url }} 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v3 33 | - name: Setup Pages 34 | uses: actions/configure-pages@v3 35 | - name: Upload artifact 36 | uses: actions/upload-pages-artifact@v1 37 | with: 38 | # Upload entire repository 39 | path: '.' 40 | - name: Deploy to GitHub Pages 41 | id: deployment 42 | uses: actions/deploy-pages@v1 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.cache 3 | /.npm 4 | /.yarn 5 | /.DS_Store 6 | /.git 7 | /examples 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.cache 3 | /.npm 4 | /.yarn 5 | /.DS_Store 6 | /package-lock.json 7 | /yarn.lock 8 | /webpack.config.js 9 | /examples 10 | /website 11 | /docs 12 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "printWidth": 150, 4 | "singleQuote": false, 5 | "trailingComma": "none", 6 | "semi": true, 7 | "arrowParens": "always" 8 | } 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | jeetpatel0070070@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jeet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
Next Generation Text Animation Library.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
15 |
16 |
17 |
18 | Live Examples
19 |
Some cool text.😎😎
78 | ``` 79 | 80 | Initialize textify to see magic (add gsap too). 81 | ```javascript 82 | import Textify from "textify.js"; 83 | import gsap from "gsap"; 84 | 85 | new Textify({}, gsap); 86 | ``` 87 | 88 |By default textify use default configurations for text animations. You can pass an configuration object during initialization to tweak it.
89 | 90 | # 91 | ## Configuration options 92 | 93 | ```javascript 94 | splitType: "lines words chars", // chars or words or lines 95 | largeText: false, // true or false 96 | observer: { 97 | repeat: false, // true or false 98 | threshold: 0.5 // 0.0 ~ 1.0 99 | }, 100 | animation: { 101 | by: "chars", // chars or words or lines 102 | duration: 0.5, // seconds 103 | stagger: 0.05, // seconds 104 | delay: 0.0, // seconds 105 | ease: "ease", // ease or linear or cubic-bezier 106 | customAnimation: false, // true or false 107 | transformOrigin: "center center", // center center or top left or top center or top right or center right or bottom right or bottom center or bottom left or center left 108 | animateProps: { 109 | opacity: 1, // 0 ~ 1 110 | y: "100%", // -100 ~ 100 (%) 111 | x: 0, // -100 ~ 100 (%) 112 | scale: 1, // 0 ~ 1 113 | rotate: 0, // -360 ~ 360 114 | skewX: 0, // -360 ~ 360 115 | skewY: 0 // -360 ~ 360 116 | } 117 | ``` 118 | 119 | For, more information about configs, check Documentation 120 | 121 | # 122 | # Documentation 123 | Check main documentation of Textify.js here: 124 | 125 | - [Site](https://textify-js.vercel.app) 126 | - [Documentation & Examples](https://textify-js.vercel.app/documentation) 127 | - [Demo](https://textify-js.vercel.app/example) 128 | 129 | # 130 | # Methods 131 | Textify contains instance methods. these are used to control the animation. these methods are help to maintain animation stability. these 132 | methods are following: 133 | 134 | * `animateIn` - Reveal animation. 135 | * `animateOut` - Hide animation. 136 | * `reset` - Re-calulate all texts position and offset (call on DOM changes and resize event) 137 | 138 | Example: 139 | ```javascript 140 | const textObj = new Textify({}, gsap); 141 | 142 | // reveal all animations of textObj 143 | textObj.animateIn(); 144 | 145 | // hide all animations of textObj 146 | textObj.animateOut(); 147 | 148 | // Re-calulate all texts position and offset 149 | textObj.reset(); 150 | 151 | ``` 152 | 153 | ## License 154 | 155 | Released under [MIT](/LICENSE) by [@MAGGIx1404](https://github.com/MAGGIx1404). 156 | 157 | ## Rate us 158 | 159 | Enjoying textify.js? [Please leave a short review on Openbase](https://openbase.com/js/textify.js#rate) 160 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.1.x | :white_check_mark: | 11 | | 5.0.x | :x: | 12 | | 4.0.x | :white_check_mark: | 13 | | < 4.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Use this section to tell people how to report a vulnerability. 18 | 19 | Tell them where to go, how often they can expect to get an update on a 20 | reported vulnerability, what to expect if the vulnerability is accepted or 21 | declined, etc. 22 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceType": "unambiguous", 3 | "plugins": ["@babel/plugin-transform-runtime"], 4 | "presets": ["@babel/preset-env"] 5 | } 6 | -------------------------------------------------------------------------------- /dist/Textify.min.css: -------------------------------------------------------------------------------- 1 | /* fix safari custom kerning by adding a space after each character */ 2 | * { 3 | -webkit-font-feature-settings: "kern" 1; 4 | -moz-font-feature-settings: "kern" 1; 5 | -ms-font-feature-settings: "kern" 1; 6 | -o-font-feature-settings: "kern" 1; 7 | font-feature-settings: "kern" 1; 8 | font-kerning: none; 9 | -webkit-text-rendering: optimizeSpeed; 10 | text-rendering: optimizeSpeed; 11 | -webkit-transform: translateZ(0); 12 | transform: translateZ(0); 13 | } 14 | 15 | /* ----------------------------------------- */ 16 | .textify .word, 17 | .textify .char { 18 | display: inline-block; 19 | will-change: transform, opacity; /* for safari */ 20 | } 21 | 22 | /* ----------------------------------------- */ 23 | 24 | .textify .char { 25 | position: relative; 26 | } 27 | 28 | /** 29 | * Populate the psuedo elements with the character to allow for expanded effects 30 | * Set to `display: none` by default; just add `display: block` when you want 31 | * to use the psuedo elements 32 | */ 33 | .textify .char::before, 34 | .textify .char::after, 35 | .textify .word::after, 36 | .textify .word::before, 37 | .textify .line::after, 38 | .textify .line::before { 39 | position: absolute; 40 | top: 0; 41 | left: 0; 42 | visibility: hidden; 43 | transition: inherit; 44 | display: none; 45 | } 46 | 47 | .textify .char::before, 48 | .textify .char::after { 49 | content: attr(data-char); 50 | } 51 | 52 | .textify .word::after, 53 | .textify .word::before { 54 | content: attr(data-word); 55 | } 56 | 57 | .textify .line::after, 58 | .textify .line::before { 59 | content: attr(data-line); 60 | } 61 | 62 | /* ------------------------------------------- */ 63 | 64 | .textify .line-box, 65 | .textify .line { 66 | overflow: hidden; 67 | } 68 | -------------------------------------------------------------------------------- /dist/Textify.min.js: -------------------------------------------------------------------------------- 1 | /*! For license information please see Textify.min.js.LICENSE.txt */ 2 | !function(D,u){"object"==typeof exports&&"object"==typeof module?module.exports=u():"function"==typeof define&&define.amd?define([],u):"object"==typeof exports?exports["Textify.js"]=u():D["Textify.js"]=u()}(this,function(){"use strict";var e={d:function(D,u){for(var t in u)e.o(u,t)&&!e.o(D,t)&&Object.defineProperty(D,t,{enumerable:!0,get:u[t]})}},D=(e.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(D){if("object"==typeof window)return window}}(),e.o=function(D,u){return Object.prototype.hasOwnProperty.call(D,u)},{});function i(D,u){(null==u||u>D.length)&&(u=D.length);for(var t=0,e=new Array(u);t":">")}}function G(D,u){for(var t=u.length;-1<--t;)D.push(u[t])}function z(D,u,t){for(var e;D&&D!==u;){if(e=D._next||D.nextSibling)return e.textContent.charAt(0)===t;D=D.parentNode||D._parent}}function $(D){for(var u,t=c(D.childNodes),e=t.length,i=0;i14 | The pages are not responsive yet. Please view this site on a desktop or laptop for now. we 15 | are working on it. Thank you for your patience. 16 |
17 |18 | We are looking for contributors to help us with this. If you are interested, please check 19 | out project's github repo and create a pull request. Thank you. 20 |
21 | 27 | Go to Github 28 | 29 |3 |5 | 6 | 7 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /docs/main-site/src/components/FooterBox.vue: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /docs/main-site/src/components/LargeAnimationBox.vue: -------------------------------------------------------------------------------- 1 | 2 |4 |
{{ content }}
4 | 5 | 8 | 11 |d.clientWidth)&&(d.style.width=c-V+"px",d.clientWidth
14 | Textify.js is a JavaScript library that allows you to animate text in a web page. It is
15 | a simple and easy to use library that provides you with a variety of animations to
16 | choose from and allows you to customize them to your liking. It is also very easy to
17 | install and use.
18 |
24 | Start playing around with your own Textify demo on CodePen with this template that
25 | includes all of the essentials!
26 |
33 | Textify can be called without parameters, but only define
34 | engine and it will use the default options. Textify converts
35 | the text into a small div & span and
36 | add a class to each. You can use the class to create css animations.
37 |
39 | Note: Textify.js's animations are not working properly without
40 | Textify.min.css. So make sure to include it in your
41 | project.
42 | CSS link : JavaScript link : GSAP (Engine) : Install Textify.js from NPM:
70 | import Textify.js and gsap in your project and use it like this:
71 |
79 | Or you can use the CDN link in your project and use it like this: (default)
80 |
90 | And then you can use it as a Module in a modern web browsers,
91 | node.js, and deno. This is the recommended way to use Textify.js.
92 |
100 | Note: Textify.js's animations are not working properly without
101 | Textify.min.css. So make sure to include it in your
102 | project.
103 | Intialize Textify.js : (connect gsap with it)
113 | Textify.js default selector is data-textify. So you can use it
114 | in your HTML like this:
115 | Then, see the magic! and animation will start automatically.
448 | Textify contains instance methods. these are used to
449 | control the animation. these methods are help to maintain animation stability. these
450 | methods are following:
451 |
493 | Using Textify you can create custom
494 | CSS animations. Textify's
495 | CSS provide some default and helpfull
496 | CSS styles for selector and it's pseudo elements. you can use
497 | these styles to create custom animations.
498 |
500 | Textify's selector get some default data attributes.
501 | these data attributes are used to create custom animations. you can use these data
502 | attributes in your CSS to create custom animations.
503 |
506 | When you are using Textify in your project, make sure
507 | call the Textify's instance methods after the webpage's
508 | all fonts or
509 | font-family are loaded. otherwise, animation will broke
510 | the animation.
511 |
513 | We recommend to use
514 |
519 | fontfaceobserver
521 | to detect the fonts are loaded or not. and then call the
522 | Textify's instance methods. you can use
523 | Textify like this:
524 |
17 | These are some selected animations for your project. you can get code from here and use it
18 | in your project.
19 |
29 | These animations are large text animations for small & large paragraphs. you can get code
30 | from here and use it
31 |
27 | Textify.js have a powerful animations engine, which are use to create smooth, creative
28 | or seamless animations of typography.
29 |
34 | Textify.js provide multiple animations types or custom animations on GSAP's power.
35 |
40 | Textify.js is easy to use, you can create animations with just a few lines of code.
41 |
46 | Textify.js is no dependencies, it's only use
47 | GSAP for animations.
48 | Working on other features, stay tuned!What is Textify.js?
13 | Getting Started
23 | Basic Usage
32 |
46 | https://cdn.jsdelivr.net/npm/textify.js/dist/Textify.min.css
48 |
52 | https://cdn.jsdelivr.net/npm/textify.js/dist/Textify.min.js
54 |
58 | https://cdnjs.cloudflare.com/ajax/libs/gsap/3.6.1/gsap.min.js
60 | Installation
65 | npm install textify.js gsap --save
68 | import gsap from "gsap";
74 | import Textify from "textify.js";
75 |
76 | new Textify({}, gsap);
77 |
83 | https://cdn.jsdelivr.net/npm/textify.js/dist/Textify.min.js
84 |
85 |
86 | https://cdnjs.cloudflare.com/ajax/libs/gsap/3.6.1/gsap.min.js
87 |
88 | import gsap from "gsap";
95 | import Textify from "textify.js";
96 |
97 | new Textify({}, gsap);
98 | How to use Textify.js?
108 | new Textify({ }, gsap);
111 | <h1 data-textify>Hello World</h1>
118 | Config :
124 |
443 | Instance Methods
447 | Extra Tips
492 | Important Tip
505 |
527 | import Textify from "textify.js";
528 |
541 |
529 | import FontFaceObserver from "fontfaceobserver";
530 |
531 |
532 | const font = new FontFaceObserver("Roboto");
533 |
534 |
535 | font.load().then(() => {
536 |
537 | new Textify();
538 |
539 | });
540 | Thanks for using Textify
549 | More animations coming soon...
45 | Features
23 |
24 |
51 | Powerful animations engine
26 | Multiple animations types
33 | Easy to use
39 | No dependencies
45 | Let's build animations
57 |
";
33 | } else {
34 | result.push(node.outerHTML);
35 | }
36 | node = node.nextSibling;
37 | }
38 | return result;
39 | }
40 |
41 | export function emojiSafeSplit(text, delimiter, trim) {
42 | text += ""; // make sure it's cast as a string. Someone may pass in a number.
43 | if (trim) {
44 | text = text.replace(_trimExp, "");
45 | }
46 | if (delimiter && delimiter !== "") {
47 | return text.replace(/>/g, ">").replace(/= 0xd800 && character.charCodeAt(0) <= 0xdbff) ||
58 | (text.charCodeAt(i + 1) >= 0xfe00 && text.charCodeAt(i + 1) <= 0xfe0f)
59 | ) {
60 | //special emoji characters use 2 or 4 unicode characters that we must keep together.
61 | j = ((text.substr(i, 12).split(emojiExp) || [])[1] || "").length || 2;
62 | character = text.substr(i, j);
63 | result.emoji = 1;
64 | i += j - 1;
65 | }
66 | result.push(character === ">" ? ">" : character === "<" ? "<" : character);
67 | }
68 | return result;
69 | }
70 |
--------------------------------------------------------------------------------
/src/Utils/Config.js:
--------------------------------------------------------------------------------
1 | const Config = {
2 | splitType: "lines words chars", // chars or words or lines
3 | tag: "div", // span or div
4 | charsClass: "char", // class name for chars
5 | wordsClass: "word", // class name for words
6 | linesClass: "line", // class name for lines
7 | position: "relative", // absolute or relative
8 | largeText: false, // true or false
9 | observer: {
10 | repeat: false, // true or false
11 | threshold: 0.5 // 0.0 ~ 1.0
12 | },
13 | animation: {
14 | by: "chars", // chars or words or lines
15 | duration: 0.5, // seconds
16 | stagger: 0.05, // seconds
17 | delay: 0.0, // seconds
18 | ease: "ease", // ease or linear or cubic-bezier
19 | customAnimation: false, // true or false
20 | transformOrigin: "center center", // center center or top left or top center or top right or center right or bottom right or bottom center or bottom left or center left
21 | animateProps: {
22 | opacity: 1, // 0 ~ 1
23 | y: "100%", // -100 ~ 100 (%)
24 | x: 0, // -100 ~ 100 (%)
25 | scale: 1, // 0 ~ 1
26 | rotate: 0, // -360 ~ 360
27 | skewX: 0, // -360 ~ 360
28 | skewY: 0 // -360 ~ 360
29 | }
30 | }
31 | };
32 |
33 | export { Config };
34 |
--------------------------------------------------------------------------------
/src/Utils/Text.js:
--------------------------------------------------------------------------------
1 | import { emojiExp, getText } from "./Break.js";
2 |
3 | let _document,
4 | _window,
5 | _coreInitted,
6 | _stripExp = /(?:\r|\n|\t\t)/g,
7 | _multipleSpacesExp = /(?:\s\s+)/g,
8 | _initCore = () => {
9 | _document = document;
10 | _window = window;
11 | _coreInitted = 1;
12 | },
13 | _bonusValidated = 1,
14 | _getComputedStyle = (element) => _window.getComputedStyle(element),
15 | _isArray = Array.isArray,
16 | _slice = [].slice,
17 | _toArray = (value, leaveStrings) => {
18 | let type;
19 | return _isArray(value)
20 | ? value
21 | : (type = typeof value) === "string" && !leaveStrings && value
22 | ? _slice.call(_document.querySelectorAll(value), 0)
23 | : value && type === "object" && "length" in value
24 | ? _slice.call(value, 0)
25 | : value
26 | ? [value]
27 | : [];
28 | },
29 | _isAbsolute = (vars) => vars.position === "absolute" || vars.absolute === true,
30 | _findSpecialChars = (text, chars) => {
31 | let i = chars.length,
32 | s;
33 | while (--i > -1) {
34 | s = chars[i];
35 | if (text.substr(0, s.length) === s) {
36 | return s.length;
37 | }
38 | }
39 | },
40 | customStyle = " style='position:relative;display:inline-block;'",
41 | cssClass = (cssClass = "", tag) => {
42 | let iterate = ~cssClass.indexOf("++"),
43 | num = 1;
44 | if (iterate) {
45 | cssClass = cssClass.split("++").join("");
46 | }
47 | return () => "<" + tag + customStyle + (cssClass ? " class='" + cssClass + (iterate ? num++ : "") + "'>" : ">");
48 | },
49 | _swapText = (element, oldText, newText) => {
50 | let type = element.nodeType;
51 | if (type === 1 || type === 9 || type === 11) {
52 | for (element = element.firstChild; element; element = element.nextSibling) {
53 | _swapText(element, oldText, newText);
54 | }
55 | } else if (type === 3 || type === 4) {
56 | element.nodeValue = element.nodeValue.split(oldText).join(newText);
57 | }
58 | },
59 | _pushReversed = (a, merge) => {
60 | let i = merge.length;
61 | while (--i > -1) {
62 | a.push(merge[i]);
63 | }
64 | },
65 | _isBeforeWordDelimiter = (e, root, wordDelimiter) => {
66 | let next;
67 | while (e && e !== root) {
68 | next = e._next || e.nextSibling;
69 | if (next) {
70 | return next.textContent.charAt(0) === wordDelimiter;
71 | }
72 | e = e.parentNode || e._parent;
73 | }
74 | },
75 | _deWordify = (e) => {
76 | let children = _toArray(e.childNodes),
77 | l = children.length,
78 | i,
79 | child;
80 | for (i = 0; i < l; i++) {
81 | child = children[i];
82 | if (child._isSplit) {
83 | _deWordify(child);
84 | } else {
85 | if (i && child.previousSibling.nodeType === 3) {
86 | child.previousSibling.nodeValue += child.nodeType === 3 ? child.nodeValue : child.firstChild.nodeValue;
87 | } else if (child.nodeType !== 3) {
88 | e.insertBefore(child.firstChild, child);
89 | }
90 | e.removeChild(child);
91 | }
92 | }
93 | },
94 | _getStyleAsNumber = (name, computedStyle) => parseFloat(computedStyle[name]) || 0,
95 | _setPositionsAfterSplit = (element, vars, allChars, allWords, allLines, origWidth, origHeight) => {
96 | let cs = _getComputedStyle(element),
97 | paddingLeft = _getStyleAsNumber("paddingLeft", cs),
98 | lineOffsetY = -999,
99 | borderTopAndBottom = _getStyleAsNumber("borderBottomWidth", cs) + _getStyleAsNumber("borderTopWidth", cs),
100 | borderLeftAndRight = _getStyleAsNumber("borderLeftWidth", cs) + _getStyleAsNumber("borderRightWidth", cs),
101 | padTopAndBottom = _getStyleAsNumber("paddingTop", cs) + _getStyleAsNumber("paddingBottom", cs),
102 | padLeftAndRight = _getStyleAsNumber("paddingLeft", cs) + _getStyleAsNumber("paddingRight", cs),
103 | lineThreshold = _getStyleAsNumber("fontSize", cs) * 0.2,
104 | textAlign = cs.textAlign,
105 | charArray = [],
106 | wordArray = [],
107 | lineArray = [],
108 | wordDelimiter = vars.wordDelimiter || " ",
109 | tag = vars.tag ? vars.tag : vars.span ? "span" : "div",
110 | types = vars.type || vars.split || "chars,words,lines",
111 | lines = allLines && ~types.indexOf("lines") ? [] : null,
112 | words = ~types.indexOf("words"),
113 | chars = ~types.indexOf("chars"),
114 | absolute = _isAbsolute(vars),
115 | linesClass = vars.linesClass,
116 | iterateLine = ~(linesClass || "").indexOf("++"),
117 | spaceNodesToRemove = [],
118 | i,
119 | j,
120 | l,
121 | node,
122 | nodes,
123 | isChild,
124 | curLine,
125 | addWordSpaces,
126 | style,
127 | lineNode,
128 | lineWidth,
129 | offset;
130 | if (iterateLine) {
131 | linesClass = linesClass.split("++").join("");
132 | }
133 |
134 | j = element.getElementsByTagName("*");
135 | l = j.length;
136 | nodes = [];
137 | for (i = 0; i < l; i++) {
138 | nodes[i] = j[i];
139 | }
140 |
141 | if (lines || absolute) {
142 | for (i = 0; i < l; i++) {
143 | node = nodes[i];
144 | isChild = node.parentNode === element;
145 | if (isChild || absolute || (chars && !words)) {
146 | offset = node.offsetTop;
147 | if (lines && isChild && Math.abs(offset - lineOffsetY) > lineThreshold && (node.nodeName !== "BR" || i === 0)) {
148 | curLine = [];
149 | lines.push(curLine);
150 | lineOffsetY = offset;
151 | }
152 | if (absolute) {
153 | node._x = node.offsetLeft;
154 | node._y = offset;
155 | node._w = node.offsetWidth;
156 | node._h = node.offsetHeight;
157 | }
158 | if (lines) {
159 | if (
160 | (node._isSplit && isChild) ||
161 | (!chars && isChild) ||
162 | (words && isChild) ||
163 | (!words && node.parentNode.parentNode === element && !node.parentNode._isSplit)
164 | ) {
165 | curLine.push(node);
166 | node._x -= paddingLeft;
167 | if (_isBeforeWordDelimiter(node, element, wordDelimiter)) {
168 | node._wordEnd = true;
169 | }
170 | }
171 | if (node.nodeName === "BR" && ((node.nextSibling && node.nextSibling.nodeName === "BR") || i === 0)) {
172 | lines.push([]);
173 | }
174 | }
175 | }
176 | }
177 | }
178 |
179 | for (i = 0; i < l; i++) {
180 | node = nodes[i];
181 | isChild = node.parentNode === element;
182 | if (node.nodeName === "BR") {
183 | if (lines || absolute) {
184 | if (node.parentNode) {
185 | node.parentNode.removeChild(node);
186 | }
187 | nodes.splice(i--, 1);
188 | l--;
189 | } else if (!words) {
190 | element.appendChild(node);
191 | }
192 | continue;
193 | }
194 |
195 | if (absolute) {
196 | style = node.style;
197 | if (!words && !isChild) {
198 | node._x += node.parentNode._x;
199 | node._y += node.parentNode._y;
200 | }
201 | style.left = node._x + "px";
202 | style.top = node._y + "px";
203 | style.position = "absolute";
204 | style.display = "block";
205 | style.width = node._w + 1 + "px";
206 | style.height = node._h + "px";
207 | }
208 |
209 | if (!words && chars) {
210 | if (node._isSplit) {
211 | node._next = node.nextSibling;
212 | node.parentNode.appendChild(node);
213 | } else if (node.parentNode._isSplit) {
214 | node._parent = node.parentNode;
215 | if (!node.previousSibling && node.firstChild) {
216 | node.firstChild._isFirst = true;
217 | }
218 | if (node.nextSibling && node.nextSibling.textContent === " " && !node.nextSibling.nextSibling) {
219 | spaceNodesToRemove.push(node.nextSibling);
220 | }
221 | node._next = node.nextSibling && node.nextSibling._isFirst ? null : node.nextSibling;
222 | node.parentNode.removeChild(node);
223 | nodes.splice(i--, 1);
224 | l--;
225 | } else if (!isChild) {
226 | offset = !node.nextSibling && _isBeforeWordDelimiter(node.parentNode, element, wordDelimiter);
227 | if (node.parentNode._parent) {
228 | node.parentNode._parent.appendChild(node);
229 | }
230 | if (offset) {
231 | node.parentNode.appendChild(_document.createTextNode(" "));
232 | }
233 | if (tag === "span") {
234 | node.style.display = "inline";
235 | }
236 | charArray.push(node);
237 | }
238 | } else if (node.parentNode._isSplit && !node._isSplit && node.innerHTML !== "") {
239 | wordArray.push(node);
240 | } else if (chars && !node._isSplit) {
241 | if (tag === "span") {
242 | node.style.display = "inline";
243 | }
244 | charArray.push(node);
245 | }
246 | }
247 |
248 | i = spaceNodesToRemove.length;
249 | while (--i > -1) {
250 | spaceNodesToRemove[i].parentNode.removeChild(spaceNodesToRemove[i]);
251 | }
252 |
253 | if (lines) {
254 | if (absolute) {
255 | lineNode = _document.createElement(tag);
256 | element.appendChild(lineNode);
257 | lineWidth = lineNode.offsetWidth + "px";
258 | offset = lineNode.offsetParent === element ? 0 : element.offsetLeft;
259 | element.removeChild(lineNode);
260 | }
261 | style = element.style.cssText;
262 | element.style.cssText = "display:none;";
263 | while (element.firstChild) {
264 | element.removeChild(element.firstChild);
265 | }
266 | addWordSpaces = wordDelimiter === " " && (!absolute || (!words && !chars));
267 | for (i = 0; i < lines.length; i++) {
268 | curLine = lines[i];
269 | lineNode = _document.createElement(tag);
270 | lineNode.style.cssText = "display:block;text-align:" + textAlign + ";position:" + (absolute ? "absolute;" : "relative;");
271 | if (linesClass) {
272 | lineNode.className = linesClass + (iterateLine ? i + 1 : "");
273 | }
274 | lineArray.push(lineNode);
275 | l = curLine.length;
276 | for (j = 0; j < l; j++) {
277 | if (curLine[j].nodeName !== "BR") {
278 | node = curLine[j];
279 | lineNode.appendChild(node);
280 | if (addWordSpaces && node._wordEnd) {
281 | lineNode.appendChild(_document.createTextNode(" "));
282 | }
283 | if (absolute) {
284 | if (j === 0) {
285 | lineNode.style.top = node._y + "px";
286 | lineNode.style.left = paddingLeft + offset + "px";
287 | }
288 | node.style.top = "0px";
289 | if (offset) {
290 | node.style.left = node._x - offset + "px";
291 | }
292 | }
293 | }
294 | }
295 | if (l === 0) {
296 | lineNode.innerHTML = " ";
297 | } else if (!words && !chars) {
298 | _deWordify(lineNode);
299 | _swapText(lineNode, String.fromCharCode(160), " ");
300 | }
301 | if (absolute) {
302 | lineNode.style.width = lineWidth;
303 | lineNode.style.height = node._h + "px";
304 | }
305 | element.appendChild(lineNode);
306 | }
307 | element.style.cssText = style;
308 | }
309 |
310 | if (absolute) {
311 | if (origHeight > element.clientHeight) {
312 | element.style.height = origHeight - padTopAndBottom + "px";
313 | if (element.clientHeight < origHeight) {
314 | element.style.height = origHeight + borderTopAndBottom + "px";
315 | }
316 | }
317 | if (origWidth > element.clientWidth) {
318 | element.style.width = origWidth - padLeftAndRight + "px";
319 | if (element.clientWidth < origWidth) {
320 | element.style.width = origWidth + borderLeftAndRight + "px";
321 | }
322 | }
323 | }
324 | _pushReversed(allChars, charArray);
325 | if (words) {
326 | _pushReversed(allWords, wordArray);
327 | }
328 | _pushReversed(allLines, lineArray);
329 | },
330 | _splitRawText = (element, vars, wordStart, charStart) => {
331 | let tag = vars.tag ? vars.tag : vars.span ? "span" : "div",
332 | types = vars.type || vars.split || "chars,words,lines",
333 | chars = ~types.indexOf("chars"),
334 | absolute = _isAbsolute(vars),
335 | wordDelimiter = vars.wordDelimiter || " ",
336 | space = wordDelimiter !== " " ? "" : absolute ? " " : " ",
337 | wordEnd = "" + tag + ">",
338 | wordIsOpen = 1,
339 | specialChars = vars.specialChars ? (typeof vars.specialChars === "function" ? vars.specialChars : _findSpecialChars) : null,
340 | text,
341 | splitText,
342 | i,
343 | j,
344 | l,
345 | character,
346 | hasTagStart,
347 | testResult,
348 | container = _document.createElement("div"),
349 | parent = element.parentNode;
350 |
351 | parent.insertBefore(container, element);
352 | container.textContent = element.nodeValue;
353 | parent.removeChild(element);
354 | element = container;
355 | text = getText(element);
356 | hasTagStart = text.indexOf("<") !== -1;
357 |
358 | if (vars.reduceWhiteSpace !== false) {
359 | text = text.replace(_multipleSpacesExp, " ").replace(_stripExp, "");
360 | }
361 | if (hasTagStart) {
362 | text = text.split("<").join("{{LT}}");
363 | }
364 | l = text.length;
365 | splitText = (text.charAt(0) === " " ? space : "") + wordStart();
366 | for (i = 0; i < l; i++) {
367 | character = text.charAt(i);
368 | if (specialChars && (testResult = specialChars(text.substr(i), vars.specialChars))) {
369 | character = text.substr(i, testResult || 1);
370 | splitText += chars && character !== " " ? charStart() + character + "" + tag + ">" : character;
371 | i += testResult - 1;
372 | } else if (character === wordDelimiter && text.charAt(i - 1) !== wordDelimiter && i) {
373 | splitText += wordIsOpen ? wordEnd : "";
374 | wordIsOpen = 0;
375 | while (text.charAt(i + 1) === wordDelimiter) {
376 | splitText += space;
377 | i++;
378 | }
379 | if (i === l - 1) {
380 | splitText += space;
381 | } else if (text.charAt(i + 1) !== ")") {
382 | splitText += space + wordStart();
383 | wordIsOpen = 1;
384 | }
385 | } else if (character === "{" && text.substr(i, 6) === "{{LT}}") {
386 | splitText += chars ? charStart() + "{{LT}}" + "" + tag + ">" : "{{LT}}";
387 | i += 5;
388 | } else if (
389 | (character.charCodeAt(0) >= 0xd800 && character.charCodeAt(0) <= 0xdbff) ||
390 | (text.charCodeAt(i + 1) >= 0xfe00 && text.charCodeAt(i + 1) <= 0xfe0f)
391 | ) {
392 | j = ((text.substr(i, 12).split(emojiExp) || [])[1] || "").length || 2;
393 | splitText += chars && character !== " " ? charStart() + text.substr(i, j) + "" + tag + ">" : text.substr(i, j);
394 | i += j - 1;
395 | } else {
396 | splitText += chars && character !== " " ? charStart() + character + "" + tag + ">" : character;
397 | }
398 | }
399 | element.outerHTML = splitText + (wordIsOpen ? wordEnd : "");
400 | if (hasTagStart) {
401 | _swapText(parent, "{{LT}}", "<");
402 | }
403 | },
404 | _split = (element, vars, wordStart, charStart) => {
405 | let children = _toArray(element.childNodes),
406 | l = children.length,
407 | absolute = _isAbsolute(vars),
408 | i,
409 | child;
410 | if (element.nodeType !== 3 || l > 1) {
411 | vars.absolute = false;
412 | for (i = 0; i < l; i++) {
413 | child = children[i];
414 | if (child.nodeType !== 3 || /\S+/.test(child.nodeValue)) {
415 | if (absolute && child.nodeType !== 3 && _getComputedStyle(child).display === "inline") {
416 | child.style.display = "inline-block";
417 | child.style.position = "relative";
418 | }
419 | child._isSplit = true;
420 | _split(child, vars, wordStart, charStart);
421 | }
422 | }
423 | vars.absolute = absolute;
424 | element._isSplit = true;
425 | return;
426 | }
427 | _splitRawText(element, vars, wordStart, charStart);
428 | };
429 |
430 | export class Splitter {
431 | constructor(element, vars) {
432 | if (!_coreInitted) {
433 | _initCore();
434 | }
435 | this.elements = _toArray(element);
436 | this.chars = [];
437 | this.words = [];
438 | this.lines = [];
439 | this._originals = [];
440 | this.vars = vars || {};
441 | if (_bonusValidated) {
442 | this._split(vars);
443 | }
444 | }
445 |
446 | _split(vars) {
447 | if (this.isSplit) {
448 | this.revert();
449 | }
450 | this.vars = vars = vars || this.vars;
451 | this._originals.length = this.chars.length = this.words.length = this.lines.length = 0;
452 | let i = this.elements.length,
453 | tag = vars.tag ? vars.tag : vars.span ? "span" : "div",
454 | wordStart = cssClass(vars.wordsClass, tag),
455 | charStart = cssClass(vars.charsClass, tag),
456 | origHeight,
457 | origWidth,
458 | e;
459 | while (--i > -1) {
460 | e = this.elements[i];
461 | this._originals[i] = e.innerHTML;
462 | origHeight = e.clientHeight;
463 | origWidth = e.clientWidth;
464 | _split(e, vars, wordStart, charStart);
465 | _setPositionsAfterSplit(e, vars, this.chars, this.words, this.lines, origWidth, origHeight);
466 | }
467 | this.chars.reverse();
468 | this.words.reverse();
469 | this.lines.reverse();
470 | this.isSplit = true;
471 | return this;
472 | }
473 |
474 | revert() {
475 | let originals = this._originals;
476 | if (!originals) {
477 | throw "revert() call wasn't scoped properly.";
478 | }
479 | this.elements.forEach((e, i) => (e.innerHTML = originals[i]));
480 | this.chars = [];
481 | this.words = [];
482 | this.lines = [];
483 | this.isSplit = false;
484 | return this;
485 | }
486 |
487 | static create(element, vars) {
488 | return new Splitter(element, vars);
489 | }
490 | }
491 |
492 | export { Splitter as default };
493 |
--------------------------------------------------------------------------------
/src/Utils/dom.js:
--------------------------------------------------------------------------------
1 | export function mapEach(element, callback) {
2 | if (element instanceof window.HTMLElement) return [callback(element)];
3 | if (typeof element === "object") {
4 | return Object.keys(element).map((key) => callback(element[key]));
5 | } else {
6 | return element.map(callback);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/Utils/isBrowser.js:
--------------------------------------------------------------------------------
1 | const isBrowser =
2 | typeof window !== "undefined" &&
3 | typeof document !== "undefined" &&
4 | typeof navigator !== "undefined" &&
5 | typeof requestAnimationFrame !== "undefined";
6 |
7 | export { isBrowser };
8 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-prototype-builtins */
2 | import Texts from "./types/Texts";
3 | import { Config } from "./utils/Config";
4 | import { isBrowser } from "./utils/isBrowser";
5 | import { mapEach } from "./utils/Dom";
6 |
7 | // Deep merge two objects.
8 | // @param target Object - The target object.
9 | // @param source Object - The source object.
10 | // @returns Object - The merged object.
11 | function deepMerge(target, source) {
12 | if (typeof target !== "object" || typeof source !== "object") {
13 | return source;
14 | }
15 | const merged = { ...target };
16 | for (const key in source) {
17 | if (source.hasOwnProperty(key)) {
18 | if (merged.hasOwnProperty(key)) {
19 | merged[key] = deepMerge(merged[key], source[key]);
20 | } else {
21 | merged[key] = source[key];
22 | }
23 | }
24 | }
25 | return merged;
26 | }
27 |
28 | // Textify
29 | // @param options Object - The options object.
30 | // @returns Object - The Textify instance.
31 | class Textify {
32 | /**
33 | * @constructor
34 | * @param {Object} options
35 | * @param {Object} engine
36 | * **/
37 | constructor(options, engine) {
38 | this.DEFAULT_ELEMENT = options.el || "[data-textify]";
39 | this.options = options;
40 | this.config = Config;
41 | this.engine = engine;
42 |
43 | // if engine is not defined, throw an error
44 | if (!this.engine) {
45 | throw new Error("engine is not defined. Please import a gsap. See https://greensock.com/docs/v3/Installation for more info.");
46 | }
47 |
48 | // Merge the options with the default config.
49 | this.controls = deepMerge(this.config, this.options);
50 |
51 | // Check if the browser is supported.
52 | if (isBrowser) {
53 | if (!document.querySelector(this.DEFAULT_ELEMENT)) {
54 | throw new Error(`Textify: Element "${this.DEFAULT_ELEMENT}" is not found.`);
55 | }
56 | this.TARGETS = [...document.querySelectorAll(this.DEFAULT_ELEMENT)];
57 | this.ANIMATIONS = mapEach(this.TARGETS, (element) => {
58 | return new Texts({
59 | element: element,
60 | controls: this.controls,
61 | engine: this.engine
62 | });
63 | });
64 | }
65 | }
66 |
67 | animateIn() {
68 | !isBrowser && console.log("Textify is not supported in this environment.");
69 | this.ANIMATIONS.forEach((animation) => {
70 | animation.animateIn();
71 | });
72 | }
73 |
74 | animateOut() {
75 | !isBrowser && console.log("Textify is not supported in this environment.");
76 | this.ANIMATIONS.forEach((animation) => {
77 | animation.animateOut();
78 | });
79 | }
80 |
81 | reset() {
82 | !isBrowser && console.log("Textify is not supported in this environment.");
83 | this.ANIMATIONS.forEach((animation) => {
84 | animation.reset();
85 | });
86 | }
87 | }
88 |
89 | export default Textify;
90 |
91 | // make Textify global
92 | if (isBrowser) {
93 | window.Textify = Textify;
94 | } else {
95 | global.Textify = Textify;
96 | console.log("Textify is not supported in this environment.");
97 | }
98 |
--------------------------------------------------------------------------------
/src/types/Texts.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 | import Splitter from "../utils/Text";
3 | import { isBrowser } from "../utils/isBrowser";
4 |
5 | // Texts class
6 | // this class is responsible for splitting the text and animating it
7 | // it also creates an observer to animate the text when it is in the viewport
8 | // if the browser does not support IntersectionObserver, the text will be animated immediately
9 | // @param {element} element - the element to animate
10 | // @param {controls} controls - the controls object
11 | export default class Texts {
12 | /**
13 | * @constructor
14 | * @param {element} element - the element to animate
15 | * @param {controls} controls - the controls object
16 | * @param {engine} engine - the engine object
17 | * **/
18 | constructor({ element, controls, engine }) {
19 | this.element = element;
20 | this.controls = controls;
21 | this.engine = engine;
22 |
23 | this.observer = null;
24 | this.text = null;
25 | this.textParts = {};
26 | this.animatedElements = [];
27 |
28 | this.isError = false;
29 |
30 | this.threshold = this.controls.observer.threshold;
31 | this.repeat = this.controls.observer.repeat;
32 | this.animation = this.controls.animation;
33 | this.customAnimation = this.controls.animation.customAnimation;
34 |
35 | // _split
36 | this._split();
37 |
38 | // createObserver
39 | if (isBrowser && "IntersectionObserver" in window) {
40 | this.createObserver();
41 | this.customAnimation ? null : this.reset();
42 | } else {
43 | this.customAnimation ? null : this.animateIn();
44 | }
45 | }
46 |
47 | _split() {
48 | // if largeText is true, split the text by lines and throw an error
49 | if (this.controls.largeText) {
50 | if (this.controls.splitType !== "lines") {
51 | this.isError = true;
52 | throw Error("Textify: Large text must be split by lines only. Please set splitType to 'lines'");
53 | }
54 |
55 | if (this.animation.by !== "lines") {
56 | this.isError = true;
57 | throw Error("Textify: Large text must be animated by lines only. Please set animation.by to 'lines'");
58 | }
59 | }
60 |
61 | this.text = new Splitter(this.element, {
62 | type: this.controls.splitType,
63 | charsClass: this.controls.charsClass,
64 | wordsClass: this.controls.wordsClass,
65 | linesClass: this.controls.linesClass,
66 | position: this.controls.position,
67 | tag: this.controls.tag
68 | });
69 | this.textParts = {
70 | chars: this.text.chars,
71 | words: this.text.words,
72 | lines: this.text.lines
73 | };
74 |
75 | if (this.animation.by === "chars" && this.textParts.chars.length > 0) {
76 | this.animatedElements = this.textParts.chars;
77 | this.animatedElements.forEach((el, index) => {
78 | el.setAttribute("data-char", el.textContent);
79 | el.setAttribute("data-char-index", index);
80 | });
81 | }
82 | if (this.animation.by === "words" && this.textParts.words.length > 0) {
83 | this.animatedElements = this.textParts.words;
84 | this.animatedElements.forEach((el, index) => {
85 | el.setAttribute("data-word", el.textContent);
86 | el.setAttribute("data-word-index", index);
87 | });
88 | }
89 | if (this.animation.by === "lines" && this.textParts.lines.length > 0) {
90 | this.animatedElements = this.textParts.lines;
91 | this.animatedElements.forEach((el, index) => {
92 | el.setAttribute("data-line", el.textContent);
93 | el.setAttribute("data-line-index", index);
94 | });
95 | }
96 |
97 | if (this.controls.largeText) {
98 | if (this.textParts.lines.length === 0) {
99 | this.isError = true;
100 | throw Error("Textify: Large text must have at least one line");
101 | }
102 |
103 | this.textParts.lines.forEach((line) => {
104 | const div = document.createElement("div");
105 | div.appendChild(line);
106 | this.element.appendChild(div);
107 | div.classList.add("line-box");
108 | });
109 | }
110 |
111 | this.element.classList.add("textify");
112 | }
113 |
114 | createObserver() {
115 | this.observer = new window.IntersectionObserver((entries) => {
116 | entries.forEach((entry) => {
117 | if (entry.isIntersecting) {
118 | entry.target.classList.add("is-animated");
119 | this.customAnimation ? null : this.animateIn();
120 | this.repeat ? null : this.observer.unobserve(entry.target);
121 | } else {
122 | this.customAnimation ? null : this.reset();
123 | this.repeat ? entry.target.classList.remove("is-animated") : null;
124 | }
125 | });
126 | });
127 | this.observer.observe(this.element);
128 | }
129 |
130 | animateIn() {
131 | if (this.isError) return;
132 | if (this.animation.customAnimation) {
133 | return this.element.classList.add("textify-custom-animation");
134 | }
135 |
136 | this.engine.to(this.animatedElements, {
137 | opacity: 1,
138 | y: 0,
139 | x: 0,
140 | scale: 1,
141 | rotate: 0,
142 | skewX: 0,
143 | skewY: 0,
144 | duration: this.animation.duration,
145 | stagger: this.animation.stagger,
146 | delay: this.animation.delay,
147 | ease: this.animation.ease
148 | });
149 | }
150 |
151 | animateOut() {
152 | if (this.isError) return;
153 | if (this.animation.customAnimation) {
154 | return this.element.classList.remove("textify-custom-animation");
155 | }
156 |
157 | this.engine.to(this.animatedElements, {
158 | duration: this.animation.duration,
159 | stagger: this.animation.stagger,
160 | transformOrigin: this.animation.transformOrigin,
161 | opacity: this.animation.animateProps.opacity,
162 | y: this.animation.animateProps.y,
163 | x: this.animation.animateProps.x,
164 | scale: this.animation.animateProps.scale,
165 | rotate: this.animation.animateProps.rotate,
166 | skewX: this.animation.animateProps.skewX,
167 | skewY: this.animation.animateProps.skewY,
168 | ease: this.animation.ease
169 | });
170 | }
171 |
172 | reset() {
173 | if (this.isError) return;
174 | if (this.animation.customAnimation) {
175 | return this.element.classList.remove("textify-custom-animation");
176 | }
177 |
178 | this.engine.set(this.animatedElements, {
179 | duration: 0.1, // for better performance and to avoid flickering
180 | stagger: 0, // for better performance and to avoid flickering
181 | transformOrigin: this.animation.transformOrigin,
182 | opacity: this.animation.animateProps.opacity,
183 | y: this.animation.animateProps.y,
184 | x: this.animation.animateProps.x,
185 | scale: this.animation.animateProps.scale,
186 | rotate: this.animation.animateProps.rotate,
187 | skewX: this.animation.animateProps.skewX,
188 | skewY: this.animation.animateProps.skewY,
189 | ease: "none" // for better performance and to avoid flickering
190 | });
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/style/Textify.css:
--------------------------------------------------------------------------------
1 | /* fix safari custom kerning by adding a space after each character */
2 | * {
3 | -webkit-font-feature-settings: "kern" 1;
4 | -moz-font-feature-settings: "kern" 1;
5 | -ms-font-feature-settings: "kern" 1;
6 | -o-font-feature-settings: "kern" 1;
7 | font-feature-settings: "kern" 1;
8 | font-kerning: none;
9 | -webkit-text-rendering: optimizeSpeed;
10 | text-rendering: optimizeSpeed;
11 | -webkit-transform: translateZ(0);
12 | transform: translateZ(0);
13 | }
14 |
15 | /* ----------------------------------------- */
16 | .textify .word,
17 | .textify .char {
18 | display: inline-block;
19 | will-change: transform, opacity; /* for safari */
20 | }
21 |
22 | /* ----------------------------------------- */
23 |
24 | .textify .char {
25 | position: relative;
26 | }
27 |
28 | /**
29 | * Populate the psuedo elements with the character to allow for expanded effects
30 | * Set to `display: none` by default; just add `display: block` when you want
31 | * to use the psuedo elements
32 | */
33 | .textify .char::before,
34 | .textify .char::after,
35 | .textify .word::after,
36 | .textify .word::before,
37 | .textify .line::after,
38 | .textify .line::before {
39 | position: absolute;
40 | top: 0;
41 | left: 0;
42 | visibility: hidden;
43 | transition: inherit;
44 | display: none;
45 | }
46 |
47 | .textify .char::before,
48 | .textify .char::after {
49 | content: attr(data-char);
50 | }
51 |
52 | .textify .word::after,
53 | .textify .word::before {
54 | content: attr(data-word);
55 | }
56 |
57 | .textify .line::after,
58 | .textify .line::before {
59 | content: attr(data-line);
60 | }
61 |
62 | /* ------------------------------------------- */
63 |
64 | .textify .line-box,
65 | .textify .line {
66 | overflow: hidden;
67 | }
68 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const copyWebpackPlugin = require("copy-webpack-plugin");
3 | const TerserPlugin = require("terser-webpack-plugin");
4 |
5 | module.exports = {
6 | mode: "production",
7 | entry: "./src/index.js",
8 | output: {
9 | path: path.resolve(__dirname, "dist"),
10 | filename: "Textify.min.js",
11 | clean: true,
12 | library: {
13 | name: "Textify.js",
14 | type: "umd",
15 | export: "default"
16 | },
17 | globalObject: "this",
18 | environment: {
19 | arrowFunction: false
20 | }
21 | },
22 | module: {
23 | rules: [
24 | {
25 | test: /\.js$/,
26 | exclude: /node_modules/,
27 | use: {
28 | loader: "babel-loader",
29 | options: {
30 | presets: ["@babel/preset-env"],
31 | plugins: ["@babel/plugin-transform-runtime"]
32 | }
33 | }
34 | },
35 | {
36 | test: /\.css$/i,
37 | use: ["style-loader", "css-loader"]
38 | }
39 | ]
40 | },
41 | plugins: [
42 | new copyWebpackPlugin({
43 | patterns: [{ from: "./style/Textify.css", to: "Textify.min.css" }]
44 | })
45 | ],
46 | optimization: {
47 | minimize: true,
48 | minimizer: [
49 | new TerserPlugin({
50 | test: /\.js(\?.*)?$/i,
51 | extractComments: "all",
52 | minify: TerserPlugin.uglifyJsMinify
53 | })
54 | ]
55 | }
56 | };
57 |
--------------------------------------------------------------------------------