adds tooltips, improves arrow
This commit is contained in:
parent
492d3e9769
commit
34256f992a
16 changed files with 359 additions and 167 deletions
|
@ -31,7 +31,7 @@ export const IconButton = styled('button', {
|
|||
variants: {
|
||||
size: {
|
||||
small: {
|
||||
'& > svg': {
|
||||
'& svg': {
|
||||
height: '16px',
|
||||
width: '16px',
|
||||
},
|
||||
|
@ -39,7 +39,7 @@ export const IconButton = styled('button', {
|
|||
medium: {
|
||||
height: 44,
|
||||
width: 44,
|
||||
'& > svg': {
|
||||
'& svg': {
|
||||
height: '20px',
|
||||
width: '20px',
|
||||
},
|
||||
|
@ -47,7 +47,7 @@ export const IconButton = styled('button', {
|
|||
large: {
|
||||
height: 44,
|
||||
width: 44,
|
||||
'& > svg': {
|
||||
'& svg': {
|
||||
height: '24px',
|
||||
width: '24px',
|
||||
},
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { IconButton } from 'components/shared'
|
||||
import Tooltip from 'components/tooltip'
|
||||
import { strokes } from 'lib/shape-styles'
|
||||
import { Square } from 'react-feather'
|
||||
import state, { useSelector } from 'state'
|
||||
|
@ -10,8 +11,10 @@ export default function QuickColorSelect() {
|
|||
|
||||
return (
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger as={IconButton} title="color">
|
||||
<DropdownMenu.Trigger as={IconButton}>
|
||||
<Tooltip label="Color">
|
||||
<Square fill={strokes[color]} stroke={strokes[color]} />
|
||||
</Tooltip>
|
||||
</DropdownMenu.Trigger>
|
||||
<ColorContent
|
||||
onChange={(color) => state.send('CHANGED_STYLE', { color })}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { IconButton } from 'components/shared'
|
||||
import Tooltip from 'components/tooltip'
|
||||
import state, { useSelector } from 'state'
|
||||
import { DashStyle } from 'types'
|
||||
import {
|
||||
|
@ -21,8 +22,8 @@ export default function QuickdashSelect() {
|
|||
|
||||
return (
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger as={IconButton} title="dash">
|
||||
{dashes[dash]}
|
||||
<DropdownMenu.Trigger as={IconButton}>
|
||||
<Tooltip label="Dash">{dashes[dash]}</Tooltip>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownContent direction="vertical">
|
||||
<DashItem isActive={dash === DashStyle.Solid} dash={DashStyle.Solid} />
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { IconButton } from 'components/shared'
|
||||
import Tooltip from 'components/tooltip'
|
||||
import { Circle } from 'react-feather'
|
||||
import state, { useSelector } from 'state'
|
||||
import { SizeStyle } from 'types'
|
||||
|
@ -16,8 +17,10 @@ export default function QuickSizeSelect() {
|
|||
|
||||
return (
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger as={IconButton} title="size">
|
||||
<DropdownMenu.Trigger as={IconButton}>
|
||||
<Tooltip label="Size">
|
||||
<Circle size={sizes[size]} stroke="none" fill="currentColor" />
|
||||
</Tooltip>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownContent direction="vertical">
|
||||
<SizeItem isActive={size === SizeStyle.Small} size={SizeStyle.Small} />
|
||||
|
|
|
@ -4,7 +4,7 @@ import * as Panel from 'components/panel'
|
|||
import { useRef } from 'react'
|
||||
import { IconButton } from 'components/shared'
|
||||
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 { strokes } from 'lib/shape-styles'
|
||||
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 QuickSizeSelect from './quick-size-select'
|
||||
import QuickdashSelect from './quick-dash-select'
|
||||
import Tooltip from 'components/tooltip'
|
||||
|
||||
export default function StylePanel() {
|
||||
const rContainer = useRef<HTMLDivElement>(null)
|
||||
|
@ -54,7 +55,9 @@ export default function StylePanel() {
|
|||
size="small"
|
||||
onClick={() => state.send('TOGGLED_STYLE_PANEL_OPEN')}
|
||||
>
|
||||
<Tooltip label="More">
|
||||
<ChevronDown />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
</>
|
||||
)}
|
||||
|
@ -125,35 +128,49 @@ function SelectedShapeStyles() {
|
|||
size="small"
|
||||
onClick={() => state.send('DUPLICATED')}
|
||||
>
|
||||
<Tooltip label="Duplicate">
|
||||
<CopyIcon />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
disabled={!hasSelection}
|
||||
size="small"
|
||||
onClick={() => state.send('ROTATED_CCW')}
|
||||
>
|
||||
<Tooltip label="Rotate">
|
||||
<RotateCounterClockwiseIcon />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
disabled={!hasSelection}
|
||||
size="small"
|
||||
onClick={() => state.send('TOGGLED_SHAPE_HIDE')}
|
||||
>
|
||||
<Tooltip label="Toogle Hidden">
|
||||
{isAllHidden ? <EyeClosedIcon /> : <EyeOpenIcon />}
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
disabled={!hasSelection}
|
||||
size="small"
|
||||
onClick={() => state.send('TOGGLED_SHAPE_LOCK')}
|
||||
>
|
||||
<Tooltip label="Toogle Locked">
|
||||
{isAllLocked ? <LockClosedIcon /> : <LockOpen1Icon />}
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
disabled={!hasSelection}
|
||||
size="small"
|
||||
onClick={() => state.send('TOGGLED_SHAPE_ASPECT_LOCK')}
|
||||
>
|
||||
<Tooltip label="Toogle Aspect Ratio Lock">
|
||||
{isAllAspectLocked ? <AspectRatioIcon /> : <BoxIcon />}
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
</ButtonsRow>
|
||||
<ButtonsRow>
|
||||
|
@ -162,35 +179,49 @@ function SelectedShapeStyles() {
|
|||
size="small"
|
||||
onClick={() => state.send('MOVED', { type: MoveType.ToBack })}
|
||||
>
|
||||
<Tooltip label="Move to Back">
|
||||
<PinBottomIcon />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
disabled={!hasSelection}
|
||||
size="small"
|
||||
onClick={() => state.send('MOVED', { type: MoveType.Backward })}
|
||||
>
|
||||
<Tooltip label="Move Backward">
|
||||
<ArrowDownIcon />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
disabled={!hasSelection}
|
||||
size="small"
|
||||
onClick={() => state.send('MOVED', { type: MoveType.Forward })}
|
||||
>
|
||||
<Tooltip label="Move Forward">
|
||||
<ArrowUpIcon />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
disabled={!hasSelection}
|
||||
size="small"
|
||||
onClick={() => state.send('MOVED', { type: MoveType.ToFront })}
|
||||
>
|
||||
<Tooltip label="More to Front">
|
||||
<PinTopIcon />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
disabled={!hasSelection}
|
||||
size="small"
|
||||
onClick={() => state.send('DELETED')}
|
||||
>
|
||||
<Trash2 />
|
||||
<Tooltip label="Delete">
|
||||
<Trash2 size="15" />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
</ButtonsRow>
|
||||
<AlignDistribute
|
||||
|
|
|
@ -18,6 +18,7 @@ import styled from 'styles'
|
|||
import { ShapeType } from 'types'
|
||||
import UndoRedo from './undo-redo'
|
||||
import Zoom from './zoom'
|
||||
import Tooltip from '../tooltip'
|
||||
|
||||
const selectArrowTool = () => state.send('SELECTED_ARROW_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')
|
||||
|
||||
export default function ToolsPanel() {
|
||||
const activeTool = useSelector((state) =>
|
||||
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 activeTool = useSelector((s) => s.data.activeTool)
|
||||
|
||||
const isToolLocked = useSelector((s) => s.data.settings.isToolLocked)
|
||||
|
||||
|
@ -56,6 +44,7 @@ export default function ToolsPanel() {
|
|||
<Zoom />
|
||||
<Flex size={{ '@sm': 'small' }}>
|
||||
<Container>
|
||||
<Tooltip label="Select">
|
||||
<IconButton
|
||||
name="select"
|
||||
size={{ '@initial': 'small', '@sm': 'small', '@md': 'large' }}
|
||||
|
@ -64,8 +53,10 @@ export default function ToolsPanel() {
|
|||
>
|
||||
<CursorArrowIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Container>
|
||||
<Container>
|
||||
<Tooltip label="Draw">
|
||||
<IconButton
|
||||
name={ShapeType.Draw}
|
||||
size={{ '@initial': 'medium', '@sm': 'small', '@md': 'large' }}
|
||||
|
@ -74,6 +65,8 @@ export default function ToolsPanel() {
|
|||
>
|
||||
<Pencil1Icon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip label="Rectangle">
|
||||
<IconButton
|
||||
name={ShapeType.Rectangle}
|
||||
size={{ '@initial': 'medium', '@sm': 'small', '@md': 'large' }}
|
||||
|
@ -82,6 +75,8 @@ export default function ToolsPanel() {
|
|||
>
|
||||
<SquareIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip label="Ellipse">
|
||||
<IconButton
|
||||
name={ShapeType.Circle}
|
||||
size={{ '@initial': 'medium', '@sm': 'small', '@md': 'large' }}
|
||||
|
@ -90,6 +85,8 @@ export default function ToolsPanel() {
|
|||
>
|
||||
<CircleIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip label="Arrow">
|
||||
<IconButton
|
||||
name={ShapeType.Arrow}
|
||||
size={{ '@initial': 'medium', '@sm': 'small', '@md': 'large' }}
|
||||
|
@ -98,6 +95,7 @@ export default function ToolsPanel() {
|
|||
>
|
||||
<ArrowTopRightIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{/* <IconButton
|
||||
name={ShapeType.Circle}
|
||||
size={{ '@initial': 'medium', '@sm': 'small', '@md': 'large' }}
|
||||
|
@ -132,19 +130,23 @@ export default function ToolsPanel() {
|
|||
</IconButton> */}
|
||||
</Container>
|
||||
<Container>
|
||||
<Tooltip label="Lock Tool">
|
||||
<IconButton
|
||||
size={{ '@initial': 'small', '@sm': 'small', '@md': 'large' }}
|
||||
onClick={selectToolLock}
|
||||
>
|
||||
{isToolLocked ? <LockClosedIcon /> : <LockOpen1Icon />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{isPenLocked && (
|
||||
<Tooltip label="Unlock Pen">
|
||||
<IconButton
|
||||
size={{ '@initial': 'small', '@sm': 'small', '@md': 'large' }}
|
||||
onClick={selectToolLock}
|
||||
>
|
||||
<Pencil2Icon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Container>
|
||||
</Flex>
|
||||
|
|
|
@ -2,6 +2,7 @@ import { IconButton } from 'components/shared'
|
|||
import { RotateCcw, RotateCw, Trash2 } from 'react-feather'
|
||||
import state, { useSelector } from 'state'
|
||||
import styled from 'styles'
|
||||
import Tooltip from '../tooltip'
|
||||
|
||||
const undo = () => state.send('UNDO')
|
||||
const redo = () => state.send('REDO')
|
||||
|
@ -10,15 +11,21 @@ const clear = () => state.send('CLEARED_PAGE')
|
|||
export default function UndoRedo() {
|
||||
return (
|
||||
<Container size={{ '@sm': 'small' }}>
|
||||
<Tooltip label="Undo">
|
||||
<IconButton onClick={undo}>
|
||||
<RotateCcw />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip label="Redo">
|
||||
<IconButton onClick={redo}>
|
||||
<RotateCw />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip label="Clear Canvas">
|
||||
<IconButton onClick={clear}>
|
||||
<Trash2 />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import { ZoomInIcon, ZoomOutIcon } from '@radix-ui/react-icons'
|
|||
import { IconButton } from 'components/shared'
|
||||
import state, { useSelector } from 'state'
|
||||
import styled from 'styles'
|
||||
import Tooltip from '../tooltip'
|
||||
|
||||
const zoomIn = () => state.send('ZOOMED_IN')
|
||||
const zoomOut = () => state.send('ZOOMED_OUT')
|
||||
|
@ -11,13 +12,19 @@ const zoomToActual = () => state.send('ZOOMED_TO_ACTUAL')
|
|||
export default function Zoom() {
|
||||
return (
|
||||
<Container size={{ '@sm': 'small' }}>
|
||||
<Tooltip label="Zoom Out">
|
||||
<IconButton onClick={zoomOut}>
|
||||
<ZoomOutIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip label="Zoom In">
|
||||
<IconButton onClick={zoomIn}>
|
||||
<ZoomInIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip label="Reset Zoom">
|
||||
<ZoomCounter />
|
||||
</Tooltip>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
|
36
components/tooltip.tsx
Normal file
36
components/tooltip.tsx
Normal 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',
|
||||
})
|
|
@ -6,15 +6,11 @@ export const strokes: Record<ColorStyle, string> = {
|
|||
[ColorStyle.LightGray]: 'rgba(224, 226, 230, 1.000)',
|
||||
[ColorStyle.Gray]: 'rgba(172, 181, 189, 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.Teal]: 'rgba(9, 167, 120, 1.000)',
|
||||
[ColorStyle.Cyan]: 'rgba(14, 152, 173, 1.000)',
|
||||
[ColorStyle.Blue]: 'rgba(28, 126, 214, 1.000)',
|
||||
[ColorStyle.Indigo]: 'rgba(66, 99, 235, 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.Orange]: 'rgba(247, 103, 6, 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.LightGray]: 'rgba(255, 255, 255, 1.000)',
|
||||
[ColorStyle.Gray]: 'rgba(224, 226, 230, 1.000)',
|
||||
[ColorStyle.Black]: 'rgba(224, 226, 230, 1.000)',
|
||||
[ColorStyle.Lime]: 'rgba(243, 252, 227, 1.000)',
|
||||
[ColorStyle.Black]: 'rgba(255, 255, 255, 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.Blue]: 'rgba(231, 245, 255, 1.000)',
|
||||
[ColorStyle.Indigo]: 'rgba(237, 242, 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.Orange]: 'rgba(255, 244, 229, 1.000)',
|
||||
[ColorStyle.Yellow]: 'rgba(255, 249, 219, 1.000)',
|
||||
|
|
|
@ -97,44 +97,60 @@ const arrow = registerShapeUtils<ArrowShape>({
|
|||
const { start, end, bend: _bend } = handles
|
||||
|
||||
const arrowDist = vec.dist(start.point, end.point)
|
||||
const bendDist = arrowDist * bend
|
||||
const showCircle = Math.abs(bendDist) > 20
|
||||
const showCircle = !vec.isEqual(
|
||||
_bend.point,
|
||||
vec.med(start.point, end.point)
|
||||
)
|
||||
|
||||
const style = getShapeStyle(shape.style)
|
||||
|
||||
// Arrowhead
|
||||
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 v = vec.rot(vec.mul(vec.neg(u), length), angle)
|
||||
const b = vec.add(points[1], vec.rot(v, Math.PI / 6))
|
||||
const c = vec.add(points[1], vec.rot(v, -(Math.PI / 6)))
|
||||
let body: JSX.Element
|
||||
let endAngle: number
|
||||
|
||||
if (showCircle && !ctpCache.has(handles)) {
|
||||
if (showCircle) {
|
||||
if (!ctpCache.has(handles)) {
|
||||
ctpCache.set(
|
||||
handles,
|
||||
circleFromThreePoints(start.point, end.point, _bend.point)
|
||||
)
|
||||
}
|
||||
|
||||
const circle = showCircle && getCtp(shape)
|
||||
const circle = getCtp(shape)
|
||||
|
||||
return (
|
||||
<g id={id}>
|
||||
{circle ? (
|
||||
<>
|
||||
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
|
||||
const length = Math.min(arrowDist / 2, 16 + +style.strokeWidth * 2)
|
||||
const u = vec.uni(vec.vec(start.point, end.point))
|
||||
const v = vec.rot(vec.mul(vec.neg(u), length), endAngle)
|
||||
const b = vec.add(points[1], vec.rot(v, Math.PI / 6))
|
||||
const c = vec.add(points[1], vec.rot(v, -(Math.PI / 6)))
|
||||
|
||||
return (
|
||||
<g id={id}>
|
||||
{body}
|
||||
<circle
|
||||
cx={start.point[0]}
|
||||
cy={start.point[1]}
|
||||
|
@ -301,11 +317,11 @@ function getArrowArcPath(
|
|||
}
|
||||
|
||||
function getBendPoint(shape: ArrowShape) {
|
||||
const { start, end, bend } = shape.handles
|
||||
const { start, end } = shape.handles
|
||||
|
||||
const dist = vec.dist(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))
|
||||
|
||||
return Math.abs(bendDist) < 10
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { v4 as uuid } from 'uuid'
|
||||
import * as vec from 'utils/vec'
|
||||
import { DashStyle, DrawShape, ShapeType } from 'types'
|
||||
import { DashStyle, DrawShape, ShapeStyles, ShapeType } from 'types'
|
||||
import { registerShapeUtils } from './index'
|
||||
import { intersectPolylineBounds } from 'utils/intersections'
|
||||
import { boundsContainPolygon } from 'utils/bounds'
|
||||
|
@ -11,7 +11,6 @@ import {
|
|||
getSvgPathFromStroke,
|
||||
translateBounds,
|
||||
} from 'utils/utils'
|
||||
import styled from 'styles'
|
||||
import { defaultStyle, getShapeStyle } from 'lib/shape-styles'
|
||||
|
||||
const pathCache = new WeakMap<DrawShape['points'], string>([])
|
||||
|
@ -48,17 +47,7 @@ const draw = registerShapeUtils<DrawShape>({
|
|||
const styles = getShapeStyle(style)
|
||||
|
||||
if (!pathCache.has(points)) {
|
||||
pathCache.set(
|
||||
points,
|
||||
getSvgPathFromStroke(
|
||||
getStroke(points, {
|
||||
size: +styles.strokeWidth * 2,
|
||||
thinning: 0.9,
|
||||
end: { taper: 100 },
|
||||
start: { taper: 40 },
|
||||
})
|
||||
)
|
||||
)
|
||||
renderPath(shape, style)
|
||||
}
|
||||
|
||||
if (points.length < 2) {
|
||||
|
@ -155,9 +144,11 @@ const draw = registerShapeUtils<DrawShape>({
|
|||
},
|
||||
|
||||
applyStyles(shape, style) {
|
||||
Object.assign(shape.style, style)
|
||||
shape.style.isFilled = false
|
||||
shape.style.dash = DashStyle.Solid
|
||||
const styles = { ...shape.style, ...style }
|
||||
styles.isFilled = false
|
||||
styles.dash = DashStyle.Solid
|
||||
shape.style = styles
|
||||
shape.points = [...shape.points]
|
||||
return this
|
||||
},
|
||||
|
||||
|
@ -166,6 +157,18 @@ const draw = registerShapeUtils<DrawShape>({
|
|||
|
||||
export default draw
|
||||
|
||||
const DrawPath = styled('path', {
|
||||
strokeWidth: 0,
|
||||
})
|
||||
function renderPath(shape: DrawShape, style: ShapeStyles) {
|
||||
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 },
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
"@radix-ui/react-dropdown-menu": "^0.0.19",
|
||||
"@radix-ui/react-icons": "^1.0.3",
|
||||
"@radix-ui/react-radio-group": "^0.0.16",
|
||||
"@radix-ui/react-tooltip": "^0.0.18",
|
||||
"@state-designer/react": "^1.7.1",
|
||||
"@stitches/react": "^0.1.9",
|
||||
"framer-motion": "^4.1.16",
|
||||
|
|
|
@ -58,6 +58,7 @@ const initialData: Data = {
|
|||
point: [0, 0],
|
||||
zoom: 1,
|
||||
},
|
||||
activeTool: 'select',
|
||||
brush: undefined,
|
||||
boundsRotation: 0,
|
||||
pointedId: null,
|
||||
|
@ -142,6 +143,7 @@ const state = createState({
|
|||
initial: 'selecting',
|
||||
states: {
|
||||
selecting: {
|
||||
onEnter: 'setActiveToolSelect',
|
||||
on: {
|
||||
SAVED: 'forceSave',
|
||||
UNDO: 'undo',
|
||||
|
@ -322,6 +324,7 @@ const state = createState({
|
|||
},
|
||||
states: {
|
||||
draw: {
|
||||
onEnter: 'setActiveToolDraw',
|
||||
initial: 'creating',
|
||||
states: {
|
||||
creating: {
|
||||
|
@ -361,6 +364,7 @@ const state = createState({
|
|||
},
|
||||
},
|
||||
dot: {
|
||||
onEnter: 'setActiveToolDot',
|
||||
initial: 'creating',
|
||||
states: {
|
||||
creating: {
|
||||
|
@ -417,6 +421,7 @@ const state = createState({
|
|||
},
|
||||
},
|
||||
arrow: {
|
||||
onEnter: 'setActiveToolArrow',
|
||||
initial: 'creating',
|
||||
states: {
|
||||
creating: {
|
||||
|
@ -462,6 +467,7 @@ const state = createState({
|
|||
},
|
||||
},
|
||||
circle: {
|
||||
onEnter: 'setActiveToolCircle',
|
||||
initial: 'creating',
|
||||
states: {
|
||||
creating: {
|
||||
|
@ -492,6 +498,7 @@ const state = createState({
|
|||
},
|
||||
},
|
||||
ellipse: {
|
||||
onEnter: 'setActiveToolEllipse',
|
||||
initial: 'creating',
|
||||
states: {
|
||||
creating: {
|
||||
|
@ -519,6 +526,7 @@ const state = createState({
|
|||
},
|
||||
},
|
||||
rectangle: {
|
||||
onEnter: 'setActiveToolRectangle',
|
||||
initial: 'creating',
|
||||
states: {
|
||||
creating: {
|
||||
|
@ -549,6 +557,7 @@ const state = createState({
|
|||
},
|
||||
},
|
||||
ray: {
|
||||
onEnter: 'setActiveToolRay',
|
||||
initial: 'creating',
|
||||
states: {
|
||||
creating: {
|
||||
|
@ -579,6 +588,7 @@ const state = createState({
|
|||
},
|
||||
},
|
||||
line: {
|
||||
onEnter: 'setActiveToolLine',
|
||||
initial: 'creating',
|
||||
states: {
|
||||
creating: {
|
||||
|
@ -608,7 +618,9 @@ const state = createState({
|
|||
},
|
||||
},
|
||||
},
|
||||
polyline: {},
|
||||
polyline: {
|
||||
onEnter: 'setActiveToolPolyline',
|
||||
},
|
||||
},
|
||||
},
|
||||
drawingShape: {
|
||||
|
@ -1011,6 +1023,42 @@ const state = createState({
|
|||
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 --------------------- */
|
||||
|
||||
zoomIn(data) {
|
||||
|
@ -1019,7 +1067,7 @@ const state = createState({
|
|||
const center = [window.innerWidth / 2, window.innerHeight / 2]
|
||||
|
||||
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)
|
||||
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 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)
|
||||
camera.point = vec.add(camera.point, vec.sub(p1, p0))
|
||||
|
||||
|
@ -1067,10 +1115,11 @@ const state = createState({
|
|||
|
||||
const bounds = getSelectedBounds(data)
|
||||
|
||||
const zoom =
|
||||
const zoom = getCameraZoom(
|
||||
bounds.width > bounds.height
|
||||
? (window.innerWidth - 128) / bounds.width
|
||||
: (window.innerHeight - 128) / bounds.height
|
||||
)
|
||||
|
||||
const mx = (window.innerWidth - bounds.width * 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
|
||||
? (window.innerWidth - 128) / bounds.width
|
||||
: (window.innerHeight - 128) / bounds.height
|
||||
)
|
||||
|
||||
const mx = (window.innerWidth - bounds.width * 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 p0 = screenToWorld(payload.point, data)
|
||||
camera.zoom = clamp(next, 0.1, 3)
|
||||
camera.zoom = getCameraZoom(next)
|
||||
const p1 = screenToWorld(payload.point, data)
|
||||
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 p0 = screenToWorld(payload.point, data)
|
||||
camera.zoom = clamp(next, 0.1, 3)
|
||||
camera.zoom = getCameraZoom(next)
|
||||
const p1 = screenToWorld(payload.point, data)
|
||||
camera.point = vec.add(camera.point, vec.sub(p1, p0))
|
||||
|
||||
|
@ -1336,3 +1386,7 @@ let session: Sessions.BaseSession
|
|||
export default state
|
||||
|
||||
export const useSelector = createSelectorHook(state)
|
||||
|
||||
function getCameraZoom(zoom: number) {
|
||||
return clamp(zoom, 0.1, 5)
|
||||
}
|
||||
|
|
5
types.ts
5
types.ts
|
@ -23,6 +23,7 @@ export interface Data {
|
|||
point: number[]
|
||||
zoom: number
|
||||
}
|
||||
activeTool: ShapeType | 'select'
|
||||
brush?: Bounds
|
||||
boundsRotation: number
|
||||
selectedIds: Set<string>
|
||||
|
@ -72,15 +73,11 @@ export enum ColorStyle {
|
|||
LightGray = 'LightGray',
|
||||
Gray = 'Gray',
|
||||
Black = 'Black',
|
||||
Lime = 'Lime',
|
||||
Green = 'Green',
|
||||
Teal = 'Teal',
|
||||
Cyan = 'Cyan',
|
||||
Blue = 'Blue',
|
||||
Indigo = 'Indigo',
|
||||
Violet = 'Violet',
|
||||
Grape = 'Grape',
|
||||
Pink = 'Pink',
|
||||
Red = 'Red',
|
||||
Orange = 'Orange',
|
||||
Yellow = 'Yellow',
|
||||
|
|
39
yarn.lock
39
yarn.lock
|
@ -1500,6 +1500,29 @@
|
|||
"@radix-ui/primitive" "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":
|
||||
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"
|
||||
|
@ -1538,6 +1561,13 @@
|
|||
dependencies:
|
||||
"@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":
|
||||
version "0.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-0.0.7.tgz#e3a55fa7183ef436042198787bf38f8c9befcc14"
|
||||
|
@ -1553,6 +1583,15 @@
|
|||
dependencies:
|
||||
"@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":
|
||||
version "0.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-0.0.5.tgz#6000d8d800288114af4bbc5863e6b58755d7d978"
|
||||
|
|
Loading…
Reference in a new issue