arrows now works with relative positions ratio from their parent

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

@ -1,12 +0,0 @@
export function calculateRatio(
it: { x: number; y: number },
parent: DOMRect,
): { x: number; y: number } {
const relativeXPixels = it.x - parent.x
const relativeYPixels = it.y - parent.y
const xRatio = relativeXPixels / parent.width
const yRatio = relativeYPixels / parent.height
return { x: xRatio, y: yRatio }
}

@ -1,5 +1,4 @@
<svg width="567" height="269" viewBox="0 0 567 269" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="55" width="457" height="269" fill="#D9D9D9"/>
<line x1="73" y1="24" x2="495" y2="24" stroke="black" stroke-width="2"/>
<line x1="494" y1="23" x2="494" y2="247" stroke="black" stroke-width="2"/>
<line x1="495" y1="248" x2="73" y2="248" stroke="black" stroke-width="2"/>

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

@ -10,10 +10,10 @@ export interface ArrowActionProps {
}
export default function ArrowAction({
onHeadDropped,
onHeadPicked,
onHeadMoved,
}: ArrowActionProps) {
onHeadDropped,
onHeadPicked,
onHeadMoved,
}: ArrowActionProps) {
const arrowHeadRef = useRef<HTMLDivElement>(null)
return (

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

@ -1,17 +1,26 @@
import {
CSSProperties,
ReactElement,
RefObject,
useCallback,
useEffect,
useRef,
useState,
} from "react"
import { angle, middlePos, Pos, relativeTo } from "./Pos"
import {
add,
angle,
middlePos,
Pos,
posWithinBase,
ratioWithinBase,
} from "./Pos"
import "../../style/bendable_arrows.css"
import Draggable from "react-draggable"
export interface BendableArrowProps {
area: RefObject<HTMLElement>
startPos: Pos
segments: Segment[]
onSegmentsChanges: (edges: Segment[]) => void
@ -55,6 +64,7 @@ function Triangle({ fill }: { fill: string }) {
}
export default function BendableArrow({
area,
startPos,
segments,
@ -66,15 +76,10 @@ export default function BendableArrow({
}: BendableArrowProps) {
const containerRef = useRef<HTMLDivElement>(null)
const svgRef = useRef<SVGSVGElement>(null)
const pathRef = useRef<SVGPathElement>(null)
const styleWidth = style?.width ?? ArrowStyleDefaults.width
const [controlPointsDots, setControlPointsDots] = useState<ReactElement[]>(
[],
)
useEffect(() => {
setInternalSegments(segments)
}, [segments])
@ -86,49 +91,38 @@ export default function BendableArrow({
const headRef = useRef<HTMLDivElement>(null)
const tailRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const basePos =
containerRef.current!.parentElement!.getBoundingClientRect()
setControlPointsDots(computeControlPoints(basePos))
const selectionHandler = (e: MouseEvent) => {
if (!(e.target instanceof Node)) return
const isSelected = containerRef.current!.contains(e.target)
setIsSelected(isSelected)
}
document.addEventListener("mousedown", selectionHandler)
return () => document.removeEventListener("mousedown", selectionHandler)
}, [])
function computeControlPoints(basePos: Pos) {
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,
}
function computeControlPoints(parentBase: DOMRect) {
return segments.map(({ next, controlPoint }, i) => {
const prev = i == 0 ? startPos : segments[i - 1].next
const prevRelative = posWithinBase(prev, parentBase)
const nextRelative = posWithinBase(next, parentBase)
const cpPos =
controlPoint ||
ratioWithinBase(
add(
{
x: prevRelative.x / 2 + nextRelative.x / 2,
y: prevRelative.y / 2 + nextRelative.y / 2,
},
parentBase,
),
parentBase,
)
return (
<ControlPoint
key={i}
pos={cpPos}
basePos={basePos}
posRatio={cpPos}
parentBase={parentBase}
onPosValidated={(controlPoint) => {
const segment = internalSegments[i]
const segments = internalSegments.toSpliced(i, 1, {
const segment = segments[i]
const newSegments = segments.toSpliced(i, 1, {
...segment,
controlPoint,
})
onSegmentsChanges(segments)
onSegmentsChanges(newSegments)
}}
onMoves={(controlPoint) => {
setInternalSegments((is) => {
@ -144,6 +138,8 @@ export default function BendableArrow({
}
const update = useCallback(() => {
const parentBase = area.current!.getBoundingClientRect()
// only one segment is supported for now, which is the first.
// any other segments will be ignored
const segment = internalSegments[0] ?? null
@ -152,14 +148,11 @@ export default function BendableArrow({
const endPos = segment.next
const basePos =
containerRef.current!.parentElement!.getBoundingClientRect()
const startRelative = relativeTo(startPos, basePos)
const endRelative = relativeTo(endPos!, basePos)
const startRelative = posWithinBase(startPos, parentBase)
const endRelative = posWithinBase(endPos, parentBase)
const controlPoint = segment.controlPoint
? relativeTo(segment.controlPoint, basePos)
? posWithinBase(segment.controlPoint, parentBase)
: {
x: startRelative.x / 2 + endRelative.x / 2,
y: startRelative.y / 2 + endRelative.y / 2,
@ -205,10 +198,13 @@ export default function BendableArrow({
const segmentsRelatives = internalSegments.map(
({ next, controlPoint }) => {
return {
next: relativeTo(next, basePos),
next: posWithinBase(next, parentBase),
cp: controlPoint
? relativeTo(controlPoint, basePos)
: undefined,
? posWithinBase(controlPoint, parentBase)
: {
x: startRelative.x / 2 + endRelative.x / 2,
y: startRelative.y / 2 + endRelative.y / 2,
},
}
},
)
@ -219,11 +215,7 @@ export default function BendableArrow({
if (idx == internalSegments.length - 1) {
//if it is the last element
next = constraintInCircle(next, controlPoint, endRadius!)
}
if (cp == undefined) {
return `L${next.x - left} ${next.y - top}`
next = constraintInCircle(next, cp, endRadius!)
}
return `C${cp.x - left} ${cp.y - top}, ${cp.x - left} ${
@ -235,12 +227,27 @@ 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)
setControlPointsDots(computeControlPoints(basePos))
}, [startPos, internalSegments])
useEffect(update, [update])
useEffect(() => {
const selectionHandler = (e: MouseEvent) => {
if (!(e.target instanceof Node)) return
const isSelected = containerRef.current!.contains(e.target)
setIsSelected(isSelected)
}
document.addEventListener("mousedown", selectionHandler)
window.addEventListener("resize", update)
return () => {
document.removeEventListener("mousedown", selectionHandler)
window.removeEventListener("resize", update)
}
}, [update, containerRef])
return (
<div
ref={containerRef}
@ -275,37 +282,41 @@ export default function BendableArrow({
{style?.tail?.call(style) ?? <Triangle fill={"blue"} />}
</div>
{isSelected && controlPointsDots}
{isSelected &&
computeControlPoints(area.current!.getBoundingClientRect())}
</div>
)
}
interface ControlPointProps {
pos: Pos
basePos: Pos
posRatio: Pos
parentBase: DOMRect
onMoves: (currentPos: Pos) => void
onPosValidated: (newPos: Pos | undefined) => void
radius?: number
}
function ControlPoint({
pos,
posRatio,
parentBase,
onMoves,
onPosValidated,
radius = 7,
}: ControlPointProps) {
const ref = useRef<HTMLDivElement>(null)
const pos = posWithinBase(posRatio, parentBase)
return (
<Draggable
nodeRef={ref}
onStop={() => {
const pointPos = middlePos(ref.current!.getBoundingClientRect())
onPosValidated(pointPos)
onPosValidated(ratioWithinBase(pointPos, parentBase))
}}
onDrag={() => {
const pointPos = middlePos(ref.current!.getBoundingClientRect())
onMoves(pointPos)
onMoves(ratioWithinBase(pointPos, parentBase))
}}
position={{ x: pos.x - radius, y: pos.y - radius }}>
<div

@ -39,3 +39,17 @@ export function angle(a: Pos, b: Pos): number {
const r = relativeTo(a, b)
return Math.atan2(r.x, r.y)
}
export function ratioWithinBase(pos: Pos, base: DOMRect): Pos {
return {
x: (pos.x - base.x) / base.width,
y: (pos.y - base.y) / base.height,
}
}
export function posWithinBase(ratio: Pos, base: DOMRect): Pos {
return {
x: ratio.x * base.width,
y: ratio.y * base.height,
}
}

@ -1,20 +1,22 @@
import "../../style/basket_court.css"
import {CourtBall} from "./CourtBall";
import {ReactElement, RefObject, useCallback, useState,} from "react"
import { CourtBall } from "./CourtBall"
import {
ReactElement,
RefObject,
useCallback,
useLayoutEffect,
useState,
} from "react"
import CourtPlayer from "./CourtPlayer"
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 { middlePos, NULL_POS, Pos, ratioWithinBase } from "../arrows/Pos"
import BallAction from "../actions/BallAction"
import {CourtObject} from "../../tactic/CourtObjects";
import { CourtObject } from "../../tactic/CourtObjects"
export interface BasketCourtProps {
players: Player[]
@ -30,7 +32,7 @@ export interface BasketCourtProps {
onBallRemove: () => void
onBallMoved: (ball: DOMRect) => void
courtImage: string
courtImage: () => ReactElement
courtRef: RefObject<HTMLDivElement>
}
@ -70,12 +72,21 @@ export function BasketCourt({
const targetPos = document
.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,
moveFrom: middlePos(originRef.getBoundingClientRect()),
segments: [{ next: middlePos(targetPos) }],
moveFrom: start,
segments: [{ next: end }],
}
setActions((actions) => [...actions, action])
return
@ -98,8 +109,11 @@ export function BasketCourt({
const [previewArrowEdges, setPreviewArrowEdges] = useState<Segment[]>([])
const updateActionsRelatedTo = useCallback((player: Player) => {
const newPos = middlePos(
document.getElementById(player.id)!.getBoundingClientRect(),
const newPos = ratioWithinBase(
middlePos(
document.getElementById(player.id)!.getBoundingClientRect(),
),
courtRef.current!.getBoundingClientRect(),
)
setActions((actions) =>
actions.map((a) => {
@ -124,14 +138,18 @@ export function BasketCourt({
)
}, [])
const [internActions, setInternActions] = useState<Action[]>([])
useLayoutEffect(() => setInternActions(actions), [actions])
return (
<div
id="court-container"
className="court-container"
ref={courtRef}
style={{ position: "relative" }}>
<img src={courtImage} alt={"court"} id="court-svg" />
{courtImage()}
{actions.map((action, idx) => renderAction(action, idx))}
{internActions.map((action, idx) => renderAction(action, idx))}
{players.map((player) => (
<CourtPlayer
@ -148,17 +166,37 @@ export function BasketCourt({
/>,
<ArrowAction
key={2}
onHeadMoved={(headPos) =>
onHeadMoved={(headPos) => {
const baseBounds =
courtRef.current!.getBoundingClientRect()
setPreviewArrowEdges([
{ next: middlePos(headPos) },
{
next: ratioWithinBase(
middlePos(headPos),
baseBounds,
),
},
])
}
}}
onHeadPicked={(headPos) => {
const baseBounds =
courtRef.current!.getBoundingClientRect()
setPreviewArrowOriginPos(
middlePos(pieceRef.getBoundingClientRect()),
ratioWithinBase(
middlePos(
pieceRef.getBoundingClientRect(),
),
baseBounds,
),
)
setPreviewArrowEdges([
{ next: middlePos(headPos) },
{
next: ratioWithinBase(
middlePos(headPos),
baseBounds,
),
},
])
setPreviewArrowEnabled(true)
}}
@ -167,7 +205,14 @@ export function BasketCourt({
setPreviewArrowEnabled(false)
}}
/>,
player.hasBall && <BallAction key={3} onDrop={ref => onBallMoved(ref.getBoundingClientRect())}/>
player.hasBall && (
<BallAction
key={3}
onDrop={(ref) =>
onBallMoved(ref.getBoundingClientRect())
}
/>
),
]}
/>
))}
@ -188,6 +233,7 @@ export function BasketCourt({
{isPreviewArrowEnabled && (
<BendableArrow
area={courtRef}
startPos={previewArrowOriginPos}
segments={previewArrowEdges}
//do nothing on change, not really possible as it's a preview arrow

@ -3,7 +3,7 @@ import "../../style/player.css"
import Draggable from "react-draggable"
import { PlayerPiece } from "./PlayerPiece"
import { Player } from "../../tactic/Player"
import { calculateRatio } from "../../Utils"
import { ratioWithinBase } from "../arrows/Pos"
export interface PlayerProps<A extends ReactNode> {
player: Player
@ -41,7 +41,7 @@ export default function CourtPlayer<A extends ReactNode>({
const pieceBounds = pieceRef.current!.getBoundingClientRect()
const parentBounds = parentRef.current!.getBoundingClientRect()
const { x, y } = calculateRatio(pieceBounds, parentBounds)
const { x, y } = ratioWithinBase(pieceBounds, parentBounds)
onChange({
id: player.id,

@ -1,20 +0,0 @@
#court-container {
display: flex;
align-content: center;
align-items: center;
justify-content: center;
height: 100%;
background-color: var(--main-color);
}
#court-svg {
margin: 35px 0 35px 0;
height: 87%;
user-select: none;
-webkit-user-drag: none;
}
#court-svg * {
stroke: var(--selected-team-secondarycolor);
}

@ -82,7 +82,9 @@
#court-div {
background-color: var(--background-color);
height: 100%;
width: 100%;
display: flex;
align-items: center;
@ -90,11 +92,32 @@
align-content: center;
}
#court-div-bounds {
padding: 20px 20px 20px 20px;
#court-image-div {
background-color: white;
height: 100%;
width: 100%;
}
.court-container {
display: flex;
align-content: center;
align-items: center;
justify-content: center;
height: 75%;
}
#court-image {
height: 100%;
width: 100%;
user-select: none;
-webkit-user-drag: none;
}
#court-image * {
stroke: var(--selected-team-secondarycolor);
}
.react-draggable {
z-index: 2;
}

@ -1,25 +1,36 @@
import {CSSProperties, Dispatch, SetStateAction, useCallback, useMemo, useRef, useState,} from "react"
import {
CSSProperties,
Dispatch,
SetStateAction,
useCallback,
useMemo,
useRef,
useState,
} from "react"
import "../style/editor.css"
import TitleInput from "../components/TitleInput"
import plainCourt from "../assets/court/full_court.svg"
import halfCourt from "../assets/court/half_court.svg"
import PlainCourt from "../assets/court/full_court.svg?react"
import HalfCourt from "../assets/court/half_court.svg?react"
import {BallPiece} from "../components/editor/BallPiece"
import { BallPiece } from "../components/editor/BallPiece"
import {Rack} from "../components/Rack"
import {PlayerPiece} from "../components/editor/PlayerPiece"
import {Player} from "../tactic/Player"
import { Rack } from "../components/Rack"
import { PlayerPiece } from "../components/editor/PlayerPiece"
import { Player } from "../tactic/Player"
import {Tactic, TacticContent} from "../tactic/Tactic"
import {fetchAPI} from "../Fetcher"
import {Team} from "../tactic/Team"
import {calculateRatio} from "../Utils"
import { Tactic, TacticContent } from "../tactic/Tactic"
import { fetchAPI } from "../Fetcher"
import { Team } from "../tactic/Team"
import SavingState, {SaveState, SaveStates,} from "../components/editor/SavingState"
import SavingState, {
SaveState,
SaveStates,
} from "../components/editor/SavingState"
import {CourtObject} from "../tactic/CourtObjects"
import {CourtAction} from "./editor/CourtAction"
import {BasketCourt} from "../components/editor/BasketCourt";
import { CourtObject } from "../tactic/CourtObjects"
import { CourtAction } from "./editor/CourtAction"
import { BasketCourt } from "../components/editor/BasketCourt"
import { ratioWithinBase } from "../components/arrows/Pos"
const ERROR_STYLE: CSSProperties = {
borderColor: "red",
@ -150,7 +161,7 @@ function EditorView({
const refBounds = ref.getBoundingClientRect()
const courtBounds = courtDivContentRef.current!.getBoundingClientRect()
const { x, y } = calculateRatio(refBounds, courtBounds)
const { x, y } = ratioWithinBase(refBounds, courtBounds)
setContent((content) => {
return {
@ -178,7 +189,7 @@ function EditorView({
const refBounds = ref.getBoundingClientRect()
const courtBounds = courtDivContentRef.current!.getBoundingClientRect()
const { x, y } = calculateRatio(refBounds, courtBounds)
const { x, y } = ratioWithinBase(refBounds, courtBounds)
let courtObject: CourtObject
@ -283,7 +294,7 @@ function EditorView({
}
const courtBounds = courtDivContentRef.current!.getBoundingClientRect()
const { x, y } = calculateRatio(refBounds, courtBounds)
const { x, y } = ratioWithinBase(refBounds, courtBounds)
let courtObject: CourtObject
courtObject = {
@ -309,7 +320,10 @@ function EditorView({
...content,
players: toSplicedPlayers(content.players, player, false),
objects: [...content.objects],
actions: content.actions.filter(a => a.toPlayerId !== player.id && a.fromPlayerId !== player.id),
actions: content.actions.filter(
(a) =>
a.toPlayerId !== player.id && a.fromPlayerId !== player.id,
),
}))
let setter
switch (player.team) {
@ -420,12 +434,10 @@ function EditorView({
<BasketCourt
players={content.players}
objects={content.objects}
actions={content.actions}
onBallMoved={onBallDrop}
courtImage={
courtType == "PLAIN" ? plainCourt : halfCourt
}
courtImage={() => <Court courtType={courtType} />}
courtRef={courtDivContentRef}
actions={content.actions}
setActions={(actions) =>
setContent((content) => ({
...content,
@ -437,10 +449,15 @@ function EditorView({
<CourtAction
key={i}
action={action}
courtRef={courtDivContentRef}
onActionChanges={(a) =>
setContent((content) => ({
...content,
actions: content.actions.toSpliced(i, 1, a),
actions: content.actions.toSpliced(
i,
1,
a,
),
}))
}
/>
@ -486,6 +503,18 @@ function renderCourtObject(courtObject: RackedCourtObject) {
throw new Error("unknown racked court object ", courtObject.key)
}
function Court({ courtType }: { courtType: string }) {
return (
<div id="court-image-div">
{courtType == "PLAIN" ? (
<PlainCourt id="court-image" />
) : (
<HalfCourt id="court-image" />
)}
</div>
)
}
function getRackPlayers(team: Team, players: Player[]): RackedPlayer[] {
return ["1", "2", "3", "4", "5"]
.filter(

@ -1,14 +1,21 @@
import { Action } from "../../tactic/Action"
import BendableArrow from "../../components/arrows/BendableArrow"
import { RefObject } from "react"
export interface CourtActionProps {
action: Action
onActionChanges: (a: Action) => void
courtRef: RefObject<HTMLElement>
}
export function CourtAction({ action, onActionChanges }: CourtActionProps) {
export function CourtAction({
action,
onActionChanges,
courtRef,
}: CourtActionProps) {
return (
<BendableArrow
area={courtRef}
startPos={action.moveFrom}
segments={action.segments}
onSegmentsChanges={(edges) => {

Loading…
Cancel
Save