diff --git a/README.md b/README.md index e69de29..0527753 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,65 @@ +
+ +

+ +

+ +## About The Project + +Pixel Artify is a web-based tool for transforming images into pixel art. Try it out at [https://pixelartify.com](https://pixelartify.com)! + +![Demo Screenshot](client/src/assets/images/demo-screenshot.png) + + +## Getting Started + +### Prerequisites + +This project requires Node.js which can be installed [here](https://nodejs.org/en/). + +### Development Setup + +1. Clone the repo: + +```sh + +git clone https://github.com/shannonlui/pixel-artify.git + +``` + +2. Install the required dependencies in the client directory: + +```sh + +cd client +npm install + +``` + +4. Run the client app in development mode: + +```js + +npm start + +``` + + +## Features +- [x] Pixelate uploaded images with customizable pixel size +- [x] Adjust contrast, brightness, saturation, and color palette +- [x] Paint, erase and color selection on canvas +- [ ] Resizable canvas and image export +- [ ] Zooming and panning the canvas +- [ ] Undo or redo changes + + +## Acknowledgments +This project was built using [React.js](https://reactjs.org/), Redux, and the following libraries: + +* [Color Thief](https://github.com/lokesh/color-thief) +* [FileSaver.js](https://github.com/eligrey/FileSaver.js/) +* [Pica](https://github.com/nodeca/pica) +* [React Color](https://github.com/casesandberg/react-color) +* [React Icons](https://github.com/react-icons/react-icons) + diff --git a/client/package.json b/client/package.json index 277809c..92391fa 100644 --- a/client/package.json +++ b/client/package.json @@ -3,9 +3,12 @@ "version": "0.1.0", "private": true, "dependencies": { - "file-saver": "^2.0.2", + "file-saver": "^2.0.5", + "pica": "^9.0.1", "react": "^16.8.6", + "react-color": "^2.19.3", "react-dom": "^16.8.6", + "react-icons": "^4.3.1", "react-redux": "^7.0.3", "react-router-dom": "^5.0.0", "react-scripts": "3.0.0", diff --git a/client/src/assets/images/demo-screenshot.png b/client/src/assets/images/demo-screenshot.png new file mode 100644 index 0000000..c537569 Binary files /dev/null and b/client/src/assets/images/demo-screenshot.png differ diff --git a/client/src/assets/images/logo-left.png b/client/src/assets/images/logo-left.png new file mode 100644 index 0000000..90c3d81 Binary files /dev/null and b/client/src/assets/images/logo-left.png differ diff --git a/client/src/assets/images/logo-right.png b/client/src/assets/images/logo-right.png new file mode 100644 index 0000000..2fb4958 Binary files /dev/null and b/client/src/assets/images/logo-right.png differ diff --git a/client/src/assets/images/logo.png b/client/src/assets/images/logo.png new file mode 100644 index 0000000..40e50e4 Binary files /dev/null and b/client/src/assets/images/logo.png differ diff --git a/client/src/components/Button/Button.js b/client/src/components/Button/Button.js new file mode 100644 index 0000000..325b1be --- /dev/null +++ b/client/src/components/Button/Button.js @@ -0,0 +1,17 @@ +import React from 'react'; + +import styles from './Button.module.css'; + +function Button(props) { + const { children, className, primary, secondary, small, ...otherProps } = props; + const classNames = [styles.button, className, primary && styles.primary, + secondary && styles.secondary, small && styles.small]; + + return ( + + ); +} + +export default Button; \ No newline at end of file diff --git a/client/src/components/Button/Button.module.css b/client/src/components/Button/Button.module.css new file mode 100644 index 0000000..ad3ba1e --- /dev/null +++ b/client/src/components/Button/Button.module.css @@ -0,0 +1,31 @@ +.button { + background-color: #ef6461; + border: none; + border-radius: 5px; + padding: 5px 10px; + color: white; + font-weight: bold; + text-transform: uppercase; + cursor: pointer; +} + +.primary { + background-color: #1c9197; +} + +.secondary { + background-color: grey; +} + +.small { + font-size: 0.8em; +} + +.label { + display: flex; + align-items: center; +} + +.label svg { + margin-right: 5px; +} \ No newline at end of file diff --git a/client/src/components/FormControl/FormControl.module.css b/client/src/components/FormControl/FormControl.module.css index 862ed7a..1118cd2 100644 --- a/client/src/components/FormControl/FormControl.module.css +++ b/client/src/components/FormControl/FormControl.module.css @@ -1,12 +1,12 @@ .control { color: white; - margin-bottom: 64px; + margin-bottom: 40px; } .control .label { display: flex; align-items: center; justify-content: space-between; - margin-bottom: 10px; + margin-bottom: 2px; font-weight: bold; } \ No newline at end of file diff --git a/client/src/components/Loading/Loading.module.css b/client/src/components/Loading/Loading.module.css index 10f5545..d31a4aa 100644 --- a/client/src/components/Loading/Loading.module.css +++ b/client/src/components/Loading/Loading.module.css @@ -1,5 +1,13 @@ .loading { text-align: center; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + transform: -webkit-translate(-50%, -50%); + transform: -moz-translate(-50%, -50%); + transform: -ms-translate(-50%, -50%); + z-index: 9999; } .text { diff --git a/client/src/components/Modal/Modal.js b/client/src/components/Modal/Modal.js new file mode 100644 index 0000000..2a19267 --- /dev/null +++ b/client/src/components/Modal/Modal.js @@ -0,0 +1,30 @@ +import React from 'react'; +import { FiX } from 'react-icons/fi'; + +import styles from './Modal.module.css'; +import Button from '../Button/Button'; + +function Modal(props) { + return ( +
+
e.stopPropagation()}> + + { + props.header &&
+ {props.header} +
+ } + { props.header &&
} +
+ {props.children} +
+
+ + +
+
+
+ ); +} + +export default Modal; \ No newline at end of file diff --git a/client/src/components/Modal/Modal.module.css b/client/src/components/Modal/Modal.module.css new file mode 100644 index 0000000..72cd18e --- /dev/null +++ b/client/src/components/Modal/Modal.module.css @@ -0,0 +1,66 @@ +.background { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.6); + z-index: 20; +} + +.modal { + position: relative; + background: #444; + min-width: 250px; + width: 40%; + top: 50%; + left: 50%; + transform: translate(-50%,-50%); + z-index: 100; + border-radius: 5px; + color: white; + overflow-y: scroll; +} + +.header { + font-size: 1.2em; + font-weight: bold; + padding: 24px; + margin-right: 24px; + color: #ddd; +} + +.closeBtn { + position: absolute; + top: 24px; + right: 24px; + font-size: 1.4em; + cursor: pointer; + color: white; + background: none; + border: none; +} + +.body { + padding: 24px; + margin-bottom: 60px; + min-height: 100px; + height: 50%; + overflow: auto; +} + +.footer { + position: absolute; + bottom: 24px; + right: 24px; +} + +.footer button { + margin-left: 12px; +} + +@media only screen and (max-width: 400px) { + .header { + font-size: 1em; + } +} \ No newline at end of file diff --git a/client/src/components/SliderControl/SliderControl.module.css b/client/src/components/SliderControl/SliderControl.module.css index 3e61172..494718c 100644 --- a/client/src/components/SliderControl/SliderControl.module.css +++ b/client/src/components/SliderControl/SliderControl.module.css @@ -6,6 +6,7 @@ height: 8px; outline: none; border: none; + background: #ddd; } .slider::-webkit-slider-thumb { diff --git a/client/src/components/Toolbar/Toolbar.js b/client/src/components/Toolbar/Toolbar.js index 93fc866..7e24ca1 100644 --- a/client/src/components/Toolbar/Toolbar.js +++ b/client/src/components/Toolbar/Toolbar.js @@ -9,7 +9,11 @@ const classes = { const toolbar = (props) => (
- pixel artify + + + pixel artify + +
); diff --git a/client/src/components/Toolbar/Toolbar.module.css b/client/src/components/Toolbar/Toolbar.module.css index 4ba366e..ece8cd2 100644 --- a/client/src/components/Toolbar/Toolbar.module.css +++ b/client/src/components/Toolbar/Toolbar.module.css @@ -14,7 +14,7 @@ .logo { font-family: 'VT323', serif; - font-size: 46px; + font-size: 38px; margin-top: -5px; color:white; text-decoration: none; @@ -30,8 +30,19 @@ color: #ef6461; } +.logoLeft { + width: 20px; + margin-right: 10px; +} + +.logoRight { + width: 20px; + margin-left: 8px; +} + @media only screen and (max-width: 768px) { .toolbar { justify-content: center; + padding: 0; } } \ No newline at end of file diff --git a/client/src/constants/constants.js b/client/src/constants/constants.js new file mode 100644 index 0000000..a0193b3 --- /dev/null +++ b/client/src/constants/constants.js @@ -0,0 +1,5 @@ +export const TOOL_TYPES = { + PAINT: "PAINT", + COLOR_PICK: "COLOR_PICK", + ERASE: "ERASE" +} \ No newline at end of file diff --git a/client/src/pages/ImageEditor/Canvas/Canvas.js b/client/src/pages/ImageEditor/Canvas/Canvas.js index b5faef7..7864f4d 100644 --- a/client/src/pages/ImageEditor/Canvas/Canvas.js +++ b/client/src/pages/ImageEditor/Canvas/Canvas.js @@ -1,31 +1,190 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { saveAs } from 'file-saver'; +import Pica from 'pica'; import styles from './Canvas.module.css'; import * as actions from '../../../store/actions'; -import { getColorDifference } from '../../../utils/colorDifference'; +import { getClosestColor, convertRgbToHex, adjustImageColors } from '../../../utils/colorDifference'; +import { TOOL_TYPES } from '../../../constants/constants'; +import { isDevEnv } from '../../../utils/utility'; class Canvas extends Component { constructor(props) { super(props); + this.state = { + img: null, + isPainting: false + }; + this.grid = React.createRef(); this.canvas = React.createRef(); + this.mouseLayer = React.createRef(); } componentDidMount() { this.props.setExportImage(this.saveCanvas); + this.props.setResetCanvas(this.resetCanvas); - let img = this.props.img; - img.onload = () => { + this.props.origImg.onload = () => { + this.resizeImage(this.props.origImg); + this.drawGrid(); + } + + // Add listeners for painting on the canvas using a mouse (from user input) + this.mouseLayer.current.addEventListener('mousedown', this.handeStartPainting); + this.mouseLayer.current.addEventListener('mouseup', this.handleStopPainting); + this.mouseLayer.current.addEventListener('mousemove', this.handleContinuePainting); + this.mouseLayer.current.addEventListener("mouseout", this.handleMouseOut); + + // Trigger a confirmation dialog to ask user if they really want to leave the page + if (!isDevEnv()) + window.onbeforeunload = () => true; + } + + componentDidUpdate(prevProps) { + if (this.props.pixelSize !== prevProps.pixelSize || this.props.contrast !== prevProps.contrast + || this.props.brightness !== prevProps.brightness || this.props.saturation !== prevProps.saturation + || this.props.colorCount !== prevProps.colorCount) + { + this.resetCanvas(); + } + } + + componentWillUnmount() { + // Remove the trigger for confirming if user really want to leave the page + window.onbeforeunload = null; + } + + resetCanvas = () => { + // Redraw the pixelated image. This will clear any painting done by the user! + this.pixelate(this.state.img, this.props.pixelSize); + this.applyColorAdjustments(); + } + + handeStartPainting = (event) => { + if (!this.props.isPaintEnabled) + return; + if (this.props.toolType === TOOL_TYPES.COLOR_PICK) { + // Get the color from the pixel that was touched + const ctx = this.canvas.current.getContext('2d'); + const coord = this.getPosition(event); + const imgData = ctx.getImageData(coord.x, coord.y, 1, 1).data; + const hex = convertRgbToHex(imgData[0], imgData[1], imgData[2]); + this.props.onChangeColor(hex); + } else { + // Start painting + this.setState({isPainting: true}); + this.paintOnCanvas(event); + } + } + + handleStopPainting = (event) => { + this.setState({isPainting: false}); + } + + handleContinuePainting = (event) => { + if (!this.props.isPaintEnabled) + return; + if (this.state.isPainting) { + this.paintOnCanvas(event); + } + if (this.props.toolType !== TOOL_TYPES.COLOR_PICK) { + // Clear the canvas for the mouse layer + const mouseCanvas = this.mouseLayer.current; + const mouseCtx = mouseCanvas.getContext('2d'); + mouseCtx.clearRect(0, 0, mouseCanvas.width, mouseCanvas.height); + // Draw a transparent rectangle to preview where the mouse will paint + this.drawSquare(event, mouseCtx, "rgba(0, 0, 0, 0.3)"); + } + } + + handleMouseOut = (event) => { + // TODO: add helper function for clearing canvas + const mouseCanvas = this.mouseLayer.current; + const mouseCtx = mouseCanvas.getContext('2d'); + mouseCtx.clearRect(0, 0, mouseCanvas.width, mouseCanvas.height); + } + + getPosition = (event) => { + var rect = this.canvas.current.getBoundingClientRect(); + return { + x: event.clientX - rect.left, + y: event.clientY - rect.top + }; + } + + paintOnCanvas = (event) => { + const ctx = this.canvas.current.getContext('2d'); + const color = this.props.toolType === TOOL_TYPES.PAINT ? this.props.paintColor : null; + this.drawSquare(event, ctx, color); + } + + drawSquare = (event, ctx, color) => { + let { pixelSize, brushSize } = this.props; + const length = brushSize * pixelSize; + const coord = this.getPosition(event); + const offset = Math.floor(brushSize / 2) * pixelSize; + const x = Math.floor(coord.x / pixelSize) * pixelSize - offset; + const y = Math.floor(coord.y / pixelSize) * pixelSize - offset; + if (color == null) { + ctx.clearRect(x, y, length, length); + } else { + ctx.fillStyle = color; + ctx.fillRect(x, y, length, length); + } + } + + resizeImage = (img) => { + const canvas = this.canvas.current; + // TODO: do not hardcode these numbers here + const isMobile = window.innerWidth <= 700; + const maxWidth = isMobile ? (window.innerWidth - 20) : (window.innerWidth - 340); + const maxHeight = isMobile ? 280 : (window.innerHeight - 20); + if (img.width > maxWidth || img.height > maxHeight) { + const ratio = Math.max(img.width / maxWidth, img.height / maxHeight); + this.setCanvasSize(Math.round(img.width / ratio), Math.round(img.height / ratio)); + Pica().resize(img, canvas) + .then(result => this.setResizedImage(result.toDataURL())); + } else { + this.setCanvasSize(img.width, img.height); + this.setResizedImage(img.src); + } + } + + setCanvasSize = (width, height) => { + const canvases = [this.canvas.current, this.mouseLayer.current, this.grid.current]; + for (var c of canvases) { + c.width = width; + c.height = height; + } + } + + setResizedImage = (imgSrc) => { + const resizedImg = new Image(); + resizedImg.src = imgSrc; + resizedImg.onload = () => { + this.setState({img: resizedImg}); + this.pixelate(resizedImg, this.props.pixelSize); this.props.onLoadImageSuccess(); - this.pixelate(img, +this.props.pixelSize); } } - componentDidUpdate() { - this.pixelate(this.props.img, +this.props.pixelSize); - this.adjustColors(); + drawGrid = () => { + const { pixelSize } = this.props; + const canvas = this.grid.current; + const ctx = canvas.getContext('2d'); + const cols = Math.floor(canvas.width, pixelSize); + const rows = Math.floor(canvas.height, pixelSize); + // Clear the canvas + ctx.clearRect(0, 0, canvas.width, canvas.height); + // Draw a checkerboard pattern to be the background of the main canvas + for (let y = 0; y < rows; y++) { + for (let x = 0; x < cols; x++) { + ctx.fillStyle = ["#888", "#666"][(x + y) % 2]; // Alternate the fill color + ctx.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize); + } + } } pixelate = (img, pixelSize) => { @@ -54,7 +213,7 @@ class Canvas extends Component { let alpha = imgData[pos + 3]; if (alpha > 0) alpha = 255; if (this.props.palette && this.props.palette.length > 0) { - [red, green, blue] = this.getClosestColor(this.props.palette, [red, green, blue]); + [red, green, blue] = getClosestColor(this.props.palette, [red, green, blue]); } ctx.fillStyle = `rgba(${red}, ${green}, ${blue}, ${alpha})`; ctx.fillRect(x, y, pixelSize, pixelSize); @@ -63,78 +222,49 @@ class Canvas extends Component { } saveCanvas = () => { - var ua = window.navigator.userAgent; - var iOS = !!ua.match(/iPad/i) || !!ua.match(/iPhone/i); - var webkit = !!ua.match(/WebKit/i); - var iOSSafari = iOS && webkit && !ua.match(/CriOS/i); const canvasURL = this.canvas.current.toDataURL('image/png'); - if (iOSSafari) { - window.open(canvasURL, '_blank'); - } else { - saveAs(canvasURL, 'pixelartify.png'); - } + saveAs(canvasURL, 'pixelartify.png'); } - getClosestColor(colors, target) { - let minDiff = Number.MAX_SAFE_INTEGER; - let closest = null; - const length = colors.length; - for (let i = 0; i < length; i++) { - let diff = getColorDifference(target, colors[i]); - if (diff <= minDiff) { - minDiff = diff; - closest = colors[i]; - } - } - return closest; - } - - adjustColors = () => { + applyColorAdjustments = () => { const canvas = this.canvas.current; const ctx = canvas.getContext('2d'); const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); - const d = imgData.data; - - const saturation = (this.props.saturation / 100) + 1; - const brightness = this.props.brightness * 0.75; - const contrast = (this.props.contrast / 100) + 1; - const con = 128 * (1 - contrast); - - // Adjust the saturation, brightness, and contrast of each pixel - for (let i = 0; i < d.length; i += 4) { - const gray = d[i] * 0.3086 + d[i + 1] * 0.6094 + d[i + 2] * 0.0820; - const sat = gray * (1 - saturation); - - d[i] = (d[i] * saturation + sat + brightness) * contrast + con; - d[i + 1] = (d[i + 1] * saturation + sat + brightness) * contrast + con; - d[i + 2] = (d[i + 2] * saturation + sat + brightness) * contrast + con; - } - + adjustImageColors(imgData.data, this.props.saturation, this.props.brightness, this.props.contrast); ctx.putImageData(imgData, 0, 0); } render() { return( - + + + + + ); } } const mapStateToProps = state => { return { - img: state.image, + origImg: state.image, + isPaintEnabled: state.isPaintEnabled, + paintColor: state.paintColor, pixelSize: state.pixelSize, + brushSize: state.brushSize, contrast: state.contrast, brightness: state.brightness, saturation: state.saturation, colorCount: state.colorCount, - palette: state.colorPalette + palette: state.colorPalette, + toolType: state.toolType, }; }; const mapDispatchToProps = dispatch => { return { onLoadImageSuccess: () => dispatch(actions.loadImageSuccess()), + onChangeColor: (color) => dispatch(actions.updatePaintColor(color)), }; }; diff --git a/client/src/pages/ImageEditor/Canvas/Canvas.module.css b/client/src/pages/ImageEditor/Canvas/Canvas.module.css index 101e564..5161b70 100644 --- a/client/src/pages/ImageEditor/Canvas/Canvas.module.css +++ b/client/src/pages/ImageEditor/Canvas/Canvas.module.css @@ -1,4 +1,9 @@ .canvas { - max-width: 85%; - max-height: 85%; + position: absolute; +} + +@media only screen and (max-width: 700px) { + .hideOnSmallScreen { + opacity: 0; + } } \ No newline at end of file diff --git a/client/src/pages/ImageEditor/Controls/Controls.js b/client/src/pages/ImageEditor/Controls/Controls.js index 84369c7..d717bbf 100644 --- a/client/src/pages/ImageEditor/Controls/Controls.js +++ b/client/src/pages/ImageEditor/Controls/Controls.js @@ -30,6 +30,7 @@ const Controls = (props) => { props.onChangeColorCount(e.target.value, props.img)} + className={styles.colorCountInput} placeholder="Max number of colors" /> {(props.colorCount > 1 && props.colorCount < 51) ? null :

Value must be between 2 and 50

} @@ -49,7 +50,7 @@ const Controls = (props) => { @@ -83,7 +84,7 @@ const Controls = (props) => { } + onClick={props.onEnablePaint}>Continue ); }; @@ -107,6 +108,7 @@ const mapDispatchToProps = dispatch => { onChangeBrightness: (brightness) => dispatch(actions.updateBrightness(brightness)), onChangeSaturation: (saturation) => dispatch(actions.updateSaturation(saturation)), onChangeColorCount: (colorCount, image) => dispatch(actions.updateColorCount(colorCount, image)), + onEnablePaint: () => dispatch(actions.enablePaint()) }; }; diff --git a/client/src/pages/ImageEditor/Controls/Controls.module.css b/client/src/pages/ImageEditor/Controls/Controls.module.css index 7c67d61..b4a9c82 100644 --- a/client/src/pages/ImageEditor/Controls/Controls.module.css +++ b/client/src/pages/ImageEditor/Controls/Controls.module.css @@ -1,6 +1,6 @@ .controls { - padding-left: 40px; - padding-right: 40px; + padding-left: 30px; + padding-right: 30px; } .controls img { @@ -38,6 +38,10 @@ padding-top: 4px; } +.colorCountInput { + margin-top: 10px; +} + @media only screen and (max-width: 768px) { .controls { padding-left: 30px; diff --git a/client/src/pages/ImageEditor/FileMenu/FileMenu.js b/client/src/pages/ImageEditor/FileMenu/FileMenu.js new file mode 100644 index 0000000..755a8d2 --- /dev/null +++ b/client/src/pages/ImageEditor/FileMenu/FileMenu.js @@ -0,0 +1,75 @@ +import React, { useState } from 'react'; +import { connect } from 'react-redux'; +import { FiHome, FiRefreshCcw, FiSave } from 'react-icons/fi'; + +import * as actions from '../../../store/actions'; +import styles from './FileMenu.module.css'; +import Button from '../../../components/Button/Button'; +import Modal from '../../../components/Modal/Modal'; + +function FileMenu(props) { + const [showResetModal, setShowResetModal] = useState(false); + const [showHomeModal, setShowHomeModal] = useState(false); + + const handleOpenResetModal = () => setShowResetModal(true); + const handleCloseResetModal = () => setShowResetModal(false); + const handleOpenHomeModal = () => setShowHomeModal(true); + const handleCloseHomeModal = () => setShowHomeModal(false); + + const handleReset = () => { + // Reset by re-uploading the image + props.onLoadImage(props.img); + props.onLoadImageSuccess(); + props.resetCanvas(); + setShowResetModal(false); + }; + + const renderResetModal = () => { + return ( + + You will lose all the changes you made. + + ) + }; + + const handleReturnHome = () => props.history.push("/"); + + const renderHomeModal = () => { + return ( + + You will lose all the changes you made. + + ) + }; + + return ( +
+ + + + {showResetModal && renderResetModal()} + {showHomeModal && renderHomeModal()} +
+ ); +} + +const mapStateToProps = state => { + return { + img: state.image + }; +}; + +const mapDispatchToProps = dispatch => { + return { + onLoadImage: (img) => dispatch(actions.loadImage(img)), + onLoadImageSuccess: () => dispatch(actions.loadImageSuccess()) + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(FileMenu); \ No newline at end of file diff --git a/client/src/pages/ImageEditor/FileMenu/FileMenu.module.css b/client/src/pages/ImageEditor/FileMenu/FileMenu.module.css new file mode 100644 index 0000000..5c5968e --- /dev/null +++ b/client/src/pages/ImageEditor/FileMenu/FileMenu.module.css @@ -0,0 +1,5 @@ +.container { + display: flex; + justify-content: space-between; + padding: 0 30px 30px 30px; +} \ No newline at end of file diff --git a/client/src/pages/ImageEditor/ImageEditor.js b/client/src/pages/ImageEditor/ImageEditor.js index 44e9560..34bbea0 100644 --- a/client/src/pages/ImageEditor/ImageEditor.js +++ b/client/src/pages/ImageEditor/ImageEditor.js @@ -1,25 +1,49 @@ import React, { Component } from 'react'; -import { Link } from 'react-router-dom'; import { connect } from 'react-redux'; import styles from './ImageEditor.module.css'; - +import FileMenu from './FileMenu/FileMenu'; import Controls from './Controls/Controls'; import Canvas from './Canvas/Canvas'; import Loading from '../../components/Loading/Loading'; +import PaintTools from './PaintTools/PaintTools'; class ImageEditor extends Component { + componentDidMount() { + this.validateImage() + } + + componentDidUpdate() { + this.validateImage() + } + + validateImage = () => { + // Redirect to home page if image does not have source + if (this.props.img.src === null || this.props.img.src === "") { + this.props.history.push("/") + } + } + render() { return (
- pixel artify - this.exportImage()} /> +
+ + pixel artify + +
+ this.exportImage()} + resetCanvas={() => this.resetCanvas()} + history={this.props.history}/> + {this.props.isPaintEnabled ? : }
-
Preview
- this.exportImage = click} /> - {this.props.loading ? : null} + this.exportImage = click} + setResetCanvas={click => this.resetCanvas = click} /> + {this.props.loading && }
); @@ -28,7 +52,9 @@ class ImageEditor extends Component { const mapStateToProps = state => { return { - loading: state.loading + loading: state.loading, + img: state.image, + isPaintEnabled: state.isPaintEnabled }; }; diff --git a/client/src/pages/ImageEditor/ImageEditor.module.css b/client/src/pages/ImageEditor/ImageEditor.module.css index 1f416c1..017f645 100644 --- a/client/src/pages/ImageEditor/ImageEditor.module.css +++ b/client/src/pages/ImageEditor/ImageEditor.module.css @@ -1,11 +1,12 @@ .sidebar { height: 100%; - width: 340px; + width: 320px; position: fixed; top: 0; left: 0; background-color: #333; - overflow-x: hidden; + overflow-y: scroll; + z-index: 10; } .sidebar:after { @@ -19,31 +20,35 @@ color: white; text-decoration: none; text-align: center; - margin-bottom: 40px; - padding-top: 15px; - padding-bottom: 17px; + margin-bottom: 20px; + padding-top: 10px; + padding-bottom: 12px; border-bottom: 1px solid #555; } +.logoLeft { + width: 20px; + margin-right: 10px; +} + +.logoRight { + width: 20px; + margin-left: 8px; +} + .content { - height: 100vh; - margin-left: 340px; - position: relative; + min-height: 100vh; + margin-left: 320px; + display: flex; + justify-content: center; + align-items: center; background: #444; } .content canvas { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); border: 1px dashed #666; } -.previewLabel { - display: none; -} - @media only screen and (max-width: 700px) { .sidebar { height: auto; @@ -53,25 +58,11 @@ } .content { + min-height: 300px; height: 300px; width: 100%; margin-left: 0; position: fixed; bottom: 0; } - - .content canvas { - position: static; - transform: none; - display: block; - margin: 20px auto; - max-height: 200px; - } - - .previewLabel { - display: block; - color: white; - font-weight: bold; - margin: 20px 30px; - } } diff --git a/client/src/pages/ImageEditor/PaintTools/PaintTools.js b/client/src/pages/ImageEditor/PaintTools/PaintTools.js new file mode 100644 index 0000000..65435ca --- /dev/null +++ b/client/src/pages/ImageEditor/PaintTools/PaintTools.js @@ -0,0 +1,86 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { SketchPicker } from 'react-color'; +import { CgColorPicker, CgErase } from "react-icons/cg"; +import { IoIosBrush } from "react-icons/io"; + +import * as actions from '../../../store/actions'; +import styles from './PaintTools.module.css'; +import { TOOL_TYPES } from '../../../constants/constants'; +import Button from '../../../components/Button/Button'; +import SliderControl from '../../../components/SliderControl/SliderControl'; + +class PaintTools extends Component { + constructor(props) { + super(props); + } + + handleChangeColor = (color) => { + this.props.onChangeColor(color.hex); + }; + + renderToolButton = (toolType, icon) => { + const buttonStyle = (this.props.toolType === toolType) ? styles.selected : null; + return ( + + ); + }; + + render() { + const pickerStyles = { + picker: { + padding: '10px 10px 0', + boxSizing: 'initial', + background: '#ddd', + borderRadius: '4px', + } + } + return ( +
+
+
Paint
+
+ {this.renderToolButton(TOOL_TYPES.PAINT, )} + {this.renderToolButton(TOOL_TYPES.COLOR_PICK, )} + {this.renderToolButton(TOOL_TYPES.ERASE, )} +
+
+
+ +
+ +
+ ); + } +} + +const mapStateToProps = state => { + return { + color: state.paintColor, + toolType: state.toolType, + brushSize: state.brushSize, + }; +}; + +const mapDispatchToProps = dispatch => { + return { + onChangeColor: (color) => dispatch(actions.updatePaintColor(color)), + onChangeToolType: (toolType) => dispatch(actions.updateToolType(toolType)), + onChangeBrushSize: (brushSize) => dispatch(actions.updateBrushSize(brushSize)), + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(PaintTools); \ No newline at end of file diff --git a/client/src/pages/ImageEditor/PaintTools/PaintTools.module.css b/client/src/pages/ImageEditor/PaintTools/PaintTools.module.css new file mode 100644 index 0000000..a55f057 --- /dev/null +++ b/client/src/pages/ImageEditor/PaintTools/PaintTools.module.css @@ -0,0 +1,40 @@ +.container { + margin: 0 30px; +} + +.toolsContainer { + display: flex; + justify-content: space-between; +} + +.toolsLabel { + padding: 5px 0px; + color: white; + font-weight: bold; +} + +.tools button { + border-radius: 4px; + background: none; + padding: 4px 8px; + border: none; + color: white; +} + +.tools button:hover { + background: grey; +} + +.tools .selected, .tools .selected:hover { + background: #ef6461; +} + +.tools button svg { + margin-right: 0; +} + +.pickerContainer { + display: flex; + justify-content: center; + margin-bottom: 40px; +} \ No newline at end of file diff --git a/client/src/store/actionTypes.js b/client/src/store/actionTypes.js index 20fe9d0..8566e10 100644 --- a/client/src/store/actionTypes.js +++ b/client/src/store/actionTypes.js @@ -1,7 +1,11 @@ export const LOAD_IMAGE = 'LOAD_IMAGE'; export const LOAD_IMAGE_SUCCESS = 'LOAD_IMAGE_SUCCESS'; export const UPDATE_PIXEL_SIZE = 'UPDATE_PIXEL_SIZE'; +export const UPDATE_BRUSH_SIZE = 'UPDATE_BRUSH_SIZE'; export const UPDATE_CONTRAST = 'UPDATE_CONTRAST'; export const UPDATE_BRIGHTNESS = 'UPDATE_BRIGHTNESS'; export const UPDATE_SATURATION = 'UPDATE_SATURATION'; -export const UPDATE_COLOR_COUNT = 'UPDATE_COLOR_COUNT'; \ No newline at end of file +export const UPDATE_COLOR_COUNT = 'UPDATE_COLOR_COUNT'; +export const ENABLE_PAINT = 'ENABLE_PAINT'; +export const UPDATE_PAINT_COLOR = 'UPDATE_PAINT_COLOR'; +export const UPDATE_TOOL_TYPE = 'UPDATE_TOOL_TYPE'; \ No newline at end of file diff --git a/client/src/store/actions.js b/client/src/store/actions.js index 4994dce..9b0100b 100644 --- a/client/src/store/actions.js +++ b/client/src/store/actions.js @@ -21,6 +21,13 @@ export const updatePixelSize = (pixelSize) => { }; }; +export const updateBrushSize = (brushSize) => { + return { + type: actionTypes.UPDATE_BRUSH_SIZE, + brushSize: brushSize + }; +}; + export const updateContrast = (contrast) => { return { type: actionTypes.UPDATE_CONTRAST, @@ -43,7 +50,6 @@ export const updateSaturation = (saturation) => { }; }; - export const updateColorCount = (colorCount, image) => { const colorThief = new ColorThief(); let palette = []; @@ -56,3 +62,23 @@ export const updateColorCount = (colorCount, image) => { colorPalette: palette }; }; + +export const enablePaint = () => { + return { + type: actionTypes.ENABLE_PAINT + }; +} + +export const updatePaintColor = (color) => { + return { + type: actionTypes.UPDATE_PAINT_COLOR, + paintColor: color + }; +}; + +export const updateToolType = (toolType) => { + return { + type: actionTypes.UPDATE_TOOL_TYPE, + toolType: toolType + }; +}; \ No newline at end of file diff --git a/client/src/store/reducer.js b/client/src/store/reducer.js index 6b6423a..c0be7ec 100644 --- a/client/src/store/reducer.js +++ b/client/src/store/reducer.js @@ -1,46 +1,90 @@ import * as actionTypes from './actionTypes'; -import { updateObject } from '../utils/utility'; +import { updateObject, createReducer, isDevEnv } from '../utils/utility'; import testImg from '../assets/images/car.png'; +import { TOOL_TYPES } from '../constants/constants'; const createTestImage = () => { const img = new Image(); - img.src = testImg; + if (isDevEnv()) { + img.src = testImg; + } return img; }; const initialState = { loading: false, image: createTestImage(), - pixelSize: 4, + isPaintEnabled: false, + paintColor: '#000', + pixelSize: 6, + brushSize: 1, contrast: 0, brightness: 0, saturation: 0, colorCount: '', - colorPalette: [] + colorPalette: [], + toolType: TOOL_TYPES.PAINT, }; -const reducer = (state = initialState, action) => { - switch (action.type) { - case actionTypes.LOAD_IMAGE: - return updateObject(initialState, {image: action.image, loading: true}); - case actionTypes.LOAD_IMAGE_SUCCESS: - return updateObject(state, {loading: false}); - case actionTypes.UPDATE_PIXEL_SIZE: - return updateObject(state, {pixelSize: action.pixelSize}); - case actionTypes.UPDATE_CONTRAST: - return updateObject(state, {contrast: action.contrast}); - case actionTypes.UPDATE_BRIGHTNESS: - return updateObject(state, {brightness: action.brightness}); - case actionTypes.UPDATE_SATURATION: - return updateObject(state, {saturation: action.saturation}); - case actionTypes.UPDATE_COLOR_COUNT: - return updateObject(state, { - colorCount: action.colorCount, - colorPalette: action.colorPalette - }); - default: - return state; - } -}; +function loadImage(state, action) { + return updateObject(initialState, {image: action.image, loading: true}); +} + +function loadImageSuccess(state, action) { + return updateObject(state, {loading: false}); +} + +function enablePaint(state, action) { + return updateObject(state, {isPaintEnabled: true}); +} + +function updatePixelSize(state, action) { + return updateObject(state, {pixelSize: +action.pixelSize}); +} + +function updateBrushSize(state, action) { + return updateObject(state, {brushSize: +action.brushSize}); +} + +function updateContrast(state, action) { + return updateObject(state, {contrast: action.contrast}); +} + +function updateBrightness(state, action) { + return updateObject(state, {brightness: action.brightness}); +} + +function updateSaturation(state, action) { + return updateObject(state, {saturation: action.saturation}); +} + +function updateColorCount(state, action) { + return updateObject(state, { + colorCount: action.colorCount, + colorPalette: action.colorPalette + }); +} + +function updatePaintColor(state, action) { + return updateObject(state, {paintColor: action.paintColor}); +} + +function updateToolType(state, action) { + return updateObject(state, {toolType: action.toolType}); +} + +const reducer = createReducer(initialState, { + [actionTypes.LOAD_IMAGE]: loadImage, + [actionTypes.LOAD_IMAGE_SUCCESS]: loadImageSuccess, + [actionTypes.ENABLE_PAINT]: enablePaint, + [actionTypes.UPDATE_PIXEL_SIZE]: updatePixelSize, + [actionTypes.UPDATE_BRUSH_SIZE]: updateBrushSize, + [actionTypes.UPDATE_CONTRAST]: updateContrast, + [actionTypes.UPDATE_BRIGHTNESS]: updateBrightness, + [actionTypes.UPDATE_SATURATION]: updateSaturation, + [actionTypes.UPDATE_COLOR_COUNT]: updateColorCount, + [actionTypes.UPDATE_PAINT_COLOR]: updatePaintColor, + [actionTypes.UPDATE_TOOL_TYPE]: updateToolType, +}); export default reducer; \ No newline at end of file diff --git a/client/src/utils/colorDifference.js b/client/src/utils/colorDifference.js index 39db52c..99bd597 100644 --- a/client/src/utils/colorDifference.js +++ b/client/src/utils/colorDifference.js @@ -1,3 +1,19 @@ +// From a given list of colors, return the color that is the most +// similar to the given target color. +export function getClosestColor(colors, target) { + let minDiff = Number.MAX_SAFE_INTEGER; + let closest = null; + const length = colors.length; + for (let i = 0; i < length; i++) { + let diff = getColorDifference(target, colors[i]); + if (diff <= minDiff) { + minDiff = diff; + closest = colors[i]; + } + } + return closest; +} + export function getColorDifference(rgb1, rgb2) { const lab1 = convertRgbToLgb(rgb1); const lab2 = convertRgbToLgb(rgb2); @@ -10,6 +26,11 @@ export function convertRgbToLgb(rgb) { return lab; } +// https://stackoverflow.com/a/5624139 +export function convertRgbToHex(r, g, b) { + return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); +} + /** * Convert Standard-RGB to XYZ color space. Adapted from * http://www.easyrgb.com/en/math.php @@ -117,4 +138,22 @@ export function deltaE(labA, labB){ var deltaHkhsh = deltaH / (sh); var i = deltaLKlsl * deltaLKlsl + deltaCkcsc * deltaCkcsc + deltaHkhsh * deltaHkhsh; return i < 0 ? 0 : Math.sqrt(i); +} + +// Adjust the saturation, brightness, and contrast of an image +export function adjustImageColors(img, saturation, brightness, contrast) { + const s = (saturation / 100) + 1; + const b = brightness * 0.75; + const c = (contrast / 100) + 1; + const intercept = 128 * (1 - c); // line intercept for contrast https://stackoverflow.com/a/37714937 + + for (let i = 0; i < img.length; i += 4) { + // Calculate grayness for adjusting saturation https://stackoverflow.com/a/34183839 + const gray = img[i] * 0.3086 + img[i + 1] * 0.6094 + img[i + 2] * 0.0820; + const grayVal = gray * (1 - s); + // Update pixel values with saturation, brightness, and contrast adjusted + img[i] = (img[i] * s + grayVal + b) * c + intercept; + img[i + 1] = (img[i + 1] * s + grayVal + b) * c + intercept; + img[i + 2] = (img[i + 2] * s + grayVal + b) * c + intercept; + } } \ No newline at end of file diff --git a/client/src/utils/utility.js b/client/src/utils/utility.js index fccf025..05b14a6 100644 --- a/client/src/utils/utility.js +++ b/client/src/utils/utility.js @@ -3,4 +3,17 @@ export const updateObject = (oldObject, updatedValues) => { ...oldObject, ...updatedValues } -}; \ No newline at end of file +}; + +// https://redux.js.org/usage/structuring-reducers/refactoring-reducer-example +export function createReducer(initialState, handlers) { + return function reducer(state = initialState, action) { + if (handlers.hasOwnProperty(action.type)) { + return handlers[action.type](state, action) + } else { + return state + } + } +} + +export const isDevEnv = () => process.env.NODE_ENV === 'development'; \ No newline at end of file diff --git a/client/yarn.lock b/client/yarn.lock index 337ef09..29fdc72 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -1095,6 +1095,13 @@ dependencies: regenerator-runtime "^0.13.2" +"@babel/runtime@^7.1.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.15.4", "@babel/runtime@^7.9.2": + version "7.18.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.0.tgz#6d77142a19cb6088f0af662af1ada37a604d34ae" + integrity sha512-YMQvx/6nKEaucl0MY56mwIG483xk8SDNdlUwb2Ts6FUpr7fm85DxEmsY18LXBNhcTz6tO6JwZV8w1W06v8UKeg== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.1.0", "@babel/template@^7.1.2", "@babel/template@^7.2.2": version "7.2.2" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.2.2.tgz#005b3fdf0ed96e88041330379e0da9a708eb2907" @@ -1205,6 +1212,11 @@ dependencies: "@hapi/hoek" "6.x.x" +"@icons/material@^0.2.4": + version "0.2.4" + resolved "https://registry.yarnpkg.com/@icons/material/-/material-0.2.4.tgz#e90c9f71768b3736e76d7dd6783fc6c2afa88bc8" + integrity sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw== + "@jest/console@^24.7.1": version "24.7.1" resolved "https://registry.yarnpkg.com/@jest/console/-/console-24.7.1.tgz#32a9e42535a97aedfe037e725bd67e954b459545" @@ -1502,6 +1514,14 @@ dependencies: "@babel/types" "^7.3.0" +"@types/hoist-non-react-statics@^3.3.0": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" + integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff" @@ -1527,11 +1547,40 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-11.10.5.tgz#fbaca34086bdc118011e1f05c47688d432f2d571" integrity sha512-DuIRlQbX4K+d5I+GMnv+UfnGh+ist0RdlvOp+JZ7ePJ6KQONCFQv/gKYSU1ZzbVdFSUCKZOltjmpFAGGv5MdYA== +"@types/prop-types@*": + version "15.7.5" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" + integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== + "@types/q@^1.5.1": version "1.5.1" resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.1.tgz#48fd98c1561fe718b61733daed46ff115b496e18" integrity sha512-eqz8c/0kwNi/OEHQfvIuJVLTst3in0e7uTKeuY+WL/zfKn0xVujOTp42bS/vUUokhK5P2BppLd9JXMOMHcgbjA== +"@types/react-redux@^7.1.20": + version "7.1.24" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.24.tgz#6caaff1603aba17b27d20f8ad073e4c077e975c0" + integrity sha512-7FkurKcS1k0FHZEtdbbgN8Oc6b+stGSfZYjQGicofJ0j4U0qIn/jaSvnP2pLwZKiai3/17xqqxkkrxTgN8UNbQ== + dependencies: + "@types/hoist-non-react-statics" "^3.3.0" + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + redux "^4.0.0" + +"@types/react@*": + version "18.0.9" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.9.tgz#d6712a38bd6cd83469603e7359511126f122e878" + integrity sha512-9bjbg1hJHUm4De19L1cHiW0Jvx3geel6Qczhjd0qY5VKVE2X5+x77YxAepuCwVh4vrgZJdgEJw48zrhRIeF4Nw== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + +"@types/scheduler@*": + version "0.16.2" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" + integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== + "@types/stack-utils@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" @@ -3344,6 +3393,11 @@ cssstyle@^1.1.1: dependencies: cssom "0.3.x" +csstype@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.0.tgz#4ddcac3718d787cf9df0d1b7d15033925c8f29f2" + integrity sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA== + cyclist@~0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" @@ -4258,6 +4312,11 @@ file-loader@3.0.1: loader-utils "^1.0.2" schema-utils "^1.0.0" +file-saver@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38" + integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA== + filesize@3.6.1: version "3.6.1" resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.6.1.tgz#090bb3ee01b6f801a8a8be99d31710b3422bb317" @@ -4612,6 +4671,11 @@ globby@^6.1.0: pify "^2.0.0" pinkie-promise "^2.0.0" +glur@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/glur/-/glur-1.1.2.tgz#f20ea36db103bfc292343921f1f91e83c3467689" + integrity sha1-8g6jbbEDv8KSNDkh8fkeg8NGdok= + graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6: version "4.1.15" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" @@ -4776,6 +4840,18 @@ hex-color-regex@^1.1.0: resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== +history@^4.9.0: + version "4.10.1" + resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3" + integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew== + dependencies: + "@babel/runtime" "^7.1.2" + loose-envify "^1.2.0" + resolve-pathname "^3.0.0" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" + value-equal "^1.0.1" + hmac-drbg@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -4785,6 +4861,13 @@ hmac-drbg@^1.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" +hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + hosted-git-info@^2.1.4: version "2.7.1" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047" @@ -5400,6 +5483,11 @@ is-wsl@^1.1.0: resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= + isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -6188,6 +6276,11 @@ locate-path@^3.0.0: p-locate "^3.0.0" path-exists "^3.0.0" +lodash-es@^4.17.15: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" + integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== + lodash._reinterpolate@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" @@ -6243,12 +6336,17 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== +lodash@^4.0.1, lodash@^4.17.15: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + loglevel@^1.4.1: version "1.6.1" resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.1.tgz#e0fc95133b6ef276cdc8887cdaf24aa6f156f8fa" integrity sha1-4PyVEztu8nbNyIh82vJKpvFW+Po= -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -6313,6 +6411,11 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" +material-colors@^1.2.1: + version "1.2.6" + resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46" + integrity sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg== + md5.js@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" @@ -6434,6 +6537,14 @@ mimic-fn@^1.0.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== +mini-create-react-context@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz#072171561bfdc922da08a60c2197a497cc2d1d5e" + integrity sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ== + dependencies: + "@babel/runtime" "^7.12.1" + tiny-warning "^1.0.3" + mini-css-extract-plugin@0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.5.0.tgz#ac0059b02b9692515a637115b0cc9fed3a35c7b0" @@ -6564,6 +6675,14 @@ multicast-dns@^6.0.1: dns-packet "^1.3.1" thunky "^1.0.2" +multimath@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/multimath/-/multimath-2.0.0.tgz#0d37acf67c328f30e3d8c6b0d3209e6082710302" + integrity sha512-toRx66cAMJ+Ccz7pMIg38xSIrtnbozk0dchXezwQDMgQmbGpfxjtv68H+L00iFL8hxDaVjrmwAFSb3I6bg8Q2g== + dependencies: + glur "^1.1.2" + object-assign "^4.1.1" + mute-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" @@ -7187,6 +7306,13 @@ path-to-regexp@0.1.7: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= +path-to-regexp@^1.7.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" + integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== + dependencies: + isarray "0.0.1" + path-type@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" @@ -7217,6 +7343,16 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= +pica@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/pica/-/pica-9.0.1.tgz#9ba5a5e81fc09dca9800abef9fb8388434b18b2f" + integrity sha512-v0U4vY6Z3ztz9b4jBIhCD3WYoecGXCQeCsYep+sXRefViL+mVVoTL+wqzdPeE+GpBFsRUtQZb6dltvAt2UkMtQ== + dependencies: + glur "^1.1.2" + multimath "^2.0.0" + object-assign "^4.1.1" + webworkify "^1.5.0" + pify@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -8019,6 +8155,15 @@ prompts@^2.0.1: kleur "^3.0.2" sisteransi "^1.0.0" +prop-types@^15.5.10, prop-types@^15.7.2: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + prop-types@^15.6.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" @@ -8189,6 +8334,19 @@ react-app-polyfill@^1.0.0: regenerator-runtime "0.13.2" whatwg-fetch "3.0.0" +react-color@^2.19.3: + version "2.19.3" + resolved "https://registry.yarnpkg.com/react-color/-/react-color-2.19.3.tgz#ec6c6b4568312a3c6a18420ab0472e146aa5683d" + integrity sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA== + dependencies: + "@icons/material" "^0.2.4" + lodash "^4.17.15" + lodash-es "^4.17.15" + material-colors "^1.2.1" + prop-types "^15.5.10" + reactcss "^1.2.0" + tinycolor2 "^1.4.1" + react-dev-utils@^9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-9.0.0.tgz#356d95db442441c5d4748e0e49f4fd1e71aecbbd" @@ -8220,21 +8378,31 @@ react-dev-utils@^9.0.0: strip-ansi "5.2.0" text-table "0.2.0" -react-dom@16.8.6: - version "16.8.6" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.6.tgz#71d6303f631e8b0097f56165ef608f051ff6e10f" - integrity sha512-1nL7PIq9LTL3fthPqwkvr2zY7phIPjYrT0jp4HjyEQrEROnw4dG41VVwi/wfoCneoleqrNX7iAD+pXebJZwrwA== +react-dom@^16.8.6: + version "16.14.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.14.0.tgz#7ad838ec29a777fb3c75c3a190f661cf92ab8b89" + integrity sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" prop-types "^15.6.2" - scheduler "^0.13.6" + scheduler "^0.19.1" react-error-overlay@^5.1.5: version "5.1.5" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-5.1.5.tgz#884530fd055476c764eaa8ab13b8ecf1f57bbf2c" integrity sha512-O9JRum1Zq/qCPFH5qVEvDDrVun8Jv9vbHtZXCR1EuRj9sKg1xJTlHxBzU6AkCzpvxRLuiY4OKImy3cDLQ+UTdg== +react-icons@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.3.1.tgz#2fa92aebbbc71f43d2db2ed1aed07361124e91ca" + integrity sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ== + +react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + react-is@^16.8.1: version "16.8.4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.4.tgz#90f336a68c3a29a096a3d648ab80e87ec61482a2" @@ -8245,6 +8413,52 @@ react-is@^16.8.4: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16" integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA== +react-is@^17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + +react-redux@^7.0.3: + version "7.2.8" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.8.tgz#a894068315e65de5b1b68899f9c6ee0923dd28de" + integrity sha512-6+uDjhs3PSIclqoCk0kd6iX74gzrGc3W5zcAjbrFgEdIjRSQObdIwfx80unTkVUYvbQ95Y8Av3OvFHq1w5EOUw== + dependencies: + "@babel/runtime" "^7.15.4" + "@types/react-redux" "^7.1.20" + hoist-non-react-statics "^3.3.2" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-is "^17.0.2" + +react-router-dom@^5.0.0: + version "5.3.3" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.3.3.tgz#8779fc28e6691d07afcaf98406d3812fe6f11199" + integrity sha512-Ov0tGPMBgqmbu5CDmN++tv2HQ9HlWDuWIIqn4b88gjlAN5IHI+4ZUZRcpz9Hl0azFIwihbLDYw1OiHGRo7ZIng== + dependencies: + "@babel/runtime" "^7.12.13" + history "^4.9.0" + loose-envify "^1.3.1" + prop-types "^15.6.2" + react-router "5.3.3" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" + +react-router@5.3.3: + version "5.3.3" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.3.3.tgz#8e3841f4089e728cf82a429d92cdcaa5e4a3a288" + integrity sha512-mzQGUvS3bM84TnbtMYR8ZjKnuPJ71IjSzR+DE6UkUqvN4czWIqEs17yLL8xkAycv4ev0AiN+IGrWu88vJs/p2w== + dependencies: + "@babel/runtime" "^7.12.13" + history "^4.9.0" + hoist-non-react-statics "^3.1.0" + loose-envify "^1.3.1" + mini-create-react-context "^0.4.0" + path-to-regexp "^1.7.0" + prop-types "^15.6.2" + react-is "^16.6.0" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" + react-scripts@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-3.0.0.tgz#a715613ef3eace025907b409cec8505096e0233e" @@ -8303,15 +8517,21 @@ react-scripts@3.0.0: optionalDependencies: fsevents "2.0.6" -react@16.8.6: - version "16.8.6" - resolved "https://registry.yarnpkg.com/react/-/react-16.8.6.tgz#ad6c3a9614fd3a4e9ef51117f54d888da01f2bbe" - integrity sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw== +react@^16.8.6: + version "16.14.0" + resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d" + integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" prop-types "^15.6.2" - scheduler "^0.13.6" + +reactcss@^1.2.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/reactcss/-/reactcss-1.2.3.tgz#c00013875e557b1cf0dfd9a368a1c3dab3b548dd" + integrity sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A== + dependencies: + lodash "^4.0.1" read-pkg-up@^2.0.0: version "2.0.0" @@ -8392,6 +8612,13 @@ recursive-readdir@2.2.2: dependencies: minimatch "3.0.4" +redux@^4.0.0, redux@^4.0.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.0.tgz#46f10d6e29b6666df758780437651eeb2b969f13" + integrity sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA== + dependencies: + "@babel/runtime" "^7.9.2" + regenerate-unicode-properties@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.0.1.tgz#58a4a74e736380a7ab3c5f7e03f303a941b31289" @@ -8421,6 +8648,11 @@ regenerator-runtime@^0.11.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== +regenerator-runtime@^0.13.4: + version "0.13.9" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" + integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== + regenerator-transform@^0.13.4: version "0.13.4" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.13.4.tgz#18f6763cf1382c69c36df76c6ce122cc694284fb" @@ -8616,6 +8848,11 @@ resolve-from@^4.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== +resolve-pathname@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd" + integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng== + resolve-url@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" @@ -8753,10 +8990,10 @@ saxes@^3.1.9: dependencies: xmlchars "^1.3.1" -scheduler@^0.13.6: - version "0.13.6" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.6.tgz#466a4ec332467b31a91b9bf74e5347072e4cd889" - integrity sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ== +scheduler@^0.19.1: + version "0.19.1" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196" + integrity sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" @@ -9483,6 +9720,21 @@ timsort@^0.3.0: resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= +tiny-invariant@^1.0.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.2.0.tgz#a1141f86b672a9148c72e978a19a73b9b94a15a9" + integrity sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg== + +tiny-warning@^1.0.0, tiny-warning@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" + integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== + +tinycolor2@^1.4.1: + version "1.4.2" + resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803" + integrity sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA== + tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -9836,6 +10088,11 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +value-equal@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" + integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw== + vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" @@ -10042,6 +10299,11 @@ websocket-extensions@>=0.1.1: resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29" integrity sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg== +webworkify@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/webworkify/-/webworkify-1.5.0.tgz#734ad87a774de6ebdd546e1d3e027da5b8f4a42c" + integrity sha512-AMcUeyXAhbACL8S2hqqdqOLqvJ8ylmIbNwUIqQujRSouf4+eUFaXbG6F1Rbu+srlJMmxQWsiU7mOJi0nMBfM1g== + whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3, whatwg-encoding@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0"