├── .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
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------