├── website ├── public │ ├── robots.txt │ ├── static │ │ ├── og.png │ │ └── favicons │ │ │ ├── favicon.ico │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── mstile-270x270.png │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── browserconfig.xml │ │ │ └── site.webmanifest │ └── fonts │ │ └── inter-var-latin.woff2 ├── components │ └── Container │ │ ├── index.ts │ │ └── Container.tsx ├── lib │ └── classNames.ts ├── next.config.js ├── next-env.d.ts ├── postcss.config.js ├── vercel.json ├── pages │ ├── api │ │ └── hello.ts │ ├── _app.tsx │ ├── _document.tsx │ └── index.tsx ├── .prettierignore ├── tailwind.config.js ├── tsconfig.json ├── LICENSE ├── package.json └── styles │ └── globals.css ├── pnpm-workspace.yaml ├── meshgrad ├── tsup.config.ts ├── dist │ ├── index.d.ts │ ├── index.mjs │ └── index.js ├── package.json └── src │ └── index.ts ├── package.json ├── .gitignore ├── tsconfig.json ├── LICENSE ├── README.md └── pnpm-lock.yaml /website/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "website" 3 | - "meshgrad" 4 | -------------------------------------------------------------------------------- /website/components/Container/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Container' 2 | -------------------------------------------------------------------------------- /website/public/static/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cristicretu/meshgrad/HEAD/website/public/static/og.png -------------------------------------------------------------------------------- /website/lib/classNames.ts: -------------------------------------------------------------------------------- 1 | export default function cn(...classes: string[]): string { 2 | return classes.filter(Boolean).join(' ') 3 | } 4 | -------------------------------------------------------------------------------- /website/public/fonts/inter-var-latin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cristicretu/meshgrad/HEAD/website/public/fonts/inter-var-latin.woff2 -------------------------------------------------------------------------------- /website/public/static/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cristicretu/meshgrad/HEAD/website/public/static/favicons/favicon.ico -------------------------------------------------------------------------------- /website/public/static/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cristicretu/meshgrad/HEAD/website/public/static/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /website/public/static/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cristicretu/meshgrad/HEAD/website/public/static/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /website/public/static/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cristicretu/meshgrad/HEAD/website/public/static/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /website/public/static/favicons/mstile-270x270.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cristicretu/meshgrad/HEAD/website/public/static/favicons/mstile-270x270.png -------------------------------------------------------------------------------- /website/public/static/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cristicretu/meshgrad/HEAD/website/public/static/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /website/public/static/favicons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cristicretu/meshgrad/HEAD/website/public/static/favicons/android-chrome-512x512.png -------------------------------------------------------------------------------- /meshgrad/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | sourcemap: false, 5 | minify: true, 6 | dts: true, 7 | format: ["esm", "cjs"], 8 | }); 9 | -------------------------------------------------------------------------------- /website/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | output: 'export', 3 | eslint: { 4 | dirs: ['pages', 'components'], 5 | }, 6 | images: { 7 | domains: ['api.microlink.io'], 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /website/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /website/postcss.config.js: -------------------------------------------------------------------------------- 1 | // If you want to use other PostCSS plugins, see the following: 2 | // https://tailwindcss.com/docs/using-with-preprocessors 3 | module.exports = { 4 | plugins: { 5 | tailwindcss: {}, 6 | autoprefixer: {}, 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /website/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "headers": [ 3 | { 4 | "source": "/fonts/inter-var-latin.woff2", 5 | "headers": [ 6 | { 7 | "key": "Cache-Control", 8 | "value": "public, max-age=31536000, immutable" 9 | } 10 | ] 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /website/pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | import { NextApiRequest, NextApiResponse } from 'next' 4 | 5 | export default function helloAPI(req: NextApiRequest, res: NextApiResponse) { 6 | res.status(200).json({ name: 'John Doe' }) 7 | } 8 | -------------------------------------------------------------------------------- /website/public/static/favicons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #00aba9 7 | 8 | 9 | -------------------------------------------------------------------------------- /meshgrad/dist/index.d.ts: -------------------------------------------------------------------------------- 1 | declare const generateMeshGradient: (length: number, baseColor?: string, hash?: number) => string; 2 | declare const generateJSXMeshGradient: (length: number, baseColor?: string, hash?: number) => { 3 | backgroundColor: string; 4 | backgroundImage: string; 5 | }; 6 | 7 | export { generateJSXMeshGradient, generateMeshGradient }; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "meshgrad-root", 3 | "private": true, 4 | "scripts": { 5 | "build": "pnpm -F meshgrad build", 6 | "dev": "pnpm -F meshgrad build --watch", 7 | "website": "pnpm -F meshgrad-website dev", 8 | "preinstall": "npx only-allow pnpm" 9 | }, 10 | "devDependencies": { 11 | "tsup": "6.2.1", 12 | "typescript": "4.6.4" 13 | }, 14 | "packageManager": "pnpm@7.9.0" 15 | } 16 | -------------------------------------------------------------------------------- /website/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeProvider } from 'next-themes' 2 | import 'tailwindcss/tailwind.css' 3 | import type { AppProps } from 'next/app' 4 | 5 | import 'styles/globals.css' 6 | 7 | function MyApp({ Component, pageProps }: AppProps) { 8 | return ( 9 | 10 | 11 | 12 | ) 13 | } 14 | 15 | export default MyApp 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | */dist/ 13 | */.next/ 14 | /out/ 15 | 16 | # production 17 | */build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | # vercel 35 | .vercel 36 | -------------------------------------------------------------------------------- /website/.prettierignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": false, 8 | "forceConsistentCasingInFileNames": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "react", 15 | "noEmit": true 16 | }, 17 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"], 18 | "exclude": ["node_modules", "build", "dist", ".next"] 19 | } 20 | -------------------------------------------------------------------------------- /website/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const { fontFamily } = require('tailwindcss/defaultTheme') 2 | const colors = require('tailwindcss/colors') 3 | 4 | module.exports = { 5 | mode: 'jit', 6 | content: [ 7 | './components/**/*.{js,ts,jsx,tsx}', 8 | './pages/**/*.{js,ts,jsx,tsx}', 9 | ], 10 | darkMode: 'class', 11 | theme: { 12 | extend: { 13 | colors: { 14 | 'gray-1000': '#050505', 15 | gray: colors.neutral, 16 | }, 17 | }, 18 | fontFamily: { 19 | sans: ['Inter', ...fontFamily.sans], 20 | }, 21 | }, 22 | variants: { 23 | extend: {}, 24 | }, 25 | plugins: [], 26 | } 27 | -------------------------------------------------------------------------------- /website/public/static/favicons/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Meshgrad", 3 | "short_name": "meshgrad.cretu.dev", 4 | "description": "meshgrad", 5 | "icons": [ 6 | { 7 | "src": "/static/favicons/android-chrome-192x192.png", 8 | "sizes": "192x192", 9 | "type": "image/png" 10 | }, 11 | { 12 | "src": "/static/favicons/android-chrome-512x512.png", 13 | "sizes": "512x512", 14 | "type": "image/png" 15 | } 16 | ], 17 | "background_color": "#ffffff", 18 | "theme_color": "#ffffff", 19 | "display": "standalone", 20 | "dir": "ltr", 21 | "lang": "en-US", 22 | "orientation": "portrait-primary", 23 | "start_url": "../../index.html" 24 | } 25 | -------------------------------------------------------------------------------- /website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "es5", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "noEmit": true, 9 | "module": "esnext", 10 | "moduleResolution": "node", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "esModuleInterop": true, 16 | "allowSyntheticDefaultImports": true, 17 | "strict": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "jsx": "preserve" 20 | }, 21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Cristian Crețu 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 | -------------------------------------------------------------------------------- /website/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Cristi 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 | -------------------------------------------------------------------------------- /meshgrad/dist/index.mjs: -------------------------------------------------------------------------------- 1 | var i=()=>Math.round(Math.random()*360),c=n=>Math.round(Math.random()*(n*100)%100),g=(n,t,e)=>Math.round(t/e*(n*100)%100),d=n=>{if(n){n=n.replace(/#/g,""),n.length===3&&(n=n.split("").map(function(b){return b+b}).join(""));var t=/^([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})[\da-z]{0,0}$/i.exec(n);if(t){var e=parseInt(t[1],16),r=parseInt(t[2],16),a=parseInt(t[3],16);e/=255,r/=255,a/=255;var o=Math.max(e,r,a),s=Math.min(e,r,a),u=(o+s)/2;if(o==s)u=0;else{var m=o-s;switch(o){case e:u=(r-a)/m+(rArray.from({length:n},(e,r)=>r===0?`hsl(${t}, 100%, 74%)`:r2?r/2:r)}, 100%, ${64-r*(1-2*(r%2))*1.75}%)`:`hsl(${t-150*(1-2*(r%2))}, 100%, ${66-r*(1-2*(r%2))*1.25}%)`),M=(n,t,e)=>Array.from({length:n},(r,a)=>`radial-gradient(at ${e?g(a,e,n):c(a)}% ${e?g(a*10,e,n):c(a*10)}%, ${t[a]} 0px, transparent 55%) 2 | `),f=(n,t,e)=>{let r=p(n,t||i()),a=M(n,r,e||void 0);return[r[0],a.join(",")]},$=(n,t,e)=>{let[r,a]=f(n,d(t)?d(t):void 0,e||void 0);return`background-color: ${r}; background-image:${a}`},k=(n,t,e)=>{let[r,a]=f(n,d(t)?d(t):void 0,e||void 0);return{backgroundColor:r,backgroundImage:a}};export{k as generateJSXMeshGradient,$ as generateMeshGradient}; 3 | -------------------------------------------------------------------------------- /meshgrad/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.15", 3 | "description": "A tiny utility for generating CSS Mesh Gradients", 4 | "homepage": "https://meshgrad.cretu.dev", 5 | "name": "meshgrad", 6 | "author": { 7 | "email": "crisemcr@gmail.com", 8 | "name": "Cristian Crețu", 9 | "url": "https://cretu.dev" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/cristicretu/meshgrad/issues" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/cristicretu/meshgrad.git" 17 | }, 18 | "license": "MIT", 19 | "main": "./dist/index.js", 20 | "module": "./dist/index.mjs", 21 | "types": "./dist/index.d.ts", 22 | "files": [ 23 | "dist" 24 | ], 25 | "scripts": { 26 | "prepublishOnly": "cp ../README.md . && pnpm build", 27 | "postpublish": "rm README.md", 28 | "build": "tsup src", 29 | "dev": "tsup src --watch" 30 | }, 31 | "keywords": [ 32 | "mesh-gradients", 33 | "gradient", 34 | "css-gradient", 35 | "meshgradient", 36 | "gradient-generator", 37 | "npm-gradient", 38 | "npm-meshgradient", 39 | "css-meshgradient", 40 | "mesh-gradient-generator", 41 | "mesh-gradient-generator-npm", 42 | "mesh-gradient-generator-npm-package", 43 | "mesh-gradient-generator-npm-package-css", 44 | "mesh-gradient-generator-npm-package-css-meshgradient" 45 | ], 46 | "devDependencies": { 47 | "tsup": "^6.2.2" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | # 🎨 Meshgrad ![meshgrad minzip package size](https://img.shields.io/bundlephobia/minzip/meshgrad) ![meshgrad package version](https://img.shields.io/npm/v/meshgrad.svg?colorB=blue) 6 | 7 | Meshgrad is a tiny utility to generate native-CSS Mesh Gradients. 8 | 9 | [Demo - meshgrad.cretu.dev](https://meshgrad.cretu.dev/) 10 | 11 | ## Install 12 | 13 | ```bash 14 | $ npm install meshgrad 15 | ``` 16 | 17 | ## Use 18 | 19 | ### Options: 20 | 21 | - length: _number_ - ammount of color stops 22 | - baseColor: _string_ - hex string that specifies the root color 23 | - hash: _number_ - specify a number that will generate the same position each time 24 | 25 | ### Vanilla Javascript 26 | 27 | ```js 28 | 37 | 38 |
39 | ``` 40 | 41 | ### Svelte 42 | 43 | ```js 44 | 50 | 51 |
52 | ``` 53 | 54 | ### React JSX 55 | 56 | ```jsx 57 | import { generateJSXMeshGradient } from "meshgrad"; 58 | 59 | // Number of color stops 60 | const ELEMENTS = 6; 61 | 62 | export function App() { 63 | return
; 64 | } 65 | ``` 66 | 67 | ### License 68 | 69 | MIT 70 | -------------------------------------------------------------------------------- /meshgrad/dist/index.js: -------------------------------------------------------------------------------- 1 | var b=Object.defineProperty;var p=Object.getOwnPropertyDescriptor;var M=Object.getOwnPropertyNames;var $=Object.prototype.hasOwnProperty;var k=(r,n)=>{for(var t in n)b(r,t,{get:n[t],enumerable:!0})},l=(r,n,t,e)=>{if(n&&typeof n=="object"||typeof n=="function")for(let a of M(n))!$.call(r,a)&&a!==t&&b(r,a,{get:()=>n[a],enumerable:!(e=p(n,a))||e.enumerable});return r};var v=r=>l(b({},"__esModule",{value:!0}),r);var A={};k(A,{generateJSXMeshGradient:()=>y,generateMeshGradient:()=>j});module.exports=v(A);var I=()=>Math.round(Math.random()*360),g=r=>Math.round(Math.random()*(r*100)%100),f=(r,n,t)=>Math.round(n/t*(r*100)%100),d=r=>{if(r){r=r.replace(/#/g,""),r.length===3&&(r=r.split("").map(function(c){return c+c}).join(""));var n=/^([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})[\da-z]{0,0}$/i.exec(r);if(n){var t=parseInt(n[1],16),e=parseInt(n[2],16),a=parseInt(n[3],16);t/=255,e/=255,a/=255;var o=Math.max(t,e,a),s=Math.min(t,e,a),u=(o+s)/2;if(o==s)u=0;else{var m=o-s;switch(o){case t:u=(e-a)/m+(eArray.from({length:r},(t,e)=>e===0?`hsl(${n}, 100%, 74%)`:e2?e/2:e)}, 100%, ${64-e*(1-2*(e%2))*1.75}%)`:`hsl(${n-150*(1-2*(e%2))}, 100%, ${66-e*(1-2*(e%2))*1.25}%)`),S=(r,n,t)=>Array.from({length:r},(e,a)=>`radial-gradient(at ${t?f(a,t,r):g(a)}% ${t?f(a*10,t,r):g(a*10)}%, ${n[a]} 0px, transparent 55%) 2 | `),i=(r,n,t)=>{let e=G(r,n||I()),a=S(r,e,t||void 0);return[e[0],a.join(",")]},j=(r,n,t)=>{let[e,a]=i(r,d(n)?d(n):void 0,t||void 0);return`background-color: ${e}; background-image:${a}`},y=(r,n,t)=>{let[e,a]=i(r,d(n)?d(n):void 0,t||void 0);return{backgroundColor:e,backgroundImage:a}};0&&(module.exports={generateJSXMeshGradient,generateMeshGradient}); 3 | -------------------------------------------------------------------------------- /website/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { Head, Html, Main, NextScript } from 'next/document' 2 | 3 | class MyDocument extends Document { 4 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 5 | static async getInitialProps(ctx: any) { 6 | const initialProps = await Document.getInitialProps(ctx) 7 | return { ...initialProps } 8 | } 9 | 10 | render() { 11 | return ( 12 | 13 | 14 | 21 | 22 | 23 | 28 | 34 | 40 | 45 | 49 | 54 | 59 | 60 | 61 |
62 | 63 | 64 | 65 | ) 66 | } 67 | } 68 | 69 | export default MyDocument 70 | -------------------------------------------------------------------------------- /website/components/Container/Container.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/dist/client/router' 2 | import Head from 'next/head' 3 | 4 | import cn from 'lib/classNames' 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 7 | export default function Container(props: any) { 8 | const { children, ...customMeta } = props 9 | const router = useRouter() 10 | 11 | const meta = { 12 | title: 'Meshgrad', 13 | description: 'A tiny utility for generating CSS Mesh Gradients.', 14 | image: 'https://meshgrad.cretu.dev/static/og.png', 15 | type: 'website', 16 | ...customMeta, 17 | } 18 | 19 | return ( 20 | <> 21 |
29 | 30 | 31 | 35 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | {meta.date && ( 50 | 51 | )} 52 | 53 | 54 |
64 |
{children}
65 |
66 |
67 | 68 | ) 69 | } 70 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "meshgrad-website", 3 | "version": "0.0.1", 4 | "private": true, 5 | "sideEffects": false, 6 | "scripts": { 7 | "dev": "next dev", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "npx eslint . --ext .ts,.tsx", 11 | "lint:fix": "npx eslint . --ext .ts,.tsx --fix", 12 | "format": "prettier --write .", 13 | "check-format": "prettier --list-different \"./**/*.{md,mdx,ts,js,tsx,jsx}\"", 14 | "pre-commit": "yarn lint:fix", 15 | "pre-push": "yarn check-format" 16 | }, 17 | "prettier": { 18 | "singleQuote": true, 19 | "jsxSingleQuote": true, 20 | "tabWidth": 2, 21 | "printWidth": 80, 22 | "useTabs": false, 23 | "semi": false, 24 | "trailingComma": "es5", 25 | "bracketSpacing": true, 26 | "arrowParens": "avoid" 27 | }, 28 | "eslintConfig": { 29 | "extends": [ 30 | "next", 31 | "eslint:recommended", 32 | "next/core-web-vitals", 33 | "eslint:recommended", 34 | "plugin:@typescript-eslint/recommended", 35 | "plugin:prettier/recommended" 36 | ], 37 | "parser": "@typescript-eslint/parser", 38 | "plugins": [ 39 | "@typescript-eslint" 40 | ], 41 | "ignorePatterns": [ 42 | "postcss.config.js", 43 | "tailwind.config.js" 44 | ], 45 | "rules": { 46 | "import/order": [ 47 | "error", 48 | { 49 | "groups": [ 50 | "builtin", 51 | "external", 52 | "internal" 53 | ], 54 | "pathGroups": [ 55 | { 56 | "pattern": "react", 57 | "group": "external", 58 | "position": "before" 59 | } 60 | ], 61 | "pathGroupsExcludedImportTypes": [ 62 | "react" 63 | ], 64 | "newlines-between": "always", 65 | "alphabetize": { 66 | "order": "asc", 67 | "caseInsensitive": true 68 | } 69 | } 70 | ], 71 | "no-unused-vars": "off", 72 | "no-console": "warn", 73 | "@typescript-eslint/explicit-module-boundary-types": "off" 74 | }, 75 | "globals": { 76 | "React": true, 77 | "JSX": true 78 | } 79 | }, 80 | "dependencies": { 81 | "eslint-config-prettier": "^8.5.0", 82 | "next": "^12.2.3", 83 | "next-themes": "^0.2.0", 84 | "react": "^18.2.0", 85 | "react-dom": "^18.2.0", 86 | "react-use": "^17.4.0" 87 | }, 88 | "devDependencies": { 89 | "@types/node": "^18.6.1", 90 | "@types/react": "^18.0.15", 91 | "@typescript-eslint/eslint-plugin": "^5.31.0", 92 | "@typescript-eslint/parser": "^5.31.0", 93 | "autoprefixer": "^10.4.7", 94 | "eslint": "^8.20.0", 95 | "eslint-config-next": "12.2.3", 96 | "eslint-plugin-prettier": "^4.2.1", 97 | "meshgrad": "^0.0.10", 98 | "postcss": "^8.4.14", 99 | "prettier": "^2.7.1", 100 | "tailwindcss": "^3.1.6", 101 | "typescript": "^4.7.4" 102 | }, 103 | "browserslist": { 104 | "production": [ 105 | ">0.2%", 106 | "not dead", 107 | "not op_mini all" 108 | ], 109 | "development": [ 110 | "last 1 chrome version", 111 | "last 1 firefox version", 112 | "last 1 safari version" 113 | ] 114 | }, 115 | "keywords": [ 116 | "tailwindcss", 117 | "template", 118 | "radix", 119 | "react", 120 | "nextjs" 121 | ], 122 | "author": { 123 | "email": "crisemcr@gmail.com", 124 | "name": "Cristian Crețu", 125 | "url": "https://cretu.dev" 126 | }, 127 | "license": "MIT", 128 | "bugs": { 129 | "url": "https://github.com/cristicretu/meshgrad/issues" 130 | }, 131 | "homepage": "https://meshgrad.cretu.dev" 132 | } 133 | -------------------------------------------------------------------------------- /meshgrad/src/index.ts: -------------------------------------------------------------------------------- 1 | // Generate a random hue from 0 - 360 2 | const getColor = (): number => { 3 | return Math.round(Math.random() * 360); 4 | }; 5 | 6 | const getPercent = (value: number): number => { 7 | return Math.round((Math.random() * (value * 100)) % 100); 8 | }; 9 | 10 | const getHashPercent = ( 11 | value: number, 12 | hash: number, 13 | length: number 14 | ): number => { 15 | return Math.round(((hash / length) * (value * 100)) % 100); 16 | }; 17 | 18 | const hexToHSL = (hex?: string): number | undefined => { 19 | if (!hex) return undefined; 20 | hex = hex.replace(/#/g, ""); 21 | if (hex.length === 3) { 22 | hex = hex 23 | .split("") 24 | .map(function (hex) { 25 | return hex + hex; 26 | }) 27 | .join(""); 28 | } 29 | var result = /^([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})[\da-z]{0,0}$/i.exec(hex); 30 | if (!result) { 31 | return undefined; 32 | } 33 | var r = parseInt(result[1], 16); 34 | var g = parseInt(result[2], 16); 35 | var b = parseInt(result[3], 16); 36 | (r /= 255), (g /= 255), (b /= 255); 37 | var max = Math.max(r, g, b), 38 | min = Math.min(r, g, b); 39 | var h = (max + min) / 2; 40 | if (max == min) { 41 | h = 0; 42 | } else { 43 | var d = max - min; 44 | switch (max) { 45 | case r: 46 | h = (g - b) / d + (g < b ? 6 : 0); 47 | break; 48 | case g: 49 | h = (b - r) / d + 2; 50 | break; 51 | case b: 52 | h = (r - g) / d + 4; 53 | break; 54 | } 55 | h /= 6; 56 | } 57 | h = Math.round(360 * h); 58 | 59 | return h; 60 | }; 61 | 62 | const genColors = (length: number, initialHue: number): string[] => { 63 | return Array.from({ length }, (_, i) => { 64 | // analogous colors + complementary colors 65 | // https://uxplanet.org/how-to-use-a-split-complementary-color-scheme-in-design-a6c3f1e22644 66 | 67 | // base color 68 | if (i === 0) { 69 | return `hsl(${initialHue}, 100%, 74%)`; 70 | } 71 | // analogous colors 72 | if (i < length / 1.4) { 73 | return `hsl(${ 74 | initialHue - 30 * (1 - 2 * (i % 2)) * (i > 2 ? i / 2 : i) 75 | }, 100%, ${64 - i * (1 - 2 * (i % 2)) * 1.75}%)`; 76 | } 77 | 78 | // complementary colors 79 | return `hsl(${initialHue - 150 * (1 - 2 * (i % 2))}, 100%, ${ 80 | 66 - i * (1 - 2 * (i % 2)) * 1.25 81 | }%)`; 82 | }); 83 | }; 84 | 85 | const genGrad = (length: number, colors: string[], hash?: number): string[] => { 86 | return Array.from({ length }, (_, i) => { 87 | return `radial-gradient(at ${ 88 | hash ? getHashPercent(i, hash, length) : getPercent(i) 89 | }% ${hash ? getHashPercent(i * 10, hash, length) : getPercent(i * 10)}%, ${ 90 | colors[i] 91 | } 0px, transparent 55%)\n`; 92 | }); 93 | }; 94 | 95 | const genStops = (length: number, baseColor?: number, hash?: number) => { 96 | // get the color for the radial gradient 97 | const colors = genColors(length, baseColor ? baseColor : getColor()); 98 | // generate the radial gradient 99 | const proprieties = genGrad(length, colors, hash ? hash : undefined); 100 | return [colors[0], proprieties.join(",")]; 101 | }; 102 | 103 | const generateMeshGradient = ( 104 | length: number, 105 | baseColor?: string, 106 | hash?: number 107 | ) => { 108 | const [bgColor, bgImage] = genStops( 109 | length, 110 | hexToHSL(baseColor) ? hexToHSL(baseColor) : undefined, 111 | hash ? hash : undefined 112 | ); 113 | return `background-color: ${bgColor}; background-image:${bgImage}`; 114 | }; 115 | 116 | const generateJSXMeshGradient = ( 117 | length: number, 118 | baseColor?: string, 119 | hash?: number 120 | ) => { 121 | const [bgColor, bgImage] = genStops( 122 | length, 123 | hexToHSL(baseColor) ? hexToHSL(baseColor) : undefined, 124 | hash ? hash : undefined 125 | ); 126 | return { backgroundColor: bgColor, backgroundImage: bgImage }; 127 | }; 128 | 129 | export { generateMeshGradient as generateMeshGradient }; 130 | export { generateJSXMeshGradient as generateJSXMeshGradient }; 131 | -------------------------------------------------------------------------------- /website/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | @font-face { 7 | font-family: 'Inter'; 8 | font-style: normal; 9 | font-weight: 100 900; 10 | font-display: optional; 11 | src: url(/fonts/inter-var-latin.woff2) format('woff2'); 12 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, 13 | U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, 14 | U+2215, U+FEFF, U+FFFD; 15 | } 16 | 17 | .capsize::before { 18 | content: ''; 19 | margin-bottom: -0.098em; 20 | display: table; 21 | } 22 | 23 | .capsize::after { 24 | content: ''; 25 | margin-top: -0.219em; 26 | display: table; 27 | } 28 | 29 | pre::-webkit-scrollbar { 30 | display: none; 31 | } 32 | 33 | pre { 34 | -ms-overflow-style: none; /* IE and Edge */ 35 | scrollbar-width: none; /* Firefox */ 36 | } 37 | 38 | /* https://seek-oss.github.io/capsize/ */ 39 | .capsize::before { 40 | content: ''; 41 | margin-bottom: -0.098em; 42 | display: table; 43 | } 44 | 45 | .capsize::after { 46 | content: ''; 47 | margin-top: -0.219em; 48 | display: table; 49 | } 50 | 51 | ::-moz-selection { 52 | color: #fff; 53 | background: #000; 54 | } 55 | 56 | .light ::selection { 57 | color: #fff; 58 | background: #000; 59 | } 60 | 61 | .dark ::selection { 62 | background: #fff; 63 | color: #000; 64 | } 65 | 66 | html { 67 | @apply max-h-screen antialiased; 68 | } 69 | 70 | * { 71 | box-sizing: border-box; 72 | } 73 | 74 | body { 75 | @apply p-0 m-0 font-sans; 76 | } 77 | 78 | body:after { 79 | content: ''; 80 | position: fixed; 81 | top: -50%; 82 | right: -50%; 83 | bottom: -50%; 84 | left: -50%; 85 | z-index: -1; 86 | @apply bg-white dark:bg-black; 87 | } 88 | } 89 | 90 | @layer components { 91 | /* needed to override tailwind forms styles */ 92 | select { 93 | @apply w-full px-4 py-2 border border-gray-200 rounded outline-none text-primary bg-gray-1000 bg-opacity-5 hover:bg-opacity-10 focus:border-gray-1000 focus:outline-none focus:ring-0 dark:border-gray-800 dark:bg-white dark:focus:border-gray-600; 94 | } 95 | 96 | button:focus, 97 | a:focus { 98 | @apply outline-none ring-2 ring-blue-500 ring-offset-2 ring-offset-white dark:ring-blue-500 dark:ring-offset-black; 99 | } 100 | 101 | button:active:not(:focus-visible), 102 | a:active:not(:focus-visible), 103 | button:focus:not(:focus-visible), 104 | a:focus:not(:focus-visible) { 105 | @apply outline-none ring-0 ring-offset-0; 106 | } 107 | 108 | /* input[type='text'], 109 | textarea { 110 | @apply bg-white border border-gray-200 outline-none dark:border-gray-700 dark:bg-gray-800; 111 | } */ 112 | 113 | input[type='checkbox'] { 114 | @apply dark:border-gray-600 dark:bg-gray-700; 115 | } 116 | 117 | /* input:focus, 118 | textarea:focus { 119 | @apply outline-none ring-2 ring-blue-500 ring-offset-2 ring-offset-white dark:ring-offset-black; 120 | } */ 121 | 122 | .font-list-heading { 123 | @apply font-sans text-lg font-bold text-primary; 124 | } 125 | 126 | .tabbed-navigation::-webkit-scrollbar { 127 | display: none; 128 | } 129 | 130 | .light .bg-dots { 131 | background: linear-gradient(90deg, #f9fafb 15px, transparent 1%) center, 132 | linear-gradient(#f9fafb 15px, transparent 1%) center, rgba(0, 0, 0, 0.24); 133 | background-size: 16px 16px; 134 | } 135 | 136 | .dark .bg-dots { 137 | background: linear-gradient(90deg, #050505 15px, transparent 1%) center, 138 | linear-gradient(#050505 15px, transparent 1%) center, 139 | rgba(255, 255, 255, 0.12); 140 | background-size: 16px 16px; 141 | } 142 | } 143 | 144 | /* Your own custom utilities */ 145 | @layer utilities { 146 | .text-primary { 147 | @apply text-gray-1000 dark:text-gray-100; 148 | } 149 | 150 | .text-secondary { 151 | @apply text-gray-700 dark:text-gray-300; 152 | } 153 | 154 | .text-tertiary { 155 | @apply text-gray-500 dark:text-gray-400; 156 | } 157 | 158 | .text-quaternary { 159 | @apply text-gray-400 dark:text-gray-500; 160 | } 161 | 162 | .bg-elevated { 163 | @apply bg-gray-400 bg-opacity-5 dark:bg-gray-50; 164 | } 165 | 166 | .bg-primary { 167 | @apply bg-gray-100/60 dark:bg-gray-1000/70; 168 | } 169 | 170 | .highlight { 171 | @apply bg-black/10 dark:bg-white/[.06]; 172 | } 173 | 174 | .button-primary-x { 175 | @apply flex items-center justify-center flex-none px-4 py-2 space-x-2 text-sm font-semibold leading-none text-gray-700 transition-all bg-white border border-gray-400 rounded-md shadow-sm cursor-pointer dark:text-gray-300 hover:text-gray-1000 dark:bg-gray-900 border-opacity-30 dark:border-opacity-30 dark:border-gray-500 dark:hover:text-white hover:border-opacity-50 dark:hover:border-opacity-50 hover:shadow-sm; 176 | } 177 | 178 | .button-primary-y { 179 | @apply flex items-center justify-center flex-none px-2 py-2 space-x-2 font-semibold leading-none text-gray-700 bg-white border border-gray-400 rounded-md shadow-sm cursor-pointer dark:text-gray-300 hover:text-gray-1000 dark:bg-gray-900 border-opacity-30 dark:border-opacity-30 dark:border-gray-500 dark:hover:text-white hover:border-opacity-50 dark:hover:border-opacity-50 hover:shadow-sm; 180 | } 181 | 182 | .filter-none { 183 | filter: none; 184 | } 185 | 186 | .filter-grayscale { 187 | filter: grayscale(100%); 188 | } 189 | 190 | .filter-saturate { 191 | -webkit-filter: brightness(105%) saturate(200%) contrast(1); 192 | filter: brightness(105%) saturate(200%) contrast(1); 193 | } 194 | 195 | .filter-blur { 196 | backdrop-filter: saturate(180%) blur(25px); 197 | } 198 | 199 | .highlight-link-hover { 200 | @apply -mx-0.5 rounded-sm bg-opacity-20 px-0.5 text-gray-1000 no-underline dark:bg-opacity-100 dark:text-gray-300 md:hover:bg-yellow-400 md:hover:bg-opacity-30 md:dark:hover:bg-yellow-500 md:dark:hover:bg-opacity-100 md:dark:hover:text-gray-1000; 201 | } 202 | 203 | .highlight-link { 204 | @apply -mx-0.5 bg-opacity-20 px-0.5 text-gray-1000 dark:bg-gray-100 dark:bg-opacity-20 dark:text-gray-300 md:hover:bg-opacity-30 md:dark:hover:bg-yellow-500 md:dark:hover:bg-opacity-100 md:dark:hover:text-gray-1000; 205 | } 206 | 207 | .blink { 208 | animation: blink-animation 1.5s steps(2, start) infinite; 209 | -webkit-animation: blink-animation 1.5s steps(2, start) infinite; 210 | } 211 | } 212 | 213 | #__next { 214 | display: flex; 215 | flex-direction: column; 216 | min-height: 100vh; 217 | } 218 | -------------------------------------------------------------------------------- /website/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | import { generateJSXMeshGradient } from 'meshgrad' 4 | 5 | import Container from 'components/Container' 6 | import cn from 'lib/classNames' 7 | 8 | import packageJSON from '../../meshgrad/package.json' 9 | 10 | const ELEMENTS = 6 11 | 12 | export default function Home() { 13 | const [isServer, setIsServer] = useState(true) 14 | const [history, setHistory] = useState([generateJSXMeshGradient(ELEMENTS)]) 15 | const [index, setIndex] = useState(0) 16 | 17 | const handleNewGradient = () => { 18 | setIndex(history.length) 19 | setHistory([...history, generateJSXMeshGradient(ELEMENTS)]) 20 | } 21 | 22 | useEffect(() => { 23 | setIsServer(false) 24 | }, []) 25 | 26 | return ( 27 | 28 |
29 | 30 |

Meshgrad

31 |

32 | A tiny utility for generating CSS Mesh Gradients. 33 |

34 |
35 | 36 | 37 |
38 | 39 | 45 |
49 |
53 |
54 | 73 | 74 | 93 |
94 |
95 |