reset arrow curves by deleting the central control point

pull/82/head
maxime 1 year ago committed by maxime.batista
parent 6c92471062
commit afd7b0570c

@ -38,9 +38,7 @@ export default function ArrowAction({
onHeadMoved(headBounds) onHeadMoved(headBounds)
}} }}
position={{ x: 0, y: 0 }}> position={{ x: 0, y: 0 }}>
<div <div ref={arrowHeadRef} className="arrow-head-pick" />
ref={arrowHeadRef}
className="arrow-head-pick"/>
</Draggable> </Draggable>
</div> </div>
) )

@ -20,3 +20,4 @@ export default function BallAction({onDrop}: BallActionProps) {
</Draggable> </Draggable>
) )
} }

@ -1,4 +1,11 @@
import {CSSProperties, ReactElement, useCallback, useEffect, useRef, useState,} from "react" import {
CSSProperties,
ReactElement,
useCallback,
useEffect,
useRef,
useState,
} from "react"
import { angle, middlePos, Pos, relativeTo } from "./Pos" import { angle, middlePos, Pos, relativeTo } from "./Pos"
import "../../style/bendable_arrows.css" import "../../style/bendable_arrows.css"
@ -39,7 +46,6 @@ function constraintInCircle(pos: Pos, from: Pos, radius: number): Pos {
} }
} }
function Triangle({ fill }: { fill: string }) { function Triangle({ fill }: { fill: string }) {
return ( return (
<svg viewBox={"0 0 50 50"} width={20} height={20}> <svg viewBox={"0 0 50 50"} width={20} height={20}>
@ -58,8 +64,6 @@ export default function BendableArrow({
startRadius = 0, startRadius = 0,
endRadius = 0, endRadius = 0,
}: BendableArrowProps) { }: BendableArrowProps) {
const containerRef = useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null)
const svgRef = useRef<SVGSVGElement>(null) const svgRef = useRef<SVGSVGElement>(null)
@ -73,34 +77,31 @@ export default function BendableArrow({
useEffect(() => { useEffect(() => {
setInternalSegments(segments) setInternalSegments(segments)
}, [segments]); }, [segments])
const [internalSegments, setInternalSegments] = useState(segments) const [internalSegments, setInternalSegments] = useState(segments)
const [isSelected, setIsSelected] = useState(false) const [isSelected, setIsSelected] = useState(false)
const headRef = useRef<HTMLDivElement>(null) const headRef = useRef<HTMLDivElement>(null)
const tailRef = useRef<HTMLDivElement>(null) const tailRef = useRef<HTMLDivElement>(null)
useEffect(() => { useEffect(() => {
const basePos =
const basePos = containerRef.current!.parentElement!.getBoundingClientRect() containerRef.current!.parentElement!.getBoundingClientRect()
setControlPointsDots(computeControlPoints(basePos)) setControlPointsDots(computeControlPoints(basePos))
const selectionHandler = (e: MouseEvent) => { const selectionHandler = (e: MouseEvent) => {
if (!(e.target instanceof Node)) if (!(e.target instanceof Node)) return
return
setIsSelected(containerRef.current!.contains(e.target)) const isSelected = containerRef.current!.contains(e.target)
setIsSelected(isSelected)
} }
document.addEventListener('mousedown', selectionHandler) document.addEventListener("mousedown", selectionHandler)
return () => document.removeEventListener('mousedown', selectionHandler) return () => document.removeEventListener("mousedown", selectionHandler)
}, [])
}, []);
function computeControlPoints(basePos: Pos) { function computeControlPoints(basePos: Pos) {
return internalSegments.map(({ next, controlPoint }, i) => { return internalSegments.map(({ next, controlPoint }, i) => {
@ -109,8 +110,9 @@ export default function BendableArrow({
const prevRelative = relativeTo(prev, basePos) const prevRelative = relativeTo(prev, basePos)
const nextRelative = relativeTo(next, basePos) const nextRelative = relativeTo(next, basePos)
const cpPos = controlPoint
const cpPos = controlPoint ? relativeTo(controlPoint, basePos) : { ? relativeTo(controlPoint, basePos)
: {
x: prevRelative.x / 2 + nextRelative.x / 2, x: prevRelative.x / 2 + nextRelative.x / 2,
y: prevRelative.y / 2 + nextRelative.y / 2, y: prevRelative.y / 2 + nextRelative.y / 2,
} }
@ -122,12 +124,18 @@ export default function BendableArrow({
basePos={basePos} basePos={basePos}
onPosValidated={(controlPoint) => { onPosValidated={(controlPoint) => {
const segment = internalSegments[i] const segment = internalSegments[i]
const segments = internalSegments.toSpliced(i, 1, {...segment, controlPoint}) const segments = internalSegments.toSpliced(i, 1, {
...segment,
controlPoint,
})
onSegmentsChanges(segments) onSegmentsChanges(segments)
}} }}
onMoves={(controlPoint) => { onMoves={(controlPoint) => {
setInternalSegments(is => { setInternalSegments((is) => {
return is.toSpliced(i, 1, {...is[i], controlPoint}) return is.toSpliced(i, 1, {
...is[i],
controlPoint,
})
}) })
}} }}
/> />
@ -135,9 +143,7 @@ export default function BendableArrow({
}) })
} }
const update = useCallback(() => { const update = useCallback(() => {
// only one segment is supported for now, which is the first. // only one segment is supported for now, which is the first.
// any other segments will be ignored // any other segments will be ignored
const segment = internalSegments[0] ?? null const segment = internalSegments[0] ?? null
@ -146,17 +152,19 @@ export default function BendableArrow({
const endPos = segment.next const endPos = segment.next
const basePos = containerRef.current!.parentElement!.getBoundingClientRect() const basePos =
containerRef.current!.parentElement!.getBoundingClientRect()
const startRelative = relativeTo(startPos, basePos) const startRelative = relativeTo(startPos, basePos)
const endRelative = relativeTo(endPos!, basePos) const endRelative = relativeTo(endPos!, basePos)
const controlPoint = segment.controlPoint ? relativeTo(segment.controlPoint, basePos) : { const controlPoint = segment.controlPoint
? relativeTo(segment.controlPoint, basePos)
: {
x: startRelative.x / 2 + endRelative.x / 2, x: startRelative.x / 2 + endRelative.x / 2,
y: startRelative.y / 2 + endRelative.y / 2, y: startRelative.y / 2 + endRelative.y / 2,
} }
const tailPos = constraintInCircle( const tailPos = constraintInCircle(
startRelative, startRelative,
controlPoint, controlPoint,
@ -171,34 +179,39 @@ export default function BendableArrow({
const left = Math.min(tailPos.x, headPos.x) const left = Math.min(tailPos.x, headPos.x)
const top = Math.min(tailPos.y, headPos.y) const top = Math.min(tailPos.y, headPos.y)
Object.assign(tailRef.current!.style, { Object.assign(tailRef.current!.style, {
left: tailPos.x + "px", left: tailPos.x + "px",
top: tailPos.y + "px", top: tailPos.y + "px",
transformOrigin: "top center", transformOrigin: "top center",
transform: `translateX(-50%) rotate(${-angle(tailPos, controlPoint) * (180 / Math.PI)}deg)` transform: `translateX(-50%) rotate(${
-angle(tailPos, controlPoint) * (180 / Math.PI)
}deg)`,
} as CSSProperties) } as CSSProperties)
Object.assign(headRef.current!.style, { Object.assign(headRef.current!.style, {
left: headPos.x + "px", left: headPos.x + "px",
top: headPos.y + "px", top: headPos.y + "px",
transformOrigin: "top center", transformOrigin: "top center",
transform: `translateX(-50%) rotate(${-angle(headPos, controlPoint) * (180 / Math.PI)}deg)` transform: `translateX(-50%) rotate(${
-angle(headPos, controlPoint) * (180 / Math.PI)
}deg)`,
} as CSSProperties) } as CSSProperties)
const svgStyle: CSSProperties = { const svgStyle: CSSProperties = {
left: left + "px", left: left + "px",
top: top + "px", top: top + "px",
} }
const segmentsRelatives = internalSegments.map(({next, controlPoint}) => { const segmentsRelatives = internalSegments.map(
({ next, controlPoint }) => {
return { return {
next: relativeTo(next, basePos), next: relativeTo(next, basePos),
cp: controlPoint ? relativeTo(controlPoint, basePos) : undefined cp: controlPoint
? relativeTo(controlPoint, basePos)
: undefined,
} }
}) },
)
const computedSegments = segmentsRelatives const computedSegments = segmentsRelatives
.map(({ next: n, cp }, idx) => { .map(({ next: n, cp }, idx) => {
@ -206,18 +219,16 @@ export default function BendableArrow({
if (idx == internalSegments.length - 1) { if (idx == internalSegments.length - 1) {
//if it is the last element //if it is the last element
next = constraintInCircle( next = constraintInCircle(next, controlPoint, endRadius!)
next,
controlPoint,
endRadius!,
)
} }
if (cp == undefined) { if (cp == undefined) {
return `L${next.x - left} ${next.y - top}` return `L${next.x - left} ${next.y - top}`
} }
return `C${cp.x - left} ${cp.y - top}, ${cp.x - left} ${cp.y - top}, ${next.x - left} ${next.y - top}` return `C${cp.x - left} ${cp.y - top}, ${cp.x - left} ${
cp.y - top
}, ${next.x - left} ${next.y - top}`
}) })
.join(" ") .join(" ")
@ -225,16 +236,15 @@ export default function BendableArrow({
pathRef.current!.setAttribute("d", d) pathRef.current!.setAttribute("d", d)
Object.assign(svgRef.current!.style, svgStyle) Object.assign(svgRef.current!.style, svgStyle)
if (isSelected) {
setControlPointsDots(computeControlPoints(basePos)) setControlPointsDots(computeControlPoints(basePos))
}
}, [startPos, internalSegments]) }, [startPos, internalSegments])
useEffect(update, [update]) useEffect(update, [update])
return ( return (
<div ref={containerRef} style={{position: "absolute", top: 0, left: 0}}> <div
ref={containerRef}
style={{ position: "absolute", top: 0, left: 0 }}>
<svg <svg
ref={svgRef} ref={svgRef}
style={{ style={{
@ -251,17 +261,17 @@ export default function BendableArrow({
/> />
</svg> </svg>
<div className={"arrow-head"} <div
className={"arrow-head"}
style={{ position: "absolute", transformOrigin: "center" }} style={{ position: "absolute", transformOrigin: "center" }}
ref={headRef} ref={headRef}>
>
{style?.head?.call(style) ?? <Triangle fill={"red"} />} {style?.head?.call(style) ?? <Triangle fill={"red"} />}
</div> </div>
<div className={"arrow-tail"} <div
className={"arrow-tail"}
style={{ position: "absolute", transformOrigin: "center" }} style={{ position: "absolute", transformOrigin: "center" }}
ref={tailRef} ref={tailRef}>
>
{style?.tail?.call(style) ?? <Triangle fill={"blue"} />} {style?.tail?.call(style) ?? <Triangle fill={"blue"} />}
</div> </div>
@ -272,9 +282,9 @@ export default function BendableArrow({
interface ControlPointProps { interface ControlPointProps {
pos: Pos pos: Pos
basePos: Pos, basePos: Pos
onMoves: (currentPos: Pos) => void onMoves: (currentPos: Pos) => void
onPosValidated: (newPos: Pos) => void, onPosValidated: (newPos: Pos | undefined) => void
radius?: number radius?: number
} }
@ -297,8 +307,7 @@ function ControlPoint({
const pointPos = middlePos(ref.current!.getBoundingClientRect()) const pointPos = middlePos(ref.current!.getBoundingClientRect())
onMoves(pointPos) onMoves(pointPos)
}} }}
position={{x: pos.x - radius, y: pos.y - radius}} position={{ x: pos.x - radius, y: pos.y - radius }}>
>
<div <div
ref={ref} ref={ref}
className={"arrow-edge-control-point"} className={"arrow-edge-control-point"}
@ -307,6 +316,12 @@ function ControlPoint({
width: radius * 2, width: radius * 2,
height: radius * 2, height: radius * 2,
}} }}
onKeyDown={(e) => {
if (e.key == "Delete") {
onPosValidated(undefined)
}
}}
tabIndex={0}
/> />
</Draggable> </Draggable>
) )

@ -3,6 +3,7 @@ import {CourtBall} from "./CourtBall";
import {ReactElement, RefObject, useCallback, useState,} from "react" import {ReactElement, RefObject, useCallback, 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"
@ -12,20 +13,21 @@ import ArrowAction from "../actions/ArrowAction"
import BendableArrow, { Segment } from "../arrows/BendableArrow" import BendableArrow, { Segment } from "../arrows/BendableArrow"
import { middlePos, NULL_POS, Pos } from "../arrows/Pos" import { middlePos, NULL_POS, Pos } from "../arrows/Pos"
import BallAction from "../actions/BallAction"; import BallAction from "../actions/BallAction"
import {CourtObject} from "../../tactic/CourtObjects"; import {CourtObject} from "../../tactic/CourtObjects";
export interface BasketCourtProps { export interface BasketCourtProps {
players: Player[] players: Player[]
actions: Action[] actions: Action[]
objects: CourtObject[] objects: CourtObject[]
renderAction: (a: Action, key: number) => ReactElement renderAction: (a: Action, key: number) => ReactElement
setActions: (f: (a: Action[]) => Action[]) => void setActions: (f: (a: Action[]) => Action[]) => void
onPlayerRemove: (p: Player) => void onPlayerRemove: (p: Player) => void
onPlayerChange: (p: Player) => void onPlayerChange: (p: Player) => void
onBallRemove: () => void onBallRemove: () => void
onBallMoved: (ball: DOMRect) => void onBallMoved: (ball: DOMRect) => void
courtImage: string courtImage: string
@ -33,20 +35,21 @@ export interface BasketCourtProps {
} }
export function BasketCourt({ export function BasketCourt({
objects,
onBallMoved,
onBallRemove,
players, players,
actions, actions,
objects,
renderAction, renderAction,
setActions, setActions,
onPlayerRemove, onPlayerRemove,
onPlayerChange, onPlayerChange,
onBallMoved,
onBallRemove,
courtImage, courtImage,
courtRef, courtRef,
}: BasketCourtProps) { }: BasketCourtProps) {
function placeArrow(originRef: HTMLElement, arrowHead: DOMRect) {
function bindArrowToPlayer(originRef: HTMLElement, arrowHead: DOMRect) {
for (const player of players) { for (const player of players) {
if (player.id == originRef.id) { if (player.id == originRef.id) {
continue continue
@ -64,7 +67,9 @@ export function BasketCourt({
playerBounds.left > arrowHead.right playerBounds.left > arrowHead.right
) )
) { ) {
const targetPos = document.getElementById(player.id)!.getBoundingClientRect() const targetPos = document
.getElementById(player.id)!
.getBoundingClientRect()
const action: Action = { const action: Action = {
fromPlayerId: originRef.id, fromPlayerId: originRef.id,
toPlayerId: player.id, toPlayerId: player.id,
@ -73,8 +78,17 @@ export function BasketCourt({
segments: [{ next: middlePos(targetPos) }], segments: [{ next: middlePos(targetPos) }],
} }
setActions((actions) => [...actions, action]) setActions((actions) => [...actions, action])
return
} }
} }
const action: Action = {
fromPlayerId: originRef.id,
type: MovementActionKind.MOVE,
moveFrom: middlePos(originRef.getBoundingClientRect()),
segments: [{ next: middlePos(arrowHead) }],
}
setActions((actions) => [...actions, action])
} }
const [previewArrowOriginPos, setPreviewArrowOriginPos] = const [previewArrowOriginPos, setPreviewArrowOriginPos] =
@ -84,26 +98,37 @@ export function BasketCourt({
const [previewArrowEdges, setPreviewArrowEdges] = useState<Segment[]>([]) const [previewArrowEdges, setPreviewArrowEdges] = useState<Segment[]>([])
const updateActionsRelatedTo = useCallback((player: Player) => { const updateActionsRelatedTo = useCallback((player: Player) => {
const newPos = middlePos(document.getElementById(player.id)!.getBoundingClientRect()) const newPos = middlePos(
setActions(actions => actions.map(a => { document.getElementById(player.id)!.getBoundingClientRect(),
)
setActions((actions) =>
actions.map((a) => {
if (a.fromPlayerId == player.id) { if (a.fromPlayerId == player.id) {
return { ...a, moveFrom: newPos } return { ...a, moveFrom: newPos }
} }
if (a.toPlayerId == player.id) { if (a.toPlayerId == player.id) {
const segments = a.segments.toSpliced(a.segments.length - 1, 1, { const segments = a.segments.toSpliced(
a.segments.length - 1,
1,
{
...a.segments[a.segments.length - 1], ...a.segments[a.segments.length - 1],
next: newPos next: newPos,
}) },
)
return { ...a, segments } return { ...a, segments }
} }
return a return a
})) }),
)
}, []) }, [])
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(action, idx))} {actions.map((action, idx) => renderAction(action, idx))}
@ -138,7 +163,7 @@ export function BasketCourt({
setPreviewArrowEnabled(true) setPreviewArrowEnabled(true)
}} }}
onHeadDropped={(headRect) => { onHeadDropped={(headRect) => {
bindArrowToPlayer(pieceRef, headRect) placeArrow(pieceRef, headRect)
setPreviewArrowEnabled(false) setPreviewArrowEnabled(false)
}} }}
/>, />,
@ -167,6 +192,7 @@ export function BasketCourt({
segments={previewArrowEdges} segments={previewArrowEdges}
//do nothing on change, not really possible as it's a preview arrow //do nothing on change, not really possible as it's a preview arrow
onSegmentsChanges={() => {}} onSegmentsChanges={() => {}}
//TODO place those values in constants
endRadius={17} endRadius={17}
startRadius={26} startRadius={26}
/> />

@ -1,4 +1,3 @@
import { ReactNode, RefObject, useRef } from "react" import { ReactNode, RefObject, useRef } from "react"
import "../../style/player.css" import "../../style/player.css"
import Draggable from "react-draggable" import Draggable from "react-draggable"
@ -62,12 +61,20 @@ export default function CourtPlayer<A extends ReactNode>({
left: `${x * 100}%`, left: `${x * 100}%`,
top: `${y * 100}%`, top: `${y * 100}%`,
}}> }}>
<div tabIndex={0} className="player-content" <div
tabIndex={0}
className="player-content"
onKeyUp={(e) => { onKeyUp={(e) => {
if (e.key == "Delete") onRemove() if (e.key == "Delete") onRemove()
}}> }}>
<div className="player-actions">{availableActions(pieceRef.current!)}</div> <div className="player-actions">
<PlayerPiece team={player.team} text={player.role} hasBall={hasBall} /> {availableActions(pieceRef.current!)}
</div>
<PlayerPiece
team={player.team}
text={player.role}
hasBall={hasBall}
/>
</div> </div>
</div> </div>
</Draggable> </Draggable>

@ -3,18 +3,19 @@
border-radius: 100px; border-radius: 100px;
background-color: black; background-color: black;
outline: none;
} }
.arrow-edge-control-point:hover { .arrow-edge-control-point:hover {
background-color: var(--selection-color); background-color: var(--selection-color);
} }
.arrow-path { .arrow-path {
pointer-events: stroke; pointer-events: stroke;
cursor: pointer; cursor: pointer;
} }
.arrow-path:hover, .arrow-path:active { .arrow-path:hover,
stroke: var(--selection-color) .arrow-path:active {
stroke: var(--selection-color);
} }

@ -11,8 +11,8 @@ export enum MovementActionKind {
export type Action = { type: MovementActionKind } & MovementAction export type Action = { type: MovementActionKind } & MovementAction
export interface MovementAction { export interface MovementAction {
fromPlayerId: PlayerId, fromPlayerId: PlayerId
toPlayerId: PlayerId, toPlayerId?: PlayerId
moveFrom: Pos moveFrom: Pos
segments: Segment[] segments: Segment[]
} }

@ -3,7 +3,6 @@ import { Team } from "./Team"
export type PlayerId = string export type PlayerId = string
export interface Player { export interface Player {
readonly id: PlayerId readonly id: PlayerId
/** /**

@ -108,11 +108,17 @@ function EditorView({
const [content, setContent, saveState] = useContentState( const [content, setContent, saveState] = useContentState(
initialContent, initialContent,
isInGuestMode ? SaveStates.Guest : SaveStates.Ok, isInGuestMode ? SaveStates.Guest : SaveStates.Ok,
useMemo(() => debounceAsync((content) => useMemo(
() =>
debounceAsync(
(content) =>
onContentChange(content).then((success) => onContentChange(content).then((success) =>
success ? SaveStates.Ok : SaveStates.Err, success ? SaveStates.Ok : SaveStates.Err,
) ),
, 250), [onContentChange]) 250,
),
[onContentChange],
),
) )
const [allies, setAllies] = useState( const [allies, setAllies] = useState(
@ -490,11 +496,14 @@ function getRackPlayers(team: Team, players: Player[]): RackedPlayer[] {
.map((key) => ({ team, key })) .map((key) => ({ team, key }))
} }
function debounceAsync<A, B>(f: (args: A) => Promise<B>, delay = 1000): (args: A) => Promise<B> { function debounceAsync<A, B>(
let task = 0; f: (args: A) => Promise<B>,
delay = 1000,
): (args: A) => Promise<B> {
let task = 0
return (args: A) => { return (args: A) => {
clearTimeout(task) clearTimeout(task)
return new Promise(resolve => { return new Promise((resolve) => {
task = setTimeout(() => f(args).then(resolve), delay) task = setTimeout(() => f(args).then(resolve), delay)
}) })
} }

@ -6,11 +6,7 @@ export interface CourtActionProps {
onActionChanges: (a: Action) => void onActionChanges: (a: Action) => void
} }
export function CourtAction({ export function CourtAction({ action, onActionChanges }: CourtActionProps) {
action,
onActionChanges,
}: CourtActionProps) {
return ( return (
<BendableArrow <BendableArrow
startPos={action.moveFrom} startPos={action.moveFrom}
@ -18,7 +14,8 @@ export function CourtAction({
onSegmentsChanges={(edges) => { onSegmentsChanges={(edges) => {
onActionChanges({ ...action, segments: edges }) onActionChanges({ ...action, segments: edges })
}} }}
endRadius={26} //TODO place those magic values in constants
endRadius={action.toPlayerId ? 26 : 17}
startRadius={26} startRadius={26}
/> />
) )

Loading…
Cancel
Save