├── .github └── FUNDING.yml ├── public ├── favicon.ico ├── logo192.png ├── logo512.png ├── screenshot.png ├── robots.txt ├── manifest.json └── index.html ├── src ├── Assets │ ├── logo.png │ └── screenshot.png ├── Styles │ ├── Footer.css │ ├── Preview │ │ ├── Degrees.css │ │ ├── Dimensions.css │ │ ├── ExpandButton.css │ │ ├── DownloadButton.css │ │ ├── Center.css │ │ └── Preview.css │ ├── StopBar │ │ └── StopBar.css │ ├── HexPicker │ │ ├── ColorSlider.css │ │ ├── HexPicker.css │ │ ├── CurrentColor.css │ │ └── HexGradient.css │ ├── CSS │ │ ├── CopyConfirmation.css │ │ ├── CSS.css │ │ └── CopyButton.css │ ├── Header.css │ ├── Suggested │ │ ├── Suggested.css │ │ └── SuggestedItem.css │ └── Stack │ │ ├── AddColorButton.css │ │ ├── Stack.css │ │ └── StackItem.css ├── Utils │ ├── inputConstants.js │ ├── hexConstants.js │ ├── screenDimensionConstants.js │ ├── Gradient.js │ ├── generalUtils.js │ ├── Color.js │ ├── gradientConstants.js │ └── colorUtils.js ├── Components │ ├── Footer.js │ ├── Component.js │ ├── Preview │ │ ├── Degrees.js │ │ ├── DownloadButton.js │ │ ├── ExpandButton.js │ │ ├── Dimensions.js │ │ ├── Center.js │ │ ├── LinearRadial.js │ │ └── Preview.js │ ├── Header.js │ ├── CSS │ │ ├── CopyConfirmation.js │ │ ├── CopyButton.js │ │ └── CSS.js │ ├── Stack │ │ ├── AddColorButton.js │ │ ├── Stack.js │ │ └── StackItem.js │ ├── Suggested │ │ ├── SuggestedItem.js │ │ └── Suggested.js │ ├── HexPicker │ │ ├── HexGradient.js │ │ ├── HexPicker.js │ │ ├── CurrentColor.js │ │ └── ColorSlider.js │ └── StopBar │ │ └── StopBar.js ├── setupTests.js ├── App.test.js ├── index.css ├── index.js ├── App.css ├── serviceWorker.js └── App.js ├── README.md ├── LICENSE └── package.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: dopevog 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dopevog/gradient/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dopevog/gradient/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dopevog/gradient/HEAD/public/logo512.png -------------------------------------------------------------------------------- /src/Assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dopevog/gradient/HEAD/src/Assets/logo.png -------------------------------------------------------------------------------- /public/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dopevog/gradient/HEAD/public/screenshot.png -------------------------------------------------------------------------------- /src/Styles/Footer.css: -------------------------------------------------------------------------------- 1 | .footer-container { 2 | position: fixed; 3 | bottom: 0px; 4 | } 5 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/Assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dopevog/gradient/HEAD/src/Assets/screenshot.png -------------------------------------------------------------------------------- /src/Utils/inputConstants.js: -------------------------------------------------------------------------------- 1 | const MAX_SIZE = 32767; 2 | 3 | const ENTER_KEY = 13; 4 | 5 | export { MAX_SIZE, ENTER_KEY }; 6 | -------------------------------------------------------------------------------- /src/Styles/Preview/Degrees.css: -------------------------------------------------------------------------------- 1 | .degrees-container { 2 | margin-left: 15px; 3 | align-items: baseline; 4 | display: flex; 5 | } 6 | -------------------------------------------------------------------------------- /src/Styles/StopBar/StopBar.css: -------------------------------------------------------------------------------- 1 | .stopbar-container { 2 | margin-bottom: 30px; 3 | display: flex; 4 | align-items: flex-start; 5 | } 6 | -------------------------------------------------------------------------------- /src/Styles/HexPicker/ColorSlider.css: -------------------------------------------------------------------------------- 1 | .colorslide-container { 2 | height: 250px; 3 | width: 20px; 4 | position: relative; 5 | display: flex; 6 | align-items: baseline; 7 | } 8 | -------------------------------------------------------------------------------- /src/Utils/hexConstants.js: -------------------------------------------------------------------------------- 1 | const LABEL_GRAY = '#8b8b8b'; 2 | 3 | const INPUT_TEXT_GRAY = '#686868'; 4 | 5 | const HOVER_GRAY = '#cecece'; 6 | 7 | export { LABEL_GRAY, INPUT_TEXT_GRAY, HOVER_GRAY }; 8 | -------------------------------------------------------------------------------- /src/Components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../Styles/Footer.css'; 3 | 4 | function Footer(props) { 5 | return
; 6 | } 7 | 8 | export default Footer; 9 | -------------------------------------------------------------------------------- /src/Utils/screenDimensionConstants.js: -------------------------------------------------------------------------------- 1 | const IPHONE_10 = { 2 | width: 1125, 3 | height: 2436, 4 | }; 5 | 6 | const IPHONE_6 = { 7 | width: 750, 8 | height: 1334, 9 | }; 10 | 11 | export { IPHONE_10, IPHONE_6 }; 12 | -------------------------------------------------------------------------------- /src/Styles/HexPicker/HexPicker.css: -------------------------------------------------------------------------------- 1 | .hexpicker-container { 2 | display: flex; 3 | flex-direction: column; 4 | margin-right: 50px; 5 | } 6 | 7 | .hexpicker-bottom { 8 | display: flex; 9 | margin: 20px 0px; 10 | } 11 | -------------------------------------------------------------------------------- /src/Styles/Preview/Dimensions.css: -------------------------------------------------------------------------------- 1 | .dimensions-container { 2 | display: flex; 3 | align-items: baseline; 4 | padding-right: 10px; 5 | flex-grow: 1; 6 | } 7 | 8 | .dimensions-container { 9 | border-right: 1px solid var(--label-gray); 10 | } 11 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/Components/Component.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../Styles/Component.css'; 3 | 4 | class ClassComponent extends React.Component { 5 | render() { 6 | return ( 7 |
8 |

Hello World!

9 |
10 | ); 11 | } 12 | } 13 | 14 | function FunctionComponent(props) { 15 | return
; 16 | } 17 | export default Component; 18 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want your app to work offline and load faster, you can change 15 | // unregister() to register() below. Note this comes with some pitfalls. 16 | // Learn more about service workers: https://bit.ly/CRA-PWA 17 | serviceWorker.unregister(); 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gradient 2 | [Gradient](https://gradientorg.netlify.app/) is a custom gradiant making web app. 3 | ## Use it [here](https://gradientorg.netlify.app/) 4 | ![image](https://user-images.githubusercontent.com/82938580/120974866-3f869480-c78e-11eb-81eb-a0048a74960b.png) 5 | ## Features 6 | * Custom Gradient Making Using Sliders And Color Picker 7 | * Use Ready Made Gradients 8 | * Download Gradient Image 9 | * Copy CSS Code 10 | ## Local Usage 11 | ``` 12 | npm install 13 | npm start 14 | ``` 15 | ## License 16 | This Project is [MIT Licensed](https://github.com/yokiorg/gradient/blob/main/LICENSE) 17 | -------------------------------------------------------------------------------- /src/Styles/CSS/CopyConfirmation.css: -------------------------------------------------------------------------------- 1 | .copyconfirmation-container { 2 | width: 100%; 3 | position: fixed; 4 | bottom: 20px; 5 | display: flex; 6 | justify-content: center; 7 | } 8 | 9 | .copyconfirmation-content { 10 | display: flex; 11 | width: 250px; 12 | height: 30px; 13 | border-radius: 5px; 14 | box-shadow: 2px 2px 7px var(--input-text-gray); 15 | padding: 7px; 16 | align-items: center; 17 | justify-content: center; 18 | } 19 | 20 | .copyconfirmation-content p { 21 | color: var(--input-text-gray); 22 | margin-left: 5px; 23 | } 24 | -------------------------------------------------------------------------------- /src/Styles/Preview/ExpandButton.css: -------------------------------------------------------------------------------- 1 | .expandbutton-container { 2 | position: absolute; 3 | left: 0px; 4 | bottom: 55px; 5 | width: 40px; 6 | box-shadow: 2px 2px 7px black; 7 | cursor: pointer; 8 | border-radius: 25px; 9 | height: 40px; 10 | background-color: var(--input-text-light-gray); 11 | } 12 | 13 | .expandbutton-container:hover { 14 | background-color: var(--hover-gray); 15 | } 16 | 17 | .expandbutton-container:hover svg { 18 | transform: scale(1.1); 19 | } 20 | 21 | .expandbutton-container svg { 22 | transition: transform 0.1s; 23 | } 24 | -------------------------------------------------------------------------------- /src/Styles/Preview/DownloadButton.css: -------------------------------------------------------------------------------- 1 | .downloadbutton-container { 2 | position: absolute; 3 | left: 0px; 4 | bottom: 0px; 5 | width: 40px; 6 | box-shadow: 2px 2px 7px black; 7 | cursor: pointer; 8 | border-radius: 25px; 9 | height: 40px; 10 | background-color: var(--input-text-light-gray); 11 | } 12 | 13 | .downloadbutton-container:hover { 14 | background-color: var(--hover-gray); 15 | } 16 | 17 | .downloadbutton-container:hover svg { 18 | transform: scale(1.1); 19 | } 20 | 21 | .downloadbutton-container svg { 22 | transition: transform 0.1s; 23 | } 24 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/Components/Preview/Degrees.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../../Styles/Preview/Degrees.css'; 3 | 4 | function Degrees(props) { 5 | const { degrees, handleDegreesChange } = props; 6 | 7 | return ( 8 |
9 |

°

10 | 17 |
18 | ); 19 | } 20 | 21 | export default Degrees; 22 | -------------------------------------------------------------------------------- /src/Components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../Styles/Header.css'; 3 | import logo from '../Assets/logo.png'; 4 | 5 | function Header(props) { 6 | return ( 7 |
8 | logo 9 |

10 | gradient 11 | 18 | 19 | 20 |

21 |

BETA

22 |
23 | ); 24 | } 25 | 26 | export default Header; 27 | -------------------------------------------------------------------------------- /src/Styles/CSS/CSS.css: -------------------------------------------------------------------------------- 1 | .css-container { 2 | margin-top: 15px; 3 | position: relative; 4 | height: auto; 5 | display: flex; 6 | flex-direction: column; 7 | width: 350px; 8 | } 9 | 10 | .css-content { 11 | height: 154px; 12 | } 13 | 14 | .css-content textarea { 15 | background: var(--css-gray); 16 | color: var(--input-text-light-gray); 17 | text-align: left; 18 | font-family: monospace; 19 | padding: 20px; 20 | font-size: 12px; 21 | width: 350px; 22 | border-radius: 5px; 23 | resize: none; 24 | box-sizing: border-box; 25 | } 26 | 27 | .css-container:hover .copybutton-container { 28 | display: flex; 29 | -webkit-animation: fadein 0.5s; 30 | animation: fadein 0.5s; 31 | } 32 | -------------------------------------------------------------------------------- /src/Styles/CSS/CopyButton.css: -------------------------------------------------------------------------------- 1 | .copybutton-container { 2 | display: none; 3 | position: absolute; 4 | right: 10px; 5 | bottom: 10px; 6 | box-shadow: 2px 2px 7px black; 7 | cursor: pointer; 8 | border-radius: 25px; 9 | height: 40px; 10 | background-color: var(--input-text-light-gray); 11 | 12 | -moz-transition: all 0.1s ease-in; 13 | -o-transition: all 0.1s ease-in; 14 | -webkit-transition: all 0.1s ease-in; 15 | transition: all 0.1s ease-in; 16 | } 17 | 18 | .copybutton-container:hover { 19 | background-color: var(--hover-gray); 20 | } 21 | 22 | .copybutton-container:hover svg { 23 | transform: scale(1.1); 24 | } 25 | 26 | .copybutton-container svg { 27 | transition: transform 0.1s; 28 | } 29 | -------------------------------------------------------------------------------- /src/Styles/Header.css: -------------------------------------------------------------------------------- 1 | .header-container { 2 | height: auto; 3 | background-color: #fafafa; 4 | display: flex; 5 | padding: 5px 50px; 6 | align-items: center; 7 | } 8 | 9 | .header-container h1 { 10 | background: linear-gradient(45deg, #87cefa 0%, #da71d6 100%); 11 | background-clip: text; 12 | -webkit-background-clip: text; 13 | color: transparent; 14 | padding: 0px; 15 | margin: 0px; 16 | font-weight: bold; 17 | font-size: 30px; 18 | } 19 | 20 | .header-container img { 21 | height: 25px; 22 | width: 25px; 23 | margin-right: 10px; 24 | } 25 | 26 | .header-container p { 27 | margin-left: 5px; 28 | font-size: 10px; 29 | font-weight: 600; 30 | color: var(--label-gray); 31 | margin-bottom: 15px; 32 | } 33 | -------------------------------------------------------------------------------- /src/Components/CSS/CopyConfirmation.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../../Styles/CSS/CopyConfirmation.css'; 3 | import Slide from '@material-ui/core/Slide'; 4 | import { MdCheckCircle } from 'react-icons/md'; 5 | import { INPUT_TEXT_GRAY } from '../../Utils/hexConstants'; 6 | 7 | function CopyConfirmation(props) { 8 | const { display } = props; 9 | 10 | return ( 11 | 12 |
13 |
14 | 15 |

CSS copied!

16 |
17 |
18 |
19 | ); 20 | } 21 | 22 | export default CopyConfirmation; 23 | -------------------------------------------------------------------------------- /src/Styles/HexPicker/CurrentColor.css: -------------------------------------------------------------------------------- 1 | .currentcolor-container { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | 6 | .currentcolor-content { 7 | display: flex; 8 | margin: 0px 0px; 9 | } 10 | 11 | .currentcolor-colorbox { 12 | border-radius: 5px; 13 | height: 150px; 14 | width: 150px; 15 | } 16 | 17 | .currentcolor-info { 18 | display: grid; 19 | grid-template-columns: 30% 70%; 20 | grid-template-rows: 25% 25% 25%; 21 | width: 130px; 22 | align-items: center; 23 | margin-left: 20px; 24 | } 25 | 26 | .currentcolor-info p { 27 | text-align: right; 28 | color: var(--label-gray); 29 | font-weight: 600; 30 | padding-right: 10px; 31 | margin: 0px; 32 | } 33 | 34 | .currentcolor-info input { 35 | width: 80px; 36 | height: 25px; 37 | font-size: 12px; 38 | } 39 | -------------------------------------------------------------------------------- /src/Styles/Preview/Center.css: -------------------------------------------------------------------------------- 1 | .center-container { 2 | display: flex; 3 | align-items: center; 4 | flex-grow: 1; 5 | justify-content: flex-end; 6 | margin-right: 3px; 7 | margin-left: -3px; 8 | } 9 | 10 | .center-grid { 11 | margin-left: 5px; 12 | display: grid; 13 | height: 40px; 14 | width: 40px; 15 | grid-template-columns: 1fr 1fr 1fr; 16 | grid-gap: 1px; 17 | cursor: pointer; 18 | } 19 | 20 | .center-grid-item { 21 | height: 100%; 22 | width: 100%; 23 | border: 1px solid var(--label-gray); 24 | box-sizing: content-box; 25 | } 26 | 27 | .center-grid-item:hover { 28 | background-color: var(--hover-gray); 29 | } 30 | 31 | .center-grid-item-selected { 32 | height: 100%; 33 | width: 100%; 34 | background-color: var(--hover-gray); 35 | border: 0.5px solid var(--label-gray); 36 | } 37 | -------------------------------------------------------------------------------- /src/Components/CSS/CopyButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../../Styles/CSS/CopyButton.css'; 3 | import { MdContentCopy } from 'react-icons/md'; 4 | import { INPUT_TEXT_GRAY } from '../../Utils/hexConstants'; 5 | import { ButtonBase } from '@material-ui/core'; 6 | 7 | function CopyButton(props) { 8 | const { clickFunction } = props; 9 | 10 | return ( 11 |
16 | 17 | 22 | 23 |
24 | ); 25 | } 26 | 27 | export default CopyButton; 28 | -------------------------------------------------------------------------------- /src/Styles/Suggested/Suggested.css: -------------------------------------------------------------------------------- 1 | .suggested-container { 2 | display: flex; 3 | flex-direction: column; 4 | margin-top: 30px; 5 | } 6 | 7 | .suggested-content { 8 | display: flex; 9 | } 10 | 11 | .suggested-header { 12 | display: flex; 13 | align-items: center; 14 | } 15 | 16 | .suggested-icon-container { 17 | cursor: pointer; 18 | margin-left: 10px; 19 | padding: 5px; 20 | border-radius: 20px; 21 | height: 23px; 22 | width: 23px; 23 | margin-bottom: 5px; 24 | display: flex; 25 | align-items: center; 26 | justify-content: center; 27 | } 28 | 29 | .suggested-icon-container svg { 30 | transition: transform 0.1s; 31 | } 32 | 33 | .suggested-icon-container:hover { 34 | background-color: var(--hover-gray); 35 | } 36 | 37 | .suggested-icon-container:hover svg { 38 | transform: scale(1.1); 39 | } 40 | -------------------------------------------------------------------------------- /src/Components/Preview/DownloadButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../../Styles/Preview/DownloadButton.css'; 3 | import { FiDownload } from 'react-icons/fi'; 4 | import { INPUT_TEXT_GRAY } from '../../Utils/hexConstants'; 5 | import { ButtonBase } from '@material-ui/core'; 6 | 7 | function DownloadButton(props) { 8 | const { clickFunction } = props; 9 | 10 | return ( 11 |
16 | 17 | 22 | 23 |
24 | ); 25 | } 26 | 27 | export default DownloadButton; 28 | -------------------------------------------------------------------------------- /src/Components/Preview/ExpandButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../../Styles/Preview/ExpandButton.css'; 3 | import { BsArrowsAngleExpand } from 'react-icons/bs'; 4 | import { INPUT_TEXT_GRAY } from '../../Utils/hexConstants'; 5 | import { ButtonBase } from '@material-ui/core'; 6 | 7 | function ExpandButton(props) { 8 | const { clickFunction } = props; 9 | 10 | return ( 11 |
16 | 17 | 22 | 23 |
24 | ); 25 | } 26 | 27 | export default ExpandButton; 28 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | gradient 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/Components/Stack/AddColorButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../../Styles/Stack/AddColorButton.css'; 3 | import { IoIosAdd } from 'react-icons/io'; 4 | import { INPUT_TEXT_GRAY } from '../../Utils/hexConstants'; 5 | import { ButtonBase } from '@material-ui/core'; 6 | 7 | function AddColorButton(props) { 8 | const { clickFunction, disabled } = props; 9 | 10 | const buttonContainer = disabled 11 | ? 'add-color-button-disabled' 12 | : 'add-color-container'; 13 | 14 | return ( 15 |
16 | 17 |
18 | 19 |

Add Color

20 |
21 |
22 |
23 | ); 24 | } 25 | 26 | export default AddColorButton; 27 | -------------------------------------------------------------------------------- /src/Components/Suggested/SuggestedItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../../Styles/Suggested/SuggestedItem.css'; 3 | import Fade from '@material-ui/core/Fade'; 4 | 5 | function SuggestedItem(props) { 6 | const { gradient, selected, setSuggested } = props; 7 | const background = gradient.toBgString(); 8 | const selectedDiv = selected ? 'suggesteditem-selected' : ''; 9 | 10 | return ( 11 | 12 |
13 |
14 |
18 |

{gradient.name || ''}

19 |
20 |
21 |
22 | ); 23 | } 24 | 25 | export default SuggestedItem; 26 | -------------------------------------------------------------------------------- /src/Components/Preview/Dimensions.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../../Styles/Preview/Dimensions.css'; 3 | 4 | function Dimensions(props) { 5 | const { height, width, handleWidthChange, handleHeightChange } = props; 6 | 7 | return ( 8 |
9 |

W

10 | 17 |

H

18 | 25 |
26 | ); 27 | } 28 | 29 | export default Dimensions; 30 | -------------------------------------------------------------------------------- /src/Styles/Suggested/SuggestedItem.css: -------------------------------------------------------------------------------- 1 | .suggesteditem-container { 2 | display: flex; 3 | flex-direction: column; 4 | width: auto; 5 | align-items: center; 6 | padding: 10px; 7 | cursor: pointer; 8 | border-radius: 5px; 9 | 10 | -moz-transition: all 0.1s ease-in; 11 | -o-transition: all 0.1s ease-in; 12 | -webkit-transition: all 0.1s ease-in; 13 | transition: all 0.1s ease-in; 14 | } 15 | 16 | .suggesteditem-container p { 17 | color: var(--input-text-gray); 18 | } 19 | 20 | .suggesteditem-container:hover p { 21 | font-weight: 600; 22 | } 23 | 24 | .suggesteditem-colorbox { 25 | height: 60px; 26 | width: 60px; 27 | border-radius: 5px; 28 | margin-bottom: 5px; 29 | } 30 | 31 | .suggesteditem-container:hover { 32 | background-color: var(--hover-gray); 33 | } 34 | 35 | .suggesteditem-selected { 36 | border-radius: 5px; 37 | background-color: var(--hover-gray); 38 | } 39 | 40 | .suggesteditem-selected p { 41 | font-weight: 600; 42 | } 43 | -------------------------------------------------------------------------------- /src/Styles/Stack/AddColorButton.css: -------------------------------------------------------------------------------- 1 | .add-color-container { 2 | margin-top: 20px; 3 | display: flex; 4 | justify-content: center; 5 | } 6 | 7 | .add-color-container p { 8 | color: var(--input-text-gray); 9 | } 10 | 11 | .add-color-button { 12 | display: flex; 13 | align-items: center; 14 | justify-content: center; 15 | padding: 5px 15px 5px 10px; 16 | border-radius: 30px; 17 | box-shadow: 2px 2px 7px var(--input-text-gray); 18 | width: 130px; 19 | cursor: pointer; 20 | -moz-transition: all 0.1s ease-in; 21 | -o-transition: all 0.1s ease-in; 22 | -webkit-transition: all 0.1s ease-in; 23 | transition: all 0.1s ease-in; 24 | } 25 | 26 | .add-color-button-disabled { 27 | display: none; 28 | } 29 | 30 | .add-color-button:hover { 31 | background-color: var(--hover-gray); 32 | } 33 | 34 | .add-color-button:hover svg { 35 | transform: scale(1.1); 36 | } 37 | 38 | .add-color-button:hover p { 39 | font-weight: 600; 40 | } 41 | 42 | .add-color-button svg { 43 | transition: transform 0.1s; 44 | } 45 | -------------------------------------------------------------------------------- /src/Styles/Stack/Stack.css: -------------------------------------------------------------------------------- 1 | .stack-container { 2 | display: flex; 3 | flex-direction: column; 4 | width: 450px; 5 | height: 400px; 6 | } 7 | 8 | .stack-icon-container { 9 | cursor: pointer; 10 | padding: 5px; 11 | margin-left: 10px; 12 | border-radius: 20px; 13 | height: 23px; 14 | margin-bottom: 5px; 15 | display: flex; 16 | align-items: center; 17 | justify-content: center; 18 | } 19 | 20 | .stack-icon-container svg { 21 | transition: transform 0.1s; 22 | } 23 | 24 | .stack-icon-container:hover { 25 | background-color: var(--hover-gray); 26 | } 27 | 28 | .stack-icon-container:hover svg { 29 | transform: scale(1.1); 30 | } 31 | 32 | .stack-header { 33 | display: flex; 34 | align-items: center; 35 | } 36 | 37 | .stack-container-label { 38 | display: grid; 39 | width: 100%; 40 | grid-template-columns: 10% 55% 25% 10%; 41 | grid-template-rows: 100%; 42 | align-items: center; 43 | } 44 | 45 | .stack-container-label p { 46 | text-align: center; 47 | color: var(--label-gray); 48 | font-weight: 600; 49 | } 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-Present Vedant Kothari 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gradient", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "^4.9.14", 7 | "@testing-library/jest-dom": "^4.2.4", 8 | "@testing-library/react": "^9.5.0", 9 | "@testing-library/user-event": "^7.2.1", 10 | "react": "^16.13.1", 11 | "react-dom": "^16.13.1", 12 | "react-draggable": "^4.4.2", 13 | "react-icons": "^3.10.0", 14 | "react-scripts": "3.4.1" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": "react-app" 24 | }, 25 | "browserslist": { 26 | "production": [ 27 | ">0.2%", 28 | "not dead", 29 | "not op_mini all" 30 | ], 31 | "development": [ 32 | "last 1 chrome version", 33 | "last 1 firefox version", 34 | "last 1 safari version" 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Styles/HexPicker/HexGradient.css: -------------------------------------------------------------------------------- 1 | .hexgradient-container { 2 | height: 250px; 3 | width: 250px; 4 | margin-right: 20px; 5 | position: relative; 6 | display: flex; 7 | cursor: pointer; 8 | } 9 | 10 | .hexgradient-white, 11 | .hexgradient-black { 12 | border-radius: 5px; 13 | } 14 | 15 | .hexgradient-base { 16 | border-radius: 8px 5px 5px 5px; 17 | } 18 | 19 | .hexgradient-base, 20 | .hexgradient-white, 21 | .hexgradient-black { 22 | height: 100%; 23 | width: 100%; 24 | position: absolute; 25 | } 26 | 27 | .hexgradient-white { 28 | background: linear-gradient(to right, #fff 0%, rgba(255, 255, 255, 0) 100%); 29 | } 30 | 31 | .hexgradient-black { 32 | background: linear-gradient(to bottom, transparent 0%, #000 100%); 33 | } 34 | 35 | .sl-slider { 36 | height: 20px; 37 | width: 20px; 38 | border-radius: 20px; 39 | background-color: var(--input-text-light-gray); 40 | box-shadow: 2px 2px 7px black; 41 | z-index: 1; 42 | margin-top: -10px; 43 | margin-left: -10px; 44 | } 45 | 46 | .sl-slider:hover { 47 | background-color: var(--hover-gray); 48 | } 49 | -------------------------------------------------------------------------------- /src/Components/CSS/CSS.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../../Styles/CSS/CSS.css'; 3 | import CopyButton from './CopyButton'; 4 | 5 | class CSS extends React.Component { 6 | copy = (e) => { 7 | const { showCSSConfirmation } = this.props; 8 | this.textArea.select(); 9 | document.execCommand('copy'); 10 | e.target.focus(); 11 | 12 | showCSSConfirmation(); 13 | }; 14 | 15 | render() { 16 | const { gradient } = this.props; 17 | const background = gradient.toCSSBgString(); 18 | 19 | return ( 20 |
21 |

CSS

22 |
23 | 31 | this.copy(e)} /> 32 |
33 |
34 | ); 35 | } 36 | } 37 | 38 | export default CSS; 39 | -------------------------------------------------------------------------------- /src/Components/Preview/Center.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../../Styles/Preview/Center.css'; 3 | 4 | function Center(props) { 5 | const { center, clickFunction } = props; 6 | const positions = [ 7 | 'top left', 8 | 'top center', 9 | 'top right', 10 | 'center left', 11 | 'center center', 12 | 'center right', 13 | 'bottom left', 14 | 'bottom center', 15 | 'bottom right', 16 | ]; 17 | 18 | const renderPositions = positions.map((position) => 19 | position === center ? ( 20 |
24 | ) : ( 25 |
clickFunction(position)} 28 | key={'center' + position} 29 | title={`Change gradient center to ${position}`} 30 | /> 31 | ) 32 | ); 33 | return ( 34 |
35 |

CENTER

36 |
{renderPositions}
37 |
38 | ); 39 | } 40 | 41 | export default Center; 42 | -------------------------------------------------------------------------------- /src/Components/HexPicker/HexGradient.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../../Styles/HexPicker/HexGradient.css'; 3 | import Draggable from 'react-draggable'; 4 | 5 | function HexGradient(props) { 6 | const { colorwheelColor, SV, updatePosition } = props; 7 | const { x, y } = SV; 8 | 9 | function Slider() { 10 | return ( 11 | { 13 | updatePosition({ x: value.x, y: value.y }); 14 | }} 15 | defaultPosition={{ x, y }} 16 | bounds={{ left: 0, right: 250, top: 0, bottom: 250 }} 17 | > 18 |
19 |
20 | ); 21 | } 22 | 23 | return ( 24 |
28 |
32 |
33 |
34 | 35 |
36 | ); 37 | } 38 | 39 | export default HexGradient; 40 | -------------------------------------------------------------------------------- /src/Components/HexPicker/HexPicker.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../../Styles/HexPicker/HexPicker.css'; 3 | import ColorSlider from './ColorSlider'; 4 | import HexGradient from './HexGradient'; 5 | import CurrentColor from './CurrentColor'; 6 | 7 | function HexPicker(props) { 8 | const { 9 | colorwheelColor, 10 | color, 11 | handleHexChange, 12 | handleRChange, 13 | handleGChange, 14 | handleBChange, 15 | hue, 16 | handleColorSlider, 17 | SV, 18 | updatePosition, 19 | } = props; 20 | return ( 21 |
22 | 29 |
30 | 35 | 36 |
37 |
38 | ); 39 | } 40 | 41 | export default HexPicker; 42 | -------------------------------------------------------------------------------- /src/Components/Suggested/Suggested.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../../Styles/Suggested/Suggested.css'; 3 | import SuggestedItem from './SuggestedItem'; 4 | import { FiShuffle } from 'react-icons/fi'; 5 | import { INPUT_TEXT_GRAY } from '../../Utils/hexConstants'; 6 | import { ButtonBase } from '@material-ui/core'; 7 | 8 | function Suggested(props) { 9 | const { selected, suggested, setSuggested, shuffleSuggested } = props; 10 | 11 | const renderSuggested = suggested.map((gradient) => ( 12 | setSuggested(e, gradient.name)} 17 | /> 18 | )); 19 | 20 | return ( 21 |
22 |
23 |

SUGGESTED

24 |
shuffleSuggested(e)} 27 | title='Shuffle' 28 | > 29 | 30 | 31 | 32 |
33 |
34 |
{renderSuggested}
35 |
36 | ); 37 | } 38 | 39 | export default Suggested; 40 | -------------------------------------------------------------------------------- /src/Styles/Preview/Preview.css: -------------------------------------------------------------------------------- 1 | .preview-container { 2 | display: table; 3 | flex-direction: column; 4 | height: 450px; 5 | width: 350px; 6 | } 7 | 8 | .preview-header { 9 | display: flex; 10 | justify-content: space-between; 11 | } 12 | 13 | .preview-header h2 { 14 | flex-grow: 1; 15 | } 16 | 17 | .preview-content { 18 | border-radius: 5px; 19 | position: absolute; 20 | top: 0px; 21 | left: 0px; 22 | } 23 | 24 | .preview-content-wrapper { 25 | position: relative; 26 | height: 350px; 27 | width: 350px; 28 | display: table; 29 | } 30 | 31 | .preview-interface { 32 | display: flex; 33 | margin-top: 10px; 34 | align-items: center; 35 | justify-content: space-between; 36 | } 37 | 38 | .preview-buttons-container { 39 | display: none; 40 | -moz-transition: all 0.1s ease-in; 41 | -o-transition: all 0.1s ease-in; 42 | -webkit-transition: all 0.1s ease-in; 43 | transition: all 0.1s ease-in; 44 | position: absolute; 45 | left: 10px; 46 | } 47 | 48 | .preview-container:hover .preview-buttons-container { 49 | display: flex; 50 | -webkit-animation: fadein 0.5s; 51 | animation: fadein 0.5s; 52 | } 53 | 54 | .preview-interface input { 55 | height: 40px; 56 | width: 70px; 57 | font-size: 15px; 58 | } 59 | 60 | .preview-interface p { 61 | color: var(--label-gray); 62 | font-weight: 600; 63 | } 64 | 65 | .preview-header { 66 | display: flex; 67 | align-items: center; 68 | margin-bottom: 10px; 69 | } 70 | 71 | .preview-header h2 { 72 | padding-right: 20px; 73 | border-right: 1px solid var(--label-gray); 74 | margin: 0px; 75 | } 76 | -------------------------------------------------------------------------------- /src/Components/Preview/LinearRadial.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | RadioGroup, 4 | FormControl, 5 | FormControlLabel, 6 | Radio, 7 | } from '@material-ui/core'; 8 | import { withStyles } from '@material-ui/core/styles'; 9 | import { LABEL_GRAY, HOVER_GRAY } from '../../Utils/hexConstants'; 10 | 11 | const GrayRadio = withStyles({ 12 | root: { 13 | color: LABEL_GRAY, 14 | '&$checked': { 15 | color: LABEL_GRAY, 16 | }, 17 | '&:hover': { 18 | background: HOVER_GRAY, 19 | }, 20 | padding: 3, 21 | flexGrow: 1, 22 | }, 23 | checked: {}, 24 | })((props) => ); 25 | 26 | function LinearRadial(props) { 27 | const { isLinear, changeFunction } = props; 28 | const value = isLinear ? 'linear' : 'radial'; 29 | 30 | return ( 31 | 32 | 38 | } 41 | label='LINEAR' 42 | title='Change to linear gradient' 43 | /> 44 | } 47 | label='RADIAL' 48 | title='Change to radial gradient' 49 | /> 50 | 51 | 52 | ); 53 | } 54 | 55 | export default LinearRadial; 56 | -------------------------------------------------------------------------------- /src/Styles/Stack/StackItem.css: -------------------------------------------------------------------------------- 1 | .stackitem-container { 2 | display: grid; 3 | grid-template-columns: 10% 55% 25% 10%; 4 | width: auto; 5 | grid-template-rows: 100%; 6 | align-items: center; 7 | padding: 0px 10px; 8 | border-radius: 5px; 9 | opacity: 0.999; 10 | position: relative; 11 | 12 | -moz-transition: all 0.1s ease-in; 13 | -o-transition: all 0.1s ease-in; 14 | -webkit-transition: all 0.1s ease-in; 15 | transition: all 0.1s ease-in; 16 | } 17 | 18 | .stackitem-drag svg, 19 | .stackitem-close svg { 20 | transition: transform 0.1s; 21 | } 22 | 23 | .stackitem-close svg { 24 | cursor: pointer; 25 | } 26 | 27 | .stackitem-drag svg { 28 | cursor: move; 29 | } 30 | 31 | .stackitem-drag svg:hover, 32 | .stackitem-close svg:hover { 33 | transform: scale(1.15); 34 | } 35 | 36 | .stackitem-container input { 37 | height: 40px; 38 | font-size: 15px; 39 | width: 90%; 40 | margin: 7px; 41 | } 42 | 43 | .stackitem-icon-container { 44 | width: 48px; 45 | } 46 | 47 | .stackitem-drag, 48 | .stackitem-close, 49 | .stackitem-no-close { 50 | display: none; 51 | } 52 | 53 | .stackitem-container:hover .stackitem-drag, 54 | .stackitem-container:hover .stackitem-close, 55 | .stackitem-container:hover .stackitem-no-close, 56 | .stackitem-selected .stackitem-drag, 57 | .stackitem-selected .stackitem-close, 58 | .stackitem-selected .stackitem-no-close { 59 | display: flex; 60 | align-items: center; 61 | } 62 | 63 | .stackitem-no-close { 64 | cursor: not-allowed !important; 65 | } 66 | 67 | .stackitem-container:hover { 68 | background-color: var(--hover-gray); 69 | } 70 | 71 | .stackitem-selected { 72 | background-color: var(--hover-gray); 73 | border-radius: 5px; 74 | } 75 | 76 | .stackitem-dark input { 77 | color: var(--input-text-light-gray); 78 | } 79 | -------------------------------------------------------------------------------- /src/Components/StopBar/StopBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../../Styles/StopBar/StopBar.css'; 3 | import Slider from '@material-ui/core/Slider'; 4 | import { withStyles } from '@material-ui/core/styles'; 5 | 6 | function StopBar(props) { 7 | const { gradient, handleStopSlider } = props; 8 | const background = gradient.toStopBarBgString(); 9 | const stopValues = gradient.getSortedStops(); 10 | 11 | const StopBarSlider = withStyles({ 12 | root: { 13 | borderRadius: 4, 14 | width: 800, 15 | padding: '0px !important', 16 | }, 17 | thumb: { 18 | height: 30, 19 | width: 10, 20 | backgroundColor: 'var(--input-text-light-gray)', 21 | opacity: 50, 22 | borderRadius: 5, 23 | marginTop: 5, 24 | marginLeft: 5, 25 | marginRight: 5, 26 | boxShadow: '2px 2px 7px black', 27 | '&:focus, &:hover, &$active': { 28 | boxShadow: '2px 2px 7px black', 29 | background: 'var(--hover-gray)', 30 | }, 31 | }, 32 | track: { 33 | height: 40, 34 | borderRadius: 4, 35 | background: 'transparent', 36 | }, 37 | rail: { 38 | height: 40, 39 | borderRadius: 4, 40 | background, 41 | opacity: 100, 42 | paddingLeft: '10px', 43 | paddingRight: '10px', 44 | width: 800, 45 | }, 46 | })(Slider); 47 | 48 | return ( 49 |
50 | handleStopSlider(value)} 54 | title='Change stops' 55 | /> 56 |
57 | ); 58 | } 59 | 60 | export default StopBar; 61 | -------------------------------------------------------------------------------- /src/Components/HexPicker/CurrentColor.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../../Styles/HexPicker/CurrentColor.css'; 3 | 4 | function CurrentColor(props) { 5 | const { 6 | color, 7 | handleHexChange, 8 | handleRChange, 9 | handleGChange, 10 | handleBChange, 11 | } = props; 12 | const { hex, r, g, b } = color; 13 | 14 | return ( 15 |
16 |

COLOR

17 |
18 |
22 |
23 |

#

24 | handleHexChange(e, false)} 29 | title='Enter hex code' 30 | > 31 |

R

32 | handleRChange(e)} 36 | title='Enter red value' 37 | > 38 |

G

39 | handleGChange(e)} 43 | title='Enter green value' 44 | > 45 |

B

46 | handleBChange(e)} 50 | title='Enter blue value' 51 | > 52 |
53 |
54 |
55 | ); 56 | } 57 | 58 | export default CurrentColor; 59 | -------------------------------------------------------------------------------- /src/Components/HexPicker/ColorSlider.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../../Styles/HexPicker/ColorSlider.css'; 3 | import Slider from '@material-ui/core/Slider'; 4 | import { withStyles } from '@material-ui/core/styles'; 5 | 6 | function ColorSlider(props) { 7 | const { hue, handleColorSlider } = props; 8 | 9 | const ColorSlider = withStyles({ 10 | root: { 11 | borderRadius: 4, 12 | height: '240px !important', 13 | width: 20, 14 | padding: '0px 0px !important', 15 | position: 'absolute', 16 | bottom: '0px', 17 | display: 'flex !important', 18 | }, 19 | thumb: { 20 | height: 7, 21 | width: 15, 22 | backgroundColor: 'var(--input-text-light-gray)', 23 | opacity: 50, 24 | borderRadius: 5, 25 | boxShadow: '2px 2px 7px black', 26 | '&:focus, &:hover, &$active': { 27 | boxShadow: '2px 2px 7px black', 28 | background: 'var(--hover-gray)', 29 | }, 30 | margin: '0px 2px 2px 2px !important', 31 | }, 32 | 33 | track: { 34 | borderRadius: 4, 35 | background: 'transparent', 36 | height: 250, 37 | }, 38 | rail: { 39 | borderRadius: 4, 40 | background: 41 | 'linear-gradient(to top, red 0%, #ff0 17%, lime 33%, cyan 50%, blue 66%, magenta 83%, red 100%)', 42 | opacity: 100, 43 | width: '20px !important', 44 | paddingTop: '10px', 45 | paddingBottom: '10px', 46 | height: '230px !important', 47 | position: 'absolute', 48 | bottom: '0px', 49 | }, 50 | })(Slider); 51 | 52 | return ( 53 |
54 | handleColorSlider(value)} 60 | title='Change hue' 61 | /> 62 |
63 | ); 64 | } 65 | 66 | export default ColorSlider; 67 | -------------------------------------------------------------------------------- /src/Utils/Gradient.js: -------------------------------------------------------------------------------- 1 | import { toBgString, toStopBarBgString, toCSSBgString } from './colorUtils'; 2 | 3 | class Gradient { 4 | constructor(stack, isLinear, degrees, center, name) { 5 | this.stack = stack; // array of Color objects 6 | this.isLinear = isLinear; // false if radial 7 | this.degrees = degrees; // Number 0 - 360 8 | this.center = center; // one of 9 positions 9 | this.name = name; // name of friendo if suggested 10 | } 11 | 12 | toBgString = () => { 13 | return toBgString(this); 14 | }; 15 | 16 | toStopBarBgString = () => { 17 | return toStopBarBgString(this); 18 | }; 19 | 20 | toCSSBgString = () => { 21 | return toCSSBgString(this); 22 | }; 23 | 24 | // sort stack by increasing order of stop values 25 | sortStack = () => { 26 | const { stack } = this; 27 | stack.sort((a, b) => (a.stop > b.stop ? 1 : -1)); 28 | let selected; 29 | for (let i = 0; i < stack.length; i++) { 30 | const color = stack[i]; 31 | color.index = i; 32 | if (color.selected) { 33 | selected = i; 34 | } 35 | } 36 | return selected; 37 | }; 38 | 39 | clone = () => { 40 | return new Gradient( 41 | this.stack.map((color) => color.clone()), 42 | this.isLinear, 43 | this.degrees, 44 | this.center, 45 | '' 46 | ); 47 | }; 48 | 49 | getSortedStops = () => { 50 | const { stack } = this; 51 | return stack 52 | .map((color) => color.stop) 53 | .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); 54 | }; 55 | 56 | reverse = () => { 57 | const { stack } = this; 58 | let stops = this.getSortedStops(); 59 | let stopValue; 60 | 61 | stack.reverse(); 62 | 63 | for (let i = 0; i < stack.length; i++) { 64 | const color = stack[i]; 65 | color.index = i; 66 | color.stop = stops[i]; 67 | 68 | if (color.selected) { 69 | stopValue = stops[i]; 70 | } 71 | } 72 | 73 | return stopValue; 74 | }; 75 | } 76 | 77 | export { Gradient }; 78 | -------------------------------------------------------------------------------- /src/Components/Stack/Stack.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../../Styles/Stack/Stack.css'; 3 | import StackItem from './StackItem'; 4 | import AddColorButton from './AddColorButton'; 5 | import { BsArrowUpDown } from 'react-icons/bs'; 6 | import { INPUT_TEXT_GRAY } from '../../Utils/hexConstants'; 7 | import { ButtonBase } from '@material-ui/core'; 8 | 9 | function Stack(props) { 10 | const { 11 | gradient, 12 | addColor, 13 | changeSelected, 14 | deleteColor, 15 | handleKeyDown, 16 | changeValue, 17 | stopValue, 18 | handleHexChange, 19 | onDragStart, 20 | onDragOver, 21 | onDragEnd, 22 | reverseStack, 23 | handleStopChange, 24 | } = props; 25 | const { stack } = gradient; 26 | 27 | const renderStack = stack.map((color) => ( 28 | deleteColor(e, color.index)} 32 | changeSelected={() => changeSelected(color.index)} 33 | key={'stackitem-' + color.index} 34 | cannotDelete={stack.length === 2} 35 | handleKeyDown={handleKeyDown} 36 | changeValue={changeValue} 37 | stopValue={stopValue} 38 | handleHexChange={handleHexChange} 39 | onDragStart={onDragStart} 40 | onDragOver={onDragOver} 41 | onDragEnd={onDragEnd} 42 | handleStopChange={handleStopChange} 43 | /> 44 | )); 45 | 46 | return ( 47 |
48 |
49 |

STACK

50 |
55 | 56 | 57 | 58 |
59 |
60 | 61 |
62 |

63 |

HEX

64 |

%

65 |
66 | {renderStack} 67 | 71 |
72 | ); 73 | } 74 | 75 | export default Stack; 76 | -------------------------------------------------------------------------------- /src/Utils/generalUtils.js: -------------------------------------------------------------------------------- 1 | function shuffle(array) { 2 | var currentIndex = array.length, 3 | temporaryValue, 4 | randomIndex; 5 | 6 | // While there remain elements to shuffle... 7 | while (0 !== currentIndex) { 8 | // Pick a remaining element... 9 | randomIndex = Math.floor(Math.random() * currentIndex); 10 | currentIndex -= 1; 11 | 12 | // And swap it with the current element. 13 | temporaryValue = array[currentIndex]; 14 | array[currentIndex] = array[randomIndex]; 15 | array[randomIndex] = temporaryValue; 16 | } 17 | 18 | return array; 19 | } 20 | 21 | function toRadians(degrees) { 22 | return degrees * (Math.PI / 180); 23 | } 24 | 25 | function calculateCenterOffset(width, height, center) { 26 | switch (center) { 27 | case 'top left': 28 | return { width: -width / 2, height: -height / 2 }; 29 | case 'top center': 30 | return { width: 0, height: -height / 2 }; 31 | case 'top right': 32 | return { width: width / 2, height: -height / 2 }; 33 | case 'center left': 34 | return { width: -width / 2, height: 0 }; 35 | case 'center center': 36 | return { width: 0, height: 0 }; 37 | case 'center right': 38 | return { width: width / 2, height: 0 }; 39 | case 'bottom left': 40 | return { width: -width / 2, height: height / 2 }; 41 | case 'bottom center': 42 | return { width: 0, height: height / 2 }; 43 | case 'bottom right': 44 | return { width: width / 2, height: height / 2 }; 45 | default: 46 | return; 47 | } 48 | } 49 | 50 | function calculateRadius(width, height, center) { 51 | const hyp = Math.sqrt(width * width + height * height); 52 | const longer = Math.max(width, height); 53 | const shorter = Math.min(width, height); 54 | const halfHype = Math.sqrt((longer * longer) / 4 + shorter * shorter); 55 | 56 | switch (center) { 57 | case 'top left': 58 | case 'top right': 59 | case 'bottom right': 60 | case 'bottom left': 61 | return hyp; 62 | case 'top center': 63 | case 'center right': 64 | case 'bottom center': 65 | case 'center left': 66 | return halfHype; 67 | case 'center center': 68 | return longer / 2; 69 | default: 70 | return; 71 | } 72 | } 73 | 74 | function padLeft(s) { 75 | return s.length === 1 ? '0' + s : s; 76 | } 77 | 78 | export { shuffle, toRadians, calculateCenterOffset, calculateRadius, padLeft }; 79 | -------------------------------------------------------------------------------- /src/Utils/Color.js: -------------------------------------------------------------------------------- 1 | import { 2 | hexToRGB, 3 | isDark, 4 | getColorwheel, 5 | getHue, 6 | hexToRgb, 7 | getSL, 8 | hslToHex, 9 | rgbToHsv, 10 | hsvToHex, 11 | } from './colorUtils'; 12 | 13 | class Color { 14 | constructor(hex, stop, selected, index) { 15 | this.hex = hex; // 6 char String hex representation of a color 16 | this.stop = stop; // Number 0 - 100 17 | this.selected = selected; // true if selected 18 | this.index = index; // current place in the stack 19 | this.r = this.getRGB('r') || 0; 20 | this.g = this.getRGB('g') || 0; 21 | this.b = this.getRGB('b') || 0; 22 | this.blackPosition = null; 23 | } 24 | 25 | getRGB = (primary) => { 26 | return hexToRGB(this.hex, primary); 27 | }; 28 | 29 | isDark = () => { 30 | return isDark(this.hex); // return if the color is dark 31 | }; 32 | 33 | getColorwheel = () => { 34 | return getColorwheel(this.hex); 35 | }; 36 | 37 | getHue = () => { 38 | const rgb = hexToRgb(this.hex); 39 | if (rgb) { 40 | return getHue(rgb); 41 | } 42 | }; 43 | 44 | isEqual = (color) => { 45 | const { hex } = this; 46 | return color.hex === hex; 47 | }; 48 | 49 | clone = () => { 50 | return new Color(this.hex, this.stop, this.selected, this.index); 51 | }; 52 | 53 | changeHue = (h) => { 54 | const sl = getSL(hexToRgb(this.hex)); 55 | const s = sl.s; 56 | const l = sl.l; 57 | const hsl = { h, s, l }; 58 | this.hex = hslToHex(hsl); 59 | this.r = this.getRGB('r') || 0; 60 | this.g = this.getRGB('g') || 0; 61 | this.b = this.getRGB('b') || 0; 62 | }; 63 | 64 | getSvPosition = () => { 65 | const rgb = hexToRgb(this.hex); 66 | if (rgb) { 67 | const { s, v } = rgbToHsv(rgb); 68 | let x, y; 69 | 70 | if (this.blackPosition) { 71 | x = this.blackPosition.x; 72 | y = this.blackPosition.y; 73 | this.blackPosition = null; 74 | } else { 75 | x = 2.5 * s; 76 | y = 2.5 * (100 - v); 77 | } 78 | return { x, y }; 79 | } 80 | }; 81 | 82 | changeColorFromPosition = ({ x, y }) => { 83 | const h = this.getHue(); 84 | const s = x / 2.5; 85 | const v = 100 - y / 2.5; 86 | this.hex = hsvToHex({ h, s, v }); 87 | this.r = this.getRGB('r') || 0; 88 | this.g = this.getRGB('g') || 0; 89 | this.b = this.getRGB('b') || 0; 90 | 91 | if (y === 250) { 92 | this.blackPosition = { x, y }; 93 | } 94 | }; 95 | } 96 | 97 | export { Color }; 98 | -------------------------------------------------------------------------------- /src/Components/Stack/StackItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../../Styles/Stack/StackItem.css'; 3 | import { IoIosMenu, IoIosClose } from 'react-icons/io'; 4 | import { INPUT_TEXT_GRAY } from '../../Utils/hexConstants'; 5 | 6 | function StackItem(props) { 7 | const { 8 | color, 9 | deleteFunction, 10 | changeSelected, 11 | cannotDelete, 12 | handleKeyDown, 13 | changeValue, 14 | stopValue, 15 | handleHexChange, 16 | onDragStart, 17 | onDragOver, 18 | onDragEnd, 19 | handleStopChange, 20 | } = props; 21 | const { hex, stop, selected } = color; 22 | const selectedDiv = selected ? 'stackitem-selected' : ''; 23 | const darkDiv = color.isDark() ? 'stackitem-dark' : ''; 24 | const closeDiv = cannotDelete ? 'stackitem-no-close' : 'stackitem-close'; 25 | const displayedValue = selected ? stopValue : stop; 26 | 27 | return ( 28 |
29 |
onDragOver(e, color)} 33 | > 34 |
onDragStart(e, color)} 38 | onDragEnd={onDragEnd} 39 | > 40 |
41 | 47 |
48 |
49 |
50 | { 56 | handleHexChange(e, true); 57 | }} 58 | title='Enter hex code' 59 | > 60 |
61 | handleKeyDown(e)} 65 | onChange={(e) => changeValue(e)} 66 | onBlur={(e) => handleStopChange(e)} 67 | title='Enter stop value' 68 | > 69 |
70 |
71 | 78 |
79 |
80 |
81 |
82 | ); 83 | } 84 | 85 | export default StackItem; 86 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Poppins:400,600,700'); 2 | @import url('https://fonts.googleapis.com/css?family=Open+Sans:400,600,700'); 3 | 4 | * { 5 | font-family: 'Poppins'; 6 | font-weight: 400; 7 | } 8 | 9 | :root { 10 | --label-gray: #8b8b8b; 11 | --input-bg-gray: #ebebeb; 12 | --input-text-gray: #686868; 13 | --hover-gray: #cecece; 14 | --input-text-light-gray: #f5f5f5; 15 | --css-gray: #333333; 16 | } 17 | 18 | .App { 19 | display: inline-flex; 20 | flex-direction: column; 21 | background-color: var(--input-text-light-gray); 22 | max-height: 100%; 23 | min-height: 100vh; 24 | min-width: 100vw; 25 | /* max-width: 100%; */ 26 | } 27 | 28 | .container { 29 | display: inline-flex; 30 | padding: 20px 50px 0px 50px; 31 | justify-content: space-around; 32 | width: auto; 33 | } 34 | 35 | .wrapper { 36 | display: flex; 37 | justify-content: center; 38 | } 39 | 40 | input { 41 | background-color: var(--input-bg-gray); 42 | color: var(--input-text-gray); 43 | border: none; 44 | margin: 3px; 45 | border-radius: 5px; 46 | text-align: center; 47 | box-sizing: border-box; 48 | } 49 | 50 | input::-webkit-outer-spin-button, 51 | input::-webkit-inner-spin-button { 52 | -webkit-appearance: none; 53 | margin: 0; 54 | } 55 | 56 | input[type='number'] { 57 | -moz-appearance: textfield; 58 | } 59 | 60 | input:focus, 61 | textarea:focus { 62 | box-shadow: inset 2px 2px 7px var(--input-text-gray); 63 | border-radius: 5px; 64 | outline: none; 65 | } 66 | 67 | h2 { 68 | color: var(--label-gray); 69 | text-align: left; 70 | padding: 0px; 71 | margin: 0px 0px 5px 0px; 72 | font-weight: 600; 73 | } 74 | 75 | p { 76 | padding: 0px; 77 | margin: 0px; 78 | } 79 | 80 | @keyframes fadein { 81 | from { 82 | opacity: 0; 83 | } 84 | to { 85 | opacity: 1; 86 | } 87 | } 88 | 89 | @-webkit-keyframes fadein { 90 | from { 91 | opacity: 0; 92 | } 93 | to { 94 | opacity: 1; 95 | } 96 | } 97 | 98 | .left { 99 | display: inline-flex; 100 | flex-direction: column; 101 | flex-grow: 1; 102 | justify-content: space-around; 103 | } 104 | 105 | .right { 106 | display: flex; 107 | flex-direction: column; 108 | margin-left: 50px; 109 | flex-grow: 1; 110 | } 111 | 112 | .color-picker { 113 | display: flex; 114 | } 115 | 116 | .color-picker-left { 117 | flex-grow: 1; 118 | } 119 | 120 | .color-picker-right { 121 | display: flex; 122 | flex-direction: column; 123 | margin-left: 20px; 124 | flex-grow: 1; 125 | } 126 | 127 | .MuiButtonBase-root p { 128 | font-size: 15px; 129 | } 130 | 131 | .MuiButtonBase-root { 132 | border-radius: 20px !important; 133 | } 134 | 135 | .MuiFormGroup-root { 136 | flex-direction: row !important; 137 | flex-wrap: nowrap !important; 138 | flex-grow: 1; 139 | justify-content: flex-end; 140 | margin-right: -13px; 141 | } 142 | 143 | .MuiTypography-body1 { 144 | font-weight: 600 !important; 145 | color: var(--label-gray) !important; 146 | font-family: 'Poppins' !important; 147 | margin-left: 2px !important; 148 | } 149 | -------------------------------------------------------------------------------- /src/Utils/gradientConstants.js: -------------------------------------------------------------------------------- 1 | import { Gradient } from './Gradient'; 2 | import { Color } from './Color'; 3 | 4 | const KAREN = new Gradient( 5 | [new Color('87cefa', 0, true, 0), new Color('da71d6', 100, false, 1)], 6 | true, 7 | 45, 8 | 'center center', 9 | 'Karen' 10 | ); 11 | 12 | const DORA = new Gradient( 13 | [new Color('fdafaf', 0, true, 0), new Color('a6a8f2', 100, false, 1)], 14 | true, 15 | 45, 16 | 'center center', 17 | 'Dora' 18 | ); 19 | 20 | const STEVEN = new Gradient( 21 | [ 22 | new Color('faee01', 0, true, 0), 23 | new Color('f37721', 50, false, 1), 24 | new Color('ec2e24', 100, false, 2), 25 | ], 26 | true, 27 | 160, 28 | 'center center', 29 | 'Steven' 30 | ); 31 | 32 | const SHARON = new Gradient( 33 | [new Color('de6262', 0, true, 0), new Color('ffb88c', 100, false, 1)], 34 | true, 35 | 30, 36 | 'top right', 37 | 'Sharon' 38 | ); 39 | 40 | const BRANDY = new Gradient( 41 | [ 42 | new Color('47bcd5', 19, true, 0), 43 | new Color('299de2', 37, false, 1), 44 | new Color('2879d9', 52, false, 2), 45 | new Color('2b3bc4', 77, false, 3), 46 | new Color('2e189c', 92, false, 4), 47 | ], 48 | false, 49 | 0, 50 | 'top center', 51 | 'Brandy' 52 | ); 53 | 54 | const CHARLIE = new Gradient( 55 | [new Color('89cff0', 0, true, 0), new Color('77dd77', 100, false, 1)], 56 | false, 57 | 180, 58 | 'center left', 59 | 'Charlie' 60 | ); 61 | 62 | const JUDY = new Gradient( 63 | [ 64 | new Color('ff598d', 0, true, 0), 65 | new Color('d197ff', 33, false, 1), 66 | new Color('ffafe6', 67, false, 2), 67 | new Color('fff7ba', 100, false, 3), 68 | ], 69 | false, 70 | 45, 71 | 'bottom right', 72 | 'Judy' 73 | ); 74 | 75 | const BRYAN = new Gradient( 76 | [ 77 | new Color('020024', 0, true, 0), 78 | new Color('343258', 20, false, 1), 79 | new Color('555570', 43, false, 2), 80 | new Color('336d80', 64, false, 3), 81 | new Color('8ec3cd', 100, false, 4), 82 | ], 83 | true, 84 | 0, 85 | 'bottom right', 86 | 'Bryan' 87 | ); 88 | 89 | const MAX = new Gradient( 90 | [new Color('a1c4fd', 31, true, 0), new Color('c2e9fb', 100, false, 1)], 91 | true, 92 | 78, 93 | 'center center', 94 | 'Max' 95 | ); 96 | 97 | const JEFF = new Gradient( 98 | [ 99 | new Color('9c1eaf', 0, true, 0), 100 | new Color('838e46', 40, false, 1), 101 | new Color('72d4ba', 100, false, 2), 102 | ], 103 | true, 104 | 90, 105 | 'center center', 106 | 'Jeff' 107 | ); 108 | 109 | const REILLY = new Gradient( 110 | [ 111 | new Color('f58025', 0, true, 0), 112 | new Color('ee5a62', 25, false, 1), 113 | new Color('e8589f', 50, false, 2), 114 | new Color('b969eb', 75, false, 3), 115 | new Color('6f7bfc', 100, false, 4), 116 | ], 117 | true, 118 | 35, 119 | 'center center', 120 | 'Reilly' 121 | ); 122 | 123 | const SUGGESTIONS = [ 124 | KAREN, 125 | DORA, 126 | STEVEN, 127 | SHARON, 128 | BRANDY, 129 | CHARLIE, 130 | JUDY, 131 | BRYAN, 132 | MAX, 133 | JEFF, 134 | REILLY, 135 | ]; 136 | 137 | export { SUGGESTIONS }; 138 | -------------------------------------------------------------------------------- /src/Components/Preview/Preview.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../../Styles/Preview/Preview.css'; 3 | import DownloadButton from './DownloadButton'; 4 | import ExpandButton from './ExpandButton'; 5 | import Dimensions from './Dimensions'; 6 | import Degrees from './Degrees'; 7 | import LinearRadial from './LinearRadial'; 8 | import Center from './Center'; 9 | import { generateImage } from '../../Utils/colorUtils'; 10 | 11 | class Preview extends React.Component { 12 | generateImage() { 13 | const { gradient, width, height } = this.props; 14 | return generateImage(gradient, width, height); 15 | } 16 | 17 | isValidImage = () => { 18 | const { width, height } = this.props; 19 | return width && height; 20 | }; 21 | 22 | download = () => { 23 | const url = this.generateImage(); 24 | const link = document.createElement('a'); 25 | link.download = 'gradient'; 26 | link.href = url; 27 | link.click(); 28 | }; 29 | 30 | expand = () => { 31 | const url = this.generateImage(); 32 | const w = window.open('about:blank'); 33 | const image = new Image(); 34 | image.src = url; 35 | 36 | setTimeout(function () { 37 | w.document.write(image.outerHTML); 38 | }, 0); 39 | }; 40 | 41 | render() { 42 | const { 43 | gradient, 44 | width, 45 | height, 46 | handleLinearRadialChange, 47 | handleCenterChange, 48 | handleWidthChange, 49 | handleHeightChange, 50 | handleDegreesChange, 51 | } = this.props; 52 | const { degrees, isLinear, center } = gradient; 53 | const buttonsDisplayStyle = this.isValidImage() 54 | ? { '': '' } 55 | : { display: 'none' }; 56 | const Customize = isLinear ? ( 57 | 61 | ) : ( 62 |
63 | ); 64 | const background = gradient.toBgString(); 65 | const DIV_MAX = 350; 66 | 67 | const longer = Math.max(height, width); 68 | const shorter = Math.min(height, width); 69 | 70 | let scaledHeight, scaledWidth; 71 | 72 | if (longer === height) { 73 | scaledHeight = DIV_MAX; 74 | scaledWidth = (DIV_MAX / longer) * shorter; 75 | buttonsDisplayStyle.bottom = '10px'; 76 | } else { 77 | scaledWidth = DIV_MAX; 78 | scaledHeight = (DIV_MAX / longer) * shorter; 79 | buttonsDisplayStyle.top = '106px'; 80 | } 81 | 82 | return ( 83 |
84 |
85 |

PREVIEW

86 | 90 |
91 |
92 |
100 |
104 | 105 | 106 |
107 |
108 |
109 | 115 | {Customize} 116 |
117 |
118 | ); 119 | } 120 | } 121 | 122 | export default Preview; 123 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Utils/colorUtils.js: -------------------------------------------------------------------------------- 1 | import { 2 | toRadians, 3 | calculateCenterOffset, 4 | calculateRadius, 5 | } from './generalUtils'; 6 | 7 | function hexToRGB(hex, primary) { 8 | let s; 9 | switch (primary) { 10 | case 'r': 11 | s = hex.substring(0, 2); 12 | break; 13 | case 'g': 14 | s = hex.substring(2, 4); 15 | break; 16 | case 'b': 17 | s = hex.substring(4, 6); 18 | break; 19 | default: 20 | console.log('Not valid primary.'); 21 | } 22 | return parseInt(s, 16); 23 | } 24 | 25 | function getLuminanceFromHex(hex) { 26 | const r = hexToRGB(hex, 'r'), 27 | g = hexToRGB(hex, 'g'), 28 | b = hexToRGB(hex, 'b'); 29 | const rgb = [r, g, b]; 30 | 31 | for (let i = 0; i < rgb.length; i++) { 32 | let c = rgb[i]; 33 | c /= 255; 34 | 35 | if (c > 0.03928) { 36 | c = Math.pow((c + 0.055) / 1.055, 2.4); 37 | } else { 38 | c /= 12.92; 39 | } 40 | 41 | rgb[i] = c; 42 | } 43 | 44 | return 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2]; 45 | } 46 | 47 | function isDark(hex) { 48 | let L = getLuminanceFromHex(hex); 49 | return L <= 0.5; 50 | } 51 | 52 | function toBgString(gradient) { 53 | const { stack, isLinear, degrees, center } = gradient; 54 | const validDegrees = degrees || 0; 55 | let centerString = center === 'center center' ? '' : ' at ' + center; 56 | let background = isLinear 57 | ? 'linear-gradient(' + validDegrees + 'deg, ' 58 | : 'radial-gradient(circle' + centerString + ', '; 59 | 60 | let colorString = []; 61 | for (let i = 0; i < stack.length; i++) { 62 | colorString.push('#' + stack[i].hex + ' ' + stack[i].stop + '%'); 63 | } 64 | 65 | background += colorString.join(', ') + ')'; 66 | 67 | return background; 68 | } 69 | 70 | function toStopBarBgString(gradient) { 71 | const { stack } = gradient; 72 | let background = 'linear-gradient(90deg, '; 73 | 74 | let colorString = []; 75 | for (let i = 0; i < stack.length; i++) { 76 | colorString.push('#' + stack[i].hex + ' ' + stack[i].stop + '%'); 77 | } 78 | 79 | background += colorString.join(', ') + ')'; 80 | 81 | return background; 82 | } 83 | 84 | function toCSSBgString(gradient) { 85 | const { stack, isLinear, degrees, center } = gradient; 86 | const validDegrees = degrees || 0; 87 | let centerString = center === 'center center' ? '' : ' at ' + center; 88 | 89 | let background = 'background: '; 90 | background += isLinear 91 | ? 'linear-gradient(\n ' + validDegrees + 'deg,\n ' 92 | : 'radial-gradient(\n circle' + centerString + ',\n '; 93 | 94 | let colorString = []; 95 | for (let i = 0; i < stack.length; i++) { 96 | colorString.push('#' + stack[i].hex + ' ' + stack[i].stop + '%'); 97 | } 98 | 99 | background += colorString.join(',\n ') + '\n);'; 100 | 101 | return background; 102 | } 103 | 104 | function hexToRgb(hex) { 105 | const r = hexToRGB(hex, 'r'); 106 | const g = hexToRGB(hex, 'g'); 107 | const b = hexToRGB(hex, 'b'); 108 | if (!Number.isNaN(r) && !Number.isNaN(g) && !Number.isNaN(b)) { 109 | return { r, g, b }; 110 | } 111 | } 112 | 113 | function getHue(rgb) { 114 | // Make r, g, and b fractions of 1 115 | let { r, g, b } = rgb; 116 | r /= 255; 117 | g /= 255; 118 | b /= 255; 119 | 120 | // Find greatest and smallest channel values 121 | let cmin = Math.min(r, g, b), 122 | cmax = Math.max(r, g, b), 123 | delta = cmax - cmin, 124 | h = 0; 125 | 126 | if (delta === 0) h = 0; 127 | // Red is max 128 | else if (cmax === r) h = ((g - b) / delta) % 6; 129 | // Green is max 130 | else if (cmax === g) h = (b - r) / delta + 2; 131 | // Blue is max 132 | else h = (r - g) / delta + 4; 133 | 134 | h = Math.round(h * 60); 135 | 136 | // Make negative hues positive behind 360° 137 | if (h < 0) h += 360; 138 | 139 | return h; 140 | } 141 | 142 | function getSL(rgb) { 143 | let { r, g, b } = rgb; 144 | r /= 255; 145 | g /= 255; 146 | b /= 255; 147 | 148 | // Find greatest and smallest channel values 149 | let cmin = Math.min(r, g, b), 150 | cmax = Math.max(r, g, b), 151 | delta = cmax - cmin, 152 | s = 0, 153 | l = 0; 154 | 155 | l = (cmax + cmin) / 2; 156 | 157 | // Calculate saturation 158 | s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1)); 159 | 160 | // Multiply l and s by 100 161 | s = +(s * 100).toFixed(1); 162 | l = +(l * 100).toFixed(1); 163 | return { s, l }; 164 | } 165 | 166 | // 167 | function hslToHex(hsl) { 168 | let { h, s, l } = hsl; 169 | 170 | s /= 100; 171 | l /= 100; 172 | 173 | let c = (1 - Math.abs(2 * l - 1)) * s, 174 | x = c * (1 - Math.abs(((h / 60) % 2) - 1)), 175 | m = l - c / 2, 176 | r = 0, 177 | g = 0, 178 | b = 0; 179 | 180 | if (0 <= h && h < 60) { 181 | r = c; 182 | g = x; 183 | b = 0; 184 | } else if (60 <= h && h < 120) { 185 | r = x; 186 | g = c; 187 | b = 0; 188 | } else if (120 <= h && h < 180) { 189 | r = 0; 190 | g = c; 191 | b = x; 192 | } else if (180 <= h && h < 240) { 193 | r = 0; 194 | g = x; 195 | b = c; 196 | } else if (240 <= h && h < 300) { 197 | r = x; 198 | g = 0; 199 | b = c; 200 | } else if (300 <= h && h < 360) { 201 | r = c; 202 | g = 0; 203 | b = x; 204 | } 205 | // Having obtained RGB, convert channels to hex 206 | r = Math.round((r + m) * 255).toString(16); 207 | g = Math.round((g + m) * 255).toString(16); 208 | b = Math.round((b + m) * 255).toString(16); 209 | 210 | // Prepend 0s, if necessary 211 | if (r.length === 1) r = '0' + r; 212 | if (g.length === 1) g = '0' + g; 213 | if (b.length === 1) b = '0' + b; 214 | 215 | return r + g + b; 216 | } 217 | 218 | // hex to hex 219 | function getColorwheel(hex) { 220 | let rgb = hexToRgb(hex); 221 | if (rgb) { 222 | let h = getHue(rgb); 223 | let hsl = { 224 | h, 225 | s: 100, 226 | l: 50, 227 | }; 228 | 229 | return hslToHex(hsl); 230 | } else { 231 | return 'ffffff'; 232 | } 233 | } 234 | 235 | function generateImage(gradient, width, height) { 236 | const { stack, isLinear, degrees, center } = gradient; 237 | const validDegrees = degrees || 0; 238 | 239 | const canvas = document.createElement('CANVAS'); 240 | canvas.width = width; 241 | canvas.height = height; 242 | const ctx = canvas.getContext('2d'); 243 | let g; 244 | 245 | if (isLinear) { 246 | const maxLen = width; 247 | const aspect = height / width; 248 | const angle = Math.PI / 2 + toRadians(validDegrees); 249 | g = ctx.createLinearGradient( 250 | width / 2 + Math.cos(angle) * maxLen * 0.5, 251 | height / 2 + Math.sin(angle) * maxLen * 0.5 * aspect, 252 | width / 2 - Math.cos(angle) * maxLen * 0.5, 253 | height / 2 - Math.sin(angle) * maxLen * 0.5 * aspect 254 | ); 255 | } else { 256 | const radius = calculateRadius(width, height, center); 257 | const start = (stack[0].stop / 100) * radius; 258 | const end = (stack[stack.length - 1].stop / 100) * radius; 259 | const offset = calculateCenterOffset(width, height, center); 260 | 261 | g = ctx.createRadialGradient( 262 | width / 2 + offset.width, 263 | height / 2 + offset.height, 264 | start, 265 | width / 2 + offset.width, 266 | height / 2 + offset.height, 267 | end 268 | ); 269 | } 270 | 271 | stack.forEach((color) => { 272 | const { hex, stop } = color; 273 | g.addColorStop(stop / 100, '#' + hex); 274 | }); 275 | 276 | // Fill with gradient 277 | ctx.fillStyle = g; 278 | // (startx, starty, endx, endy) 279 | ctx.fillRect(0, 0, width, height); 280 | const url = canvas.toDataURL('image/png'); 281 | 282 | return url; 283 | } 284 | 285 | function rgbToHsv(rgb) { 286 | let { r, g, b } = rgb; 287 | r /= 255; 288 | g /= 255; 289 | b /= 255; 290 | 291 | var max = Math.max(r, g, b), 292 | min = Math.min(r, g, b); 293 | var h, 294 | s, 295 | v = max; 296 | 297 | var d = max - min; 298 | s = max === 0 ? 0 : d / max; 299 | 300 | if (max === min) { 301 | h = 0; // achromatic 302 | } else { 303 | switch (max) { 304 | case r: 305 | h = (g - b) / d + (g < b ? 6 : 0); 306 | break; 307 | case g: 308 | h = (b - r) / d + 2; 309 | break; 310 | case b: 311 | h = (r - g) / d + 4; 312 | break; 313 | default: 314 | return; 315 | } 316 | 317 | h /= 6; 318 | } 319 | 320 | s *= 100; 321 | v *= 100; 322 | 323 | return { h, s, v }; 324 | } 325 | 326 | function hsvToHex(hsv) { 327 | let { h, s, v } = hsv; 328 | h /= 360; 329 | s /= 100; 330 | v /= 100; 331 | var r, g, b; 332 | 333 | var i = Math.floor(h * 6); 334 | var f = h * 6 - i; 335 | var p = v * (1 - s); 336 | var q = v * (1 - f * s); 337 | var t = v * (1 - (1 - f) * s); 338 | 339 | switch (i % 6) { 340 | case 0: 341 | r = v; 342 | g = t; 343 | b = p; 344 | break; 345 | case 1: 346 | r = q; 347 | g = v; 348 | b = p; 349 | break; 350 | case 2: 351 | r = p; 352 | g = v; 353 | b = t; 354 | break; 355 | case 3: 356 | r = p; 357 | g = q; 358 | b = v; 359 | break; 360 | case 4: 361 | r = t; 362 | g = p; 363 | b = v; 364 | break; 365 | case 5: 366 | r = v; 367 | g = p; 368 | b = q; 369 | break; 370 | default: 371 | return; 372 | } 373 | 374 | r *= 255; 375 | g *= 255; 376 | b *= 255; 377 | 378 | return ( 379 | (1 << 24) + 380 | (Math.round(r) << 16) + 381 | (Math.round(g) << 8) + 382 | Math.round(b) 383 | ) 384 | .toString(16) 385 | .slice(1); 386 | } 387 | 388 | export { 389 | hexToRGB, 390 | isDark, 391 | toBgString, 392 | toStopBarBgString, 393 | getColorwheel, 394 | toCSSBgString, 395 | generateImage, 396 | getHue, 397 | hexToRgb, 398 | getSL, 399 | hslToHex, 400 | rgbToHsv, 401 | hsvToHex, 402 | }; 403 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './App.css'; 3 | import Header from './Components/Header'; 4 | import HexPicker from './Components/HexPicker/HexPicker'; 5 | import Stack from './Components/Stack/Stack'; 6 | import Suggested from './Components/Suggested/Suggested'; 7 | import StopBar from './Components/StopBar/StopBar'; 8 | import CSS from './Components/CSS/CSS'; 9 | import Preview from './Components/Preview/Preview'; 10 | import { SUGGESTIONS } from './Utils/gradientConstants'; 11 | import { Color } from './Utils/Color'; 12 | import { shuffle, padLeft } from './Utils/generalUtils'; 13 | import { MAX_SIZE, ENTER_KEY } from './Utils/inputConstants'; 14 | import CopyConfirmation from './Components/CSS/CopyConfirmation'; 15 | import Hidden from '@material-ui/core/Hidden'; 16 | 17 | class App extends React.Component { 18 | state = { 19 | gradient: null, 20 | selected: 0, 21 | width: window.screen.width, 22 | height: window.screen.height, 23 | suggestedSelected: '', 24 | suggested: [], 25 | stopValue: null, 26 | draggedColor: null, 27 | cssConfirmationDisplay: false, 28 | }; 29 | 30 | componentWillMount() { 31 | this.shuffleSuggested(); 32 | } 33 | 34 | shuffleSuggested = (e) => { 35 | if (e) { 36 | e.stopPropagation(); 37 | } 38 | 39 | let shuffledSuggested = shuffle(SUGGESTIONS); 40 | let shownSuggested = shuffledSuggested.slice(0, 4); 41 | let first = shownSuggested[0]; 42 | 43 | this.setState({ 44 | gradient: first.clone(), 45 | suggestedSelected: first.name, 46 | suggested: shownSuggested, 47 | stopValue: first.stack[0].stop, 48 | }); 49 | }; 50 | 51 | addColor = () => { 52 | const { gradient, selected } = this.state; 53 | let gradientCopy = gradient.clone(); 54 | let { stack } = gradientCopy; 55 | 56 | if (stack.length < 5) { 57 | // set current selected as false 58 | stack[selected].selected = false; 59 | 60 | // recalculate stops 61 | stack.forEach((c) => { 62 | c.stop = Math.round((c.index / stack.length) * 100); 63 | }); 64 | 65 | const defaultColor = new Color('ffffff', 100, true, stack.length); 66 | stack.push(defaultColor); 67 | 68 | this.setState({ 69 | gradient: gradientCopy, 70 | selected: defaultColor.index, 71 | stopValue: 100, 72 | }); 73 | } 74 | }; 75 | 76 | deleteColor = (e, deletedIndex) => { 77 | const { gradient, selected, suggestedSelected } = this.state; 78 | let gradientCopy = gradient.clone(); 79 | let { stack } = gradientCopy; 80 | 81 | if (suggestedSelected) { 82 | this.unsetSuggested(); 83 | } 84 | 85 | if (stack.length > 2) { 86 | e.stopPropagation(); 87 | 88 | // if deleting currently selected 89 | if (deletedIndex === selected) { 90 | let nextSelected; 91 | 92 | if (deletedIndex === stack.length - 1) { 93 | // if deleting last one set selected as the one before 94 | nextSelected = deletedIndex - 1; 95 | } else { 96 | // else set selected as the one after 97 | nextSelected = deletedIndex + 1; 98 | } 99 | stack[nextSelected].selected = true; 100 | } 101 | 102 | // delete item 103 | stack.splice(deletedIndex, 1); 104 | 105 | // update indices and stopValue 106 | let newSelected, stopValue; 107 | for (let i = 0; i < stack.length; i++) { 108 | const color = stack[i]; 109 | color.index = i; 110 | if (color.selected) { 111 | newSelected = i; 112 | stopValue = color.stop; 113 | } 114 | } 115 | 116 | this.setState({ 117 | gradient: gradientCopy, 118 | selected: newSelected, 119 | stopValue, 120 | }); 121 | } 122 | }; 123 | 124 | changeSelected = (index) => { 125 | const { gradient, selected } = this.state; 126 | const { stack } = gradient; 127 | let stackCopy = [...stack]; 128 | 129 | // set curr selected to false 130 | stackCopy[selected].selected = false; 131 | 132 | // set arg to selected and change this.state.selected 133 | stackCopy[index].selected = true; 134 | 135 | this.setState((prevState) => ({ 136 | gradient: { 137 | ...prevState.gradient, 138 | stack: stackCopy, 139 | }, 140 | selected: index, 141 | stopValue: stackCopy[index].stop, 142 | })); 143 | }; 144 | 145 | setSuggested = (e, suggestedName) => { 146 | e.stopPropagation(); 147 | 148 | const { suggested } = this.state; 149 | 150 | let selectedGradient; 151 | /* iterate through suggested checking if the suggested's 152 | name matches the one selected, if so, set 153 | this.state.gradient as its clone */ 154 | suggested.forEach((gradient) => { 155 | if (gradient.name === suggestedName) { 156 | let clone = gradient.clone(); 157 | this.setState({ gradient: clone }); 158 | selectedGradient = clone; 159 | } 160 | }); 161 | 162 | this.setState({ 163 | suggestedSelected: suggestedName, 164 | selected: 0, 165 | stopValue: selectedGradient.stack[0].stop, 166 | }); 167 | }; 168 | 169 | unsetSuggested = () => { 170 | const { suggestedSelected } = this.state; 171 | if (suggestedSelected) { 172 | this.setState({ suggestedSelected: '' }); 173 | } 174 | }; 175 | 176 | handleLinearRadialChange = () => { 177 | const { gradient } = this.state; 178 | let gradientCopy = gradient.clone(); 179 | const change = !gradient.isLinear; 180 | gradientCopy.isLinear = change; 181 | 182 | this.setState({ 183 | gradient: gradientCopy, 184 | }); 185 | }; 186 | 187 | handleCenterChange = (center) => { 188 | const { gradient } = this.state; 189 | let gradientCopy = gradient.clone(); 190 | gradientCopy.center = center; 191 | 192 | this.setState({ 193 | gradient: gradientCopy, 194 | }); 195 | }; 196 | 197 | handleWidthChange = (e) => { 198 | let { value } = e.target; 199 | 200 | if (value) { 201 | value = Number(value); 202 | } 203 | if (value <= MAX_SIZE) { 204 | this.setState({ 205 | width: value, 206 | }); 207 | } 208 | }; 209 | 210 | handleHeightChange = (e) => { 211 | let { value } = e.target; 212 | 213 | if (value) { 214 | value = Number(value); 215 | } 216 | if (value <= MAX_SIZE) { 217 | this.setState({ 218 | height: value, 219 | }); 220 | } 221 | }; 222 | 223 | handleDegreesChange = (e) => { 224 | let { value } = e.target; 225 | 226 | if (value) { 227 | value = Number(value); 228 | } 229 | 230 | if (value >= 0 && value < 360) { 231 | const { gradient } = this.state; 232 | let gradientCopy = gradient.clone(); 233 | gradientCopy.degrees = value; 234 | 235 | this.setState({ 236 | gradient: gradientCopy, 237 | }); 238 | } 239 | }; 240 | 241 | handleStopChange = (e) => { 242 | let { value } = e.target; 243 | const { gradient, selected } = this.state; 244 | 245 | if (value) { 246 | value = Number(value); 247 | } 248 | 249 | if (value >= 0 && value <= 100) { 250 | let gradientCopy = gradient.clone(); 251 | let { stack } = gradientCopy; 252 | 253 | // update the value 254 | stack[selected].stop = value; 255 | 256 | // sort the stack in increasing order of stops 257 | let newSelected = gradientCopy.sortStack(); 258 | 259 | this.setState({ 260 | gradient: gradientCopy, 261 | selected: newSelected, 262 | }); 263 | } else { 264 | this.setState({ stopValue: gradient.stack[selected].stop }); 265 | } 266 | }; 267 | 268 | handleKeyDown = (e) => { 269 | if (e.keyCode === ENTER_KEY) { 270 | this.handleStopChange(e); 271 | } 272 | }; 273 | 274 | setValue = (stopValue) => { 275 | this.setState({ stopValue }); 276 | }; 277 | 278 | changeValue = (e) => { 279 | const { value } = e.target; 280 | this.setState({ stopValue: value }); 281 | }; 282 | 283 | // hasPound = true for stackItem, false for currentColor 284 | handleHexChange = (e, hasPound) => { 285 | let { value } = e.target; 286 | const { selected, gradient } = this.state; 287 | let gradientCopy = gradient.clone(); 288 | 289 | if (hasPound) { 290 | value = value.substring(1); 291 | } 292 | 293 | gradientCopy.stack[selected].hex = value; 294 | 295 | this.setState({ 296 | gradient: gradientCopy, 297 | }); 298 | }; 299 | 300 | onDragStart = (e, draggedColor) => { 301 | // step drag item to entire stack item, instead of just icon 302 | e.dataTransfer.effectAllowed = 'move'; 303 | e.dataTransfer.setData('text/html', e.target.parentNode); 304 | e.dataTransfer.setDragImage(e.target.parentNode, 20, 20); 305 | 306 | this.setState({ 307 | draggedColor, 308 | }); 309 | }; 310 | 311 | onDragOver = (e, color) => { 312 | // let item drop where its dragged over 313 | e.preventDefault(); 314 | 315 | const { gradient, draggedColor } = this.state; 316 | const gradientCopy = gradient.clone(); 317 | const { stack } = gradientCopy; 318 | 319 | // if dragged over is same as dragged item 320 | if (draggedColor.isEqual(color)) { 321 | return; 322 | } 323 | 324 | // create stack without dragged item 325 | const newStack = stack.filter((color) => !color.isEqual(draggedColor)); 326 | 327 | // insert dragged item 328 | newStack.splice(color.index, 0, draggedColor); 329 | 330 | let stops = gradientCopy.getSortedStops(); 331 | 332 | // set indicies 333 | for (let i = 0; i < newStack.length; i++) { 334 | const currColor = newStack[i]; 335 | currColor.index = i; 336 | currColor.stop = stops[i]; 337 | } 338 | gradientCopy.stack = newStack; 339 | 340 | this.setState({ gradient: gradientCopy }); 341 | }; 342 | 343 | onDragEnd = () => { 344 | const { gradient, draggedColor } = this.state; 345 | const gradientCopy = gradient.clone(); 346 | const { stack } = gradientCopy; 347 | let selected, stopValue; 348 | 349 | // save original stops 350 | let stops = stack.map((color) => color.stop); 351 | 352 | // update selected and stops 353 | for (let i = 0; i < stack.length; i++) { 354 | const color = stack[i]; 355 | 356 | if (!color.isEqual(draggedColor)) { 357 | color.selected = false; 358 | } else { 359 | color.selected = true; 360 | selected = color.index; 361 | stopValue = stops[i]; 362 | } 363 | } 364 | 365 | this.setState({ 366 | draggedColor: null, 367 | gradient: gradientCopy, 368 | selected, 369 | stopValue, 370 | }); 371 | }; 372 | 373 | handleRChange = (e) => { 374 | let { value } = e.target; 375 | 376 | const { selected, gradient } = this.state; 377 | let gradientCopy = gradient.clone(); 378 | const { stack } = gradientCopy; 379 | let currColor = stack[selected]; 380 | let { g, b } = currColor; 381 | 382 | let r; 383 | if (value) { 384 | value = Number(value); 385 | r = value; 386 | } else { 387 | r = 0; 388 | } 389 | 390 | if (value >= 0 && value <= 255) { 391 | r = padLeft(r.toString(16)); 392 | g = padLeft(g.toString(16)); 393 | b = padLeft(b.toString(16)); 394 | const newHex = r + g + b; 395 | currColor.hex = newHex; 396 | currColor.r = value; 397 | } 398 | 399 | this.setState({ 400 | gradient: gradientCopy, 401 | }); 402 | }; 403 | 404 | handleGChange = (e) => { 405 | let { value } = e.target; 406 | 407 | const { selected, gradient } = this.state; 408 | let gradientCopy = gradient.clone(); 409 | const { stack } = gradientCopy; 410 | let currColor = stack[selected]; 411 | let { r, b } = currColor; 412 | 413 | let g; 414 | if (value) { 415 | value = Number(value); 416 | g = value; 417 | } else { 418 | g = 0; 419 | } 420 | 421 | if (value >= 0 && value <= 255) { 422 | r = padLeft(r.toString(16)); 423 | g = padLeft(g.toString(16)); 424 | b = padLeft(b.toString(16)); 425 | const newHex = r + g + b; 426 | currColor.hex = newHex; 427 | currColor.g = value; 428 | } 429 | 430 | this.setState({ 431 | gradient: gradientCopy, 432 | }); 433 | }; 434 | 435 | handleBChange = (e) => { 436 | let { value } = e.target; 437 | 438 | const { selected, gradient } = this.state; 439 | let gradientCopy = gradient.clone(); 440 | const { stack } = gradientCopy; 441 | let currColor = stack[selected]; 442 | let { r, g } = currColor; 443 | 444 | let b; 445 | if (value) { 446 | value = Number(value); 447 | b = value; 448 | } else { 449 | b = 0; 450 | } 451 | 452 | if (value >= 0 && value <= 255) { 453 | r = padLeft(r.toString(16)); 454 | g = padLeft(g.toString(16)); 455 | b = padLeft(b.toString(16)); 456 | const newHex = r + g + b; 457 | currColor.hex = newHex; 458 | currColor.b = value; 459 | } 460 | 461 | this.setState({ 462 | gradient: gradientCopy, 463 | }); 464 | }; 465 | 466 | reverseStack = () => { 467 | const { selected, gradient } = this.state; 468 | let gradientCopy = gradient.clone(); 469 | const { stack } = gradientCopy; 470 | 471 | let newSelected = stack.length - 1 - selected; 472 | let stopValue = gradientCopy.reverse(); 473 | 474 | this.setState({ 475 | gradient: gradientCopy, 476 | selected: newSelected, 477 | stopValue, 478 | }); 479 | }; 480 | 481 | showCSSConfirmation = () => { 482 | this.setState({ cssConfirmationDisplay: true }); 483 | setTimeout( 484 | () => this.setState({ cssConfirmationDisplay: false }), 485 | 2000 486 | ); 487 | }; 488 | 489 | handleStopSlider = (values) => { 490 | const { gradient } = this.state; 491 | let gradientCopy = gradient.clone(); 492 | const { stack } = gradientCopy; 493 | 494 | let stopValue; 495 | for (let i = 0; i < stack.length; i++) { 496 | const color = stack[i]; 497 | color.stop = values[i]; 498 | 499 | if (color.selected) { 500 | stopValue = values[i]; 501 | } 502 | } 503 | 504 | this.setState({ 505 | gradient: gradientCopy, 506 | stopValue, 507 | }); 508 | }; 509 | 510 | // value is the H 511 | handleColorSlider = (value) => { 512 | const { gradient, selected } = this.state; 513 | let gradientCopy = gradient.clone(); 514 | const { stack } = gradientCopy; 515 | const color = stack[selected]; 516 | color.changeHue(value); 517 | 518 | this.setState({ 519 | gradient: gradientCopy, 520 | }); 521 | }; 522 | 523 | updatePosition = ({ x, y }) => { 524 | const { gradient, selected } = this.state; 525 | let gradientCopy = gradient.clone(); 526 | const { stack } = gradientCopy; 527 | const color = stack[selected]; 528 | color.changeColorFromPosition({ x, y }); 529 | 530 | this.setState({ 531 | gradient: gradientCopy, 532 | }); 533 | }; 534 | 535 | render() { 536 | const { 537 | gradient, 538 | suggestedSelected, 539 | suggested, 540 | selected, 541 | height, 542 | width, 543 | stopValue, 544 | cssConfirmationDisplay, 545 | } = this.state; 546 | const { stack } = gradient; 547 | const color = stack[selected]; 548 | 549 | return ( 550 |
551 |
552 | 553 |
554 |
555 |
556 | 560 |
561 |
562 | 583 |
584 |
585 | 604 | 612 |
613 |
614 |
615 |
616 |
617 |
618 | 632 | 638 |
639 |
640 |
641 | 642 |
643 | 644 | 649 | gradient is currently not supported on mobile 650 | devices. Please use the site on a larger screen. 651 | 652 | 653 |
654 | ); 655 | } 656 | } 657 | 658 | export default App; 659 | --------------------------------------------------------------------------------