diff --git a/front/components/actions/ArrowAction.tsx b/front/components/actions/ArrowAction.tsx index e52670e..286d0aa 100644 --- a/front/components/actions/ArrowAction.tsx +++ b/front/components/actions/ArrowAction.tsx @@ -1,52 +1,47 @@ import "../../style/actions/arrow_action.css" import Draggable from "react-draggable" -import {RefObject, useRef} from "react" -import Xarrow, {useXarrow, Xwrapper} from "react-xarrows" + +import {useRef} from "react" export interface ArrowActionProps { - originRef: RefObject - onArrowDropped: (arrowHead: DOMRect) => void + onHeadDropped: (headBounds: DOMRect) => void + onHeadPicked: (headBounds: DOMRect) => void, + onHeadMoved: (headBounds: DOMRect) => void, } export default function ArrowAction({ - originRef, - onArrowDropped, + onHeadDropped, + onHeadPicked, + onHeadMoved }: ArrowActionProps) { const arrowHeadRef = useRef(null) - const updateXarrow = useXarrow() return (
- - { - const headBounds = - arrowHeadRef.current!.getBoundingClientRect() - updateXarrow() - onArrowDropped(headBounds) - }} - position={{x: 0, y: 0}}> -
- - -
- -
- + { + const headBounds = + arrowHeadRef.current!.getBoundingClientRect() + onHeadPicked(headBounds) + }} + onStop={() => { + const headBounds = + arrowHeadRef.current!.getBoundingClientRect() + onHeadDropped(headBounds) + }} + onDrag={() => { + const headBounds = + arrowHeadRef.current!.getBoundingClientRect() + onHeadMoved(headBounds) + }} + position={{x: 0, y: 0}}> +
+
) } diff --git a/front/components/actions/BallAction.tsx b/front/components/actions/BallAction.tsx index a18659e..5dc8c78 100644 --- a/front/components/actions/BallAction.tsx +++ b/front/components/actions/BallAction.tsx @@ -1,4 +1,6 @@ import {BallPiece} from "../editor/BallPiece"; +import Draggable from "react-draggable"; +import {useRef} from "react"; export interface BallActionProps { @@ -6,7 +8,15 @@ export interface BallActionProps { } export default function BallAction({onDrop}: BallActionProps) { + const ref = useRef(null) return ( - + onDrop(ref.current!)} + nodeRef={ref} + > +
+ +
+
) } \ No newline at end of file diff --git a/front/components/arrows/BendableArrow.tsx b/front/components/arrows/BendableArrow.tsx new file mode 100644 index 0000000..33540fd --- /dev/null +++ b/front/components/arrows/BendableArrow.tsx @@ -0,0 +1,81 @@ +import { CSSProperties, ReactElement, useCallback, useEffect, useRef } from "react" +import { add, Pos, relativeTo, size } from "./Pos" + +export interface BendableArrowProps { + basePos: Pos + startPos: Pos + endPos: Pos + + startRadius?: number + endRadius?: number + + + style?: ArrowStyle +} + +export interface ArrowStyle { + width?: number, + head?: () => ReactElement, + tail?: () => ReactElement, +} + + +const ArrowStyleDefaults = { + width: 4 +} + +export default function BendableArrow({ basePos, startPos, endPos, style, startRadius = 0, endRadius = 0 }: BendableArrowProps) { + const svgRef = useRef(null) + + const pathRef = useRef(null); + + const styleWidth = style?.width ?? ArrowStyleDefaults.width + + + const update = () => { + const startRelative = relativeTo(startPos, basePos) + const endRelative = relativeTo(endPos, basePos) + + // the width and height of the arrow svg + const svgBoxBounds = size(startPos, endPos) + + const left = Math.min(startRelative.x, endRelative.x) + const top = Math.min(startRelative.y, endRelative.y) + + + const svgStyle: CSSProperties = { + width: `${svgBoxBounds.x}px`, + height: `${svgBoxBounds.y}px`, + + left: `${left}px`, + top: `${top}px`, + } + + + const d = `M${startRelative.x - left} ${startRelative.y - top} L${endRelative.x - left} ${endRelative.y - top}` + pathRef.current!.setAttribute("d", d) + + Object.assign(svgRef.current!.style, svgStyle) + } + + useEffect(() => { + //update on resize + window.addEventListener('resize', update) + + return () => window.removeEventListener('resize', update) + }, [svgRef, basePos, startPos, endPos]) + //update on position changes + useEffect(update, [svgRef, basePos, startPos, endPos]) + + return ( + + + + ) +} diff --git a/front/components/arrows/Pos.ts b/front/components/arrows/Pos.ts new file mode 100644 index 0000000..107aee2 --- /dev/null +++ b/front/components/arrows/Pos.ts @@ -0,0 +1,36 @@ +export interface Pos { + x: number + y: number +} + +export const NULL_POS: Pos = { x: 0, y: 0 } + +/** + * Returns position of a relative to b + * @param a + * @param b + */ +export function relativeTo(a: Pos, b: Pos): Pos { + return { x: a.x - b.x, y: a.y - b.y } +} + +/** + * Returns the middle position of the given rectangle + * @param rect + */ +export function middlePos(rect: DOMRect): Pos { + return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 } +} + +/** + * Returns x and y distance between given two pos + * @param a + * @param b + */ +export function size(a: Pos, b: Pos): Pos { + return { x: Math.abs(a.x - b.x), y: Math.abs(a.y - b.y) } +} + +export function add(a: Pos, b: Pos): Pos { + return { x: a.x + b.x, y: a.y + b.y } +} diff --git a/front/components/editor/BasketCourt.tsx b/front/components/editor/BasketCourt.tsx index 89b53e9..8f49c51 100644 --- a/front/components/editor/BasketCourt.tsx +++ b/front/components/editor/BasketCourt.tsx @@ -1,23 +1,27 @@ import "../../style/basket_court.css" -import {ReactElement, RefObject} from "react" + +import {ReactElement, RefObject, useState} from "react" import CourtPlayer from "./CourtPlayer" import {Player} from "../../tactic/Player" import {Action, MovementActionKind} from "../../tactic/Action" import RemoveAction from "../actions/RemoveAction" import ArrowAction from "../actions/ArrowAction" + import {useXarrow} from "react-xarrows" -import BallAction from "../actions/BallAction"; + +import BendableArrow from "../arrows/BendableArrow" +import {middlePos, NULL_POS, Pos} from "../arrows/Pos" import {CourtObject} from "../../tactic/CourtObjects"; import {CourtBall} from "./CourtBall"; +import BallAction from "../actions/BallAction"; export interface BasketCourtProps { players: Player[] actions: Action[] objects: CourtObject[] - renderAction: (a: Action) => ReactElement + renderAction: (courtBounds: DOMRect, a: Action, idx: number) => ReactElement setActions: (f: (a: Action[]) => Action[]) => void onPlayerRemove: (p: Player) => void - onBallDrop: (ref: HTMLElement) => void onPlayerChange: (p: Player) => void onBallRemove: () => void @@ -29,25 +33,22 @@ export interface BasketCourtProps { } export function BasketCourt({ - players, - objects, - actions, - renderAction, - setActions, - onBallDrop, - onPlayerRemove, - onBallRemove, - onBallMoved, - onPlayerChange, - courtImage, - courtRef, -}: BasketCourtProps) { - function bindArrowToPlayer( - originRef: RefObject, - arrowHead: DOMRect, - ) { + objects, + onBallMoved, + onBallRemove, + players, + actions, + renderAction, + setActions, + onPlayerRemove, + onPlayerChange, + courtImage, + courtRef, + }: BasketCourtProps) { + + function bindArrowToPlayer(originRef: HTMLElement, arrowHead: DOMRect) { for (const player of players) { - if (player.id == originRef.current!.id) { + if (player.id == originRef.id) { continue } @@ -65,7 +66,7 @@ export function BasketCourt({ ) { const action = { type: MovementActionKind.SCREEN, - moveFrom: originRef.current!.id, + moveFrom: originRef.id, moveTo: player.id, } setActions((actions) => [...actions, action]) @@ -75,9 +76,18 @@ export function BasketCourt({ const updateArrows = useXarrow() + const [previewArrowOriginPos, setPreviewArrowOriginPos] = + useState(NULL_POS) + const [previewArrowEndPos, setPreviewArrowEndPos] = useState(NULL_POS) + const [isPreviewArrowEnabled, setPreviewArrowEnabled] = useState(false) + return ( -
- {"court"} +
+ {"court"} + + {actions.map((action, idx) => renderAction(courtRef.current!.getBoundingClientRect(), action, idx))} + + {players.map((player) => ( , - bindArrowToPlayer(pieceRef, headRect) + onHeadMoved={(headPos) => + setPreviewArrowEndPos(middlePos(headPos)) } + onHeadPicked={(headRef) => { + setPreviewArrowOriginPos( + middlePos(pieceRef.getBoundingClientRect()), + ) + setPreviewArrowEndPos(middlePos(headRef)) + setPreviewArrowEnabled(true) + }} + onHeadDropped={(headRect) => { + setPreviewArrowEnabled(false) + bindArrowToPlayer(pieceRef, headRect) + }} />, - player.hasBall && + player.hasBall && onBallMoved(ref.getBoundingClientRect())}/> ]} /> ))} @@ -117,7 +137,13 @@ export function BasketCourt({ throw new Error("unknown court object", object.type) })} - {actions.map(renderAction)} + {isPreviewArrowEnabled && ( + + )}
) } diff --git a/front/components/editor/CourtPlayer.tsx b/front/components/editor/CourtPlayer.tsx index b3577cf..9df09b7 100644 --- a/front/components/editor/CourtPlayer.tsx +++ b/front/components/editor/CourtPlayer.tsx @@ -11,8 +11,8 @@ export interface PlayerProps { onDrag: () => void, onChange: (p: Player) => void onRemove: () => void - parentRef: RefObject - availableActions: (ro: RefObject) => A[] + parentRef: RefObject + availableActions: (ro: HTMLElement) => A[] } /** @@ -66,7 +66,7 @@ export default function CourtPlayer({ onKeyUp={(e) => { if (e.key == "Delete") onRemove() }}> -
{availableActions(pieceRef)}
+
{availableActions(pieceRef.current!)}
diff --git a/front/style/actions/arrow_action.css b/front/style/actions/arrow_action.css index 0588326..15b2bc5 100644 --- a/front/style/actions/arrow_action.css +++ b/front/style/actions/arrow_action.css @@ -18,16 +18,16 @@ } .arrow-head-xarrow { - visibility: hidden; + visibility: visible; } .arrow-action:active .arrow-head-xarrow { visibility: visible; } -.arrow-action:active .arrow-head-pick { - min-height: unset; - min-width: unset; - width: 0; - height: 0; -} \ No newline at end of file +/*.arrow-action:active .arrow-head-pick {*/ +/* min-height: unset;*/ +/* min-width: unset;*/ +/* width: 0;*/ +/* height: 0;*/ +/*}*/ \ No newline at end of file diff --git a/front/views/Editor.tsx b/front/views/Editor.tsx index ca06435..11c9fc5 100644 --- a/front/views/Editor.tsx +++ b/front/views/Editor.tsx @@ -18,7 +18,7 @@ import {Team} from "../tactic/Team" import {calculateRatio} from "../Utils" import SavingState, {SaveState, SaveStates,} from "../components/editor/SavingState" -import {renderAction} from "./editor/ActionsRender" +import {ActionRender} from "./editor/ActionsRender" import {CourtObject} from "../tactic/CourtObjects" @@ -417,12 +417,13 @@ function EditorView({ courtRef={courtDivContentRef} actions={content.actions} setActions={(actions) => - setContent((c) => ({ - players: c.players, - actions: actions(c.actions), + setContent((content) => ({ + ...content, + players: content.players, + actions: actions(content.actions), })) } - renderAction={renderAction} + renderAction={(basePos, action, idx) => } onPlayerChange={(player) => { const playerBounds = document .getElementById(player.id)! diff --git a/front/views/editor/ActionsRender.tsx b/front/views/editor/ActionsRender.tsx index 23c2ab8..3e93165 100644 --- a/front/views/editor/ActionsRender.tsx +++ b/front/views/editor/ActionsRender.tsx @@ -1,35 +1,24 @@ import { Action, MovementActionKind } from "../../tactic/Action" import Xarrow, { Xwrapper } from "react-xarrows" import { xarrowPropsType } from "react-xarrows/lib/types" +import BendableArrow from "../../components/arrows/BendableArrow" +import { middlePos, Pos } from "../../components/arrows/Pos" -export function renderAction(action: Action) { +export function ActionRender({basePos, action}: {basePos: Pos, action: Action}) { const from = action.moveFrom; const to = action.moveTo; - let arrowStyle: xarrowPropsType = {start: from, end: to, color: "var(--arrows-color)"} - - switch (action.type) { - case MovementActionKind.DRIBBLE: - arrowStyle.dashness = { - animation: true, - strokeLen: 5, - nonStrokeLen: 5 - } - break - case MovementActionKind.SCREEN: - arrowStyle.headShape = "circle" - arrowStyle.headSize = 2.5 - break - case MovementActionKind.MOVE: - } - - + const fromPos = document.getElementById(from)!.getBoundingClientRect() + const toPos = document.getElementById(to)!.getBoundingClientRect() return ( - - - + ) } diff --git a/package.json b/package.json index f3399c3..9c3a9d8 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ }, "devDependencies": { "@vitejs/plugin-react": "^4.1.0", + "eslint-plugin-react-hooks": "^4.6.0", "prettier": "^3.1.0", "typescript": "^5.2.2", "vite-plugin-svgr": "^4.1.0"