├── .gitignore
├── README.md
├── license.txt
├── media
├── screenshot-0.jpg
└── screenshot-1.jpg
├── package-lock.json
├── package.json
├── public
├── favicon.png
├── index.html
└── manifest.json
├── src
├── App.css
├── App.js
├── App.test.js
├── Components
│ ├── ButtonControls.jsx
│ ├── CodeOutput.jsx
│ ├── ColorCube.jsx
│ ├── ColorPickers.jsx
│ ├── Controls.jsx
│ ├── DegreeInput.jsx
│ ├── GradientSelection.jsx
│ ├── Header.jsx
│ ├── NavButtons.jsx
│ └── SavedGradient.jsx
├── Models
│ ├── Gradient.js
│ └── Store.js
├── Pages
│ ├── Main.jsx
│ └── Settings.jsx
├── Utility
│ ├── debounce.js
│ └── evaluate.js
├── defaultStore.js
├── index.css
├── index.js
├── logo.png
└── registerServiceWorker.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
console.log('dragStart')}
16 | onDrop={() => console.log('drop')}
17 | onDrag={() => console.log('drag')}
18 | onDragEnd={() => console.log('dragEnd')}
19 | style={{
20 | backgroundColor: color,
21 | width: '.75em',
22 | height: '.75em',
23 | padding: '1em',
24 | margin: '.4em',
25 | display: 'inline-block',
26 | background: `linear-gradient(${
27 | gradient.degrees.length === 0 ? '160' : gradient.degrees
28 | }deg,${linearGradient.toString()})`
29 | }}
30 | />
31 | )
32 | }
33 |
34 | export default observer(SavedGradient)
35 |
--------------------------------------------------------------------------------
/src/Models/Gradient.js:
--------------------------------------------------------------------------------
1 | import { types } from 'mobx-state-tree'
2 | import chroma from 'chroma-js'
3 |
4 | const Gradient = types
5 | .model('Gradient', {
6 | colors: types.array(types.string),
7 | grades: 8,
8 | degrees: 160,
9 | mode: types.string
10 | })
11 | .actions(self => ({
12 | addGrade: () => ++self.grades,
13 | removeGrade: () => self.grades > 2 && self.colors.length <= self.grades && --self.grades,
14 | addColor: () => self.colors.push(chroma.random().hex()),
15 | removeColor: () => self.colors.length > 2 && self.colors.pop(),
16 | changeColor: (color, index) => (self.colors[index] = color),
17 | setMode: mode => (self.mode = mode),
18 | changeDegrees: deg => (self.degrees = parseInt(deg, 10))
19 | }))
20 |
21 | export default Gradient
22 |
--------------------------------------------------------------------------------
/src/Models/Store.js:
--------------------------------------------------------------------------------
1 | import { types, applySnapshot } from 'mobx-state-tree'
2 | import Gradient from './Gradient'
3 | import { RouterModel } from 'mst-react-router'
4 | import defaultStore from '../defaultStore'
5 |
6 | export const defaultCode = `const selected = store.selectedGradient
7 | const linearGradient = chroma
8 | .scale(selected.colors)
9 | .mode(selected.mode)
10 | .colors(selected.grades)
11 |
12 | const deg = selected.degrees.length === 0 ? '160' : selected.degrees
13 | const gradient = linearGradient.toString()
14 | const backgroundStyle = \`linear-gradient($\{deg}deg,$\{gradient});\`
15 | return backgroundStyle`
16 |
17 | const Store = types
18 | .model('Store', {
19 | selected: types.optional(types.number, 0),
20 | gradients: types.array(Gradient),
21 | uiHidden: types.optional(types.boolean, true),
22 | uiHiddenLocked: types.optional(types.boolean, false),
23 | router: RouterModel,
24 | outputCode: types.optional(types.string, defaultCode)
25 | })
26 | .views(self => ({
27 | get selectedGradient () {
28 | if (self.selected >= self.gradients.length) {
29 | return self.gradients[self.gradients.length - 1]
30 | } else {
31 | return self.gradients[self.selected]
32 | }
33 | }
34 | }))
35 | .actions(self => ({
36 | selectGradient: index => {
37 | self.selected = index
38 | },
39 | reset: () => {
40 | /* I know this is a bit hacky, but has to do for now */
41 | const current = JSON.parse(JSON.stringify(self))
42 | Object.assign(current, {...defaultStore, uiHidden: true, uiHiddenLocked: true, selected: 0, outputCode: defaultCode})
43 | applySnapshot(self, current)
44 | },
45 | addGradient: () => {
46 | self.gradients.push({
47 | colors: ['#ffffff', '#000000'],
48 | grades: 2,
49 | mode: 'lch'
50 | })
51 | },
52 | deleteSelectedGradient: () => {
53 | self.gradients.length > 1 && self.gradients.splice(self.selected, 1)
54 | },
55 | hideUI: () => {
56 | self.uiHidden = true
57 | },
58 | showUI: () => {
59 | if (self.router.location.pathname === '/') {
60 | self.uiHidden = false
61 | }
62 | },
63 | toggleUILock: () => {
64 | self.uiHiddenLocked = !self.uiHiddenLocked
65 | },
66 | lockUIHidden: () => {
67 | self.uiHiddenLocked = true
68 | },
69 | unlockUIHidden: () => {
70 | self.uiHiddenLocked = false
71 | },
72 | setOutputCode: code => {
73 | self.outputCode = code
74 | }
75 | }))
76 |
77 | export default Store
78 |
--------------------------------------------------------------------------------
/src/Pages/Main.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import chroma from 'chroma-js'
3 | import posed from 'react-pose'
4 | import ColorCube from '../Components/ColorCube'
5 | import ColorPickers from '../Components/ColorPickers'
6 | import GradientSelection from '../Components/GradientSelection'
7 | import CodeOutput from '../Components/CodeOutput'
8 | import { CopyToClipboard } from 'react-copy-to-clipboard'
9 | import { toast } from 'react-toastify'
10 | import { MdContentCopy } from 'react-icons/md'
11 | import { observer } from 'mobx-react'
12 | import evaluate from '../Utility/evaluate';
13 |
14 | export const AnimatedGroup = posed.div({
15 | visible: { delayChildren: 100, staggerChildren: 150 }
16 | })
17 |
18 | export const AnimatedDiv = posed.div({
19 | visible: { opacity: 1, y: 0 },
20 | hidden: { opacity: 0, y: 32 }
21 | })
22 |
23 | class Main extends React.Component {
24 | componentDidMount = () => {
25 | this.props.store.showUI()
26 | this.props.store.unlockUIHidden()
27 | }
28 | render() {
29 | const store = this.props.store
30 | const selected = store.selectedGradient
31 | const linearGradient = chroma
32 | .scale(selected.colors)
33 | .mode(selected.mode)
34 | .colors(selected.grades)
35 |
36 | const backgroundStyle = {
37 | background: `linear-gradient(${
38 | selected.degrees.length === 0 ? '160' : selected.degrees
39 | }deg,${linearGradient.toString()})`,
40 | width: '100%'
41 | }
42 |
43 | const visibility =
44 | store.uiHidden || store.uiHiddenLocked ? 'hidden' : 'visible'
45 |
46 | return (
47 |
48 |
49 |
54 | {linearGradient.map((c, idx) => )}
55 |
56 |
57 |
58 |
59 |
60 |
61 |
65 | toast('Copied to clipboard', { position: 'bottom-right' })
66 | }
67 | >
68 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | )
79 | }
80 | }
81 |
82 | export default observer(Main)
83 |
--------------------------------------------------------------------------------
/src/Pages/Settings.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import chroma from 'chroma-js'
3 | import { ObjectInspector } from 'react-inspector'
4 | import AceEditor from 'react-ace'
5 | import { observer } from 'mobx-react'
6 | import { toast } from 'react-toastify'
7 | import 'brace/mode/javascript'
8 | import 'brace/theme/pastel_on_dark'
9 |
10 | class Settings extends React.Component {
11 | componentDidMount = () => {
12 | this.props.store.hideUI()
13 | this.props.store.lockUIHidden()
14 | }
15 |
16 | render() {
17 | const store = this.props.store
18 | return (
19 |
20 |
Settings
21 |
Output
22 |
23 |
function output(chroma, store){' {'}
24 |
store.setOutputCode(code)}
33 | fontSize={14}
34 | showPrintMargin={false}
35 | showGutter={false}
36 | highlightActiveLine
37 | value={store.outputCode}
38 | setOptions={{
39 | enableBasicAutocompletion: true,
40 | enableLiveAutocompletion: true,
41 | enableSnippets: false,
42 | showLineNumbers: true,
43 | tabSize: 2
44 | }}
45 | />
46 | {'}'}
47 |
48 |
49 |
50 |
Example
51 |
52 | {(() => {
53 | let codeBeforeEval = '((chroma, store) => {'
54 | codeBeforeEval += store.outputCode + '})'
55 | try {
56 | const builtFunction = eval(codeBeforeEval)
57 | const result = builtFunction(chroma, store)
58 | return result
59 | } catch (err) {
60 | return err.toString()
61 | }
62 | })()}
63 |
64 |
65 |
66 |
67 |
68 | Application State{' '}
69 |
77 |
78 |
79 |
80 |
81 | )
82 | }
83 | }
84 |
85 | export default observer(Settings)
86 |
--------------------------------------------------------------------------------
/src/Utility/debounce.js:
--------------------------------------------------------------------------------
1 | function debounce (func, wait, immediate) {
2 | let timeout
3 | return function () {
4 | const context = this
5 | const args = arguments
6 |
7 | const later = function () {
8 | timeout = null
9 | if (!immediate) {
10 | func.apply(context, args)
11 | }
12 | }
13 |
14 | const callNow = immediate && !timeout
15 | clearTimeout(timeout)
16 | timeout = setTimeout(later, wait)
17 | if (callNow) {
18 | func.apply(context, args)
19 | }
20 | }
21 | }
22 |
23 | export default debounce
24 |
--------------------------------------------------------------------------------
/src/Utility/evaluate.js:
--------------------------------------------------------------------------------
1 | const evaluate = (store, chroma) => {
2 | try {
3 | const result = eval('((chroma, store) => {' + store.outputCode + '})')(
4 | chroma,
5 | store
6 | ).toString()
7 | return result
8 | } catch (err) {
9 | return err.toString()
10 | }
11 | }
12 |
13 | export default evaluate
14 |
--------------------------------------------------------------------------------
/src/defaultStore.js:
--------------------------------------------------------------------------------
1 | const defaultStore = {
2 | gradients: [
3 | {
4 | colors: ['#00FF87', '#2C69BD', '#3a0e37'],
5 | degrees: 160,
6 | grades: 8,
7 | mode: 'lch'
8 | },
9 | {
10 | colors: ['#FFEBD6', '#370B7F'],
11 | degrees: 160,
12 | grades: 8,
13 | mode: 'lch'
14 | },
15 | {
16 | colors: ['#650909', '#00AFFF'],
17 | degrees: 160,
18 | grades: 8,
19 | mode: 'lch'
20 | },
21 | {
22 | colors: ['#FFEB00', '#0C3776'],
23 | degrees: 160,
24 | grades: 8,
25 | mode: 'lch'
26 | },
27 | {
28 | colors: ['#00F6FF', '#FF001B'],
29 | degrees: 160,
30 | grades: 8,
31 | mode: 'lab'
32 | },
33 | {
34 | colors: ['#262E4C', '#4D754B', '#FFCD00'],
35 | degrees: 160,
36 | grades: 8,
37 | mode: 'lab'
38 | },
39 | {
40 | colors: ['#760F0F', '#DC2DE2', '#4FB7B4', '#00FFEF'],
41 | degrees: 160,
42 | grades: 8,
43 | mode: 'lab'
44 | }
45 | ]
46 | }
47 |
48 | export default defaultStore
49 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/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 registerServiceWorker from './registerServiceWorker'
6 |
7 | ReactDOM.render(
, document.getElementById('root'))
8 |
9 | registerServiceWorker()
10 |
--------------------------------------------------------------------------------
/src/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/enlyth/GradientLab/a35a0f6af3100e9612d188c898de961e8183ffcf/src/logo.png
--------------------------------------------------------------------------------
/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (isLocalhost) {
36 | // This is running on localhost. Lets check if a service worker still exists or not.
37 | checkValidServiceWorker(swUrl);
38 |
39 | // Add some additional logging to localhost, pointing developers to the
40 | // service worker/PWA documentation.
41 | navigator.serviceWorker.ready.then(() => {
42 | console.log(
43 | 'This web app is being served cache-first by a service ' +
44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ'
45 | );
46 | });
47 | } else {
48 | // Is not local host. Just register service worker
49 | registerValidSW(swUrl);
50 | }
51 | });
52 | }
53 | }
54 |
55 | function registerValidSW(swUrl) {
56 | navigator.serviceWorker
57 | .register(swUrl)
58 | .then(registration => {
59 | registration.onupdatefound = () => {
60 | const installingWorker = registration.installing;
61 | installingWorker.onstatechange = () => {
62 | if (installingWorker.state === 'installed') {
63 | if (navigator.serviceWorker.controller) {
64 | // At this point, the old content will have been purged and
65 | // the fresh content will have been added to the cache.
66 | // It's the perfect time to display a "New content is
67 | // available; please refresh." message in your web app.
68 | console.log('New content is available; please refresh.');
69 | } else {
70 | // At this point, everything has been precached.
71 | // It's the perfect time to display a
72 | // "Content is cached for offline use." message.
73 | console.log('Content is cached for offline use.');
74 | }
75 | }
76 | };
77 | };
78 | })
79 | .catch(error => {
80 | console.error('Error during service worker registration:', error);
81 | });
82 | }
83 |
84 | function checkValidServiceWorker(swUrl) {
85 | // Check if the service worker can be found. If it can't reload the page.
86 | fetch(swUrl)
87 | .then(response => {
88 | // Ensure service worker exists, and that we really are getting a JS file.
89 | if (
90 | response.status === 404 ||
91 | response.headers.get('content-type').indexOf('javascript') === -1
92 | ) {
93 | // No service worker found. Probably a different app. Reload the page.
94 | navigator.serviceWorker.ready.then(registration => {
95 | registration.unregister().then(() => {
96 | window.location.reload();
97 | });
98 | });
99 | } else {
100 | // Service worker found. Proceed as normal.
101 | registerValidSW(swUrl);
102 | }
103 | })
104 | .catch(() => {
105 | console.log(
106 | 'No internet connection found. App is running in offline mode.'
107 | );
108 | });
109 | }
110 |
111 | export function unregister() {
112 | if ('serviceWorker' in navigator) {
113 | navigator.serviceWorker.ready.then(registration => {
114 | registration.unregister();
115 | });
116 | }
117 | }
118 |
--------------------------------------------------------------------------------