implements distribution

canvas-rendering
Steve Ruiz 2021-05-26 22:47:46 +01:00
rodzic 25fc384216
commit 21927845a8
8 zmienionych plików z 222 dodań i 59 usunięć

Wyświetl plik

@ -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 />

Wyświetl plik

@ -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>
)
}

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>
)

Wyświetl plik

@ -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",

Wyświetl plik

@ -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,
])
}
},
})

Wyświetl plik

@ -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,
},
}),

Wyświetl plik

@ -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],