Adds quick style selects, rewrites styling

This commit is contained in:
Steve Ruiz 2021-06-02 12:50:34 +01:00
parent 815bf1109c
commit 7c768bddf5
45 changed files with 867 additions and 630 deletions

View file

@ -75,7 +75,7 @@ export default function Canvas() {
<g ref={rGroup}> <g ref={rGroup}>
<BoundsBg /> <BoundsBg />
<Page /> <Page />
<Selected /> {/* <Selected /> */}
<Bounds /> <Bounds />
<Handles /> <Handles />
<Brush /> <Brush />

View file

@ -1,8 +1,11 @@
import { getShapeUtils } from 'lib/shape-utils' import { getShapeUtils } from 'lib/shape-utils'
import { memo } from 'react'
import { useSelector } from 'state' import { useSelector } from 'state'
import { deepCompareArrays, getPage } from 'utils/utils' import { deepCompareArrays, getPage } from 'utils/utils'
export default function Defs() { export default function Defs() {
const zoom = useSelector((s) => s.data.camera.zoom)
const currentPageShapeIds = useSelector(({ data }) => { const currentPageShapeIds = useSelector(({ data }) => {
return Object.values(getPage(data).shapes) return Object.values(getPage(data).shapes)
.sort((a, b) => a.childIndex - b.childIndex) .sort((a, b) => a.childIndex - b.childIndex)
@ -14,12 +17,15 @@ export default function Defs() {
{currentPageShapeIds.map((id) => ( {currentPageShapeIds.map((id) => (
<Def key={id} id={id} /> <Def key={id} id={id} />
))} ))}
<filter id="expand">
<feMorphology operator="dilate" radius={2 / zoom} />
</filter>
</defs> </defs>
) )
} }
export function Def({ id }: { id: string }) { const Def = memo(({ id }: { id: string }) => {
const shape = useSelector(({ data }) => getPage(data).shapes[id]) const shape = useSelector(({ data }) => getPage(data).shapes[id])
if (!shape) return null if (!shape) return null
return getShapeUtils(shape).render(shape) return getShapeUtils(shape).render(shape)
} })

View file

@ -3,9 +3,11 @@ import { useSelector } from 'state'
import { deepCompareArrays, getPage } from 'utils/utils' import { deepCompareArrays, getPage } from 'utils/utils'
import { getShapeUtils } from 'lib/shape-utils' import { getShapeUtils } from 'lib/shape-utils'
import useShapeEvents from 'hooks/useShapeEvents' import useShapeEvents from 'hooks/useShapeEvents'
import { useRef } from 'react' import { memo, useRef } from 'react'
export default function Selected() { export default function Selected() {
const selectedIds = useSelector((s) => s.data.selectedIds)
const currentPageShapeIds = useSelector(({ data }) => { const currentPageShapeIds = useSelector(({ data }) => {
return Array.from(data.selectedIds.values()) return Array.from(data.selectedIds.values())
}, deepCompareArrays) }, deepCompareArrays)
@ -17,13 +19,14 @@ export default function Selected() {
return ( return (
<g> <g>
{currentPageShapeIds.map((id) => ( {currentPageShapeIds.map((id) => (
<ShapeOutline key={id} id={id} /> <ShapeOutline key={id} id={id} isSelected={selectedIds.has(id)} />
))} ))}
</g> </g>
) )
} }
export function ShapeOutline({ id }: { id: string }) { export const ShapeOutline = memo(
({ id, isSelected }: { id: string; isSelected: boolean }) => {
const rIndicator = useRef<SVGUseElement>(null) const rIndicator = useRef<SVGUseElement>(null)
const shape = useSelector(({ data }) => getPage(data).shapes[id]) const shape = useSelector(({ data }) => getPage(data).shapes[id])
@ -48,7 +51,8 @@ export function ShapeOutline({ id }: { id: string }) {
{...events} {...events}
/> />
) )
} }
)
const SelectIndicator = styled('path', { const SelectIndicator = styled('path', {
zStrokeWidth: 3, zStrokeWidth: 3,

View file

@ -5,12 +5,10 @@ import { getShapeUtils } from 'lib/shape-utils'
import { getPage } from 'utils/utils' import { getPage } from 'utils/utils'
import { DashStyle, ShapeStyles } from 'types' import { DashStyle, ShapeStyles } from 'types'
import useShapeEvents from 'hooks/useShapeEvents' import useShapeEvents from 'hooks/useShapeEvents'
import { shades, strokes } from 'lib/colors' import { getShapeStyle } from 'lib/shape-styles'
function Shape({ id, isSelecting }: { id: string; isSelecting: boolean }) { function Shape({ id, isSelecting }: { id: string; isSelecting: boolean }) {
const isHovered = useSelector((state) => state.data.hoveredId === id) const isSelected = useSelector((s) => s.values.selectedIds.has(id))
const isSelected = useSelector((state) => state.values.selectedIds.has(id))
const shape = useSelector(({ data }) => getPage(data).shapes[id]) const shape = useSelector(({ data }) => getPage(data).shapes[id])
@ -25,58 +23,49 @@ function Shape({ id, isSelecting }: { id: string; isSelecting: boolean }) {
if (!shape) return null if (!shape) return null
const center = getShapeUtils(shape).getCenter(shape) const center = getShapeUtils(shape).getCenter(shape)
const transform = ` const transform = `
rotate(${shape.rotation * (180 / Math.PI)}, ${center}) rotate(${shape.rotation * (180 / Math.PI)}, ${center})
translate(${shape.point}) translate(${shape.point})
` `
const style = getShapeStyle(shape.style)
return ( return (
<StyledGroup <StyledGroup ref={rGroup} isSelected={isSelected} transform={transform}>
ref={rGroup}
isHovered={isHovered}
isSelected={isSelected}
transform={transform}
stroke={'red'}
strokeWidth={10}
>
{isSelecting && ( {isSelecting && (
<HoverIndicator <HoverIndicator
as="use" as="use"
href={'#' + id} href={'#' + id}
strokeWidth={+shape.style.strokeWidth + 8} strokeWidth={+style.strokeWidth + 4}
variant={shape.style.fill === 'none' ? 'hollow' : 'filled'} variant={getShapeUtils(shape).canStyleFill ? 'filled' : 'hollow'}
{...events} {...events}
/> />
)} )}
{!shape.isHidden && ( {!shape.isHidden && <RealShape id={id} style={style} />}
<RealShape id={id} style={sanitizeStyle(shape.style)} />
)}
</StyledGroup> </StyledGroup>
) )
} }
const RealShape = memo(({ id, style }: { id: string; style: ShapeStyles }) => { const RealShape = memo(
return ( ({ id, style }: { id: string; style: ReturnType<typeof getShapeStyle> }) => {
<StyledShape return <StyledShape as="use" href={'#' + id} {...style} />
as="use" }
href={'#' + id} )
{...style}
strokeDasharray={getDash(style.dash, +style.strokeWidth)}
/>
)
})
const StyledShape = styled('path', { const StyledShape = styled('path', {
strokeLinecap: 'round', strokeLinecap: 'round',
strokeLinejoin: 'round', strokeLinejoin: 'round',
pointerEvents: 'none',
}) })
const HoverIndicator = styled('path', { const HoverIndicator = styled('path', {
fill: 'transparent', stroke: '$selected',
stroke: 'transparent',
strokeLinecap: 'round', strokeLinecap: 'round',
strokeLinejoin: 'round', strokeLinejoin: 'round',
transform: 'all .2s', transform: 'all .2s',
fill: 'transparent',
filter: 'url(#expand)',
variants: { variants: {
variant: { variant: {
hollow: { hollow: {
@ -90,52 +79,32 @@ const HoverIndicator = styled('path', {
}) })
const StyledGroup = styled('g', { const StyledGroup = styled('g', {
pointerEvents: 'none',
[`& ${HoverIndicator}`]: { [`& ${HoverIndicator}`]: {
opacity: '0', opacity: '0',
}, },
variants: { variants: {
isSelected: { isSelected: {
true: {}, true: {
false: {},
},
isHovered: {
true: {},
false: {},
},
},
compoundVariants: [
{
isSelected: true,
isHovered: true,
css: {
[`& ${HoverIndicator}`]: { [`& ${HoverIndicator}`]: {
opacity: '.4', opacity: '0.2',
stroke: '$selected', },
[`&:hover ${HoverIndicator}`]: {
opacity: '0.3',
},
[`&:active ${HoverIndicator}`]: {
opacity: '0.3',
}, },
}, },
}, false: {
{
isSelected: true,
isHovered: false,
css: {
[`& ${HoverIndicator}`]: { [`& ${HoverIndicator}`]: {
opacity: '.2', opacity: '0',
stroke: '$selected', },
[`&:hover ${HoverIndicator}`]: {
opacity: '0.16',
}, },
}, },
}, },
{
isSelected: false,
isHovered: true,
css: {
[`& ${HoverIndicator}`]: {
opacity: '.2',
stroke: '$selected',
}, },
},
},
],
}) })
function Label({ text }: { text: string }) { function Label({ text }: { text: string }) {
@ -154,25 +123,6 @@ function Label({ text }: { text: string }) {
) )
} }
function getDash(dash: DashStyle, s: number) {
switch (dash) {
case DashStyle.Solid: {
return 'none'
}
case DashStyle.Dashed: {
return `${s} ${s * 2}`
}
case DashStyle.Dotted: {
return `0 ${s * 1.5}`
}
}
}
function sanitizeStyle(style: ShapeStyles) {
const next = { ...style }
return next
}
export { HoverIndicator } export { HoverIndicator }
export default memo(Shape) export default memo(Shape)

View file

@ -119,29 +119,38 @@ export default function CodePanel() {
{isOpen ? ( {isOpen ? (
<Panel.Layout> <Panel.Layout>
<Panel.Header side="left"> <Panel.Header side="left">
<IconButton onClick={() => state.send('TOGGLED_CODE_PANEL_OPEN')}> <IconButton
size="small"
onClick={() => state.send('TOGGLED_CODE_PANEL_OPEN')}
>
<X /> <X />
</IconButton> </IconButton>
<h3>Code</h3> <h3>Code</h3>
<ButtonsGroup> <ButtonsGroup>
<FontSizeButtons> <FontSizeButtons>
<IconButton <IconButton
size="small"
disabled={!local.isIn('editingCode')} disabled={!local.isIn('editingCode')}
onClick={() => state.send('INCREASED_CODE_FONT_SIZE')} onClick={() => state.send('INCREASED_CODE_FONT_SIZE')}
> >
<ChevronUp /> <ChevronUp />
</IconButton> </IconButton>
<IconButton <IconButton
size="small"
disabled={!local.isIn('editingCode')} disabled={!local.isIn('editingCode')}
onClick={() => state.send('DECREASED_CODE_FONT_SIZE')} onClick={() => state.send('DECREASED_CODE_FONT_SIZE')}
> >
<ChevronDown /> <ChevronDown />
</IconButton> </IconButton>
</FontSizeButtons> </FontSizeButtons>
<IconButton onClick={() => local.send('TOGGLED_DOCS')}> <IconButton
size="small"
onClick={() => local.send('TOGGLED_DOCS')}
>
<Info /> <Info />
</IconButton> </IconButton>
<IconButton <IconButton
size="small"
disabled={!local.isIn('editingCode')} disabled={!local.isIn('editingCode')}
onClick={() => local.send('SAVED_CODE')} onClick={() => local.send('SAVED_CODE')}
> >
@ -169,7 +178,10 @@ export default function CodePanel() {
</Panel.Footer> </Panel.Footer>
</Panel.Layout> </Panel.Layout>
) : ( ) : (
<IconButton onClick={() => state.send('TOGGLED_CODE_PANEL_OPEN')}> <IconButton
size="small"
onClick={() => state.send('TOGGLED_CODE_PANEL_OPEN')}
>
<Code /> <Code />
</IconButton> </IconButton>
)} )}

View file

@ -1,12 +1,12 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable @typescript-eslint/ban-ts-comment */
import styled from "styles" import styled from 'styles'
import React, { useEffect, useRef } from "react" import React, { useEffect, useRef } from 'react'
import state, { useSelector } from "state" import state, { useSelector } from 'state'
import { X, Code, PlayCircle } from "react-feather" import { X, Code, PlayCircle } from 'react-feather'
import { IconButton } from "components/shared" import { IconButton } from 'components/shared'
import * as Panel from "../panel" import * as Panel from '../panel'
import Control from "./control" import Control from './control'
import { deepCompareArrays } from "utils/utils" import { deepCompareArrays } from 'utils/utils'
export default function ControlPanel() { export default function ControlPanel() {
const rContainer = useRef<HTMLDivElement>(null) const rContainer = useRef<HTMLDivElement>(null)
@ -21,7 +21,10 @@ export default function ControlPanel() {
{isOpen ? ( {isOpen ? (
<Panel.Layout> <Panel.Layout>
<Panel.Header> <Panel.Header>
<IconButton onClick={() => state.send("CLOSED_CODE_PANEL")}> <IconButton
size="small"
onClick={() => state.send('CLOSED_CODE_PANEL')}
>
<X /> <X />
</IconButton> </IconButton>
<h3>Controls</h3> <h3>Controls</h3>
@ -33,7 +36,10 @@ export default function ControlPanel() {
</ControlsList> </ControlsList>
</Panel.Layout> </Panel.Layout>
) : ( ) : (
<IconButton onClick={() => state.send("OPENED_CODE_PANEL")}> <IconButton
size="small"
onClick={() => state.send('OPENED_CODE_PANEL')}
>
<Code /> <Code />
</IconButton> </IconButton>
)} )}
@ -43,20 +49,20 @@ export default function ControlPanel() {
const ControlsList = styled(Panel.Content, { const ControlsList = styled(Panel.Content, {
padding: 12, padding: 12,
display: "grid", display: 'grid',
gridTemplateColumns: "1fr 4fr", gridTemplateColumns: '1fr 4fr',
gridAutoRows: "24px", gridAutoRows: '24px',
alignItems: "center", alignItems: 'center',
gridColumnGap: "8px", gridColumnGap: '8px',
gridRowGap: "8px", gridRowGap: '8px',
"& input": { '& input': {
font: "$ui", font: '$ui',
fontSize: "$1", fontSize: '$1',
border: "1px solid $inputBorder", border: '1px solid $inputBorder',
backgroundColor: "$input", backgroundColor: '$input',
color: "$text", color: '$text',
height: "100%", height: '100%',
padding: "0px 6px", padding: '0px 6px',
}, },
}) })

View file

@ -9,7 +9,7 @@ export const Root = styled('div', {
userSelect: 'none', userSelect: 'none',
zIndex: 200, zIndex: 200,
border: '1px solid $panel', border: '1px solid $panel',
boxShadow: '0px 2px 4px rgba(0,0,0,.12)', boxShadow: '0px 2px 4px rgba(0,0,0,.2)',
variants: { variants: {
isOpen: { isOpen: {

View file

@ -7,7 +7,7 @@ export const IconButton = styled('button', {
borderRadius: '4px', borderRadius: '4px',
padding: '0', padding: '0',
margin: '0', margin: '0',
display: 'flex', display: 'grid',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
outline: 'none', outline: 'none',
@ -15,6 +15,11 @@ export const IconButton = styled('button', {
pointerEvents: 'all', pointerEvents: 'all',
cursor: 'pointer', cursor: 'pointer',
'& > *': {
gridRow: 1,
gridColumn: 1,
},
'&:hover:not(:disabled)': { '&:hover:not(:disabled)': {
backgroundColor: '$hover', backgroundColor: '$hover',
}, },
@ -23,30 +28,28 @@ export const IconButton = styled('button', {
opacity: '0.5', opacity: '0.5',
}, },
variants: {
size: {
small: {
'& > svg': { '& > svg': {
height: '16px', height: '16px',
width: '16px', width: '16px',
}, },
},
variants: {
size: {
small: {},
medium: { medium: {
height: 44, height: 44,
width: 44, width: 44,
'& svg': { '& > svg': {
height: 16, height: '16px',
width: 16, width: '16px',
strokeWidth: 0,
}, },
}, },
large: { large: {
height: 44, height: 44,
width: 44, width: 44,
'& svg': { '& > svg': {
height: 24, height: '24px',
width: 24, width: '24px',
strokeWidth: 0,
}, },
}, },
}, },

View file

@ -64,34 +64,58 @@ export default function AlignDistribute({
}) { }) {
return ( return (
<Container> <Container>
<IconButton disabled={!hasTwoOrMore} onClick={alignLeft}> <IconButton size="small" disabled={!hasTwoOrMore} onClick={alignLeft}>
<AlignLeftIcon /> <AlignLeftIcon />
</IconButton> </IconButton>
<IconButton disabled={!hasTwoOrMore} onClick={alignCenterHorizontal}> <IconButton
size="small"
disabled={!hasTwoOrMore}
onClick={alignCenterHorizontal}
>
<AlignCenterHorizontallyIcon /> <AlignCenterHorizontallyIcon />
</IconButton> </IconButton>
<IconButton disabled={!hasTwoOrMore} onClick={alignRight}> <IconButton size="small" disabled={!hasTwoOrMore} onClick={alignRight}>
<AlignRightIcon /> <AlignRightIcon />
</IconButton> </IconButton>
<IconButton disabled={!hasTwoOrMore} onClick={stretchHorizontally}> <IconButton
size="small"
disabled={!hasTwoOrMore}
onClick={stretchHorizontally}
>
<StretchHorizontallyIcon /> <StretchHorizontallyIcon />
</IconButton> </IconButton>
<IconButton disabled={!hasThreeOrMore} onClick={distributeHorizontally}> <IconButton
size="small"
disabled={!hasThreeOrMore}
onClick={distributeHorizontally}
>
<SpaceEvenlyHorizontallyIcon /> <SpaceEvenlyHorizontallyIcon />
</IconButton> </IconButton>
<IconButton disabled={!hasTwoOrMore} onClick={alignTop}> <IconButton size="small" disabled={!hasTwoOrMore} onClick={alignTop}>
<AlignTopIcon /> <AlignTopIcon />
</IconButton> </IconButton>
<IconButton disabled={!hasTwoOrMore} onClick={alignCenterVertical}> <IconButton
size="small"
disabled={!hasTwoOrMore}
onClick={alignCenterVertical}
>
<AlignCenterVerticallyIcon /> <AlignCenterVerticallyIcon />
</IconButton> </IconButton>
<IconButton disabled={!hasTwoOrMore} onClick={alignBottom}> <IconButton size="small" disabled={!hasTwoOrMore} onClick={alignBottom}>
<AlignBottomIcon /> <AlignBottomIcon />
</IconButton> </IconButton>
<IconButton disabled={!hasTwoOrMore} onClick={stretchVertically}> <IconButton
size="small"
disabled={!hasTwoOrMore}
onClick={stretchVertically}
>
<StretchVerticallyIcon /> <StretchVerticallyIcon />
</IconButton> </IconButton>
<IconButton disabled={!hasThreeOrMore} onClick={distributeVertically}> <IconButton
size="small"
disabled={!hasThreeOrMore}
onClick={distributeVertically}
>
<SpaceEvenlyVerticallyIcon /> <SpaceEvenlyVerticallyIcon />
</IconButton> </IconButton>
</Container> </Container>

View file

@ -0,0 +1,28 @@
import { IconButton } from 'components/shared'
import { strokes } from 'lib/shape-styles'
import { ColorStyle } from 'types'
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { Square } from 'react-feather'
import styled from 'styles'
import { DropdownContent } from './shared'
export default function ColorContent({
onChange,
}: {
onChange: (color: ColorStyle) => void
}) {
return (
<DropdownContent sideOffset={0} side="bottom">
{Object.keys(strokes).map((color: ColorStyle) => (
<DropdownMenu.DropdownMenuItem
as={IconButton}
key={color}
title={color}
onSelect={() => onChange(color)}
>
<Square fill={strokes[color]} stroke="none" size="22" />
</DropdownMenu.DropdownMenuItem>
))}
</DropdownContent>
)
}

View file

@ -1,129 +1,25 @@
import * as DropdownMenu from '@radix-ui/react-dropdown-menu' import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { strokes } from 'lib/shape-styles'
import { ColorStyle } from 'types'
import { IconWrapper, RowButton } from './shared'
import { Square } from 'react-feather' import { Square } from 'react-feather'
import styled from 'styles' import ColorContent from './color-content'
interface Props { interface Props {
colors: Record<string, string> color: ColorStyle
onChange: (color: string) => void onChange: (color: ColorStyle) => void
children: React.ReactNode
} }
export default function ColorPicker({ colors, onChange, children }: Props) { export default function ColorPicker({ color, onChange }: Props) {
return ( return (
<DropdownMenu.Root> <DropdownMenu.Root>
{children} <DropdownMenu.Trigger as={RowButton}>
<Colors sideOffset={4}> <label htmlFor="color">Color</label>
{Object.entries(colors).map(([name, color]) => ( <IconWrapper>
<ColorButton key={name} title={name} onSelect={() => onChange(name)}> <Square fill={strokes[color]} />
<ColorIcon color={color} /> </IconWrapper>
</ColorButton> </DropdownMenu.Trigger>
))} <ColorContent onChange={onChange} />
</Colors>
</DropdownMenu.Root> </DropdownMenu.Root>
) )
} }
export function ColorIcon({ color }: { color: string }) {
return (
<Square fill={color} strokeDasharray={color === 'none' ? '2, 3' : 'none'} />
)
}
export const Colors = styled(DropdownMenu.Content, {
display: 'grid',
padding: 4,
gridTemplateColumns: 'repeat(6, 1fr)',
border: '1px solid $border',
backgroundColor: '$panel',
borderRadius: 4,
boxShadow: '0px 5px 15px -5px hsla(206,22%,7%,.15)',
})
export const ColorButton = styled(DropdownMenu.Item, {
position: 'relative',
cursor: 'pointer',
height: 32,
width: 32,
border: 'none',
padding: 'none',
background: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
'&::before': {
content: "''",
position: 'absolute',
top: 4,
left: 4,
right: 4,
bottom: 4,
pointerEvents: 'none',
zIndex: 0,
},
'&:hover::before': {
backgroundColor: '$hover',
borderRadius: 4,
},
'& svg': {
position: 'relative',
stroke: 'rgba(0,0,0,.2)',
strokeWidth: 1,
zIndex: 1,
},
})
export const CurrentColor = styled(DropdownMenu.Trigger, {
position: 'relative',
display: 'flex',
width: '100%',
background: 'none',
border: 'none',
cursor: 'pointer',
outline: 'none',
alignItems: 'center',
justifyContent: 'space-between',
padding: '4px 6px 4px 12px',
'&::before': {
content: "''",
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
pointerEvents: 'none',
zIndex: -1,
},
'&:hover::before': {
backgroundColor: '$hover',
borderRadius: 4,
},
'& label': {
fontFamily: '$ui',
fontSize: '$2',
fontWeight: '$1',
margin: 0,
padding: 0,
},
'& svg': {
position: 'relative',
stroke: 'rgba(0,0,0,.2)',
strokeWidth: 1,
zIndex: 1,
},
variants: {
size: {
icon: {
padding: '4px ',
width: 'auto',
},
},
},
})

View file

@ -1,4 +1,11 @@
import { Group, RadioItem } from './shared' import {
Group,
Item,
DashDashedIcon,
DashDottedIcon,
DashSolidIcon,
} from './shared'
import * as RadioGroup from '@radix-ui/react-radio-group'
import { DashStyle } from 'types' import { DashStyle } from 'types'
import state from 'state' import state from 'state'
import { ChangeEvent } from 'react' import { ChangeEvent } from 'react'
@ -16,49 +23,27 @@ interface Props {
export default function DashPicker({ dash }: Props) { export default function DashPicker({ dash }: Props) {
return ( return (
<Group name="Dash" onValueChange={handleChange}> <Group name="Dash" onValueChange={handleChange}>
<RadioItem value={DashStyle.Solid} isActive={dash === DashStyle.Solid}> <Item
as={RadioGroup.RadioGroupItem}
value={DashStyle.Solid}
isActive={dash === DashStyle.Solid}
>
<DashSolidIcon /> <DashSolidIcon />
</RadioItem> </Item>
<RadioItem value={DashStyle.Dashed} isActive={dash === DashStyle.Dashed}> <Item
as={RadioGroup.RadioGroupItem}
value={DashStyle.Dashed}
isActive={dash === DashStyle.Dashed}
>
<DashDashedIcon /> <DashDashedIcon />
</RadioItem> </Item>
<RadioItem value={DashStyle.Dotted} isActive={dash === DashStyle.Dotted}> <Item
as={RadioGroup.RadioGroupItem}
value={DashStyle.Dotted}
isActive={dash === DashStyle.Dotted}
>
<DashDottedIcon /> <DashDottedIcon />
</RadioItem> </Item>
</Group> </Group>
) )
} }
function DashSolidIcon() {
return (
<svg width="16" height="16">
<path d="M 3,8 L 13,8" strokeWidth={3} strokeLinecap="round" />
</svg>
)
}
function DashDashedIcon() {
return (
<svg width="16" height="16">
<path
d="M 2,8 L 14,8"
strokeWidth={3}
strokeLinecap="round"
strokeDasharray="4 4"
/>
</svg>
)
}
function DashDottedIcon() {
return (
<svg width="16" height="16">
<path
d="M 3,8 L 14,8"
strokeWidth={3}
strokeLinecap="round"
strokeDasharray="1 4"
/>
</svg>
)
}

View file

@ -0,0 +1,28 @@
import * as Checkbox from '@radix-ui/react-checkbox'
import { CheckIcon } from '@radix-ui/react-icons'
import { strokes } from 'lib/shape-styles'
import { Square } from 'react-feather'
import { IconWrapper, RowButton } from './shared'
interface Props {
isFilled: boolean
onChange: (isFilled: boolean) => void
}
export default function IsFilledPicker({ isFilled, onChange }: Props) {
return (
<Checkbox.Root
as={RowButton}
checked={isFilled}
onCheckedChange={(e: React.ChangeEvent<HTMLInputElement>) =>
onChange(e.currentTarget.checked)
}
>
<label htmlFor="fill">Fill</label>
<IconWrapper>
{isFilled || <Square stroke={strokes.Black} />}
<Checkbox.Indicator as={CheckIcon} />
</IconWrapper>
</Checkbox.Root>
)
}

View file

@ -0,0 +1,21 @@
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { IconButton } from 'components/shared'
import { strokes } from 'lib/shape-styles'
import { Square } from 'react-feather'
import state, { useSelector } from 'state'
import ColorContent from './color-content'
export default function QuickColorSelect() {
const color = useSelector((s) => s.values.selectedStyle.color)
return (
<DropdownMenu.Root>
<DropdownMenu.Trigger as={IconButton} title="color">
<Square fill={strokes[color]} stroke={strokes[color]} />
</DropdownMenu.Trigger>
<ColorContent
onChange={(color) => state.send('CHANGED_STYLE', { color })}
/>
</DropdownMenu.Root>
)
}

View file

@ -0,0 +1,52 @@
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { IconButton } from 'components/shared'
import state, { useSelector } from 'state'
import { DashStyle } from 'types'
import {
DropdownContent,
Item,
DashDottedIcon,
DashSolidIcon,
DashDashedIcon,
} from './shared'
const dashes = {
[DashStyle.Solid]: <DashSolidIcon />,
[DashStyle.Dashed]: <DashDashedIcon />,
[DashStyle.Dotted]: <DashDottedIcon />,
}
export default function QuickdashSelect() {
const dash = useSelector((s) => s.values.selectedStyle.dash)
return (
<DropdownMenu.Root>
<DropdownMenu.Trigger as={IconButton} title="dash">
{dashes[dash]}
</DropdownMenu.Trigger>
<DropdownContent direction="vertical">
<DashItem isActive={dash === DashStyle.Solid} dash={DashStyle.Solid} />
<DashItem
isActive={dash === DashStyle.Dashed}
dash={DashStyle.Dashed}
/>
<DashItem
isActive={dash === DashStyle.Dotted}
dash={DashStyle.Dotted}
/>
</DropdownContent>
</DropdownMenu.Root>
)
}
function DashItem({ dash, isActive }: { isActive: boolean; dash: DashStyle }) {
return (
<Item
as={DropdownMenu.DropdownMenuItem}
isActive={isActive}
onSelect={() => state.send('CHANGED_STYLE', { dash })}
>
{dashes[dash]}
</Item>
)
}

View file

@ -0,0 +1,44 @@
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { IconButton } from 'components/shared'
import { Circle } from 'react-feather'
import state, { useSelector } from 'state'
import { SizeStyle } from 'types'
import { DropdownContent, Item } from './shared'
const sizes = {
[SizeStyle.Small]: 6,
[SizeStyle.Medium]: 12,
[SizeStyle.Large]: 22,
}
export default function QuickSizeSelect() {
const size = useSelector((s) => s.values.selectedStyle.size)
return (
<DropdownMenu.Root>
<DropdownMenu.Trigger as={IconButton} title="size">
<Circle size={sizes[size]} stroke="none" fill="currentColor" />
</DropdownMenu.Trigger>
<DropdownContent direction="vertical">
<SizeItem isActive={size === SizeStyle.Small} size={SizeStyle.Small} />
<SizeItem
isActive={size === SizeStyle.Medium}
size={SizeStyle.Medium}
/>
<SizeItem isActive={size === SizeStyle.Large} size={SizeStyle.Large} />
</DropdownContent>
</DropdownMenu.Root>
)
}
function SizeItem({ size, isActive }: { isActive: boolean; size: SizeStyle }) {
return (
<Item
as={DropdownMenu.DropdownMenuItem}
isActive={isActive}
onSelect={() => state.send('CHANGED_STYLE', { size })}
>
<Circle size={sizes[size]} />
</Item>
)
}

View file

@ -1,3 +1,4 @@
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import * as RadioGroup from '@radix-ui/react-radio-group' import * as RadioGroup from '@radix-ui/react-radio-group'
import * as Panel from '../panel' import * as Panel from '../panel'
import styled from 'styles' import styled from 'styles'
@ -27,7 +28,7 @@ export const Group = styled(RadioGroup.Root, {
display: 'flex', display: 'flex',
}) })
export const RadioItem = styled(RadioGroup.Item, { export const Item = styled('button', {
height: '32px', height: '32px',
width: '32px', width: '32px',
backgroundColor: '$panel', backgroundColor: '$panel',
@ -61,16 +62,158 @@ export const RadioItem = styled(RadioGroup.Item, {
'& svg': { '& svg': {
fill: '$text', fill: '$text',
stroke: '$text', stroke: '$text',
strokeWidth: '0',
}, },
}, },
false: { false: {
'& svg': { '& svg': {
fill: '$inactive', fill: '$inactive',
stroke: '$inactive', stroke: '$inactive',
strokeWidth: '0',
}, },
}, },
}, },
}, },
}) })
export const RowButton = styled('button', {
position: 'relative',
display: 'flex',
width: '100%',
background: 'none',
border: 'none',
cursor: 'pointer',
outline: 'none',
alignItems: 'center',
justifyContent: 'space-between',
padding: '4px 6px 4px 12px',
'&::before': {
content: "''",
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
pointerEvents: 'none',
zIndex: -1,
},
'&:hover::before': {
backgroundColor: '$hover',
borderRadius: 4,
},
'& label': {
fontFamily: '$ui',
fontSize: '$2',
fontWeight: '$1',
margin: 0,
padding: 0,
},
'& svg': {
position: 'relative',
stroke: 'rgba(0,0,0,.2)',
strokeWidth: 1,
zIndex: 1,
},
variants: {
size: {
icon: {
padding: '4px ',
width: 'auto',
},
},
},
})
export const IconWrapper = styled('div', {
height: '100%',
borderRadius: '4px',
marginRight: '1px',
display: 'grid',
alignItems: 'center',
justifyContent: 'center',
outline: 'none',
border: 'none',
pointerEvents: 'all',
cursor: 'pointer',
'& svg': {
height: 22,
width: 22,
strokeWidth: 1,
},
'& > *': {
gridRow: 1,
gridColumn: 1,
},
})
export const DropdownContent = styled(DropdownMenu.Content, {
display: 'grid',
padding: 4,
gridTemplateColumns: 'repeat(4, 1fr)',
backgroundColor: '$panel',
borderRadius: 4,
border: '1px solid $panel',
boxShadow: '0px 2px 4px rgba(0,0,0,.28)',
variants: {
direction: {
vertical: {
gridTemplateColumns: '1fr',
},
},
},
})
export function DashSolidIcon() {
return (
<svg width="24" height="24" stroke="currentColor">
<circle
cx={12}
cy={12}
r={8}
fill="none"
strokeWidth={2}
strokeLinecap="round"
/>
</svg>
)
}
export function DashDashedIcon() {
return (
<svg width="24" height="24" stroke="currentColor">
<circle
cx={12}
cy={12}
r={8}
fill="none"
strokeWidth={2.5}
strokeLinecap="round"
strokeDasharray={50.26548 * 0.1}
/>
</svg>
)
}
const dottedDasharray = `${50.26548 * 0.025} ${50.26548 * 0.1}`
export function DashDottedIcon() {
return (
<svg width="24" height="24" stroke="currentColor">
<circle
cx={12}
cy={12}
r={8}
fill="none"
strokeWidth={2.5}
strokeLinecap="round"
strokeDasharray={dottedDasharray}
/>
</svg>
)
}

View file

@ -0,0 +1,40 @@
import { Group, Item } from './shared'
import * as RadioGroup from '@radix-ui/react-radio-group'
import { ChangeEvent } from 'react'
import { Circle } from 'react-feather'
import state from 'state'
import { SizeStyle } from 'types'
function handleChange(e: ChangeEvent<HTMLInputElement>) {
state.send('CHANGED_STYLE', {
size: e.currentTarget.value as SizeStyle,
})
}
export default function SizePicker({ size }: { size: SizeStyle }) {
return (
<Group name="width" onValueChange={handleChange}>
<Item
as={RadioGroup.Item}
value={SizeStyle.Small}
isActive={size === SizeStyle.Small}
>
<Circle size={6} />
</Item>
<Item
as={RadioGroup.Item}
value={SizeStyle.Medium}
isActive={size === SizeStyle.Medium}
>
<Circle size={12} />
</Item>
<Item
as={RadioGroup.Item}
value={SizeStyle.Large}
isActive={size === SizeStyle.Large}
>
<Circle size={22} />
</Item>
</Group>
)
}

View file

@ -4,13 +4,12 @@ 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 { Trash2, X } from 'react-feather' import { ChevronDown, Square, Trash2, X } from 'react-feather'
import { deepCompare, deepCompareArrays, getPage } from 'utils/utils' import { deepCompare, deepCompareArrays, getPage } from 'utils/utils'
import { shades, fills, strokes } from 'lib/colors' import { strokes } from 'lib/shape-styles'
import ColorPicker, { ColorIcon, CurrentColor } from './color-picker'
import AlignDistribute from './align-distribute' import AlignDistribute from './align-distribute'
import { MoveType, ShapeStyles } from 'types' import { MoveType } from 'types'
import WidthPicker from './width-picker' import SizePicker from './size-picker'
import { import {
ArrowDownIcon, ArrowDownIcon,
ArrowUpIcon, ArrowUpIcon,
@ -28,15 +27,14 @@ import {
RotateCounterClockwiseIcon, RotateCounterClockwiseIcon,
} from '@radix-ui/react-icons' } from '@radix-ui/react-icons'
import DashPicker from './dash-picker' import DashPicker from './dash-picker'
import QuickColorSelect from './quick-color-select'
const fillColors = { ...shades, ...fills } import ColorContent from './color-content'
const strokeColors = { ...shades, ...strokes } import { RowButton, IconWrapper } from './shared'
const getFillColor = (color: string) => { import ColorPicker from './color-picker'
if (shades[color]) { import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
return '#fff' import IsFilledPicker from './is-filled-picker'
} import QuickSizeSelect from './quick-size-select'
return fillColors[color] import QuickdashSelect from './quick-dash-select'
}
export default function StylePanel() { export default function StylePanel() {
const rContainer = useRef<HTMLDivElement>(null) const rContainer = useRef<HTMLDivElement>(null)
@ -48,12 +46,15 @@ export default function StylePanel() {
<SelectedShapeStyles /> <SelectedShapeStyles />
) : ( ) : (
<> <>
<QuickColorSelect prop="stroke" colors={strokeColors} /> <QuickColorSelect />
<QuickSizeSelect />
<QuickdashSelect />
<IconButton <IconButton
title="Style" title="Style"
size="small"
onClick={() => state.send('TOGGLED_STYLE_PANEL_OPEN')} onClick={() => state.send('TOGGLED_STYLE_PANEL_OPEN')}
> >
<DotsVerticalIcon /> <ChevronDown />
</IconButton> </IconButton>
</> </>
)} )}
@ -61,32 +62,11 @@ export default function StylePanel() {
) )
} }
function QuickColorSelect({
prop,
colors,
}: {
prop: ShapeStyles['fill'] | ShapeStyles['stroke']
colors: Record<string, string>
}) {
const value = useSelector((s) => s.values.selectedStyle[prop])
return (
<ColorPicker
colors={colors}
onChange={(color) => state.send('CHANGED_STYLE', { [prop]: color })}
>
<CurrentColor size="icon" title={prop}>
<ColorIcon color={value} />
</CurrentColor>
</ColorPicker>
)
}
// This panel is going to be hard to keep cool, as we're selecting computed // This panel is going to be hard to keep cool, as we're selecting computed
// information, based on the user's current selection. We might have to keep // information, based on the user's current selection. We might have to keep
// track of this data manually within our state. // track of this data manually within our state.
function SelectedShapeStyles({}: {}) { function SelectedShapeStyles() {
const selectedIds = useSelector( const selectedIds = useSelector(
(s) => Array.from(s.data.selectedIds.values()), (s) => Array.from(s.data.selectedIds.values()),
deepCompareArrays deepCompareArrays
@ -115,42 +95,25 @@ function SelectedShapeStyles({}: {}) {
<Panel.Layout> <Panel.Layout>
<Panel.Header side="right"> <Panel.Header side="right">
<h3>Style</h3> <h3>Style</h3>
<IconButton onClick={() => state.send('TOGGLED_STYLE_PANEL_OPEN')}> <IconButton
size="small"
onClick={() => state.send('TOGGLED_STYLE_PANEL_OPEN')}
>
<X /> <X />
</IconButton> </IconButton>
</Panel.Header> </Panel.Header>
<Content> <Content>
<ColorPicker <ColorPicker
colors={strokeColors} color={commonStyle.color}
onChange={(color) => onChange={(color) => state.send('CHANGED_STYLE', { color })}
state.send('CHANGED_STYLE', { />
stroke: strokeColors[color], <IsFilledPicker
fill: getFillColor(color), isFilled={commonStyle.isFilled}
}) onChange={(isFilled) => state.send('CHANGED_STYLE', { isFilled })}
} />
>
<CurrentColor>
<label>Color</label>
<ColorIcon color={commonStyle.stroke} />
</CurrentColor>
</ColorPicker>
{/* <Row>
<label htmlFor="filled">Filled</label>
<StyledCheckbox
checked={commonStyle.isFilled}
onCheckedChange={(e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value)
state.send('CHANGED_STYLE', {
isFilled: e.target.value === 'on',
})
}}
>
<Checkbox.Indicator as={CheckIcon} />
</StyledCheckbox>
</Row>*/}
<Row> <Row>
<label htmlFor="width">Width</label> <label htmlFor="size">Size</label>
<WidthPicker strokeWidth={Number(commonStyle.strokeWidth)} /> <SizePicker size={commonStyle.size} />
</Row> </Row>
<Row> <Row>
<label htmlFor="dash">Dash</label> <label htmlFor="dash">Dash</label>
@ -159,30 +122,35 @@ function SelectedShapeStyles({}: {}) {
<ButtonsRow> <ButtonsRow>
<IconButton <IconButton
disabled={!hasSelection} disabled={!hasSelection}
size="small"
onClick={() => state.send('DUPLICATED')} onClick={() => state.send('DUPLICATED')}
> >
<CopyIcon /> <CopyIcon />
</IconButton> </IconButton>
<IconButton <IconButton
disabled={!hasSelection} disabled={!hasSelection}
size="small"
onClick={() => state.send('ROTATED_CCW')} onClick={() => state.send('ROTATED_CCW')}
> >
<RotateCounterClockwiseIcon /> <RotateCounterClockwiseIcon />
</IconButton> </IconButton>
<IconButton <IconButton
disabled={!hasSelection} disabled={!hasSelection}
size="small"
onClick={() => state.send('TOGGLED_SHAPE_HIDE')} onClick={() => state.send('TOGGLED_SHAPE_HIDE')}
> >
{isAllHidden ? <EyeClosedIcon /> : <EyeOpenIcon />} {isAllHidden ? <EyeClosedIcon /> : <EyeOpenIcon />}
</IconButton> </IconButton>
<IconButton <IconButton
disabled={!hasSelection} disabled={!hasSelection}
size="small"
onClick={() => state.send('TOGGLED_SHAPE_LOCK')} onClick={() => state.send('TOGGLED_SHAPE_LOCK')}
> >
{isAllLocked ? <LockClosedIcon /> : <LockOpen1Icon />} {isAllLocked ? <LockClosedIcon /> : <LockOpen1Icon />}
</IconButton> </IconButton>
<IconButton <IconButton
disabled={!hasSelection} disabled={!hasSelection}
size="small"
onClick={() => state.send('TOGGLED_SHAPE_ASPECT_LOCK')} onClick={() => state.send('TOGGLED_SHAPE_ASPECT_LOCK')}
> >
{isAllAspectLocked ? <AspectRatioIcon /> : <BoxIcon />} {isAllAspectLocked ? <AspectRatioIcon /> : <BoxIcon />}
@ -191,30 +159,35 @@ function SelectedShapeStyles({}: {}) {
<ButtonsRow> <ButtonsRow>
<IconButton <IconButton
disabled={!hasSelection} disabled={!hasSelection}
size="small"
onClick={() => state.send('MOVED', { type: MoveType.ToBack })} onClick={() => state.send('MOVED', { type: MoveType.ToBack })}
> >
<PinBottomIcon /> <PinBottomIcon />
</IconButton> </IconButton>
<IconButton <IconButton
disabled={!hasSelection} disabled={!hasSelection}
size="small"
onClick={() => state.send('MOVED', { type: MoveType.Backward })} onClick={() => state.send('MOVED', { type: MoveType.Backward })}
> >
<ArrowDownIcon /> <ArrowDownIcon />
</IconButton> </IconButton>
<IconButton <IconButton
disabled={!hasSelection} disabled={!hasSelection}
size="small"
onClick={() => state.send('MOVED', { type: MoveType.Forward })} onClick={() => state.send('MOVED', { type: MoveType.Forward })}
> >
<ArrowUpIcon /> <ArrowUpIcon />
</IconButton> </IconButton>
<IconButton <IconButton
disabled={!hasSelection} disabled={!hasSelection}
size="small"
onClick={() => state.send('MOVED', { type: MoveType.ToFront })} onClick={() => state.send('MOVED', { type: MoveType.ToFront })}
> >
<PinTopIcon /> <PinTopIcon />
</IconButton> </IconButton>
<IconButton <IconButton
disabled={!hasSelection} disabled={!hasSelection}
size="small"
onClick={() => state.send('DELETED')} onClick={() => state.send('DELETED')}
> >
<Trash2 /> <Trash2 />
@ -236,7 +209,7 @@ const StylePanelRoot = styled(Panel.Root, {
overflow: 'hidden', overflow: 'hidden',
position: 'relative', position: 'relative',
border: '1px solid $panel', border: '1px solid $panel',
boxShadow: '0px 2px 4px rgba(0,0,0,.12)', boxShadow: '0px 2px 4px rgba(0,0,0,.2)',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
pointerEvents: 'all', pointerEvents: 'all',
@ -262,7 +235,6 @@ const Row = styled('div', {
width: '100%', width: '100%',
background: 'none', background: 'none',
border: 'none', border: 'none',
cursor: 'pointer',
outline: 'none', outline: 'none',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
@ -293,22 +265,3 @@ const ButtonsRow = styled('div', {
justifyContent: 'flex-start', justifyContent: 'flex-start',
padding: 4, padding: 4,
}) })
const StyledCheckbox = styled(Checkbox.Root, {
appearance: 'none',
backgroundColor: 'transparent',
border: 'none',
padding: 0,
boxShadow: 'inset 0 0 0 1px gainsboro',
width: 15,
height: 15,
borderRadius: 2,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
'&:focus': {
outline: 'none',
boxShadow: 'inset 0 0 0 1px dodgerblue, 0 0 0 1px dodgerblue',
},
})

View file

@ -1,30 +0,0 @@
import { Group, RadioItem } from './shared'
import { ChangeEvent } from 'react'
import { Circle } from 'react-feather'
import state from 'state'
function handleChange(e: ChangeEvent<HTMLInputElement>) {
state.send('CHANGED_STYLE', {
strokeWidth: Number(e.currentTarget.value),
})
}
export default function WidthPicker({
strokeWidth = 2,
}: {
strokeWidth?: number
}) {
return (
<Group name="width" onValueChange={handleChange}>
<RadioItem value="2" isActive={strokeWidth === 2}>
<Circle size={6} />
</RadioItem>
<RadioItem value="4" isActive={strokeWidth === 4}>
<Circle size={12} />
</RadioItem>
<RadioItem value="8" isActive={strokeWidth === 8}>
<Circle size={22} />
</RadioItem>
</Group>
)
}

View file

@ -106,7 +106,7 @@ export default function ToolsPanel() {
> >
<CircleIcon /> <CircleIcon />
</IconButton> */} </IconButton> */}
<IconButton {/* <IconButton
name={ShapeType.Line} name={ShapeType.Line}
size={{ '@sm': 'small', '@md': 'large' }} size={{ '@sm': 'small', '@md': 'large' }}
onClick={selectLineTool} onClick={selectLineTool}
@ -129,7 +129,7 @@ export default function ToolsPanel() {
isActive={activeTool === ShapeType.Dot} isActive={activeTool === ShapeType.Dot}
> >
<DotIcon /> <DotIcon />
</IconButton> </IconButton> */}
</Container> </Container>
<Container> <Container>
<IconButton <IconButton

View file

@ -2,6 +2,7 @@ import CodeShape from './index'
import { v4 as uuid } from 'uuid' import { v4 as uuid } from 'uuid'
import { CircleShape, ShapeType } from 'types' import { CircleShape, ShapeType } from 'types'
import { vectorToPoint } from 'utils/utils' import { vectorToPoint } from 'utils/utils'
import { defaultStyle } from 'lib/shape-styles'
export default class Circle extends CodeShape<CircleShape> { export default class Circle extends CodeShape<CircleShape> {
constructor(props = {} as Partial<CircleShape>) { constructor(props = {} as Partial<CircleShape>) {
@ -20,11 +21,7 @@ export default class Circle extends CodeShape<CircleShape> {
isAspectRatioLocked: false, isAspectRatioLocked: false,
isLocked: false, isLocked: false,
isHidden: false, isHidden: false,
style: { style: defaultStyle,
fill: '#c6cacb',
stroke: '#000',
strokeWidth: 1,
},
...props, ...props,
}) })
} }

View file

@ -2,6 +2,7 @@ import CodeShape from './index'
import { v4 as uuid } from 'uuid' import { v4 as uuid } from 'uuid'
import { DotShape, ShapeType } from 'types' import { DotShape, ShapeType } from 'types'
import { vectorToPoint } from 'utils/utils' import { vectorToPoint } from 'utils/utils'
import { defaultStyle } from 'lib/shape-styles'
export default class Dot extends CodeShape<DotShape> { export default class Dot extends CodeShape<DotShape> {
constructor(props = {} as Partial<DotShape>) { constructor(props = {} as Partial<DotShape>) {
@ -19,12 +20,12 @@ export default class Dot extends CodeShape<DotShape> {
isAspectRatioLocked: false, isAspectRatioLocked: false,
isLocked: false, isLocked: false,
isHidden: false, isHidden: false,
style: {
fill: '#c6cacb',
stroke: '#000',
strokeWidth: 1,
},
...props, ...props,
style: {
...defaultStyle,
...props.style,
isFilled: false,
},
}) })
} }

View file

@ -2,6 +2,7 @@ import CodeShape from './index'
import { v4 as uuid } from 'uuid' import { v4 as uuid } from 'uuid'
import { EllipseShape, ShapeType } from 'types' import { EllipseShape, ShapeType } from 'types'
import { vectorToPoint } from 'utils/utils' import { vectorToPoint } from 'utils/utils'
import { defaultStyle } from 'lib/shape-styles'
export default class Ellipse extends CodeShape<EllipseShape> { export default class Ellipse extends CodeShape<EllipseShape> {
constructor(props = {} as Partial<EllipseShape>) { constructor(props = {} as Partial<EllipseShape>) {
@ -21,11 +22,7 @@ export default class Ellipse extends CodeShape<EllipseShape> {
isAspectRatioLocked: false, isAspectRatioLocked: false,
isLocked: false, isLocked: false,
isHidden: false, isHidden: false,
style: { style: defaultStyle,
fill: '#c6cacb',
stroke: '#000',
strokeWidth: 1,
},
...props, ...props,
}) })
} }

View file

@ -2,6 +2,7 @@ import CodeShape from './index'
import { v4 as uuid } from 'uuid' import { v4 as uuid } from 'uuid'
import { LineShape, ShapeType } from 'types' import { LineShape, ShapeType } from 'types'
import { vectorToPoint } from 'utils/utils' import { vectorToPoint } from 'utils/utils'
import { defaultStyle } from 'lib/shape-styles'
export default class Line extends CodeShape<LineShape> { export default class Line extends CodeShape<LineShape> {
constructor(props = {} as Partial<LineShape>) { constructor(props = {} as Partial<LineShape>) {
@ -21,12 +22,12 @@ export default class Line extends CodeShape<LineShape> {
isAspectRatioLocked: false, isAspectRatioLocked: false,
isLocked: false, isLocked: false,
isHidden: false, isHidden: false,
style: {
fill: '#c6cacb',
stroke: '#000',
strokeWidth: 1,
},
...props, ...props,
style: {
...defaultStyle,
...props.style,
isFilled: false,
},
}) })
} }

View file

@ -2,6 +2,7 @@ import CodeShape from './index'
import { v4 as uuid } from 'uuid' import { v4 as uuid } from 'uuid'
import { PolylineShape, ShapeType } from 'types' import { PolylineShape, ShapeType } from 'types'
import { vectorToPoint } from 'utils/utils' import { vectorToPoint } from 'utils/utils'
import { defaultStyle } from 'lib/shape-styles'
export default class Polyline extends CodeShape<PolylineShape> { export default class Polyline extends CodeShape<PolylineShape> {
constructor(props = {} as Partial<PolylineShape>) { constructor(props = {} as Partial<PolylineShape>) {
@ -21,11 +22,7 @@ export default class Polyline extends CodeShape<PolylineShape> {
isAspectRatioLocked: false, isAspectRatioLocked: false,
isLocked: false, isLocked: false,
isHidden: false, isHidden: false,
style: { style: defaultStyle,
fill: 'none',
stroke: '#000',
strokeWidth: 1,
},
...props, ...props,
}) })
} }

View file

@ -2,6 +2,7 @@ import CodeShape from './index'
import { v4 as uuid } from 'uuid' import { v4 as uuid } from 'uuid'
import { RayShape, ShapeType } from 'types' import { RayShape, ShapeType } from 'types'
import { vectorToPoint } from 'utils/utils' import { vectorToPoint } from 'utils/utils'
import { defaultStyle } from 'lib/shape-styles'
export default class Ray extends CodeShape<RayShape> { export default class Ray extends CodeShape<RayShape> {
constructor(props = {} as Partial<RayShape>) { constructor(props = {} as Partial<RayShape>) {
@ -21,12 +22,12 @@ export default class Ray extends CodeShape<RayShape> {
isAspectRatioLocked: false, isAspectRatioLocked: false,
isLocked: false, isLocked: false,
isHidden: false, isHidden: false,
style: {
fill: '#c6cacb',
stroke: '#000',
strokeWidth: 1,
},
...props, ...props,
style: {
...defaultStyle,
...props.style,
isFilled: false,
},
}) })
} }

View file

@ -2,6 +2,7 @@ import CodeShape from './index'
import { v4 as uuid } from 'uuid' import { v4 as uuid } from 'uuid'
import { RectangleShape, ShapeType } from 'types' import { RectangleShape, ShapeType } from 'types'
import { vectorToPoint } from 'utils/utils' import { vectorToPoint } from 'utils/utils'
import { defaultStyle } from 'lib/shape-styles'
export default class Rectangle extends CodeShape<RectangleShape> { export default class Rectangle extends CodeShape<RectangleShape> {
constructor(props = {} as Partial<RectangleShape>) { constructor(props = {} as Partial<RectangleShape>) {
@ -22,11 +23,7 @@ export default class Rectangle extends CodeShape<RectangleShape> {
isAspectRatioLocked: false, isAspectRatioLocked: false,
isLocked: false, isLocked: false,
isHidden: false, isHidden: false,
style: { style: defaultStyle,
fill: '#c6cacb',
stroke: '#000',
strokeWidth: 1,
},
...props, ...props,
}) })
} }

View file

@ -1,38 +0,0 @@
export const shades = {
none: 'none',
white: 'rgba(248, 249, 250, 1.000)',
lightGray: 'rgba(224, 226, 230, 1.000)',
gray: 'rgba(172, 181, 189, 1.000)',
darkGray: 'rgba(52, 58, 64, 1.000)',
black: 'rgba(0,0,0, 1.000)',
}
export const strokes = {
lime: 'rgba(115, 184, 23, 1.000)',
green: 'rgba(54, 178, 77, 1.000)',
teal: 'rgba(9, 167, 120, 1.000)',
cyan: 'rgba(14, 152, 173, 1.000)',
blue: 'rgba(28, 126, 214, 1.000)',
indigo: 'rgba(66, 99, 235, 1.000)',
violet: 'rgba(112, 72, 232, 1.000)',
grape: 'rgba(174, 62, 200, 1.000)',
pink: 'rgba(214, 51, 108, 1.000)',
red: 'rgba(240, 63, 63, 1.000)',
orange: 'rgba(247, 103, 6, 1.000)',
yellow: 'rgba(245, 159, 0, 1.000)',
}
export const fills = {
lime: 'rgba(243, 252, 227, 1.000)',
green: 'rgba(235, 251, 238, 1.000)',
teal: 'rgba(230, 252, 245, 1.000)',
cyan: 'rgba(227, 250, 251, 1.000)',
blue: 'rgba(231, 245, 255, 1.000)',
indigo: 'rgba(237, 242, 255, 1.000)',
violet: 'rgba(242, 240, 255, 1.000)',
grape: 'rgba(249, 240, 252, 1.000)',
pink: 'rgba(254, 241, 246, 1.000)',
red: 'rgba(255, 245, 245, 1.000)',
orange: 'rgba(255, 244, 229, 1.000)',
yellow: 'rgba(255, 249, 219, 1.000)',
}

83
lib/shape-styles.ts Normal file
View file

@ -0,0 +1,83 @@
import { SVGProps } from 'react'
import { ColorStyle, DashStyle, Shape, ShapeStyles, SizeStyle } from 'types'
export const strokes: Record<ColorStyle, string> = {
[ColorStyle.White]: 'rgba(248, 249, 250, 1.000)',
[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)',
}
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.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)',
}
const strokeWidths = {
[SizeStyle.Small]: 2,
[SizeStyle.Medium]: 4,
[SizeStyle.Large]: 8,
}
const dashArrays = {
[DashStyle.Solid]: () => 'none',
[DashStyle.Dashed]: (sw: number) => `${sw} ${sw * 2}`,
[DashStyle.Dotted]: (sw: number) => `0 ${sw * 1.5}`,
}
function getStrokeWidth(size: SizeStyle) {
return strokeWidths[size]
}
function getStrokeDashArray(dash: DashStyle, strokeWidth: number) {
return dashArrays[dash](strokeWidth)
}
export function getShapeStyle(
style: ShapeStyles
): Partial<SVGProps<SVGUseElement>> {
const { color, size, dash, isFilled } = style
const strokeWidth = getStrokeWidth(size)
const strokeDasharray = getStrokeDashArray(dash, strokeWidth)
return {
stroke: strokes[color],
fill: isFilled ? fills[color] : 'none',
strokeWidth,
strokeDasharray,
}
}
export const defaultStyle = {
color: ColorStyle.Black,
size: SizeStyle.Medium,
isFilled: false,
dash: DashStyle.Solid,
}

View file

@ -1,7 +1,14 @@
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 * as svg from 'utils/svg' import * as svg from 'utils/svg'
import { ArrowShape, ShapeHandle, ShapeType } from 'types' import {
ArrowShape,
ColorStyle,
DashStyle,
ShapeHandle,
ShapeType,
SizeStyle,
} from 'types'
import { registerShapeUtils } from './index' import { registerShapeUtils } from './index'
import { circleFromThreePoints, clamp, isAngleBetween } from 'utils/utils' import { circleFromThreePoints, clamp, isAngleBetween } from 'utils/utils'
import { pointInBounds } from 'utils/bounds' import { pointInBounds } from 'utils/bounds'
@ -11,6 +18,7 @@ import {
} from 'utils/intersections' } from 'utils/intersections'
import { getBoundsFromPoints, translateBounds } from 'utils/utils' import { getBoundsFromPoints, translateBounds } from 'utils/utils'
import { pointInCircle } from 'utils/hitTests' import { pointInCircle } from 'utils/hitTests'
import { defaultStyle, getShapeStyle } from 'lib/shape-styles'
const ctpCache = new WeakMap<ArrowShape['handles'], number[]>() const ctpCache = new WeakMap<ArrowShape['handles'], number[]>()
@ -77,21 +85,23 @@ const arrow = registerShapeUtils<ArrowShape>({
}, },
...props, ...props,
style: { style: {
strokeWidth: 2, ...defaultStyle,
...props.style, ...props.style,
fill: 'none', isFilled: false,
}, },
} }
}, },
render(shape) { render(shape) {
const { id, bend, points, handles, style } = shape const { id, bend, points, handles } = shape
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 bendDist = arrowDist * bend
const showCircle = Math.abs(bendDist) > 20 const showCircle = Math.abs(bendDist) > 20
const style = getShapeStyle(shape.style)
// 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 angle = showCircle ? bend * (Math.PI * 0.48) : 0
@ -145,7 +155,7 @@ const arrow = registerShapeUtils<ArrowShape>({
applyStyles(shape, style) { applyStyles(shape, style) {
Object.assign(shape.style, style) Object.assign(shape.style, style)
shape.style.fill = 'none' shape.style.isFilled = false
return this return this
}, },

View file

@ -1,11 +1,12 @@
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 { CircleShape, ShapeType } from 'types' import { CircleShape, ColorStyle, DashStyle, ShapeType, SizeStyle } from 'types'
import { registerShapeUtils } from './index' import { registerShapeUtils } from './index'
import { boundsContained } from 'utils/bounds' import { boundsContained } from 'utils/bounds'
import { intersectCircleBounds } from 'utils/intersections' import { intersectCircleBounds } from 'utils/intersections'
import { pointInCircle } from 'utils/hitTests' import { pointInCircle } from 'utils/hitTests'
import { translateBounds } from 'utils/utils' import { translateBounds } from 'utils/utils'
import { defaultStyle, getShapeStyle } from 'lib/shape-styles'
const circle = registerShapeUtils<CircleShape>({ const circle = registerShapeUtils<CircleShape>({
boundsCache: new WeakMap([]), boundsCache: new WeakMap([]),
@ -24,21 +25,20 @@ const circle = registerShapeUtils<CircleShape>({
isAspectRatioLocked: false, isAspectRatioLocked: false,
isLocked: false, isLocked: false,
isHidden: false, isHidden: false,
style: { style: defaultStyle,
fill: '#c6cacb',
stroke: '#000',
},
...props, ...props,
} }
}, },
render({ id, radius, style }) { render({ id, radius, style }) {
const styles = getShapeStyle(style)
return ( return (
<circle <circle
id={id} id={id}
cx={radius} cx={radius}
cy={radius} cy={radius}
r={Math.max(0, radius - Number(style.strokeWidth) / 2)} r={Math.max(0, radius - Number(styles.strokeWidth) / 2)}
/> />
) )
}, },

View file

@ -6,6 +6,7 @@ import { boundsContained } from 'utils/bounds'
import { intersectCircleBounds } from 'utils/intersections' import { intersectCircleBounds } from 'utils/intersections'
import { DotCircle } from 'components/canvas/misc' import { DotCircle } from 'components/canvas/misc'
import { translateBounds } from 'utils/utils' import { translateBounds } from 'utils/utils'
import { defaultStyle } from 'lib/shape-styles'
const dot = registerShapeUtils<DotShape>({ const dot = registerShapeUtils<DotShape>({
boundsCache: new WeakMap([]), boundsCache: new WeakMap([]),
@ -23,11 +24,12 @@ const dot = registerShapeUtils<DotShape>({
isAspectRatioLocked: false, isAspectRatioLocked: false,
isLocked: false, isLocked: false,
isHidden: false, isHidden: false,
style: {
fill: '#c6cacb',
strokeWidth: '0',
},
...props, ...props,
style: {
...defaultStyle,
...props.style,
isFilled: false,
},
} }
}, },

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 { DrawShape, ShapeType } from 'types' import { DashStyle, DrawShape, 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'
@ -12,6 +12,7 @@ import {
translateBounds, translateBounds,
} from 'utils/utils' } from 'utils/utils'
import styled from 'styles' import styled from 'styles'
import { defaultStyle, getShapeStyle } from 'lib/shape-styles'
const pathCache = new WeakMap<DrawShape['points'], string>([]) const pathCache = new WeakMap<DrawShape['points'], string>([])
@ -34,11 +35,9 @@ const draw = registerShapeUtils<DrawShape>({
isHidden: false, isHidden: false,
...props, ...props,
style: { style: {
strokeWidth: 2, ...defaultStyle,
strokeLinecap: 'round',
strokeLinejoin: 'round',
...props.style, ...props.style,
fill: props.style.stroke, isFilled: false,
}, },
} }
}, },
@ -46,12 +45,14 @@ const draw = registerShapeUtils<DrawShape>({
render(shape) { render(shape) {
const { id, points, style } = shape const { id, points, style } = shape
const styles = getShapeStyle(style)
if (!pathCache.has(points)) { if (!pathCache.has(points)) {
pathCache.set( pathCache.set(
points, points,
getSvgPathFromStroke( getSvgPathFromStroke(
getStroke(points, { getStroke(points, {
size: +style.strokeWidth * 2, size: +styles.strokeWidth * 2,
thinning: 0.9, thinning: 0.9,
end: { taper: 100 }, end: { taper: 100 },
start: { taper: 40 }, start: { taper: 40 },
@ -61,15 +62,18 @@ const draw = registerShapeUtils<DrawShape>({
} }
if (points.length < 2) { if (points.length < 2) {
return <circle id={id} r={+style.strokeWidth * 0.618} /> return (
<circle id={id} r={+styles.strokeWidth * 0.618} fill={styles.stroke} />
)
} }
return <path id={id} d={pathCache.get(points)} /> return <path id={id} d={pathCache.get(points)} fill={styles.stroke} />
}, },
applyStyles(shape, style) { applyStyles(shape, style) {
Object.assign(shape.style, style) Object.assign(shape.style, style)
shape.style.fill = shape.style.stroke shape.style.isFilled = false
shape.style.dash = DashStyle.Solid
return this return this
}, },
@ -106,19 +110,11 @@ const draw = registerShapeUtils<DrawShape>({
hitTest(shape, point) { hitTest(shape, point) {
let pt = vec.sub(point, shape.point) let pt = vec.sub(point, shape.point)
let prev = shape.points[0] const min = +getShapeStyle(shape.style).strokeWidth
return shape.points.some(
for (let i = 1; i < shape.points.length; i++) { (curr, i) =>
let curr = shape.points[i] i > 0 && vec.distanceToLineSegment(shape.points[i - 1], curr, pt) < min
if ( )
vec.distanceToLineSegment(prev, curr, pt) < +shape.style.strokeWidth
) {
return true
}
prev = curr
}
return false
}, },
hitTestBounds(this, shape, brushBounds) { hitTestBounds(this, shape, brushBounds) {

View file

@ -11,6 +11,7 @@ import {
rotateBounds, rotateBounds,
translateBounds, translateBounds,
} from 'utils/utils' } from 'utils/utils'
import { defaultStyle, getShapeStyle } from 'lib/shape-styles'
const ellipse = registerShapeUtils<EllipseShape>({ const ellipse = registerShapeUtils<EllipseShape>({
boundsCache: new WeakMap([]), boundsCache: new WeakMap([]),
@ -30,22 +31,20 @@ const ellipse = registerShapeUtils<EllipseShape>({
isAspectRatioLocked: false, isAspectRatioLocked: false,
isLocked: false, isLocked: false,
isHidden: false, isHidden: false,
style: { style: defaultStyle,
fill: '#c6cacb',
stroke: '#000',
},
...props, ...props,
} }
}, },
render({ id, radiusX, radiusY, style }) { render({ id, radiusX, radiusY, style }) {
const styles = getShapeStyle(style)
return ( return (
<ellipse <ellipse
id={id} id={id}
cx={radiusX} cx={radiusX}
cy={radiusY} cy={radiusY}
rx={Math.max(0, radiusX - Number(style.strokeWidth) / 2)} rx={Math.max(0, radiusX - Number(styles.strokeWidth) / 2)}
ry={Math.max(0, radiusY - Number(style.strokeWidth) / 2)} ry={Math.max(0, radiusY - Number(styles.strokeWidth) / 2)}
/> />
) )
}, },

View file

@ -48,7 +48,7 @@ export interface ShapeUtility<K extends Readonly<Shape>> {
applyStyles( applyStyles(
this: ShapeUtility<K>, this: ShapeUtility<K>,
shape: K, shape: K,
style: ShapeStyles style: Partial<ShapeStyles>
): ShapeUtility<K> ): ShapeUtility<K>
// Set the shape's point. // Set the shape's point.

View file

@ -7,6 +7,7 @@ import { intersectCircleBounds } from 'utils/intersections'
import { DotCircle, ThinLine } from 'components/canvas/misc' import { DotCircle, ThinLine } from 'components/canvas/misc'
import { translateBounds } from 'utils/utils' import { translateBounds } from 'utils/utils'
import styled from 'styles' import styled from 'styles'
import { defaultStyle } from 'lib/shape-styles'
const line = registerShapeUtils<LineShape>({ const line = registerShapeUtils<LineShape>({
boundsCache: new WeakMap([]), boundsCache: new WeakMap([]),
@ -25,11 +26,12 @@ const line = registerShapeUtils<LineShape>({
isAspectRatioLocked: false, isAspectRatioLocked: false,
isLocked: false, isLocked: false,
isHidden: false, isHidden: false,
style: {
fill: '#c6cacb',
stroke: '#000',
},
...props, ...props,
style: {
...defaultStyle,
...props.style,
isFilled: false,
},
} }
}, },

View file

@ -5,6 +5,7 @@ import { registerShapeUtils } from './index'
import { intersectPolylineBounds } from 'utils/intersections' import { intersectPolylineBounds } from 'utils/intersections'
import { boundsContainPolygon } from 'utils/bounds' import { boundsContainPolygon } from 'utils/bounds'
import { getBoundsFromPoints, translateBounds } from 'utils/utils' import { getBoundsFromPoints, translateBounds } from 'utils/utils'
import { defaultStyle } from 'lib/shape-styles'
const polyline = registerShapeUtils<PolylineShape>({ const polyline = registerShapeUtils<PolylineShape>({
boundsCache: new WeakMap([]), boundsCache: new WeakMap([]),
@ -23,11 +24,7 @@ const polyline = registerShapeUtils<PolylineShape>({
isAspectRatioLocked: false, isAspectRatioLocked: false,
isLocked: false, isLocked: false,
isHidden: false, isHidden: false,
style: { style: defaultStyle,
strokeWidth: 2,
strokeLinecap: 'round',
strokeLinejoin: 'round',
},
...props, ...props,
} }
}, },

View file

@ -6,6 +6,7 @@ import { boundsContained } from 'utils/bounds'
import { intersectCircleBounds } from 'utils/intersections' import { intersectCircleBounds } from 'utils/intersections'
import { DotCircle, ThinLine } from 'components/canvas/misc' import { DotCircle, ThinLine } from 'components/canvas/misc'
import { translateBounds } from 'utils/utils' import { translateBounds } from 'utils/utils'
import { defaultStyle } from 'lib/shape-styles'
const ray = registerShapeUtils<RayShape>({ const ray = registerShapeUtils<RayShape>({
boundsCache: new WeakMap([]), boundsCache: new WeakMap([]),
@ -24,12 +25,12 @@ const ray = registerShapeUtils<RayShape>({
isAspectRatioLocked: false, isAspectRatioLocked: false,
isLocked: false, isLocked: false,
isHidden: false, isHidden: false,
style: {
fill: '#c6cacb',
stroke: '#000',
strokeWidth: 1,
},
...props, ...props,
style: {
...defaultStyle,
...props.style,
isFilled: false,
},
} }
}, },

View file

@ -8,6 +8,7 @@ import {
getRotatedCorners, getRotatedCorners,
translateBounds, translateBounds,
} from 'utils/utils' } from 'utils/utils'
import { defaultStyle, getShapeStyle } from 'lib/shape-styles'
const rectangle = registerShapeUtils<RectangleShape>({ const rectangle = registerShapeUtils<RectangleShape>({
boundsCache: new WeakMap([]), boundsCache: new WeakMap([]),
@ -27,23 +28,21 @@ const rectangle = registerShapeUtils<RectangleShape>({
isAspectRatioLocked: false, isAspectRatioLocked: false,
isLocked: false, isLocked: false,
isHidden: false, isHidden: false,
style: { style: defaultStyle,
fill: '#c6cacb',
stroke: '#000',
},
...props, ...props,
} }
}, },
render({ id, size, radius, style }) { render({ id, size, radius, style }) {
const styles = getShapeStyle(style)
return ( return (
<g id={id}> <g id={id}>
<rect <rect
id={id} id={id}
rx={radius} rx={radius}
ry={radius} ry={radius}
width={Math.max(0, size[0] - Number(style.strokeWidth) / 2)} width={Math.max(0, size[0] - Number(styles.strokeWidth) / 2)}
height={Math.max(0, size[1] - Number(style.strokeWidth) / 2)} height={Math.max(0, size[1] - Number(styles.strokeWidth) / 2)}
/> />
</g> </g>
) )

View file

@ -1,19 +1,19 @@
import Command from "./command" import Command from './command'
import history from "../history" import history from '../history'
import { Data, ShapeStyles } from "types" import { Data, ShapeStyles } from 'types'
import { getPage, getSelectedShapes } from "utils/utils" import { getPage, getSelectedShapes } from 'utils/utils'
import { getShapeUtils } from "lib/shape-utils" import { getShapeUtils } from 'lib/shape-utils'
import { current } from "immer" import { current } from 'immer'
export default function styleCommand(data: Data, styles: ShapeStyles) { export default function styleCommand(data: Data, styles: Partial<ShapeStyles>) {
const { currentPageId } = data const { currentPageId } = data
const initialShapes = getSelectedShapes(current(data)) const initialShapes = getSelectedShapes(current(data))
history.execute( history.execute(
data, data,
new Command({ new Command({
name: "changed_style", name: 'changed_style',
category: "canvas", category: 'canvas',
manualSelection: true, manualSelection: true,
do(data) { do(data) {
const { shapes } = getPage(data, currentPageId) const { shapes } = getPage(data, currentPageId)

View file

@ -1,6 +1,5 @@
import { Data, ShapeType } from 'types' import { Data, ShapeType } from 'types'
import shapeUtils from 'lib/shape-utils' import shapeUtils from 'lib/shape-utils'
import { shades } from 'lib/colors'
export const defaultDocument: Data['document'] = { export const defaultDocument: Data['document'] = {
pages: { pages: {
@ -10,23 +9,22 @@ export const defaultDocument: Data['document'] = {
name: 'Page 0', name: 'Page 0',
childIndex: 0, childIndex: 0,
shapes: { shapes: {
arrowShape0: shapeUtils[ShapeType.Arrow].create({ // arrowShape0: shapeUtils[ShapeType.Arrow].create({
id: 'arrowShape0', // id: 'arrowShape0',
point: [200, 200], // point: [200, 200],
points: [ // points: [
[0, 0], // [0, 0],
[200, 200], // [200, 200],
], // ],
}), // }),
arrowShape1: shapeUtils[ShapeType.Arrow].create({ // arrowShape1: shapeUtils[ShapeType.Arrow].create({
id: 'arrowShape1', // id: 'arrowShape1',
point: [100, 100], // point: [100, 100],
points: [ // points: [
[0, 0], // [0, 0],
[300, 0], // [300, 0],
], // ],
}), // }),
// shape3: shapeUtils[ShapeType.Dot].create({ // shape3: shapeUtils[ShapeType.Dot].create({
// id: 'shape3', // id: 'shape3',
// name: 'Shape 3', // name: 'Shape 3',

View file

@ -117,11 +117,11 @@ export default class BrushSession extends BaseSession {
export function getDrawSnapshot(data: Data, shapeId: string) { export function getDrawSnapshot(data: Data, shapeId: string) {
const page = getPage(current(data)) const page = getPage(current(data))
const { points, style } = page.shapes[shapeId] as DrawShape const { points } = page.shapes[shapeId] as DrawShape
return { return {
id: shapeId, id: shapeId,
points, points,
strokeWidth: style.strokeWidth,
} }
} }

View file

@ -2,7 +2,6 @@ import { createSelectorHook, createState } from '@state-designer/react'
import * as vec from 'utils/vec' import * as vec from 'utils/vec'
import inputs from './inputs' import inputs from './inputs'
import { defaultDocument } from './data' import { defaultDocument } from './data'
import { shades } from 'lib/colors'
import { createShape, getShapeUtils } from 'lib/shape-utils' import { createShape, getShapeUtils } from 'lib/shape-utils'
import history from 'state/history' import history from 'state/history'
import * as Sessions from './sessions' import * as Sessions from './sessions'
@ -15,7 +14,6 @@ import {
getCurrent, getCurrent,
getPage, getPage,
getSelectedBounds, getSelectedBounds,
getSelectedShapes,
getShape, getShape,
screenToWorld, screenToWorld,
setZoomCSS, setZoomCSS,
@ -34,6 +32,8 @@ import {
AlignType, AlignType,
StretchType, StretchType,
DashStyle, DashStyle,
SizeStyle,
ColorStyle,
} from 'types' } from 'types'
const initialData: Data = { const initialData: Data = {
@ -49,10 +49,10 @@ const initialData: Data = {
nudgeDistanceSmall: 1, nudgeDistanceSmall: 1,
}, },
currentStyle: { currentStyle: {
fill: shades.lightGray, size: SizeStyle.Medium,
stroke: shades.darkGray, color: ColorStyle.Black,
strokeWidth: 2,
dash: DashStyle.Solid, dash: DashStyle.Solid,
isFilled: false,
}, },
camera: { camera: {
point: [0, 0], point: [0, 0],
@ -131,7 +131,7 @@ const state = createState({
SELECTED_RECTANGLE_TOOL: { unless: 'isReadOnly', to: 'rectangle' }, SELECTED_RECTANGLE_TOOL: { unless: 'isReadOnly', to: 'rectangle' },
TOGGLED_CODE_PANEL_OPEN: 'toggleCodePanel', TOGGLED_CODE_PANEL_OPEN: 'toggleCodePanel',
TOGGLED_STYLE_PANEL_OPEN: 'toggleStylePanel', TOGGLED_STYLE_PANEL_OPEN: 'toggleStylePanel',
TOUCHED_CANVAS: 'closeStylePanel', POINTED_CANVAS: 'closeStylePanel',
CHANGED_STYLE: ['updateStyles', 'applyStylesToSelection'], CHANGED_STYLE: ['updateStyles', 'applyStylesToSelection'],
SELECTED_ALL: { to: 'selecting', do: 'selectAll' }, SELECTED_ALL: { to: 'selecting', do: 'selectAll' },
NUDGED: { do: 'nudgeSelection' }, NUDGED: { do: 'nudgeSelection' },
@ -1261,7 +1261,7 @@ const state = createState({
}, },
restoreSavedData(data) { restoreSavedData(data) {
history.load(data) // history.load(data)
}, },
clearBoundsRotation(data) { clearBoundsRotation(data) {
@ -1309,7 +1309,7 @@ const state = createState({
const page = getPage(data) const page = getPage(data)
const shapeStyles = selectedIds.map((id) => page.shapes[id].style) const shapeStyles = selectedIds.map((id) => page.shapes[id].style)
const commonStyle: Partial<ShapeStyles> = {} const commonStyle: ShapeStyles = {} as ShapeStyles
const overrides = new Set<string>([]) const overrides = new Set<string>([])

View file

@ -67,11 +67,49 @@ export enum ShapeType {
// Cubic = "cubic", // Cubic = "cubic",
// Conic = "conic", // Conic = "conic",
export type ShapeStyles = Partial< export enum ColorStyle {
React.SVGProps<SVGUseElement> & { White = 'White',
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',
}
export enum SizeStyle {
Small = 'Small',
Medium = 'Medium',
Large = 'Large',
}
export enum DashStyle {
Solid = 'Solid',
Dashed = 'Dashed',
Dotted = 'Dotted',
}
export type ShapeStyles = {
color: ColorStyle
size: SizeStyle
dash: DashStyle dash: DashStyle
} isFilled: boolean
> }
// export type ShapeStyles = Partial<
// React.SVGProps<SVGUseElement> & {
// dash: DashStyle
// }
// >
export interface BaseShape { export interface BaseShape {
id: string id: string
@ -180,12 +218,6 @@ export enum Decoration {
Arrow = 'Arrow', Arrow = 'Arrow',
} }
export enum DashStyle {
Solid = 'Solid',
Dashed = 'Dashed',
Dotted = 'Dotted',
}
export interface ShapeBinding { export interface ShapeBinding {
id: string id: string
index: number index: number