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}>
<BoundsBg />
<Page />
<Selected />
{/* <Selected /> */}
<Bounds />
<Handles />
<Brush />

View file

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

View file

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

View file

@ -5,12 +5,10 @@ import { getShapeUtils } from 'lib/shape-utils'
import { getPage } from 'utils/utils'
import { DashStyle, ShapeStyles } from 'types'
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 }) {
const isHovered = useSelector((state) => state.data.hoveredId === id)
const isSelected = useSelector((state) => state.values.selectedIds.has(id))
const isSelected = useSelector((s) => s.values.selectedIds.has(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
const center = getShapeUtils(shape).getCenter(shape)
const transform = `
rotate(${shape.rotation * (180 / Math.PI)}, ${center})
translate(${shape.point})
`
const style = getShapeStyle(shape.style)
return (
<StyledGroup
ref={rGroup}
isHovered={isHovered}
isSelected={isSelected}
transform={transform}
stroke={'red'}
strokeWidth={10}
>
<StyledGroup ref={rGroup} isSelected={isSelected} transform={transform}>
{isSelecting && (
<HoverIndicator
as="use"
href={'#' + id}
strokeWidth={+shape.style.strokeWidth + 8}
variant={shape.style.fill === 'none' ? 'hollow' : 'filled'}
strokeWidth={+style.strokeWidth + 4}
variant={getShapeUtils(shape).canStyleFill ? 'filled' : 'hollow'}
{...events}
/>
)}
{!shape.isHidden && (
<RealShape id={id} style={sanitizeStyle(shape.style)} />
)}
{!shape.isHidden && <RealShape id={id} style={style} />}
</StyledGroup>
)
}
const RealShape = memo(({ id, style }: { id: string; style: ShapeStyles }) => {
return (
<StyledShape
as="use"
href={'#' + id}
{...style}
strokeDasharray={getDash(style.dash, +style.strokeWidth)}
/>
)
})
const RealShape = memo(
({ id, style }: { id: string; style: ReturnType<typeof getShapeStyle> }) => {
return <StyledShape as="use" href={'#' + id} {...style} />
}
)
const StyledShape = styled('path', {
strokeLinecap: 'round',
strokeLinejoin: 'round',
pointerEvents: 'none',
})
const HoverIndicator = styled('path', {
fill: 'transparent',
stroke: 'transparent',
stroke: '$selected',
strokeLinecap: 'round',
strokeLinejoin: 'round',
transform: 'all .2s',
fill: 'transparent',
filter: 'url(#expand)',
variants: {
variant: {
hollow: {
@ -90,52 +79,32 @@ const HoverIndicator = styled('path', {
})
const StyledGroup = styled('g', {
pointerEvents: 'none',
[`& ${HoverIndicator}`]: {
opacity: '0',
},
variants: {
isSelected: {
true: {},
false: {},
},
isHovered: {
true: {},
false: {},
},
},
compoundVariants: [
{
isSelected: true,
isHovered: true,
css: {
true: {
[`& ${HoverIndicator}`]: {
opacity: '.4',
stroke: '$selected',
opacity: '0.2',
},
[`&:hover ${HoverIndicator}`]: {
opacity: '0.3',
},
[`&:active ${HoverIndicator}`]: {
opacity: '0.3',
},
},
},
{
isSelected: true,
isHovered: false,
css: {
false: {
[`& ${HoverIndicator}`]: {
opacity: '.2',
stroke: '$selected',
opacity: '0',
},
[`&:hover ${HoverIndicator}`]: {
opacity: '0.16',
},
},
},
{
isSelected: false,
isHovered: true,
css: {
[`& ${HoverIndicator}`]: {
opacity: '.2',
stroke: '$selected',
},
},
},
],
})
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 default memo(Shape)

View file

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

View file

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

View file

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

View file

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

View file

@ -64,34 +64,58 @@ export default function AlignDistribute({
}) {
return (
<Container>
<IconButton disabled={!hasTwoOrMore} onClick={alignLeft}>
<IconButton size="small" disabled={!hasTwoOrMore} onClick={alignLeft}>
<AlignLeftIcon />
</IconButton>
<IconButton disabled={!hasTwoOrMore} onClick={alignCenterHorizontal}>
<IconButton
size="small"
disabled={!hasTwoOrMore}
onClick={alignCenterHorizontal}
>
<AlignCenterHorizontallyIcon />
</IconButton>
<IconButton disabled={!hasTwoOrMore} onClick={alignRight}>
<IconButton size="small" disabled={!hasTwoOrMore} onClick={alignRight}>
<AlignRightIcon />
</IconButton>
<IconButton disabled={!hasTwoOrMore} onClick={stretchHorizontally}>
<IconButton
size="small"
disabled={!hasTwoOrMore}
onClick={stretchHorizontally}
>
<StretchHorizontallyIcon />
</IconButton>
<IconButton disabled={!hasThreeOrMore} onClick={distributeHorizontally}>
<IconButton
size="small"
disabled={!hasThreeOrMore}
onClick={distributeHorizontally}
>
<SpaceEvenlyHorizontallyIcon />
</IconButton>
<IconButton disabled={!hasTwoOrMore} onClick={alignTop}>
<IconButton size="small" disabled={!hasTwoOrMore} onClick={alignTop}>
<AlignTopIcon />
</IconButton>
<IconButton disabled={!hasTwoOrMore} onClick={alignCenterVertical}>
<IconButton
size="small"
disabled={!hasTwoOrMore}
onClick={alignCenterVertical}
>
<AlignCenterVerticallyIcon />
</IconButton>
<IconButton disabled={!hasTwoOrMore} onClick={alignBottom}>
<IconButton size="small" disabled={!hasTwoOrMore} onClick={alignBottom}>
<AlignBottomIcon />
</IconButton>
<IconButton disabled={!hasTwoOrMore} onClick={stretchVertically}>
<IconButton
size="small"
disabled={!hasTwoOrMore}
onClick={stretchVertically}
>
<StretchVerticallyIcon />
</IconButton>
<IconButton disabled={!hasThreeOrMore} onClick={distributeVertically}>
<IconButton
size="small"
disabled={!hasThreeOrMore}
onClick={distributeVertically}
>
<SpaceEvenlyVerticallyIcon />
</IconButton>
</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 { strokes } from 'lib/shape-styles'
import { ColorStyle } from 'types'
import { IconWrapper, RowButton } from './shared'
import { Square } from 'react-feather'
import styled from 'styles'
import ColorContent from './color-content'
interface Props {
colors: Record<string, string>
onChange: (color: string) => void
children: React.ReactNode
color: ColorStyle
onChange: (color: ColorStyle) => void
}
export default function ColorPicker({ colors, onChange, children }: Props) {
export default function ColorPicker({ color, onChange }: Props) {
return (
<DropdownMenu.Root>
{children}
<Colors sideOffset={4}>
{Object.entries(colors).map(([name, color]) => (
<ColorButton key={name} title={name} onSelect={() => onChange(name)}>
<ColorIcon color={color} />
</ColorButton>
))}
</Colors>
<DropdownMenu.Trigger as={RowButton}>
<label htmlFor="color">Color</label>
<IconWrapper>
<Square fill={strokes[color]} />
</IconWrapper>
</DropdownMenu.Trigger>
<ColorContent onChange={onChange} />
</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 state from 'state'
import { ChangeEvent } from 'react'
@ -16,49 +23,27 @@ interface Props {
export default function DashPicker({ dash }: Props) {
return (
<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 />
</RadioItem>
<RadioItem value={DashStyle.Dashed} isActive={dash === DashStyle.Dashed}>
</Item>
<Item
as={RadioGroup.RadioGroupItem}
value={DashStyle.Dashed}
isActive={dash === DashStyle.Dashed}
>
<DashDashedIcon />
</RadioItem>
<RadioItem value={DashStyle.Dotted} isActive={dash === DashStyle.Dotted}>
</Item>
<Item
as={RadioGroup.RadioGroupItem}
value={DashStyle.Dotted}
isActive={dash === DashStyle.Dotted}
>
<DashDottedIcon />
</RadioItem>
</Item>
</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 Panel from '../panel'
import styled from 'styles'
@ -27,7 +28,7 @@ export const Group = styled(RadioGroup.Root, {
display: 'flex',
})
export const RadioItem = styled(RadioGroup.Item, {
export const Item = styled('button', {
height: '32px',
width: '32px',
backgroundColor: '$panel',
@ -61,16 +62,158 @@ export const RadioItem = styled(RadioGroup.Item, {
'& svg': {
fill: '$text',
stroke: '$text',
strokeWidth: '0',
},
},
false: {
'& svg': {
fill: '$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 { IconButton } from 'components/shared'
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 { shades, fills, strokes } from 'lib/colors'
import ColorPicker, { ColorIcon, CurrentColor } from './color-picker'
import { strokes } from 'lib/shape-styles'
import AlignDistribute from './align-distribute'
import { MoveType, ShapeStyles } from 'types'
import WidthPicker from './width-picker'
import { MoveType } from 'types'
import SizePicker from './size-picker'
import {
ArrowDownIcon,
ArrowUpIcon,
@ -28,15 +27,14 @@ import {
RotateCounterClockwiseIcon,
} from '@radix-ui/react-icons'
import DashPicker from './dash-picker'
const fillColors = { ...shades, ...fills }
const strokeColors = { ...shades, ...strokes }
const getFillColor = (color: string) => {
if (shades[color]) {
return '#fff'
}
return fillColors[color]
}
import QuickColorSelect from './quick-color-select'
import ColorContent from './color-content'
import { RowButton, IconWrapper } from './shared'
import ColorPicker from './color-picker'
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'
export default function StylePanel() {
const rContainer = useRef<HTMLDivElement>(null)
@ -48,12 +46,15 @@ export default function StylePanel() {
<SelectedShapeStyles />
) : (
<>
<QuickColorSelect prop="stroke" colors={strokeColors} />
<QuickColorSelect />
<QuickSizeSelect />
<QuickdashSelect />
<IconButton
title="Style"
size="small"
onClick={() => state.send('TOGGLED_STYLE_PANEL_OPEN')}
>
<DotsVerticalIcon />
<ChevronDown />
</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
// information, based on the user's current selection. We might have to keep
// track of this data manually within our state.
function SelectedShapeStyles({}: {}) {
function SelectedShapeStyles() {
const selectedIds = useSelector(
(s) => Array.from(s.data.selectedIds.values()),
deepCompareArrays
@ -115,42 +95,25 @@ function SelectedShapeStyles({}: {}) {
<Panel.Layout>
<Panel.Header side="right">
<h3>Style</h3>
<IconButton onClick={() => state.send('TOGGLED_STYLE_PANEL_OPEN')}>
<IconButton
size="small"
onClick={() => state.send('TOGGLED_STYLE_PANEL_OPEN')}
>
<X />
</IconButton>
</Panel.Header>
<Content>
<ColorPicker
colors={strokeColors}
onChange={(color) =>
state.send('CHANGED_STYLE', {
stroke: strokeColors[color],
fill: getFillColor(color),
})
}
>
<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>*/}
color={commonStyle.color}
onChange={(color) => state.send('CHANGED_STYLE', { color })}
/>
<IsFilledPicker
isFilled={commonStyle.isFilled}
onChange={(isFilled) => state.send('CHANGED_STYLE', { isFilled })}
/>
<Row>
<label htmlFor="width">Width</label>
<WidthPicker strokeWidth={Number(commonStyle.strokeWidth)} />
<label htmlFor="size">Size</label>
<SizePicker size={commonStyle.size} />
</Row>
<Row>
<label htmlFor="dash">Dash</label>
@ -159,30 +122,35 @@ function SelectedShapeStyles({}: {}) {
<ButtonsRow>
<IconButton
disabled={!hasSelection}
size="small"
onClick={() => state.send('DUPLICATED')}
>
<CopyIcon />
</IconButton>
<IconButton
disabled={!hasSelection}
size="small"
onClick={() => state.send('ROTATED_CCW')}
>
<RotateCounterClockwiseIcon />
</IconButton>
<IconButton
disabled={!hasSelection}
size="small"
onClick={() => state.send('TOGGLED_SHAPE_HIDE')}
>
{isAllHidden ? <EyeClosedIcon /> : <EyeOpenIcon />}
</IconButton>
<IconButton
disabled={!hasSelection}
size="small"
onClick={() => state.send('TOGGLED_SHAPE_LOCK')}
>
{isAllLocked ? <LockClosedIcon /> : <LockOpen1Icon />}
</IconButton>
<IconButton
disabled={!hasSelection}
size="small"
onClick={() => state.send('TOGGLED_SHAPE_ASPECT_LOCK')}
>
{isAllAspectLocked ? <AspectRatioIcon /> : <BoxIcon />}
@ -191,30 +159,35 @@ function SelectedShapeStyles({}: {}) {
<ButtonsRow>
<IconButton
disabled={!hasSelection}
size="small"
onClick={() => state.send('MOVED', { type: MoveType.ToBack })}
>
<PinBottomIcon />
</IconButton>
<IconButton
disabled={!hasSelection}
size="small"
onClick={() => state.send('MOVED', { type: MoveType.Backward })}
>
<ArrowDownIcon />
</IconButton>
<IconButton
disabled={!hasSelection}
size="small"
onClick={() => state.send('MOVED', { type: MoveType.Forward })}
>
<ArrowUpIcon />
</IconButton>
<IconButton
disabled={!hasSelection}
size="small"
onClick={() => state.send('MOVED', { type: MoveType.ToFront })}
>
<PinTopIcon />
</IconButton>
<IconButton
disabled={!hasSelection}
size="small"
onClick={() => state.send('DELETED')}
>
<Trash2 />
@ -236,7 +209,7 @@ const StylePanelRoot = styled(Panel.Root, {
overflow: 'hidden',
position: 'relative',
border: '1px solid $panel',
boxShadow: '0px 2px 4px rgba(0,0,0,.12)',
boxShadow: '0px 2px 4px rgba(0,0,0,.2)',
display: 'flex',
alignItems: 'center',
pointerEvents: 'all',
@ -262,7 +235,6 @@ const Row = styled('div', {
width: '100%',
background: 'none',
border: 'none',
cursor: 'pointer',
outline: 'none',
alignItems: 'center',
justifyContent: 'space-between',
@ -293,22 +265,3 @@ const ButtonsRow = styled('div', {
justifyContent: 'flex-start',
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 />
</IconButton> */}
<IconButton
{/* <IconButton
name={ShapeType.Line}
size={{ '@sm': 'small', '@md': 'large' }}
onClick={selectLineTool}
@ -129,7 +129,7 @@ export default function ToolsPanel() {
isActive={activeTool === ShapeType.Dot}
>
<DotIcon />
</IconButton>
</IconButton> */}
</Container>
<Container>
<IconButton

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,6 +2,7 @@ import CodeShape from './index'
import { v4 as uuid } from 'uuid'
import { RectangleShape, ShapeType } from 'types'
import { vectorToPoint } from 'utils/utils'
import { defaultStyle } from 'lib/shape-styles'
export default class Rectangle extends CodeShape<RectangleShape> {
constructor(props = {} as Partial<RectangleShape>) {
@ -22,11 +23,7 @@ export default class Rectangle extends CodeShape<RectangleShape> {
isAspectRatioLocked: false,
isLocked: false,
isHidden: false,
style: {
fill: '#c6cacb',
stroke: '#000',
strokeWidth: 1,
},
style: defaultStyle,
...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 * as vec from 'utils/vec'
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 { circleFromThreePoints, clamp, isAngleBetween } from 'utils/utils'
import { pointInBounds } from 'utils/bounds'
@ -11,6 +18,7 @@ import {
} from 'utils/intersections'
import { getBoundsFromPoints, translateBounds } from 'utils/utils'
import { pointInCircle } from 'utils/hitTests'
import { defaultStyle, getShapeStyle } from 'lib/shape-styles'
const ctpCache = new WeakMap<ArrowShape['handles'], number[]>()
@ -77,21 +85,23 @@ const arrow = registerShapeUtils<ArrowShape>({
},
...props,
style: {
strokeWidth: 2,
...defaultStyle,
...props.style,
fill: 'none',
isFilled: false,
},
}
},
render(shape) {
const { id, bend, points, handles, style } = shape
const { id, bend, points, handles } = shape
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 style = getShapeStyle(shape.style)
// Arrowhead
const length = Math.min(arrowDist / 2, 16 + +style.strokeWidth * 2)
const angle = showCircle ? bend * (Math.PI * 0.48) : 0
@ -145,7 +155,7 @@ const arrow = registerShapeUtils<ArrowShape>({
applyStyles(shape, style) {
Object.assign(shape.style, style)
shape.style.fill = 'none'
shape.style.isFilled = false
return this
},

View file

@ -1,11 +1,12 @@
import { v4 as uuid } from 'uuid'
import * as vec from 'utils/vec'
import { CircleShape, ShapeType } from 'types'
import { CircleShape, ColorStyle, DashStyle, ShapeType, SizeStyle } from 'types'
import { registerShapeUtils } from './index'
import { boundsContained } from 'utils/bounds'
import { intersectCircleBounds } from 'utils/intersections'
import { pointInCircle } from 'utils/hitTests'
import { translateBounds } from 'utils/utils'
import { defaultStyle, getShapeStyle } from 'lib/shape-styles'
const circle = registerShapeUtils<CircleShape>({
boundsCache: new WeakMap([]),
@ -24,21 +25,20 @@ const circle = registerShapeUtils<CircleShape>({
isAspectRatioLocked: false,
isLocked: false,
isHidden: false,
style: {
fill: '#c6cacb',
stroke: '#000',
},
style: defaultStyle,
...props,
}
},
render({ id, radius, style }) {
const styles = getShapeStyle(style)
return (
<circle
id={id}
cx={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 { DotCircle } from 'components/canvas/misc'
import { translateBounds } from 'utils/utils'
import { defaultStyle } from 'lib/shape-styles'
const dot = registerShapeUtils<DotShape>({
boundsCache: new WeakMap([]),
@ -23,11 +24,12 @@ const dot = registerShapeUtils<DotShape>({
isAspectRatioLocked: false,
isLocked: false,
isHidden: false,
style: {
fill: '#c6cacb',
strokeWidth: '0',
},
...props,
style: {
...defaultStyle,
...props.style,
isFilled: false,
},
}
},

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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