├── .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 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
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 |
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 |
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 | [](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 | Key | Type | Default | Description |
---|
colors | string[] | | Array of colors in hex format. Allowed maximum of 4 colors. |
210 | fps | number | 30 | Animation speed. |
211 | tails | number | 90 | Tail speed animation. |
212 | animate | boolean | true | Animation is enabled. |
213 | scrollAnimate | boolean | false | Animation is enabled when scrolling. |
214 | pattern | PatternOptions | | Pattern options. |
215 | pattern.image | string | | Wallpaper image. Use standard pattern or create your own. |
216 | pattern.mask | boolean | false | Option enables a mask for the background image using the mask-image css-property. |
217 | pattern.background | string | #000 | Background color for the pattern image. Background does not work when pattern.mask is enabled. |
218 | pattern.size | string | auto | Size of the pattern image. |
219 | pattern.blur | number | 0 | Blur of the pattern image. Blur does not work when pattern.mask is enabled. |
220 | pattern.opacity | number | 0.5 | Opacity 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 |
44 |
45 |
46 |
47 |
48 |
49 |
54 |
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 |
17 |
18 |
19 |
20 |
21 |
22 |
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 |
17 |
18 |
19 |
20 |
21 |
22 |
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 |
17 |
18 |
19 |
20 |
21 |
22 |
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 |
17 |
18 |
19 |
20 |
21 |
22 |
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 |
35 |
36 |
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 |
--------------------------------------------------------------------------------