Improves outlines

canvas-rendering
Steve Ruiz 2021-05-12 12:27:33 +01:00
rodzic b746601de7
commit addf4185f0
13 zmienionych plików z 184 dodań i 78 usunięć

Wyświetl plik

@ -1,5 +1,5 @@
import { useSelector } from "state"
import { CircleShape, ShapeProps } from "types"
import { Indicator, HoverIndicator } from "./indicator"
import ShapeGroup from "./shape-g"
function BaseCircle({
@ -9,25 +9,25 @@ function BaseCircle({
strokeWidth = 0,
}: ShapeProps<CircleShape>) {
return (
<circle
cx={radius}
cy={radius}
r={radius}
fill={fill}
stroke={stroke}
strokeWidth={strokeWidth}
/>
<>
<HoverIndicator as="circle" cx={radius} cy={radius} r={radius - 1} />
<circle
cx={radius}
cy={radius}
r={radius - strokeWidth / 2}
fill={fill}
stroke={stroke}
strokeWidth={strokeWidth}
/>
<Indicator as="circle" cx={radius} cy={radius} r={radius - 1} />
</>
)
}
export default function Circle({ id, point, radius }: CircleShape) {
const isSelected = useSelector((state) => state.values.selectedIds.has(id))
return (
<ShapeGroup id={id} point={point}>
<BaseCircle radius={radius} />
{isSelected && (
<BaseCircle radius={radius} fill="none" stroke="blue" strokeWidth={1} />
)}
</ShapeGroup>
)
}

Wyświetl plik

@ -1,40 +1,39 @@
import { useSelector } from "state"
import { Indicator, HoverIndicator } from "./indicator"
import { DotShape, ShapeProps } from "types"
import ShapeGroup from "./shape-g"
const dotRadius = 4
function BaseDot({
fill = "#999",
stroke = "none",
strokeWidth = 0,
strokeWidth = 1,
}: ShapeProps<DotShape>) {
return (
<>
<circle
cx={strokeWidth}
cy={strokeWidth}
r={8}
fill="transparent"
stroke="none"
strokeWidth="0"
<HoverIndicator
as="circle"
cx={dotRadius}
cy={dotRadius}
r={dotRadius - 1}
/>
<circle
cx={strokeWidth}
cy={strokeWidth}
r={Math.max(1, 4 - strokeWidth)}
cx={dotRadius}
cy={dotRadius}
r={dotRadius - strokeWidth / 2}
fill={fill}
stroke={stroke}
strokeWidth={strokeWidth}
/>
<Indicator as="circle" cx={dotRadius} cy={dotRadius} r={dotRadius - 1} />
</>
)
}
export default function Dot({ id, point }: DotShape) {
const isSelected = useSelector((state) => state.values.selectedIds.has(id))
return (
<ShapeGroup id={id} point={point}>
<BaseDot />
{isSelected && <BaseDot fill="none" stroke="blue" strokeWidth={1} />}
</ShapeGroup>
)
}

Wyświetl plik

@ -0,0 +1,21 @@
import styled from "styles"
const Indicator = styled("path", {
fill: "none",
stroke: "transparent",
strokeWidth: "2",
pointerEvents: "none",
strokeLineCap: "round",
strokeLinejoin: "round",
})
const HoverIndicator = styled("path", {
fill: "none",
stroke: "transparent",
strokeWidth: "8",
pointerEvents: "all",
strokeLinecap: "round",
strokeLinejoin: "round",
})
export { Indicator, HoverIndicator }

Wyświetl plik

@ -1,5 +1,5 @@
import { useSelector } from "state"
import { PolylineShape, ShapeProps } from "types"
import { Indicator, HoverIndicator } from "./indicator"
import ShapeGroup from "./shape-g"
function BasePolyline({
@ -10,28 +10,24 @@ function BasePolyline({
}: ShapeProps<PolylineShape>) {
return (
<>
<polyline
points={points.toString()}
fill="none"
stroke="transparent"
strokeWidth={12}
/>
<HoverIndicator as="polyline" points={points.toString()} />
<polyline
points={points.toString()}
fill={fill}
stroke={stroke}
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeLinejoin="round"
/>
<Indicator as="polyline" points={points.toString()} />
</>
)
}
export default function Polyline({ id, point, points }: PolylineShape) {
const isSelected = useSelector((state) => state.values.selectedIds.has(id))
return (
<ShapeGroup id={id} point={point}>
<BasePolyline points={points} />
{isSelected && <BasePolyline points={points} fill="none" stroke="blue" />}
</ShapeGroup>
)
}

Wyświetl plik

@ -1,5 +1,5 @@
import { useSelector } from "state"
import { RectangleShape, ShapeProps } from "types"
import { HoverIndicator, Indicator } from "./indicator"
import ShapeGroup from "./shape-g"
function BaseRectangle({
@ -9,26 +9,38 @@ function BaseRectangle({
strokeWidth = 0,
}: ShapeProps<RectangleShape>) {
return (
<rect
x={strokeWidth}
y={strokeWidth}
width={size[0] - strokeWidth * 2}
height={size[1] - strokeWidth * 2}
fill={fill}
stroke={stroke}
strokeWidth={strokeWidth}
/>
<>
<HoverIndicator
as="rect"
x={1}
y={1}
width={size[0] - 2}
height={size[1] - 2}
/>
<rect
x={strokeWidth / 2}
y={strokeWidth / 2}
width={size[0] - strokeWidth}
height={size[1] - strokeWidth}
fill={fill}
stroke={stroke}
strokeWidth={strokeWidth}
/>
<Indicator
as="rect"
x={1}
y={1}
width={size[0] - 2}
height={size[1] - 2}
/>
</>
)
}
export default function Rectangle({ id, point, size }: RectangleShape) {
const isSelected = useSelector((state) => state.values.selectedIds.has(id))
return (
<ShapeGroup id={id} point={point}>
<BaseRectangle size={size} />
{isSelected && (
<BaseRectangle size={size} fill="none" stroke="blue" strokeWidth={1} />
)}
</ShapeGroup>
)
}

Wyświetl plik

@ -1,6 +1,8 @@
import state from "state"
import state, { useSelector } from "state"
import React, { useCallback, useRef } from "react"
import { getPointerEventInfo } from "utils/utils"
import { Indicator, HoverIndicator } from "./indicator"
import styled from "styles"
export default function ShapeGroup({
id,
@ -12,6 +14,7 @@ export default function ShapeGroup({
point: number[]
}) {
const rGroup = useRef<SVGGElement>(null)
const isSelected = useSelector((state) => state.values.selectedIds.has(id))
const handlePointerDown = useCallback(
(e: React.PointerEvent) => {
@ -44,8 +47,9 @@ export default function ShapeGroup({
)
return (
<g
<StyledGroup
ref={rGroup}
isSelected={isSelected}
transform={`translate(${point})`}
onPointerDown={handlePointerDown}
onPointerUp={handlePointerUp}
@ -53,6 +57,31 @@ export default function ShapeGroup({
onPointerLeave={handlePointerLeave}
>
{children}
</g>
</StyledGroup>
)
}
const StyledGroup = styled("g", {
[`& ${HoverIndicator}`]: {
opacity: "0",
},
variants: {
isSelected: {
true: {
[`& ${Indicator}`]: {
stroke: "$selected",
},
[`&:hover ${HoverIndicator}`]: {
opacity: "1",
stroke: "$hint",
},
},
false: {
[`&:hover ${HoverIndicator}`]: {
opacity: "1",
stroke: "$hint",
},
},
},
},
})

Wyświetl plik

@ -1,7 +1,10 @@
import useKeyboardEvents from "hooks/useKeyboardEvents"
import Canvas from "./canvas/canvas"
import StatusBar from "./status-bar"
export default function Editor() {
useKeyboardEvents()
return (
<>
<Canvas />

Wyświetl plik

@ -0,0 +1,30 @@
import { useEffect } from "react"
import state from "state"
import { getKeyboardEventInfo } from "utils/utils"
export default function useKeyboardEvents() {
useEffect(() => {
function handleKeyDown(e: KeyboardEvent) {
if (e.key === "Escape") {
state.send("CANCELLED")
}
state.send("PRESSED_KEY", getKeyboardEventInfo(e))
}
function handleKeyUp(e: KeyboardEvent) {
if (e.key === "Escape") {
state.send("CANCELLED")
}
state.send("RELEASED_KEY", getKeyboardEventInfo(e))
}
document.body.addEventListener("keydown", handleKeyDown)
document.body.addEventListener("keyup", handleKeyUp)
return () => {
document.body.removeEventListener("keydown", handleKeyDown)
document.body.removeEventListener("keyup", handleKeyUp)
}
}, [])
}

Wyświetl plik

@ -10,8 +10,8 @@ import {
} from "utils/intersections"
interface BrushSnapshot {
selectedIds: string[]
shapes: { shape: Shape; test: (bounds: Bounds) => boolean }[]
selectedIds: Set<string>
shapes: { id: string; test: (bounds: Bounds) => boolean }[]
}
export default class BrushSession extends BaseSession {
@ -31,25 +31,33 @@ export default class BrushSession extends BaseSession {
const brushBounds = getBoundsFromPoints(origin, point)
data.selectedIds = [
...snapshot.selectedIds,
...snapshot.shapes
.filter(({ test }) => test(brushBounds))
.map(({ shape }) => shape.id),
]
for (let { test, id } of snapshot.shapes) {
if (test(brushBounds)) {
data.selectedIds.add(id)
} else if (data.selectedIds.has(id)) {
data.selectedIds.delete(id)
}
}
data.brush = brushBounds
}
cancel = (data: Data) => {
data.brush = undefined
data.selectedIds = this.snapshot.selectedIds
data.selectedIds = new Set(this.snapshot.selectedIds)
}
complete = (data: Data) => {
data.brush = undefined
}
/**
* Get a snapshot of the current selected ids, for each shape that is
* not already selected, the shape's id and a test to see whether the
* brush will intersect that shape. For tests, start broad -> fine.
* @param data
* @returns
*/
static getSnapshot(data: Data): BrushSnapshot {
const {
selectedIds,
@ -57,19 +65,17 @@ export default class BrushSession extends BaseSession {
currentPageId,
} = current(data)
const currentlySelected = new Set(selectedIds)
return {
selectedIds: [...data.selectedIds],
selectedIds: new Set(data.selectedIds),
shapes: Object.values(pages[currentPageId].shapes)
.filter((shape) => !currentlySelected.has(shape.id))
.filter((shape) => !selectedIds.has(shape.id))
.map((shape) => {
switch (shape.type) {
case ShapeType.Dot: {
const bounds = shapeUtils[shape.type].getBounds(shape)
return {
shape,
id: shape.id,
test: (brushBounds: Bounds) =>
boundsContained(bounds, brushBounds) ||
intersectCircleBounds(shape.point, 4, brushBounds).length > 0,
@ -79,7 +85,7 @@ export default class BrushSession extends BaseSession {
const bounds = shapeUtils[shape.type].getBounds(shape)
return {
shape,
id: shape.id,
test: (brushBounds: Bounds) =>
boundsContained(bounds, brushBounds) ||
intersectCircleBounds(
@ -93,7 +99,7 @@ export default class BrushSession extends BaseSession {
const bounds = shapeUtils[shape.type].getBounds(shape)
return {
shape,
id: shape.id,
test: (brushBounds: Bounds) =>
boundsContained(bounds, brushBounds) ||
boundsCollide(bounds, brushBounds),
@ -106,10 +112,11 @@ export default class BrushSession extends BaseSession {
)
return {
shape,
id: shape.id,
test: (brushBounds: Bounds) =>
boundsContained(bounds, brushBounds) ||
intersectPolylineBounds(points, brushBounds).length > 0,
(boundsCollide(bounds, brushBounds) &&
intersectPolylineBounds(points, brushBounds).length > 0),
}
}
default: {

Wyświetl plik

@ -12,7 +12,7 @@ const initialData: Data = {
},
brush: undefined,
pointedId: null,
selectedIds: [],
selectedIds: new Set([]),
currentPageId: "page0",
document: defaultDocument,
}
@ -61,7 +61,7 @@ const state = createState({
},
conditions: {
isPointedShapeSelected(data) {
return data.selectedIds.includes(data.pointedId)
return data.selectedIds.has(data.pointedId)
},
isPressingShiftKey(data, payload: { shiftKey: boolean }) {
return payload.shiftKey
@ -93,14 +93,14 @@ const state = createState({
data.pointedId = undefined
},
clearSelectedIds(data) {
data.selectedIds = []
data.selectedIds.clear()
},
pullPointedIdFromSelectedIds(data) {
const { selectedIds, pointedId } = data
selectedIds.splice(selectedIds.indexOf(pointedId, 1))
selectedIds.delete(pointedId)
},
pushPointedIdToSelectedIds(data) {
data.selectedIds.push(data.pointedId)
data.selectedIds.add(data.pointedId)
},
// Camera
zoomCamera(data, payload: { delta: number; point: number[] }) {

Wyświetl plik

@ -8,6 +8,8 @@ const { styled, global, css, theme, getCssString } = createCss({
colors: {
brushFill: "rgba(0,0,0,.1)",
brushStroke: "rgba(0,0,0,.5)",
hint: "rgba(66, 133, 244, 0.200)",
selected: "rgba(66, 133, 244, 1.000)",
},
space: {},
fontSizes: {

Wyświetl plik

@ -5,7 +5,7 @@ export interface Data {
}
brush?: Bounds
currentPageId: string
selectedIds: string[]
selectedIds: Set<string>
pointedId?: string
document: {
pages: Record<string, Page>
@ -121,4 +121,6 @@ export type ShapeSpecificProps<T extends Shape> = Pick<
>
export type ShapeProps<T extends Shape> = Partial<BaseShapeStyles> &
ShapeSpecificProps<T>
ShapeSpecificProps<T> & { id?: Shape["id"] }
export type ShapeIndicatorProps<T extends Shape> = ShapeSpecificProps<T>

Wyświetl plik

@ -848,3 +848,8 @@ export function getPointerEventInfo(e: React.PointerEvent | WheelEvent) {
const { shiftKey, ctrlKey, metaKey, altKey } = e
return { point: [e.clientX, e.clientY], shiftKey, ctrlKey, metaKey, altKey }
}
export function getKeyboardEventInfo(e: React.KeyboardEvent | KeyboardEvent) {
const { shiftKey, ctrlKey, metaKey, altKey } = e
return { key: e.key, shiftKey, ctrlKey, metaKey, altKey }
}