Adds dragging / translation

This commit is contained in:
Steve Ruiz 2021-05-13 07:44:52 +01:00
parent 7ec9457ac2
commit 8c81823b20
18 changed files with 340 additions and 141 deletions

View file

@ -1,7 +1,10 @@
import { useRef } from "react"
import state, { useSelector } from "state" import state, { useSelector } from "state"
import inputs from "state/inputs"
import styled from "styles" import styled from "styles"
export default function BoundsBg() { export default function BoundsBg() {
const rBounds = useRef<SVGRectElement>(null)
const bounds = useSelector((state) => state.values.selectedBounds) const bounds = useSelector((state) => state.values.selectedBounds)
if (!bounds) return null if (!bounds) return null
@ -10,19 +13,15 @@ export default function BoundsBg() {
return ( return (
<StyledBoundsBg <StyledBoundsBg
ref={rBounds}
x={minX} x={minX}
y={minY} y={minY}
width={width} width={width}
height={height} height={height}
onPointerDown={(e) => { onPointerDown={(e) => {
if (e.buttons !== 1) return if (e.buttons !== 1) return
state.send("POINTED_BOUNDS", { rBounds.current.setPointerCapture(e.pointerId)
shiftKey: e.shiftKey, state.send("POINTED_BOUNDS", inputs.pointerDown(e))
optionKey: e.altKey,
metaKey: e.metaKey || e.ctrlKey,
ctrlKey: e.ctrlKey,
buttons: e.buttons,
})
}} }}
/> />
) )

View file

@ -1,6 +1,7 @@
import state, { useSelector } from "state" import state, { useSelector } from "state"
import { motion } from "framer-motion" import { motion } from "framer-motion"
import styled from "styles" import styled from "styles"
import inputs from "state/inputs"
export default function Bounds() { export default function Bounds() {
const bounds = useSelector((state) => state.values.selectedBounds) const bounds = useSelector((state) => state.values.selectedBounds)
@ -23,38 +24,42 @@ export default function Bounds() {
height={height} height={height}
pointerEvents="none" pointerEvents="none"
/> />
<Corner {width * zoom > 8 && (
x={minX} <>
y={minY} <Corner
corner={0} x={minX}
width={cp} y={minY}
height={cp} corner={0}
cursor="nwse-resize" width={cp}
/> height={cp}
<Corner cursor="nwse-resize"
x={maxX} />
y={minY} <Corner
corner={1} x={maxX}
width={cp} y={minY}
height={cp} corner={1}
cursor="nesw-resize" width={cp}
/> height={cp}
<Corner cursor="nesw-resize"
x={maxX} />
y={maxY} <Corner
corner={2} x={maxX}
width={cp} y={maxY}
height={cp} corner={2}
cursor="nwse-resize" width={cp}
/> height={cp}
<Corner cursor="nwse-resize"
x={minX} />
y={maxY} <Corner
corner={3} x={minX}
width={cp} y={maxY}
height={cp} corner={3}
cursor="nesw-resize" width={cp}
/> height={cp}
cursor="nesw-resize"
/>
</>
)}
<EdgeHorizontal <EdgeHorizontal
x={minX + p} x={minX + p}
y={minY} y={minY}
@ -65,11 +70,7 @@ export default function Bounds() {
if (e.buttons !== 1) return if (e.buttons !== 1) return
state.send("POINTED_BOUNDS_EDGE", { state.send("POINTED_BOUNDS_EDGE", {
edge: 0, edge: 0,
shiftKey: e.shiftKey, ...inputs.pointerDown(e),
optionKey: e.altKey,
metaKey: e.metaKey,
ctrlKey: e.ctrlKey,
buttons: e.buttons,
}) })
document.body.style.cursor = "ns-resize" document.body.style.cursor = "ns-resize"
}} }}
@ -84,11 +85,7 @@ export default function Bounds() {
if (e.buttons !== 1) return if (e.buttons !== 1) return
state.send("POINTED_BOUNDS_EDGE", { state.send("POINTED_BOUNDS_EDGE", {
edge: 1, edge: 1,
shiftKey: e.shiftKey, ...inputs.pointerDown(e),
optionKey: e.altKey,
metaKey: e.metaKey,
ctrlKey: e.ctrlKey,
buttons: e.buttons,
}) })
document.body.style.cursor = "ew-resize" document.body.style.cursor = "ew-resize"
}} }}
@ -103,11 +100,7 @@ export default function Bounds() {
if (e.buttons !== 1) return if (e.buttons !== 1) return
state.send("POINTED_BOUNDS_EDGE", { state.send("POINTED_BOUNDS_EDGE", {
edge: 2, edge: 2,
shiftKey: e.shiftKey, ...inputs.pointerDown(e),
optionKey: e.altKey,
metaKey: e.metaKey,
ctrlKey: e.ctrlKey,
buttons: e.buttons,
}) })
document.body.style.cursor = "ns-resize" document.body.style.cursor = "ns-resize"
}} }}
@ -122,11 +115,7 @@ export default function Bounds() {
if (e.buttons !== 1) return if (e.buttons !== 1) return
state.send("POINTED_BOUNDS_EDGE", { state.send("POINTED_BOUNDS_EDGE", {
edge: 3, edge: 3,
shiftKey: e.shiftKey, ...inputs.pointerDown(e),
optionKey: e.altKey,
metaKey: e.metaKey,
ctrlKey: e.ctrlKey,
buttons: e.buttons,
}) })
document.body.style.cursor = "ew-resize" document.body.style.cursor = "ew-resize"
}} }}
@ -168,11 +157,7 @@ function Corner({
if (e.buttons !== 1) return if (e.buttons !== 1) return
state.send("POINTED_ROTATE_CORNER", { state.send("POINTED_ROTATE_CORNER", {
corner, corner,
shiftKey: e.shiftKey, ...inputs.pointerDown(e),
optionKey: e.altKey,
metaKey: e.metaKey,
ctrlKey: e.ctrlKey,
buttons: e.buttons,
}) })
document.body.style.cursor = "grabbing" document.body.style.cursor = "grabbing"
}} }}
@ -190,18 +175,13 @@ function Corner({
if (e.buttons !== 1) return if (e.buttons !== 1) return
state.send("POINTED_BOUNDS_CORNER", { state.send("POINTED_BOUNDS_CORNER", {
corner, corner,
shiftKey: e.shiftKey, ...inputs.pointerDown(e),
optionKey: e.altKey,
metaKey: e.metaKey,
ctrlKey: e.ctrlKey,
buttons: e.buttons,
}) })
document.body.style.cursor = "nesw-resize" document.body.style.cursor = "nesw-resize"
}} }}
onPanEnd={restoreCursor} onPanEnd={restoreCursor}
onTap={restoreCursor} onTap={restoreCursor}
style={{ cursor }} style={{ cursor }}
className="strokewidth-ui stroke-bounds fill-corner"
/> />
</g> </g>
) )
@ -268,9 +248,9 @@ function EdgeVertical({
) )
} }
function restoreCursor() { function restoreCursor(e: PointerEvent) {
state.send("STOPPED_POINTING", { id: "bounds", ...inputs.pointerUp(e) })
document.body.style.cursor = "default" document.body.style.cursor = "default"
state.send("STOPPED_POINTING")
} }
const StyledEdge = styled(motion.rect, { const StyledEdge = styled(motion.rect, {

View file

@ -8,6 +8,7 @@ import Brush from "./brush"
import state from "state" import state from "state"
import Bounds from "./bounds" import Bounds from "./bounds"
import BoundsBg from "./bounds-bg" import BoundsBg from "./bounds-bg"
import inputs from "state/inputs"
export default function Canvas() { export default function Canvas() {
const rCanvas = useRef<SVGSVGElement>(null) const rCanvas = useRef<SVGSVGElement>(null)
@ -18,16 +19,16 @@ export default function Canvas() {
const handlePointerDown = useCallback((e: React.PointerEvent) => { const handlePointerDown = useCallback((e: React.PointerEvent) => {
rCanvas.current.setPointerCapture(e.pointerId) rCanvas.current.setPointerCapture(e.pointerId)
state.send("POINTED_CANVAS", getPointerEventInfo(e)) state.send("POINTED_CANVAS", inputs.pointerDown(e))
}, []) }, [])
const handlePointerMove = useCallback((e: React.PointerEvent) => { const handlePointerMove = useCallback((e: React.PointerEvent) => {
state.send("MOVED_POINTER", getPointerEventInfo(e)) state.send("MOVED_POINTER", inputs.pointerMove(e))
}, []) }, [])
const handlePointerUp = useCallback((e: React.PointerEvent) => { const handlePointerUp = useCallback((e: React.PointerEvent) => {
rCanvas.current.releasePointerCapture(e.pointerId) rCanvas.current.releasePointerCapture(e.pointerId)
state.send("STOPPED_POINTING", getPointerEventInfo(e)) state.send("STOPPED_POINTING", { id: "canvas", ...inputs.pointerUp(e) })
}, []) }, [])
return ( return (

View file

@ -1,6 +1,7 @@
import React, { useCallback, useRef, memo } from "react" import React, { useCallback, useRef, memo } from "react"
import state, { useSelector } from "state" import state, { useSelector } from "state"
import { getPointerEventInfo } from "utils/utils" import { getPointerEventInfo } from "utils/utils"
import inputs from "state/inputs"
import shapes from "lib/shapes" import shapes from "lib/shapes"
import styled from "styles" import styled from "styles"
@ -18,7 +19,7 @@ function Shape({ id }: { id: string }) {
(e: React.PointerEvent) => { (e: React.PointerEvent) => {
e.stopPropagation() e.stopPropagation()
rGroup.current.setPointerCapture(e.pointerId) rGroup.current.setPointerCapture(e.pointerId)
state.send("POINTED_SHAPE", { id, ...getPointerEventInfo(e) }) state.send("POINTED_SHAPE", { id, ...inputs.pointerDown(e) })
}, },
[id] [id]
) )
@ -27,20 +28,18 @@ function Shape({ id }: { id: string }) {
(e: React.PointerEvent) => { (e: React.PointerEvent) => {
e.stopPropagation() e.stopPropagation()
rGroup.current.releasePointerCapture(e.pointerId) rGroup.current.releasePointerCapture(e.pointerId)
state.send("STOPPED_POINTING_SHAPE", { id, ...getPointerEventInfo(e) }) state.send("STOPPED_POINTING", { id, ...inputs.pointerUp(e) })
}, },
[id] [id]
) )
const handlePointerEnter = useCallback( const handlePointerEnter = useCallback(
(e: React.PointerEvent) => (e: React.PointerEvent) => state.send("HOVERED_SHAPE", { id }),
state.send("HOVERED_SHAPE", { id, ...getPointerEventInfo(e) }),
[id] [id]
) )
const handlePointerLeave = useCallback( const handlePointerLeave = useCallback(
(e: React.PointerEvent) => (e: React.PointerEvent) => state.send("UNHOVERED_SHAPE", { id }),
state.send("UNHOVERED_SHAPE", { id, ...getPointerEventInfo(e) }),
[id] [id]
) )

View file

@ -56,7 +56,8 @@ const Circle: BaseLibShape<ShapeType.Circle> = {
return shape return shape
}, },
translate(shape) { translate(shape, delta) {
shape.point = vec.add(shape.point, delta)
return shape return shape
}, },

View file

@ -52,7 +52,8 @@ const Dot: BaseLibShape<ShapeType.Dot> = {
return shape return shape
}, },
translate(shape) { translate(shape, delta) {
shape.point = vec.add(shape.point, delta)
return shape return shape
}, },

View file

@ -61,7 +61,8 @@ const Polyline: BaseLibShape<ShapeType.Polyline> = {
return shape return shape
}, },
translate(shape) { translate(shape, delta) {
shape.point = vec.add(shape.point, delta)
return shape return shape
}, },

View file

@ -54,7 +54,8 @@ const Rectangle: BaseLibShape<ShapeType.Rectangle> = {
return shape return shape
}, },
translate(shape) { translate(shape, delta) {
shape.point = vec.add(shape.point, delta)
return shape return shape
}, },

View file

@ -4,30 +4,14 @@ import { Data } from "types"
export type CommandFn<T> = (data: T, initial?: boolean) => void export type CommandFn<T> = (data: T, initial?: boolean) => void
export enum CommandType {
ChangeBounds,
CreateGlob,
CreateNode,
Delete,
Split,
Move,
MoveAnchor,
ReorderGlobs,
ReorderNodes,
Paste,
ToggleCap,
ToggleLocked,
SetProperty,
SetItems,
Transform,
}
/** /**
* A command makes changes to some applicate state. Every command has an "undo" * A command makes changes to some applicate state. Every command has an "undo"
* method to reverse its changes. The apps history is a series of commands. * method to reverse its changes. The apps history is a series of commands.
*/ */
export class BaseCommand<T extends any> { export class BaseCommand<T extends any> {
timestamp = Date.now() timestamp = Date.now()
name: string
category: string
private undoFn: CommandFn<T> private undoFn: CommandFn<T>
private doFn: CommandFn<T> private doFn: CommandFn<T>
protected restoreBeforeSelectionState: (data: T) => void protected restoreBeforeSelectionState: (data: T) => void
@ -36,11 +20,14 @@ export class BaseCommand<T extends any> {
protected manualSelection: boolean protected manualSelection: boolean
constructor(options: { constructor(options: {
type: CommandType
do: CommandFn<T> do: CommandFn<T>
undo: CommandFn<T> undo: CommandFn<T>
name: string
category: string
manualSelection?: boolean manualSelection?: boolean
}) { }) {
this.name = options.name
this.category = options.category
this.doFn = options.do this.doFn = options.do
this.undoFn = options.undo this.undoFn = options.undo
this.manualSelection = options.manualSelection || false this.manualSelection = options.manualSelection || false
@ -87,8 +74,11 @@ export class BaseCommand<T extends any> {
* to mutate the state's data. Actions do not effect the "active states" in * to mutate the state's data. Actions do not effect the "active states" in
* the app. * the app.
*/ */
export class Command extends BaseCommand<Data> { export default class Command extends BaseCommand<Data> {
saveSelectionState = (data: Data) => { saveSelectionState = (data: Data) => {
return (data: Data) => {} const selectedIds = new Set(data.selectedIds)
return (data: Data) => {
data.selectedIds = selectedIds
}
} }
} }

View file

@ -1,7 +1,9 @@
import { Data } from "types" import { Data } from "types"
import { BaseCommand } from "./command" import { BaseCommand } from "./command"
class BaseHistory<T> { // A singleton to manage history changes.
class History<T> {
private stack: BaseCommand<T>[] = [] private stack: BaseCommand<T>[] = []
private pointer = -1 private pointer = -1
private maxLength = 100 private maxLength = 100
@ -44,7 +46,7 @@ class BaseHistory<T> {
if (typeof window === "undefined") return if (typeof window === "undefined") return
if (typeof localStorage === "undefined") return if (typeof localStorage === "undefined") return
localStorage.setItem("glob_aldata_v6", JSON.stringify(data)) localStorage.setItem("code_slate_0.0.1", JSON.stringify(data))
} }
disable = () => { disable = () => {
@ -60,4 +62,4 @@ class BaseHistory<T> {
} }
} }
export default new BaseHistory<Data>() export default new History<Data>()

5
state/commands/index.ts Normal file
View file

@ -0,0 +1,5 @@
import translate from "./translate-command"
const commands = { translate }
export default commands

View file

@ -0,0 +1,32 @@
import Command from "./command"
import history from "./history"
import { TranslateSnapshot } from "state/sessions/translate-session"
import { Data } from "types"
export default function translateCommand(
data: Data,
before: TranslateSnapshot,
after: TranslateSnapshot
) {
history.execute(
data,
new Command({
name: "translate_shapes",
category: "canvas",
do(data) {
const { shapes } = data.document.pages[after.currentPageId]
for (let { id, point } of after.shapes) {
shapes[id].point = point
}
},
undo(data) {
const { shapes } = data.document.pages[before.currentPageId]
for (let { id, point } of before.shapes) {
shapes[id].point = point
}
},
})
)
}

52
state/inputs.tsx Normal file
View file

@ -0,0 +1,52 @@
import { PointerInfo } from "types"
class Inputs {
points: Record<string, PointerInfo> = {}
pointerDown(e: PointerEvent | React.PointerEvent) {
const { shiftKey, ctrlKey, metaKey, altKey } = e
this.points[e.pointerId] = {
pointerId: e.pointerId,
origin: [e.clientX, e.clientY],
point: [e.clientX, e.clientY],
shiftKey,
ctrlKey,
metaKey,
altKey,
}
return this.points[e.pointerId]
}
pointerMove(e: PointerEvent | React.PointerEvent) {
if (this.points[e.pointerId]) {
this.points[e.pointerId].point = [e.clientX, e.clientY]
return this.points[e.pointerId]
}
const { shiftKey, ctrlKey, metaKey, altKey } = e
return {
pointerId: e.pointerId,
origin: [e.clientX, e.clientY],
point: [e.clientX, e.clientY],
shiftKey,
ctrlKey,
metaKey,
altKey,
}
}
pointerUp(e: PointerEvent | React.PointerEvent) {
this.points[e.pointerId].point = [e.clientX, e.clientY]
const info = this.points[e.pointerId]
delete this.points[e.pointerId]
return info
}
}
export default new Inputs()

View file

@ -3,15 +3,15 @@ import { Data } from "types"
export default class BaseSession { export default class BaseSession {
constructor(data: Data) {} constructor(data: Data) {}
update = (data: Data, ...args: unknown[]) => { update(data: Data, ...args: unknown[]) {
// Update the state // Update the state
} }
complete = (data: Data, ...args: unknown[]) => { complete(data: Data, ...args: unknown[]) {
// Create a command // Create a command
} }
cancel = (data: Data) => { cancel(data: Data) {
// Clean up the change // Clean up the change
} }
} }

View file

@ -1,4 +1,5 @@
import BaseSession from "./brush-session" import BaseSession from "./base-session"
import BrushSession from "./brush-session" import BrushSession from "./brush-session"
import TranslateSession from "./translate-session"
export { BrushSession, BaseSession } export { BrushSession, BaseSession, TranslateSession }

View file

@ -0,0 +1,62 @@
import { Data } from "types"
import * as vec from "utils/vec"
import BaseSession from "./base-session"
import commands from "state/commands"
import { current } from "immer"
export default class TranslateSession extends BaseSession {
delta = [0, 0]
origin: number[]
snapshot: TranslateSnapshot
constructor(data: Data, point: number[]) {
super(data)
this.origin = point
this.snapshot = getTranslateSnapshot(data)
}
update(data: Data, point: number[]) {
const { currentPageId, shapes } = this.snapshot
const { document } = data
const delta = vec.vec(this.origin, point)
for (let shape of shapes) {
document.pages[currentPageId].shapes[shape.id].point = vec.add(
shape.point,
delta
)
}
}
cancel(data: Data) {
const { document } = data
for (let shape of this.snapshot.shapes) {
document.pages[this.snapshot.currentPageId].shapes[shape.id].point =
shape.point
}
}
complete(data: Data) {
commands.translate(data, this.snapshot, getTranslateSnapshot(data))
}
}
export function getTranslateSnapshot(data: Data) {
const {
document: { pages },
currentPageId,
} = current(data)
const { shapes } = pages[currentPageId]
return {
currentPageId,
shapes: Array.from(data.selectedIds.values())
.map((id) => shapes[id])
.map(({ id, point }) => ({ id, point })),
}
}
export type TranslateSnapshot = ReturnType<typeof getTranslateSnapshot>

View file

@ -1,12 +1,13 @@
import { createSelectorHook, createState } from "@state-designer/react" import { createSelectorHook, createState } from "@state-designer/react"
import { clamp, getCommonBounds, screenToWorld } from "utils/utils" import { clamp, getCommonBounds, screenToWorld } from "utils/utils"
import * as vec from "utils/vec" import * as vec from "utils/vec"
import { Bounds, Data, Shape, ShapeType } from "types" import { Bounds, Data, PointerInfo, Shape, ShapeType } from "types"
import { defaultDocument } from "./data" import { defaultDocument } from "./data"
import Shapes from "lib/shapes" import Shapes from "lib/shapes"
import * as Sessions from "./sessions" import * as Sessions from "./sessions"
const initialData: Data = { const initialData: Data = {
isReadOnly: false,
camera: { camera: {
point: [0, 0], point: [0, 0],
zoom: 1, zoom: 1,
@ -31,36 +32,84 @@ const state = createState({
initial: "selecting", initial: "selecting",
states: { states: {
selecting: { selecting: {
on: { initial: "notPointing",
POINTED_CANVAS: { to: "brushSelecting" }, states: {
POINTED_SHAPE: [ notPointing: {
"setPointedId", on: {
{ POINTED_CANVAS: { to: "brushSelecting" },
if: "isPressingShiftKey", POINTED_BOUNDS: { to: "pointingBounds" },
then: { POINTED_SHAPE: [
if: "isPointedShapeSelected", "setPointedId",
do: "pullPointedIdFromSelectedIds", {
else: "pushPointedIdToSelectedIds", if: "isPressingShiftKey",
}, then: {
else: ["clearSelectedIds", "pushPointedIdToSelectedIds"], if: "isPointedShapeSelected",
do: "pullPointedIdFromSelectedIds",
else: {
do: "pushPointedIdToSelectedIds",
to: "pointingBounds",
},
},
else: [
{
unless: "isPointedShapeSelected",
do: ["clearSelectedIds", "pushPointedIdToSelectedIds"],
},
{
to: "pointingBounds",
},
],
},
],
}, },
], },
}, pointingBounds: {
}, on: {
brushSelecting: { STOPPED_POINTING: [
onEnter: [ {
{ unless: "isPressingShiftKey", do: "clearSelectedIds" }, unless: "isPressingShiftKey",
"startBrushSession", do: ["clearSelectedIds", "pushPointedIdToSelectedIds"],
], },
on: { { to: "notPointing" },
MOVED_POINTER: "updateBrushSession", ],
PANNED_CAMERA: "updateBrushSession", MOVED_POINTER: {
STOPPED_POINTING: { do: "completeSession", to: "selecting" }, unless: "isReadOnly",
CANCELLED: { do: "cancelSession", to: "selecting" }, if: "distanceImpliesDrag",
to: "draggingSelection",
},
},
},
draggingSelection: {
onEnter: "startTranslateSession",
on: {
MOVED_POINTER: "updateTranslateSession",
PANNED_CAMERA: "updateTranslateSession",
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
CANCELLED: { do: "cancelSession", to: "selecting" },
},
},
brushSelecting: {
onEnter: [
{ unless: "isPressingShiftKey", do: "clearSelectedIds" },
"startBrushSession",
],
on: {
MOVED_POINTER: "updateBrushSession",
PANNED_CAMERA: "updateBrushSession",
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
CANCELLED: { do: "cancelSession", to: "selecting" },
},
},
}, },
}, },
}, },
conditions: { conditions: {
isReadOnly(data) {
return data.isReadOnly
},
distanceImpliesDrag(data, payload: PointerInfo) {
return vec.dist2(payload.origin, payload.point) > 16
},
isPointedShapeSelected(data) { isPointedShapeSelected(data) {
return data.selectedIds.has(data.pointedId) return data.selectedIds.has(data.pointedId)
}, },
@ -77,6 +126,7 @@ const state = createState({
session.complete(data) session.complete(data)
session = undefined session = undefined
}, },
// Brushing
startBrushSession(data, payload: { point: number[] }) { startBrushSession(data, payload: { point: number[] }) {
session = new Sessions.BrushSession( session = new Sessions.BrushSession(
data, data,
@ -86,6 +136,17 @@ const state = createState({
updateBrushSession(data, payload: { point: number[] }) { updateBrushSession(data, payload: { point: number[] }) {
session.update(data, screenToWorld(payload.point, data)) session.update(data, screenToWorld(payload.point, data))
}, },
// Dragging / Translating
startTranslateSession(data, payload: { point: number[] }) {
session = new Sessions.TranslateSession(
data,
screenToWorld(payload.point, data)
)
},
updateTranslateSession(data, payload: { point: number[] }) {
session.update(data, screenToWorld(payload.point, data))
},
// Selection // Selection
setPointedId(data, payload: { id: string }) { setPointedId(data, payload: { id: string }) {
data.pointedId = payload.id data.pointedId = payload.id

View file

@ -1,6 +1,7 @@
import React from "react" import React from "react"
export interface Data { export interface Data {
isReadOnly: boolean
camera: { camera: {
point: number[] point: number[]
zoom: number zoom: number
@ -124,8 +125,18 @@ export type BaseLibShape<K extends ShapeType> = {
getBounds(shape: Shapes[K]): Bounds getBounds(shape: Shapes[K]): Bounds
hitTest(shape: Shapes[K], test: number[]): boolean hitTest(shape: Shapes[K], test: number[]): boolean
rotate(shape: Shapes[K]): Shapes[K] rotate(shape: Shapes[K]): Shapes[K]
translate(shape: Shapes[K]): Shapes[K] translate(shape: Shapes[K], delta: number[]): Shapes[K]
scale(shape: Shapes[K], scale: number): Shapes[K] scale(shape: Shapes[K], scale: number): Shapes[K]
stretch(shape: Shapes[K], scaleX: number, scaleY: number): Shapes[K] stretch(shape: Shapes[K], scaleX: number, scaleY: number): Shapes[K]
render(shape: Shapes[K]): JSX.Element render(shape: Shapes[K]): JSX.Element
} }
export interface PointerInfo {
pointerId: number
origin: number[]
point: number[]
shiftKey: boolean
ctrlKey: boolean
metaKey: boolean
altKey: boolean
}