add screen, move, dribble and throw arrows

pull/82/head
maxime.batista 1 year ago
parent 727ab33644
commit 26e99a3a03

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="512" height="512" x="0" y="0" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512" xml:space="preserve" class=""> <g transform="matrix(1.0899999999999992,0,0,1.0899999999999992,-23.040010986327673,-23.03998867034892)"> <path fill-rule="evenodd" d="M250.131 307.122 424.673 132.58v73.421c0 17.6 14.4 31.999 31.999 31.999 17.6 0 32-14.399 32-31.999V23.328H305.998c-17.6 0-31.999 14.4-31.999 31.999 0 17.6 14.399 32 31.999 32h73.421L204.935 261.81c-39.932 40.129-110.352 12.463-110.352-45.627 0-35.683 28.926-64.609 64.609-64.609 30.018 0 55.252 20.472 62.508 48.216l48.526-48.526c-22.324-38.099-63.688-63.689-111.034-63.689-71.028 0-128.608 57.58-128.608 128.608.001 115.408 139.655 170.832 219.547 90.939zm-149.25 58.743c25.733 10.037 53.881 13.203 81.205 9.303l-104.17 104.17c-12.445 12.445-32.809 12.445-45.254 0s-12.445-32.809 0-45.254z" clip-rule="evenodd" fill="#F00" opacity="1" data-original="#F00" class=""> </path> </g> </svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@ -1,6 +1,6 @@
import "../../style/actions/arrow_action.css" import "../../style/actions/arrow_action.css"
import Draggable from "react-draggable" import Draggable from "react-draggable"
import arrowPng from "../../assets/icon/arrow.svg"
import { useRef } from "react" import { useRef } from "react"
export interface ArrowActionProps { export interface ArrowActionProps {
@ -18,7 +18,7 @@ export default function ArrowAction({
return ( return (
<div className="arrow-action"> <div className="arrow-action">
<div className="arrow-action-pin" /> <img className="arrow-action-icon" src={arrowPng} alt="add arrow" />
<Draggable <Draggable
nodeRef={arrowHeadRef} nodeRef={arrowHeadRef}
@ -43,3 +43,32 @@ export default function ArrowAction({
</div> </div>
) )
} }
export function ScreenHead() {
return (
<div
style={{ backgroundColor: "black", height: "5px", width: "25px" }}
/>
)
}
export function MoveToHead() {
return (
<svg viewBox={"0 0 50 50"} width={20} height={20}>
<polygon points={"50 0, 0 0, 25 40"} fill="#000" />
</svg>
)
}
export function ShootHead() {
return (
<svg viewBox={"0 0 50 50"} width={15} height={15} overflow={"visible"}>
<path
d={"M0 0 L50 50 M0 50 L50 0"}
stroke="#000"
strokeWidth={10}
fill={"transparent"}
/>
</svg>
)
}

@ -1,10 +0,0 @@
import RemoveIcon from "../../assets/icon/remove.svg?react"
import "../../style/actions/remove_action.css"
export interface RemoveActionProps {
onRemove: () => void
}
export default function RemoveAction({ onRemove }: RemoveActionProps) {
return <RemoveIcon className="remove-action" onClick={onRemove} />
}

@ -1,4 +1,12 @@
import {CSSProperties, ReactElement, RefObject, useCallback, useEffect, useRef, useState,} from "react" import {
CSSProperties,
ReactElement,
RefObject,
useCallback,
useEffect,
useRef,
useState,
} from "react"
import { import {
add, add,
angle, angle,
@ -10,7 +18,6 @@ import {
Pos, Pos,
posWithinBase, posWithinBase,
ratioWithinBase, ratioWithinBase,
relativeTo,
} from "./Pos" } from "./Pos"
import "../../style/bendable_arrows.css" import "../../style/bendable_arrows.css"
@ -21,6 +28,7 @@ export interface BendableArrowProps {
startPos: Pos startPos: Pos
segments: Segment[] segments: Segment[]
onSegmentsChanges: (edges: Segment[]) => void onSegmentsChanges: (edges: Segment[]) => void
forceStraight: boolean
startRadius?: number startRadius?: number
endRadius?: number endRadius?: number
@ -32,6 +40,7 @@ export interface BendableArrowProps {
export interface ArrowStyle { export interface ArrowStyle {
width?: number width?: number
dashArray?: string
head?: () => ReactElement head?: () => ReactElement
tail?: () => ReactElement tail?: () => ReactElement
} }
@ -54,27 +63,19 @@ function constraintInCircle(pos: Pos, from: Pos, radius: number): Pos {
} }
} }
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}/>
</svg>
)
}
export default function BendableArrow({ export default function BendableArrow({
area, area,
startPos, startPos,
segments, segments,
onSegmentsChanges, onSegmentsChanges,
forceStraight,
style,
startRadius = 0, style,
endRadius = 0, startRadius = 0,
onDeleteRequested = () => { endRadius = 0,
}, onDeleteRequested,
}: BendableArrowProps) { }: BendableArrowProps) {
const containerRef = useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null)
const svgRef = useRef<SVGSVGElement>(null) const svgRef = useRef<SVGSVGElement>(null)
const pathRef = useRef<SVGPathElement>(null) const pathRef = useRef<SVGPathElement>(null)
@ -93,7 +94,7 @@ export default function BendableArrow({
const tailRef = useRef<HTMLDivElement>(null) const tailRef = useRef<HTMLDivElement>(null)
function computeControlPoints(parentBase: DOMRect) { function computeControlPoints(parentBase: DOMRect) {
return segments.flatMap(({next, controlPoint}, i) => { return segments.flatMap(({ next, controlPoint }, i) => {
const prev = i == 0 ? startPos : segments[i - 1].next const prev = i == 0 ? startPos : segments[i - 1].next
const prevRelative = posWithinBase(prev, parentBase) const prevRelative = posWithinBase(prev, parentBase)
@ -134,34 +135,34 @@ export default function BendableArrow({
}} }}
/>, />,
//next pos point (only if this is not the last segment) //next pos point (only if this is not the last segment)
i != segments.length - 1 && <ArrowPoint i != segments.length - 1 && (
className={"arrow-point-next"} <ArrowPoint
posRatio={next} className={"arrow-point-next"}
parentBase={parentBase} posRatio={next}
onPosValidated={next => { parentBase={parentBase}
const currentSegment = segments[i] onPosValidated={(next) => {
const newSegments = segments.toSpliced(i, 1, { const currentSegment = segments[i]
...currentSegment, const newSegments = segments.toSpliced(i, 1, {
next, ...currentSegment,
})
onSegmentsChanges(newSegments)
}}
onRemove={() => {
onSegmentsChanges(segments.toSpliced(
Math.max(i - 1, 0),
1,
)
)
}}
onMoves={next => {
setInternalSegments((is) => {
return is.toSpliced(i, 1, {
...is[i],
next, next,
}) })
}) onSegmentsChanges(newSegments)
}} }}
/> onRemove={() => {
onSegmentsChanges(
segments.toSpliced(Math.max(i - 1, 0), 1),
)
}}
onMoves={(next) => {
setInternalSegments((is) => {
return is.toSpliced(i, 1, {
...is[i],
next,
})
})
}}
/>
),
] ]
}) })
} }
@ -170,21 +171,28 @@ export default function BendableArrow({
const parentBase = area.current!.getBoundingClientRect() const parentBase = area.current!.getBoundingClientRect()
const firstSegment = internalSegments[0] ?? null const firstSegment = internalSegments[0] ?? null
if (firstSegment == null) throw new Error("segments might not be empty.") if (firstSegment == null)
throw new Error("segments might not be empty.")
const lastSegment = internalSegments[internalSegments.length - 1] const lastSegment = internalSegments[internalSegments.length - 1]
const startRelative = posWithinBase(startPos, parentBase) const startRelative = posWithinBase(startPos, parentBase)
const endRelative = posWithinBase(lastSegment.next, parentBase) const endRelative = posWithinBase(lastSegment.next, parentBase)
const startNext = firstSegment.controlPoint const startNext =
? posWithinBase(firstSegment.controlPoint, parentBase) firstSegment.controlPoint && !forceStraight
: posWithinBase(firstSegment.next, parentBase) ? posWithinBase(firstSegment.controlPoint, parentBase)
: posWithinBase(firstSegment.next, parentBase)
const endPrevious = lastSegment.controlPoint
? posWithinBase(lastSegment.controlPoint, parentBase) const endPrevious = forceStraight
: internalSegments[internalSegments.length - 2] ? startRelative
? posWithinBase(internalSegments[internalSegments.length - 2].next, parentBase) : lastSegment.controlPoint
? posWithinBase(lastSegment.controlPoint, parentBase)
: internalSegments[internalSegments.length - 2]
? posWithinBase(
internalSegments[internalSegments.length - 2].next,
parentBase,
)
: startRelative : startRelative
const tailPos = constraintInCircle( const tailPos = constraintInCircle(
@ -192,11 +200,7 @@ export default function BendableArrow({
startNext, startNext,
startRadius!, startRadius!,
) )
const headPos = constraintInCircle( const headPos = constraintInCircle(endRelative, endPrevious, endRadius!)
endRelative,
endPrevious,
endRadius!,
)
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)
@ -224,25 +228,29 @@ export default function BendableArrow({
top: top + "px", top: top + "px",
} }
const segmentsRelatives = internalSegments.map( const segmentsRelatives = (
({next, controlPoint}, idx) => { forceStraight ? internalSegments.slice(-1) : internalSegments
const nextPos = posWithinBase(next, parentBase) ).map(({ next, controlPoint }, idx) => {
return { const nextPos = posWithinBase(next, parentBase)
next: nextPos, return {
cp: controlPoint next: nextPos,
cp:
controlPoint && !forceStraight
? posWithinBase(controlPoint, parentBase) ? posWithinBase(controlPoint, parentBase)
: between( : between(
idx == 0 idx == 0
? startRelative ? startRelative
: posWithinBase(internalSegments[idx - 1].next, parentBase), : posWithinBase(
nextPos internalSegments[idx - 1].next,
), parentBase,
} ),
}, nextPos,
) ),
}
})
const computedSegments = segmentsRelatives const computedSegments = segmentsRelatives
.map(({next: n, cp}, idx) => { .map(({ next: n, cp }, idx) => {
let next = n let next = n
if (idx == internalSegments.length - 1) { if (idx == internalSegments.length - 1) {
@ -259,7 +267,7 @@ export default function BendableArrow({
const d = `M${tailPos.x - left} ${tailPos.y - top} ` + computedSegments const d = `M${tailPos.x - left} ${tailPos.y - top} ` + computedSegments
pathRef.current!.setAttribute("d", d) pathRef.current!.setAttribute("d", d)
Object.assign(svgRef.current!.style, svgStyle) Object.assign(svgRef.current!.style, svgStyle)
}, [startPos, internalSegments]) }, [startPos, internalSegments, forceStraight])
useEffect(update, [update]) useEffect(update, [update])
@ -281,12 +289,16 @@ export default function BendableArrow({
}, [update, containerRef]) }, [update, containerRef])
useEffect(() => { useEffect(() => {
if (forceStraight) return
const addSegment = (e: MouseEvent) => { const addSegment = (e: MouseEvent) => {
const parentBase = area.current!.getBoundingClientRect() const parentBase = area.current!.getBoundingClientRect()
const clickAbsolutePos: Pos = {x: e.x, y: e.y} const clickAbsolutePos: Pos = { x: e.x, y: e.y }
const clickPosBaseRatio = ratioWithinBase(clickAbsolutePos, parentBase) const clickPosBaseRatio = ratioWithinBase(
clickAbsolutePos,
parentBase,
)
let segmentInsertionIndex = -1 let segmentInsertionIndex = -1
let segmentInsertionIsOnRightOfCP = false let segmentInsertionIsOnRightOfCP = false
@ -295,53 +307,60 @@ export default function BendableArrow({
let currentPos = i == 0 ? startPos : segments[i - 1].next let currentPos = i == 0 ? startPos : segments[i - 1].next
let nextPos = segment.next let nextPos = segment.next
let controlPointPos = segment.controlPoint ? segment.controlPoint : between(currentPos, nextPos) let controlPointPos = segment.controlPoint
? segment.controlPoint
const result = searchOnSegment(currentPos, controlPointPos, nextPos, clickPosBaseRatio, 0.05) : between(currentPos, nextPos)
if (result == PointSegmentSearchResult.NOT_FOUND)
continue const result = searchOnSegment(
currentPos,
controlPointPos,
nextPos,
clickPosBaseRatio,
0.05,
)
if (result == PointSegmentSearchResult.NOT_FOUND) continue
segmentInsertionIndex = i segmentInsertionIndex = i
segmentInsertionIsOnRightOfCP = result == PointSegmentSearchResult.RIGHT_TO_CONTROL_POINT segmentInsertionIsOnRightOfCP =
result == PointSegmentSearchResult.RIGHT_TO_CONTROL_POINT
break break
} }
if (segmentInsertionIndex == -1) if (segmentInsertionIndex == -1) return
return
const splicedSegment: Segment = segments[segmentInsertionIndex] const splicedSegment: Segment = segments[segmentInsertionIndex]
let newSegments: Segment[] onSegmentsChanges(
if (segmentInsertionIsOnRightOfCP) { segments.toSpliced(
newSegments = segments.toSpliced(
segmentInsertionIndex,
1,
{next: clickPosBaseRatio, controlPoint: splicedSegment.controlPoint},
{next: splicedSegment.next, controlPoint: undefined}
)
} else {
newSegments = segments.toSpliced(
segmentInsertionIndex, segmentInsertionIndex,
1, 1,
{next: clickPosBaseRatio, controlPoint: undefined}, {
{next: splicedSegment.next, controlPoint: splicedSegment.controlPoint} next: clickPosBaseRatio,
) controlPoint: segmentInsertionIsOnRightOfCP
} ? splicedSegment.controlPoint
: undefined,
onSegmentsChanges(newSegments) },
{
next: splicedSegment.next,
controlPoint: segmentInsertionIsOnRightOfCP
? undefined
: splicedSegment.controlPoint,
},
),
)
} }
pathRef?.current?.addEventListener('dblclick', addSegment) pathRef?.current?.addEventListener("dblclick", addSegment)
return () => { return () => {
pathRef?.current?.removeEventListener('dblclick', addSegment) pathRef?.current?.removeEventListener("dblclick", addSegment)
} }
}, [pathRef, segments]); }, [pathRef, segments, onSegmentsChanges])
return ( return (
<div <div
ref={containerRef} ref={containerRef}
style={{position: "absolute", top: 0, left: 0}}> style={{ position: "absolute", top: 0, left: 0 }}>
<svg <svg
ref={svgRef} ref={svgRef}
style={{ style={{
@ -354,29 +373,33 @@ export default function BendableArrow({
ref={pathRef} ref={pathRef}
stroke={"#000"} stroke={"#000"}
strokeWidth={styleWidth} strokeWidth={styleWidth}
strokeDasharray={style?.dashArray}
fill="none" fill="none"
tabIndex={0} tabIndex={0}
onKeyUp={(e) => { onKeyUp={(e) => {
if (e.key == "Delete") onDeleteRequested() if (onDeleteRequested && e.key == "Delete")
onDeleteRequested()
}} }}
/> />
</svg> </svg>
<div <div
className={"arrow-head"} 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)}
</div> </div>
<div <div
className={"arrow-tail"} 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)}
</div> </div>
{isSelected && computeControlPoints(area.current!.getBoundingClientRect())} {!forceStraight &&
isSelected &&
computeControlPoints(area.current!.getBoundingClientRect())}
</div> </div>
) )
} }
@ -394,13 +417,20 @@ interface ControlPointProps {
enum PointSegmentSearchResult { enum PointSegmentSearchResult {
LEFT_TO_CONTROL_POINT, LEFT_TO_CONTROL_POINT,
RIGHT_TO_CONTROL_POINT, RIGHT_TO_CONTROL_POINT,
NOT_FOUND NOT_FOUND,
} }
function searchOnSegment(startPos: Pos, controlPoint: Pos, endPos: Pos, point: Pos, minDistance: number): PointSegmentSearchResult { function searchOnSegment(
startPos: Pos,
controlPoint: Pos,
const step = 1 / ((distance(startPos, controlPoint) + distance(controlPoint, endPos)) / minDistance) endPos: Pos,
point: Pos,
minDistance: number,
): PointSegmentSearchResult {
const step =
1 /
((distance(startPos, controlPoint) + distance(controlPoint, endPos)) /
minDistance)
const p0MinusP1 = minus(startPos, controlPoint) const p0MinusP1 = minus(startPos, controlPoint)
const p2MinusP1 = minus(endPos, controlPoint) const p2MinusP1 = minus(endPos, controlPoint)
@ -408,22 +438,12 @@ function searchOnSegment(startPos: Pos, controlPoint: Pos, endPos: Pos, point: P
function getDistanceAt(t: number): number { function getDistanceAt(t: number): number {
// apply the bezier function // apply the bezier function
const pos = add( const pos = add(
add( add(controlPoint, mul(p0MinusP1, (1 - t) ** 2)),
controlPoint, mul(p2MinusP1, t ** 2),
mul(
p0MinusP1,
(1 - t) ** 2
)
),
mul(
p2MinusP1,
t ** 2
)
) )
return distance(pos, point) return distance(pos, point)
} }
for (let t = 0; t < 1; t += step) { for (let t = 0; t < 1; t += step) {
if (getDistanceAt(t) <= minDistance) if (getDistanceAt(t) <= minDistance)
return t >= 0.5 return t >= 0.5
@ -439,18 +459,18 @@ let slice = 0.5
for (let i = 0; i < 100; i++) { for (let i = 0; i < 100; i++) {
t += slice t += slice
slice /= 2 slice /= 2
// console.log(t) // console.log(t)
} }
function ArrowPoint({ function ArrowPoint({
className, className,
posRatio, posRatio,
parentBase, parentBase,
onMoves, onMoves,
onPosValidated, onPosValidated,
onRemove, onRemove,
radius = 7, radius = 7,
}: ControlPointProps) { }: ControlPointProps) {
const ref = useRef<HTMLDivElement>(null) const ref = useRef<HTMLDivElement>(null)
const pos = posWithinBase(posRatio, parentBase) const pos = posWithinBase(posRatio, parentBase)
@ -466,7 +486,7 @@ function ArrowPoint({
const pointPos = middlePos(ref.current!.getBoundingClientRect()) const pointPos = middlePos(ref.current!.getBoundingClientRect())
onMoves(ratioWithinBase(pointPos, parentBase)) onMoves(ratioWithinBase(pointPos, parentBase))
}} }}
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-point ${className}`} className={`arrow-point ${className}`}

@ -1,35 +1,38 @@
import {Pos} from "./Pos"; import { Pos } from "./Pos"
export interface Box { export interface Box {
x: number, x: number
y: number, y: number
width: number, width: number
height: number height: number
} }
export function boundsOf(...positions: Pos[]): Box { export function boundsOf(...positions: Pos[]): Box {
const allPosX = positions.map((p) => p.x)
const allPosX = positions.map(p => p.x) const allPosY = positions.map((p) => p.y)
const allPosY = positions.map(p => p.y)
const x = Math.min(...allPosX) const x = Math.min(...allPosX)
const y = Math.min(...allPosY) const y = Math.min(...allPosY)
const width = Math.max(...allPosX) - x const width = Math.max(...allPosX) - x
const height = Math.max(...allPosY) - y const height = Math.max(...allPosY) - y
return {x, y, width, height} return { x, y, width, height }
} }
export function surrounds(pos: Pos, width: number, height: number): Box { export function surrounds(pos: Pos, width: number, height: number): Box {
return { return {
x: pos.x + (width / 2), x: pos.x + width / 2,
y: pos.y + (height / 2), y: pos.y + height / 2,
width, width,
height height,
} }
} }
export function contains(box: Box, pos: Pos): boolean { export function contains(box: Box, pos: Pos): boolean {
return (pos.x >= box.x && pos.x <= box.x + box.width && pos.y >= box.y && pos.y <= box.y + box.height) return (
pos.x >= box.x &&
pos.x <= box.x + box.width &&
pos.y >= box.y &&
pos.y <= box.y + box.height
)
} }

@ -3,7 +3,7 @@ export interface Pos {
y: number 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 * Returns position of a relative to b
@ -11,7 +11,7 @@ export const NULL_POS: Pos = {x: 0, y: 0}
* @param b * @param b
*/ */
export function relativeTo(a: Pos, b: Pos): Pos { 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,20 +19,19 @@ export function relativeTo(a: Pos, b: Pos): Pos {
* @param rect * @param rect
*/ */
export function middlePos(rect: DOMRect): Pos { 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 }
} }
export function add(a: Pos, b: Pos): Pos { 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 minus(a: Pos, b: Pos): Pos { export function minus(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 mul(a: Pos, t: number): Pos { export function mul(a: Pos, t: number): Pos {
return {x: a.x * t, y: a.y * t} return { x: a.x * t, y: a.y * t }
} }
export function distance(a: Pos, b: Pos): number { export function distance(a: Pos, b: Pos): number {
@ -61,6 +60,6 @@ export function posWithinBase(ratio: Pos, base: DOMRect): Pos {
export function between(a: Pos, b: Pos): Pos { export function between(a: Pos, b: Pos): Pos {
return { return {
x: a.x / 2 + b.x / 2, x: a.x / 2 + b.x / 2,
y: a.y / 2 + b.y / 2 y: a.y / 2 + b.y / 2,
} }
} }

@ -9,14 +9,13 @@ import {
} from "react" } 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, ActionKind } from "../../tactic/Action"
import RemoveAction from "../actions/RemoveAction"
import ArrowAction from "../actions/ArrowAction" import ArrowAction from "../actions/ArrowAction"
import { middlePos, ratioWithinBase } from "../arrows/Pos"
import BendableArrow, { Segment } from "../arrows/BendableArrow"
import { middlePos, NULL_POS, Pos, ratioWithinBase } from "../arrows/Pos"
import BallAction from "../actions/BallAction" import BallAction from "../actions/BallAction"
import { CourtObject } from "../../tactic/CourtObjects" import { CourtObject } from "../../tactic/CourtObjects"
import { contains } from "../arrows/Box"
import { CourtAction } from "../../views/editor/CourtAction"
export interface BasketCourtProps { export interface BasketCourtProps {
players: Player[] players: Player[]
@ -32,7 +31,7 @@ export interface BasketCourtProps {
onBallRemove: () => void onBallRemove: () => void
onBallMoved: (ball: DOMRect) => void onBallMoved: (ball: DOMRect) => void
courtImage: () => ReactElement courtImage: ReactElement
courtRef: RefObject<HTMLDivElement> courtRef: RefObject<HTMLDivElement>
} }
@ -51,9 +50,16 @@ export function BasketCourt({
courtImage, courtImage,
courtRef, courtRef,
}: BasketCourtProps) { }: BasketCourtProps) {
function placeArrow(originRef: HTMLElement, arrowHead: DOMRect) { function placeArrow(origin: Player, arrowHead: DOMRect) {
const originRef = document.getElementById(origin.id)!
const courtBounds = courtRef.current!.getBoundingClientRect()
const start = ratioWithinBase(
middlePos(originRef.getBoundingClientRect()),
courtBounds,
)
for (const player of players) { for (const player of players) {
if (player.id == originRef.id) { if (player.id == origin.id) {
continue continue
} }
@ -73,18 +79,12 @@ export function BasketCourt({
.getElementById(player.id)! .getElementById(player.id)!
.getBoundingClientRect() .getBoundingClientRect()
const courtBounds = courtRef.current!.getBoundingClientRect()
const start = ratioWithinBase(
middlePos(originRef.getBoundingClientRect()),
courtBounds,
)
const end = ratioWithinBase(middlePos(targetPos), courtBounds) const end = ratioWithinBase(middlePos(targetPos), courtBounds)
const action: Action = { const action: Action = {
fromPlayerId: originRef.id, fromPlayerId: originRef.id,
toPlayerId: player.id, toPlayerId: player.id,
type: MovementActionKind.SCREEN, type: origin.hasBall ? ActionKind.SHOOT : ActionKind.SCREEN,
moveFrom: start, moveFrom: start,
segments: [{ next: end }], segments: [{ next: end }],
} }
@ -95,18 +95,19 @@ export function BasketCourt({
const action: Action = { const action: Action = {
fromPlayerId: originRef.id, fromPlayerId: originRef.id,
type: MovementActionKind.MOVE, type: origin.hasBall ? ActionKind.DRIBBLE : ActionKind.MOVE,
moveFrom: middlePos(originRef.getBoundingClientRect()), moveFrom: ratioWithinBase(
segments: [{ next: middlePos(arrowHead) }], middlePos(originRef.getBoundingClientRect()),
courtBounds,
),
segments: [
{ next: ratioWithinBase(middlePos(arrowHead), courtBounds) },
],
} }
setActions((actions) => [...actions, action]) setActions((actions) => [...actions, action])
} }
const [previewArrowOriginPos, setPreviewArrowOriginPos] = const [previewAction, setPreviewAction] = useState<Action | null>(null)
useState<Pos>(NULL_POS)
const [isPreviewArrowEnabled, setPreviewArrowEnabled] = useState(false)
const [previewArrowEdges, setPreviewArrowEdges] = useState<Segment[]>([])
const updateActionsRelatedTo = useCallback((player: Player) => { const updateActionsRelatedTo = useCallback((player: Player) => {
const newPos = ratioWithinBase( const newPos = ratioWithinBase(
@ -147,9 +148,7 @@ export function BasketCourt({
className="court-container" className="court-container"
ref={courtRef} ref={courtRef}
style={{ position: "relative" }}> style={{ position: "relative" }}>
{courtImage()} {courtImage}
{internActions.map((action, idx) => renderAction(action, idx))}
{players.map((player) => ( {players.map((player) => (
<CourtPlayer <CourtPlayer
@ -160,54 +159,79 @@ export function BasketCourt({
onRemove={() => onPlayerRemove(player)} onRemove={() => onPlayerRemove(player)}
parentRef={courtRef} parentRef={courtRef}
availableActions={(pieceRef) => [ availableActions={(pieceRef) => [
<RemoveAction
key={1}
onRemove={() => onPlayerRemove(player)}
/>,
<ArrowAction <ArrowAction
key={2} key={1}
onHeadMoved={(headPos) => { onHeadMoved={(headPos) => {
const baseBounds = const baseBounds =
courtRef.current!.getBoundingClientRect() courtRef.current!.getBoundingClientRect()
setPreviewArrowEdges([
{ const arrowHeadPos = middlePos(headPos)
next: ratioWithinBase(
middlePos(headPos), const target = players.find(
baseBounds, (p) =>
p != player &&
contains(
document
.getElementById(p.id)!
.getBoundingClientRect(),
arrowHeadPos,
), ),
}, )
])
setPreviewAction((action) => ({
...action!,
segments: [
{
next: ratioWithinBase(
arrowHeadPos,
baseBounds,
),
},
],
type: player.hasBall
? target
? ActionKind.SHOOT
: ActionKind.DRIBBLE
: target
? ActionKind.SCREEN
: ActionKind.MOVE,
}))
}} }}
onHeadPicked={(headPos) => { onHeadPicked={(headPos) => {
;(document.activeElement as HTMLElement).blur()
const baseBounds = const baseBounds =
courtRef.current!.getBoundingClientRect() courtRef.current!.getBoundingClientRect()
setPreviewArrowOriginPos( setPreviewAction({
ratioWithinBase( type: player.hasBall
? ActionKind.DRIBBLE
: ActionKind.MOVE,
fromPlayerId: player.id,
toPlayerId: undefined,
moveFrom: ratioWithinBase(
middlePos( middlePos(
pieceRef.getBoundingClientRect(), pieceRef.getBoundingClientRect(),
), ),
baseBounds, baseBounds,
), ),
) segments: [
setPreviewArrowEdges([ {
{ next: ratioWithinBase(
next: ratioWithinBase( middlePos(headPos),
middlePos(headPos), baseBounds,
baseBounds, ),
), },
}, ],
]) })
setPreviewArrowEnabled(true)
}} }}
onHeadDropped={(headRect) => { onHeadDropped={(headRect) => {
placeArrow(pieceRef, headRect) placeArrow(player, headRect)
setPreviewArrowEnabled(false) setPreviewAction(null)
}} }}
/>, />,
player.hasBall && ( player.hasBall && (
<BallAction <BallAction
key={3} key={2}
onDrop={(ref) => onDrop={(ref) =>
onBallMoved(ref.getBoundingClientRect()) onBallMoved(ref.getBoundingClientRect())
} }
@ -217,6 +241,8 @@ export function BasketCourt({
/> />
))} ))}
{internActions.map((action, idx) => renderAction(action, idx))}
{objects.map((object) => { {objects.map((object) => {
if (object.type == "ball") { if (object.type == "ball") {
return ( return (
@ -231,16 +257,13 @@ export function BasketCourt({
throw new Error("unknown court object", object.type) throw new Error("unknown court object", object.type)
})} })}
{isPreviewArrowEnabled && ( {previewAction && (
<BendableArrow <CourtAction
area={courtRef} courtRef={courtRef}
startPos={previewArrowOriginPos} action={previewAction}
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={() => {}} onActionDeleted={() => {}}
//TODO place those values in constants onActionChanges={() => {}}
endRadius={17}
startRadius={26}
/> />
)} )}
</div> </div>

@ -2,31 +2,22 @@
height: 50%; height: 50%;
} }
.arrow-action-pin, .arrow-action-icon {
.arrow-head-pick { user-select: none;
position: absolute; -moz-user-select: none;
min-width: 10px; max-width: 17px;
min-height: 10px; max-height: 17px;
border-radius: 100px;
background-color: red;
cursor: grab;
} }
.arrow-head-pick { .arrow-head-pick {
background-color: red; position: absolute;
} cursor: grab;
top: 0;
.arrow-head-xarrow { left: 0;
visibility: visible; min-width: 17px;
min-height: 17px;
} }
.arrow-action:active .arrow-head-xarrow { .arrow-head-pick:active {
visibility: visible; cursor: crosshair;
} }
/*.arrow-action:active .arrow-head-pick {*/
/* min-height: unset;*/
/* min-width: unset;*/
/* width: 0;*/
/* height: 0;*/
/*}*/

@ -41,15 +41,15 @@
position: absolute; position: absolute;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-evenly;
align-content: space-between; align-content: space-between;
align-items: center; align-items: center;
visibility: hidden; visibility: hidden;
margin-bottom: 10%; transform: translateY(-25px);
transform: translateY(-20px);
height: 20px; height: 20px;
width: 150%;
gap: 25%; gap: 25%;
} }

@ -2,13 +2,14 @@ import { Pos } from "../components/arrows/Pos"
import { Segment } from "../components/arrows/BendableArrow" import { Segment } from "../components/arrows/BendableArrow"
import { PlayerId } from "./Player" import { PlayerId } from "./Player"
export enum MovementActionKind { export enum ActionKind {
SCREEN = "SCREEN", SCREEN = "SCREEN",
DRIBBLE = "DRIBBLE", DRIBBLE = "DRIBBLE",
MOVE = "MOVE", MOVE = "MOVE",
SHOOT = "SHOOT",
} }
export type Action = { type: MovementActionKind } & MovementAction export type Action = { type: ActionKind } & MovementAction
export interface MovementAction { export interface MovementAction {
fromPlayerId: PlayerId fromPlayerId: PlayerId

@ -31,6 +31,7 @@ import { CourtObject } from "../tactic/CourtObjects"
import { CourtAction } from "./editor/CourtAction" import { CourtAction } from "./editor/CourtAction"
import { BasketCourt } from "../components/editor/BasketCourt" import { BasketCourt } from "../components/editor/BasketCourt"
import { ratioWithinBase } from "../components/arrows/Pos" import { ratioWithinBase } from "../components/arrows/Pos"
import { Action, ActionKind } from "../tactic/Action"
const ERROR_STYLE: CSSProperties = { const ERROR_STYLE: CSSProperties = {
borderColor: "red", borderColor: "red",
@ -254,16 +255,42 @@ function EditorView({
return -1 return -1
} }
function updateActions(actions: Action[], players: Player[]) {
return actions.map((action) => {
const originHasBall = players.find(
(p) => p.id == action.fromPlayerId,
)!.hasBall
let type = action.type
if (originHasBall && type == ActionKind.MOVE) {
type = ActionKind.DRIBBLE
} else if (originHasBall && type == ActionKind.SCREEN) {
type = ActionKind.SHOOT
} else if (type == ActionKind.DRIBBLE) {
type = ActionKind.MOVE
} else if (type == ActionKind.SHOOT) {
type = ActionKind.SCREEN
}
return {
...action,
type,
}
})
}
const onBallDropOnPlayer = (playerCollidedIdx: number) => { const onBallDropOnPlayer = (playerCollidedIdx: number) => {
setContent((content) => { setContent((content) => {
const ballObj = content.objects.findIndex((o) => o.type == "ball") const ballObj = content.objects.findIndex((o) => o.type == "ball")
let player = content.players.at(playerCollidedIdx) as Player let player = content.players.at(playerCollidedIdx) as Player
const players = content.players.toSpliced(playerCollidedIdx, 1, {
...player,
hasBall: true,
})
return { return {
...content, ...content,
players: content.players.toSpliced(playerCollidedIdx, 1, { actions: updateActions(content.actions, players),
...player, players,
hasBall: true,
}),
objects: content.objects.toSpliced(ballObj, 1), objects: content.objects.toSpliced(ballObj, 1),
} }
}) })
@ -303,13 +330,16 @@ function EditorView({
bottomRatio: y, bottomRatio: y,
} }
const players = content.players.map((player) => ({
...player,
hasBall: false,
}))
setContent((content) => { setContent((content) => {
return { return {
...content, ...content,
players: content.players.map((player) => ({ actions: updateActions(content.actions, players),
...player, players,
hasBall: false,
})),
objects: [...content.objects, courtObject], objects: [...content.objects, courtObject],
} }
}) })
@ -436,7 +466,7 @@ function EditorView({
objects={content.objects} objects={content.objects}
actions={content.actions} actions={content.actions}
onBallMoved={onBallDrop} onBallMoved={onBallDrop}
courtImage={() => <Court courtType={courtType} />} courtImage={<Court courtType={courtType} />}
courtRef={courtDivContentRef} courtRef={courtDivContentRef}
setActions={(actions) => setActions={(actions) =>
setContent((content) => ({ setContent((content) => ({

@ -1,6 +1,11 @@
import { Action } from "../../tactic/Action" import { Action, ActionKind } from "../../tactic/Action"
import BendableArrow from "../../components/arrows/BendableArrow" import BendableArrow from "../../components/arrows/BendableArrow"
import { RefObject } from "react" import { RefObject } from "react"
import {
MoveToHead,
ScreenHead,
ShootHead,
} from "../../components/actions/ArrowAction"
export interface CourtActionProps { export interface CourtActionProps {
action: Action action: Action
@ -15,8 +20,31 @@ export function CourtAction({
onActionDeleted, onActionDeleted,
courtRef, courtRef,
}: CourtActionProps) { }: CourtActionProps) {
let head
switch (action.type) {
case ActionKind.DRIBBLE:
case ActionKind.MOVE:
head = () => <MoveToHead />
break
case ActionKind.SCREEN:
head = () => <ScreenHead />
break
case ActionKind.SHOOT:
head = () => <ShootHead />
}
let dashArray
switch (action.type) {
case ActionKind.SHOOT:
dashArray = "10 5"
break
case ActionKind.DRIBBLE:
dashArray = "4"
}
return ( return (
<BendableArrow <BendableArrow
forceStraight={action.type == ActionKind.SHOOT}
area={courtRef} area={courtRef}
startPos={action.moveFrom} startPos={action.moveFrom}
segments={action.segments} segments={action.segments}
@ -25,8 +53,12 @@ export function CourtAction({
}} }}
//TODO place those magic values in constants //TODO place those magic values in constants
endRadius={action.toPlayerId ? 26 : 17} endRadius={action.toPlayerId ? 26 : 17}
startRadius={26} startRadius={0}
onDeleteRequested={onActionDeleted} onDeleteRequested={onActionDeleted}
style={{
head,
dashArray,
}}
/> />
) )
} }

Loading…
Cancel
Save