reset arrow curves by deleting the central control point

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

@ -1,7 +1,7 @@
import "../../style/actions/arrow_action.css"
import Draggable from "react-draggable"
import {useRef} from "react"
import { useRef } from "react"
export interface ArrowActionProps {
onHeadDropped: (headBounds: DOMRect) => void
@ -18,7 +18,7 @@ export default function ArrowAction({
return (
<div className="arrow-action">
<div className="arrow-action-pin"/>
<div className="arrow-action-pin" />
<Draggable
nodeRef={arrowHeadRef}
@ -37,10 +37,8 @@ export default function ArrowAction({
arrowHeadRef.current!.getBoundingClientRect()
onHeadMoved(headBounds)
}}
position={{x: 0, y: 0}}>
<div
ref={arrowHeadRef}
className="arrow-head-pick"/>
position={{ x: 0, y: 0 }}>
<div ref={arrowHeadRef} className="arrow-head-pick" />
</Draggable>
</div>
)

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

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

@ -3,7 +3,7 @@ export interface Pos {
y: number
}
export const NULL_POS: Pos = {x: 0, y: 0}
export const NULL_POS: Pos = { x: 0, y: 0 }
/**
* Returns position of a relative to b
@ -11,7 +11,7 @@ export const NULL_POS: Pos = {x: 0, y: 0}
* @param b
*/
export function relativeTo(a: Pos, b: Pos): Pos {
return {x: a.x - b.x, y: a.y - b.y}
return { x: a.x - b.x, y: a.y - b.y }
}
/**
@ -19,7 +19,7 @@ export function relativeTo(a: Pos, b: Pos): Pos {
* @param rect
*/
export function middlePos(rect: DOMRect): Pos {
return {x: rect.x + rect.width / 2, y: rect.y + rect.height / 2}
return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 }
}
/**
@ -28,11 +28,11 @@ export function middlePos(rect: DOMRect): Pos {
* @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)}
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}
return { x: a.x + b.x, y: a.y + b.y }
}
export function angle(a: Pos, b: Pos): number {

@ -3,29 +3,31 @@ import {CourtBall} from "./CourtBall";
import {ReactElement, RefObject, useCallback, useState,} from "react"
import CourtPlayer from "./CourtPlayer"
import {Player} from "../../tactic/Player"
import {Action, MovementActionKind} from "../../tactic/Action"
import { Player } from "../../tactic/Player"
import { Action, MovementActionKind } from "../../tactic/Action"
import RemoveAction from "../actions/RemoveAction"
import ArrowAction from "../actions/ArrowAction"
import BendableArrow, {Segment} from "../arrows/BendableArrow"
import {middlePos, NULL_POS, Pos} from "../arrows/Pos"
import BallAction from "../actions/BallAction";
import BendableArrow, { Segment } from "../arrows/BendableArrow"
import { middlePos, NULL_POS, Pos } from "../arrows/Pos"
import BallAction from "../actions/BallAction"
import {CourtObject} from "../../tactic/CourtObjects";
export interface BasketCourtProps {
players: Player[]
actions: Action[]
objects: CourtObject[]
renderAction: (a: Action, key: number) => ReactElement
setActions: (f: (a: Action[]) => Action[]) => void
onPlayerRemove: (p: Player) => void
onPlayerChange: (p: Player) => void
onBallRemove: () => void
onBallMoved: (ball: DOMRect) => void
courtImage: string
@ -33,20 +35,21 @@ export interface BasketCourtProps {
}
export function BasketCourt({
objects,
onBallMoved,
onBallRemove,
players,
actions,
renderAction,
setActions,
onPlayerRemove,
onPlayerChange,
courtImage,
courtRef,
}: BasketCourtProps) {
function bindArrowToPlayer(originRef: HTMLElement, arrowHead: DOMRect) {
players,
actions,
objects,
renderAction,
setActions,
onPlayerRemove,
onPlayerChange,
onBallMoved,
onBallRemove,
courtImage,
courtRef,
}: BasketCourtProps) {
function placeArrow(originRef: HTMLElement, arrowHead: DOMRect) {
for (const player of players) {
if (player.id == originRef.id) {
continue
@ -64,17 +67,28 @@ export function BasketCourt({
playerBounds.left > arrowHead.right
)
) {
const targetPos = document.getElementById(player.id)!.getBoundingClientRect()
const targetPos = document
.getElementById(player.id)!
.getBoundingClientRect()
const action: Action = {
fromPlayerId: originRef.id,
toPlayerId: player.id,
type: MovementActionKind.SCREEN,
moveFrom: middlePos(originRef.getBoundingClientRect()),
segments: [{next: middlePos(targetPos)}],
segments: [{ next: middlePos(targetPos) }],
}
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] =
@ -84,27 +98,38 @@ export function BasketCourt({
const [previewArrowEdges, setPreviewArrowEdges] = useState<Segment[]>([])
const updateActionsRelatedTo = useCallback((player: Player) => {
const newPos = middlePos(document.getElementById(player.id)!.getBoundingClientRect())
setActions(actions => actions.map(a => {
if (a.fromPlayerId == player.id) {
return {...a, moveFrom: newPos}
}
const newPos = middlePos(
document.getElementById(player.id)!.getBoundingClientRect(),
)
setActions((actions) =>
actions.map((a) => {
if (a.fromPlayerId == player.id) {
return { ...a, moveFrom: newPos }
}
if (a.toPlayerId == player.id) {
const segments = a.segments.toSpliced(a.segments.length - 1, 1, {
...a.segments[a.segments.length - 1],
next: newPos
})
return {...a, segments}
}
if (a.toPlayerId == player.id) {
const segments = a.segments.toSpliced(
a.segments.length - 1,
1,
{
...a.segments[a.segments.length - 1],
next: newPos,
},
)
return { ...a, segments }
}
return a
}))
return a
}),
)
}, [])
return (
<div id="court-container" ref={courtRef} style={{position: "relative"}}>
<img src={courtImage} alt={"court"} id="court-svg"/>
<div
id="court-container"
ref={courtRef}
style={{ position: "relative" }}>
<img src={courtImage} alt={"court"} id="court-svg" />
{actions.map((action, idx) => renderAction(action, idx))}
@ -125,7 +150,7 @@ export function BasketCourt({
key={2}
onHeadMoved={(headPos) =>
setPreviewArrowEdges([
{next: middlePos(headPos)},
{ next: middlePos(headPos) },
])
}
onHeadPicked={(headPos) => {
@ -133,12 +158,12 @@ export function BasketCourt({
middlePos(pieceRef.getBoundingClientRect()),
)
setPreviewArrowEdges([
{next: middlePos(headPos)},
{ next: middlePos(headPos) },
])
setPreviewArrowEnabled(true)
}}
onHeadDropped={(headRect) => {
bindArrowToPlayer(pieceRef, headRect)
placeArrow(pieceRef, headRect)
setPreviewArrowEnabled(false)
}}
/>,
@ -167,6 +192,7 @@ export function BasketCourt({
segments={previewArrowEdges}
//do nothing on change, not really possible as it's a preview arrow
onSegmentsChanges={() => {}}
//TODO place those values in constants
endRadius={17}
startRadius={26}
/>

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

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

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

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

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

@ -1,4 +1,4 @@
import {Action} from "../../tactic/Action"
import { Action } from "../../tactic/Action"
import BendableArrow from "../../components/arrows/BendableArrow"
export interface CourtActionProps {
@ -6,19 +6,16 @@ export interface CourtActionProps {
onActionChanges: (a: Action) => void
}
export function CourtAction({
action,
onActionChanges,
}: CourtActionProps) {
export function CourtAction({ action, onActionChanges }: CourtActionProps) {
return (
<BendableArrow
startPos={action.moveFrom}
segments={action.segments}
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}
/>
)

Loading…
Cancel
Save