adds tooltips, improves arrow

This commit is contained in:
Steve Ruiz 2021-06-02 22:17:38 +01:00
parent 492d3e9769
commit 34256f992a
16 changed files with 359 additions and 167 deletions

View file

@ -31,7 +31,7 @@ export const IconButton = styled('button', {
variants: { variants: {
size: { size: {
small: { small: {
'& > svg': { '& svg': {
height: '16px', height: '16px',
width: '16px', width: '16px',
}, },
@ -39,7 +39,7 @@ export const IconButton = styled('button', {
medium: { medium: {
height: 44, height: 44,
width: 44, width: 44,
'& > svg': { '& svg': {
height: '20px', height: '20px',
width: '20px', width: '20px',
}, },
@ -47,7 +47,7 @@ export const IconButton = styled('button', {
large: { large: {
height: 44, height: 44,
width: 44, width: 44,
'& > svg': { '& svg': {
height: '24px', height: '24px',
width: '24px', width: '24px',
}, },

View file

@ -1,5 +1,6 @@
import * as DropdownMenu from '@radix-ui/react-dropdown-menu' import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { IconButton } from 'components/shared' import { IconButton } from 'components/shared'
import Tooltip from 'components/tooltip'
import { strokes } from 'lib/shape-styles' import { strokes } from 'lib/shape-styles'
import { Square } from 'react-feather' import { Square } from 'react-feather'
import state, { useSelector } from 'state' import state, { useSelector } from 'state'
@ -10,8 +11,10 @@ export default function QuickColorSelect() {
return ( return (
<DropdownMenu.Root> <DropdownMenu.Root>
<DropdownMenu.Trigger as={IconButton} title="color"> <DropdownMenu.Trigger as={IconButton}>
<Square fill={strokes[color]} stroke={strokes[color]} /> <Tooltip label="Color">
<Square fill={strokes[color]} stroke={strokes[color]} />
</Tooltip>
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
<ColorContent <ColorContent
onChange={(color) => state.send('CHANGED_STYLE', { color })} onChange={(color) => state.send('CHANGED_STYLE', { color })}

View file

@ -1,5 +1,6 @@
import * as DropdownMenu from '@radix-ui/react-dropdown-menu' import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { IconButton } from 'components/shared' import { IconButton } from 'components/shared'
import Tooltip from 'components/tooltip'
import state, { useSelector } from 'state' import state, { useSelector } from 'state'
import { DashStyle } from 'types' import { DashStyle } from 'types'
import { import {
@ -21,8 +22,8 @@ export default function QuickdashSelect() {
return ( return (
<DropdownMenu.Root> <DropdownMenu.Root>
<DropdownMenu.Trigger as={IconButton} title="dash"> <DropdownMenu.Trigger as={IconButton}>
{dashes[dash]} <Tooltip label="Dash">{dashes[dash]}</Tooltip>
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
<DropdownContent direction="vertical"> <DropdownContent direction="vertical">
<DashItem isActive={dash === DashStyle.Solid} dash={DashStyle.Solid} /> <DashItem isActive={dash === DashStyle.Solid} dash={DashStyle.Solid} />

View file

@ -1,5 +1,6 @@
import * as DropdownMenu from '@radix-ui/react-dropdown-menu' import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { IconButton } from 'components/shared' import { IconButton } from 'components/shared'
import Tooltip from 'components/tooltip'
import { Circle } from 'react-feather' import { Circle } from 'react-feather'
import state, { useSelector } from 'state' import state, { useSelector } from 'state'
import { SizeStyle } from 'types' import { SizeStyle } from 'types'
@ -16,8 +17,10 @@ export default function QuickSizeSelect() {
return ( return (
<DropdownMenu.Root> <DropdownMenu.Root>
<DropdownMenu.Trigger as={IconButton} title="size"> <DropdownMenu.Trigger as={IconButton}>
<Circle size={sizes[size]} stroke="none" fill="currentColor" /> <Tooltip label="Size">
<Circle size={sizes[size]} stroke="none" fill="currentColor" />
</Tooltip>
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
<DropdownContent direction="vertical"> <DropdownContent direction="vertical">
<SizeItem isActive={size === SizeStyle.Small} size={SizeStyle.Small} /> <SizeItem isActive={size === SizeStyle.Small} size={SizeStyle.Small} />

View file

@ -4,7 +4,7 @@ import * as Panel from 'components/panel'
import { useRef } from 'react' import { useRef } from 'react'
import { IconButton } from 'components/shared' import { IconButton } from 'components/shared'
import * as Checkbox from '@radix-ui/react-checkbox' import * as Checkbox from '@radix-ui/react-checkbox'
import { ChevronDown, Square, Trash2, X } from 'react-feather' import { ChevronDown, Square, Tool, Trash2, X } from 'react-feather'
import { deepCompare, deepCompareArrays, getPage } from 'utils/utils' import { deepCompare, deepCompareArrays, getPage } from 'utils/utils'
import { strokes } from 'lib/shape-styles' import { strokes } from 'lib/shape-styles'
import AlignDistribute from './align-distribute' import AlignDistribute from './align-distribute'
@ -35,6 +35,7 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import IsFilledPicker from './is-filled-picker' import IsFilledPicker from './is-filled-picker'
import QuickSizeSelect from './quick-size-select' import QuickSizeSelect from './quick-size-select'
import QuickdashSelect from './quick-dash-select' import QuickdashSelect from './quick-dash-select'
import Tooltip from 'components/tooltip'
export default function StylePanel() { export default function StylePanel() {
const rContainer = useRef<HTMLDivElement>(null) const rContainer = useRef<HTMLDivElement>(null)
@ -54,7 +55,9 @@ export default function StylePanel() {
size="small" size="small"
onClick={() => state.send('TOGGLED_STYLE_PANEL_OPEN')} onClick={() => state.send('TOGGLED_STYLE_PANEL_OPEN')}
> >
<ChevronDown /> <Tooltip label="More">
<ChevronDown />
</Tooltip>
</IconButton> </IconButton>
</> </>
)} )}
@ -125,35 +128,49 @@ function SelectedShapeStyles() {
size="small" size="small"
onClick={() => state.send('DUPLICATED')} onClick={() => state.send('DUPLICATED')}
> >
<CopyIcon /> <Tooltip label="Duplicate">
<CopyIcon />
</Tooltip>
</IconButton> </IconButton>
<IconButton <IconButton
disabled={!hasSelection} disabled={!hasSelection}
size="small" size="small"
onClick={() => state.send('ROTATED_CCW')} onClick={() => state.send('ROTATED_CCW')}
> >
<RotateCounterClockwiseIcon /> <Tooltip label="Rotate">
<RotateCounterClockwiseIcon />
</Tooltip>
</IconButton> </IconButton>
<IconButton <IconButton
disabled={!hasSelection} disabled={!hasSelection}
size="small" size="small"
onClick={() => state.send('TOGGLED_SHAPE_HIDE')} onClick={() => state.send('TOGGLED_SHAPE_HIDE')}
> >
{isAllHidden ? <EyeClosedIcon /> : <EyeOpenIcon />} <Tooltip label="Toogle Hidden">
{isAllHidden ? <EyeClosedIcon /> : <EyeOpenIcon />}
</Tooltip>
</IconButton> </IconButton>
<IconButton <IconButton
disabled={!hasSelection} disabled={!hasSelection}
size="small" size="small"
onClick={() => state.send('TOGGLED_SHAPE_LOCK')} onClick={() => state.send('TOGGLED_SHAPE_LOCK')}
> >
{isAllLocked ? <LockClosedIcon /> : <LockOpen1Icon />} <Tooltip label="Toogle Locked">
{isAllLocked ? <LockClosedIcon /> : <LockOpen1Icon />}
</Tooltip>
</IconButton> </IconButton>
<IconButton <IconButton
disabled={!hasSelection} disabled={!hasSelection}
size="small" size="small"
onClick={() => state.send('TOGGLED_SHAPE_ASPECT_LOCK')} onClick={() => state.send('TOGGLED_SHAPE_ASPECT_LOCK')}
> >
{isAllAspectLocked ? <AspectRatioIcon /> : <BoxIcon />} <Tooltip label="Toogle Aspect Ratio Lock">
{isAllAspectLocked ? <AspectRatioIcon /> : <BoxIcon />}
</Tooltip>
</IconButton> </IconButton>
</ButtonsRow> </ButtonsRow>
<ButtonsRow> <ButtonsRow>
@ -162,35 +179,49 @@ function SelectedShapeStyles() {
size="small" size="small"
onClick={() => state.send('MOVED', { type: MoveType.ToBack })} onClick={() => state.send('MOVED', { type: MoveType.ToBack })}
> >
<PinBottomIcon /> <Tooltip label="Move to Back">
<PinBottomIcon />
</Tooltip>
</IconButton> </IconButton>
<IconButton <IconButton
disabled={!hasSelection} disabled={!hasSelection}
size="small" size="small"
onClick={() => state.send('MOVED', { type: MoveType.Backward })} onClick={() => state.send('MOVED', { type: MoveType.Backward })}
> >
<ArrowDownIcon /> <Tooltip label="Move Backward">
<ArrowDownIcon />
</Tooltip>
</IconButton> </IconButton>
<IconButton <IconButton
disabled={!hasSelection} disabled={!hasSelection}
size="small" size="small"
onClick={() => state.send('MOVED', { type: MoveType.Forward })} onClick={() => state.send('MOVED', { type: MoveType.Forward })}
> >
<ArrowUpIcon /> <Tooltip label="Move Forward">
<ArrowUpIcon />
</Tooltip>
</IconButton> </IconButton>
<IconButton <IconButton
disabled={!hasSelection} disabled={!hasSelection}
size="small" size="small"
onClick={() => state.send('MOVED', { type: MoveType.ToFront })} onClick={() => state.send('MOVED', { type: MoveType.ToFront })}
> >
<PinTopIcon /> <Tooltip label="More to Front">
<PinTopIcon />
</Tooltip>
</IconButton> </IconButton>
<IconButton <IconButton
disabled={!hasSelection} disabled={!hasSelection}
size="small" size="small"
onClick={() => state.send('DELETED')} onClick={() => state.send('DELETED')}
> >
<Trash2 /> <Tooltip label="Delete">
<Trash2 size="15" />
</Tooltip>
</IconButton> </IconButton>
</ButtonsRow> </ButtonsRow>
<AlignDistribute <AlignDistribute

View file

@ -18,6 +18,7 @@ import styled from 'styles'
import { ShapeType } from 'types' import { ShapeType } from 'types'
import UndoRedo from './undo-redo' import UndoRedo from './undo-redo'
import Zoom from './zoom' import Zoom from './zoom'
import Tooltip from '../tooltip'
const selectArrowTool = () => state.send('SELECTED_ARROW_TOOL') const selectArrowTool = () => state.send('SELECTED_ARROW_TOOL')
const selectCircleTool = () => state.send('SELECTED_CIRCLE_TOOL') const selectCircleTool = () => state.send('SELECTED_CIRCLE_TOOL')
@ -32,20 +33,7 @@ const selectSelectTool = () => state.send('SELECTED_SELECT_TOOL')
const selectToolLock = () => state.send('TOGGLED_TOOL_LOCK') const selectToolLock = () => state.send('TOGGLED_TOOL_LOCK')
export default function ToolsPanel() { export default function ToolsPanel() {
const activeTool = useSelector((state) => const activeTool = useSelector((s) => s.data.activeTool)
state.whenIn({
arrow: ShapeType.Arrow,
circle: ShapeType.Circle,
dot: ShapeType.Dot,
draw: ShapeType.Draw,
ellipse: ShapeType.Ellipse,
line: ShapeType.Line,
polyline: ShapeType.Polyline,
ray: ShapeType.Ray,
rectangle: ShapeType.Rectangle,
selecting: 'select',
})
)
const isToolLocked = useSelector((s) => s.data.settings.isToolLocked) const isToolLocked = useSelector((s) => s.data.settings.isToolLocked)
@ -56,48 +44,58 @@ export default function ToolsPanel() {
<Zoom /> <Zoom />
<Flex size={{ '@sm': 'small' }}> <Flex size={{ '@sm': 'small' }}>
<Container> <Container>
<IconButton <Tooltip label="Select">
name="select" <IconButton
size={{ '@initial': 'small', '@sm': 'small', '@md': 'large' }} name="select"
onClick={selectSelectTool} size={{ '@initial': 'small', '@sm': 'small', '@md': 'large' }}
isActive={activeTool === 'select'} onClick={selectSelectTool}
> isActive={activeTool === 'select'}
<CursorArrowIcon /> >
</IconButton> <CursorArrowIcon />
</IconButton>
</Tooltip>
</Container> </Container>
<Container> <Container>
<IconButton <Tooltip label="Draw">
name={ShapeType.Draw} <IconButton
size={{ '@initial': 'medium', '@sm': 'small', '@md': 'large' }} name={ShapeType.Draw}
onClick={selectDrawTool} size={{ '@initial': 'medium', '@sm': 'small', '@md': 'large' }}
isActive={activeTool === ShapeType.Draw} onClick={selectDrawTool}
> isActive={activeTool === ShapeType.Draw}
<Pencil1Icon /> >
</IconButton> <Pencil1Icon />
<IconButton </IconButton>
name={ShapeType.Rectangle} </Tooltip>
size={{ '@initial': 'medium', '@sm': 'small', '@md': 'large' }} <Tooltip label="Rectangle">
onClick={selectRectangleTool} <IconButton
isActive={activeTool === ShapeType.Rectangle} name={ShapeType.Rectangle}
> size={{ '@initial': 'medium', '@sm': 'small', '@md': 'large' }}
<SquareIcon /> onClick={selectRectangleTool}
</IconButton> isActive={activeTool === ShapeType.Rectangle}
<IconButton >
name={ShapeType.Circle} <SquareIcon />
size={{ '@initial': 'medium', '@sm': 'small', '@md': 'large' }} </IconButton>
onClick={selectEllipseTool} </Tooltip>
isActive={activeTool === ShapeType.Ellipse} <Tooltip label="Ellipse">
> <IconButton
<CircleIcon /> name={ShapeType.Circle}
</IconButton> size={{ '@initial': 'medium', '@sm': 'small', '@md': 'large' }}
<IconButton onClick={selectEllipseTool}
name={ShapeType.Arrow} isActive={activeTool === ShapeType.Ellipse}
size={{ '@initial': 'medium', '@sm': 'small', '@md': 'large' }} >
onClick={selectArrowTool} <CircleIcon />
isActive={activeTool === ShapeType.Arrow} </IconButton>
> </Tooltip>
<ArrowTopRightIcon /> <Tooltip label="Arrow">
</IconButton> <IconButton
name={ShapeType.Arrow}
size={{ '@initial': 'medium', '@sm': 'small', '@md': 'large' }}
onClick={selectArrowTool}
isActive={activeTool === ShapeType.Arrow}
>
<ArrowTopRightIcon />
</IconButton>
</Tooltip>
{/* <IconButton {/* <IconButton
name={ShapeType.Circle} name={ShapeType.Circle}
size={{ '@initial': 'medium', '@sm': 'small', '@md': 'large' }} size={{ '@initial': 'medium', '@sm': 'small', '@md': 'large' }}
@ -132,19 +130,23 @@ export default function ToolsPanel() {
</IconButton> */} </IconButton> */}
</Container> </Container>
<Container> <Container>
<IconButton <Tooltip label="Lock Tool">
size={{ '@initial': 'small', '@sm': 'small', '@md': 'large' }}
onClick={selectToolLock}
>
{isToolLocked ? <LockClosedIcon /> : <LockOpen1Icon />}
</IconButton>
{isPenLocked && (
<IconButton <IconButton
size={{ '@initial': 'small', '@sm': 'small', '@md': 'large' }} size={{ '@initial': 'small', '@sm': 'small', '@md': 'large' }}
onClick={selectToolLock} onClick={selectToolLock}
> >
<Pencil2Icon /> {isToolLocked ? <LockClosedIcon /> : <LockOpen1Icon />}
</IconButton> </IconButton>
</Tooltip>
{isPenLocked && (
<Tooltip label="Unlock Pen">
<IconButton
size={{ '@initial': 'small', '@sm': 'small', '@md': 'large' }}
onClick={selectToolLock}
>
<Pencil2Icon />
</IconButton>
</Tooltip>
)} )}
</Container> </Container>
</Flex> </Flex>

View file

@ -2,6 +2,7 @@ import { IconButton } from 'components/shared'
import { RotateCcw, RotateCw, Trash2 } from 'react-feather' import { RotateCcw, RotateCw, Trash2 } from 'react-feather'
import state, { useSelector } from 'state' import state, { useSelector } from 'state'
import styled from 'styles' import styled from 'styles'
import Tooltip from '../tooltip'
const undo = () => state.send('UNDO') const undo = () => state.send('UNDO')
const redo = () => state.send('REDO') const redo = () => state.send('REDO')
@ -10,15 +11,21 @@ const clear = () => state.send('CLEARED_PAGE')
export default function UndoRedo() { export default function UndoRedo() {
return ( return (
<Container size={{ '@sm': 'small' }}> <Container size={{ '@sm': 'small' }}>
<IconButton onClick={undo}> <Tooltip label="Undo">
<RotateCcw /> <IconButton onClick={undo}>
</IconButton> <RotateCcw />
<IconButton onClick={redo}> </IconButton>
<RotateCw /> </Tooltip>
</IconButton> <Tooltip label="Redo">
<IconButton onClick={clear}> <IconButton onClick={redo}>
<Trash2 /> <RotateCw />
</IconButton> </IconButton>
</Tooltip>
<Tooltip label="Clear Canvas">
<IconButton onClick={clear}>
<Trash2 />
</IconButton>
</Tooltip>
</Container> </Container>
) )
} }

View file

@ -2,6 +2,7 @@ import { ZoomInIcon, ZoomOutIcon } from '@radix-ui/react-icons'
import { IconButton } from 'components/shared' import { IconButton } from 'components/shared'
import state, { useSelector } from 'state' import state, { useSelector } from 'state'
import styled from 'styles' import styled from 'styles'
import Tooltip from '../tooltip'
const zoomIn = () => state.send('ZOOMED_IN') const zoomIn = () => state.send('ZOOMED_IN')
const zoomOut = () => state.send('ZOOMED_OUT') const zoomOut = () => state.send('ZOOMED_OUT')
@ -11,13 +12,19 @@ const zoomToActual = () => state.send('ZOOMED_TO_ACTUAL')
export default function Zoom() { export default function Zoom() {
return ( return (
<Container size={{ '@sm': 'small' }}> <Container size={{ '@sm': 'small' }}>
<IconButton onClick={zoomOut}> <Tooltip label="Zoom Out">
<ZoomOutIcon /> <IconButton onClick={zoomOut}>
</IconButton> <ZoomOutIcon />
<IconButton onClick={zoomIn}> </IconButton>
<ZoomInIcon /> </Tooltip>
</IconButton> <Tooltip label="Zoom In">
<ZoomCounter /> <IconButton onClick={zoomIn}>
<ZoomInIcon />
</IconButton>
</Tooltip>
<Tooltip label="Reset Zoom">
<ZoomCounter />
</Tooltip>
</Container> </Container>
) )
} }

36
components/tooltip.tsx Normal file
View file

@ -0,0 +1,36 @@
import * as _Tooltip from '@radix-ui/react-tooltip'
import React from 'react'
import styled from 'styles'
export default function Tooltip({
children,
label,
side = 'top',
}: {
children: React.ReactNode
label: string
side?: 'bottom' | 'left' | 'right' | 'top'
}) {
return (
<_Tooltip.Root>
<_Tooltip.Trigger as="span">{children}</_Tooltip.Trigger>
<StyledContent side={side} sideOffset={8}>
{label}
<StyledArrow />
</StyledContent>
</_Tooltip.Root>
)
}
const StyledContent = styled(_Tooltip.Content, {
borderRadius: 3,
padding: '6px 12px',
fontSize: '$1',
backgroundColor: '$text',
color: '$panel',
})
const StyledArrow = styled(_Tooltip.Arrow, {
fill: '$text',
margin: '0 8px',
})

View file

@ -6,15 +6,11 @@ export const strokes: Record<ColorStyle, string> = {
[ColorStyle.LightGray]: 'rgba(224, 226, 230, 1.000)', [ColorStyle.LightGray]: 'rgba(224, 226, 230, 1.000)',
[ColorStyle.Gray]: 'rgba(172, 181, 189, 1.000)', [ColorStyle.Gray]: 'rgba(172, 181, 189, 1.000)',
[ColorStyle.Black]: 'rgba(0,0,0, 1.000)', [ColorStyle.Black]: 'rgba(0,0,0, 1.000)',
[ColorStyle.Lime]: 'rgba(115, 184, 23, 1.000)',
[ColorStyle.Green]: 'rgba(54, 178, 77, 1.000)', [ColorStyle.Green]: 'rgba(54, 178, 77, 1.000)',
[ColorStyle.Teal]: 'rgba(9, 167, 120, 1.000)',
[ColorStyle.Cyan]: 'rgba(14, 152, 173, 1.000)', [ColorStyle.Cyan]: 'rgba(14, 152, 173, 1.000)',
[ColorStyle.Blue]: 'rgba(28, 126, 214, 1.000)', [ColorStyle.Blue]: 'rgba(28, 126, 214, 1.000)',
[ColorStyle.Indigo]: 'rgba(66, 99, 235, 1.000)', [ColorStyle.Indigo]: 'rgba(66, 99, 235, 1.000)',
[ColorStyle.Violet]: 'rgba(112, 72, 232, 1.000)', [ColorStyle.Violet]: 'rgba(112, 72, 232, 1.000)',
[ColorStyle.Grape]: 'rgba(174, 62, 200, 1.000)',
[ColorStyle.Pink]: 'rgba(214, 51, 108, 1.000)',
[ColorStyle.Red]: 'rgba(240, 63, 63, 1.000)', [ColorStyle.Red]: 'rgba(240, 63, 63, 1.000)',
[ColorStyle.Orange]: 'rgba(247, 103, 6, 1.000)', [ColorStyle.Orange]: 'rgba(247, 103, 6, 1.000)',
[ColorStyle.Yellow]: 'rgba(245, 159, 0, 1.000)', [ColorStyle.Yellow]: 'rgba(245, 159, 0, 1.000)',
@ -24,16 +20,12 @@ export const fills = {
[ColorStyle.White]: 'rgba(224, 226, 230, 1.000)', [ColorStyle.White]: 'rgba(224, 226, 230, 1.000)',
[ColorStyle.LightGray]: 'rgba(255, 255, 255, 1.000)', [ColorStyle.LightGray]: 'rgba(255, 255, 255, 1.000)',
[ColorStyle.Gray]: 'rgba(224, 226, 230, 1.000)', [ColorStyle.Gray]: 'rgba(224, 226, 230, 1.000)',
[ColorStyle.Black]: 'rgba(224, 226, 230, 1.000)', [ColorStyle.Black]: 'rgba(255, 255, 255, 1.000)',
[ColorStyle.Lime]: 'rgba(243, 252, 227, 1.000)',
[ColorStyle.Green]: 'rgba(235, 251, 238, 1.000)', [ColorStyle.Green]: 'rgba(235, 251, 238, 1.000)',
[ColorStyle.Teal]: 'rgba(230, 252, 245, 1.000)',
[ColorStyle.Cyan]: 'rgba(227, 250, 251, 1.000)', [ColorStyle.Cyan]: 'rgba(227, 250, 251, 1.000)',
[ColorStyle.Blue]: 'rgba(231, 245, 255, 1.000)', [ColorStyle.Blue]: 'rgba(231, 245, 255, 1.000)',
[ColorStyle.Indigo]: 'rgba(237, 242, 255, 1.000)', [ColorStyle.Indigo]: 'rgba(237, 242, 255, 1.000)',
[ColorStyle.Violet]: 'rgba(242, 240, 255, 1.000)', [ColorStyle.Violet]: 'rgba(242, 240, 255, 1.000)',
[ColorStyle.Grape]: 'rgba(249, 240, 252, 1.000)',
[ColorStyle.Pink]: 'rgba(254, 241, 246, 1.000)',
[ColorStyle.Red]: 'rgba(255, 245, 245, 1.000)', [ColorStyle.Red]: 'rgba(255, 245, 245, 1.000)',
[ColorStyle.Orange]: 'rgba(255, 244, 229, 1.000)', [ColorStyle.Orange]: 'rgba(255, 244, 229, 1.000)',
[ColorStyle.Yellow]: 'rgba(255, 249, 219, 1.000)', [ColorStyle.Yellow]: 'rgba(255, 249, 219, 1.000)',

View file

@ -97,44 +97,60 @@ const arrow = registerShapeUtils<ArrowShape>({
const { start, end, bend: _bend } = handles const { start, end, bend: _bend } = handles
const arrowDist = vec.dist(start.point, end.point) const arrowDist = vec.dist(start.point, end.point)
const bendDist = arrowDist * bend const showCircle = !vec.isEqual(
const showCircle = Math.abs(bendDist) > 20 _bend.point,
vec.med(start.point, end.point)
)
const style = getShapeStyle(shape.style) const style = getShapeStyle(shape.style)
let body: JSX.Element
let endAngle: number
if (showCircle) {
if (!ctpCache.has(handles)) {
ctpCache.set(
handles,
circleFromThreePoints(start.point, end.point, _bend.point)
)
}
const circle = getCtp(shape)
body = (
<path
d={getArrowArcPath(start, end, circle, bend)}
fill="none"
strokeLinecap="round"
/>
)
const CE =
vec.angle([circle[0], circle[1]], end.point) -
vec.angle(start.point, end.point) +
(Math.PI / 2) * (bend > 0 ? 0.98 : -0.98)
endAngle = CE
} else {
body = (
<polyline
points={[start.point, end.point].join(' ')}
strokeLinecap="round"
/>
)
endAngle = 0
}
// Arrowhead // Arrowhead
const length = Math.min(arrowDist / 2, 16 + +style.strokeWidth * 2) const length = Math.min(arrowDist / 2, 16 + +style.strokeWidth * 2)
const angle = showCircle ? bend * (Math.PI * 0.48) : 0
const u = vec.uni(vec.vec(start.point, end.point)) const u = vec.uni(vec.vec(start.point, end.point))
const v = vec.rot(vec.mul(vec.neg(u), length), angle) const v = vec.rot(vec.mul(vec.neg(u), length), endAngle)
const b = vec.add(points[1], vec.rot(v, Math.PI / 6)) const b = vec.add(points[1], vec.rot(v, Math.PI / 6))
const c = vec.add(points[1], vec.rot(v, -(Math.PI / 6))) const c = vec.add(points[1], vec.rot(v, -(Math.PI / 6)))
if (showCircle && !ctpCache.has(handles)) {
ctpCache.set(
handles,
circleFromThreePoints(start.point, end.point, _bend.point)
)
}
const circle = showCircle && getCtp(shape)
return ( return (
<g id={id}> <g id={id}>
{circle ? ( {body}
<>
<path
d={getArrowArcPath(start, end, circle, bend)}
fill="none"
strokeLinecap="round"
/>
</>
) : (
<polyline
points={[start.point, end.point].join(' ')}
strokeLinecap="round"
/>
)}
<circle <circle
cx={start.point[0]} cx={start.point[0]}
cy={start.point[1]} cy={start.point[1]}
@ -301,11 +317,11 @@ function getArrowArcPath(
} }
function getBendPoint(shape: ArrowShape) { function getBendPoint(shape: ArrowShape) {
const { start, end, bend } = shape.handles const { start, end } = shape.handles
const dist = vec.dist(start.point, end.point) const dist = vec.dist(start.point, end.point)
const midPoint = vec.med(start.point, end.point) const midPoint = vec.med(start.point, end.point)
const bendDist = (dist / 2) * shape.bend const bendDist = (dist / 2) * shape.bend * Math.min(1, dist / 128)
const u = vec.uni(vec.vec(start.point, end.point)) const u = vec.uni(vec.vec(start.point, end.point))
return Math.abs(bendDist) < 10 return Math.abs(bendDist) < 10

View file

@ -1,6 +1,6 @@
import { v4 as uuid } from 'uuid' import { v4 as uuid } from 'uuid'
import * as vec from 'utils/vec' import * as vec from 'utils/vec'
import { DashStyle, DrawShape, ShapeType } from 'types' import { DashStyle, DrawShape, ShapeStyles, ShapeType } from 'types'
import { registerShapeUtils } from './index' import { registerShapeUtils } from './index'
import { intersectPolylineBounds } from 'utils/intersections' import { intersectPolylineBounds } from 'utils/intersections'
import { boundsContainPolygon } from 'utils/bounds' import { boundsContainPolygon } from 'utils/bounds'
@ -11,7 +11,6 @@ import {
getSvgPathFromStroke, getSvgPathFromStroke,
translateBounds, translateBounds,
} from 'utils/utils' } from 'utils/utils'
import styled from 'styles'
import { defaultStyle, getShapeStyle } from 'lib/shape-styles' import { defaultStyle, getShapeStyle } from 'lib/shape-styles'
const pathCache = new WeakMap<DrawShape['points'], string>([]) const pathCache = new WeakMap<DrawShape['points'], string>([])
@ -48,17 +47,7 @@ const draw = registerShapeUtils<DrawShape>({
const styles = getShapeStyle(style) const styles = getShapeStyle(style)
if (!pathCache.has(points)) { if (!pathCache.has(points)) {
pathCache.set( renderPath(shape, style)
points,
getSvgPathFromStroke(
getStroke(points, {
size: +styles.strokeWidth * 2,
thinning: 0.9,
end: { taper: 100 },
start: { taper: 40 },
})
)
)
} }
if (points.length < 2) { if (points.length < 2) {
@ -155,9 +144,11 @@ const draw = registerShapeUtils<DrawShape>({
}, },
applyStyles(shape, style) { applyStyles(shape, style) {
Object.assign(shape.style, style) const styles = { ...shape.style, ...style }
shape.style.isFilled = false styles.isFilled = false
shape.style.dash = DashStyle.Solid styles.dash = DashStyle.Solid
shape.style = styles
shape.points = [...shape.points]
return this return this
}, },
@ -166,6 +157,18 @@ const draw = registerShapeUtils<DrawShape>({
export default draw export default draw
const DrawPath = styled('path', { function renderPath(shape: DrawShape, style: ShapeStyles) {
strokeWidth: 0, const styles = getShapeStyle(style)
})
pathCache.set(
shape.points,
getSvgPathFromStroke(
getStroke(shape.points, {
size: +styles.strokeWidth * 2,
thinning: 0.9,
end: { taper: 100 },
start: { taper: 40 },
})
)
)
}

View file

@ -13,6 +13,7 @@
"@radix-ui/react-dropdown-menu": "^0.0.19", "@radix-ui/react-dropdown-menu": "^0.0.19",
"@radix-ui/react-icons": "^1.0.3", "@radix-ui/react-icons": "^1.0.3",
"@radix-ui/react-radio-group": "^0.0.16", "@radix-ui/react-radio-group": "^0.0.16",
"@radix-ui/react-tooltip": "^0.0.18",
"@state-designer/react": "^1.7.1", "@state-designer/react": "^1.7.1",
"@stitches/react": "^0.1.9", "@stitches/react": "^0.1.9",
"framer-motion": "^4.1.16", "framer-motion": "^4.1.16",

View file

@ -58,6 +58,7 @@ const initialData: Data = {
point: [0, 0], point: [0, 0],
zoom: 1, zoom: 1,
}, },
activeTool: 'select',
brush: undefined, brush: undefined,
boundsRotation: 0, boundsRotation: 0,
pointedId: null, pointedId: null,
@ -142,6 +143,7 @@ const state = createState({
initial: 'selecting', initial: 'selecting',
states: { states: {
selecting: { selecting: {
onEnter: 'setActiveToolSelect',
on: { on: {
SAVED: 'forceSave', SAVED: 'forceSave',
UNDO: 'undo', UNDO: 'undo',
@ -322,6 +324,7 @@ const state = createState({
}, },
states: { states: {
draw: { draw: {
onEnter: 'setActiveToolDraw',
initial: 'creating', initial: 'creating',
states: { states: {
creating: { creating: {
@ -361,6 +364,7 @@ const state = createState({
}, },
}, },
dot: { dot: {
onEnter: 'setActiveToolDot',
initial: 'creating', initial: 'creating',
states: { states: {
creating: { creating: {
@ -417,6 +421,7 @@ const state = createState({
}, },
}, },
arrow: { arrow: {
onEnter: 'setActiveToolArrow',
initial: 'creating', initial: 'creating',
states: { states: {
creating: { creating: {
@ -462,6 +467,7 @@ const state = createState({
}, },
}, },
circle: { circle: {
onEnter: 'setActiveToolCircle',
initial: 'creating', initial: 'creating',
states: { states: {
creating: { creating: {
@ -492,6 +498,7 @@ const state = createState({
}, },
}, },
ellipse: { ellipse: {
onEnter: 'setActiveToolEllipse',
initial: 'creating', initial: 'creating',
states: { states: {
creating: { creating: {
@ -519,6 +526,7 @@ const state = createState({
}, },
}, },
rectangle: { rectangle: {
onEnter: 'setActiveToolRectangle',
initial: 'creating', initial: 'creating',
states: { states: {
creating: { creating: {
@ -549,6 +557,7 @@ const state = createState({
}, },
}, },
ray: { ray: {
onEnter: 'setActiveToolRay',
initial: 'creating', initial: 'creating',
states: { states: {
creating: { creating: {
@ -579,6 +588,7 @@ const state = createState({
}, },
}, },
line: { line: {
onEnter: 'setActiveToolLine',
initial: 'creating', initial: 'creating',
states: { states: {
creating: { creating: {
@ -608,7 +618,9 @@ const state = createState({
}, },
}, },
}, },
polyline: {}, polyline: {
onEnter: 'setActiveToolPolyline',
},
}, },
}, },
drawingShape: { drawingShape: {
@ -1011,6 +1023,42 @@ const state = createState({
commands.rotateCcw(data) commands.rotateCcw(data)
}, },
/* ---------------------- Tool ---------------------- */
setActiveTool(data, payload: { tool: ShapeType | 'select' }) {
data.activeTool = payload.tool
},
setActiveToolSelect(data) {
data.activeTool = 'select'
},
setActiveToolDraw(data) {
data.activeTool = ShapeType.Draw
},
setActiveToolRectangle(data) {
data.activeTool = ShapeType.Rectangle
},
setActiveToolEllipse(data) {
data.activeTool = ShapeType.Ellipse
},
setActiveToolArrow(data) {
data.activeTool = ShapeType.Arrow
},
setActiveToolDot(data) {
data.activeTool = ShapeType.Dot
},
setActiveToolPolyline(data) {
data.activeTool = ShapeType.Polyline
},
setActiveToolRay(data) {
data.activeTool = ShapeType.Ray
},
setActiveToolCircle(data) {
data.activeTool = ShapeType.Circle
},
setActiveToolLine(data) {
data.activeTool = ShapeType.Line
},
/* --------------------- Camera --------------------- */ /* --------------------- Camera --------------------- */
zoomIn(data) { zoomIn(data) {
@ -1019,7 +1067,7 @@ const state = createState({
const center = [window.innerWidth / 2, window.innerHeight / 2] const center = [window.innerWidth / 2, window.innerHeight / 2]
const p0 = screenToWorld(center, data) const p0 = screenToWorld(center, data)
camera.zoom = Math.min(3, (i + 1) * 0.25) camera.zoom = getCameraZoom((i + 1) * 0.25)
const p1 = screenToWorld(center, data) const p1 = screenToWorld(center, data)
camera.point = vec.add(camera.point, vec.sub(p1, p0)) camera.point = vec.add(camera.point, vec.sub(p1, p0))
@ -1031,7 +1079,7 @@ const state = createState({
const center = [window.innerWidth / 2, window.innerHeight / 2] const center = [window.innerWidth / 2, window.innerHeight / 2]
const p0 = screenToWorld(center, data) const p0 = screenToWorld(center, data)
camera.zoom = Math.max(0.1, (i - 1) * 0.25) camera.zoom = getCameraZoom((i - 1) * 0.25)
const p1 = screenToWorld(center, data) const p1 = screenToWorld(center, data)
camera.point = vec.add(camera.point, vec.sub(p1, p0)) camera.point = vec.add(camera.point, vec.sub(p1, p0))
@ -1067,10 +1115,11 @@ const state = createState({
const bounds = getSelectedBounds(data) const bounds = getSelectedBounds(data)
const zoom = const zoom = getCameraZoom(
bounds.width > bounds.height bounds.width > bounds.height
? (window.innerWidth - 128) / bounds.width ? (window.innerWidth - 128) / bounds.width
: (window.innerHeight - 128) / bounds.height : (window.innerHeight - 128) / bounds.height
)
const mx = (window.innerWidth - bounds.width * zoom) / 2 / zoom const mx = (window.innerWidth - bounds.width * zoom) / 2 / zoom
const my = (window.innerHeight - bounds.height * zoom) / 2 / zoom const my = (window.innerHeight - bounds.height * zoom) / 2 / zoom
@ -1096,10 +1145,11 @@ const state = createState({
) )
) )
const zoom = const zoom = getCameraZoom(
bounds.width > bounds.height bounds.width > bounds.height
? (window.innerWidth - 128) / bounds.width ? (window.innerWidth - 128) / bounds.width
: (window.innerHeight - 128) / bounds.height : (window.innerHeight - 128) / bounds.height
)
const mx = (window.innerWidth - bounds.width * zoom) / 2 / zoom const mx = (window.innerWidth - bounds.width * zoom) / 2 / zoom
const my = (window.innerHeight - bounds.height * zoom) / 2 / zoom const my = (window.innerHeight - bounds.height * zoom) / 2 / zoom
@ -1114,7 +1164,7 @@ const state = createState({
const next = camera.zoom - (payload.delta / 100) * camera.zoom const next = camera.zoom - (payload.delta / 100) * camera.zoom
const p0 = screenToWorld(payload.point, data) const p0 = screenToWorld(payload.point, data)
camera.zoom = clamp(next, 0.1, 3) camera.zoom = getCameraZoom(next)
const p1 = screenToWorld(payload.point, data) const p1 = screenToWorld(payload.point, data)
camera.point = vec.add(camera.point, vec.sub(p1, p0)) camera.point = vec.add(camera.point, vec.sub(p1, p0))
@ -1140,7 +1190,7 @@ const state = createState({
const next = camera.zoom - (payload.distanceDelta / 300) * camera.zoom const next = camera.zoom - (payload.distanceDelta / 300) * camera.zoom
const p0 = screenToWorld(payload.point, data) const p0 = screenToWorld(payload.point, data)
camera.zoom = clamp(next, 0.1, 3) camera.zoom = getCameraZoom(next)
const p1 = screenToWorld(payload.point, data) const p1 = screenToWorld(payload.point, data)
camera.point = vec.add(camera.point, vec.sub(p1, p0)) camera.point = vec.add(camera.point, vec.sub(p1, p0))
@ -1336,3 +1386,7 @@ let session: Sessions.BaseSession
export default state export default state
export const useSelector = createSelectorHook(state) export const useSelector = createSelectorHook(state)
function getCameraZoom(zoom: number) {
return clamp(zoom, 0.1, 5)
}

View file

@ -23,6 +23,7 @@ export interface Data {
point: number[] point: number[]
zoom: number zoom: number
} }
activeTool: ShapeType | 'select'
brush?: Bounds brush?: Bounds
boundsRotation: number boundsRotation: number
selectedIds: Set<string> selectedIds: Set<string>
@ -72,15 +73,11 @@ export enum ColorStyle {
LightGray = 'LightGray', LightGray = 'LightGray',
Gray = 'Gray', Gray = 'Gray',
Black = 'Black', Black = 'Black',
Lime = 'Lime',
Green = 'Green', Green = 'Green',
Teal = 'Teal',
Cyan = 'Cyan', Cyan = 'Cyan',
Blue = 'Blue', Blue = 'Blue',
Indigo = 'Indigo', Indigo = 'Indigo',
Violet = 'Violet', Violet = 'Violet',
Grape = 'Grape',
Pink = 'Pink',
Red = 'Red', Red = 'Red',
Orange = 'Orange', Orange = 'Orange',
Yellow = 'Yellow', Yellow = 'Yellow',

View file

@ -1500,6 +1500,29 @@
"@radix-ui/primitive" "0.0.5" "@radix-ui/primitive" "0.0.5"
"@radix-ui/react-compose-refs" "0.0.5" "@radix-ui/react-compose-refs" "0.0.5"
"@radix-ui/react-tooltip@^0.0.18":
version "0.0.18"
resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-0.0.18.tgz#7594297dbc2acf101ac45fdb414c7bc0ac9426bc"
integrity sha512-oYRAbbTZJ8zEokrrk5pe7QbzF+ZnzMby8mBplrkColi0ntToJ7RKzPgUs1OOFvmg/Nld0Iy2FufrTNlyyEI3kQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "0.0.5"
"@radix-ui/react-compose-refs" "0.0.5"
"@radix-ui/react-context" "0.0.5"
"@radix-ui/react-id" "0.0.6"
"@radix-ui/react-polymorphic" "0.0.11"
"@radix-ui/react-popper" "0.0.16"
"@radix-ui/react-portal" "0.0.13"
"@radix-ui/react-presence" "0.0.14"
"@radix-ui/react-primitive" "0.0.13"
"@radix-ui/react-slot" "0.0.10"
"@radix-ui/react-use-controllable-state" "0.0.6"
"@radix-ui/react-use-escape-keydown" "0.0.6"
"@radix-ui/react-use-layout-effect" "0.0.5"
"@radix-ui/react-use-previous" "0.0.5"
"@radix-ui/react-use-rect" "0.0.7"
"@radix-ui/react-visually-hidden" "0.0.13"
"@radix-ui/react-use-body-pointer-events@0.0.6": "@radix-ui/react-use-body-pointer-events@0.0.6":
version "0.0.6" version "0.0.6"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-body-pointer-events/-/react-use-body-pointer-events-0.0.6.tgz#30b21301880417e7dbb345871ff5a83f2abe0d8d" resolved "https://registry.yarnpkg.com/@radix-ui/react-use-body-pointer-events/-/react-use-body-pointer-events-0.0.6.tgz#30b21301880417e7dbb345871ff5a83f2abe0d8d"
@ -1538,6 +1561,13 @@
dependencies: dependencies:
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
"@radix-ui/react-use-previous@0.0.5":
version "0.0.5"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-previous/-/react-use-previous-0.0.5.tgz#75191d1fa0ac24c560fe8cfbaa2f1174858cbb2f"
integrity sha512-GjtJlWlDAEMqCm2RDnVdWI6tk4/ZQfRq/VlP05Xy5rFZj6lD37VZWVWUELMBasRPzd2AS/9wPmphOgjH0VnE5A==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-rect@0.0.7": "@radix-ui/react-use-rect@0.0.7":
version "0.0.7" version "0.0.7"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-0.0.7.tgz#e3a55fa7183ef436042198787bf38f8c9befcc14" resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-0.0.7.tgz#e3a55fa7183ef436042198787bf38f8c9befcc14"
@ -1553,6 +1583,15 @@
dependencies: dependencies:
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
"@radix-ui/react-visually-hidden@0.0.13":
version "0.0.13"
resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-0.0.13.tgz#c7f69097eb7d796dcd9117cdd228d87991c08baf"
integrity sha512-8VNuE4/3PnyrLv1je56fxaa5qka0Nb6/FlyQEDF2HCPpxVOWR4sxRfSBe8cjy+Me+pJN9ZoKBIuoFCVRk54xJA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-polymorphic" "0.0.11"
"@radix-ui/react-primitive" "0.0.13"
"@radix-ui/rect@0.0.5": "@radix-ui/rect@0.0.5":
version "0.0.5" version "0.0.5"
resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-0.0.5.tgz#6000d8d800288114af4bbc5863e6b58755d7d978" resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-0.0.5.tgz#6000d8d800288114af4bbc5863e6b58755d7d978"