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

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

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

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

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

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

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

Loading…
Cancel
Save