Adds quick style selects, rewrites styling
This commit is contained in:
parent
815bf1109c
commit
7c768bddf5
45 changed files with 867 additions and 630 deletions
|
@ -75,7 +75,7 @@ export default function Canvas() {
|
|||
<g ref={rGroup}>
|
||||
<BoundsBg />
|
||||
<Page />
|
||||
<Selected />
|
||||
{/* <Selected /> */}
|
||||
<Bounds />
|
||||
<Handles />
|
||||
<Brush />
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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,38 +19,40 @@ 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 }) {
|
||||
const rIndicator = useRef<SVGUseElement>(null)
|
||||
export const ShapeOutline = memo(
|
||||
({ id, isSelected }: { id: string; isSelected: boolean }) => {
|
||||
const rIndicator = useRef<SVGUseElement>(null)
|
||||
|
||||
const shape = useSelector(({ data }) => getPage(data).shapes[id])
|
||||
const shape = useSelector(({ data }) => getPage(data).shapes[id])
|
||||
|
||||
const events = useShapeEvents(id, rIndicator)
|
||||
const events = useShapeEvents(id, rIndicator)
|
||||
|
||||
if (!shape) return null
|
||||
if (!shape) return null
|
||||
|
||||
const transform = `
|
||||
const transform = `
|
||||
rotate(${shape.rotation * (180 / Math.PI)},
|
||||
${getShapeUtils(shape).getCenter(shape)})
|
||||
translate(${shape.point})
|
||||
`
|
||||
|
||||
return (
|
||||
<SelectIndicator
|
||||
ref={rIndicator}
|
||||
as="use"
|
||||
href={'#' + id}
|
||||
transform={transform}
|
||||
isLocked={shape.isLocked}
|
||||
{...events}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<SelectIndicator
|
||||
ref={rIndicator}
|
||||
as="use"
|
||||
href={'#' + id}
|
||||
transform={transform}
|
||||
isLocked={shape.isLocked}
|
||||
{...events}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
const SelectIndicator = styled('path', {
|
||||
zStrokeWidth: 3,
|
||||
|
|
|
@ -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: {},
|
||||
true: {
|
||||
[`& ${HoverIndicator}`]: {
|
||||
opacity: '0.2',
|
||||
},
|
||||
[`&:hover ${HoverIndicator}`]: {
|
||||
opacity: '0.3',
|
||||
},
|
||||
[`&:active ${HoverIndicator}`]: {
|
||||
opacity: '0.3',
|
||||
},
|
||||
},
|
||||
false: {
|
||||
[`& ${HoverIndicator}`]: {
|
||||
opacity: '0',
|
||||
},
|
||||
[`&:hover ${HoverIndicator}`]: {
|
||||
opacity: '0.16',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
compoundVariants: [
|
||||
{
|
||||
isSelected: true,
|
||||
isHovered: true,
|
||||
css: {
|
||||
[`& ${HoverIndicator}`]: {
|
||||
opacity: '.4',
|
||||
stroke: '$selected',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
isSelected: true,
|
||||
isHovered: false,
|
||||
css: {
|
||||
[`& ${HoverIndicator}`]: {
|
||||
opacity: '.2',
|
||||
stroke: '$selected',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
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)
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
})
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
|
||||
'& > svg': {
|
||||
height: '16px',
|
||||
width: '16px',
|
||||
},
|
||||
|
||||
variants: {
|
||||
size: {
|
||||
small: {},
|
||||
small: {
|
||||
'& > svg': {
|
||||
height: '16px',
|
||||
width: '16px',
|
||||
},
|
||||
},
|
||||
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',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
|
|
28
components/style-panel/color-content.tsx
Normal file
28
components/style-panel/color-content.tsx
Normal 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>
|
||||
)
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
28
components/style-panel/is-filled-picker.tsx
Normal file
28
components/style-panel/is-filled-picker.tsx
Normal 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>
|
||||
)
|
||||
}
|
21
components/style-panel/quick-color-select.tsx
Normal file
21
components/style-panel/quick-color-select.tsx
Normal 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>
|
||||
)
|
||||
}
|
52
components/style-panel/quick-dash-select.tsx
Normal file
52
components/style-panel/quick-dash-select.tsx
Normal 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>
|
||||
)
|
||||
}
|
44
components/style-panel/quick-size-select.tsx
Normal file
44
components/style-panel/quick-size-select.tsx
Normal 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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
40
components/style-panel/size-picker.tsx
Normal file
40
components/style-panel/size-picker.tsx
Normal 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>
|
||||
)
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
})
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
83
lib/shape-styles.ts
Normal 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,
|
||||
}
|
|
@ -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
|
||||
},
|
||||
|
||||
|
|
|
@ -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)}
|
||||
/>
|
||||
)
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)}
|
||||
/>
|
||||
)
|
||||
},
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>([])
|
||||
|
||||
|
|
54
types.ts
54
types.ts
|
@ -67,11 +67,49 @@ export enum ShapeType {
|
|||
// Cubic = "cubic",
|
||||
// Conic = "conic",
|
||||
|
||||
export type ShapeStyles = Partial<
|
||||
React.SVGProps<SVGUseElement> & {
|
||||
dash: DashStyle
|
||||
}
|
||||
>
|
||||
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
|
||||
|
|
Loading…
Reference in a new issue