├── .gitignore ├── src ├── vite-env.d.ts ├── react-app-env.d.ts ├── examples │ ├── without-hook.tsx │ ├── custom-characters.tsx │ ├── examples.tsx │ ├── initial-value.tsx │ └── loop.tsx ├── dencrypt.test.ts ├── index.ts ├── index.test.ts └── dencrypt.ts ├── docs ├── dencrypt.gif └── example1.gif ├── .editorconfig ├── tsconfig.node.json ├── index.html ├── tsconfig.json ├── vite.config.ts ├── rollup.config.js ├── LICENSE ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /docs/dencrypt.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crazko/use-dencrypt-effect/HEAD/docs/dencrypt.gif -------------------------------------------------------------------------------- /docs/example1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crazko/use-dencrypt-effect/HEAD/docs/example1.gif -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true, 7 | "resolveJsonModule": true 8 | }, 9 | "include": ["vite.config.ts", "*.json"] 10 | } 11 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Examples 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { resolve } from "node:path"; 4 | import { defineConfig } from "vite"; 5 | import react from "@vitejs/plugin-react"; 6 | import dts from "vite-plugin-dts"; 7 | 8 | import packageJson from "./package.json"; 9 | 10 | export default defineConfig({ 11 | plugins: [react(), dts()], 12 | 13 | build: { 14 | lib: { 15 | entry: resolve("src", "index.ts"), 16 | name: packageJson.name, 17 | fileName: `index`, 18 | }, 19 | rollupOptions: { 20 | external: [...Object.keys(packageJson.peerDependencies)], 21 | }, 22 | }, 23 | 24 | test: { 25 | environment: "jsdom", 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /src/examples/without-hook.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { dencrypt } from ".."; 3 | 4 | export const WithoutHook = () => { 5 | const element = React.useRef(null); 6 | 7 | const setValue = dencrypt({ 8 | callback: (value) => { 9 | element.current!.textContent = value; 10 | }, 11 | }); 12 | 13 | return ( 14 | <> 15 |

16 | Even though this example is using React, dencrypt(){" "} 17 | function can be used without it. 18 |

19 |

20 | :{" "} 21 | value 22 |

23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/examples/custom-characters.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useDencrypt } from ".."; 3 | 4 | const value = "lorem ipsum"; 5 | 6 | export const CustomCharacters = () => { 7 | const [result, setResult] = useDencrypt(value, { chars: "\\/" }); 8 | 9 | React.useEffect(() => { 10 | let run = true; 11 | 12 | const loop = async () => { 13 | while (run) { 14 | await new Promise((resolve) => setTimeout(resolve, 500)); 15 | await setResult(value); 16 | } 17 | }; 18 | 19 | loop(); 20 | 21 | return () => { 22 | run = false; 23 | }; 24 | }, [setResult]); 25 | 26 | return ( 27 |
{result}
28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /src/examples/examples.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | 4 | import { InitialValue } from "./initial-value"; 5 | import { Loop } from "./loop"; 6 | import { WithoutHook } from "./without-hook"; 7 | import { CustomCharacters } from "./custom-characters"; 8 | 9 | const App = () => ( 10 |
11 |

Examples

12 |

Initial Value

13 | 14 | 15 |

Loop Through Values

16 | 17 | 18 |

Without Hook

19 | 20 | 21 |

Custom Characters

22 | 23 |
24 | ); 25 | 26 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 27 | 28 | 29 | 30 | ); 31 | -------------------------------------------------------------------------------- /src/examples/initial-value.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useDencrypt } from ".."; 3 | 4 | type LinkProps = { 5 | children: string; 6 | }; 7 | 8 | const Link = React.memo(({ children }: LinkProps) => { 9 | const [value, setValue] = useDencrypt(children); 10 | 11 | return ( 12 | setValue(children)}> 13 | {value} 14 | 15 | ); 16 | }); 17 | 18 | export const InitialValue = () => { 19 | return ( 20 |
    21 |
  • 22 | Home 23 |
  • 24 |
  • 25 | About 26 |
  • 27 |
  • 28 | Blog 29 |
  • 30 |
  • 31 | Projects 32 |
  • 33 |
  • 34 | Contact 35 |
  • 36 |
37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2' 2 | import commonjs from 'rollup-plugin-commonjs' 3 | import external from 'rollup-plugin-peer-deps-external' 4 | import resolve from 'rollup-plugin-node-resolve' 5 | import url from 'rollup-plugin-url' 6 | 7 | import pkg from './package.json' 8 | 9 | export default { 10 | input: 'src/index.tsx', 11 | output: [ 12 | { 13 | file: pkg.main, 14 | format: 'cjs', 15 | exports: 'named', 16 | sourcemap: true 17 | }, 18 | { 19 | file: pkg.module, 20 | format: 'es', 21 | exports: 'named', 22 | sourcemap: true 23 | } 24 | ], 25 | plugins: [ 26 | external(), 27 | url({ exclude: ['**/*.svg'] }), 28 | resolve(), 29 | typescript({ 30 | rollupCommonJSResolveHack: true, 31 | clean: true 32 | }), 33 | commonjs() 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /src/examples/loop.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useDencrypt } from ".."; 3 | 4 | const values = ["useDencrypt", "Customizable", "React Hook", "Text Effect", ""]; 5 | 6 | export const Loop = () => { 7 | const [result, setResult] = useDencrypt(); 8 | 9 | React.useEffect(() => { 10 | let i = 0; 11 | let run = true; 12 | 13 | const loop = async () => { 14 | while (run) { 15 | await new Promise((resolve) => setTimeout(resolve, 1000)); 16 | await setResult(values[i]); 17 | 18 | i = i === values.length - 1 ? 0 : i + 1; 19 | } 20 | }; 21 | 22 | loop(); 23 | 24 | return () => { 25 | run = false; 26 | }; 27 | }, [setResult]); 28 | 29 | return ( 30 |
33 | {result} 34 |
35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Roman Veselý 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "use-dencrypt-effect", 3 | "version": "2.0.0", 4 | "type": "module", 5 | "description": "A custom React hook generating crypting text effect.", 6 | "author": "crazko", 7 | "license": "MIT", 8 | "repository": "crazko/use-dencrypt-effect", 9 | "main": "dist/index.umd.cjs", 10 | "module": "dist/index.js", 11 | "types": "dist/index.d.ts", 12 | "files": [ 13 | "dist" 14 | ], 15 | "exports": { 16 | ".": { 17 | "import": "./dist/index.js", 18 | "require": "./dist/index.umd.cjs" 19 | } 20 | }, 21 | "engines": { 22 | "node": ">=8", 23 | "npm": ">=5" 24 | }, 25 | "scripts": { 26 | "test": "vitest", 27 | "build": "tsc && vite build", 28 | "start": "vite", 29 | "prepare": "npm run build" 30 | }, 31 | "peerDependencies": { 32 | "react": "^16.8 || >= 17.x" 33 | }, 34 | "devDependencies": { 35 | "@testing-library/dom": "^9.0.1", 36 | "@testing-library/react": "^14.0.0", 37 | "@types/react": "^18.0.27", 38 | "@types/react-dom": "^18.0.10", 39 | "@vitejs/plugin-react": "^3.1.0", 40 | "jsdom": "^21.1.0", 41 | "react": "^18.2.0", 42 | "react-dom": "^18.2.0", 43 | "typescript": "^4.9.3", 44 | "vite": "^4.1.0", 45 | "vite-plugin-dts": "^2.1.0", 46 | "vitest": "^0.29.2" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/dencrypt.test.ts: -------------------------------------------------------------------------------- 1 | import { waitFor } from "@testing-library/dom"; 2 | import { expect, test } from "vitest"; 3 | 4 | import { dencrypt } from "./"; 5 | 6 | test("accepts initial value", () => { 7 | const initialValue = "value"; 8 | let result = ""; 9 | 10 | dencrypt({ 11 | initialValue, 12 | callback: (value) => { 13 | result = value; 14 | }, 15 | }); 16 | 17 | expect(result).toBe(initialValue); 18 | }); 19 | 20 | test("changes value with text effect", async () => { 21 | let result = ""; 22 | 23 | const setValue = dencrypt({ 24 | callback: (value) => { 25 | result = value; 26 | }, 27 | chars: ".", 28 | }); 29 | 30 | expect(result).toBe(""); 31 | 32 | setValue("foo"); 33 | 34 | await waitFor(() => expect(result).toBe(".")); 35 | await waitFor(() => expect(result).toBe("..")); 36 | await waitFor(() => expect(result).toBe("...")); 37 | await waitFor(() => expect(result).toBe("f..")); 38 | await waitFor(() => expect(result).toBe("fo.")); 39 | await waitFor(() => expect(result).toBe("foo")); 40 | 41 | setValue("hi"); 42 | 43 | await waitFor(() => expect(result).toBe(".oo")); 44 | await waitFor(() => expect(result).toBe("..o")); 45 | await waitFor(() => expect(result).toBe("...")); 46 | await waitFor(() => expect(result).toBe("h..")); 47 | await waitFor(() => expect(result).toBe("hi.")); 48 | await waitFor(() => expect(result).toBe("hi")); 49 | }); 50 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | dencrypt, 4 | DencryptInitialOptions, 5 | DencryptDefaultOptions, 6 | } from "./dencrypt"; 7 | 8 | type DencryptReturnType = ReturnType; 9 | type UseDencryptReturnType = [string, DencryptReturnType]; 10 | 11 | export function useDencrypt(): UseDencryptReturnType; 12 | export function useDencrypt( 13 | initialValue: Required 14 | ): UseDencryptReturnType; 15 | export function useDencrypt( 16 | options: DencryptDefaultOptions 17 | ): UseDencryptReturnType; 18 | export function useDencrypt( 19 | initialValue: Required, 20 | options: DencryptDefaultOptions 21 | ): UseDencryptReturnType; 22 | export function useDencrypt( 23 | v?: string | DencryptDefaultOptions, 24 | o?: DencryptDefaultOptions 25 | ) { 26 | let initialValue = ""; 27 | let options: DencryptDefaultOptions = {}; 28 | 29 | if (typeof v === "object") { 30 | options = v; 31 | } else if (typeof v === "string") { 32 | initialValue = v; 33 | options = o ? o : {}; 34 | } 35 | 36 | const [result, setResult] = React.useState(); 37 | const [setValue, setSetValue] = React.useState(() => 38 | dencrypt({ 39 | ...options, 40 | initialValue, 41 | callback: setResult, 42 | }) 43 | ); 44 | 45 | return [result, setValue]; 46 | } 47 | 48 | export { dencrypt }; 49 | -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook, act, waitFor } from "@testing-library/react"; 2 | import { expect, test } from "vitest"; 3 | 4 | import { useDencrypt } from "./"; 5 | 6 | test("accepts initial value", () => { 7 | const initialValue = "value"; 8 | 9 | const { result } = renderHook(() => useDencrypt(initialValue)); 10 | 11 | expect(result.current[0]).toBe(initialValue); 12 | }); 13 | 14 | test("changes value with text effect", async () => { 15 | const { result } = renderHook(() => 16 | useDencrypt({ 17 | chars: ".", 18 | }) 19 | ); 20 | 21 | expect(result.current[0]).toBe(undefined); 22 | 23 | act(() => { 24 | result.current[1]("foo"); 25 | }); 26 | 27 | await waitFor(() => expect(result.current[0]).toBe("")); 28 | await waitFor(() => expect(result.current[0]).toBe(".")); 29 | await waitFor(() => expect(result.current[0]).toBe("..")); 30 | await waitFor(() => expect(result.current[0]).toBe("...")); 31 | await waitFor(() => expect(result.current[0]).toBe("f..")); 32 | await waitFor(() => expect(result.current[0]).toBe("fo.")); 33 | await waitFor(() => expect(result.current[0]).toBe("foo")); 34 | 35 | act(() => { 36 | result.current[1]("hi"); 37 | }); 38 | 39 | await waitFor(() => expect(result.current[0]).toBe(".oo")); 40 | await waitFor(() => expect(result.current[0]).toBe("..o")); 41 | await waitFor(() => expect(result.current[0]).toBe("...")); 42 | await waitFor(() => expect(result.current[0]).toBe("h..")); 43 | await waitFor(() => expect(result.current[0]).toBe("hi.")); 44 | await waitFor(() => expect(result.current[0]).toBe("hi")); 45 | }); 46 | -------------------------------------------------------------------------------- /src/dencrypt.ts: -------------------------------------------------------------------------------- 1 | export type DencryptInitialOptions = { 2 | initialValue?: string; 3 | callback: (value: string) => void; 4 | }; 5 | export type DencryptDefaultOptions = Partial; 6 | 7 | const defaultOptions = { 8 | chars: "-./^*!}<~$012345abcdef", 9 | interval: 50, 10 | }; 11 | 12 | const getRandomChar = (chars: string) => 13 | chars[Math.floor(Math.random() * chars.length)]; 14 | 15 | const getChar = ( 16 | i: number, 17 | j: number, 18 | maxLength: number, 19 | oldValue: string, 20 | newValue: string, 21 | chars: string 22 | ) => { 23 | if (j > i) { 24 | return oldValue[j]; 25 | } 26 | 27 | if (i >= maxLength && j < i - maxLength) { 28 | return newValue[j]; 29 | } 30 | 31 | return getRandomChar(chars); 32 | }; 33 | 34 | export const dencrypt = ( 35 | options: DencryptInitialOptions & DencryptDefaultOptions 36 | ) => { 37 | const { chars, interval, callback, initialValue } = { 38 | ...defaultOptions, 39 | ...options, 40 | }; 41 | 42 | let lastValue: string; 43 | let isCrypting: ReturnType; 44 | 45 | if (initialValue) { 46 | lastValue = initialValue; 47 | callback(lastValue); 48 | } 49 | 50 | function* calculateValues(nextValue: string, prevValue = "") { 51 | const nextLength = nextValue.length; 52 | const prevLength = prevValue.length; 53 | const maxLength = Math.max(nextLength, prevLength); 54 | const iterations = 2 * maxLength; 55 | 56 | let i = 0; 57 | 58 | yield prevValue; 59 | 60 | while (i < iterations) { 61 | yield [...new Array(maxLength)] 62 | .map((_, j) => getChar(i, j, maxLength, prevValue, nextValue, chars)) 63 | .join(""); 64 | 65 | i++; 66 | } 67 | 68 | yield nextValue; 69 | } 70 | 71 | const setValue = (value: string) => { 72 | clearInterval(isCrypting); 73 | const values = calculateValues(value, lastValue); 74 | 75 | return new Promise((resolve) => { 76 | isCrypting = setInterval(() => { 77 | var next = values.next(); 78 | 79 | if (next.done) { 80 | clearInterval(isCrypting); 81 | resolve(lastValue); 82 | } else { 83 | lastValue = next.value; 84 | callback(lastValue); 85 | } 86 | }, interval); 87 | }); 88 | }; 89 | 90 | return setValue; 91 | }; 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | ![Dencrypt example](https://github.com/crazko/use-dencrypt-effect/raw/master/docs/dencrypt.gif) 4 | 5 |
6 | 7 | # use-dencrypt-effect 8 | 9 | [![NPM](https://img.shields.io/npm/v/use-dencrypt-effect.svg)](https://www.npmjs.com/package/use-dencrypt-effect) 10 | 11 | 12 | A custom [React hook](https://reactjs.org/docs/hooks-intro.html) generating crypting text effect. 13 | 14 | **Live demo**: https://codesandbox.io/s/use-dencrypt-effect-7td0f. 15 | 16 | ## Install 17 | 18 | ```bash 19 | npm install --save use-dencrypt-effect 20 | ``` 21 | 22 | ## Usage 23 | 24 | ```tsx 25 | import * as React from "react"; 26 | 27 | import { useDencrypt } from "use-dencrypt-effect"; 28 | 29 | const Example = () => { 30 | const [value, setValue] = useDencrypt("initialValue"); 31 | 32 | return
setValue("newValue")}>{value}
; 33 | }; 34 | ``` 35 | 36 | ## API 37 | 38 | ### useDencrypt(initialValue?, options?) 39 | 40 | Returns a tuple `[value, setValue]` consisting of an actual value and a method to set a new value. Just like `useState()` hook. 41 | 42 | #### value 43 | 44 | Type: `string` 45 | 46 | Result of the animation. 47 | 48 | #### setValue(newValue) 49 | 50 | Sets a value and starts new animation. 51 | 52 | Returns a promise which is resolved when animation for `newValue` ends. 53 | 54 | ##### newValue 55 | 56 | Type: `string` 57 | 58 | A value used for next animation. 59 | 60 | #### initialValue 61 | 62 | Type: `string` 63 | 64 | Optional value that is returned immediately. 65 | 66 | #### options 67 | 68 | Type: `Object` 69 | 70 | All parameters are optional. 71 | 72 | ##### chars 73 | 74 | Type: `string`\ 75 | Default: `-./^*!}<~$012345abcdef` 76 | 77 | Characters used for the effect. Picked by random. 78 | 79 | ##### interval 80 | 81 | Type: `number`\ 82 | Default: `50` 83 | 84 | Number of miliseconds it takes for every animation step (one character). 85 | 86 | ## Examples 87 | 88 | See [`./src/examples`](./src/examples) directory. 89 | 90 | - [Custom Characters](./src/examples/custom-characters.tsx) 91 | - [Initial Value](./src/examples/initial-value.tsx) 92 | - [Loop Through Values](./src/examples/loop.tsx) 93 | - [Use without React hook](./src/examples/without-hook.tsx) 94 | 95 | ### One character 96 | 97 | ![](https://github.com/crazko/use-dencrypt-effect/raw/master/docs/example1.gif) 98 | 99 | ```js 100 | const Example = () => { 101 | const [value, setValue] = useDencrypt({ chars: "_" }); 102 | 103 | // ... 104 | ``` 105 | 106 | ### Run effect on hover 107 | 108 | [Live Example](https://vojdivon.sk/) | [Source Code](https://github.com/ParalelnaPolisKE/vojdivon.sk/blob/54fcbf5c573de485b5d6ed2051d515da7f0bf252/src/index.jsx#L43) 109 | --------------------------------------------------------------------------------