make straight lines

pull/82/head
Override-6 2 years ago committed by maxime.batista
parent de8fbdb3a2
commit df3e60ca1a

@ -1,52 +1,47 @@
import "../../style/actions/arrow_action.css" import "../../style/actions/arrow_action.css"
import Draggable from "react-draggable" import Draggable from "react-draggable"
import {RefObject, useRef} from "react"
import Xarrow, {useXarrow, Xwrapper} from "react-xarrows" import {useRef} from "react"
export interface ArrowActionProps { export interface ArrowActionProps {
originRef: RefObject<HTMLDivElement> onHeadDropped: (headBounds: DOMRect) => void
onArrowDropped: (arrowHead: DOMRect) => void onHeadPicked: (headBounds: DOMRect) => void,
onHeadMoved: (headBounds: DOMRect) => void,
} }
export default function ArrowAction({ export default function ArrowAction({
originRef, onHeadDropped,
onArrowDropped, onHeadPicked,
onHeadMoved
}: ArrowActionProps) { }: ArrowActionProps) {
const arrowHeadRef = useRef<HTMLDivElement>(null) const arrowHeadRef = useRef<HTMLDivElement>(null)
const updateXarrow = useXarrow()
return ( return (
<div className="arrow-action"> <div className="arrow-action">
<div className="arrow-action-pin"/> <div className="arrow-action-pin"/>
<Xwrapper>
<Draggable <Draggable
nodeRef={arrowHeadRef} nodeRef={arrowHeadRef}
onDrag={updateXarrow} onStart={() => {
const headBounds =
arrowHeadRef.current!.getBoundingClientRect()
onHeadPicked(headBounds)
}}
onStop={() => { onStop={() => {
const headBounds = const headBounds =
arrowHeadRef.current!.getBoundingClientRect() arrowHeadRef.current!.getBoundingClientRect()
updateXarrow() onHeadDropped(headBounds)
onArrowDropped(headBounds) }}
onDrag={() => {
const headBounds =
arrowHeadRef.current!.getBoundingClientRect()
onHeadMoved(headBounds)
}} }}
position={{x: 0, y: 0}}> position={{x: 0, y: 0}}>
<div <div
style={{
position: "absolute",
}}
ref={arrowHeadRef} ref={arrowHeadRef}
className="arrow-head-pick" className="arrow-head-pick"/>
onMouseDown={updateXarrow}/>
</Draggable> </Draggable>
<div className={"arrow-head-xarrow"}>
<Xarrow
start={originRef}
end={arrowHeadRef}
startAnchor={"auto"}
/>
</div>
</Xwrapper>
</div> </div>
) )
} }

@ -1,4 +1,6 @@
import {BallPiece} from "../editor/BallPiece"; import {BallPiece} from "../editor/BallPiece";
import Draggable from "react-draggable";
import {useRef} from "react";
export interface BallActionProps { export interface BallActionProps {
@ -6,7 +8,15 @@ export interface BallActionProps {
} }
export default function BallAction({onDrop}: BallActionProps) { export default function BallAction({onDrop}: BallActionProps) {
const ref = useRef<HTMLDivElement>(null)
return ( return (
<BallPiece onDrop={onDrop} /> <Draggable
onStop={() => onDrop(ref.current!)}
nodeRef={ref}
>
<div ref={ref}>
<BallPiece/>
</div>
</Draggable>
) )
} }

@ -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<SVGSVGElement>(null)
const pathRef = useRef<SVGPathElement>(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 (
<svg ref={svgRef} style={{
overflow: "visible",
position: "absolute",
}}>
<path
ref={pathRef}
stroke={"#000"}
strokeWidth={styleWidth} />
</svg>
)
}

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

@ -1,23 +1,27 @@
import "../../style/basket_court.css" import "../../style/basket_court.css"
import {ReactElement, RefObject} from "react"
import {ReactElement, RefObject, useState} from "react"
import CourtPlayer from "./CourtPlayer" import CourtPlayer from "./CourtPlayer"
import {Player} from "../../tactic/Player" import {Player} from "../../tactic/Player"
import {Action, MovementActionKind} from "../../tactic/Action" import {Action, MovementActionKind} from "../../tactic/Action"
import RemoveAction from "../actions/RemoveAction" import RemoveAction from "../actions/RemoveAction"
import ArrowAction from "../actions/ArrowAction" import ArrowAction from "../actions/ArrowAction"
import {useXarrow} from "react-xarrows" 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 {CourtObject} from "../../tactic/CourtObjects";
import {CourtBall} from "./CourtBall"; import {CourtBall} from "./CourtBall";
import BallAction from "../actions/BallAction";
export interface BasketCourtProps { export interface BasketCourtProps {
players: Player[] players: Player[]
actions: Action[] actions: Action[]
objects: CourtObject[] objects: CourtObject[]
renderAction: (a: Action) => ReactElement renderAction: (courtBounds: DOMRect, a: Action, idx: number) => ReactElement
setActions: (f: (a: Action[]) => Action[]) => void setActions: (f: (a: Action[]) => Action[]) => void
onPlayerRemove: (p: Player) => void onPlayerRemove: (p: Player) => void
onBallDrop: (ref: HTMLElement) => void
onPlayerChange: (p: Player) => void onPlayerChange: (p: Player) => void
onBallRemove: () => void onBallRemove: () => void
@ -29,25 +33,22 @@ export interface BasketCourtProps {
} }
export function BasketCourt({ export function BasketCourt({
players,
objects, objects,
onBallMoved,
onBallRemove,
players,
actions, actions,
renderAction, renderAction,
setActions, setActions,
onBallDrop,
onPlayerRemove, onPlayerRemove,
onBallRemove,
onBallMoved,
onPlayerChange, onPlayerChange,
courtImage, courtImage,
courtRef, courtRef,
}: BasketCourtProps) { }: BasketCourtProps) {
function bindArrowToPlayer(
originRef: RefObject<HTMLDivElement>, function bindArrowToPlayer(originRef: HTMLElement, arrowHead: DOMRect) {
arrowHead: DOMRect,
) {
for (const player of players) { for (const player of players) {
if (player.id == originRef.current!.id) { if (player.id == originRef.id) {
continue continue
} }
@ -65,7 +66,7 @@ export function BasketCourt({
) { ) {
const action = { const action = {
type: MovementActionKind.SCREEN, type: MovementActionKind.SCREEN,
moveFrom: originRef.current!.id, moveFrom: originRef.id,
moveTo: player.id, moveTo: player.id,
} }
setActions((actions) => [...actions, action]) setActions((actions) => [...actions, action])
@ -75,9 +76,18 @@ export function BasketCourt({
const updateArrows = useXarrow() const updateArrows = useXarrow()
const [previewArrowOriginPos, setPreviewArrowOriginPos] =
useState<Pos>(NULL_POS)
const [previewArrowEndPos, setPreviewArrowEndPos] = useState<Pos>(NULL_POS)
const [isPreviewArrowEnabled, setPreviewArrowEnabled] = useState(false)
return ( return (
<div id="court-container" ref={courtRef} style={{position: "relative"}}> <div id="court-container" ref={courtRef} style={{position: "relative"}}>
<img src={courtImage} alt={"court"} id="court-svg"/> <img src={courtImage} alt={"court"} id="court-svg"/>
{actions.map((action, idx) => renderAction(courtRef.current!.getBoundingClientRect(), action, idx))}
{players.map((player) => ( {players.map((player) => (
<CourtPlayer <CourtPlayer
key={player.id} key={player.id}
@ -93,12 +103,22 @@ export function BasketCourt({
/>, />,
<ArrowAction <ArrowAction
key={2} key={2}
originRef={pieceRef} onHeadMoved={(headPos) =>
onArrowDropped={(headRect) => setPreviewArrowEndPos(middlePos(headPos))
bindArrowToPlayer(pieceRef, headRect)
} }
onHeadPicked={(headRef) => {
setPreviewArrowOriginPos(
middlePos(pieceRef.getBoundingClientRect()),
)
setPreviewArrowEndPos(middlePos(headRef))
setPreviewArrowEnabled(true)
}}
onHeadDropped={(headRect) => {
setPreviewArrowEnabled(false)
bindArrowToPlayer(pieceRef, headRect)
}}
/>, />,
player.hasBall && <BallAction key={3} onDrop={onBallDrop}/> player.hasBall && <BallAction key={3} onDrop={ref => onBallMoved(ref.getBoundingClientRect())}/>
]} ]}
/> />
))} ))}
@ -117,7 +137,13 @@ export function BasketCourt({
throw new Error("unknown court object", object.type) throw new Error("unknown court object", object.type)
})} })}
{actions.map(renderAction)} {isPreviewArrowEnabled && (
<BendableArrow
basePos={courtRef.current!.getBoundingClientRect()}
startPos={previewArrowOriginPos}
endPos={previewArrowEndPos}
/>
)}
</div> </div>
) )
} }

@ -11,8 +11,8 @@ export interface PlayerProps<A extends ReactNode> {
onDrag: () => void, onDrag: () => void,
onChange: (p: Player) => void onChange: (p: Player) => void
onRemove: () => void onRemove: () => void
parentRef: RefObject<HTMLDivElement> parentRef: RefObject<HTMLElement>
availableActions: (ro: RefObject<HTMLDivElement>) => A[] availableActions: (ro: HTMLElement) => A[]
} }
/** /**
@ -66,7 +66,7 @@ export default function CourtPlayer<A extends ReactNode>({
onKeyUp={(e) => { onKeyUp={(e) => {
if (e.key == "Delete") onRemove() if (e.key == "Delete") onRemove()
}}> }}>
<div className="player-actions">{availableActions(pieceRef)}</div> <div className="player-actions">{availableActions(pieceRef.current!)}</div>
<PlayerPiece team={player.team} text={player.role} hasBall={hasBall} /> <PlayerPiece team={player.team} text={player.role} hasBall={hasBall} />
</div> </div>
</div> </div>

@ -18,16 +18,16 @@
} }
.arrow-head-xarrow { .arrow-head-xarrow {
visibility: hidden; visibility: visible;
} }
.arrow-action:active .arrow-head-xarrow { .arrow-action:active .arrow-head-xarrow {
visibility: visible; visibility: visible;
} }
.arrow-action:active .arrow-head-pick { /*.arrow-action:active .arrow-head-pick {*/
min-height: unset; /* min-height: unset;*/
min-width: unset; /* min-width: unset;*/
width: 0; /* width: 0;*/
height: 0; /* height: 0;*/
} /*}*/

@ -18,7 +18,7 @@ import {Team} from "../tactic/Team"
import {calculateRatio} from "../Utils" import {calculateRatio} from "../Utils"
import SavingState, {SaveState, SaveStates,} from "../components/editor/SavingState" import SavingState, {SaveState, SaveStates,} from "../components/editor/SavingState"
import {renderAction} from "./editor/ActionsRender" import {ActionRender} from "./editor/ActionsRender"
import {CourtObject} from "../tactic/CourtObjects" import {CourtObject} from "../tactic/CourtObjects"
@ -417,12 +417,13 @@ function EditorView({
courtRef={courtDivContentRef} courtRef={courtDivContentRef}
actions={content.actions} actions={content.actions}
setActions={(actions) => setActions={(actions) =>
setContent((c) => ({ setContent((content) => ({
players: c.players, ...content,
actions: actions(c.actions), players: content.players,
actions: actions(content.actions),
})) }))
} }
renderAction={renderAction} renderAction={(basePos, action, idx) => <ActionRender key={idx} basePos={basePos} action={action}/>}
onPlayerChange={(player) => { onPlayerChange={(player) => {
const playerBounds = document const playerBounds = document
.getElementById(player.id)! .getElementById(player.id)!

@ -1,35 +1,24 @@
import { Action, MovementActionKind } from "../../tactic/Action" import { Action, MovementActionKind } from "../../tactic/Action"
import Xarrow, { Xwrapper } from "react-xarrows" import Xarrow, { Xwrapper } from "react-xarrows"
import { xarrowPropsType } from "react-xarrows/lib/types" 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 from = action.moveFrom;
const to = action.moveTo; const to = action.moveTo;
let arrowStyle: xarrowPropsType = {start: from, end: to, color: "var(--arrows-color)"} const fromPos = document.getElementById(from)!.getBoundingClientRect()
const toPos = document.getElementById(to)!.getBoundingClientRect()
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:
}
return ( return (
<Xwrapper key={`${action.type}-${from}-${to}`}> <BendableArrow
<Xarrow {...arrowStyle}/> key={`${action.type}-${from}-${to}`}
</Xwrapper> basePos={basePos}
startPos={middlePos(fromPos)}
endPos={middlePos(toPos)}
/>
) )
} }

@ -33,6 +33,7 @@
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-react": "^4.1.0", "@vitejs/plugin-react": "^4.1.0",
"eslint-plugin-react-hooks": "^4.6.0",
"prettier": "^3.1.0", "prettier": "^3.1.0",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite-plugin-svgr": "^4.1.0" "vite-plugin-svgr": "^4.1.0"

Loading…
Cancel
Save