├── .changeset ├── README.md └── config.json ├── .editorconfig ├── .gitattributes ├── .github ├── markdown-autodocs │ ├── options.json │ ├── usage-react.js │ └── usage.js └── workflows │ ├── gh-pages.yaml │ ├── markdown-autodocs.yml │ └── npm-publish.yaml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc.js ├── .vscode └── extensions.json ├── LICENSE ├── README.md ├── examples ├── basic │ ├── index.html │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── vite-env.d.ts │ └── tsconfig.json ├── export-as-image │ ├── index.html │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── vite-env.d.ts │ └── tsconfig.json ├── prefers-mask │ ├── index.html │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── vite-env.d.ts │ └── tsconfig.json ├── with-react │ ├── index.html │ ├── package.json │ ├── src │ │ ├── App.tsx │ │ ├── main.tsx │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts └── with-vue │ ├── index.html │ ├── package.json │ ├── src │ ├── App.vue │ ├── main.ts │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── package.json ├── packages ├── react │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.tsx │ │ └── vite-env.d.ts │ ├── tsconfig.json │ └── vite.config.ts ├── twallpaper-webgl │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── distance.ts │ │ ├── fragment-shader.glsl │ │ ├── hex-to-vec3.ts │ │ ├── index.ts │ │ ├── load-shaders.ts │ │ ├── style.css │ │ ├── twallpaper.ts │ │ ├── vertex-shader.glsl │ │ └── vite-env.d.ts │ ├── tsconfig.json │ └── vite.config.ts ├── twallpaper │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── colors.ts │ │ ├── constants.ts │ │ ├── index.ts │ │ ├── style.css │ │ ├── twallpaper.ts │ │ ├── types.ts │ │ └── vite-env.d.ts │ ├── tsconfig.json │ └── vite.config.ts └── vue │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ ├── TWallpaper.vue │ ├── index.ts │ └── vite-env.d.ts │ ├── tsconfig.json │ └── vite.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── turbo.json └── website ├── index.html ├── package.json ├── public ├── CNAME ├── index.css ├── patterns │ ├── animals.svg │ ├── astronaut_cats.svg │ ├── beach.svg │ ├── cats_and_dogs.svg │ ├── christmas.svg │ ├── fantasy.svg │ ├── games.svg │ ├── late_night_delight.svg │ ├── magic.svg │ ├── math.svg │ ├── paris.svg │ ├── snowflakes.svg │ ├── space.svg │ ├── star_wars.svg │ ├── sweets.svg │ ├── tattoos.svg │ ├── underwater_world.svg │ ├── unicorn.svg │ └── zoo.svg ├── twallpaper-original.js └── utya.webp ├── src ├── colors.ts ├── config.ts ├── index.ts ├── patterns.ts └── webgl.ts ├── tsconfig.json └── webgl.html /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "master", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [ 11 | "basic", 12 | "export-as-image", 13 | "prefers-mask", 14 | "with-react", 15 | "with-vue", 16 | "website" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/markdown-autodocs/options.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Key": "colors", 4 | "Type": "string[]", 5 | "Default": "", 6 | "Description": "Array of colors in hex format. Allowed maximum of 4 colors." 7 | }, 8 | { 9 | "Key": "fps", 10 | "Type": "number", 11 | "Default": "30", 12 | "Description": "Animation speed." 13 | }, 14 | { 15 | "Key": "tails", 16 | "Type": "number", 17 | "Default": "90", 18 | "Description": "Tail speed animation." 19 | }, 20 | { 21 | "Key": "animate", 22 | "Type": "boolean", 23 | "Default": "true", 24 | "Description": "Animation is enabled." 25 | }, 26 | { 27 | "Key": "scrollAnimate", 28 | "Type": "boolean", 29 | "Default": "false", 30 | "Description": "Animation is enabled when scrolling." 31 | }, 32 | { 33 | "Key": "pattern", 34 | "Type": "PatternOptions", 35 | "Default": "", 36 | "Description": "Pattern options." 37 | }, 38 | { 39 | "Key": "pattern.image", 40 | "Type": "string", 41 | "Default": "", 42 | "Description": "Wallpaper image. Use standard pattern or create your own." 43 | }, 44 | { 45 | "Key": "pattern.mask", 46 | "Type": "boolean", 47 | "Default": "false", 48 | "Description": "Option enables a mask for the background image using the mask-image css-property." 49 | }, 50 | { 51 | "Key": "pattern.background", 52 | "Type": "string", 53 | "Default": "#000", 54 | "Description": "Background color for the pattern image. Background does not work when pattern.mask is enabled." 55 | }, 56 | { 57 | "Key": "pattern.size", 58 | "Type": "string", 59 | "Default": "auto", 60 | "Description": "Size of the pattern image." 61 | }, 62 | { 63 | "Key": "pattern.blur", 64 | "Type": "number", 65 | "Default": "0", 66 | "Description": "Blur of the pattern image. Blur does not work when pattern.mask is enabled." 67 | }, 68 | { 69 | "Key": "pattern.opacity", 70 | "Type": "number", 71 | "Default": "0.5", 72 | "Description": "Opacity of the pattern image." 73 | } 74 | ] 75 | -------------------------------------------------------------------------------- /.github/markdown-autodocs/usage-react.js: -------------------------------------------------------------------------------- 1 | import { TWallpaper } from '@twallpaper/react' 2 | import '@twallpaper/react/css' 3 | 4 | export function App() { 5 | return ( 6 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /.github/markdown-autodocs/usage.js: -------------------------------------------------------------------------------- 1 | import { TWallpaper } from 'twallpaper' 2 | import 'twallpaper/css' 3 | 4 | const container = document.querySelector('.tw-wrap') 5 | const wallpaper = new TWallpaper(container, { 6 | colors: [ 7 | '#dbddbb', 8 | '#6ba587', 9 | '#d5d88d', 10 | '#88b884' 11 | ] 12 | }) 13 | wallpaper.init() 14 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yaml: -------------------------------------------------------------------------------- 1 | name: GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | cache-and-install: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | 15 | - name: Install Node.js 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: 16 19 | 20 | - name: Install pnpm 21 | uses: pnpm/action-setup@v2 22 | id: pnpm-install 23 | with: 24 | version: 8 25 | run_install: false 26 | 27 | - name: Get pnpm store directory 28 | id: pnpm-cache 29 | shell: bash 30 | run: | 31 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 32 | 33 | - name: Setup pnpm cache 34 | uses: actions/cache@v3 35 | with: 36 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 37 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 38 | restore-keys: | 39 | ${{ runner.os }}-pnpm-store- 40 | 41 | - name: Install dependencies 42 | run: pnpm install 43 | 44 | - name: Build website 45 | run: pnpm website:build 46 | 47 | - name: Deploy to GitHub Pages 48 | uses: JamesIves/github-pages-deploy-action@4.1.5 49 | with: 50 | branch: gh-pages 51 | folder: ./website/dist 52 | -------------------------------------------------------------------------------- /.github/workflows/markdown-autodocs.yml: -------------------------------------------------------------------------------- 1 | name: Markdown Autodocs 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | 8 | jobs: 9 | auto-update-readme: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Markdown autodocs 14 | uses: dineshsonachalam/markdown-autodocs@v1.0.7 15 | with: 16 | commit_author: Vitalij Ryndin 17 | commit_user_email: sys@crashmax.ru 18 | commit_message: 'docs: update readme' 19 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yaml: -------------------------------------------------------------------------------- 1 | name: NPM Publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | permissions: 9 | id-token: write 10 | 11 | jobs: 12 | cache-and-install: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | 18 | - name: Install Node.js 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: 18 22 | 23 | - name: Install pnpm 24 | uses: pnpm/action-setup@v2 25 | id: pnpm-install 26 | with: 27 | version: 8 28 | run_install: false 29 | 30 | - name: Get pnpm store directory 31 | id: pnpm-cache 32 | shell: bash 33 | run: | 34 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 35 | 36 | - name: Setup pnpm cache 37 | uses: actions/cache@v3 38 | with: 39 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 40 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 41 | restore-keys: | 42 | ${{ runner.os }}-pnpm-store- 43 | 44 | - name: Install dependencies 45 | run: pnpm install 46 | 47 | - name: Build packages 48 | run: pnpm build 49 | 50 | - name: Publish packages 51 | shell: bash 52 | run: | 53 | echo "//registry.npmjs.org/:_authToken="${{ secrets.NPM_TOKEN }}"" > ~/.npmrc 54 | pnpm -r --filter='./packages/*' publish --access public --provenance 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/dist 3 | **/.turbo 4 | *.tgz 5 | *.log 6 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | provenance=true 2 | auto-install-peers=true 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | build 4 | logs 5 | .next 6 | .turbo 7 | .github 8 | .angular 9 | .svelte-kit 10 | *.log 11 | *.lock 12 | *.yaml 13 | *.yml 14 | *.sh 15 | *.svg 16 | *rc.* 17 | *.htm 18 | *.html 19 | *.json 20 | *.md 21 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@crashmax/prettier-config') 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar"] 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Vitalij Ryndin 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 |
2 |

3 | 4 | 5 |
6 |

TWallpaper

7 | 8 |

9 | 10 |

11 | 🌈 Multicolor gradient wallpaper created algorithmically and shimmers smoothly. 12 |

13 | 14 |

15 | 16 | GitHub Workflow Status 17 | 18 | 19 | npm 20 | 21 | 22 | npm 23 | 24 | 25 | npm bundle size 26 | 27 |

28 | 29 | ## Features 30 | 31 | - 🔥 Zero [dependencies](https://www.npmjs.com/package/twallpaper?activeTab=dependents) 32 | - ⚙️ Flexible [configuration](#options-1) 33 | - 📦 Lightweight ([~2.5kB gzipped](https://bundlephobia.com/package/twallpaper)) 34 | - 📜 Supports [TypeScript](https://www.typescriptlang.org) type definition 35 | 36 | ## Installation 37 | 38 | ```sh 39 | npm install twallpaper 40 | ``` 41 | 42 | ```sh 43 | yarn add twallpaper 44 | ``` 45 | 46 | ```sh 47 | pnpm add twallpaper 48 | ``` 49 | 50 | ## Demo 51 | 52 | You can play with `twallpaper` on [twallpaper.js.org](https://twallpaper.js.org) 53 | 54 | ## Usage (vanilla) 55 | 56 | 57 | 58 | ```js 59 | import { TWallpaper } from 'twallpaper' 60 | import 'twallpaper/css' 61 | 62 | const container = document.querySelector('.tw-wrap') 63 | const wallpaper = new TWallpaper(container, { 64 | colors: [ 65 | '#dbddbb', 66 | '#6ba587', 67 | '#d5d88d', 68 | '#88b884' 69 | ] 70 | }) 71 | wallpaper.init() 72 | ``` 73 | 74 | 75 | 76 | Edit twallpaper-typescript-example 77 | 78 | 79 | ## React 80 | 81 | ```sh 82 | npm install @twallpaper/react 83 | ``` 84 | 85 | ```sh 86 | yarn add @twallpaper/react 87 | ``` 88 | 89 | ```sh 90 | pnpm add @twallpaper/react 91 | ``` 92 | 93 | 94 | 95 | ```js 96 | import { TWallpaper } from '@twallpaper/react' 97 | import '@twallpaper/react/css' 98 | 99 | export function App() { 100 | return ( 101 | 111 | ) 112 | } 113 | ``` 114 | 115 | 116 | 117 | Edit twallpaper-react-example 118 | 119 | 120 | ## Vue 121 | 122 | ```sh 123 | npm install @twallpaper/vue 124 | ``` 125 | 126 | ```sh 127 | yarn add @twallpaper/vue 128 | ``` 129 | 130 | ```sh 131 | pnpm add @twallpaper/vue 132 | ``` 133 | 134 | [![Edit @twallpaper/vue](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/compassionate-fog-wmhg4d?fontsize=14&hidenavigation=1&theme=dark) 135 | 136 | ## Using CDN 137 | ```html 138 | 139 | 140 | 141 | 142 | 143 | ``` 144 | 145 | ## API 146 | 147 | ### `.init(options?, container?)` 148 | Initialize animation (before reinitializing, calls the `dispose()` method). 149 | 150 | #### options 151 | Type: [`TWallpaperOptions`](https://github.com/crashmax-dev/twallpaper/blob/master/packages/twallpaper/src/types.ts#L21-L28) 152 | 153 | #### container 154 | Type: `Element` 155 | 156 | ### `.animate(start?)` 157 | Start or stop animation. 158 | 159 | #### start 160 | Type: `boolean`\ 161 | Default: `true` 162 | 163 | ### `.dispose()` 164 | Destroy the instance wallpaper. 165 | 166 | ### `.scrollAnimate(start?)` 167 | Start or stop mouse scroll animation. 168 | 169 | #### start 170 | Type: `boolean`\ 171 | Default: `false` 172 | 173 | ### `.toNextPosition(onNext?)` 174 | Next animation position (animation turns off after use). 175 | 176 | #### onNext 177 | Execution `toNextPosition` is finished.\ 178 | Type: `function` 179 | 180 | ### `.updateColors(colors)` 181 | Force update colors. 182 | 183 | #### colors 184 | Type: `string[]` 185 | 186 | ### `.updateFrametime(fps?)` 187 | Force update frametime. 188 | 189 | #### fps 190 | Type: `number`\ 191 | Default: `30` 192 | 193 | ### `.updatePattern(pattern)` 194 | Force update pattern options. 195 | 196 | #### pattern 197 | Type: [`PatternOptions`](https://github.com/crashmax-dev/twallpaper/blob/master/packages/twallpaper/src/types.ts#L12-L19) 198 | 199 | ### `.updateTails(tails?)` 200 | Force update tails speed. 201 | 202 | #### tails 203 | Type: `number`\ 204 | Default `90` 205 | 206 | ## Options 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 |
KeyTypeDefaultDescription
colorsstring[]Array of colors in hex format. Allowed maximum of 4 colors.
fpsnumber30Animation speed.
tailsnumber90Tail speed animation.
animatebooleantrueAnimation is enabled.
scrollAnimatebooleanfalseAnimation is enabled when scrolling.
patternPatternOptionsPattern options.
pattern.imagestringWallpaper image. Use standard pattern or create your own.
pattern.maskbooleanfalseOption enables a mask for the background image using the mask-image css-property.
pattern.backgroundstring#000Background color for the pattern image. Background does not work when pattern.mask is enabled.
pattern.sizestringautoSize of the pattern image.
pattern.blurnumber0Blur of the pattern image. Blur does not work when pattern.mask is enabled.
pattern.opacitynumber0.5Opacity of the pattern image.
221 | 222 | -------------------------------------------------------------------------------- /examples/basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | twallpaper (basic) 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "twallpaper": "workspace:*" 13 | }, 14 | "devDependencies": { 15 | "typescript": "^5.0.4", 16 | "vite": "^4.3.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/basic/src/index.ts: -------------------------------------------------------------------------------- 1 | import { TWallpaper } from 'twallpaper' 2 | import type { TWallpaperOptions } from 'twallpaper' 3 | import 'twallpaper/css' 4 | 5 | const container = document.querySelector('#app')! 6 | const options: TWallpaperOptions = { 7 | animate: false, 8 | scrollAnimate: true, 9 | fps: 60, 10 | tails: 40, 11 | colors: [ 12 | '#4f5bd5', 13 | '#962fbf', 14 | '#dd6cb9', 15 | '#fec496' 16 | ], 17 | pattern: { 18 | mask: true, 19 | size: '420px', 20 | image: 'https://twallpaper.js.org/patterns/games.svg' 21 | } 22 | } 23 | 24 | const wallpaper = new TWallpaper(container, options) 25 | wallpaper.init() 26 | -------------------------------------------------------------------------------- /examples/basic/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/basic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ESNext", "DOM"], 7 | "moduleResolution": "Node", 8 | "strict": true, 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "esModuleInterop": true, 13 | "noEmit": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noImplicitReturns": true, 17 | "skipLibCheck": true 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /examples/export-as-image/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | twallpaper (export-as-image) 7 | 25 | 26 | 27 |
28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /examples/export-as-image/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "export-as-image", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "devDependencies": { 12 | "typescript": "^5.0.4", 13 | "vite": "^4.3.1" 14 | }, 15 | "dependencies": { 16 | "html-to-image": "^1.11.11", 17 | "twallpaper": "workspace:*" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/export-as-image/src/index.ts: -------------------------------------------------------------------------------- 1 | import { toPng } from 'html-to-image' 2 | import { TWallpaper } from 'twallpaper' 3 | import type { TWallpaperOptions } from 'twallpaper' 4 | import 'twallpaper/css' 5 | 6 | const options: TWallpaperOptions = { 7 | animate: true, 8 | fps: 60, 9 | tails: 30, 10 | pattern: { 11 | // mask: true, // mask doesn't work 12 | // background: '#000', 13 | // blur: 0.5, 14 | // opacity: 0.5, 15 | image: 'https://twallpaper.js.org/patterns/underwater_world.svg' 16 | }, 17 | colors: [ 18 | '#527bdd', 19 | '#009fdd', 20 | '#a4dbff', 21 | '#f7f7f7' 22 | ] 23 | } 24 | 25 | const container = document.querySelector('#app')! 26 | const wallpaper = new TWallpaper(container) 27 | wallpaper.init(options) 28 | 29 | const button = document.createElement('button') 30 | button.textContent = 'Export as Image' 31 | button.addEventListener('click', exportAsImage) 32 | document.body.appendChild(button) 33 | 34 | async function exportAsImage() { 35 | const wallpaperCanvas = document.querySelector('.tw-canvas')! 36 | const wallpaperPattern = document.querySelector('.tw-pattern')! 37 | 38 | const canvas = document.createElement('canvas') 39 | const ctx = canvas.getContext('2d')! 40 | canvas.classList.add('canvas-preview') 41 | canvas.width = wallpaperCanvas.clientWidth 42 | canvas.height = wallpaperCanvas.clientHeight 43 | document.body.appendChild(canvas) 44 | 45 | const gradient = new Image() 46 | gradient.src = await toPng(wallpaperCanvas) 47 | gradient.onload = () => { 48 | ctx.drawImage(gradient, 0, 0) 49 | } 50 | 51 | const pattern = new Image() 52 | pattern.src = await toPng(wallpaperPattern) 53 | pattern.onload = async () => { 54 | ctx.globalCompositeOperation = 'multiply' 55 | ctx.drawImage(pattern, 0, 0) 56 | 57 | const image = await toPng(canvas) 58 | const link = document.createElement('a') 59 | link.download = 'twallpaper.png' 60 | link.href = image 61 | link.click() 62 | canvas.remove() 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /examples/export-as-image/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/export-as-image/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ESNext", "DOM"], 7 | "moduleResolution": "Node", 8 | "strict": true, 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "esModuleInterop": true, 13 | "noEmit": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noImplicitReturns": true, 17 | "skipLibCheck": true 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /examples/prefers-mask/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | twallpaper (prefers-mask) 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/prefers-mask/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prefers-mask", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "devDependencies": { 12 | "typescript": "^5.0.4", 13 | "vite": "^4.3.1" 14 | }, 15 | "dependencies": { 16 | "twallpaper": "workspace:*" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/prefers-mask/src/index.ts: -------------------------------------------------------------------------------- 1 | import { TWallpaper } from 'twallpaper' 2 | import type { TWallpaperOptions } from 'twallpaper' 3 | import 'twallpaper/css' 4 | 5 | const container = document.querySelector('#app')! 6 | const wallpaper = new TWallpaper(container) 7 | const options: TWallpaperOptions = { 8 | fps: 60, 9 | tails: 30, 10 | colors: [ 11 | '#dbddbb', 12 | '#6ba587', 13 | '#d5d88d', 14 | '#88b884' 15 | ], 16 | pattern: { 17 | image: 'https://twallpaper.js.org/patterns/games.svg' 18 | } 19 | } 20 | wallpaper.init(options) 21 | 22 | function toggleMask(mask: boolean): void { 23 | wallpaper.updatePattern({ 24 | ...options.pattern, 25 | mask 26 | }) 27 | } 28 | 29 | // Emulate `prefers-color-scheme` media query in Google Chrome 30 | // https://stackoverflow.com/a/59223868 31 | if (window.matchMedia) { 32 | const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') 33 | toggleMask(mediaQuery.matches) 34 | 35 | mediaQuery.addEventListener('change', (event) => { 36 | toggleMask(event.matches) 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /examples/prefers-mask/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/prefers-mask/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ESNext", "DOM"], 7 | "moduleResolution": "Node", 8 | "strict": true, 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "esModuleInterop": true, 13 | "noEmit": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noImplicitReturns": true, 17 | "skipLibCheck": true 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /examples/with-react/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | twallpaper (with-react) 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/with-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-react", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@twallpaper/react": "workspace:*", 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0" 15 | }, 16 | "devDependencies": { 17 | "@types/react": "^18.0.38", 18 | "@types/react-dom": "^18.0.11", 19 | "@vitejs/plugin-react": "^4.0.0", 20 | "typescript": "^5.0.4", 21 | "vite": "^4.3.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/with-react/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react' 2 | import { TWallpaper } from '@twallpaper/react' 3 | import type { TWallpaperHandlers } from '@twallpaper/react' 4 | import '@twallpaper/react/css' 5 | 6 | export function App() { 7 | const ref = useRef(null) 8 | return ( 9 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /examples/with-react/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import { App } from './App' 4 | 5 | const root = document.getElementById('root')! 6 | createRoot(root).render( 7 | 8 | 9 | 10 | ) 11 | -------------------------------------------------------------------------------- /examples/with-react/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/with-react/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 | -------------------------------------------------------------------------------- /examples/with-react/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /examples/with-react/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react' 2 | import { defineConfig } from 'vite' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | -------------------------------------------------------------------------------- /examples/with-vue/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | @twallpaper/vue 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/with-vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-vue", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vue-tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@twallpaper/vue": "workspace:*", 13 | "vue": "^3.2.47" 14 | }, 15 | "devDependencies": { 16 | "@vitejs/plugin-vue": "^4.1.0", 17 | "typescript": "^5.0.4", 18 | "vite": "^4.3.1", 19 | "vue-tsc": "^1.4.4" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/with-vue/src/App.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 55 | 56 | 66 | -------------------------------------------------------------------------------- /examples/with-vue/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | 4 | createApp(App).mount('#app') 5 | -------------------------------------------------------------------------------- /examples/with-vue/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue' 5 | const component: DefineComponent<{}, {}, any> 6 | export default component 7 | } 8 | -------------------------------------------------------------------------------- /examples/with-vue/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "esModuleInterop": true, 12 | "lib": ["ESNext", "DOM"], 13 | "skipLibCheck": true, 14 | "noEmit": true 15 | }, 16 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 17 | "references": [{ "path": "./tsconfig.node.json" }] 18 | } 19 | -------------------------------------------------------------------------------- /examples/with-vue/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /examples/with-vue/vite.config.ts: -------------------------------------------------------------------------------- 1 | import vue from '@vitejs/plugin-vue' 2 | import { defineConfig } from 'vite' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [vue()] 7 | }) 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@twallpaper/monorepo", 3 | "version": "2.0.0", 4 | "description": "🌈 Multicolor gradient wallpaper created algorithmically and shimmers smoothly.", 5 | "private": true, 6 | "scripts": { 7 | "dev": "turbo run dev --filter='./packages/*'", 8 | "build": "turbo run build --filter='./packages/*'", 9 | "website:dev": "turbo run dev --filter='./website/'", 10 | "website:build": "turbo run build --filter='./website'", 11 | "format": "prettier --write '**/*.{js,ts,tsx,json}'", 12 | "changeset": "changeset" 13 | }, 14 | "license": "MIT", 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/crashmax-dev/twallpaper.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/crashmax-dev/twallpaper/issues" 21 | }, 22 | "homepage": "https://twallpaper.js.org", 23 | "keywords": [ 24 | "react", 25 | "multicolor", 26 | "gradient", 27 | "wallpaper", 28 | "background", 29 | "animation", 30 | "telegram", 31 | "canvas" 32 | ], 33 | "devDependencies": { 34 | "@changesets/cli": "^2.26.2", 35 | "@crashmax/prettier-config": "^3.2.1", 36 | "@crashmax/tsconfig": "^2.0.1", 37 | "@types/node": "18.16.0", 38 | "turbo": "^1.9.3", 39 | "vite-plugin-dts": "^2.3.0" 40 | }, 41 | "engines": { 42 | "pnpm": ">=8.0.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/react/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @twallpaper/react 2 | 3 | ## 2.1.2 4 | 5 | ### Patch Changes 6 | 7 | - fix: initialize animate 8 | - Updated dependencies 9 | - twallpaper@2.1.2 10 | 11 | ## 2.1.1 12 | 13 | ### Patch Changes 14 | 15 | - feat(workspace): add `@changesets/cli` 16 | ci(npm): add `--provenance` 17 | - Updated dependencies 18 | - twallpaper@2.1.1 19 | -------------------------------------------------------------------------------- /packages/react/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 | 5 |
6 |

TWallpaper

7 | 8 |

9 | 10 |

11 | 🌈 Multicolor gradient wallpaper created algorithmically and shimmers smoothly. 12 |

13 | 14 |

15 | 16 | npm 17 | 18 | 19 | npm 20 | 21 | 22 | npm bundle size 23 | 24 |

25 | 26 | ## Installation 27 | 28 | ```sh 29 | npm install @twallpaper/react 30 | ``` 31 | 32 | ```sh 33 | yarn add @twallpaper/react 34 | ``` 35 | 36 | ```sh 37 | pnpm add @twallpaper/react 38 | ``` 39 | -------------------------------------------------------------------------------- /packages/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@twallpaper/react", 3 | "version": "2.1.2", 4 | "description": "🌈 Multicolor gradient wallpaper created algorithmically and shimmers smoothly.", 5 | "type": "module", 6 | "types": "./dist/index.d.ts", 7 | "main": "./dist/index.cjs", 8 | "module": "./dist/index.js", 9 | "exports": { 10 | ".": { 11 | "import": "./dist/index.js", 12 | "require": "./dist/index.cjs" 13 | }, 14 | "./dist/style.css": "./dist/style.css", 15 | "./css": "./dist/style.css" 16 | }, 17 | "files": [ 18 | "dist" 19 | ], 20 | "license": "MIT", 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/crashmax-dev/twallpaper.git" 24 | }, 25 | "bugs": { 26 | "url": "https://github.com/crashmax-dev/twallpaper/issues" 27 | }, 28 | "homepage": "https://twallpaper.js.org", 29 | "keywords": [ 30 | "react", 31 | "multicolor", 32 | "gradient", 33 | "wallpaper", 34 | "background", 35 | "animation", 36 | "telegram", 37 | "canvas" 38 | ], 39 | "scripts": { 40 | "dev": "vite build --watch", 41 | "build": "vite build" 42 | }, 43 | "dependencies": { 44 | "twallpaper": "workspace:2.1.2" 45 | }, 46 | "devDependencies": { 47 | "@types/react": "^18.0.38", 48 | "@vitejs/plugin-react": "^4.0.0", 49 | "react": "^18.2.0", 50 | "typescript": "^5.0.4", 51 | "vite": "^4.3.1" 52 | }, 53 | "peerDependencies": { 54 | "@types/react": ">=16.8.0", 55 | "react": ">=16.8.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/react/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { TWallpaper as TW } from 'twallpaper' 3 | import type { PatternOptions, TWallpaperOptions } from 'twallpaper' 4 | import 'twallpaper/css' 5 | 6 | interface TWallpaperProps extends React.HTMLAttributes { 7 | options: TWallpaperOptions 8 | } 9 | 10 | interface TWallpaperHandlers { 11 | animate(start?: boolean): void 12 | scrollAnimate(start?: boolean): void 13 | updateColors(colors: string[]): void 14 | updateFrametime(fps: number): void 15 | updatePattern(pattern: PatternOptions): void 16 | updateTails(tails: number): void 17 | toNextPosition(onNext?: () => void): void 18 | } 19 | 20 | const TWallpaper = React.forwardRef( 21 | ({ options, ...props }, ref) => { 22 | const container = React.useRef(null) 23 | const twallpaper = React.useRef() 24 | 25 | React.useImperativeHandle(ref, () => ({ 26 | animate(start) { 27 | twallpaper.current!.animate(start) 28 | }, 29 | scrollAnimate(start) { 30 | twallpaper.current!.scrollAnimate(start) 31 | }, 32 | updateColors(colors) { 33 | twallpaper.current!.updateColors(colors) 34 | }, 35 | updateFrametime(fps) { 36 | twallpaper.current!.updateFrametime(fps) 37 | }, 38 | updatePattern(pattern) { 39 | twallpaper.current!.updatePattern(pattern) 40 | }, 41 | updateTails(tails) { 42 | twallpaper.current!.updateTails(tails) 43 | }, 44 | toNextPosition(onNext) { 45 | twallpaper.current!.toNextPosition(onNext) 46 | } 47 | })) 48 | 49 | React.useEffect(() => { 50 | if (!twallpaper.current) { 51 | twallpaper.current = new TW(container.current!) 52 | } 53 | 54 | twallpaper.current.init(options) 55 | 56 | return () => { 57 | twallpaper.current!.dispose() 58 | } 59 | }, []) 60 | 61 | return ( 62 |
66 | ) 67 | } 68 | ) 69 | 70 | export { TWallpaper } 71 | export default TWallpaper 72 | export type { 73 | TWallpaperProps, 74 | TWallpaperHandlers, 75 | TWallpaperOptions, 76 | PatternOptions 77 | } 78 | -------------------------------------------------------------------------------- /packages/react/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@crashmax/tsconfig", 3 | "compilerOptions": { 4 | "useDefineForClassFields": false, 5 | "jsx": "preserve", 6 | "outDir": "dist" 7 | }, 8 | "include": [ 9 | "src/**/*" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/react/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react' 2 | import { resolve } from 'path' 3 | import { defineConfig } from 'vite' 4 | import dts from 'vite-plugin-dts' 5 | import { description, homepage, name, version } from './package.json' 6 | 7 | export default defineConfig({ 8 | plugins: [react({ jsxRuntime: 'classic' }), dts({ insertTypesEntry: true })], 9 | esbuild: { 10 | banner: 11 | `/**\n * name: ${name}` + 12 | `\n * description: ${description}` + 13 | `\n * version: ${version}` + 14 | `\n * homepage: ${homepage}` + 15 | '\n */' + 16 | '\n"use client";' 17 | }, 18 | build: { 19 | target: 'esnext', 20 | lib: { 21 | entry: resolve(__dirname, 'src/index.tsx'), 22 | name: 'TWallpaper', 23 | formats: ['es', 'cjs'], 24 | fileName: 'index' 25 | }, 26 | rollupOptions: { 27 | external: ['react', 'twallpaper'], 28 | output: { 29 | exports: 'named', 30 | globals: { 31 | react: 'React', 32 | twallpaper: 'twallpaper' 33 | } 34 | } 35 | } 36 | } 37 | }) 38 | -------------------------------------------------------------------------------- /packages/twallpaper-webgl/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @twallpaper/webgl 2 | 3 | ## 2.2.0 4 | 5 | ### Minor Changes 6 | 7 | - feat: add webgl implementation 8 | -------------------------------------------------------------------------------- /packages/twallpaper-webgl/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 | 5 |
6 |

TWallpaper

7 | 8 |

9 | 10 |

11 | 🌈 Multicolor gradient wallpaper created algorithmically and shimmers smoothly. 12 |

13 | 14 |

15 | 16 | npm 17 | 18 | 19 | npm 20 | 21 | 22 | npm bundle size 23 | 24 |

25 | 26 | ## Installation 27 | 28 | ```sh 29 | npm install @twallpaper/webgl 30 | ``` 31 | 32 | ```sh 33 | yarn add @twallpaper/webgl 34 | ``` 35 | 36 | ```sh 37 | pnpm add @twallpaper/webgl 38 | ``` 39 | -------------------------------------------------------------------------------- /packages/twallpaper-webgl/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@twallpaper/webgl", 3 | "version": "2.2.0", 4 | "description": "🌈 Multicolor gradient wallpaper created algorithmically and shimmers smoothly.", 5 | "type": "module", 6 | "types": "./dist/index.d.ts", 7 | "main": "./dist/index.cjs.js", 8 | "jsdelivr": "./dist/index.umd.js", 9 | "unpkg": "./dist/index.umd.js", 10 | "module": "./dist/index.es.js", 11 | "exports": { 12 | ".": { 13 | "require": "./dist/index.cjs.js", 14 | "import": "./dist/index.es.js" 15 | }, 16 | "./dist/style.css": "./dist/style.css", 17 | "./css": "./dist/style.css" 18 | }, 19 | "files": [ 20 | "dist" 21 | ], 22 | "license": "MIT", 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/crashmax-dev/twallpaper.git" 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/crashmax-dev/twallpaper/issues" 29 | }, 30 | "homepage": "https://twallpaper.js.org", 31 | "keywords": [ 32 | "react", 33 | "multicolor", 34 | "gradient", 35 | "wallpaper", 36 | "background", 37 | "animation", 38 | "telegram", 39 | "canvas", 40 | "webgl" 41 | ], 42 | "scripts": { 43 | "dev": "vite build --watch", 44 | "build": "vite build" 45 | }, 46 | "devDependencies": { 47 | "typescript": "^5.0.4", 48 | "vite": "^4.3.1" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/twallpaper-webgl/src/distance.ts: -------------------------------------------------------------------------------- 1 | export function distance(a: number[], b: number[]): number { 2 | return Math.sqrt((a[1] - b[1]) * (a[1] - b[1])) 3 | } 4 | -------------------------------------------------------------------------------- /packages/twallpaper-webgl/src/fragment-shader.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | uniform vec2 resolution; 4 | 5 | uniform vec3 color1; 6 | uniform vec3 color2; 7 | uniform vec3 color3; 8 | uniform vec3 color4; 9 | 10 | uniform vec2 color1Pos; 11 | uniform vec2 color2Pos; 12 | uniform vec2 color3Pos; 13 | uniform vec2 color4Pos; 14 | 15 | void main() { 16 | vec2 position = gl_FragCoord.xy / resolution.xy; 17 | position.y = 1.0 - position.y; 18 | 19 | float dp1 = distance(position, color1Pos); 20 | float dp2 = distance(position, color2Pos); 21 | float dp3 = distance(position, color3Pos); 22 | float dp4 = distance(position, color4Pos); 23 | float minD = min(dp1, min(dp2, min(dp3, dp4))); 24 | float p = 3.0; 25 | 26 | dp1 = pow(1.0 - (dp1 - minD), p); 27 | dp2 = pow(1.0 - (dp2 - minD), p); 28 | dp3 = pow(1.0 - (dp3 - minD), p); 29 | dp4 = pow(1.0 - (dp4 - minD), p); 30 | float dpt = abs(dp1 + dp2 + dp3 + dp4); 31 | 32 | gl_FragColor = 33 | (vec4(color1 / 255.0, 1.0) * dp1 / dpt) + 34 | (vec4(color2 / 255.0, 1.0) * dp2 / dpt) + 35 | (vec4(color3 / 255.0, 1.0) * dp3 / dpt) + 36 | (vec4(color4 / 255.0, 1.0) * dp4 / dpt); 37 | } 38 | -------------------------------------------------------------------------------- /packages/twallpaper-webgl/src/hex-to-vec3.ts: -------------------------------------------------------------------------------- 1 | export type Vec3 = [r: number, g: number, b: number] 2 | 3 | export function hexToVec3(hex: string): Vec3 { 4 | if (hex.startsWith('#')) { 5 | hex = hex.slice(1) 6 | } 7 | 8 | const r = parseInt(hex.slice(0, 2), 16) 9 | const g = parseInt(hex.slice(2, 4), 16) 10 | const b = parseInt(hex.slice(4, 6), 16) 11 | 12 | return [ 13 | r, 14 | g, 15 | b 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /packages/twallpaper-webgl/src/index.ts: -------------------------------------------------------------------------------- 1 | import { TWallpaperWebGL } from './twallpaper.js' 2 | 3 | export { hexToVec3 } from './hex-to-vec3.js' 4 | 5 | export type { TWallpaperWebGLOptions } from './twallpaper.js' 6 | export type { Vec3 } from './hex-to-vec3.js' 7 | 8 | export { TWallpaperWebGL } 9 | export default TWallpaperWebGL 10 | -------------------------------------------------------------------------------- /packages/twallpaper-webgl/src/load-shaders.ts: -------------------------------------------------------------------------------- 1 | export function loadShaders( 2 | gl: WebGLRenderingContext, 3 | shaderSources: [vertexShader: string, fragmentShader: string] 4 | ): readonly [WebGLShader, WebGLShader] { 5 | const [vertexShader, fragmentShader] = shaderSources 6 | return [ 7 | createShader(gl, vertexShader, gl.VERTEX_SHADER), 8 | createShader(gl, fragmentShader, gl.FRAGMENT_SHADER)] as const 9 | } 10 | 11 | function createShader( 12 | gl: WebGLRenderingContext, 13 | shaderSource: string, 14 | shaderType: number 15 | ): WebGLShader { 16 | const shader = gl.createShader(shaderType)! 17 | gl.shaderSource(shader, shaderSource) 18 | gl.compileShader(shader) 19 | gl.getShaderParameter(shader, gl.COMPILE_STATUS) 20 | return shader 21 | } 22 | -------------------------------------------------------------------------------- /packages/twallpaper-webgl/src/style.css: -------------------------------------------------------------------------------- 1 | .tw-mask { 2 | -webkit-mask: center repeat; 3 | -webkit-mask-size: var(--tw-size) auto; 4 | -webkit-mask-image: var(--tw-image); 5 | opacity: var(--tw-opacity); 6 | } 7 | 8 | .tw-wrap { 9 | position: fixed; 10 | left: 0; 11 | top: 0; 12 | width: 100%; 13 | height: 100%; 14 | pointer-events: none; 15 | z-index: -1; 16 | background-color: var(--tw-background-color); 17 | } 18 | 19 | .tw-canvas.tw-mask + .tw-pattern { 20 | background-image: none; 21 | opacity: initial; 22 | filter: none; 23 | } 24 | 25 | .tw-canvas { 26 | top: 0; 27 | left: 0; 28 | width: 100%; 29 | height: 100%; 30 | position: absolute; 31 | } 32 | 33 | .tw-pattern { 34 | position: absolute; 35 | left: 0; 36 | top: 0; 37 | width: 100%; 38 | height: 100%; 39 | mix-blend-mode: soft-light; 40 | background: center repeat; 41 | background-size: var(--tw-size) auto; 42 | background-image: var(--tw-image); 43 | filter: blur(var(--tw-blur)); 44 | opacity: var(--tw-opacity); 45 | } 46 | -------------------------------------------------------------------------------- /packages/twallpaper-webgl/src/twallpaper.ts: -------------------------------------------------------------------------------- 1 | import { distance } from './distance.js' 2 | import fragmentShader from './fragment-shader.glsl?raw' 3 | import { loadShaders } from './load-shaders.js' 4 | import vertexShader from './vertex-shader.glsl?raw' 5 | import type { Vec3 } from './hex-to-vec3.js' 6 | 7 | const KEY_POINTS = [ 8 | [0.265, 0.582], // 0 9 | [0.176, 0.918], // 1 10 | [1 - 0.585, 1 - 0.164], // 0 11 | [0.644, 0.755], // 1 12 | [1 - 0.265, 1 - 0.582], // 0 13 | [1 - 0.176, 1 - 0.918], // 1 14 | [0.585, 0.164], // 0 15 | [1 - 0.644, 1 - 0.755] // 1 16 | 17 | ] 18 | 19 | export interface TWallpaperWebGLOptions { 20 | colors: Vec3[] 21 | mask: boolean 22 | image: string 23 | backgroundColor: string 24 | size: number 25 | opacity: number 26 | } 27 | 28 | export class TWallpaperWebGL { 29 | private gl: WebGLRenderingContext 30 | private glslProgram: WebGLProgram 31 | 32 | private options: TWallpaperWebGLOptions 33 | private gradientContainer: HTMLCanvasElement 34 | private maskContainer: HTMLDivElement 35 | 36 | private resolutionLoc: WebGLUniformLocation 37 | private color1Loc: WebGLUniformLocation 38 | private color2Loc: WebGLUniformLocation 39 | private color3Loc: WebGLUniformLocation 40 | private color4Loc: WebGLUniformLocation 41 | private color1PosLoc: WebGLUniformLocation 42 | private color2PosLoc: WebGLUniformLocation 43 | private color3PosLoc: WebGLUniformLocation 44 | private color4PosLoc: WebGLUniformLocation 45 | 46 | private targetColor1Pos: number[] 47 | private targetColor2Pos: number[] 48 | private targetColor3Pos: number[] 49 | private targetColor4Pos: number[] 50 | 51 | private color1Pos: number[] 52 | private color2Pos: number[] 53 | private color3Pos: number[] 54 | private color4Pos: number[] 55 | 56 | private keyShift = 0 57 | private speed = 0.1 58 | private animating = false 59 | 60 | constructor(private readonly container: HTMLElement) { 61 | this.render = this.render.bind(this) 62 | } 63 | 64 | updateOptions(options: Partial): void { 65 | this.options = { ...this.options, ...options } 66 | } 67 | 68 | init(options: TWallpaperWebGLOptions): void { 69 | this.updateOptions(options) 70 | 71 | this.gradientContainer = document.createElement('canvas') 72 | this.gradientContainer.classList.add('tw-canvas') 73 | 74 | this.maskContainer = document.createElement('div') 75 | this.maskContainer.classList.add('tw-pattern') 76 | 77 | this.container.classList.add('tw-wrap') 78 | this.container.append(this.gradientContainer, this.maskContainer) 79 | 80 | this.updateMask() 81 | 82 | this.gl = this.gradientContainer.getContext('webgl')! 83 | if (!this.gl) { 84 | throw new Error('WebGL not supported') 85 | } 86 | 87 | this.glslProgram = this.gl.createProgram()! 88 | if (!this.glslProgram) { 89 | throw new Error('Unable to create WebGLProgram') 90 | } 91 | 92 | const shaders = loadShaders(this.gl, [vertexShader, fragmentShader]) 93 | for (const shader of shaders) { 94 | this.gl.attachShader(this.glslProgram, shader) 95 | } 96 | 97 | this.gl.linkProgram(this.glslProgram) 98 | 99 | if (!this.gl.getProgramParameter(this.glslProgram, this.gl.LINK_STATUS)) { 100 | throw new Error('Unable to initialize the shader program.') 101 | } 102 | 103 | this.gl.useProgram(this.glslProgram) 104 | 105 | // look up where the vertex data needs to go. 106 | const positionAttributeLocation = this.gl.getAttribLocation( 107 | this.glslProgram, 108 | 'a_position' 109 | ) 110 | 111 | // Create a buffer to put three 2d clip space points in 112 | const positionBuffer = this.gl.createBuffer() 113 | 114 | // Bind it to ARRAY_BUFFER (think of it as ARRAY_BUFFER = positionBuffer) 115 | this.gl.bindBuffer(this.gl.ARRAY_BUFFER, positionBuffer) 116 | 117 | // fill it with a 2 triangles that cover clipspace 118 | this.gl.bufferData( 119 | this.gl.ARRAY_BUFFER, 120 | new Float32Array([ 121 | -1, 122 | -1, // first triangle 123 | 1, 124 | -1, 125 | -1, 126 | 1, 127 | -1, 128 | 1, // second triangle 129 | 1, 130 | -1, 131 | 1, 132 | 1 133 | 134 | ]), 135 | this.gl.STATIC_DRAW 136 | ) 137 | 138 | // Tell WebGL how to convert from clip space to pixels 139 | this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height) 140 | 141 | // Tell it to use our program (pair of shaders) 142 | this.gl.useProgram(this.glslProgram) 143 | 144 | // Turn on the attribute 145 | this.gl.enableVertexAttribArray(positionAttributeLocation) 146 | 147 | // Bind the position buffer. 148 | this.gl.bindBuffer(this.gl.ARRAY_BUFFER, positionBuffer) 149 | 150 | // Tell the attribute how to get data out of positionBuffer (ARRAY_BUFFER) 151 | this.gl.vertexAttribPointer( 152 | positionAttributeLocation, 153 | 2, // 2 components per iteration 154 | this.gl.FLOAT, // the data is 32bit floats 155 | false, // don't normalize the data 156 | 0, // 0 = move forward size * sizeof(type) each iteration to get the next position 157 | 0 // start at the beginning of the buffer 158 | ) 159 | 160 | this.resolutionLoc = this.gl.getUniformLocation( 161 | this.glslProgram, 162 | 'resolution' 163 | )! 164 | this.color1Loc = this.gl.getUniformLocation(this.glslProgram, 'color1')! 165 | this.color2Loc = this.gl.getUniformLocation(this.glslProgram, 'color2')! 166 | this.color3Loc = this.gl.getUniformLocation(this.glslProgram, 'color3')! 167 | this.color4Loc = this.gl.getUniformLocation(this.glslProgram, 'color4')! 168 | this.color1PosLoc = this.gl.getUniformLocation( 169 | this.glslProgram, 170 | 'color1Pos' 171 | )! 172 | this.color2PosLoc = this.gl.getUniformLocation( 173 | this.glslProgram, 174 | 'color2Pos' 175 | )! 176 | this.color3PosLoc = this.gl.getUniformLocation( 177 | this.glslProgram, 178 | 'color3Pos' 179 | )! 180 | this.color4PosLoc = this.gl.getUniformLocation( 181 | this.glslProgram, 182 | 'color4Pos' 183 | )! 184 | 185 | this.updateColors() 186 | 187 | this.color1Pos = [this.targetColor1Pos![0], this.targetColor1Pos![1]]! 188 | this.color2Pos = [this.targetColor2Pos![0], this.targetColor2Pos![1]]! 189 | this.color3Pos = [this.targetColor3Pos![0], this.targetColor3Pos![1]]! 190 | this.color4Pos = [this.targetColor4Pos![0], this.targetColor4Pos![1]]! 191 | 192 | this.renderGradient() 193 | } 194 | 195 | updateMask(): void { 196 | const { image, mask, opacity, size, backgroundColor } = this.options 197 | 198 | this.container.style.setProperty('--tw-opacity', `${opacity}`) 199 | this.container.style.setProperty('--tw-size', `${size}px`) 200 | this.container.style.setProperty('--tw-background-color', backgroundColor) 201 | 202 | this.container.style.setProperty('--tw-image', `url(${image})`) 203 | 204 | if (mask) { 205 | this.gradientContainer.classList.add('tw-mask') 206 | } else { 207 | this.gradientContainer.classList.remove('tw-mask') 208 | } 209 | } 210 | 211 | renderGradient(): void { 212 | this.gl.uniform2fv(this.resolutionLoc, [ 213 | this.gl.canvas.width, 214 | this.gl.canvas.height 215 | ]) 216 | this.gl.uniform3fv(this.color1Loc, this.options.colors[0]) 217 | this.gl.uniform3fv(this.color2Loc, this.options.colors[1]) 218 | this.gl.uniform3fv(this.color3Loc, this.options.colors[2]) 219 | this.gl.uniform3fv(this.color4Loc, this.options.colors[3]) 220 | this.gl.uniform2fv(this.color1PosLoc, this.color1Pos) 221 | this.gl.uniform2fv(this.color2PosLoc, this.color2Pos) 222 | this.gl.uniform2fv(this.color3PosLoc, this.color3Pos) 223 | this.gl.uniform2fv(this.color4PosLoc, this.color4Pos) 224 | 225 | this.gl.drawArrays( 226 | this.gl.TRIANGLES, 227 | 0, // offset 228 | 6 // num vertices to process 229 | ) 230 | } 231 | 232 | animate(): void { 233 | this.updateColors() 234 | if (!this.animating) { 235 | requestAnimationFrame(this.render) 236 | } 237 | } 238 | 239 | private updateColors(): void { 240 | this.targetColor1Pos = KEY_POINTS[this.keyShift % 8] 241 | this.targetColor2Pos = KEY_POINTS[(this.keyShift + 2) % 8] 242 | this.targetColor3Pos = KEY_POINTS[(this.keyShift + 4) % 8] 243 | this.targetColor4Pos = KEY_POINTS[(this.keyShift + 6) % 8] 244 | this.keyShift = (this.keyShift + 1) % 8 245 | } 246 | 247 | private render(): void { 248 | this.animating = true 249 | 250 | if ( 251 | distance(this.color1Pos, this.targetColor1Pos) > 0.01 || 252 | distance(this.color2Pos, this.targetColor2Pos) > 0.01 || 253 | distance(this.color3Pos, this.targetColor3Pos) > 0.01 || 254 | distance(this.color3Pos, this.targetColor3Pos) > 0.01 255 | ) { 256 | this.color1Pos[0] = 257 | this.color1Pos[0] * (1 - this.speed) + 258 | this.targetColor1Pos[0] * this.speed 259 | this.color1Pos[1] = 260 | this.color1Pos[1] * (1 - this.speed) + 261 | this.targetColor1Pos[1] * this.speed 262 | this.color2Pos[0] = 263 | this.color2Pos[0] * (1 - this.speed) + 264 | this.targetColor2Pos[0] * this.speed 265 | this.color2Pos[1] = 266 | this.color2Pos[1] * (1 - this.speed) + 267 | this.targetColor2Pos[1] * this.speed 268 | this.color3Pos[0] = 269 | this.color3Pos[0] * (1 - this.speed) + 270 | this.targetColor3Pos[0] * this.speed 271 | this.color3Pos[1] = 272 | this.color3Pos[1] * (1 - this.speed) + 273 | this.targetColor3Pos[1] * this.speed 274 | this.color4Pos[0] = 275 | this.color4Pos[0] * (1 - this.speed) + 276 | this.targetColor4Pos[0] * this.speed 277 | this.color4Pos[1] = 278 | this.color4Pos[1] * (1 - this.speed) + 279 | this.targetColor4Pos[1] * this.speed 280 | this.renderGradient() 281 | requestAnimationFrame(this.render) 282 | } else { 283 | this.animating = false 284 | } 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /packages/twallpaper-webgl/src/vertex-shader.glsl: -------------------------------------------------------------------------------- 1 | attribute vec4 a_position; 2 | 3 | void main() { 4 | gl_Position = a_position; 5 | } 6 | -------------------------------------------------------------------------------- /packages/twallpaper-webgl/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/twallpaper-webgl/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@crashmax/tsconfig", 3 | "compilerOptions": { 4 | "moduleResolution": "node", 5 | "strictNullChecks": false, 6 | "outDir": "dist" 7 | }, 8 | "include": [ 9 | "src/**/*" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/twallpaper-webgl/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { copyFile } from 'node:fs/promises' 2 | import { resolve } from 'node:path' 3 | import { defineConfig } from 'vite' 4 | import dts from 'vite-plugin-dts' 5 | 6 | export default defineConfig({ 7 | plugins: [ 8 | dts({ 9 | insertTypesEntry: true, 10 | async afterBuild() { 11 | try { 12 | await copyFile('src/style.css', 'dist/style.css') 13 | } catch (err) { 14 | console.log(err) 15 | process.exit(1) 16 | } 17 | } 18 | }) 19 | ], 20 | build: { 21 | target: 'esnext', 22 | lib: { 23 | entry: resolve(__dirname, 'src/index.ts'), 24 | name: 'TWallpaperWebGL', 25 | formats: [ 26 | 'cjs', 27 | 'es', 28 | 'umd' 29 | ], 30 | fileName: (format) => `index.${format}.js` 31 | }, 32 | rollupOptions: { 33 | output: { 34 | exports: 'named' 35 | } 36 | } 37 | } 38 | }) 39 | -------------------------------------------------------------------------------- /packages/twallpaper/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # twallpaper 2 | 3 | ## 2.1.2 4 | 5 | ### Patch Changes 6 | 7 | - fix: initialize animate 8 | 9 | ## 2.1.1 10 | 11 | ### Patch Changes 12 | 13 | - feat(workspace): add `@changesets/cli` 14 | ci(npm): add `--provenance` 15 | -------------------------------------------------------------------------------- /packages/twallpaper/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 | 5 |
6 |

TWallpaper

7 | 8 |

9 | 10 |

11 | 🌈 Multicolor gradient wallpaper created algorithmically and shimmers smoothly. 12 |

13 | 14 |

15 | 16 | npm 17 | 18 | 19 | npm 20 | 21 | 22 | npm bundle size 23 | 24 |

25 | 26 | ## Installation 27 | 28 | ```sh 29 | npm install twallpaper 30 | ``` 31 | 32 | ```sh 33 | yarn add twallpaper 34 | ``` 35 | 36 | ```sh 37 | pnpm add twallpaper 38 | ``` 39 | -------------------------------------------------------------------------------- /packages/twallpaper/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twallpaper", 3 | "version": "2.1.2", 4 | "description": "🌈 Multicolor gradient wallpaper created algorithmically and shimmers smoothly.", 5 | "type": "module", 6 | "types": "./dist/index.d.ts", 7 | "main": "./dist/index.cjs.js", 8 | "jsdelivr": "./dist/index.umd.js", 9 | "unpkg": "./dist/index.umd.js", 10 | "module": "./dist/index.es.js", 11 | "exports": { 12 | ".": { 13 | "require": "./dist/index.cjs.js", 14 | "import": "./dist/index.es.js" 15 | }, 16 | "./dist/style.css": "./dist/style.css", 17 | "./css": "./dist/style.css" 18 | }, 19 | "files": [ 20 | "dist" 21 | ], 22 | "license": "MIT", 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/crashmax-dev/twallpaper.git" 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/crashmax-dev/twallpaper/issues" 29 | }, 30 | "homepage": "https://twallpaper.js.org", 31 | "keywords": [ 32 | "react", 33 | "multicolor", 34 | "gradient", 35 | "wallpaper", 36 | "background", 37 | "animation", 38 | "telegram", 39 | "canvas" 40 | ], 41 | "scripts": { 42 | "dev": "vite build --watch", 43 | "build": "vite build" 44 | }, 45 | "devDependencies": { 46 | "typescript": "^5.0.4", 47 | "vite": "^4.3.1" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/twallpaper/src/colors.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/zero-dependency/utils/blob/master/src/hex.ts 2 | 3 | export interface Rgb { 4 | r: number 5 | g: number 6 | b: number 7 | } 8 | 9 | /** 10 | * Convert hex color string to rgb color object 11 | * @param hex hex color string 12 | * @returns rgb color object 13 | */ 14 | export function hexToRgb(hex: string): Rgb | null { 15 | const result = isHexColor(hex) 16 | 17 | // prettier-ignore 18 | return result ? { 19 | r: parseInt(result[1]!, 16), 20 | g: parseInt(result[2]!, 16), 21 | b: parseInt(result[3]!, 16) 22 | } : null 23 | } 24 | 25 | /** 26 | * Check if hex color string is valid 27 | * @param hex hex color string 28 | * @returns `RegExpExecArray` if hex is valid, `null` otherwise 29 | */ 30 | function isHexColor(hex: string): RegExpExecArray | null { 31 | if (hex.length === 4) { 32 | hex = `${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}` 33 | } 34 | return /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) 35 | } 36 | -------------------------------------------------------------------------------- /packages/twallpaper/src/constants.ts: -------------------------------------------------------------------------------- 1 | import type { Position } from './types.js' 2 | 3 | // prettier-ignore 4 | export const curve: number[] = [ 5 | 0, 0.25, 0.50, 0.75, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5, 6, 7, 8, 9, 10, 11, 12, 6 | 13, 14, 15, 16, 17, 18, 18.3, 18.6, 18.9, 19.2, 19.5, 19.8, 20.1, 20.4, 20.7, 7 | 21, 21.3, 21.6, 21.9, 22.2, 22.5, 22.8, 23.1, 23.4, 23.7, 24.0, 24.3, 24.6, 8 | 24.9, 25.2, 25.5, 25.8, 26.1, 26.3, 26.4, 26.5, 26.6, 26.7, 26.8, 26.9, 27 9 | ] 10 | 11 | export const positions: Position[] = [ 12 | { x: 0.8, y: 0.1 }, 13 | { x: 0.6, y: 0.2 }, 14 | { x: 0.35, y: 0.25 }, 15 | { x: 0.25, y: 0.6 }, 16 | { x: 0.2, y: 0.9 }, 17 | { x: 0.4, y: 0.8 }, 18 | { x: 0.65, y: 0.75 }, 19 | { x: 0.75, y: 0.4 } 20 | ] 21 | -------------------------------------------------------------------------------- /packages/twallpaper/src/index.ts: -------------------------------------------------------------------------------- 1 | import { TWallpaper } from './twallpaper.js' 2 | 3 | export type { PatternOptions, TWallpaperOptions } from './types.js' 4 | 5 | export { TWallpaper } 6 | export default TWallpaper 7 | -------------------------------------------------------------------------------- /packages/twallpaper/src/style.css: -------------------------------------------------------------------------------- 1 | .tw-mask { 2 | -webkit-mask: center repeat; 3 | -webkit-mask-size: var(--tw-size) auto; 4 | -webkit-mask-image: var(--tw-image); 5 | opacity: var(--tw-opacity); 6 | } 7 | 8 | .tw-wrap { 9 | position: fixed; 10 | left: 0; 11 | top: 0; 12 | width: 100%; 13 | height: 100%; 14 | pointer-events: none; 15 | z-index: -1; 16 | background: var(--tw-background); 17 | } 18 | 19 | .tw-canvas.tw-mask + .tw-pattern { 20 | background-image: none; 21 | opacity: initial; 22 | filter: none; 23 | } 24 | 25 | .tw-canvas { 26 | top: 0; 27 | left: 0; 28 | width: 100%; 29 | height: 100%; 30 | position: absolute; 31 | } 32 | 33 | .tw-pattern { 34 | position: absolute; 35 | left: 0; 36 | top: 0; 37 | width: 100%; 38 | height: 100%; 39 | mix-blend-mode: overlay; 40 | background: center repeat; 41 | background-size: var(--tw-size) auto; 42 | background-image: var(--tw-image); 43 | filter: blur(var(--tw-blur)); 44 | opacity: var(--tw-opacity); 45 | } 46 | -------------------------------------------------------------------------------- /packages/twallpaper/src/twallpaper.ts: -------------------------------------------------------------------------------- 1 | import { hexToRgb } from './colors.js' 2 | import { curve, positions } from './constants.js' 3 | import type { 4 | PatternOptions, 5 | Position, 6 | RgbColor, 7 | TWallpaperOptions 8 | } from './types.js' 9 | 10 | export class TWallpaper { 11 | private width = 50 12 | private height = 50 13 | private phase = 0 14 | private tail = 0 15 | private tails: number // 90 16 | private scrollTails = 50 17 | private timestamp: number 18 | private frametime: number // 1000 / 15 19 | private scrollDelta = 0 20 | private scrollTicking = false 21 | private frames: ImageData[] = [] 22 | private rgb: RgbColor[] = [] 23 | private curve = curve 24 | private positions = positions 25 | private phases = positions.length 26 | 27 | private interval: ReturnType | null 28 | private raf: ReturnType | null 29 | private wheel: (event: WheelEvent) => void 30 | 31 | private hc: HTMLCanvasElement 32 | private hctx: CanvasRenderingContext2D 33 | private canvas: HTMLCanvasElement 34 | private ctx: CanvasRenderingContext2D 35 | private pattern: HTMLDivElement | null 36 | 37 | constructor( 38 | private container?: HTMLElement, 39 | private options?: TWallpaperOptions 40 | ) { 41 | this.wheel = this.onWheel.bind(this) 42 | } 43 | 44 | private getPositions(shift: number): Position[] { 45 | const positions = [...this.positions] 46 | while (shift > 0) { 47 | positions.push(positions.shift()!) 48 | shift-- 49 | } 50 | 51 | const result = [] 52 | for (let i = 0; i < positions.length; i += 2) { 53 | result.push(positions[i]) 54 | } 55 | 56 | return result 57 | } 58 | 59 | private curPosition(phase: number, tail: number): Position[] { 60 | tail %= this.tails 61 | const pos = this.getPositions(phase % this.phases) 62 | 63 | if (tail) { 64 | const next_pos = this.getPositions(++phase % this.phases) 65 | const d1x = (next_pos[0].x - pos[0].x) / this.tails 66 | const d1y = (next_pos[0].y - pos[0].y) / this.tails 67 | const d2x = (next_pos[1].x - pos[1].x) / this.tails 68 | const d2y = (next_pos[1].y - pos[1].y) / this.tails 69 | const d3x = (next_pos[2].x - pos[2].x) / this.tails 70 | const d3y = (next_pos[2].y - pos[2].y) / this.tails 71 | const d4x = (next_pos[3].x - pos[3].x) / this.tails 72 | const d4y = (next_pos[3].y - pos[3].y) / this.tails 73 | 74 | return [ 75 | { 76 | x: pos[0].x + d1x * tail, 77 | y: pos[0].y + d1y * tail 78 | }, 79 | { 80 | x: pos[1].x + d2x * tail, 81 | y: pos[1].y + d2y * tail 82 | }, 83 | { 84 | x: pos[2].x + d3x * tail, 85 | y: pos[2].y + d3y * tail 86 | }, 87 | { 88 | x: pos[3].x + d4x * tail, 89 | y: pos[3].y + d4y * tail 90 | } 91 | ] 92 | } 93 | 94 | return pos 95 | } 96 | 97 | private changeTail(diff: number): void { 98 | this.tail += diff 99 | 100 | while (this.tail >= this.tails) { 101 | this.tail -= this.tails 102 | this.phase++ 103 | 104 | if (this.phase >= this.phases) { 105 | this.phase -= this.phases 106 | } 107 | } 108 | 109 | while (this.tail < 0) { 110 | this.tail += this.tails 111 | this.phase-- 112 | 113 | if (this.phase < 0) { 114 | this.phase += this.phases 115 | } 116 | } 117 | } 118 | 119 | private onWheel(event: WheelEvent): void { 120 | if (this.interval) { 121 | return 122 | } 123 | 124 | this.scrollDelta += event.deltaY 125 | 126 | if (!this.scrollTicking) { 127 | requestAnimationFrame(() => this.drawOnWheel()) 128 | this.scrollTicking = true 129 | } 130 | } 131 | 132 | private drawOnWheel(): void { 133 | let diff = this.scrollDelta / this.scrollTails 134 | this.scrollDelta %= this.scrollTails 135 | 136 | diff = diff > 0 ? Math.floor(diff) : Math.ceil(diff) 137 | 138 | if (diff) { 139 | this.changeTail(diff) 140 | this.drawGradient() 141 | } 142 | 143 | this.scrollTicking = false 144 | } 145 | 146 | private drawNextPositionAnimated(callback?: () => void): void { 147 | if (this.frames.length > 0) { 148 | const id = this.frames.shift()! 149 | this.drawImageData(id) 150 | } else { 151 | clearInterval(this.interval!) 152 | this.interval = null 153 | 154 | if (callback) { 155 | callback() 156 | } 157 | } 158 | } 159 | 160 | private getGradientImageData(positions: Position[]): ImageData { 161 | const id = this.hctx.createImageData(this.width, this.height) 162 | const pixels = id.data 163 | let offset = 0 164 | 165 | for (let y = 0; y < this.height; y++) { 166 | const directPixelY = y / this.height 167 | const centerDistanceY = directPixelY - 0.5 168 | const centerDistanceY2 = centerDistanceY * centerDistanceY 169 | 170 | for (let x = 0; x < this.width; x++) { 171 | const directPixelX = x / this.width 172 | 173 | const centerDistanceX = directPixelX - 0.5 174 | const centerDistance = Math.sqrt( 175 | centerDistanceX * centerDistanceX + centerDistanceY2 176 | ) 177 | 178 | const swirlFactor = 0.35 * centerDistance 179 | const theta = swirlFactor * swirlFactor * 0.8 * 8 180 | const sinTheta = Math.sin(theta) 181 | const cosTheta = Math.cos(theta) 182 | 183 | const pixelX = Math.max( 184 | 0, 185 | Math.min( 186 | 1, 187 | 0.5 + centerDistanceX * cosTheta - centerDistanceY * sinTheta 188 | ) 189 | ) 190 | const pixelY = Math.max( 191 | 0, 192 | Math.min( 193 | 1, 194 | 0.5 + centerDistanceX * sinTheta + centerDistanceY * cosTheta 195 | ) 196 | ) 197 | 198 | let distanceSum = 0 199 | let r = 0 200 | let g = 0 201 | let b = 0 202 | 203 | for (let i = 0; i < this.rgb.length; i++) { 204 | const colorX = positions[i].x 205 | const colorY = positions[i].y 206 | 207 | const distanceX = pixelX - colorX 208 | const distanceY = pixelY - colorY 209 | 210 | let distance = Math.max( 211 | 0, 212 | 0.9 - Math.sqrt(distanceX * distanceX + distanceY * distanceY) 213 | ) 214 | distance = distance * distance * distance * distance 215 | distanceSum += distance 216 | 217 | r += (distance * this.rgb[i].r) / 255 218 | g += (distance * this.rgb[i].g) / 255 219 | b += (distance * this.rgb[i].b) / 255 220 | } 221 | 222 | pixels[offset++] = (r / distanceSum) * 255 223 | pixels[offset++] = (g / distanceSum) * 255 224 | pixels[offset++] = (b / distanceSum) * 255 225 | pixels[offset++] = 0xff // 255 226 | } 227 | } 228 | 229 | return id 230 | } 231 | 232 | private drawImageData(id: ImageData): void { 233 | this.hctx.putImageData(id, 0, 0) 234 | this.ctx.drawImage(this.hc, 0, 0, this.width, this.height) 235 | } 236 | 237 | private drawGradient(): void { 238 | const position = this.curPosition(this.phase, this.tail) 239 | this.drawImageData(this.getGradientImageData(position)) 240 | } 241 | 242 | private requestAnimate(): void { 243 | this.raf = requestAnimationFrame(() => this.doAnimate()) 244 | } 245 | 246 | private doAnimate(): void { 247 | const now = +Date.now() 248 | 249 | if (now - this.timestamp < this.frametime) { 250 | return this.requestAnimate() 251 | } 252 | 253 | this.timestamp = now 254 | this.changeTail(1) 255 | this.drawGradient() 256 | this.requestAnimate() 257 | } 258 | 259 | /** 260 | * Initialize wallpaper 261 | * @param options wallpaper options 262 | * @param container container element 263 | */ 264 | init(options?: TWallpaperOptions, container?: HTMLElement): void { 265 | this.options = options ? { ...this.options, ...options } : this.options 266 | this.container = container ?? this.container 267 | 268 | if (!this.container || !this.options.colors.length) { 269 | throw new Error('Container or colors do not exist') 270 | } 271 | 272 | this.dispose() 273 | 274 | if (!this.hc) { 275 | this.hc = document.createElement('canvas') 276 | this.hc.width = this.width 277 | this.hc.height = this.height 278 | this.hctx = this.hc.getContext('2d')! 279 | } 280 | 281 | this.canvas = document.createElement('canvas') 282 | this.canvas.classList.add('tw-canvas') 283 | this.canvas.width = this.width 284 | this.canvas.height = this.height 285 | this.ctx = this.canvas.getContext('2d')! 286 | this.container.appendChild(this.canvas) 287 | 288 | if (!this.container.classList.contains('tw-wrap')) { 289 | this.container.classList.add('tw-wrap') 290 | } 291 | 292 | if (this.options.pattern) { 293 | this.pattern = document.createElement('div') 294 | this.pattern.classList.add('tw-pattern') 295 | this.updatePattern(this.options.pattern) 296 | this.container.appendChild(this.pattern) 297 | } 298 | 299 | this.animate(this.options.animate) 300 | this.updateTails(this.options.tails) 301 | this.updateColors(this.options.colors) 302 | this.updateFrametime(this.options.fps) 303 | this.scrollAnimate(this.options.scrollAnimate) 304 | this.drawGradient() 305 | } 306 | 307 | /** 308 | * Dispose wallpaper 309 | */ 310 | dispose(): void { 311 | if (this.hc) { 312 | clearInterval(this.interval!) 313 | this.interval = null 314 | this.animate(false) 315 | this.canvas.remove() 316 | this.pattern?.remove() 317 | this.hc.remove() 318 | this.frames = [] 319 | } 320 | } 321 | 322 | /** 323 | * Tails speed animation 324 | * @param tails number of tails 325 | * @default 90 326 | */ 327 | updateTails(tails = 90): void { 328 | if (tails > 0) { 329 | this.tails = tails 330 | } 331 | } 332 | 333 | /** 334 | * Frame time is just the time between frames 335 | * @param fps frames per second 336 | * @default 30 337 | */ 338 | updateFrametime(fps = 30): void { 339 | this.frametime = 1000 / fps 340 | } 341 | 342 | /** 343 | * Update pattern options 344 | * @param pattern pattern options 345 | */ 346 | updatePattern(pattern: PatternOptions): void { 347 | if (!this.pattern || !this.container) return 348 | 349 | const { 350 | size = 'auto', 351 | opacity = 0.5, 352 | blur = 0, 353 | background = '#000', 354 | image, 355 | mask 356 | } = pattern 357 | 358 | this.container.style.setProperty('--tw-size', size) 359 | this.container.style.setProperty('--tw-opacity', `${opacity}`) 360 | this.container.style.setProperty('--tw-blur', `${blur}px`) 361 | this.container.style.setProperty('--tw-background', background) 362 | 363 | if (image) { 364 | this.container.style.setProperty('--tw-image', `url(${image})`) 365 | } else { 366 | this.container.style.removeProperty('--tw-image') 367 | } 368 | 369 | if (mask) { 370 | this.canvas.classList.add('tw-mask') 371 | } else { 372 | this.canvas.classList.remove('tw-mask') 373 | } 374 | } 375 | 376 | /** 377 | * Colors for gradient, use 1-4 hex codes 378 | * @param hexCodes hex colors 379 | */ 380 | updateColors(hexCodes: string[]): void { 381 | const colors = hexCodes 382 | .reduce((rgbColors, color) => { 383 | const rgb = hexToRgb(color) 384 | 385 | if (rgb) { 386 | rgbColors.push(rgb) 387 | } 388 | 389 | return rgbColors 390 | }, []) 391 | .slice(0, 4) 392 | 393 | if (!colors.length) { 394 | throw new Error( 395 | 'Colors do not exist or are not valid hex codes (e.g. #fff or #ffffff)' 396 | ) 397 | } 398 | 399 | this.rgb = colors 400 | } 401 | 402 | /** 403 | * Next animation position (animation turns off after use) 404 | * @param callback execution `toNextPosition` is finished 405 | */ 406 | toNextPosition(callback?: () => void): void { 407 | clearInterval(this.interval!) 408 | this.animate(false) 409 | this.frames = [] 410 | 411 | const prev_pos = this.getPositions(this.phase % this.phases) 412 | this.phase++ 413 | const pos = this.getPositions(this.phase % this.phases) 414 | 415 | const h = 27 416 | const d1x = (pos[0].x - prev_pos[0].x) / h 417 | const d1y = (pos[0].y - prev_pos[0].y) / h 418 | const d2x = (pos[1].x - prev_pos[1].x) / h 419 | const d2y = (pos[1].y - prev_pos[1].y) / h 420 | const d3x = (pos[2].x - prev_pos[2].x) / h 421 | const d3y = (pos[2].y - prev_pos[2].y) / h 422 | const d4x = (pos[3].x - prev_pos[3].x) / h 423 | const d4y = (pos[3].y - prev_pos[3].y) / h 424 | 425 | for (let frame = 0; frame < this.curve.length; frame++) { 426 | const cur_pos: Position[] = [ 427 | { 428 | x: prev_pos[0].x + d1x * this.curve[frame], 429 | y: prev_pos[0].y + d1y * this.curve[frame] 430 | }, 431 | { 432 | x: prev_pos[1].x + d2x * this.curve[frame], 433 | y: prev_pos[1].y + d2y * this.curve[frame] 434 | }, 435 | { 436 | x: prev_pos[2].x + d3x * this.curve[frame], 437 | y: prev_pos[2].y + d3y * this.curve[frame] 438 | }, 439 | { 440 | x: prev_pos[3].x + d4x * this.curve[frame], 441 | y: prev_pos[3].y + d4y * this.curve[frame] 442 | } 443 | ] 444 | 445 | this.frames.push(this.getGradientImageData(cur_pos)) 446 | } 447 | 448 | this.interval = setInterval(() => { 449 | this.drawNextPositionAnimated(callback) 450 | }, this.frametime) 451 | } 452 | 453 | /** 454 | * Start or stop animation 455 | * @param start start or stop animation 456 | * @default true 457 | */ 458 | animate(start = true): void { 459 | if (start) { 460 | this.doAnimate() 461 | } else if (this.raf) { 462 | cancelAnimationFrame(this.raf) 463 | this.raf = null 464 | } 465 | } 466 | 467 | /** 468 | * Start or stop mouse scroll animation 469 | * @param start start or stop scroll animation 470 | * @default false 471 | */ 472 | scrollAnimate(start = false): void { 473 | if (start) { 474 | document.addEventListener('wheel', this.wheel) 475 | } else { 476 | document.removeEventListener('wheel', this.wheel) 477 | } 478 | } 479 | } 480 | -------------------------------------------------------------------------------- /packages/twallpaper/src/types.ts: -------------------------------------------------------------------------------- 1 | export interface Position { 2 | x: number 3 | y: number 4 | } 5 | 6 | export interface RgbColor { 7 | r: number 8 | g: number 9 | b: number 10 | } 11 | 12 | export interface PatternOptions { 13 | image?: string 14 | mask?: boolean 15 | background?: string 16 | blur?: number 17 | size?: string 18 | opacity?: number 19 | } 20 | 21 | export interface TWallpaperOptions { 22 | colors: string[] 23 | fps?: number 24 | tails?: number 25 | animate?: boolean 26 | scrollAnimate?: boolean 27 | pattern?: PatternOptions 28 | } 29 | -------------------------------------------------------------------------------- /packages/twallpaper/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/twallpaper/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@crashmax/tsconfig", 3 | "compilerOptions": { 4 | "moduleResolution": "node", 5 | "strictNullChecks": false, 6 | "outDir": "dist" 7 | }, 8 | "include": [ 9 | "src/**/*" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/twallpaper/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { appendFile, copyFile, readFile, writeFile } from 'node:fs/promises' 2 | import { resolve } from 'node:path' 3 | import { defineConfig } from 'vite' 4 | import dts from 'vite-plugin-dts' 5 | import { description, homepage, name, version } from './package.json' 6 | 7 | export default defineConfig({ 8 | plugins: [ 9 | dts({ 10 | insertTypesEntry: true, 11 | async afterBuild() { 12 | try { 13 | await copyFile('src/style.css', 'dist/style.css') 14 | const umdFile = await readFile('dist/index.umd.js') 15 | await writeFile( 16 | 'dist/index.umd.js', 17 | `/**\n * name: ${name}` + 18 | `\n * description: ${description}` + 19 | `\n * version: ${version}` + 20 | `\n * homepage: ${homepage}` + 21 | '\n */\n' 22 | ) 23 | await appendFile('dist/index.umd.js', umdFile) 24 | } catch (err) { 25 | console.log(err) 26 | process.exit(1) 27 | } 28 | } 29 | }) 30 | ], 31 | build: { 32 | target: 'esnext', 33 | lib: { 34 | entry: resolve(__dirname, 'src/index.ts'), 35 | name: 'TWallpaper', 36 | formats: [ 37 | 'cjs', 38 | 'es', 39 | 'umd' 40 | ], 41 | fileName: (format) => `index.${format}.js` 42 | }, 43 | rollupOptions: { 44 | output: { 45 | exports: 'named' 46 | } 47 | } 48 | } 49 | }) 50 | -------------------------------------------------------------------------------- /packages/vue/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @twallpaper/vue 2 | 3 | ## 2.1.2 4 | 5 | ### Patch Changes 6 | 7 | - fix: initialize animate 8 | - Updated dependencies 9 | - twallpaper@2.1.2 10 | 11 | ## 2.1.1 12 | 13 | ### Patch Changes 14 | 15 | - feat(workspace): add `@changesets/cli` 16 | ci(npm): add `--provenance` 17 | - Updated dependencies 18 | - twallpaper@2.1.1 19 | -------------------------------------------------------------------------------- /packages/vue/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 | 5 |
6 |

TWallpaper

7 | 8 |

9 | 10 |

11 | 🌈 Multicolor gradient wallpaper created algorithmically and shimmers smoothly. 12 |

13 | 14 |

15 | 16 | npm 17 | 18 | 19 | npm 20 | 21 | 22 | npm bundle size 23 | 24 |

25 | 26 | ## Installation 27 | 28 | ```sh 29 | npm install @twallpaper/vue 30 | ``` 31 | 32 | ```sh 33 | yarn add @twallpaper/vue 34 | ``` 35 | 36 | ```sh 37 | pnpm add @twallpaper/vue 38 | ``` 39 | -------------------------------------------------------------------------------- /packages/vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@twallpaper/vue", 3 | "version": "2.1.2", 4 | "description": "🌈 Multicolor gradient wallpaper created algorithmically and shimmers smoothly.", 5 | "type": "module", 6 | "types": "./dist/index.d.ts", 7 | "main": "./dist/index.cjs", 8 | "module": "./dist/index.js", 9 | "exports": { 10 | ".": { 11 | "import": "./dist/index.js", 12 | "require": "./dist/index.cjs" 13 | }, 14 | "./dist/style.css": "./dist/style.css", 15 | "./css": "./dist/style.css" 16 | }, 17 | "files": [ 18 | "dist" 19 | ], 20 | "license": "MIT", 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/crashmax-dev/twallpaper.git" 24 | }, 25 | "bugs": { 26 | "url": "https://github.com/crashmax-dev/twallpaper/issues" 27 | }, 28 | "homepage": "https://twallpaper.js.org", 29 | "keywords": [ 30 | "vue", 31 | "react", 32 | "multicolor", 33 | "gradient", 34 | "wallpaper", 35 | "background", 36 | "animation", 37 | "telegram", 38 | "canvas" 39 | ], 40 | "scripts": { 41 | "dev": "vite build --watch", 42 | "build": "vite build && pnpm types", 43 | "types": "vue-tsc --declaration --emitDeclarationOnly" 44 | }, 45 | "dependencies": { 46 | "twallpaper": "workspace:2.1.2", 47 | "vue": "^3.2.47" 48 | }, 49 | "devDependencies": { 50 | "@vitejs/plugin-vue": "^4.1.0", 51 | "typescript": "^5.0.4", 52 | "vite": "^4.3.1", 53 | "vue-tsc": "^1.4.4" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/vue/src/TWallpaper.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 37 | -------------------------------------------------------------------------------- /packages/vue/src/index.ts: -------------------------------------------------------------------------------- 1 | import TWallpaper from './TWallpaper.vue' 2 | import type { TWallpaperOptions } from 'twallpaper' 3 | 4 | export { TWallpaper } 5 | export default TWallpaper 6 | export type { TWallpaperOptions } 7 | -------------------------------------------------------------------------------- /packages/vue/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue' 5 | const component: DefineComponent<{}, {}, any> 6 | export default component 7 | } 8 | -------------------------------------------------------------------------------- /packages/vue/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@crashmax/tsconfig", 3 | "compilerOptions": { 4 | "jsx": "preserve", 5 | "outDir": "dist", 6 | "noEmitOnError": false 7 | }, 8 | "include": [ 9 | "src" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/vue/vite.config.ts: -------------------------------------------------------------------------------- 1 | import vue from '@vitejs/plugin-vue' 2 | import { resolve } from 'path' 3 | import { defineConfig } from 'vite' 4 | import { description, homepage, name, version } from './package.json' 5 | 6 | export default defineConfig({ 7 | plugins: [vue()], 8 | esbuild: { 9 | banner: 10 | `/**\n * name: ${name}` + 11 | `\n * description: ${description}` + 12 | `\n * version: ${version}` + 13 | `\n * homepage: ${homepage}` + 14 | '\n */' 15 | }, 16 | build: { 17 | target: 'esnext', 18 | lib: { 19 | entry: resolve(__dirname, 'src/index.ts'), 20 | name: 'TWallpaper', 21 | formats: ['es', 'cjs'], 22 | fileName: 'index' 23 | }, 24 | rollupOptions: { 25 | external: ['vue', 'twallpaper'], 26 | output: { 27 | exports: 'named', 28 | globals: { 29 | vue: 'Vue', 30 | twallpaper: 'TWallpaper' 31 | } 32 | } 33 | } 34 | } 35 | }) 36 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'examples/*' 3 | - 'packages/*' 4 | - 'website' 5 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turborepo.org/schema.json", 3 | "baseBranch": "origin/master", 4 | "pipeline": { 5 | "build": { 6 | "dependsOn": [ 7 | "^build" 8 | ], 9 | "outputs": [ 10 | "dist/**" 11 | ] 12 | }, 13 | "dev": { 14 | "dependsOn": [ 15 | "^build" 16 | ], 17 | "cache": false 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /website/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Telegram Wallpaper 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website", 3 | "type": "module", 4 | "version": "0.0.0", 5 | "private": true, 6 | "scripts": { 7 | "dev": "vite --open", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@twallpaper/webgl": "workspace:*", 13 | "@tweakpane/core": "^1.1.8", 14 | "lodash.clonedeep": "^4.5.0", 15 | "lodash.merge": "^4.6.2", 16 | "twallpaper": "workspace:*", 17 | "tweakpane": "^3.1.9" 18 | }, 19 | "devDependencies": { 20 | "@types/lodash.clonedeep": "^4.5.7", 21 | "@types/lodash.merge": "^4.6.7", 22 | "vite": "^4.3.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /website/public/CNAME: -------------------------------------------------------------------------------- 1 | twallpaper.js.org 2 | -------------------------------------------------------------------------------- /website/public/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0px; 3 | padding: 0px; 4 | user-select: none; 5 | } 6 | 7 | /* https://cocopon.github.io/tweakpane/theming.html */ 8 | 9 | :root { 10 | --tp-base-background-color: hsla(0, 0%, 10%, 0.8); 11 | --tp-base-shadow-color: hsla(0, 0%, 0%, 0.2); 12 | --tp-button-background-color: hsla(0, 0%, 80%, 1); 13 | --tp-button-background-color-active: hsla(0, 0%, 100%, 1); 14 | --tp-button-background-color-focus: hsla(0, 0%, 95%, 1); 15 | --tp-button-background-color-hover: hsla(0, 0%, 85%, 1); 16 | --tp-button-foreground-color: hsla(0, 0%, 0%, 0.8); 17 | --tp-container-background-color: hsla(0, 0%, 0%, 0.3); 18 | --tp-container-background-color-active: hsla(0, 0%, 0%, 0.6); 19 | --tp-container-background-color-focus: hsla(0, 0%, 0%, 0.5); 20 | --tp-container-background-color-hover: hsla(0, 0%, 0%, 0.4); 21 | --tp-container-foreground-color: hsla(0, 0%, 100%, 0.5); 22 | --tp-groove-foreground-color: hsla(0, 0%, 0%, 0.2); 23 | --tp-input-background-color: hsla(0, 0%, 0%, 0.3); 24 | --tp-input-background-color-active: hsla(0, 0%, 0%, 0.6); 25 | --tp-input-background-color-focus: hsla(0, 0%, 0%, 0.5); 26 | --tp-input-background-color-hover: hsla(0, 0%, 0%, 0.4); 27 | --tp-input-foreground-color: hsla(0, 0%, 100%, 0.5); 28 | --tp-label-foreground-color: hsla(0, 0%, 100%, 0.5); 29 | --tp-monitor-background-color: hsla(0, 0%, 0%, 0.3); 30 | --tp-monitor-foreground-color: hsla(0, 0%, 100%, 0.3); 31 | } 32 | 33 | :root .tp-dfwv { 34 | width: 300px; 35 | padding-bottom: 8px; 36 | } 37 | -------------------------------------------------------------------------------- /website/public/twallpaper-original.js: -------------------------------------------------------------------------------- 1 | var TWallpaper = (function () { 2 | var _width = 50 3 | var _height = 50 4 | var _phase = 0 5 | var _tail = 0 6 | var _tails = 90 7 | var _scrolltails = 50 8 | var _ts = 0 9 | var _fps = 15 10 | var _frametime = 1000 / _fps 11 | var _frames = [] 12 | var _interval = null 13 | var _raf = null 14 | var _colors = [] 15 | var _curve = [ 16 | 0, 17 | 0.25, 18 | 0.5, 19 | 0.75, 20 | 1, 21 | 1.5, 22 | 2, 23 | 2.5, 24 | 3, 25 | 3.5, 26 | 4, 27 | 5, 28 | 6, 29 | 7, 30 | 8, 31 | 9, 32 | 10, 33 | 11, 34 | 12, 35 | 13, 36 | 14, 37 | 15, 38 | 16, 39 | 17, 40 | 18, 41 | 18.3, 42 | 18.6, 43 | 18.9, 44 | 19.2, 45 | 19.5, 46 | 19.8, 47 | 20.1, 48 | 20.4, 49 | 20.7, 50 | 21.0, 51 | 21.3, 52 | 21.6, 53 | 21.9, 54 | 22.2, 55 | 22.5, 56 | 22.8, 57 | 23.1, 58 | 23.4, 59 | 23.7, 60 | 24.0, 61 | 24.3, 62 | 24.6, 63 | 24.9, 64 | 25.2, 65 | 25.5, 66 | 25.8, 67 | 26.1, 68 | 26.3, 69 | 26.4, 70 | 26.5, 71 | 26.6, 72 | 26.7, 73 | 26.8, 74 | 26.9, 75 | 27 76 | ] 77 | var _positions = [ 78 | { x: 0.8, y: 0.1 }, 79 | { x: 0.6, y: 0.2 }, 80 | { x: 0.35, y: 0.25 }, 81 | { x: 0.25, y: 0.6 }, 82 | { x: 0.2, y: 0.9 }, 83 | { x: 0.4, y: 0.8 }, 84 | { x: 0.65, y: 0.75 }, 85 | { x: 0.75, y: 0.4 } 86 | ] 87 | var _phases = _positions.length 88 | 89 | function hexToRgb(hex) { 90 | var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) 91 | return result 92 | ? { 93 | r: parseInt(result[1], 16), 94 | g: parseInt(result[2], 16), 95 | b: parseInt(result[3], 16) 96 | } 97 | : null 98 | } 99 | function getPositions(shift) { 100 | var positions = [].concat(_positions) 101 | while (shift > 0) { 102 | positions.push(positions.shift()) 103 | shift-- 104 | } 105 | var result = [] 106 | for (var i = 0; i < positions.length; i += 2) { 107 | result.push(positions[i]) 108 | } 109 | return result 110 | } 111 | function curPosition(phase, tail) { 112 | tail %= _tails 113 | var pos = getPositions(phase % _phases) 114 | if (tail) { 115 | var next_pos = getPositions(++phase % _phases) 116 | var d1x = (next_pos[0].x - pos[0].x) / _tails 117 | var d1y = (next_pos[0].y - pos[0].y) / _tails 118 | var d2x = (next_pos[1].x - pos[1].x) / _tails 119 | var d2y = (next_pos[1].y - pos[1].y) / _tails 120 | var d3x = (next_pos[2].x - pos[2].x) / _tails 121 | var d3y = (next_pos[2].y - pos[2].y) / _tails 122 | var d4x = (next_pos[3].x - pos[3].x) / _tails 123 | var d4y = (next_pos[3].y - pos[3].y) / _tails 124 | return [ 125 | { x: pos[0].x + d1x * tail, y: pos[0].y + d1y * tail }, 126 | { x: pos[1].x + d2x * tail, y: pos[1].y + d2y * tail }, 127 | { x: pos[2].x + d3x * tail, y: pos[2].y + d3y * tail }, 128 | { x: pos[3].x + d4x * tail, y: pos[3].y + d4y * tail } 129 | ] 130 | } 131 | return pos 132 | } 133 | function changeTail(diff) { 134 | _tail += diff 135 | while (_tail >= _tails) { 136 | _tail -= _tails 137 | _phase++ 138 | if (_phase >= _phases) { 139 | _phase -= _phases 140 | } 141 | } 142 | while (_tail < 0) { 143 | _tail += _tails 144 | _phase-- 145 | if (_phase < 0) { 146 | _phase += _phases 147 | } 148 | } 149 | } 150 | var _scrollTicking = false 151 | var _scrollDelta = 0 152 | function onWheel(e) { 153 | _scrollDelta += e.deltaY 154 | if (!_scrollTicking) { 155 | requestAnimationFrame(drawOnWheel) 156 | _scrollTicking = true 157 | } 158 | } 159 | function drawOnWheel() { 160 | var diff = _scrollDelta / _scrolltails 161 | _scrollDelta %= _scrolltails 162 | diff = diff > 0 ? Math.floor(diff) : Math.ceil(diff) 163 | if (diff) { 164 | changeTail(diff) 165 | var cur_pos = curPosition(_phase, _tail) 166 | drawGradient(cur_pos) 167 | } 168 | _scrollTicking = false 169 | } 170 | function drawNextPositionAnimated() { 171 | if (_frames.length > 0) { 172 | var id = _frames.shift() 173 | drawImageData(id) 174 | } else { 175 | clearInterval(_interval) 176 | } 177 | } 178 | function getGradientImageData(positions) { 179 | var id = wallpaper._hctx.createImageData(_width, _height) 180 | var pixels = id.data 181 | 182 | var offset = 0 183 | for (var y = 0; y < _height; y++) { 184 | var directPixelY = y / _height 185 | var centerDistanceY = directPixelY - 0.5 186 | var centerDistanceY2 = centerDistanceY * centerDistanceY 187 | 188 | for (var x = 0; x < _width; x++) { 189 | var directPixelX = x / _width 190 | 191 | var centerDistanceX = directPixelX - 0.5 192 | var centerDistance = Math.sqrt( 193 | centerDistanceX * centerDistanceX + centerDistanceY2 194 | ) 195 | 196 | var swirlFactor = 0.35 * centerDistance 197 | var theta = swirlFactor * swirlFactor * 0.8 * 8.0 198 | var sinTheta = Math.sin(theta) 199 | var cosTheta = Math.cos(theta) 200 | 201 | var pixelX = Math.max( 202 | 0.0, 203 | Math.min( 204 | 1.0, 205 | 0.5 + centerDistanceX * cosTheta - centerDistanceY * sinTheta 206 | ) 207 | ) 208 | var pixelY = Math.max( 209 | 0.0, 210 | Math.min( 211 | 1.0, 212 | 0.5 + centerDistanceX * sinTheta + centerDistanceY * cosTheta 213 | ) 214 | ) 215 | 216 | var distanceSum = 0.0 217 | 218 | var r = 0.0 219 | var g = 0.0 220 | var b = 0.0 221 | 222 | for (var i = 0; i < _colors.length; i++) { 223 | var colorX = positions[i].x 224 | var colorY = positions[i].y 225 | 226 | var distanceX = pixelX - colorX 227 | var distanceY = pixelY - colorY 228 | 229 | var distance = Math.max( 230 | 0.0, 231 | 0.9 - Math.sqrt(distanceX * distanceX + distanceY * distanceY) 232 | ) 233 | distance = distance * distance * distance * distance 234 | distanceSum += distance 235 | 236 | r += (distance * _colors[i].r) / 255 237 | g += (distance * _colors[i].g) / 255 238 | b += (distance * _colors[i].b) / 255 239 | } 240 | 241 | pixels[offset++] = (r / distanceSum) * 255.0 242 | pixels[offset++] = (g / distanceSum) * 255.0 243 | pixels[offset++] = (b / distanceSum) * 255.0 244 | pixels[offset++] = 0xff 245 | } 246 | } 247 | return id 248 | } 249 | function drawImageData(id) { 250 | wallpaper._hctx.putImageData(id, 0, 0) 251 | wallpaper._ctx.drawImage(wallpaper._hc, 0, 0, 50, 50) 252 | } 253 | function drawGradient(pos) { 254 | drawImageData(getGradientImageData(pos)) 255 | } 256 | function doAnimate() { 257 | var now = +Date.now() 258 | if (!document.hasFocus() || now - _ts < _frametime) { 259 | _raf = requestAnimationFrame(doAnimate) 260 | return 261 | } 262 | _ts = now 263 | changeTail(1) 264 | var cur_pos = curPosition(_phase, _tail) 265 | drawGradient(cur_pos) 266 | _raf = requestAnimationFrame(doAnimate) 267 | } 268 | 269 | var wallpaper = { 270 | init: function (el) { 271 | _colors = [] 272 | var colors = el.getAttribute('data-colors') || '' 273 | if (colors) { 274 | colors = colors.split(',') 275 | } 276 | for (var i = 0; i < colors.length; i++) { 277 | _colors.push(hexToRgb(colors[i])) 278 | } 279 | if (!wallpaper._hc) { 280 | wallpaper._hc = document.createElement('canvas') 281 | wallpaper._hc.width = _width 282 | wallpaper._hc.height = _height 283 | wallpaper._hctx = wallpaper._hc.getContext('2d') 284 | } 285 | wallpaper._canvas = el 286 | wallpaper._ctx = wallpaper._canvas.getContext('2d') 287 | wallpaper.update() 288 | }, 289 | update: function () { 290 | var pos = curPosition(_phase, _tail) 291 | drawGradient(pos) 292 | }, 293 | toNextPosition: function () { 294 | clearInterval(_interval) 295 | _frames = [] 296 | 297 | var prev_pos = getPositions(_phase % _phases) 298 | _phase++ 299 | var pos = getPositions(_phase % _phases) 300 | 301 | var h = 27 302 | var d1x = (pos[0].x - prev_pos[0].x) / h 303 | var d1y = (pos[0].y - prev_pos[0].y) / h 304 | var d2x = (pos[1].x - prev_pos[1].x) / h 305 | var d2y = (pos[1].y - prev_pos[1].y) / h 306 | var d3x = (pos[2].x - prev_pos[2].x) / h 307 | var d3y = (pos[2].y - prev_pos[2].y) / h 308 | var d4x = (pos[3].x - prev_pos[3].x) / h 309 | var d4y = (pos[3].y - prev_pos[3].y) / h 310 | 311 | for (var frame = 0; frame < 60; frame++) { 312 | var cur_pos = [ 313 | { 314 | x: prev_pos[0].x + d1x * _curve[frame], 315 | y: prev_pos[0].y + d1y * _curve[frame] 316 | }, 317 | { 318 | x: prev_pos[1].x + d2x * _curve[frame], 319 | y: prev_pos[1].y + d2y * _curve[frame] 320 | }, 321 | { 322 | x: prev_pos[2].x + d3x * _curve[frame], 323 | y: prev_pos[2].y + d3y * _curve[frame] 324 | }, 325 | { 326 | x: prev_pos[3].x + d4x * _curve[frame], 327 | y: prev_pos[3].y + d4y * _curve[frame] 328 | } 329 | ] 330 | _frames.push(getGradientImageData(cur_pos)) 331 | } 332 | _interval = setInterval(drawNextPositionAnimated, 1000 / 30) 333 | }, 334 | animate: function (start) { 335 | if (!start) { 336 | cancelAnimationFrame(_raf) 337 | return 338 | } 339 | doAnimate() 340 | }, 341 | scrollAnimate: function (start) { 342 | if (start) { 343 | document.addEventListener('wheel', onWheel) 344 | } else { 345 | document.removeEventListener('wheel', onWheel) 346 | } 347 | } 348 | } 349 | return wallpaper 350 | })() 351 | -------------------------------------------------------------------------------- /website/public/utya.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crashmax-dev/twallpaper/b0469c19035436c20bf4b2b1628d77e4d86c4fa1/website/public/utya.webp -------------------------------------------------------------------------------- /website/src/colors.ts: -------------------------------------------------------------------------------- 1 | export const COLORS = [ 2 | { 3 | text: 'Default', 4 | colors: [ 5 | '#dbddbb', 6 | '#6ba587', 7 | '#d5d88d', 8 | '#88b884' 9 | ] 10 | }, 11 | { 12 | text: 'Amethyst', 13 | colors: [ 14 | '#4f5bd5', 15 | '#962fbf', 16 | '#dd6cb9', 17 | '#fec496' 18 | ] 19 | }, 20 | { 21 | text: 'Calico', 22 | colors: [ 23 | '#baa161', 24 | '#ddb56d', 25 | '#cea668', 26 | '#faf4d2' 27 | ] 28 | }, 29 | { 30 | text: 'Cavern Pink', 31 | colors: [ 32 | '#ecd893', 33 | '#e5a1d0', 34 | '#edd594', 35 | '#d1a3e2' 36 | ] 37 | }, 38 | { 39 | text: 'Cheerfulness', 40 | colors: [ 41 | '#efd359', 42 | '#e984d8', 43 | '#ac86ed', 44 | '#40cdde' 45 | ] 46 | }, 47 | { 48 | text: 'France', 49 | colors: [ 50 | '#fbd9e6', 51 | '#fb9ae5', 52 | '#d5f7ff', 53 | '#73caff' 54 | ] 55 | }, 56 | { 57 | text: 'Light Wisteria', 58 | colors: [ 59 | '#b493e6', 60 | '#eab9d9', 61 | '#8376c2', 62 | '#e4b2ea' 63 | ] 64 | }, 65 | { 66 | text: 'Malibu', 67 | colors: [ 68 | '#679ced', 69 | '#e39fea', 70 | '#888dec', 71 | '#8adbf2' 72 | ] 73 | }, 74 | { 75 | text: 'Monte Carlo', 76 | colors: [ 77 | '#85d685', 78 | '#67a3f2', 79 | '#8fe1d6', 80 | '#dceb92' 81 | ] 82 | }, 83 | { 84 | text: 'Perfume', 85 | colors: [ 86 | '#b9e2ff', 87 | '#eccbff', 88 | '#a2b4ff', 89 | '#daeacb' 90 | ] 91 | }, 92 | { 93 | text: 'Periwinkle Gray', 94 | colors: [ 95 | '#efb7dc', 96 | '#c6b1ef', 97 | '#b1e9ea', 98 | '#97beeb' 99 | ] 100 | }, 101 | { 102 | text: 'Pine Glade', 103 | colors: [ 104 | '#fbe37d', 105 | '#336f55', 106 | '#fff5c5', 107 | '#7fa381' 108 | ] 109 | }, 110 | { 111 | text: 'Ice', 112 | colors: [ 113 | '#b2e3dd', 114 | '#bbead5', 115 | '#9fb0ea', 116 | '#b0cdeb' 117 | ] 118 | }, 119 | { 120 | text: 'Viola', 121 | colors: [ 122 | '#f7dd6d', 123 | '#e96caf', 124 | '#edac4c', 125 | '#a464f4' 126 | ] 127 | }, 128 | { 129 | text: 'Wewak', 130 | colors: [ 131 | '#e8c06e', 132 | '#f29ebf', 133 | '#f0e486', 134 | '#eaa36e' 135 | ] 136 | }, 137 | { 138 | text: 'Wild Willow', 139 | colors: [ 140 | '#f0c07a', 141 | '#afd677', 142 | '#e4d573', 143 | '#7fc289' 144 | ] 145 | }, 146 | { 147 | text: 'Cashmere', 148 | colors: [ 149 | '#ffe7b2', 150 | '#e2c0ff', 151 | '#ffc3b2' 152 | ] 153 | }, 154 | { 155 | text: 'Cold Purple', 156 | colors: [ 157 | '#6c8cd4', 158 | '#d4a7c9', 159 | '#b2b1ee' 160 | ] 161 | }, 162 | { 163 | text: 'Cold Blue', 164 | colors: [ 165 | '#527bdd', 166 | '#009fdd', 167 | '#a4dbff' 168 | ] 169 | } 170 | ] 171 | 172 | export function arrayColorToObject(colors: string[]) { 173 | return Object.values(colors).map((color, key) => ({ [key]: color })) 174 | } 175 | 176 | /** 177 | * Generate random colors 178 | * @param length - number of colors 179 | * @returns array of random colors 180 | */ 181 | export function generateRandomColors(length = 4): string[] { 182 | return Array.from({ length }, () => { 183 | return ( 184 | '#' + 185 | Math.floor(Math.random() * 16777215) 186 | .toString(16) 187 | .padStart(6, '0') 188 | ) 189 | }) 190 | } 191 | -------------------------------------------------------------------------------- /website/src/config.ts: -------------------------------------------------------------------------------- 1 | import cloneDeep from 'lodash.clonedeep' 2 | import { arrayColorToObject, COLORS } from './colors.js' 3 | import { PATTERN_SIZE, PATTERNS } from './patterns.js' 4 | import type { TWallpaperOptions } from 'twallpaper' 5 | 6 | export const wallpaperOptions: TWallpaperOptions = { 7 | fps: 60, 8 | tails: 90, 9 | animate: true, 10 | scrollAnimate: true, 11 | colors: COLORS[0].colors, 12 | pattern: { 13 | image: PATTERNS[0].path, 14 | background: '#000', 15 | blur: 0, 16 | size: `${PATTERN_SIZE}px`, 17 | opacity: 0.5, 18 | mask: false 19 | } 20 | } 21 | 22 | export const paneOptions = { 23 | enablePattern: true, 24 | container: document.querySelector('#app')!, 25 | stringOptions: JSON.stringify(wallpaperOptions, null, 2), 26 | copyOptions: cloneDeep(wallpaperOptions), 27 | currentColors: arrayColorToObject(wallpaperOptions.colors), 28 | patternSize: PATTERN_SIZE, 29 | mixBlendMode: 'overlay' 30 | } 31 | -------------------------------------------------------------------------------- /website/src/index.ts: -------------------------------------------------------------------------------- 1 | import merge from 'lodash.merge' 2 | import { TWallpaper } from 'twallpaper' 3 | import { Pane } from 'tweakpane' 4 | import { arrayColorToObject, COLORS, generateRandomColors } from './colors.js' 5 | import { paneOptions, wallpaperOptions } from './config.js' 6 | import { PATTERN_SIZE, PATTERNS } from './patterns.js' 7 | import type { InputBindingApi, ListApi } from 'tweakpane' 8 | import 'twallpaper/css' 9 | 10 | const wallpaper = new TWallpaper(paneOptions.container, wallpaperOptions) 11 | wallpaper.init() 12 | 13 | const wallpaperPatternElement = 14 | document.querySelector('.tw-pattern')! 15 | wallpaperPatternElement.style.mixBlendMode = paneOptions.mixBlendMode 16 | 17 | const tweakpane = new Pane({ 18 | document, 19 | expanded: true, 20 | title: document.title 21 | }) 22 | 23 | tweakpane.on('change', () => refreshPaneConsole()) 24 | 25 | function refreshPaneConsole() { 26 | paneOptions.stringOptions = JSON.stringify(wallpaperOptions, null, 2) 27 | consoleButtonCopy.title = 'Copy' 28 | consolePane.refresh() 29 | } 30 | 31 | tweakpane 32 | .addInput(wallpaperOptions, 'fps', { 33 | min: 1, 34 | max: 360, 35 | step: 1 36 | }) 37 | .on('change', ({ value }) => { 38 | wallpaper.updateFrametime(value) 39 | }) 40 | 41 | tweakpane 42 | .addInput(wallpaperOptions, 'tails', { 43 | min: 5, 44 | max: 90, 45 | step: 1 46 | }) 47 | .on('change', ({ value }) => { 48 | wallpaper.updateTails(value) 49 | }) 50 | 51 | const toggleAnimate = tweakpane 52 | .addInput(wallpaperOptions, 'animate') 53 | .on('change', ({ value }) => { 54 | wallpaper.animate(value) 55 | }) 56 | 57 | tweakpane 58 | .addInput(wallpaperOptions, 'scrollAnimate') 59 | .on('change', ({ value }) => { 60 | wallpaper.scrollAnimate(value) 61 | }) 62 | 63 | tweakpane.addButton({ title: 'Next position' }).on('click', () => { 64 | wallpaperOptions.animate = false 65 | toggleAnimate.disabled = true 66 | toggleAnimate.refresh() 67 | wallpaper.animate(false) 68 | wallpaper.toNextPosition(() => { 69 | toggleAnimate.disabled = false 70 | }) 71 | }) 72 | 73 | /** color */ 74 | const colorsInput: InputBindingApi[] = [] 75 | 76 | const colorsFolder = tweakpane.addFolder({ 77 | title: 'Color' 78 | }) 79 | 80 | const colorsList = colorsFolder.addBlade({ 81 | view: 'list', 82 | label: 'colors', 83 | value: 0, 84 | options: COLORS.map(({ text }, key) => { 85 | return { 86 | text, 87 | value: key 88 | } 89 | }) 90 | }) as ListApi 91 | 92 | colorsFolder.addButton({ title: 'Random colors' }).on('click', () => { 93 | const colors = generateRandomColors() 94 | updateColors(colors) 95 | }) 96 | 97 | colorsList.on('change', ({ value }) => { 98 | const { colors } = COLORS[value] 99 | updateColors(colors) 100 | }) 101 | 102 | function updateColors(colors: string[]): void { 103 | wallpaperOptions.colors = colors 104 | wallpaper.updateColors(colors) 105 | 106 | if (!wallpaperOptions.animate) { 107 | wallpaperOptions.animate = true 108 | toggleAnimate.refresh() 109 | } 110 | 111 | paneOptions.currentColors = arrayColorToObject(colors) 112 | generateColorsInput() 113 | } 114 | 115 | function generateColorsInput(): void { 116 | const inputs = paneOptions.currentColors.map((color, key) => { 117 | const input = colorsFolder.addInput(color, key, { 118 | label: `color ${key + 1}` 119 | }) 120 | 121 | input.on('change', ({ value }) => { 122 | color[key] = value 123 | wallpaperOptions.colors = paneOptions.currentColors.map( 124 | (color, key) => color[key] 125 | ) 126 | wallpaper.updateColors(wallpaperOptions.colors) 127 | }) 128 | 129 | input.controller_.view.labelElement.remove() 130 | input.controller_.view.valueElement.style.width = '100%' 131 | 132 | return input 133 | }) 134 | 135 | colorsInput.forEach((input) => input.dispose()) 136 | colorsInput.splice(0, colorsInput.length) 137 | colorsInput.push(...inputs) 138 | } 139 | 140 | generateColorsInput() 141 | 142 | /** pattern */ 143 | const patternsFolder = tweakpane.addFolder({ title: 'Pattern' }) 144 | 145 | patternsFolder.on('fold', () => { 146 | paneOptions.enablePattern = !paneOptions.enablePattern 147 | const newOptions = { ...wallpaperOptions } 148 | if (!paneOptions.enablePattern) { 149 | delete newOptions.pattern 150 | } 151 | paneOptions.stringOptions = JSON.stringify(newOptions, null, 2) 152 | wallpaper.updatePattern( 153 | paneOptions.enablePattern ? wallpaperOptions.pattern! : {} 154 | ) 155 | 156 | // prettier-ignore 157 | const textarea = consolePane.controller_.view.valueElement 158 | .querySelector('.tp-mllv_i')! 159 | 160 | const textareaLength = JSON.stringify(newOptions, null, 2).split('\n').length 161 | textarea.style.height = `calc(var(--bld-us) * ${textareaLength})` 162 | consolePane.refresh() 163 | }) 164 | 165 | patternsFolder 166 | .addInput(wallpaperOptions.pattern!, 'mask') 167 | .on('change', ({ value }) => { 168 | patternBlur.disabled = value! 169 | patternBackground.disabled = !value! 170 | patternMixBlendMode.disabled = value! 171 | wallpaper.updatePattern(wallpaperOptions.pattern!) 172 | }) 173 | 174 | patternsFolder 175 | .addInput(paneOptions, 'patternSize', { 176 | min: 100, 177 | max: 1000, 178 | step: 10, 179 | label: 'size' 180 | }) 181 | .on('change', ({ value }) => { 182 | wallpaperOptions.pattern!.size = `${value}px` 183 | wallpaper.updatePattern(wallpaperOptions.pattern!) 184 | }) 185 | 186 | patternsFolder 187 | .addInput(wallpaperOptions.pattern!, 'opacity', { 188 | min: 0, 189 | max: 1, 190 | step: 0.1 191 | }) 192 | .on('change', ({ value }) => { 193 | wallpaperOptions.pattern!.opacity = Number(value!.toFixed(1)) 194 | wallpaper.updatePattern(wallpaperOptions.pattern!) 195 | }) 196 | 197 | const patternBlur = patternsFolder 198 | .addInput(wallpaperOptions.pattern!, 'blur', { 199 | min: 0, 200 | max: 5, 201 | step: 0.1 202 | }) 203 | .on('change', ({ value }) => { 204 | wallpaperOptions.pattern!.blur = Number(value!.toFixed(2)) 205 | wallpaper.updatePattern(wallpaperOptions.pattern!) 206 | }) 207 | 208 | const patternBackground = patternsFolder 209 | .addInput(wallpaperOptions.pattern!, 'background', { 210 | disabled: true 211 | }) 212 | .on('change', () => { 213 | wallpaper.updatePattern(wallpaperOptions.pattern!) 214 | }) 215 | 216 | const patternMixBlendMode = patternsFolder.addBlade({ 217 | view: 'list', 218 | value: paneOptions.mixBlendMode, 219 | label: 'mix-blend-mode', 220 | options: [ 221 | { text: 'normal', value: 'normal' }, 222 | { text: 'overlay', value: 'overlay' }, 223 | { text: 'hard-light', value: 'hard-light' }, 224 | { text: 'soft-light', value: 'soft-light' } 225 | ] 226 | }) as ListApi 227 | 228 | patternMixBlendMode.on('change', (event) => { 229 | wallpaperPatternElement.style.mixBlendMode = event.value 230 | }) 231 | 232 | const patternsList = patternsFolder.addBlade({ 233 | view: 'list', 234 | value: PATTERNS[0].path, 235 | options: PATTERNS.map(({ path, text }) => { 236 | return { 237 | text, 238 | value: path 239 | } 240 | }) 241 | }) as ListApi 242 | 243 | patternsList.on('change', ({ value }) => { 244 | wallpaperOptions.pattern!.image = value 245 | wallpaper.updatePattern(wallpaperOptions.pattern!) 246 | }) 247 | 248 | /** export */ 249 | const exportFolder = tweakpane.addFolder({ 250 | title: 'Export', 251 | expanded: false 252 | }) 253 | 254 | const consolePane = exportFolder.addMonitor(paneOptions, 'stringOptions', { 255 | interval: 0, 256 | lineCount: paneOptions.stringOptions.split('\n').length, 257 | multiline: true 258 | }) 259 | 260 | const consoleTextarea = 261 | consolePane.controller_.view.valueElement.querySelector('textarea')! 262 | 263 | consolePane.controller_.view.labelElement.remove() 264 | 265 | consolePane.controller_.view.valueElement.style.width = '100%' 266 | consoleTextarea.style.overflow = 'hidden' 267 | 268 | const consoleButtonCopy = exportFolder.addButton({ title: 'Copy' }) 269 | 270 | consoleButtonCopy.on('click', () => { 271 | consoleTextarea.select() 272 | navigator.clipboard.writeText(consoleTextarea.value) 273 | consoleButtonCopy.title = 'Copied' 274 | }) 275 | 276 | exportFolder.addButton({ title: 'Download' }).on('click', () => { 277 | const blob = new Blob([JSON.stringify(wallpaperOptions, void 0, 2)], { 278 | type: 'text/plain' 279 | }) 280 | 281 | const link = document.createElement('a') 282 | link.href = URL.createObjectURL(blob) 283 | link.download = 'twallpaper-options.json' 284 | link.click() 285 | link.remove() 286 | }) 287 | 288 | /** reset */ 289 | tweakpane.addButton({ title: 'Reset' }).on('click', () => { 290 | merge(wallpaperOptions, paneOptions.copyOptions) 291 | colorsList.value = 0 292 | colorsFolder.expanded = true 293 | 294 | paneOptions.patternSize = PATTERN_SIZE 295 | paneOptions.currentColors = arrayColorToObject(COLORS[0].colors) 296 | 297 | patternsList.value = PATTERNS[0].path 298 | patternsFolder.expanded = true 299 | 300 | generateColorsInput() 301 | refreshPaneConsole() 302 | tweakpane.refresh() 303 | wallpaper.init(wallpaperOptions) 304 | }) 305 | 306 | tweakpane.addSeparator() 307 | 308 | /** webgl version */ 309 | tweakpane.addButton({ title: 'WebGL version' }).on('click', () => { 310 | window.open('/webgl.html', '_blank') 311 | }) 312 | 313 | /** github link */ 314 | tweakpane.addButton({ title: 'GitHub' }).on('click', () => { 315 | window.open('https://github.com/crashmax-dev/twallpaper', '_blank') 316 | }) 317 | 318 | /** fullscreen */ 319 | declare global { 320 | interface Element { 321 | webkitRequestFullscreen?(): void 322 | mozRequestFullScreen?(): void 323 | msRequestFullscreen?(): void 324 | } 325 | } 326 | 327 | document.addEventListener('keydown', (event) => { 328 | if (event.code === 'F11') { 329 | event.preventDefault() 330 | 331 | if (paneOptions.container.requestFullscreen) { 332 | paneOptions.container.requestFullscreen() 333 | } else if (paneOptions.container.webkitRequestFullscreen) { 334 | paneOptions.container.webkitRequestFullscreen() 335 | } else if (paneOptions.container.mozRequestFullScreen) { 336 | paneOptions.container.mozRequestFullScreen() 337 | } else if (paneOptions.container.msRequestFullscreen) { 338 | paneOptions.container.msRequestFullscreen() 339 | } 340 | } 341 | }) 342 | -------------------------------------------------------------------------------- /website/src/patterns.ts: -------------------------------------------------------------------------------- 1 | export const PATTERNS_PATH = location.href + 'patterns/' 2 | export const PATTERN_SIZE = 420 3 | export const PATTERNS = [ 4 | { 5 | text: 'Animals', 6 | path: PATTERNS_PATH + 'animals.svg' 7 | }, 8 | { 9 | text: 'Astronaut Cats', 10 | path: PATTERNS_PATH + 'astronaut_cats.svg' 11 | }, 12 | { 13 | text: 'Beach', 14 | path: PATTERNS_PATH + 'beach.svg' 15 | }, 16 | { 17 | text: 'Cats and Dogs', 18 | path: PATTERNS_PATH + 'cats_and_dogs.svg' 19 | }, 20 | { 21 | text: 'Christmas', 22 | path: PATTERNS_PATH + 'christmas.svg' 23 | }, 24 | { 25 | text: 'Fantasy', 26 | path: PATTERNS_PATH + 'fantasy.svg' 27 | }, 28 | { 29 | text: 'Late Night Delight', 30 | path: PATTERNS_PATH + 'late_night_delight.svg' 31 | }, 32 | { 33 | text: 'Magic', 34 | path: PATTERNS_PATH + 'magic.svg' 35 | }, 36 | { 37 | text: 'Math', 38 | path: PATTERNS_PATH + 'math.svg' 39 | }, 40 | { 41 | text: 'Paris', 42 | path: PATTERNS_PATH + 'paris.svg' 43 | }, 44 | { 45 | text: 'Games', 46 | path: PATTERNS_PATH + 'games.svg' 47 | }, 48 | { 49 | text: 'Snowflakes', 50 | path: PATTERNS_PATH + 'snowflakes.svg' 51 | }, 52 | { 53 | text: 'Space', 54 | path: PATTERNS_PATH + 'space.svg' 55 | }, 56 | { 57 | text: 'Star Wars', 58 | path: PATTERNS_PATH + 'star_wars.svg' 59 | }, 60 | { 61 | text: 'Sweets', 62 | path: PATTERNS_PATH + 'sweets.svg' 63 | }, 64 | { 65 | text: 'Tattoos', 66 | path: PATTERNS_PATH + 'tattoos.svg' 67 | }, 68 | { 69 | text: 'Underwater World', 70 | path: PATTERNS_PATH + 'underwater_world.svg' 71 | }, 72 | { 73 | text: 'Unicorns', 74 | path: PATTERNS_PATH + 'unicorn.svg' 75 | }, 76 | { 77 | text: 'Zoo', 78 | path: PATTERNS_PATH + 'zoo.svg' 79 | } 80 | ] 81 | -------------------------------------------------------------------------------- /website/src/webgl.ts: -------------------------------------------------------------------------------- 1 | import { hexToVec3, TWallpaperWebGL } from '@twallpaper/webgl' 2 | import '@twallpaper/webgl/css' 3 | 4 | const container = document.querySelector('#app')! 5 | const twallpaper = new TWallpaperWebGL(container) 6 | 7 | twallpaper.init({ 8 | colors: [ 9 | hexToVec3('#4f5bd5'), 10 | hexToVec3('#962fbf'), 11 | hexToVec3('#dd6cb9'), 12 | hexToVec3('#fec496')], 13 | backgroundColor: '#000000', 14 | image: `${location.origin}/patterns/magic.svg`, 15 | mask: false, 16 | opacity: 0.3, 17 | size: 420 18 | }) 19 | 20 | document.addEventListener('click', () => { 21 | twallpaper.animate() 22 | }) 23 | -------------------------------------------------------------------------------- /website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@crashmax/tsconfig", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "noUncheckedIndexedAccess": false, 6 | }, 7 | "include": [ 8 | "src/**/*" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /website/webgl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Telegram Wallpaper (WebGL) 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | --------------------------------------------------------------------------------