├── .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 | 
4 |
5 |
6 | 
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 | 
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 = `
`
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 |
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 |
104 |
105 | )
106 |
107 | export default AbstractArt
108 |
--------------------------------------------------------------------------------
/src/Components/AbstractArt/GlowEffect.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact'
2 |
3 | const GlowEffect = () => {
4 | return (
5 |
49 | )
50 | }
51 |
52 | export default GlowEffect
53 |
--------------------------------------------------------------------------------
/src/Components/CanvasComponents/MainCanvas.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact'
2 | import { useEffect, useState } from 'preact/hooks'
3 | import useCanvas from '../../hooks/useCanvas'
4 | import { useStore } from '../../hooks/useStore'
5 | import { loadElement } from '../../utils/loadElement'
6 | import { downscaleDrawCanvasImage } from '../../utils/downscaleCanvasImage'
7 | import { dynamicCanvasResize } from '../../utils/dynamicCanvasResize'
8 | import { getParams } from '../../constants'
9 | import { runWorker } from '../../worker/runWorker'
10 | import Spinner from '../UI/Spinner'
11 | import { useWindowSize } from '../../hooks/useWindowSize'
12 |
13 | const MainCanvas = (props) => {
14 | const [loading, setLoading] = useState(true)
15 | const { customize, monoTone, adjustedHue } = useStore()
16 | const { width } = useWindowSize()
17 | const { src } = props
18 | const canvasRef = useCanvas()
19 | const scale = width < 500 ? 0.8 : 1
20 |
21 | useEffect(() => {
22 | const updateCanvas = (src) => {
23 | const cv = canvasRef.current
24 |
25 | const ctx = cv.getContext('2d')
26 |
27 | loadElement(src).then((img) => {
28 | setLoading(false)
29 | dynamicCanvasResize(cv, img, scale)
30 | downscaleDrawCanvasImage(ctx, img, 0, 0, cv.width, cv.height)
31 |
32 | /*
33 | if customize, let worker do the job.
34 | */
35 | if (customize) {
36 | const params = getParams(cv)
37 |
38 | if (ctx && cv) {
39 | const { x, y, width, height } = params
40 | const imgData = ctx.getImageData(
41 | x - width / 2,
42 | y - height / 2,
43 | width,
44 | height,
45 | )
46 | runWorker(
47 | ctx,
48 | imgData,
49 | params,
50 | null,
51 | monoTone,
52 | customize,
53 | adjustedHue,
54 | )
55 | }
56 | }
57 | })
58 | }
59 | updateCanvas(src)
60 | }, [loading, src, canvasRef, adjustedHue, customize, monoTone])
61 |
62 | const content = loading ? (
63 |
64 |
65 |
loading...
66 |
67 |
68 | ) : (
69 |
78 | )
79 |
80 | return (
81 |
82 | {content}
83 |
84 | )
85 | }
86 |
87 | export default MainCanvas
88 |
--------------------------------------------------------------------------------
/src/Components/CanvasComponents/OuputCanvas.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact'
2 | import useCanvas from '../../hooks/useCanvas'
3 | import { useEffect, useState } from 'preact/hooks'
4 | import { downscaleDrawCanvasImage } from '../../utils/downscaleCanvasImage'
5 | import { useStore } from '../../hooks/useStore'
6 | import { runWorker } from '../../worker/runWorker'
7 | import { loadElement } from '../../utils/loadElement'
8 | import { getParams } from '../../constants'
9 | import Spinner from './../UI/Spinner'
10 | import { useWindowSize } from './../../hooks/useWindowSize'
11 |
12 | const OutputCanvas = (props) => {
13 | const [loading, setLoading] = useState(true)
14 | const { width } = useWindowSize()
15 | const monoTone = useStore((state) => state.monoTone)
16 | const { src, color, index, allowNextToRender } = props
17 | const canvasRef = useCanvas()
18 | const scale = width < 500 ? 0.8 : 1
19 |
20 | useEffect(() => {
21 | const updateCanvas = (src) => {
22 | const generateCanvasOutput = new Promise((resolve) => {
23 | const cv = canvasRef.current
24 | const ctx = cv.getContext('2d')
25 |
26 | loadElement(src).then((img) => {
27 | let canvasDefaultWidth = 250
28 |
29 | if ((width < 1124 && width > 1024) || width < 650) {
30 | canvasDefaultWidth = 200
31 | }
32 | if (width < 400) {
33 | canvasDefaultWidth = 175
34 | }
35 |
36 | const w = Math.min(img.width, canvasDefaultWidth)
37 | const res = img.height / img.width
38 | const h = w * res
39 | cv.width = w * scale
40 | cv.height = h * scale
41 |
42 | downscaleDrawCanvasImage(ctx, img, 0, 0, cv.width, cv.height)
43 | resolve({ ctx, canvas: cv })
44 | })
45 | })
46 |
47 | generateCanvasOutput.then(({ ctx, canvas }) => {
48 | const params = getParams(canvas)
49 |
50 | if (ctx && canvas) {
51 | const { x, y, width, height } = params
52 | const imgData = ctx.getImageData(
53 | x - width / 2,
54 | y - height / 2,
55 | width,
56 | height,
57 | )
58 |
59 | runWorker(ctx, imgData, params, color, monoTone).then(() => {
60 | setLoading(false)
61 | })
62 | }
63 | })
64 | }
65 | updateCanvas(src)
66 | }, [
67 | loading,
68 | src,
69 | canvasRef,
70 | color,
71 | monoTone,
72 | allowNextToRender,
73 | index,
74 | width,
75 | scale,
76 | ])
77 |
78 | const content = loading ? (
79 |
80 |
81 |
loading...
{' '}
82 |
83 |
84 | ) : (
85 |
86 | )
87 | return {content}
88 | }
89 |
90 | export default OutputCanvas
91 |
--------------------------------------------------------------------------------
/src/Components/Home.jsx:
--------------------------------------------------------------------------------
1 | import { Fragment, h } from 'preact'
2 | import Navbar from './UI/Navbar'
3 | import OutputGrid from './OutputSection/OutputGrid'
4 | import { useStore } from '../hooks/useStore'
5 | import { Modal } from './UI/Modal'
6 | import { useState } from 'preact/hooks'
7 | import ImageSection from './Main/ImageSection'
8 | import ImageButtons from './ImageButtons/ImageButtons'
9 | import AbstractArt from './AbstractArt/AbstractArt'
10 | import Toast from './UI/Toast'
11 | import GlowEffect from './AbstractArt/GlowEffect'
12 |
13 | function Home() {
14 | const showTints = useStore((state) => state.showTints)
15 | const toast = useStore((state) => state.toast)
16 | let [isOpen, setIsOpen] = useState(false)
17 |
18 | function closeModal() {
19 | setIsOpen(false)
20 | }
21 |
22 | function openModal() {
23 | setIsOpen(true)
24 | }
25 |
26 | return (
27 |
28 |
33 |
34 |
35 |
36 |
40 |
41 |
42 | {showTints &&
}
43 |
44 |
45 |
46 |
47 | {toast && }
48 |
49 |
50 | )
51 | }
52 |
53 | export default Home
54 |
--------------------------------------------------------------------------------
/src/Components/ImageButtons/ImageButtons.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact'
2 | import Button from '../UI/Button'
3 | import UploadIcon from '../../assets/upload.svg'
4 | import RandomIcon from '../../assets/random.svg'
5 | import { generateRandomURL } from '../../utils/generateRandomURL'
6 | import { useStore } from '../../hooks/useStore'
7 |
8 | const ImageButtons = ({ openModal }) => {
9 | const {
10 | showTints,
11 | toggleTints,
12 | toggleCustomize,
13 | toggleToast,
14 | selectImage,
15 | customize,
16 | toggleTone,
17 | monoTone,
18 | } = useStore()
19 |
20 | const handleLoadRandomImage = () => {
21 | toggleToast({
22 | spinner: true,
23 | animated: false,
24 | texts: ['Loading ...'],
25 | })
26 | generateRandomURL().then((res) => {
27 | if (res.ok) {
28 | selectImage(res.url)
29 | toggleToast()
30 | } else {
31 | toggleToast()
32 | toggleToast({
33 | spinner: false,
34 | animated: false,
35 | texts: ['Error loading image.'],
36 | })
37 | }
38 |
39 | if (!monoTone) {
40 | toggleTone()
41 | }
42 |
43 | if (showTints) {
44 | toggleTints()
45 |
46 | if (customize) {
47 | toggleCustomize()
48 | }
49 | }
50 | })
51 | }
52 |
53 | return (
54 |
55 |
59 |
63 |
64 | )
65 | }
66 |
67 | export default ImageButtons
68 |
--------------------------------------------------------------------------------
/src/Components/Main/ImageSection.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact'
2 | import ConfigBar from '../MainConfig/ConfigBar'
3 | import MainCanvas from '../CanvasComponents/MainCanvas'
4 | import { useStore } from '../../hooks/useStore'
5 | import FenceImg from '../../assets/fence.svg'
6 |
7 | const ImageSection = () => {
8 | const selectedImage = useStore((state) => state.selectedImage)
9 |
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
20 |
21 |
22 |
23 |
24 |

25 |
26 |
27 | )
28 | }
29 |
30 | export default ImageSection
31 |
--------------------------------------------------------------------------------
/src/Components/MainConfig/ConfigBar.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact'
2 | import { useStore } from '../../hooks/useStore'
3 | import Button from './../UI/Button'
4 | import Checkbox from './../UI/Checkbox'
5 | import ResetIcon from './../../assets/reset.svg'
6 | import EditIcon from './../../assets/edit.svg'
7 | import WandIcon from './../../assets/wand.svg'
8 | import Slider from './../UI/Slider'
9 | import { downloadImage } from '../../utils/downloadImage'
10 |
11 | const ConfigBar = () => {
12 | const {
13 | showTints,
14 | toggleTints,
15 | toggleCustomize,
16 | toggleTone,
17 | monoTone,
18 | adjustHue,
19 | customize,
20 | adjustedHue,
21 | selectedImage,
22 | toggleToast,
23 | selectedFileExt,
24 | } = useStore()
25 |
26 | const onCustomHueAdjust = (e) => {
27 | const adjustedVal = e.target.value
28 | adjustHue(adjustedVal)
29 | }
30 |
31 | const handleChange = () => toggleTone()
32 |
33 | const download = () => {
34 | toggleToast()
35 | downloadImage(
36 | selectedImage,
37 | null,
38 | monoTone,
39 | customize,
40 | adjustedHue,
41 | selectedFileExt,
42 | ).then(() => {
43 | toggleToast()
44 | })
45 | }
46 |
47 | return (
48 |
49 |
50 |
51 |
52 | {!customize ? (
53 |
54 |
55 | Fine tune
56 |
57 |

58 |
59 | ) : (
60 |
73 | )}
74 |
75 |
76 |
77 |
93 |
102 |
103 |
104 | )
105 | }
106 |
107 | export default ConfigBar
108 |
--------------------------------------------------------------------------------
/src/Components/OutputSection/AdjustedOutput.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact'
2 | import { downloadImage } from '../../utils/downloadImage'
3 | import { useStore } from '../../hooks/useStore'
4 | import OutputCanvas from '../CanvasComponents/OuputCanvas'
5 | import DownloadIcon from '../../assets/download.svg'
6 | import { DEFAULT_HUE } from '../../constants'
7 |
8 | export const AdjustedOutput = ({ color }) => {
9 | const selectedImage = useStore((state) => state.selectedImage)
10 | const monoTone = useStore((state) => state.monoTone)
11 | const toggleToast = useStore((state) => state.toggleToast)
12 | const selectedFileExt = useStore((state) => state.selectedFileExt)
13 |
14 | const download = () => {
15 | toggleToast()
16 | downloadImage(
17 | selectedImage,
18 | color,
19 | monoTone,
20 | false,
21 | DEFAULT_HUE,
22 | selectedFileExt,
23 | ).then(() => {
24 | toggleToast()
25 | })
26 | }
27 |
28 | return (
29 |
40 | )
41 | }
42 |
--------------------------------------------------------------------------------
/src/Components/OutputSection/OutputGrid.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact'
2 | import { useState } from 'preact/hooks'
3 | import { generateColors } from '../../utils/generateColors'
4 | import { AdjustedOutput } from './AdjustedOutput'
5 | import { OUTPUT_IMAGES } from '../../constants'
6 |
7 | const OutputGrid = () => {
8 | const [colors] = useState(() => generateColors(OUTPUT_IMAGES).reverse())
9 |
10 | return (
11 |
12 | {colors.map((c, i) => (
13 |
14 | ))}
15 |
16 | )
17 | }
18 |
19 | export default OutputGrid
20 |
--------------------------------------------------------------------------------
/src/Components/UI/AnimatedText.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact'
2 | import { useEffect, useState } from 'preact/hooks'
3 |
4 | const AnimatedText = ({ textList }) => {
5 | const [textIdx, setTextIdx] = useState(0)
6 |
7 | useEffect(() => {
8 | const interval = setInterval(() => {
9 | if (textIdx < 2) {
10 | setTextIdx((i) => i + 1)
11 | } else {
12 | clearInterval(interval)
13 | }
14 | }, 2000)
15 | return () => clearInterval(interval)
16 | }, [textIdx])
17 | return {textList[textIdx]}
18 | }
19 |
20 | export default AnimatedText
21 |
--------------------------------------------------------------------------------
/src/Components/UI/Button.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact'
2 |
3 | const Button = ({
4 | variant,
5 | href,
6 | children,
7 | fontWeight,
8 | colorRGB = '#ffffff',
9 | textSize = 'sm',
10 | onClick,
11 | }) => {
12 | const color =
13 | variant === 'white'
14 | ? 'text-black bg-white'
15 | : variant === 'glass'
16 | ? 'text-white bg-grey-light'
17 | : 'text-white bg-darkish-black'
18 |
19 | const text = textSize === 'lg' ? 'text-md lg:text-md' : 'text-sm lg:text-md'
20 | const map = { glass: 'rgba(245,245,245,.1)', solid: `${colorRGB}b2` }
21 | const customColor = map[variant]
22 |
23 | return (
24 |
35 | {children}
36 |
37 | )
38 | }
39 |
40 | export default Button
41 |
--------------------------------------------------------------------------------
/src/Components/UI/Checkbox.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact'
2 |
3 | const Checkbox = ({ text, checked, onChange }) => {
4 | return (
5 |
14 | )
15 | }
16 |
17 | export default Checkbox
18 |
--------------------------------------------------------------------------------
/src/Components/UI/Modal.jsx:
--------------------------------------------------------------------------------
1 | import { Dialog, Transition } from '@headlessui/react'
2 | import { Fragment, h } from 'preact'
3 | import { useState, useEffect } from 'preact/hooks'
4 |
5 | import { useStore } from './../../hooks/useStore'
6 | import { getFileType } from '../../utils/getFileType'
7 |
8 | export function Modal({ isOpen, closeModal }) {
9 | const [selectedFile, setSelectedFile] = useState(null)
10 | const selectImage = useStore((state) => state.selectImage)
11 | const {
12 | showTints,
13 | toggleTints,
14 | customize,
15 | toggleCustomize,
16 | monoTone,
17 | toggleTone,
18 | setSelectedFileExt,
19 | } = useStore()
20 |
21 | useEffect(() => {
22 | if (selectedFile) {
23 | selectImage(selectedFile)
24 | if (showTints) {
25 | toggleTints()
26 | if (customize) {
27 | toggleCustomize()
28 | }
29 | }
30 | // disable monoTone if new image uploaded
31 | if (monoTone) {
32 | toggleTone()
33 | }
34 | }
35 | }, [selectImage, selectedFile])
36 |
37 | const onFileChange = (evt) => {
38 | const file = evt.target.files[0]
39 | const srcURL = URL.createObjectURL(file)
40 | setSelectedFile(srcURL)
41 | setSelectedFileExt(getFileType(file.name))
42 | closeModal()
43 | }
44 |
45 | return (
46 |
47 |
48 |
110 |
111 |
112 | )
113 | }
114 |
--------------------------------------------------------------------------------
/src/Components/UI/Navbar.jsx:
--------------------------------------------------------------------------------
1 | import { Fragment, h } from 'preact'
2 | import TopImg from './../../assets/top.svg'
3 | import Button from './Button'
4 | import { useWindowSize } from './../../hooks/useWindowSize'
5 | // import GithubIcon from './../../assets/github.svg'
6 | // import BoxIcon from './../../assets/box.svg'
7 |
8 | function Navbar() {
9 | const { width } = useWindowSize()
10 | const mobileSize = width < 715
11 |
12 | return (
13 |
14 |
15 |
16 |
17 |

22 |
23 |
24 |
28 |
29 | {!mobileSize ? (
30 |
31 |
39 |
47 |
48 | ) : (
49 |
50 | {/*
55 |
56 |
57 |
62 |
63 | */}
64 |
65 | )}
66 |
67 |
68 | )
69 | }
70 |
71 | export default Navbar
72 |
--------------------------------------------------------------------------------
/src/Components/UI/Slider.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact'
2 |
3 | const Slider = ({ adjustedHue, onCustomHueAdjust }) => {
4 | return (
5 |
18 | )
19 | }
20 |
21 | export default Slider
22 |
--------------------------------------------------------------------------------
/src/Components/UI/Spinner.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact'
2 |
3 | const Spinner = () => {
4 | return (
5 |
28 | )
29 | }
30 |
31 | export default Spinner
32 |
--------------------------------------------------------------------------------
/src/Components/UI/Toast.jsx:
--------------------------------------------------------------------------------
1 | import { Fragment, h } from 'preact'
2 | import { Transition } from '@headlessui/react'
3 | import { useStore } from '../../hooks/useStore'
4 | import Spinner from './Spinner'
5 | import AnimatedText from './AnimatedText'
6 |
7 | export default function Toast() {
8 | const { toast: show, toggleToast: setShow, toastContent } = useStore()
9 |
10 | return (
11 |
12 |
19 |
20 |
30 |
31 |
32 |
33 |
34 |
35 | {toastContent.spinner && }
36 | {toastContent.animated ? (
37 |
38 | ) : (
39 |
40 | {toastContent.texts[0]}
41 |
42 | )}
43 |
44 |
45 |
46 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | )
77 | }
78 |
--------------------------------------------------------------------------------
/src/assets/box.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/default.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/uxie-io/tinter/40cd7f5f3bc2314260f2fe69a2741b75f1a871d3/src/assets/default.webp
--------------------------------------------------------------------------------
/src/assets/download.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/edit.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/fence.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/assets/github.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/mountain.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/uxie-io/tinter/40cd7f5f3bc2314260f2fe69a2741b75f1a871d3/src/assets/mountain.webp
--------------------------------------------------------------------------------
/src/assets/random.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/reset.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/assets/ss.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/uxie-io/tinter/40cd7f5f3bc2314260f2fe69a2741b75f1a871d3/src/assets/ss.jpeg
--------------------------------------------------------------------------------
/src/assets/top.svg:
--------------------------------------------------------------------------------
1 |
19 |
--------------------------------------------------------------------------------
/src/assets/upload.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/assets/wand.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | export const FILTERS = [
2 | 'DELPHOX',
3 | 'BAYLEEF',
4 | 'AZELF',
5 | 'Lunala',
6 | 'Archeops',
7 | 'Virizion',
8 | 'Deino',
9 | 'Corsola',
10 | ]
11 | export const DEFAULT_HUE = 15
12 | export const OUTPUT_IMAGES = 8
13 |
14 | export const getParams = (canvas) => ({
15 | x: 0,
16 | y: 0,
17 | width: canvas.width * 2,
18 | height: canvas.height * 2,
19 | })
20 |
21 | export const UNSPLASH_URI =
22 | 'https://source.unsplash.com/random/600x350/?abstract,nature'
23 |
--------------------------------------------------------------------------------
/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/uxie-io/tinter/40cd7f5f3bc2314260f2fe69a2741b75f1a871d3/src/favicon.ico
--------------------------------------------------------------------------------
/src/hooks/useCanvas.jsx:
--------------------------------------------------------------------------------
1 | import { useRef, useEffect } from 'preact/hooks'
2 |
3 | const useCanvas = (draw, options = {}) => {
4 | const canvasRef = useRef(null)
5 |
6 | useEffect(() => {
7 | const canvas = canvasRef.current
8 | const context = canvas.getContext(options.context || '2d')
9 | let frameCount = 0
10 | let animationFrameId
11 | const render = () => {
12 | frameCount++
13 | if (draw) {
14 | draw(context, frameCount)
15 | }
16 | animationFrameId = window.requestAnimationFrame(render)
17 | }
18 | render()
19 | return () => {
20 | window.cancelAnimationFrame(animationFrameId)
21 | }
22 | }, [draw, options.context])
23 | return canvasRef
24 | }
25 | export default useCanvas
26 |
--------------------------------------------------------------------------------
/src/hooks/useStore.jsx:
--------------------------------------------------------------------------------
1 | import create from 'zustand'
2 | import { DEFAULT_HUE } from '../constants'
3 | import img from './../assets/default.webp'
4 |
5 | export const useStore = create((set) => ({
6 | showTints: true,
7 | toggleTints: () => set((state) => ({ showTints: !state.showTints })),
8 | monoTone: false,
9 | toggleTone: () => set((state) => ({ monoTone: !state.monoTone })),
10 | customize: false,
11 | toggleCustomize: () => set((state) => ({ customize: !state.customize })),
12 | selectedImage: img,
13 | selectImage: (src) => set(() => ({ selectedImage: src })),
14 | adjustedHue: DEFAULT_HUE,
15 | adjustHue: (num) => set(() => ({ adjustedHue: num })),
16 | toast: false,
17 | toastContent: {
18 | spinner: true,
19 | animated: true,
20 | texts: ['Processing', 'Generating', 'Downloading..'],
21 | },
22 | toggleToast: (content) =>
23 | set((state) => {
24 | if (content) {
25 | return {
26 | toast: !state.toast,
27 | toastContent: content,
28 | }
29 | }
30 | return {
31 | toast: !state.toast,
32 | }
33 | }),
34 |
35 | selectedFileExt: 'png',
36 | setSelectedFileExt: (ext) => set(() => ({ selectedFileExt: ext })),
37 | }))
38 |
--------------------------------------------------------------------------------
/src/hooks/useWindowSize.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'preact/hooks'
2 |
3 | // Hook usehooks.com
4 | export const useWindowSize = () => {
5 | // Initialize state with undefined width/height so server and client renders match
6 | // Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/
7 | const [windowSize, setWindowSize] = useState({
8 | width: undefined,
9 | height: undefined,
10 | })
11 | useEffect(() => {
12 | // Handler to call on window resize
13 | function handleResize() {
14 | // Set window width/height to state
15 | setWindowSize({
16 | width: window.innerWidth,
17 | height: window.innerHeight,
18 | })
19 | }
20 | // Add event listener
21 | window.addEventListener('resize', handleResize)
22 | // Call handler right away so state gets updated with initial window size
23 | handleResize()
24 | // Remove event listener on cleanup
25 | return () => window.removeEventListener('resize', handleResize)
26 | }, []) // Empty array ensures that effect is only run on mount
27 | return windowSize
28 | }
29 |
--------------------------------------------------------------------------------
/src/hooks/useWorker.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'preact/hooks'
2 |
3 | export const useWorker = (file, onMessageCallback, postMessage, buffer) => {
4 | const [status, setStatus] = useState('loading')
5 |
6 | useEffect(() => {
7 | if (!postMessage || !postMessage.imgData || !buffer) {
8 | return
9 | }
10 |
11 | const runWorker = () => {
12 | const worker = new Worker(new URL(file, import.meta.url))
13 | worker.postMessage(postMessage, buffer)
14 | worker.onerror = (err) => {
15 | setStatus('error')
16 | throw err
17 | }
18 | worker.onmessage = (e) => {
19 | onMessageCallback(e)
20 | setStatus('success')
21 | worker.terminate()
22 | }
23 | }
24 | if (window.Worker) {
25 | runWorker()
26 | }
27 | }, [buffer, file, onMessageCallback, postMessage])
28 |
29 | return status
30 | }
31 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
10 |
12 | Tinter - Generate color image variations. Change hue online
13 |
14 |
15 |
17 |
19 |
20 |
21 |
22 |
24 |
26 |
27 |
28 |
29 |
30 |
39 |
40 |
69 |
70 |
71 |
72 |
73 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------