implements distribution
This commit is contained in:
parent
25fc384216
commit
21927845a8
8 changed files with 222 additions and 59 deletions
|
@ -13,6 +13,10 @@ export default function Editor() {
|
|||
useKeyboardEvents()
|
||||
useLoadOnMount()
|
||||
|
||||
const hasControls = useSelector(
|
||||
(s) => Object.keys(s.data.codeControls).length > 0
|
||||
)
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Canvas />
|
||||
|
@ -20,7 +24,7 @@ export default function Editor() {
|
|||
<Toolbar />
|
||||
<LeftPanels>
|
||||
<CodePanel />
|
||||
<ControlsPanel />
|
||||
{hasControls && <ControlsPanel />}
|
||||
</LeftPanels>
|
||||
<RightPanels>
|
||||
<StylePanel />
|
||||
|
|
|
@ -55,39 +55,45 @@ function distributeHorizontally() {
|
|||
state.send("DISTRIBUTED", { type: DistributeType.Horizontal })
|
||||
}
|
||||
|
||||
export default function AlignDistribute() {
|
||||
export default function AlignDistribute({
|
||||
hasTwoOrMore,
|
||||
hasThreeOrMore,
|
||||
}: {
|
||||
hasTwoOrMore: boolean
|
||||
hasThreeOrMore: boolean
|
||||
}) {
|
||||
return (
|
||||
<Container>
|
||||
<IconButton onClick={alignTop}>
|
||||
<AlignTopIcon />
|
||||
</IconButton>
|
||||
<IconButton onClick={alignCenterVertical}>
|
||||
<AlignCenterVerticallyIcon />
|
||||
</IconButton>
|
||||
<IconButton onClick={alignBottom}>
|
||||
<AlignBottomIcon />
|
||||
</IconButton>
|
||||
<IconButton onClick={stretchVertically}>
|
||||
<StretchVerticallyIcon />
|
||||
</IconButton>
|
||||
<IconButton onClick={distributeVertically}>
|
||||
<SpaceEvenlyVerticallyIcon />
|
||||
</IconButton>
|
||||
<IconButton onClick={alignLeft}>
|
||||
<IconButton disabled={!hasTwoOrMore} onClick={alignLeft}>
|
||||
<AlignLeftIcon />
|
||||
</IconButton>
|
||||
<IconButton onClick={alignCenterHorizontal}>
|
||||
<IconButton disabled={!hasTwoOrMore} onClick={alignCenterHorizontal}>
|
||||
<AlignCenterHorizontallyIcon />
|
||||
</IconButton>
|
||||
<IconButton onClick={alignRight}>
|
||||
<IconButton disabled={!hasTwoOrMore} onClick={alignRight}>
|
||||
<AlignRightIcon />
|
||||
</IconButton>
|
||||
<IconButton onClick={stretchHorizontally}>
|
||||
<IconButton disabled={!hasTwoOrMore} onClick={stretchHorizontally}>
|
||||
<StretchHorizontallyIcon />
|
||||
</IconButton>
|
||||
<IconButton onClick={distributeHorizontally}>
|
||||
<IconButton disabled={!hasThreeOrMore} onClick={distributeHorizontally}>
|
||||
<SpaceEvenlyHorizontallyIcon />
|
||||
</IconButton>
|
||||
<IconButton disabled={!hasTwoOrMore} onClick={alignTop}>
|
||||
<AlignTopIcon />
|
||||
</IconButton>
|
||||
<IconButton disabled={!hasTwoOrMore} onClick={alignCenterVertical}>
|
||||
<AlignCenterVerticallyIcon />
|
||||
</IconButton>
|
||||
<IconButton disabled={!hasTwoOrMore} onClick={alignBottom}>
|
||||
<AlignBottomIcon />
|
||||
</IconButton>
|
||||
<IconButton disabled={!hasTwoOrMore} onClick={stretchVertically}>
|
||||
<StretchVerticallyIcon />
|
||||
</IconButton>
|
||||
<IconButton disabled={!hasThreeOrMore} onClick={distributeVertically}>
|
||||
<SpaceEvenlyVerticallyIcon />
|
||||
</IconButton>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import * as DropdownMenu from "@radix-ui/react-dropdown-menu"
|
||||
import { Square } from "react-feather"
|
||||
import { colors } from "state/data"
|
||||
import styled from "styles"
|
||||
|
||||
interface Props {
|
||||
label: string
|
||||
color: string
|
||||
colors: Record<string, string>
|
||||
onChange: (color: string) => void
|
||||
}
|
||||
|
||||
export default function ColorPicker({ label, color, onChange }: Props) {
|
||||
export default function ColorPicker({ label, color, colors, onChange }: Props) {
|
||||
return (
|
||||
<DropdownMenu.Root>
|
||||
<CurrentColor>
|
||||
|
|
|
@ -3,13 +3,16 @@ import state, { useSelector } from "state"
|
|||
import * as Panel from "components/panel"
|
||||
import { useRef } from "react"
|
||||
import { IconButton } from "components/shared"
|
||||
import { Circle, Square, Trash, X } from "react-feather"
|
||||
import { Circle, Trash, X } from "react-feather"
|
||||
import { deepCompare, deepCompareArrays, getSelectedShapes } from "utils/utils"
|
||||
import { colors } from "state/data"
|
||||
import { shades, fills, strokes } from "state/data"
|
||||
|
||||
import ColorPicker from "./color-picker"
|
||||
import AlignDistribute from "./align-distribute"
|
||||
import { ShapeByType, ShapeStyles } from "types"
|
||||
import { ShapeStyles } from "types"
|
||||
|
||||
const fillColors = { ...shades, ...fills }
|
||||
const strokeColors = { ...shades, ...strokes }
|
||||
|
||||
export default function StylePanel() {
|
||||
const rContainer = useRef<HTMLDivElement>(null)
|
||||
|
@ -65,6 +68,8 @@ function SelectedShapeStyles({}: {}) {
|
|||
return style
|
||||
}, deepCompare)
|
||||
|
||||
const hasSelection = selectedIds.length > 0
|
||||
|
||||
return (
|
||||
<Panel.Layout>
|
||||
<Panel.Header>
|
||||
|
@ -73,7 +78,10 @@ function SelectedShapeStyles({}: {}) {
|
|||
</IconButton>
|
||||
<h3>Style</h3>
|
||||
<Panel.ButtonsGroup>
|
||||
<IconButton onClick={() => state.send("DELETED")}>
|
||||
<IconButton
|
||||
disabled={!hasSelection}
|
||||
onClick={() => state.send("DELETED")}
|
||||
>
|
||||
<Trash />
|
||||
</IconButton>
|
||||
</Panel.ButtonsGroup>
|
||||
|
@ -82,14 +90,19 @@ function SelectedShapeStyles({}: {}) {
|
|||
<ColorPicker
|
||||
label="Fill"
|
||||
color={shapesStyle.fill}
|
||||
colors={fillColors}
|
||||
onChange={(color) => state.send("CHANGED_STYLE", { fill: color })}
|
||||
/>
|
||||
<ColorPicker
|
||||
label="Stroke"
|
||||
color={shapesStyle.stroke}
|
||||
colors={strokeColors}
|
||||
onChange={(color) => state.send("CHANGED_STYLE", { stroke: color })}
|
||||
/>
|
||||
<AlignDistribute />
|
||||
<AlignDistribute
|
||||
hasTwoOrMore={selectedIds.length > 1}
|
||||
hasThreeOrMore={selectedIds.length > 2}
|
||||
/>
|
||||
</Content>
|
||||
</Panel.Layout>
|
||||
)
|
||||
|
|
|
@ -72,6 +72,10 @@ export default function Toolbar() {
|
|||
</Button>
|
||||
<Button onClick={() => state.send("RESET_CAMERA")}>Reset Camera</Button>
|
||||
</Section>
|
||||
<Section>
|
||||
<Button onClick={() => state.send("UNDO")}>Undo</Button>
|
||||
<Button onClick={() => state.send("REDO")}>Redo</Button>
|
||||
</Section>
|
||||
</ToolbarContainer>
|
||||
)
|
||||
}
|
||||
|
@ -80,10 +84,10 @@ const ToolbarContainer = styled("div", {
|
|||
gridArea: "toolbar",
|
||||
userSelect: "none",
|
||||
borderBottom: "1px solid black",
|
||||
display: "grid",
|
||||
gridTemplateColumns: "auto 1fr auto",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
backgroundColor: "white",
|
||||
justifyContent: "space-between",
|
||||
backgroundColor: "$panel",
|
||||
gap: 8,
|
||||
fontSize: "$1",
|
||||
zIndex: 200,
|
||||
|
@ -102,6 +106,7 @@ const Button = styled("button", {
|
|||
font: "$ui",
|
||||
fontSize: "$ui",
|
||||
height: "40px",
|
||||
outline: "none",
|
||||
borderRadius: 0,
|
||||
border: "none",
|
||||
padding: "0 12px",
|
||||
|
|
|
@ -1,16 +1,39 @@
|
|||
import Command from "./command"
|
||||
import history from "../history"
|
||||
import { AlignType, Data, DistributeType } from "types"
|
||||
import { getPage } from "utils/utils"
|
||||
import * as vec from "utils/vec"
|
||||
import {
|
||||
getBoundsCenter,
|
||||
getBoundsFromPoints,
|
||||
getCommonBounds,
|
||||
getPage,
|
||||
getSelectedShapes,
|
||||
} from "utils/utils"
|
||||
import { getShapeUtils } from "lib/shape-utils"
|
||||
|
||||
export default function distributeCommand(data: Data, type: DistributeType) {
|
||||
const { currentPageId } = data
|
||||
|
||||
const initialPoints = Object.fromEntries(
|
||||
Object.entries(getPage(data).shapes).map(([id, shape]) => [
|
||||
id,
|
||||
[...shape.point],
|
||||
const selectedShapes = getSelectedShapes(data)
|
||||
|
||||
const entries = selectedShapes.map(
|
||||
(shape) => [shape.id, getShapeUtils(shape).getBounds(shape)] as const
|
||||
)
|
||||
const boundsForShapes = Object.fromEntries(entries)
|
||||
|
||||
const commonBounds = getCommonBounds(...entries.map((entry) => entry[1]))
|
||||
|
||||
const innerBounds = getBoundsFromPoints(
|
||||
entries.map((entry) => getBoundsCenter(entry[1]))
|
||||
)
|
||||
|
||||
const midX = commonBounds.minX + commonBounds.width / 2
|
||||
const midY = commonBounds.minY + commonBounds.height / 2
|
||||
|
||||
const centers = Object.fromEntries(
|
||||
selectedShapes.map((shape) => [
|
||||
shape.id,
|
||||
getBoundsCenter(boundsForShapes[shape.id]),
|
||||
])
|
||||
)
|
||||
|
||||
|
@ -21,19 +44,113 @@ export default function distributeCommand(data: Data, type: DistributeType) {
|
|||
category: "canvas",
|
||||
do(data) {
|
||||
const { shapes } = getPage(data, currentPageId)
|
||||
const len = entries.length
|
||||
|
||||
switch (type) {
|
||||
case DistributeType.Horizontal: {
|
||||
const sortedByCenter = entries.sort(
|
||||
([a], [b]) => centers[a][0] - centers[b][0]
|
||||
)
|
||||
|
||||
const span = sortedByCenter.reduce((a, c) => a + c[1].width, 0)
|
||||
|
||||
if (span > commonBounds.width) {
|
||||
const left = sortedByCenter.sort(
|
||||
(a, b) => a[1].minX - b[1].minX
|
||||
)[0]
|
||||
|
||||
const right = sortedByCenter.sort(
|
||||
(a, b) => b[1].maxX - a[1].maxX
|
||||
)[0]
|
||||
|
||||
const entriesToMove = sortedByCenter
|
||||
.filter((a) => a !== left && a !== right)
|
||||
.sort((a, b) => centers[a[0]][0] - centers[b[0]][0])
|
||||
|
||||
const step =
|
||||
(centers[right[0]][0] - centers[left[0]][0]) / (len - 1)
|
||||
|
||||
const x = centers[left[0]][0] + step
|
||||
|
||||
for (let i = 0; i < entriesToMove.length; i++) {
|
||||
const [id, bounds] = entriesToMove[i]
|
||||
const shape = shapes[id]
|
||||
getShapeUtils(shape).translateTo(shape, [
|
||||
x + step * i - bounds.width / 2,
|
||||
bounds.minY,
|
||||
])
|
||||
}
|
||||
} else {
|
||||
const step = (commonBounds.width - span) / (len - 1)
|
||||
let x = commonBounds.minX
|
||||
|
||||
for (let i = 0; i < sortedByCenter.length - 1; i++) {
|
||||
const [id, bounds] = sortedByCenter[i]
|
||||
const shape = shapes[id]
|
||||
getShapeUtils(shape).translateTo(shape, [x, bounds.minY])
|
||||
x += bounds.width + step
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case DistributeType.Vertical: {
|
||||
const sortedByCenter = entries.sort(
|
||||
([a], [b]) => centers[a][1] - centers[b][1]
|
||||
)
|
||||
|
||||
const span = sortedByCenter.reduce((a, c) => a + c[1].height, 0)
|
||||
|
||||
if (span > commonBounds.height) {
|
||||
const top = sortedByCenter.sort(
|
||||
(a, b) => a[1].minY - b[1].minY
|
||||
)[0]
|
||||
|
||||
const bottom = sortedByCenter.sort(
|
||||
(a, b) => b[1].maxY - a[1].maxY
|
||||
)[0]
|
||||
|
||||
const entriesToMove = sortedByCenter
|
||||
.filter((a) => a !== top && a !== bottom)
|
||||
.sort((a, b) => centers[a[0]][1] - centers[b[0]][1])
|
||||
|
||||
const step =
|
||||
(centers[bottom[0]][1] - centers[top[0]][1]) / (len - 1)
|
||||
|
||||
const y = centers[top[0]][1] + step
|
||||
|
||||
for (let i = 0; i < entriesToMove.length; i++) {
|
||||
const [id, bounds] = entriesToMove[i]
|
||||
const shape = shapes[id]
|
||||
getShapeUtils(shape).translateTo(shape, [
|
||||
bounds.minX,
|
||||
y + step * i - bounds.height / 2,
|
||||
])
|
||||
}
|
||||
} else {
|
||||
const step = (commonBounds.height - span) / (len - 1)
|
||||
let y = commonBounds.minY
|
||||
|
||||
for (let i = 0; i < sortedByCenter.length - 1; i++) {
|
||||
const [id, bounds] = sortedByCenter[i]
|
||||
const shape = shapes[id]
|
||||
getShapeUtils(shape).translateTo(shape, [bounds.minX, y])
|
||||
y += bounds.height + step
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
undo(data) {
|
||||
const { shapes } = getPage(data, currentPageId)
|
||||
for (let id in initialPoints) {
|
||||
for (let id in boundsForShapes) {
|
||||
const shape = shapes[id]
|
||||
getShapeUtils(shape).translateTo(shape, initialPoints[id])
|
||||
const initialBounds = boundsForShapes[id]
|
||||
getShapeUtils(shape).translateTo(shape, [
|
||||
initialBounds.minX,
|
||||
initialBounds.minY,
|
||||
])
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
import { Data, ShapeType } from "types"
|
||||
import shapeUtils from "lib/shape-utils"
|
||||
|
||||
export const colors = {
|
||||
export const shades = {
|
||||
transparent: "transparent",
|
||||
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)",
|
||||
|
@ -22,6 +25,21 @@ export const colors = {
|
|||
yellow: "rgba(245, 159, 0, 1.000)",
|
||||
}
|
||||
|
||||
export const fills = {
|
||||
lime: "rgba(217, 245, 162, 1.000)",
|
||||
green: "rgba(177, 242, 188, 1.000)",
|
||||
teal: "rgba(149, 242, 215, 1.000)",
|
||||
cyan: "rgba(153, 233, 242, 1.000)",
|
||||
blue: "rgba(166, 216, 255, 1.000)",
|
||||
indigo: "rgba(186, 200, 255, 1.000)",
|
||||
violet: "rgba(208, 191, 255, 1.000)",
|
||||
grape: "rgba(237, 190, 250, 1.000)",
|
||||
pink: "rgba(252, 194, 215, 1.000)",
|
||||
red: "rgba(255, 201, 201, 1.000)",
|
||||
orange: "rgba(255, 216, 168, 1.000)",
|
||||
yellow: "rgba(255, 236, 153, 1.000)",
|
||||
}
|
||||
|
||||
export const defaultDocument: Data["document"] = {
|
||||
pages: {
|
||||
page0: {
|
||||
|
@ -36,8 +54,8 @@ export const defaultDocument: Data["document"] = {
|
|||
childIndex: 3,
|
||||
point: [400, 500],
|
||||
style: {
|
||||
stroke: colors.black,
|
||||
fill: colors.lightGray,
|
||||
stroke: shades.black,
|
||||
fill: shades.lightGray,
|
||||
strokeWidth: 1,
|
||||
},
|
||||
}),
|
||||
|
@ -48,8 +66,8 @@ export const defaultDocument: Data["document"] = {
|
|||
point: [100, 600],
|
||||
radius: 50,
|
||||
style: {
|
||||
stroke: colors.black,
|
||||
fill: colors.lightGray,
|
||||
stroke: shades.black,
|
||||
fill: shades.lightGray,
|
||||
strokeWidth: 1,
|
||||
},
|
||||
}),
|
||||
|
@ -61,8 +79,8 @@ export const defaultDocument: Data["document"] = {
|
|||
radiusX: 50,
|
||||
radiusY: 100,
|
||||
style: {
|
||||
stroke: colors.black,
|
||||
fill: colors.lightGray,
|
||||
stroke: shades.black,
|
||||
fill: shades.lightGray,
|
||||
strokeWidth: 1,
|
||||
},
|
||||
}),
|
||||
|
@ -74,8 +92,8 @@ export const defaultDocument: Data["document"] = {
|
|||
radiusX: 50,
|
||||
radiusY: 30,
|
||||
style: {
|
||||
stroke: colors.black,
|
||||
fill: colors.lightGray,
|
||||
stroke: shades.black,
|
||||
fill: shades.lightGray,
|
||||
strokeWidth: 1,
|
||||
},
|
||||
}),
|
||||
|
@ -86,8 +104,8 @@ export const defaultDocument: Data["document"] = {
|
|||
point: [400, 400],
|
||||
direction: [0.2, 0.2],
|
||||
style: {
|
||||
stroke: colors.black,
|
||||
fill: colors.lightGray,
|
||||
stroke: shades.black,
|
||||
fill: shades.lightGray,
|
||||
strokeWidth: 1,
|
||||
},
|
||||
}),
|
||||
|
@ -98,8 +116,8 @@ export const defaultDocument: Data["document"] = {
|
|||
point: [300, 100],
|
||||
direction: [0.5, 0.5],
|
||||
style: {
|
||||
stroke: colors.black,
|
||||
fill: colors.lightGray,
|
||||
stroke: shades.black,
|
||||
fill: shades.lightGray,
|
||||
strokeWidth: 1,
|
||||
},
|
||||
}),
|
||||
|
@ -114,8 +132,8 @@ export const defaultDocument: Data["document"] = {
|
|||
[100, 50],
|
||||
],
|
||||
style: {
|
||||
stroke: colors.black,
|
||||
fill: colors.transparent,
|
||||
stroke: shades.black,
|
||||
fill: shades.transparent,
|
||||
strokeWidth: 1,
|
||||
},
|
||||
}),
|
||||
|
@ -126,8 +144,8 @@ export const defaultDocument: Data["document"] = {
|
|||
point: [400, 600],
|
||||
size: [200, 200],
|
||||
style: {
|
||||
stroke: colors.black,
|
||||
fill: colors.lightGray,
|
||||
stroke: shades.black,
|
||||
fill: shades.lightGray,
|
||||
strokeWidth: 1,
|
||||
},
|
||||
}),
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { createSelectorHook, createState } from "@state-designer/react"
|
||||
import * as vec from "utils/vec"
|
||||
import inputs from "./inputs"
|
||||
import { colors, defaultDocument } from "./data"
|
||||
import { shades, defaultDocument } from "./data"
|
||||
import { createShape, getShapeUtils } from "lib/shape-utils"
|
||||
import history from "state/history"
|
||||
import * as Sessions from "./sessions"
|
||||
|
@ -42,8 +42,8 @@ const initialData: Data = {
|
|||
isStyleOpen: false,
|
||||
},
|
||||
currentStyle: {
|
||||
fill: colors.lightGray,
|
||||
stroke: colors.darkGray,
|
||||
fill: shades.lightGray,
|
||||
stroke: shades.darkGray,
|
||||
},
|
||||
camera: {
|
||||
point: [0, 0],
|
||||
|
|
Loading…
Reference in a new issue