├── .babelrc ├── .eslintrc.js ├── .gitignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── header.png ├── lib └── webcomponent │ ├── banner.js │ └── float-menu.js ├── package.json ├── postcss.config.js ├── src ├── App.jsx ├── Components │ ├── AbstractArt │ │ ├── AbstractArt.jsx │ │ └── GlowEffect.jsx │ ├── CanvasComponents │ │ ├── MainCanvas.jsx │ │ └── OuputCanvas.jsx │ ├── Home.jsx │ ├── ImageButtons │ │ └── ImageButtons.jsx │ ├── Main │ │ └── ImageSection.jsx │ ├── MainConfig │ │ └── ConfigBar.jsx │ ├── OutputSection │ │ ├── AdjustedOutput.jsx │ │ └── OutputGrid.jsx │ └── UI │ │ ├── AnimatedText.jsx │ │ ├── Button.jsx │ │ ├── Checkbox.jsx │ │ ├── Modal.jsx │ │ ├── Navbar.jsx │ │ ├── Slider.jsx │ │ ├── Spinner.jsx │ │ └── Toast.jsx ├── assets │ ├── box.svg │ ├── default.webp │ ├── download.svg │ ├── edit.svg │ ├── fence.svg │ ├── github.svg │ ├── mountain.webp │ ├── random.svg │ ├── reset.svg │ ├── ss.jpeg │ ├── top.svg │ ├── upload.svg │ └── wand.svg ├── constants.js ├── favicon.ico ├── hooks │ ├── useCanvas.jsx │ ├── useStore.jsx │ ├── useWindowSize.jsx │ └── useWorker.jsx ├── index.html ├── index.jsx ├── loader.svg ├── logo.png ├── styles │ ├── main.css │ ├── scroll.css │ ├── slider.css │ └── tailwind.css ├── utils │ ├── downloadImage.js │ ├── downscaleCanvasImage.js │ ├── dynamicCanvasResize.js │ ├── generateColors.js │ ├── generateRandomURL.js │ ├── getFileType.js │ ├── hslToRgb.js │ └── loadElement.js └── worker │ ├── runWorker.js │ └── tintWorker.js ├── ss.jpeg ├── tailwind.config.js ├── webpack.common.js ├── webpack.dev.js ├── webpack.prod.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": [ 4 | ["@babel/plugin-transform-react-jsx", { "pragma": "h" }] 5 | ] 6 | 7 | } 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: ['plugin:import/recommended', 'preact'], 7 | parserOptions: { 8 | ecmaFeatures: { 9 | jsx: true, 10 | }, 11 | ecmaVersion: 12, 12 | sourceType: 'module', 13 | }, 14 | plugins: [], 15 | settings: { 16 | 'import/resolver': { 17 | node: { 18 | extensions: ['.js', '.jsx'], 19 | }, 20 | }, 21 | }, 22 | rules: { 23 | semi: 'off', 24 | 'linebreak-style': 'off', 25 | 'import/prefer-default-export': 'off', 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log 4 | dist 5 | */package-lock.json 6 | .vscode 7 | .idea 8 | test/ts/**/*.js 9 | coverage 10 | *.sw[op] 11 | *.log 12 | package/ 13 | preact-*.tgz 14 | preact.tgz 15 | jsx-csstype.d.ts 16 | 17 | .vercel 18 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true 6 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Uxie.io 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 | # Tinter 2 | 3 | ![header](./header.png) 4 | 5 | 6 | Buy Me A Coffee
7 | 8 | Tinter is tiny web tool to generate color variation of images. We often use photoshop, just to render multiple hue variants of image and fine grain which works for us. This tool also generate monochrome colors of images with multiple variants, without hampering the quality of image. 9 | 10 | # 11 | 12 | ![image](./ss.jpeg) 13 | --- 14 | ## Features 15 | 16 | - Generate color variations of images. 17 | - Generate monoTone hues of images 18 | - Supports png, jpeg, webp. 19 | - Privacy focused. No image uploading to servers. 20 | - Customize hue to control and fine tune your images. 21 | - No Loss in Quality. 22 | 23 | --- 24 | ## Contributions 25 | 26 | We whole heartedly welcome new contributions either fixing a issue, adding a new customization or simply improving the stylings. 27 | 28 | We truly ❤️ pull requests! If you wish to help, you can learn more about how you can contribute to this project in the contribution guide. 29 | 30 | Give a star if you like It.👍 31 | 32 | --- 33 | 34 | ## Credits 35 | 36 | [Anup A.](https://github.com/anup-a) 37 | -------------------------------------------------------------------------------- /header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uxie-io/tinter/40cd7f5f3bc2314260f2fe69a2741b75f1a871d3/header.png -------------------------------------------------------------------------------- /lib/webcomponent/banner.js: -------------------------------------------------------------------------------- 1 | const template = document.createElement('template') 2 | 3 | template.innerHTML = `
Introducing Creatica: Generate unlimited vector website backgrounds! (Its free)🚀 Go to Editor
` 4 | 5 | class Banner extends HTMLElement { 6 | static get observedAttributes() { 7 | return ['isdark'] 8 | } 9 | constructor() { 10 | super() 11 | this.attachShadow({ mode: 'open' }) 12 | this.shadowRoot.appendChild(template.content.cloneNode(true)) 13 | 14 | const homeBtn = this.shadowRoot.querySelector('.btn-wrapper') 15 | const iconsDiv = this.shadowRoot.querySelector('.icons') 16 | 17 | const screenWidth = window.innerWidth 18 | 19 | if (screenWidth < 500) { 20 | homeBtn.classList.toggle('active') 21 | iconsDiv.classList.toggle('open') 22 | } 23 | } 24 | } 25 | 26 | window.customElements.define('banner-nav', Banner) 27 | 28 | export default Banner 29 | -------------------------------------------------------------------------------- /lib/webcomponent/float-menu.js: -------------------------------------------------------------------------------- 1 | const template = document.createElement('template') 2 | 3 | template.innerHTML = ` 4 | 93 | 94 | 96 | 97 |
98 |
99 |
100 | 101 |
102 |
103 |
104 | 105 | 3 106 |

svgwave

107 | 108 |
109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 |

tinter

121 |
122 | 123 | 124 |

shapes

125 |
126 | 127 | 128 |

Meshy

129 |
130 |
131 |
132 | ` 133 | 134 | class FloatMenu extends HTMLElement { 135 | constructor() { 136 | super() 137 | this.attachShadow({ mode: 'open' }) 138 | this.shadowRoot.appendChild(template.content.cloneNode(true)) 139 | 140 | const homeBtn = this.shadowRoot.querySelector('.btn-wrapper') 141 | const iconsDiv = this.shadowRoot.querySelector('.icons') 142 | 143 | homeBtn.addEventListener('click', () => { 144 | homeBtn.classList.toggle('active') 145 | iconsDiv.classList.toggle('open') 146 | }) 147 | 148 | const screenWidth = window.innerWidth 149 | 150 | if (screenWidth < 2000) { 151 | homeBtn.classList.toggle('active') 152 | iconsDiv.classList.toggle('open') 153 | } 154 | } 155 | } 156 | 157 | window.customElements.define('float-menu', FloatMenu) 158 | 159 | export default FloatMenu 160 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tinter", 3 | "version": "1.0.0", 4 | "description": "Generate tints of images", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "NODE_OPTIONS=--openssl-legacy-provider && npm run watch:css && webpack serve --config webpack.dev.js --open --hot ", 9 | "build": "set NODE_ENV=production&& npm run build:css && webpack --mode=production --devtool source-map --config webpack.prod.js", 10 | "build:css": "postcss src/styles/tailwind.css -o src/styles/main.css", 11 | "watch:css": "postcss src/styles/tailwind.css -o src/styles/main.css" 12 | }, 13 | "keywords": [ 14 | "tinter", 15 | "image", 16 | "hue", 17 | "color" 18 | ], 19 | "author": "Anup Aglawe", 20 | "license": "ISC", 21 | "dependencies": { 22 | "@headlessui/react": "^1.2.0", 23 | "@tailwindcss/forms": "^0.3.2", 24 | "file-saver": "^2.0.5", 25 | "preact": "^10.5.4", 26 | "zustand": "^3.5.1" 27 | }, 28 | "devDependencies": { 29 | "@babel/core": "^7.11.6", 30 | "@babel/plugin-transform-react-jsx": "^7.10.4", 31 | "@babel/preset-env": "^7.11.5", 32 | "@babel/preset-react": "^7.10.4", 33 | "autoprefixer": "^9.8.6", 34 | "babel-loader": "^8.1.0", 35 | "compression-webpack-plugin": "^6.0.3", 36 | "css-loader": "^4.3.0", 37 | "eslint": "^7.29.0", 38 | "eslint-config-airbnb": "^18.2.1", 39 | "eslint-config-airbnb-base": "^14.2.1", 40 | "eslint-config-preact": "^1.1.4", 41 | "eslint-plugin-import": "^2.23.4", 42 | "file-loader": "^6.1.0", 43 | "html-webpack-plugin": "^5.3.1", 44 | "mini-css-extract-plugin": "^0.11.3", 45 | "optimize-css-assets-webpack-plugin": "^5.0.4", 46 | "postcss": "^8.2.15", 47 | "postcss-cli": "^8.0.0", 48 | "postcss-loader": "^4.0.3", 49 | "style-loader": "^1.3.0", 50 | "tailwindcss": "^2.1.2", 51 | "webpack": "^5.89.0", 52 | "webpack-cli": "^4.7.0", 53 | "webpack-dev-server": "^3.11.2", 54 | "webpack-merge": "^5.7.3" 55 | }, 56 | "eslintConfig": { 57 | "extends": "preact" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require('tailwindcss'), require('autoprefixer')], 3 | } 4 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import { h } from 'preact' 2 | import Home from './Components/Home' 3 | import './../lib/webcomponent/float-menu' 4 | import './../lib/webcomponent/banner' 5 | 6 | function App() { 7 | return ( 8 |
9 | 10 | 11 | 12 |
13 | ) 14 | } 15 | 16 | export default App 17 | -------------------------------------------------------------------------------- /src/Components/AbstractArt/AbstractArt.jsx: -------------------------------------------------------------------------------- 1 | import { h } from 'preact' 2 | import MountainImage from '../../assets/mountain.webp' 3 | 4 | const AbstractArt = () => ( 5 |
6 | 97 | 98 | 30 | 39 | 40 | 69 | 70 | 71 | 72 |
73 |
74 |
75 |
76 | 77 | 78 | 79 | 89 | 90 | -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | import { h, render } from 'preact' 2 | import App from './App.jsx' 3 | import './styles/main.css' 4 | import './styles/slider.css' 5 | import './styles/scroll.css' 6 | 7 | render(, document.getElementById('app')) 8 | -------------------------------------------------------------------------------- /src/loader.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 13 | 17 | 18 | 19 | 24 | 28 | 32 | 33 | 34 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uxie-io/tinter/40cd7f5f3bc2314260f2fe69a2741b75f1a871d3/src/logo.png -------------------------------------------------------------------------------- /src/styles/main.css: -------------------------------------------------------------------------------- 1 | /*! tailwindcss v2.1.2 | MIT License | https://tailwindcss.com */ 2 | 3 | /*! modern-normalize v1.1.0 | MIT License | https://github.com/sindresorhus/modern-normalize */ 4 | 5 | /* 6 | Document 7 | ======== 8 | */ 9 | 10 | /** 11 | Use a better box model (opinionated). 12 | */ 13 | 14 | *, 15 | ::before, 16 | ::after { 17 | box-sizing: border-box; 18 | } 19 | 20 | /** 21 | Use a more readable tab size (opinionated). 22 | */ 23 | 24 | html { 25 | -moz-tab-size: 4; 26 | -o-tab-size: 4; 27 | tab-size: 4; 28 | } 29 | 30 | /** 31 | 1. Correct the line height in all browsers. 32 | 2. Prevent adjustments of font size after orientation changes in iOS. 33 | */ 34 | 35 | html { 36 | line-height: 1.15; /* 1 */ 37 | -webkit-text-size-adjust: 100%; /* 2 */ 38 | } 39 | 40 | /* 41 | Sections 42 | ======== 43 | */ 44 | 45 | /** 46 | Remove the margin in all browsers. 47 | */ 48 | 49 | body { 50 | margin: 0; 51 | } 52 | 53 | /** 54 | Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3) 55 | */ 56 | 57 | body { 58 | font-family: 59 | system-ui, 60 | -apple-system, /* Firefox supports this but not yet `system-ui` */ 61 | 'Segoe UI', 62 | Roboto, 63 | Helvetica, 64 | Arial, 65 | sans-serif, 66 | 'Apple Color Emoji', 67 | 'Segoe UI Emoji'; 68 | } 69 | 70 | /* 71 | Grouping content 72 | ================ 73 | */ 74 | 75 | /** 76 | 1. Add the correct height in Firefox. 77 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) 78 | */ 79 | 80 | hr { 81 | height: 0; /* 1 */ 82 | color: inherit; /* 2 */ 83 | } 84 | 85 | /* 86 | Text-level semantics 87 | ==================== 88 | */ 89 | 90 | /** 91 | Add the correct text decoration in Chrome, Edge, and Safari. 92 | */ 93 | 94 | abbr[title] { 95 | -webkit-text-decoration: underline dotted; 96 | text-decoration: underline dotted; 97 | } 98 | 99 | /** 100 | Add the correct font weight in Edge and Safari. 101 | */ 102 | 103 | b, 104 | strong { 105 | font-weight: bolder; 106 | } 107 | 108 | /** 109 | 1. Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3) 110 | 2. Correct the odd 'em' font sizing in all browsers. 111 | */ 112 | 113 | code, 114 | kbd, 115 | samp, 116 | pre { 117 | font-family: 118 | ui-monospace, 119 | SFMono-Regular, 120 | Consolas, 121 | 'Liberation Mono', 122 | Menlo, 123 | monospace; /* 1 */ 124 | font-size: 1em; /* 2 */ 125 | } 126 | 127 | /** 128 | Add the correct font size in all browsers. 129 | */ 130 | 131 | small { 132 | font-size: 80%; 133 | } 134 | 135 | /** 136 | Prevent 'sub' and 'sup' elements from affecting the line height in all browsers. 137 | */ 138 | 139 | sub, 140 | sup { 141 | font-size: 75%; 142 | line-height: 0; 143 | position: relative; 144 | vertical-align: baseline; 145 | } 146 | 147 | sub { 148 | bottom: -0.25em; 149 | } 150 | 151 | sup { 152 | top: -0.5em; 153 | } 154 | 155 | /* 156 | Tabular data 157 | ============ 158 | */ 159 | 160 | /** 161 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) 162 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) 163 | */ 164 | 165 | table { 166 | text-indent: 0; /* 1 */ 167 | border-color: inherit; /* 2 */ 168 | } 169 | 170 | /* 171 | Forms 172 | ===== 173 | */ 174 | 175 | /** 176 | 1. Change the font styles in all browsers. 177 | 2. Remove the margin in Firefox and Safari. 178 | */ 179 | 180 | button, 181 | input, 182 | optgroup, 183 | select, 184 | textarea { 185 | font-family: inherit; /* 1 */ 186 | font-size: 100%; /* 1 */ 187 | line-height: 1.15; /* 1 */ 188 | margin: 0; /* 2 */ 189 | } 190 | 191 | /** 192 | Remove the inheritance of text transform in Edge and Firefox. 193 | 1. Remove the inheritance of text transform in Firefox. 194 | */ 195 | 196 | button, 197 | select { /* 1 */ 198 | text-transform: none; 199 | } 200 | 201 | /** 202 | Correct the inability to style clickable types in iOS and Safari. 203 | */ 204 | 205 | button, 206 | [type='button'], 207 | [type='reset'] { 208 | -webkit-appearance: button; 209 | } 210 | 211 | /** 212 | Remove the inner border and padding in Firefox. 213 | */ 214 | 215 | /** 216 | Restore the focus styles unset by the previous rule. 217 | */ 218 | 219 | /** 220 | Remove the additional ':invalid' styles in Firefox. 221 | See: https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737 222 | */ 223 | 224 | /** 225 | Remove the padding so developers are not caught out when they zero out 'fieldset' elements in all browsers. 226 | */ 227 | 228 | legend { 229 | padding: 0; 230 | } 231 | 232 | /** 233 | Add the correct vertical alignment in Chrome and Firefox. 234 | */ 235 | 236 | progress { 237 | vertical-align: baseline; 238 | } 239 | 240 | /** 241 | Correct the cursor style of increment and decrement buttons in Safari. 242 | */ 243 | 244 | /** 245 | 1. Correct the odd appearance in Chrome and Safari. 246 | 2. Correct the outline style in Safari. 247 | */ 248 | 249 | /** 250 | Remove the inner padding in Chrome and Safari on macOS. 251 | */ 252 | 253 | /** 254 | 1. Correct the inability to style clickable types in iOS and Safari. 255 | 2. Change font properties to 'inherit' in Safari. 256 | */ 257 | 258 | /* 259 | Interactive 260 | =========== 261 | */ 262 | 263 | /* 264 | Add the correct display in Chrome and Safari. 265 | */ 266 | 267 | summary { 268 | display: list-item; 269 | } 270 | 271 | /** 272 | * Manually forked from SUIT CSS Base: https://github.com/suitcss/base 273 | * A thin layer on top of normalize.css that provides a starting point more 274 | * suitable for web applications. 275 | */ 276 | 277 | /** 278 | * Removes the default spacing and border for appropriate elements. 279 | */ 280 | 281 | blockquote, 282 | dl, 283 | dd, 284 | h1, 285 | h2, 286 | h3, 287 | h4, 288 | h5, 289 | h6, 290 | hr, 291 | figure, 292 | p, 293 | pre { 294 | margin: 0; 295 | } 296 | 297 | button { 298 | background-color: transparent; 299 | background-image: none; 300 | } 301 | 302 | /** 303 | * Work around a Firefox/IE bug where the transparent `button` background 304 | * results in a loss of the default `button` focus styles. 305 | */ 306 | 307 | button:focus { 308 | outline: 1px dotted; 309 | outline: 5px auto -webkit-focus-ring-color; 310 | } 311 | 312 | fieldset { 313 | margin: 0; 314 | padding: 0; 315 | } 316 | 317 | ol, 318 | ul { 319 | list-style: none; 320 | margin: 0; 321 | padding: 0; 322 | } 323 | 324 | /** 325 | * Tailwind custom reset styles 326 | */ 327 | 328 | /** 329 | * 1. Use the user's configured `sans` font-family (with Tailwind's default 330 | * sans-serif font stack as a fallback) as a sane default. 331 | * 2. Use Tailwind's default "normal" line-height so the user isn't forced 332 | * to override it to ensure consistency even when using the default theme. 333 | */ 334 | 335 | html { 336 | font-family: Poppins, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; /* 1 */ 337 | line-height: 1.5; /* 2 */ 338 | } 339 | 340 | /** 341 | * Inherit font-family and line-height from `html` so users can set them as 342 | * a class directly on the `html` element. 343 | */ 344 | 345 | body { 346 | font-family: inherit; 347 | line-height: inherit; 348 | } 349 | 350 | /** 351 | * 1. Prevent padding and border from affecting element width. 352 | * 353 | * We used to set this in the html element and inherit from 354 | * the parent element for everything else. This caused issues 355 | * in shadow-dom-enhanced elements like
where the content 356 | * is wrapped by a div with box-sizing set to `content-box`. 357 | * 358 | * https://github.com/mozdevs/cssremedy/issues/4 359 | * 360 | * 361 | * 2. Allow adding a border to an element by just adding a border-width. 362 | * 363 | * By default, the way the browser specifies that an element should have no 364 | * border is by setting it's border-style to `none` in the user-agent 365 | * stylesheet. 366 | * 367 | * In order to easily add borders to elements by just setting the `border-width` 368 | * property, we change the default border-style for all elements to `solid`, and 369 | * use border-width to hide them instead. This way our `border` utilities only 370 | * need to set the `border-width` property instead of the entire `border` 371 | * shorthand, making our border utilities much more straightforward to compose. 372 | * 373 | * https://github.com/tailwindcss/tailwindcss/pull/116 374 | */ 375 | 376 | *, 377 | ::before, 378 | ::after { 379 | box-sizing: border-box; /* 1 */ 380 | border-width: 0; /* 2 */ 381 | border-style: solid; /* 2 */ 382 | border-color: #e5e7eb; /* 2 */ 383 | } 384 | 385 | /* 386 | * Ensure horizontal rules are visible by default 387 | */ 388 | 389 | hr { 390 | border-top-width: 1px; 391 | } 392 | 393 | /** 394 | * Undo the `border-style: none` reset that Normalize applies to images so that 395 | * our `border-{width}` utilities have the expected effect. 396 | * 397 | * The Normalize reset is unnecessary for us since we default the border-width 398 | * to 0 on all elements. 399 | * 400 | * https://github.com/tailwindcss/tailwindcss/issues/362 401 | */ 402 | 403 | img { 404 | border-style: solid; 405 | } 406 | 407 | textarea { 408 | resize: vertical; 409 | } 410 | 411 | input::-moz-placeholder, textarea::-moz-placeholder { 412 | opacity: 1; 413 | color: #9ca3af; 414 | } 415 | 416 | input:-ms-input-placeholder, textarea:-ms-input-placeholder { 417 | opacity: 1; 418 | color: #9ca3af; 419 | } 420 | 421 | input::placeholder, 422 | textarea::placeholder { 423 | opacity: 1; 424 | color: #9ca3af; 425 | } 426 | 427 | button { 428 | cursor: pointer; 429 | } 430 | 431 | table { 432 | border-collapse: collapse; 433 | } 434 | 435 | h1, 436 | h2, 437 | h3, 438 | h4, 439 | h5, 440 | h6 { 441 | font-size: inherit; 442 | font-weight: inherit; 443 | } 444 | 445 | /** 446 | * Reset links to optimize for opt-in styling instead of 447 | * opt-out. 448 | */ 449 | 450 | a { 451 | color: inherit; 452 | text-decoration: inherit; 453 | } 454 | 455 | /** 456 | * Reset form element properties that are easy to forget to 457 | * style explicitly so you don't inadvertently introduce 458 | * styles that deviate from your design system. These styles 459 | * supplement a partial reset that is already applied by 460 | * normalize.css. 461 | */ 462 | 463 | button, 464 | input, 465 | optgroup, 466 | select, 467 | textarea { 468 | padding: 0; 469 | line-height: inherit; 470 | color: inherit; 471 | } 472 | 473 | /** 474 | * Use the configured 'mono' font family for elements that 475 | * are expected to be rendered with a monospace font, falling 476 | * back to the system monospace stack if there is no configured 477 | * 'mono' font family. 478 | */ 479 | 480 | pre, 481 | code, 482 | kbd, 483 | samp { 484 | font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 485 | } 486 | 487 | /** 488 | * Make replaced elements `display: block` by default as that's 489 | * the behavior you want almost all of the time. Inspired by 490 | * CSS Remedy, with `svg` added as well. 491 | * 492 | * https://github.com/mozdevs/cssremedy/issues/14 493 | */ 494 | 495 | img, 496 | svg, 497 | video, 498 | canvas, 499 | audio, 500 | iframe, 501 | embed, 502 | object { 503 | display: block; 504 | vertical-align: middle; 505 | } 506 | 507 | /** 508 | * Constrain images and videos to the parent width and preserve 509 | * their intrinsic aspect ratio. 510 | * 511 | * https://github.com/mozdevs/cssremedy/issues/14 512 | */ 513 | 514 | img, 515 | video { 516 | max-width: 100%; 517 | height: auto; 518 | } 519 | 520 | [type='text'],[type='url'],[type='time'],textarea,select { 521 | -webkit-appearance: none; 522 | -moz-appearance: none; 523 | appearance: none; 524 | background-color: #fff; 525 | border-color: #6b7280; 526 | border-width: 1px; 527 | border-radius: 0px; 528 | padding-top: 0.5rem; 529 | padding-right: 0.75rem; 530 | padding-bottom: 0.5rem; 531 | padding-left: 0.75rem; 532 | font-size: 1rem; 533 | line-height: 1.5rem; 534 | } 535 | 536 | [type='text']:focus, [type='url']:focus, [type='time']:focus, textarea:focus, select:focus { 537 | outline: 2px solid transparent; 538 | outline-offset: 2px; 539 | --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/); 540 | --tw-ring-offset-width: 0px; 541 | --tw-ring-offset-color: #fff; 542 | --tw-ring-color: #2563eb; 543 | --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); 544 | --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); 545 | box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); 546 | border-color: #2563eb; 547 | } 548 | 549 | input::-moz-placeholder, textarea::-moz-placeholder { 550 | color: #6b7280; 551 | opacity: 1; 552 | } 553 | 554 | input:-ms-input-placeholder, textarea:-ms-input-placeholder { 555 | color: #6b7280; 556 | opacity: 1; 557 | } 558 | 559 | input::placeholder,textarea::placeholder { 560 | color: #6b7280; 561 | opacity: 1; 562 | } 563 | 564 | select { 565 | background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e"); 566 | background-position: right 0.5rem center; 567 | background-repeat: no-repeat; 568 | background-size: 1.5em 1.5em; 569 | padding-right: 2.5rem; 570 | -webkit-print-color-adjust: exact; 571 | color-adjust: exact; 572 | } 573 | 574 | [type='checkbox'] { 575 | -webkit-appearance: none; 576 | -moz-appearance: none; 577 | appearance: none; 578 | padding: 0; 579 | -webkit-print-color-adjust: exact; 580 | color-adjust: exact; 581 | display: inline-block; 582 | vertical-align: middle; 583 | background-origin: border-box; 584 | -webkit-user-select: none; 585 | -moz-user-select: none; 586 | -ms-user-select: none; 587 | user-select: none; 588 | flex-shrink: 0; 589 | height: 1rem; 590 | width: 1rem; 591 | color: #2563eb; 592 | background-color: #fff; 593 | border-color: #6b7280; 594 | border-width: 1px; 595 | } 596 | 597 | [type='checkbox'] { 598 | border-radius: 0px; 599 | } 600 | 601 | [type='checkbox']:focus { 602 | outline: 2px solid transparent; 603 | outline-offset: 2px; 604 | --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/); 605 | --tw-ring-offset-width: 2px; 606 | --tw-ring-offset-color: #fff; 607 | --tw-ring-color: #2563eb; 608 | --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); 609 | --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); 610 | box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); 611 | } 612 | 613 | [type='checkbox']:checked { 614 | border-color: transparent; 615 | background-color: currentColor; 616 | background-size: 100% 100%; 617 | background-position: center; 618 | background-repeat: no-repeat; 619 | } 620 | 621 | [type='checkbox']:checked { 622 | background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e"); 623 | } 624 | 625 | [type='checkbox']:checked:hover,[type='checkbox']:checked:focus { 626 | border-color: transparent; 627 | background-color: currentColor; 628 | } 629 | 630 | [type='checkbox']:indeterminate { 631 | background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e"); 632 | border-color: transparent; 633 | background-color: currentColor; 634 | background-size: 100% 100%; 635 | background-position: center; 636 | background-repeat: no-repeat; 637 | } 638 | 639 | [type='checkbox']:indeterminate:hover,[type='checkbox']:indeterminate:focus { 640 | border-color: transparent; 641 | background-color: currentColor; 642 | } 643 | 644 | [type='file'] { 645 | background: unset; 646 | border-color: inherit; 647 | border-width: 0; 648 | border-radius: 0; 649 | padding: 0; 650 | font-size: unset; 651 | line-height: inherit; 652 | } 653 | 654 | [type='file']:focus { 655 | outline: 1px auto -webkit-focus-ring-color; 656 | } 657 | 658 | .space-y-4 > :not([hidden]) ~ :not([hidden]) { 659 | --tw-space-y-reverse: 0; 660 | margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse))); 661 | margin-bottom: calc(1rem * var(--tw-space-y-reverse)); 662 | } 663 | 664 | .sr-only { 665 | position: absolute; 666 | width: 1px; 667 | height: 1px; 668 | padding: 0; 669 | margin: -1px; 670 | overflow: hidden; 671 | clip: rect(0, 0, 0, 0); 672 | white-space: nowrap; 673 | border-width: 0; 674 | } 675 | 676 | .appearance-none { 677 | -webkit-appearance: none; 678 | -moz-appearance: none; 679 | appearance: none; 680 | } 681 | 682 | .bg-black { 683 | --tw-bg-opacity: 1; 684 | background-color: rgba(0, 0, 0, var(--tw-bg-opacity)); 685 | } 686 | 687 | .bg-white { 688 | --tw-bg-opacity: 1; 689 | background-color: rgba(255, 255, 255, var(--tw-bg-opacity)); 690 | } 691 | 692 | .bg-blue-100 { 693 | --tw-bg-opacity: 1; 694 | background-color: rgba(219, 234, 254, var(--tw-bg-opacity)); 695 | } 696 | 697 | .bg-darkish-black { 698 | --tw-bg-opacity: 1; 699 | background-color: rgba(28, 28, 31, var(--tw-bg-opacity)); 700 | } 701 | 702 | .bg-grey-light { 703 | background-color: F5F5F5; 704 | } 705 | 706 | .hover\:bg-blue-200:hover { 707 | --tw-bg-opacity: 1; 708 | background-color: rgba(191, 219, 254, var(--tw-bg-opacity)); 709 | } 710 | 711 | .bg-gradient-to-br { 712 | background-image: linear-gradient(to bottom right, var(--tw-gradient-stops)); 713 | } 714 | 715 | .from-yellow-400 { 716 | --tw-gradient-from: #fbbf24; 717 | --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, rgba(251, 191, 36, 0)); 718 | } 719 | 720 | .via-red-500 { 721 | --tw-gradient-stops: var(--tw-gradient-from), #ef4444, var(--tw-gradient-to, rgba(239, 68, 68, 0)); 722 | } 723 | 724 | .to-pink-500 { 725 | --tw-gradient-to: #ec4899; 726 | } 727 | 728 | .border-transparent { 729 | border-color: transparent; 730 | } 731 | 732 | .border-gray-500 { 733 | --tw-border-opacity: 1; 734 | border-color: rgba(107, 114, 128, var(--tw-border-opacity)); 735 | } 736 | 737 | .rounded { 738 | border-radius: 0.25rem; 739 | } 740 | 741 | .rounded-md { 742 | border-radius: 0.375rem; 743 | } 744 | 745 | .rounded-lg { 746 | border-radius: 0.5rem; 747 | } 748 | 749 | .rounded-2xl { 750 | border-radius: 1rem; 751 | } 752 | 753 | .rounded-full { 754 | border-radius: 9999px; 755 | } 756 | 757 | .border-dashed { 758 | border-style: dashed; 759 | } 760 | 761 | .border { 762 | border-width: 1px; 763 | } 764 | 765 | .cursor-pointer { 766 | cursor: pointer; 767 | } 768 | 769 | .block { 770 | display: block; 771 | } 772 | 773 | .inline-block { 774 | display: inline-block; 775 | } 776 | 777 | .flex { 778 | display: flex; 779 | } 780 | 781 | .inline-flex { 782 | display: inline-flex; 783 | } 784 | 785 | .table { 786 | display: table; 787 | } 788 | 789 | .grid { 790 | display: grid; 791 | } 792 | 793 | .contents { 794 | display: contents; 795 | } 796 | 797 | .hidden { 798 | display: none; 799 | } 800 | 801 | .flex-col { 802 | flex-direction: column; 803 | } 804 | 805 | .items-start { 806 | align-items: flex-start; 807 | } 808 | 809 | .items-end { 810 | align-items: flex-end; 811 | } 812 | 813 | .items-center { 814 | align-items: center; 815 | } 816 | 817 | .justify-start { 818 | justify-content: flex-start; 819 | } 820 | 821 | .justify-end { 822 | justify-content: flex-end; 823 | } 824 | 825 | .justify-center { 826 | justify-content: center; 827 | } 828 | 829 | .justify-between { 830 | justify-content: space-between; 831 | } 832 | 833 | .justify-around { 834 | justify-content: space-around; 835 | } 836 | 837 | .justify-evenly { 838 | justify-content: space-evenly; 839 | } 840 | 841 | .flex-1 { 842 | flex: 1 1 0%; 843 | } 844 | 845 | .flex-shrink-0 { 846 | flex-shrink: 0; 847 | } 848 | 849 | .float-right { 850 | float: right; 851 | } 852 | 853 | .font-plex { 854 | font-family: "IBM Plex Sans", sans-serif; 855 | } 856 | 857 | .font-medium { 858 | font-weight: 500; 859 | } 860 | 861 | .font-semibold { 862 | font-weight: 600; 863 | } 864 | 865 | .font-bold { 866 | font-weight: 700; 867 | } 868 | 869 | .font-extrabold { 870 | font-weight: 800; 871 | } 872 | 873 | .h-0 { 874 | height: 0px; 875 | } 876 | 877 | .h-2 { 878 | height: 0.5rem; 879 | } 880 | 881 | .h-4 { 882 | height: 1rem; 883 | } 884 | 885 | .h-5 { 886 | height: 1.25rem; 887 | } 888 | 889 | .h-6 { 890 | height: 1.5rem; 891 | } 892 | 893 | .h-8 { 894 | height: 2rem; 895 | } 896 | 897 | .h-20 { 898 | height: 5rem; 899 | } 900 | 901 | .h-36 { 902 | height: 9rem; 903 | } 904 | 905 | .h-full { 906 | height: 100%; 907 | } 908 | 909 | .h-screen { 910 | height: 100vh; 911 | } 912 | 913 | .text-sm { 914 | font-size: 0.875rem; 915 | line-height: 1.25rem; 916 | } 917 | 918 | .text-lg { 919 | font-size: 1.125rem; 920 | line-height: 1.75rem; 921 | } 922 | 923 | .text-3xl { 924 | font-size: 1.875rem; 925 | line-height: 2.25rem; 926 | } 927 | 928 | .m-1 { 929 | margin: 0.25rem; 930 | } 931 | 932 | .m-2 { 933 | margin: 0.5rem; 934 | } 935 | 936 | .m-auto { 937 | margin: auto; 938 | } 939 | 940 | .mx-0 { 941 | margin-left: 0px; 942 | margin-right: 0px; 943 | } 944 | 945 | .my-2 { 946 | margin-top: 0.5rem; 947 | margin-bottom: 0.5rem; 948 | } 949 | 950 | .my-4 { 951 | margin-top: 1rem; 952 | margin-bottom: 1rem; 953 | } 954 | 955 | .mx-4 { 956 | margin-left: 1rem; 957 | margin-right: 1rem; 958 | } 959 | 960 | .my-8 { 961 | margin-top: 2rem; 962 | margin-bottom: 2rem; 963 | } 964 | 965 | .mx-auto { 966 | margin-left: auto; 967 | margin-right: auto; 968 | } 969 | 970 | .mr-2 { 971 | margin-right: 0.5rem; 972 | } 973 | 974 | .ml-2 { 975 | margin-left: 0.5rem; 976 | } 977 | 978 | .mr-3 { 979 | margin-right: 0.75rem; 980 | } 981 | 982 | .ml-3 { 983 | margin-left: 0.75rem; 984 | } 985 | 986 | .mr-4 { 987 | margin-right: 1rem; 988 | } 989 | 990 | .mb-4 { 991 | margin-bottom: 1rem; 992 | } 993 | 994 | .ml-4 { 995 | margin-left: 1rem; 996 | } 997 | 998 | .mr-6 { 999 | margin-right: 1.5rem; 1000 | } 1001 | 1002 | .ml-6 { 1003 | margin-left: 1.5rem; 1004 | } 1005 | 1006 | .mt-8 { 1007 | margin-top: 2rem; 1008 | } 1009 | 1010 | .mb-8 { 1011 | margin-bottom: 2rem; 1012 | } 1013 | 1014 | .mb-24 { 1015 | margin-bottom: 6rem; 1016 | } 1017 | 1018 | .-ml-1 { 1019 | margin-left: -0.25rem; 1020 | } 1021 | 1022 | .max-w-sm { 1023 | max-width: 24rem; 1024 | } 1025 | 1026 | .max-w-lg { 1027 | max-width: 32rem; 1028 | } 1029 | 1030 | .max-w-full { 1031 | max-width: 100%; 1032 | } 1033 | 1034 | .min-h-screen { 1035 | min-height: 100vh; 1036 | } 1037 | 1038 | .opacity-0 { 1039 | opacity: 0; 1040 | } 1041 | 1042 | .opacity-25 { 1043 | opacity: 0.25; 1044 | } 1045 | 1046 | .opacity-30 { 1047 | opacity: 0.3; 1048 | } 1049 | 1050 | .opacity-75 { 1051 | opacity: 0.75; 1052 | } 1053 | 1054 | .opacity-100 { 1055 | opacity: 1; 1056 | } 1057 | 1058 | .focus\:outline-none:focus { 1059 | outline: 2px solid transparent; 1060 | outline-offset: 2px; 1061 | } 1062 | 1063 | .overflow-hidden { 1064 | overflow: hidden; 1065 | } 1066 | 1067 | .overflow-y-auto { 1068 | overflow-y: auto; 1069 | } 1070 | 1071 | .p-2 { 1072 | padding: 0.5rem; 1073 | } 1074 | 1075 | .p-4 { 1076 | padding: 1rem; 1077 | } 1078 | 1079 | .p-6 { 1080 | padding: 1.5rem; 1081 | } 1082 | 1083 | .p-10 { 1084 | padding: 2.5rem; 1085 | } 1086 | 1087 | .p-20 { 1088 | padding: 5rem; 1089 | } 1090 | 1091 | .py-2 { 1092 | padding-top: 0.5rem; 1093 | padding-bottom: 0.5rem; 1094 | } 1095 | 1096 | .px-4 { 1097 | padding-left: 1rem; 1098 | padding-right: 1rem; 1099 | } 1100 | 1101 | .py-6 { 1102 | padding-top: 1.5rem; 1103 | padding-bottom: 1.5rem; 1104 | } 1105 | 1106 | .py-8 { 1107 | padding-top: 2rem; 1108 | padding-bottom: 2rem; 1109 | } 1110 | 1111 | .pt-0 { 1112 | padding-top: 0px; 1113 | } 1114 | 1115 | .pt-0\.5 { 1116 | padding-top: 0.125rem; 1117 | } 1118 | 1119 | .pointer-events-none { 1120 | pointer-events: none; 1121 | } 1122 | 1123 | .pointer-events-auto { 1124 | pointer-events: auto; 1125 | } 1126 | 1127 | .fixed { 1128 | position: fixed; 1129 | } 1130 | 1131 | .absolute { 1132 | position: absolute; 1133 | } 1134 | 1135 | .relative { 1136 | position: relative; 1137 | } 1138 | 1139 | .inset-0 { 1140 | top: 0px; 1141 | right: 0px; 1142 | bottom: 0px; 1143 | left: 0px; 1144 | } 1145 | 1146 | .top-0 { 1147 | top: 0px; 1148 | } 1149 | 1150 | .right-0 { 1151 | right: 0px; 1152 | } 1153 | 1154 | .left-0 { 1155 | left: 0px; 1156 | } 1157 | 1158 | .bottom-3 { 1159 | bottom: 0.75rem; 1160 | } 1161 | 1162 | .left-3 { 1163 | left: 0.75rem; 1164 | } 1165 | 1166 | .resize { 1167 | resize: both; 1168 | } 1169 | 1170 | * { 1171 | --tw-shadow: 0 0 #0000; 1172 | } 1173 | 1174 | .shadow-lg { 1175 | --tw-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); 1176 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1177 | } 1178 | 1179 | .shadow-xl { 1180 | --tw-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); 1181 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1182 | } 1183 | 1184 | * { 1185 | --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/); 1186 | --tw-ring-offset-width: 0px; 1187 | --tw-ring-offset-color: #fff; 1188 | --tw-ring-color: rgba(59, 130, 246, 0.5); 1189 | --tw-ring-offset-shadow: 0 0 #0000; 1190 | --tw-ring-shadow: 0 0 #0000; 1191 | } 1192 | 1193 | .ring-1 { 1194 | --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); 1195 | --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); 1196 | box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); 1197 | } 1198 | 1199 | .focus\:ring-2:focus { 1200 | --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); 1201 | --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); 1202 | box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); 1203 | } 1204 | 1205 | .focus\:ring-offset-2:focus { 1206 | --tw-ring-offset-width: 2px; 1207 | } 1208 | 1209 | .ring-black { 1210 | --tw-ring-opacity: 1; 1211 | --tw-ring-color: rgba(0, 0, 0, var(--tw-ring-opacity)); 1212 | } 1213 | 1214 | .focus\:ring-indigo-500:focus { 1215 | --tw-ring-opacity: 1; 1216 | --tw-ring-color: rgba(99, 102, 241, var(--tw-ring-opacity)); 1217 | } 1218 | 1219 | .focus\:ring-pink-500:focus { 1220 | --tw-ring-opacity: 1; 1221 | --tw-ring-color: rgba(236, 72, 153, var(--tw-ring-opacity)); 1222 | } 1223 | 1224 | .ring-opacity-5 { 1225 | --tw-ring-opacity: 0.05; 1226 | } 1227 | 1228 | .text-left { 1229 | text-align: left; 1230 | } 1231 | 1232 | .text-center { 1233 | text-align: center; 1234 | } 1235 | 1236 | .text-black { 1237 | --tw-text-opacity: 1; 1238 | color: rgba(0, 0, 0, var(--tw-text-opacity)); 1239 | } 1240 | 1241 | .text-white { 1242 | --tw-text-opacity: 1; 1243 | color: rgba(255, 255, 255, var(--tw-text-opacity)); 1244 | } 1245 | 1246 | .text-gray-400 { 1247 | --tw-text-opacity: 1; 1248 | color: rgba(156, 163, 175, var(--tw-text-opacity)); 1249 | } 1250 | 1251 | .text-gray-900 { 1252 | --tw-text-opacity: 1; 1253 | color: rgba(17, 24, 39, var(--tw-text-opacity)); 1254 | } 1255 | 1256 | .text-blue-900 { 1257 | --tw-text-opacity: 1; 1258 | color: rgba(30, 58, 138, var(--tw-text-opacity)); 1259 | } 1260 | 1261 | .text-pink-500 { 1262 | --tw-text-opacity: 1; 1263 | color: rgba(236, 72, 153, var(--tw-text-opacity)); 1264 | } 1265 | 1266 | .hover\:text-gray-500:hover { 1267 | --tw-text-opacity: 1; 1268 | color: rgba(107, 114, 128, var(--tw-text-opacity)); 1269 | } 1270 | 1271 | .align-middle { 1272 | vertical-align: middle; 1273 | } 1274 | 1275 | .align-bottom { 1276 | vertical-align: bottom; 1277 | } 1278 | 1279 | .w-0 { 1280 | width: 0px; 1281 | } 1282 | 1283 | .w-4 { 1284 | width: 1rem; 1285 | } 1286 | 1287 | .w-5 { 1288 | width: 1.25rem; 1289 | } 1290 | 1291 | .w-6 { 1292 | width: 1.5rem; 1293 | } 1294 | 1295 | .w-8 { 1296 | width: 2rem; 1297 | } 1298 | 1299 | .w-36 { 1300 | width: 9rem; 1301 | } 1302 | 1303 | .w-full { 1304 | width: 100%; 1305 | } 1306 | 1307 | .z-10 { 1308 | z-index: 10; 1309 | } 1310 | 1311 | .z-50 { 1312 | z-index: 50; 1313 | } 1314 | 1315 | .grid-cols-2 { 1316 | grid-template-columns: repeat(2, minmax(0, 1fr)); 1317 | } 1318 | 1319 | .transform { 1320 | --tw-translate-x: 0; 1321 | --tw-translate-y: 0; 1322 | --tw-rotate: 0; 1323 | --tw-skew-x: 0; 1324 | --tw-skew-y: 0; 1325 | --tw-scale-x: 1; 1326 | --tw-scale-y: 1; 1327 | transform: translateX(var(--tw-translate-x)) translateY(var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); 1328 | } 1329 | 1330 | .scale-95 { 1331 | --tw-scale-x: .95; 1332 | --tw-scale-y: .95; 1333 | } 1334 | 1335 | .scale-100 { 1336 | --tw-scale-x: 1; 1337 | --tw-scale-y: 1; 1338 | } 1339 | 1340 | .translate-y-0 { 1341 | --tw-translate-y: 0px; 1342 | } 1343 | 1344 | .translate-y-2 { 1345 | --tw-translate-y: 0.5rem; 1346 | } 1347 | 1348 | .transition-all { 1349 | transition-property: all; 1350 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 1351 | transition-duration: 150ms; 1352 | } 1353 | 1354 | .transition { 1355 | transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; 1356 | transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; 1357 | transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; 1358 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 1359 | transition-duration: 150ms; 1360 | } 1361 | 1362 | .ease-in { 1363 | transition-timing-function: cubic-bezier(0.4, 0, 1, 1); 1364 | } 1365 | 1366 | .ease-out { 1367 | transition-timing-function: cubic-bezier(0, 0, 0.2, 1); 1368 | } 1369 | 1370 | .duration-100 { 1371 | transition-duration: 100ms; 1372 | } 1373 | 1374 | .duration-200 { 1375 | transition-duration: 200ms; 1376 | } 1377 | 1378 | .duration-300 { 1379 | transition-duration: 300ms; 1380 | } 1381 | 1382 | @-webkit-keyframes spin { 1383 | to { 1384 | transform: rotate(360deg); 1385 | } 1386 | } 1387 | 1388 | @keyframes spin { 1389 | to { 1390 | transform: rotate(360deg); 1391 | } 1392 | } 1393 | 1394 | @-webkit-keyframes ping { 1395 | 75%, 100% { 1396 | transform: scale(2); 1397 | opacity: 0; 1398 | } 1399 | } 1400 | 1401 | @keyframes ping { 1402 | 75%, 100% { 1403 | transform: scale(2); 1404 | opacity: 0; 1405 | } 1406 | } 1407 | 1408 | @-webkit-keyframes pulse { 1409 | 50% { 1410 | opacity: .5; 1411 | } 1412 | } 1413 | 1414 | @keyframes pulse { 1415 | 50% { 1416 | opacity: .5; 1417 | } 1418 | } 1419 | 1420 | @-webkit-keyframes bounce { 1421 | 0%, 100% { 1422 | transform: translateY(-25%); 1423 | -webkit-animation-timing-function: cubic-bezier(0.8,0,1,1); 1424 | animation-timing-function: cubic-bezier(0.8,0,1,1); 1425 | } 1426 | 1427 | 50% { 1428 | transform: none; 1429 | -webkit-animation-timing-function: cubic-bezier(0,0,0.2,1); 1430 | animation-timing-function: cubic-bezier(0,0,0.2,1); 1431 | } 1432 | } 1433 | 1434 | @keyframes bounce { 1435 | 0%, 100% { 1436 | transform: translateY(-25%); 1437 | -webkit-animation-timing-function: cubic-bezier(0.8,0,1,1); 1438 | animation-timing-function: cubic-bezier(0.8,0,1,1); 1439 | } 1440 | 1441 | 50% { 1442 | transform: none; 1443 | -webkit-animation-timing-function: cubic-bezier(0,0,0.2,1); 1444 | animation-timing-function: cubic-bezier(0,0,0.2,1); 1445 | } 1446 | } 1447 | 1448 | .animate-spin { 1449 | -webkit-animation: spin 1s linear infinite; 1450 | animation: spin 1s linear infinite; 1451 | } 1452 | 1453 | .filter { 1454 | --tw-blur: var(--tw-empty,/*!*/ /*!*/); 1455 | --tw-brightness: var(--tw-empty,/*!*/ /*!*/); 1456 | --tw-contrast: var(--tw-empty,/*!*/ /*!*/); 1457 | --tw-grayscale: var(--tw-empty,/*!*/ /*!*/); 1458 | --tw-hue-rotate: var(--tw-empty,/*!*/ /*!*/); 1459 | --tw-invert: var(--tw-empty,/*!*/ /*!*/); 1460 | --tw-saturate: var(--tw-empty,/*!*/ /*!*/); 1461 | --tw-sepia: var(--tw-empty,/*!*/ /*!*/); 1462 | --tw-drop-shadow: var(--tw-empty,/*!*/ /*!*/); 1463 | filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); 1464 | } 1465 | 1466 | .blur { 1467 | --tw-blur: blur(8px); 1468 | } 1469 | 1470 | @media (min-width: 640px) { 1471 | .sm\:items-start { 1472 | align-items: flex-start; 1473 | } 1474 | 1475 | .sm\:items-end { 1476 | align-items: flex-end; 1477 | } 1478 | 1479 | .sm\:p-6 { 1480 | padding: 1.5rem; 1481 | } 1482 | 1483 | .sm\:translate-x-0 { 1484 | --tw-translate-x: 0px; 1485 | } 1486 | 1487 | .sm\:translate-x-2 { 1488 | --tw-translate-x: 0.5rem; 1489 | } 1490 | 1491 | .sm\:translate-y-0 { 1492 | --tw-translate-y: 0px; 1493 | } 1494 | } 1495 | 1496 | @media (min-width: 768px) { 1497 | .md\:h-12 { 1498 | height: 3rem; 1499 | } 1500 | 1501 | .md\:h-auto { 1502 | height: auto; 1503 | } 1504 | 1505 | .md\:mx-16 { 1506 | margin-left: 4rem; 1507 | margin-right: 4rem; 1508 | } 1509 | 1510 | .md\:w-12 { 1511 | width: 3rem; 1512 | } 1513 | } 1514 | 1515 | @media (min-width: 1024px) { 1516 | .lg\:flex-row { 1517 | flex-direction: row; 1518 | } 1519 | 1520 | .lg\:h-2\/5 { 1521 | height: 40%; 1522 | } 1523 | 1524 | .lg\:h-3\/5 { 1525 | height: 60%; 1526 | } 1527 | 1528 | .lg\:h-screen { 1529 | height: 100vh; 1530 | } 1531 | 1532 | .lg\:text-5xl { 1533 | font-size: 3rem; 1534 | line-height: 1; 1535 | } 1536 | 1537 | .lg\:mx-8 { 1538 | margin-left: 2rem; 1539 | margin-right: 2rem; 1540 | } 1541 | 1542 | .lg\:mx-16 { 1543 | margin-left: 4rem; 1544 | margin-right: 4rem; 1545 | } 1546 | 1547 | .lg\:mb-0 { 1548 | margin-bottom: 0px; 1549 | } 1550 | 1551 | .lg\:mb-72 { 1552 | margin-bottom: 18rem; 1553 | } 1554 | 1555 | .lg\:overflow-hidden { 1556 | overflow: hidden; 1557 | } 1558 | 1559 | .lg\:py-1 { 1560 | padding-top: 0.25rem; 1561 | padding-bottom: 0.25rem; 1562 | } 1563 | 1564 | .lg\:px-4 { 1565 | padding-left: 1rem; 1566 | padding-right: 1rem; 1567 | } 1568 | 1569 | .lg\:fixed { 1570 | position: fixed; 1571 | } 1572 | 1573 | .lg\:w-1\/2 { 1574 | width: 50%; 1575 | } 1576 | } 1577 | 1578 | @media (min-width: 1280px) { 1579 | .xl\:w-2\/5 { 1580 | width: 40%; 1581 | } 1582 | 1583 | .xl\:w-3\/5 { 1584 | width: 60%; 1585 | } 1586 | } 1587 | 1588 | @media (min-width: 1536px) { 1589 | } 1590 | 1591 | @media (min-width: 362px) { 1592 | } 1593 | -------------------------------------------------------------------------------- /src/styles/scroll.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body ::-webkit-scrollbar { 6 | width: 0.4em; 7 | } 8 | 9 | body ::-webkit-scrollbar-track { 10 | box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); 11 | } 12 | 13 | body ::-webkit-scrollbar-thumb { 14 | background-color: rgb(73, 73, 73); 15 | border-radius: 8px; 16 | } 17 | 18 | #out-grid-section:hover { 19 | overflow: auto; 20 | transition: all 1s ease; 21 | } -------------------------------------------------------------------------------- /src/styles/slider.css: -------------------------------------------------------------------------------- 1 | @media screen and (-webkit-min-device-pixel-ratio: 0) { 2 | 3 | input[type="range"]::-webkit-slider-thumb { 4 | width: 15px; 5 | -webkit-appearance: none; 6 | appearance: none; 7 | height: 15px; 8 | cursor: ew-resize; 9 | background: #FFF; 10 | /* box-shadow: -405px 0 0 400px #605E5C; */ 11 | border-radius: 50%; 12 | 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/styles/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | 3 | @tailwind components; 4 | 5 | @tailwind utilities; 6 | -------------------------------------------------------------------------------- /src/utils/downloadImage.js: -------------------------------------------------------------------------------- 1 | import { downscaleDrawCanvasImage } from './downscaleCanvasImage' 2 | import { saveAs } from 'file-saver' 3 | import { loadElement } from './loadElement' 4 | import { getParams } from '../constants' 5 | 6 | export const downloadImage = ( 7 | src, 8 | color, 9 | monoTone, 10 | custom, 11 | adjustedHue, 12 | ext, 13 | ) => { 14 | // Generating hidden canvas, then modifying its pixels, then convert to blob, finally save file 15 | const generateHiddenCanvas = new Promise((resolve) => { 16 | let hidden_canv = document.createElement('canvas') 17 | hidden_canv.style.display = 'none' 18 | document.body.appendChild(hidden_canv) 19 | const ctx = hidden_canv.getContext('2d') 20 | 21 | loadElement(src).then((img) => { 22 | const w = img.width 23 | const res = img.height / img.width 24 | const h = w * res 25 | hidden_canv.width = w 26 | hidden_canv.height = h 27 | downscaleDrawCanvasImage( 28 | ctx, 29 | img, 30 | 0, 31 | 0, 32 | hidden_canv.width, 33 | hidden_canv.height, 34 | ) 35 | resolve({ ctx, canvas: hidden_canv }) 36 | }) 37 | }) 38 | 39 | const downloader = new Promise((resolve) => { 40 | generateHiddenCanvas.then(({ ctx, canvas }) => { 41 | const params = getParams(canvas) 42 | 43 | if (ctx && canvas) { 44 | const { x, y, width, height } = params 45 | const imgData = ctx.getImageData( 46 | (x - width / 2) * 1, 47 | (y - height / 2) * 1, 48 | width * 1, 49 | height * 1, 50 | ) 51 | 52 | if (window.Worker) { 53 | const worker = new Worker( 54 | new URL('../worker/tintWorker.js', import.meta.url), 55 | ) 56 | worker.postMessage( 57 | { imgData, color, monoTone, custom, adjustedHue }, 58 | [imgData.data.buffer], 59 | ) 60 | worker.onerror = (err) => err 61 | worker.onmessage = (e) => { 62 | const imgData = e.data 63 | ctx.putImageData( 64 | imgData, 65 | (params.x - params.width / 2) * 1, 66 | (params.y - params.height / 2) * 1, 67 | ) 68 | 69 | canvas.toBlob((blob) => { 70 | saveAs(blob, `download.${ext}`) 71 | const href = URL.createObjectURL(blob) 72 | worker.terminate() 73 | 74 | resolve(href) 75 | }) 76 | } 77 | } 78 | } 79 | }) 80 | }) 81 | 82 | return downloader 83 | } 84 | -------------------------------------------------------------------------------- /src/utils/downscaleCanvasImage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * https://stackoverflow.com/questions/21961839/simulation-background-size-cover-in-canvas/21961894#21961894 3 | * 4 | * Credits - Ken Fyrstenberg Nilsen 5 | * 6 | * drawImageProp(context, image [, x, y, width, height [,offsetX, offsetY]]) 7 | * 8 | * If image and context are only arguments rectangle will equal canvas 9 | */ 10 | 11 | export function downscaleDrawCanvasImage( 12 | ctx, 13 | img, 14 | x, 15 | y, 16 | w, 17 | h, 18 | offsetX, 19 | offsetY, 20 | ) { 21 | if (arguments.length === 2) { 22 | x = y = 0 23 | w = ctx.canvas.width 24 | h = ctx.canvas.height 25 | } 26 | 27 | /// default offset is center 28 | offsetX = typeof offsetX === 'number' ? offsetX : 0.5 29 | offsetY = typeof offsetY === 'number' ? offsetY : 0.5 30 | 31 | /// keep bounds [0.0, 1.0] 32 | if (offsetX < 0) offsetX = 0 33 | if (offsetY < 0) offsetY = 0 34 | if (offsetX > 1) offsetX = 1 35 | if (offsetY > 1) offsetY = 1 36 | 37 | let iw = img.width, 38 | ih = img.height, 39 | r = Math.min(w / iw, h / ih), 40 | nw = iw * r, /// new prop. width 41 | nh = ih * r, /// new prop. height 42 | cx, 43 | cy, 44 | cw, 45 | ch, 46 | ar = 1 47 | 48 | /// decide which gap to fill 49 | if (nw < w) ar = w / nw 50 | if (nh < h) ar = h / nh 51 | nw *= ar 52 | nh *= ar 53 | 54 | /// calc source rectangle 55 | cw = iw / (nw / w) 56 | ch = ih / (nh / h) 57 | 58 | cx = (iw - cw) * offsetX 59 | cy = (ih - ch) * offsetY 60 | 61 | /// make sure source rectangle is valid 62 | if (cx < 0) cx = 0 63 | if (cy < 0) cy = 0 64 | if (cw > iw) cw = iw 65 | if (ch > ih) ch = ih 66 | 67 | /// fill image in dest. rectangle 68 | ctx.drawImage(img, cx, cy, cw, ch, x, y, w, h) 69 | } 70 | -------------------------------------------------------------------------------- /src/utils/dynamicCanvasResize.js: -------------------------------------------------------------------------------- 1 | export const dynamicCanvasResize = (cv, img, scale) => { 2 | cv.width = cv.offsetWidth * scale 3 | cv.height = cv.offsetHeight * scale 4 | 5 | const cvProportion = (cv.height * 1.0) / cv.width 6 | const res = (img.height * 1.0) / img.width 7 | 8 | if (cvProportion > res) { 9 | cv.height = res * cv.width 10 | } else { 11 | cv.width = (cv.height * 1.0) / res 12 | } 13 | cv.style.width = 'auto' 14 | cv.style.height = 'auto' 15 | return cv 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/generateColors.js: -------------------------------------------------------------------------------- 1 | import { hslToRgb } from './hslToRgb' 2 | 3 | export const generateColors = (n) => { 4 | const colors = [] 5 | let abs = Math.floor(Math.abs(n)) 6 | let part = 1 / abs 7 | 8 | for (let i = 0; i < n; i++) { 9 | let color = { h: i * part, s: 0.9, l: 0.6 } 10 | let hsl = color 11 | let rgb = hslToRgb(color.h, color.s, color.l) 12 | let rgbString = `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})` 13 | colors.push({ 14 | hsl, 15 | rgb, 16 | rgbString, 17 | }) 18 | } 19 | 20 | return colors 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/generateRandomURL.js: -------------------------------------------------------------------------------- 1 | import { UNSPLASH_URI } from '../constants' 2 | 3 | export const generateRandomURL = () => 4 | new Promise((resolve) => { 5 | try { 6 | fetch(UNSPLASH_URI).then((response) => { 7 | if (response.ok) { 8 | resolve({ 9 | ok: true, 10 | url: response.url, 11 | }) 12 | } else { 13 | resolve({ 14 | ok: false, 15 | err: 'Error Fetching', 16 | }) 17 | } 18 | }) 19 | } catch (e) { 20 | resolve({ 21 | ok: false, 22 | err: e.message, 23 | }) 24 | } 25 | }) 26 | -------------------------------------------------------------------------------- /src/utils/getFileType.js: -------------------------------------------------------------------------------- 1 | export const getFileType = (fname) => 2 | fname.slice(((fname.lastIndexOf('.') - 1) >>> 0) + 2) 3 | -------------------------------------------------------------------------------- /src/utils/hslToRgb.js: -------------------------------------------------------------------------------- 1 | export function hslToRgb(h, s, l) { 2 | let r, g, b 3 | 4 | function hue2rgb(p, q, t) { 5 | if (t < 0) t += 1 6 | if (t > 1) t -= 1 7 | if (t < 1 / 6) return p + (q - p) * 6 * t 8 | if (t < 1 / 2) return q 9 | if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6 10 | return p 11 | } 12 | 13 | if (s == 0) { 14 | r = g = b = l // achromatic 15 | } else { 16 | let q = l < 0.5 ? l * (1 + s) : l + s - l * s 17 | let p = 2 * l - q 18 | r = hue2rgb(p, q, h + 1 / 3) 19 | g = hue2rgb(p, q, h) 20 | b = hue2rgb(p, q, h - 1 / 3) 21 | } 22 | 23 | return { 24 | r: Math.floor(r * 255), 25 | g: Math.floor(g * 255), 26 | b: Math.floor(b * 255), 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/loadElement.js: -------------------------------------------------------------------------------- 1 | // Returns a promise - use to load image 2 | export function loadElement(src) { 3 | return new Promise((resolve) => { 4 | const e = new Image() 5 | e.addEventListener('load', () => { 6 | resolve(e) 7 | }) 8 | e.src = src 9 | e.crossOrigin = 'Anonymous' 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /src/worker/runWorker.js: -------------------------------------------------------------------------------- 1 | export const runWorker = ( 2 | ctx, 3 | imgData, 4 | params, 5 | color, 6 | monoTone, 7 | custom, 8 | adjustedHue, 9 | ) => { 10 | return new Promise((resolve) => { 11 | if (window.Worker) { 12 | const worker = new Worker(new URL('./tintWorker.js', import.meta.url)) 13 | worker.postMessage({ imgData, color, monoTone, custom, adjustedHue }, [ 14 | imgData.data.buffer, 15 | ]) 16 | worker.onerror = (err) => resolve({ ok: false, err }) 17 | worker.onmessage = (e) => { 18 | const imgData = e.data 19 | ctx.putImageData( 20 | imgData, 21 | (params.x - params.width / 2) * 1, 22 | (params.y - params.height / 2) * 1, 23 | ) 24 | worker.terminate() 25 | resolve({ ok: true }) 26 | } 27 | } 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /src/worker/tintWorker.js: -------------------------------------------------------------------------------- 1 | function rgbToHsl(r, g, b) { 2 | r /= 255 3 | g /= 255 4 | b /= 255 5 | 6 | const max = Math.max(r, g, b), 7 | min = Math.min(r, g, b) 8 | let h, 9 | s, 10 | l = (max + min) / 2 11 | 12 | if (max == min) { 13 | h = s = 0 // achromatic 14 | } else { 15 | let d = max - min 16 | s = l > 0.5 ? d / (2 - max - min) : d / (max + min) 17 | switch (max) { 18 | case r: 19 | h = (g - b) / d + (g < b ? 6 : 0) 20 | break 21 | case g: 22 | h = (b - r) / d + 2 23 | break 24 | case b: 25 | h = (r - g) / d + 4 26 | break 27 | } 28 | h /= 6 29 | } 30 | 31 | return { h, s, l } 32 | } 33 | 34 | function hslToRgb(h, s, l) { 35 | let r, g, b 36 | 37 | function hue2rgb(p, q, t) { 38 | if (t < 0) t += 1 39 | if (t > 1) t -= 1 40 | if (t < 1 / 6) return p + (q - p) * 6 * t 41 | if (t < 1 / 2) return q 42 | if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6 43 | return p 44 | } 45 | 46 | if (s == 0) { 47 | r = g = b = l // achromatic 48 | } else { 49 | let q = l < 0.5 ? l * (1 + s) : l + s - l * s 50 | let p = 2 * l - q 51 | r = hue2rgb(p, q, h + 1 / 3) 52 | g = hue2rgb(p, q, h) 53 | b = hue2rgb(p, q, h - 1 / 3) 54 | } 55 | 56 | return { 57 | r: Math.floor(r * 255), 58 | g: Math.floor(g * 255), 59 | b: Math.floor(b * 255), 60 | } 61 | } 62 | 63 | const iteratePixels = (imgData, func, modify = false) => { 64 | const pixelData = imgData.data 65 | const len = pixelData.length 66 | 67 | for (let i = 0; i < len; i += 4) { 68 | let px = { 69 | r: pixelData[i], 70 | g: pixelData[i + 1], 71 | b: pixelData[i + 2], 72 | a: pixelData[i + 3], 73 | } 74 | 75 | // function to modify pixel data 76 | px = func(px) 77 | 78 | if (modify) { 79 | pixelData[i] = px.r 80 | pixelData[i + 1] = px.g 81 | pixelData[i + 2] = px.b 82 | pixelData[i + 3] = px.a 83 | } 84 | } 85 | // return imgData 86 | } 87 | 88 | const setImagePixels = (imgData, convertPixel, colorOrHue) => { 89 | const func = (px) => { 90 | const _px = convertPixel(px, colorOrHue) 91 | return _px 92 | } 93 | iteratePixels(imgData, func, true) 94 | } 95 | 96 | addEventListener('message', (d) => { 97 | // const custom = false 98 | // const adjustedHue = 15 99 | 100 | const { imgData, color, monoTone, custom, adjustedHue } = d.data 101 | 102 | if (custom) { 103 | // converter & recolor function 104 | const convertPixel = (_px, adjustedHue) => { 105 | let hsl = rgbToHsl(_px.r, _px.g, _px.b) 106 | const adjustedValue = adjustedHue / 100 107 | hsl.h = (hsl.h + adjustedValue) % 1 108 | 109 | // hsl.s = 0.5 110 | let px = hslToRgb(hsl.h, hsl.s, hsl.l) 111 | _px.r = px.r 112 | _px.g = px.g 113 | _px.b = px.b 114 | return _px 115 | } 116 | 117 | setImagePixels(imgData, convertPixel, adjustedHue) 118 | } else { 119 | // converter & recolor function 120 | const convertPixel = (_px, color) => { 121 | let hsl = rgbToHsl(_px.r, _px.g, _px.b) 122 | 123 | if (monoTone) { 124 | //colorize 125 | hsl.h = color.hsl.h 126 | } else { 127 | // change hue 128 | hsl.h = (hsl.h + color.hsl.h) % 1 129 | } 130 | 131 | hsl.s = 0.5 132 | let px = hslToRgb(hsl.h, hsl.s, hsl.l) 133 | _px.r = px.r 134 | _px.g = px.g 135 | _px.b = px.b 136 | return _px 137 | } 138 | 139 | setImagePixels(imgData, convertPixel, color) 140 | } 141 | 142 | postMessage(imgData, [imgData.data.buffer]) 143 | }) 144 | -------------------------------------------------------------------------------- /ss.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uxie-io/tinter/40cd7f5f3bc2314260f2fe69a2741b75f1a871d3/ss.jpeg -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: { 3 | enabled: true, 4 | content: ['./src/**/*.html', './src/**/*.jsx'], 5 | }, 6 | darkMode: 'class', 7 | variants: { 8 | extend: { 9 | backgroundColor: ['checked'], 10 | borderColor: ['checked'], 11 | }, 12 | }, 13 | 14 | theme: { 15 | extend: { 16 | colors: { 17 | 'darkish-blue': '#182635', 18 | 'darkish-black': '#1C1C1F', 19 | 'grey-light': 'F5F5F5', 20 | }, 21 | width: { 22 | '7/10': '70%', 23 | '3/10': '30%', 24 | }, 25 | height: { 26 | '7/10': '70%', 27 | '3/10': '30%', 28 | }, 29 | screens: { 30 | xs: '362px', 31 | }, 32 | }, 33 | fontFamily: { 34 | plex: ['"IBM Plex Sans"', 'sans-serif'], 35 | sans: [ 36 | 'Poppins', 37 | 'system-ui', 38 | '-apple-system', 39 | 'BlinkMacSystemFont', 40 | '"Segoe UI"', 41 | 'Roboto', 42 | '"Helvetica Neue"', 43 | 'Arial', 44 | '"Noto Sans"', 45 | 'sans-serif', 46 | '"Apple Color Emoji"', 47 | '"Segoe UI Emoji"', 48 | '"Segoe UI Symbol"', 49 | '"Noto Color Emoji"', 50 | ], 51 | serif: ['Georgia', 'Cambria', '"Times New Roman"', 'Times', 'serif'], 52 | mono: [ 53 | 'Menlo', 54 | 'Monaco', 55 | 'Consolas', 56 | '"Liberation Mono"', 57 | '"Courier New"', 58 | 'monospace', 59 | ], 60 | }, 61 | }, 62 | plugins: [ 63 | require('@tailwindcss/forms'), 64 | // ... 65 | ], 66 | 67 | // variants: { 68 | // backgroundColor: [ 69 | // 'dark', 70 | // 'dark-hover', 71 | // 'dark-group-hover', 72 | // 'dark-even', 73 | // 'dark-odd', 74 | // 'hover', 75 | // 'responsive', 76 | // ], 77 | // borderColor: [ 78 | // 'dark', 79 | // 'dark-focus', 80 | // 'dark-focus-within', 81 | // 'hover', 82 | // 'responsive', 83 | // ], 84 | // textColor: ['dark', 'dark-hover', 'dark-active', 'hover', 'responsive'], 85 | // }, 86 | } 87 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const HTMLWebpackPlugin = require('html-webpack-plugin') 3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 4 | 5 | module.exports = { 6 | entry: './src/index.jsx', 7 | output: { 8 | path: path.join(__dirname, '/dist'), 9 | filename: 'index_bundle.js', 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.(js|jsx)$/, 15 | exclude: /node_modules/, 16 | use: { 17 | loader: 'babel-loader', 18 | }, 19 | }, 20 | { 21 | test: /\.css$/, 22 | use: [ 23 | 'style-loader', 24 | { 25 | loader: MiniCssExtractPlugin.loader, 26 | }, 27 | { 28 | loader: 'css-loader', 29 | options: { 30 | importLoaders: 1, 31 | }, 32 | }, 33 | 'postcss-loader', 34 | ], 35 | }, 36 | { 37 | test: /\.(png|jp(e*)g|svg|gif|webp)$/, 38 | use: [ 39 | { 40 | loader: 'file-loader', 41 | options: { 42 | name: 'images/[hash]-[name].[ext]', 43 | }, 44 | }, 45 | ], 46 | }, 47 | ], 48 | }, 49 | resolve: { 50 | extensions: ['.js', '.jsx'], 51 | alias: { 52 | react: 'preact/compat', 53 | 'react-dom': 'preact/compat', 54 | }, 55 | }, 56 | plugins: [ 57 | new MiniCssExtractPlugin({ 58 | filename: '[name].bundle.css', 59 | chunkFilename: '[id].css', 60 | }), 61 | new HTMLWebpackPlugin({ 62 | template: './src/index.html', 63 | favicon: "./src/favicon.ico", 64 | }), 65 | ], 66 | } 67 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge') 2 | const common = require('./webpack.common.js') 3 | 4 | module.exports = merge(common, { 5 | mode: 'development', 6 | }) 7 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge') 2 | const common = require('./webpack.common.js') 3 | const CompressionPlugin = require('compression-webpack-plugin') 4 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin') 5 | const webpack = require('webpack') 6 | 7 | module.exports = merge(common, { 8 | mode: 'production', 9 | plugins: [ 10 | new webpack.DefinePlugin({ 11 | // <-- key to reducing React's size 12 | 'process.env': { 13 | NODE_ENV: JSON.stringify('production'), 14 | }, 15 | }), 16 | new CompressionPlugin(), 17 | new OptimizeCSSAssetsPlugin({}), 18 | ], 19 | }) 20 | --------------------------------------------------------------------------------