/* eslint-disable react/prop-types */ import React, {useRef, useCallback, useEffect, useState} from 'react' import PropTypes from 'prop-types' import {rem, position} from 'polished' import {FaCircle} from 'react-icons/fa' import {FiCircle} from 'react-icons/fi' import {useTranslation} from 'react-i18next' import {Stack, Flex as ChakraFlex, Box as ChakraBox} from '@chakra-ui/react' import {CopyIcon, DeleteIcon} from '@chakra-ui/icons' import useClickOutside from '../../../shared/hooks/use-click-outside' import {Menu, MenuItem} from '../../../shared/components/menu' import {FlatButton, IconButton} from '../../../shared/components/button' import {Box, Absolute} from '../../../shared/components' import Divider from '../../../shared/components/divider' import theme from '../../../shared/theme' import Flex from '../../../shared/components/flex' import {ClipboardIcon, BasketIcon} from '../../../shared/components/icons' export function Brushes({brush, onChange}) { const brushes = [4, 12, 20, 28, 36] return ( <Stack spacing={2} align="center"> {brushes.map((b, i) => ( <ChakraFlex key={b} align="center" justify="center" bg={brush === b ? 'gray.50' : 'unset'} rounded="md" boxSize={6} onClick={() => onChange(b)} > <ChakraBox key={b} bg="brandGray.500" rounded="full" boxSize={rem((i + 1) * 2)} /> </ChakraFlex> ))} </Stack> ) } Brushes.propTypes = { brush: PropTypes.number, onChange: PropTypes.func, } export function ColorPicker({visible, color, onChange}) { const colorPickerRef = useRef() const colors = [ ['ffffff', 'd2d4d9e0', '96999edd', '53565cdd'], ['ff6666dd', 'ff60e7dd', 'a066ffdd', '578fffdd'], ['0cbdd0dd', '27d980dd', 'ffd763dd', 'ffa366dd'], ] useClickOutside(colorPickerRef, () => { onChange(color) }) return ( <div style={{ display: `${visible ? '' : 'none'}`, }} > <Box css={position('relative')} ref={colorPickerRef}> <Absolute top={0} right={rem(40)} zIndex={100}> <Menu> {colors.map((row, i) => ( <Flex key={i} css={{marginLeft: rem(10), marginRight: rem(10)}}> {row.map((c, j) => { const showColor = c === 'ffffff' ? '#d2d4d9' : `#${c}` const circleStyle = { padding: rem(1), border: `${color === c ? '1px' : '0px'} solid ${showColor}`, borderRadius: '50%', fontSize: theme.fontSizes.large, } return ( <IconButton key={`${j}${j}`} icon={ c === 'ffffff' ? ( <FiCircle color={showColor} style={circleStyle} /> ) : ( <FaCircle color={showColor} style={circleStyle} /> ) } onClick={() => { if (onChange) { onChange(c) } }} ></IconButton> ) })} </Flex> ))} </Menu> </Absolute> </Box> </div> ) } ColorPicker.propTypes = { visible: PropTypes.bool, color: PropTypes.string, onChange: PropTypes.func, } export function ArrowHint({hint, leftHanded}) { return ( <ChakraBox position="relative"> <ChakraBox position="absolute" bottom="86px" zIndex={90}> {leftHanded && ( <div> <div style={{ minWidth: rem(24), minHeight: rem(40), borderLeft: `2px solid ${theme.colors.primary}`, borderTop: `2px solid ${theme.colors.primary}`, }} /> <div style={{ position: 'absolute', left: '-5px', width: 0, height: 0, borderTop: `6px solid transparent`, borderLeft: `6px solid transparent`, borderRight: `6px solid transparent`, borderBottom: 0, borderTopColor: `${theme.colors.primary}`, }} /> <div style={{ position: 'absolute', left: '30px', top: '-25px', minWidth: '75px', color: `${theme.colors.muted}`, fontWeight: `${theme.fontWeights.normal}`, }} > {hint} </div> </div> )} {!leftHanded && ( <div> <div style={{ minWidth: rem(24), minHeight: rem(40), borderRight: `2px solid ${theme.colors.primary}`, borderTop: `2px solid ${theme.colors.primary}`, }} /> <div style={{ position: 'absolute', left: rem(16), width: 0, height: 0, marginLeft: '0px', borderLeft: `6px solid transparent`, borderRight: `6px solid transparent`, borderTop: `6px solid transparent`, borderBottom: 0, borderTopColor: `${theme.colors.primary}`, }} /> <div style={{ position: 'absolute', left: '-58px', top: '-25px', minWidth: rem(52, theme.fontSizes.base), width: rem(52, theme.fontSizes.base), color: `${theme.colors.muted}`, fontWeight: `${theme.fontWeights.normal}`, }} > {hint} </div> </div> )} </ChakraBox> </ChakraBox> ) } EditorContextMenu.propTypes = { x: PropTypes.number, y: PropTypes.number, onClose: PropTypes.func.isRequired, onCopy: PropTypes.func, onPaste: PropTypes.func, onDelete: PropTypes.func, onClear: PropTypes.func, onBringOnTop: PropTypes.func, } export function EditorContextMenu({ x, y, onClose, onCopy, onPaste, onDelete, onClear, onBringOnTop, }) { const {t} = useTranslation() const contextMenuRef = useRef() useClickOutside(contextMenuRef, () => { onClose() }) return ( <Box> <Flex> <Box css={position('relative')}> <Box ref={contextMenuRef}> <Absolute top={y} left={x} zIndex={100}> <Menu> <MenuItem disabled={!onCopy} onClick={() => { onCopy() onClose() }} icon={<CopyIcon boxSize={5} />} > {`${t('Copy')} (Ctrl/Cmd+C)`} </MenuItem> <MenuItem disabled={!onPaste} onClick={() => { onPaste() onClose() }} icon={<ClipboardIcon boxSize={5} />} > {`${t('Paste image')} (Ctrl/Cmd+V)`} </MenuItem> {onBringOnTop && ( <MenuItem onClick={() => { onBringOnTop() onClose() }} > {`${t('Bring on top')} `} </MenuItem> )} <MenuItem disabled={!onDelete} onClick={() => { onDelete() onClose() }} danger icon={<DeleteIcon boxSize={5} color="red.500" />} > {`${t('Delete')} `} </MenuItem> {onClear && ( <MenuItem onClick={() => { onClear() onClose() }} icon={<BasketIcon boxSize={5} color="red.500" />} > {`${t('Clear')} `} </MenuItem> )} </Menu> </Absolute> </Box> </Box> </Flex> </Box> ) } export function ImageEraseEditor({ url, brushWidth, imageObjectProps, onDone, onChanging, isDone, }) { const canvasRef = useRef() const [isMouseDown, setIsMouseDown] = useState(false) useEffect(() => { if (isDone && onDone) { if (canvasRef.current) { onDone(canvasRef.current.toDataURL()) } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isDone]) const handleMouseMove = useCallback( e => { const ctx = canvasRef.current && canvasRef.current.getContext('2d') const x = e.nativeEvent.offsetX const y = e.nativeEvent.offsetY if (ctx && isMouseDown) { onChanging() ctx.globalCompositeOperation = 'destination-out' ctx.beginPath() ctx.arc(x, y, brushWidth / 2, 0, 2 * Math.PI) ctx.fill() } }, // eslint-disable-next-line react-hooks/exhaustive-deps [canvasRef, isMouseDown] ) const handleMouseDown = () => { setIsMouseDown(true) onChanging() } const handleMouseUp = () => { setIsMouseDown(false) } useEffect(() => { let ignore = false async function init() { if (!ignore && canvasRef.current) { let img = new Image() img.setAttribute('crossOrigin', 'anonymous') img.src = url img.onload = function() { const width = img.width * ((imageObjectProps && imageObjectProps.scaleX) || 1) const height = img.height * ((imageObjectProps && imageObjectProps.scaleY) || 1) canvasRef.current.width = width canvasRef.current.height = height const ctx = canvasRef.current.getContext('2d') ctx.drawImage(img, 0, 0, width, height) img = null } } } init() return () => { ignore = true } // eslint-disable-next-line react-hooks/exhaustive-deps }, [canvasRef]) const left = imageObjectProps && imageObjectProps.x - (imageObjectProps.width * imageObjectProps.scaleX) / 2 + 1 const top = imageObjectProps && imageObjectProps.y - (imageObjectProps.height * imageObjectProps.scaleY) / 2 + 1 const angle = (imageObjectProps && imageObjectProps.angle) || 0 return ( <Box css={position('relative')}> <Absolute top={0} left={0} zIndex={100} width="442px" css={{ height: '333px', paddingTop: '0.5px', paddingLeft: '0.5px', overflow: 'hidden', }} > <Box style={{ width: '100%', height: '100%', cursor: 'crosshair', borderRadius: rem(12), }} onMouseDown={handleMouseDown} onMouseUp={handleMouseUp} > <canvas style={{ background: 'transparent', position: 'absolute', left: `${left}px`, top: `${top}px`, transform: `rotate(${angle}deg)`, }} ref={canvasRef} onMouseMove={e => handleMouseMove(e)} ></canvas> </Box> </Absolute> </Box> ) } ImageEraseEditor.propTypes = { url: PropTypes.string, brushWidth: PropTypes.number, imageObjectProps: PropTypes.object, onDone: PropTypes.func, onChanging: PropTypes.func, isDone: PropTypes.bool, } export function ApplyChangesBottomPanel({label, onDone, onCancel}) { const {t} = useTranslation() return ( <Flex justify="space-between" align="center" css={{ marginTop: rem(10), paddingLeft: rem(20), paddingRight: rem(20), }} > {label} <Flex align="center"> <FlatButton onClick={() => onCancel()} mr={2}> {t('Cancel')} </FlatButton> <Divider vertical /> <FlatButton style={{fontWeight: theme.fontWeights.bold}} onClick={() => onDone()} ml={2} > {t('Done')} </FlatButton> </Flex> </Flex> ) } ApplyChangesBottomPanel.propTypes = { label: PropTypes.string, onDone: PropTypes.func.isRequired, onCancel: PropTypes.func.isRequired, }