From 21927845a833d8ae278692b01df56a572e8891de Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Wed, 26 May 2021 22:47:46 +0100 Subject: [PATCH] implements distribution --- components/editor.tsx | 6 +- components/style-panel/align-distribute.tsx | 48 +++---- components/style-panel/color-picker.tsx | 4 +- components/style-panel/style-panel.tsx | 23 +++- components/toolbar.tsx | 11 +- state/commands/distribute.ts | 131 ++++++++++++++++++-- state/data.ts | 52 +++++--- state/state.ts | 6 +- 8 files changed, 222 insertions(+), 59 deletions(-) diff --git a/components/editor.tsx b/components/editor.tsx index 674694039..6049d8692 100644 --- a/components/editor.tsx +++ b/components/editor.tsx @@ -13,6 +13,10 @@ export default function Editor() { useKeyboardEvents() useLoadOnMount() + const hasControls = useSelector( + (s) => Object.keys(s.data.codeControls).length > 0 + ) + return ( @@ -20,7 +24,7 @@ export default function Editor() { - + {hasControls && } diff --git a/components/style-panel/align-distribute.tsx b/components/style-panel/align-distribute.tsx index 29fb627c7..55416ab0a 100644 --- a/components/style-panel/align-distribute.tsx +++ b/components/style-panel/align-distribute.tsx @@ -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 ( - - - - - - - - - - - - - - - - + - + - + - + - + + + + + + + + + + + + + + + + ) } diff --git a/components/style-panel/color-picker.tsx b/components/style-panel/color-picker.tsx index 65be1f13d..680663125 100644 --- a/components/style-panel/color-picker.tsx +++ b/components/style-panel/color-picker.tsx @@ -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 onChange: (color: string) => void } -export default function ColorPicker({ label, color, onChange }: Props) { +export default function ColorPicker({ label, color, colors, onChange }: Props) { return ( diff --git a/components/style-panel/style-panel.tsx b/components/style-panel/style-panel.tsx index b2e2258a5..f7ed3ccca 100644 --- a/components/style-panel/style-panel.tsx +++ b/components/style-panel/style-panel.tsx @@ -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(null) @@ -65,6 +68,8 @@ function SelectedShapeStyles({}: {}) { return style }, deepCompare) + const hasSelection = selectedIds.length > 0 + return ( @@ -73,7 +78,10 @@ function SelectedShapeStyles({}: {}) {

Style

- state.send("DELETED")}> + state.send("DELETED")} + > @@ -82,14 +90,19 @@ function SelectedShapeStyles({}: {}) { state.send("CHANGED_STYLE", { fill: color })} /> state.send("CHANGED_STYLE", { stroke: color })} /> - + 1} + hasThreeOrMore={selectedIds.length > 2} + />
) diff --git a/components/toolbar.tsx b/components/toolbar.tsx index 457ddaf2e..e0e7c5ee6 100644 --- a/components/toolbar.tsx +++ b/components/toolbar.tsx @@ -72,6 +72,10 @@ export default function Toolbar() { +
+ + +
) } @@ -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", diff --git a/state/commands/distribute.ts b/state/commands/distribute.ts index fe62c00ae..9efa5e77e 100644 --- a/state/commands/distribute.ts +++ b/state/commands/distribute.ts @@ -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, + ]) } }, }) diff --git a/state/data.ts b/state/data.ts index 96742efd2..464a2478c 100644 --- a/state/data.ts +++ b/state/data.ts @@ -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, }, }), diff --git a/state/state.ts b/state/state.ts index b091b7651..37e446052 100644 --- a/state/state.ts +++ b/state/state.ts @@ -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],