@luceosports/play-rendering 2.6.0 → 2.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,17 @@
1
+ import { AnimationMode, LinePart, LineType } from '../types';
2
+ export type TimelineKey = 'keyTimes' | 'keyTimesSeq';
3
+ export type HasKeyTimes = {
4
+ keyTimes: number[];
5
+ keyTimesSeq?: number[];
6
+ };
7
+ export declare const getTimelineKey: (mode: AnimationMode) => TimelineKey;
8
+ export declare const getEffectiveTimeline: (anim: HasKeyTimes, timeline: TimelineKey) => number[];
9
+ export declare const keyTimesToChunks: (keyTimes: number[]) => [number, number][];
10
+ export declare const deriveLineChunksWithFallback: (params: {
11
+ timeline: TimelineKey;
12
+ playerAnimation: HasKeyTimes;
13
+ lineAnimation: HasKeyTimes;
14
+ lineType: LineType;
15
+ lineParts: LinePart[];
16
+ legacySupport?: boolean;
17
+ }) => [number, number][];
@@ -15,8 +15,6 @@ export class LineModel {
15
15
  get hideLineTip(): boolean;
16
16
  get animations(): LineAnimation[];
17
17
  animationsByEndTime(mode: AnimationMode): number[];
18
- get animationKeyTimeChunks(): [number, number][];
19
- set animationKeyTimeChunks(value: [number, number][]);
20
18
  lastAnimEndTime(mode: AnimationMode): number;
21
19
  get firstLinePartControlPoint(): CourtPoint;
22
20
  get lastLinePartControlPoint(): CourtPoint;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luceosports/play-rendering",
3
- "version": "2.6.0",
3
+ "version": "2.6.2",
4
4
  "main": "dist/play-rendering.js",
5
5
  "types": "dist/play-rendering.d.ts",
6
6
  "scripts": {
package/src/constants.ts CHANGED
@@ -1,63 +1,65 @@
1
- export const SPORT_TYPE_BASKETBALL = 'BASKETBALL';
2
- export const SPORT_TYPE_VOLLEYBALL = 'VOLLEYBALL';
3
- export const SPORT_TYPE_BEACH_VOLLEYBALL = 'BEACH_VOLLEYBALL';
4
- export const SPORT_TYPE_FOOTBALL = 'FOOTBALL';
5
- export const SPORT_TYPE_LACROSSE = 'LACROSSE';
6
- export const SPORT_TYPE_LACROSSE_BOX = 'LACROSSE_BOX';
7
- export const SPORT_TYPE_SOCCER = 'SOCCER';
8
- export const SPORT_TYPE_HOCKEY = 'HOCKEY';
9
- export const SPORT_TYPE_BASEBALL = 'BASEBALL';
10
- export const SPORT_TYPE_SOFTBALL = 'SOFTBALL';
11
-
12
- export const COURT_TYPE_BIG3 = 'BIG3';
13
- export const COURT_TYPE_FIBA = 'FIBA';
14
- export const COURT_TYPE_NBA = 'NBA';
15
- export const COURT_TYPE_NCAAM = 'NCAAM';
16
- export const COURT_TYPE_NCAAW = 'NCAAW';
17
- export const COURT_TYPE_US_HIGH_SCHOOL = 'US_HIGH_SCHOOL';
18
- export const COURT_TYPE_US_JUNIOR_HIGH = 'US_JUNIOR_HIGH';
19
- export const COURT_TYPE_WNBA = 'WNBA';
20
-
21
- export const COURT_TYPE_FOOTBALL_COLLEGE = 'FOOTBALL_COLLEGE';
22
- export const COURT_TYPE_FOOTBALL_NFL = 'FOOTBALL_NFL';
23
- export const COURT_TYPE_FOOTBALL_HIGH_SCHOOL = 'FOOTBALL_HIGH_SCHOOL';
24
- export const COURT_TYPE_FOOTBALL_HIGH_SCHOOL_LEGACY = 'FOOTBALL';
25
-
26
- export const COURT_TYPE_VOLLEYBALL_INDOOR = 'VOLLEYBALL_INDOOR';
27
-
28
- export const COURT_TYPE_BEACH_VOLLEYBALL_NCAA = 'BEACH_VOLLEYBALL_NCAA';
29
- export const COURT_TYPE_BEACH_VOLLEYBALL_RECREATIONAL = 'BEACH_VOLLEYBALL_RECREATIONAL';
30
-
31
- export const COURT_TYPE_LACROSSE_US_M = 'LACROSSE_US_M';
32
- export const COURT_TYPE_LACROSSE_US_W = 'LACROSSE_US_W';
33
-
34
- export const COURT_TYPE_LACROSSE_BOX_US = 'LACROSSE_BOX_US';
35
- export const COURT_TYPE_LACROSSE_BOX_CLA = 'LACROSSE_BOX_CLA';
36
-
37
- export const COURT_TYPE_SOCCER_FIFA = 'SOCCER_FIFA';
38
- export const COURT_TYPE_SOCCER_NCAA = 'SOCCER_NCAA';
39
- export const COURT_TYPE_SOCCER_NFHS = 'SOCCER_NFHS';
40
- export const COURT_TYPE_SOCCER_U10 = 'SOCCER_U10';
41
- export const COURT_TYPE_SOCCER_U12 = 'SOCCER_U12';
42
- export const COURT_TYPE_SOCCER_U19 = 'SOCCER_U19';
43
-
44
- export const COURT_TYPE_HOCKEY_NHL = 'HOCKEY_NHL';
45
- export const COURT_TYPE_HOCKEY_INTERNATIONAL = 'HOCKEY_INTERNATIONAL';
46
-
47
- export const COURT_TYPE_BASEBALL_HIGH_SCHOOL = 'BASEBALL_HIGH_SCHOOL';
48
-
49
- export const COURT_TYPE_SOFTBALL_FP_COLLEGE = 'SOFTBALL_FP_COLLEGE';
50
- export const COURT_TYPE_SOFTBALL_FP_HIGH_SCHOOL = 'SOFTBALL_FP_HS';
51
-
52
- export const SHAPE_TYPE_CIRCLE = 'CIRCLE';
53
- export const SHAPE_TYPE_SQUARE = 'SQUARE';
54
- export const SHAPE_TYPE_TRIANGLE = 'TRIANGLE';
55
- export const SHAPE_TYPE_FOV = 'FOV';
56
- export const SHAPE_TYPE_XMARK = 'XMARK';
57
- export const SHAPE_TYPE_STRAIGHT = 'STRAIGHT';
58
- export const SHAPE_TYPE_CONE = 'CONE';
59
- export const SHAPE_TYPE_LINE_CUT = 'LINE.CUT';
60
- export const SHAPE_TYPE_LINE_SCREEN = 'LINE.SCREEN';
61
- export const SHAPE_TYPE_LINE_DRIBBLE = 'LINE.DRIBBLE';
62
- export const SHAPE_TYPE_LINE_PASS = 'LINE.PASS';
63
- export const SHAPE_TYPE_LINE_HANDOFF = 'LINE.HANDOFF';
1
+ export const BALL_TRANSFER_LINE_DURATION_DEFAULT = 0.03;
2
+
3
+ export const SPORT_TYPE_BASKETBALL = 'BASKETBALL';
4
+ export const SPORT_TYPE_VOLLEYBALL = 'VOLLEYBALL';
5
+ export const SPORT_TYPE_BEACH_VOLLEYBALL = 'BEACH_VOLLEYBALL';
6
+ export const SPORT_TYPE_FOOTBALL = 'FOOTBALL';
7
+ export const SPORT_TYPE_LACROSSE = 'LACROSSE';
8
+ export const SPORT_TYPE_LACROSSE_BOX = 'LACROSSE_BOX';
9
+ export const SPORT_TYPE_SOCCER = 'SOCCER';
10
+ export const SPORT_TYPE_HOCKEY = 'HOCKEY';
11
+ export const SPORT_TYPE_BASEBALL = 'BASEBALL';
12
+ export const SPORT_TYPE_SOFTBALL = 'SOFTBALL';
13
+
14
+ export const COURT_TYPE_BIG3 = 'BIG3';
15
+ export const COURT_TYPE_FIBA = 'FIBA';
16
+ export const COURT_TYPE_NBA = 'NBA';
17
+ export const COURT_TYPE_NCAAM = 'NCAAM';
18
+ export const COURT_TYPE_NCAAW = 'NCAAW';
19
+ export const COURT_TYPE_US_HIGH_SCHOOL = 'US_HIGH_SCHOOL';
20
+ export const COURT_TYPE_US_JUNIOR_HIGH = 'US_JUNIOR_HIGH';
21
+ export const COURT_TYPE_WNBA = 'WNBA';
22
+
23
+ export const COURT_TYPE_FOOTBALL_COLLEGE = 'FOOTBALL_COLLEGE';
24
+ export const COURT_TYPE_FOOTBALL_NFL = 'FOOTBALL_NFL';
25
+ export const COURT_TYPE_FOOTBALL_HIGH_SCHOOL = 'FOOTBALL_HIGH_SCHOOL';
26
+ export const COURT_TYPE_FOOTBALL_HIGH_SCHOOL_LEGACY = 'FOOTBALL';
27
+
28
+ export const COURT_TYPE_VOLLEYBALL_INDOOR = 'VOLLEYBALL_INDOOR';
29
+
30
+ export const COURT_TYPE_BEACH_VOLLEYBALL_NCAA = 'BEACH_VOLLEYBALL_NCAA';
31
+ export const COURT_TYPE_BEACH_VOLLEYBALL_RECREATIONAL = 'BEACH_VOLLEYBALL_RECREATIONAL';
32
+
33
+ export const COURT_TYPE_LACROSSE_US_M = 'LACROSSE_US_M';
34
+ export const COURT_TYPE_LACROSSE_US_W = 'LACROSSE_US_W';
35
+
36
+ export const COURT_TYPE_LACROSSE_BOX_US = 'LACROSSE_BOX_US';
37
+ export const COURT_TYPE_LACROSSE_BOX_CLA = 'LACROSSE_BOX_CLA';
38
+
39
+ export const COURT_TYPE_SOCCER_FIFA = 'SOCCER_FIFA';
40
+ export const COURT_TYPE_SOCCER_NCAA = 'SOCCER_NCAA';
41
+ export const COURT_TYPE_SOCCER_NFHS = 'SOCCER_NFHS';
42
+ export const COURT_TYPE_SOCCER_U10 = 'SOCCER_U10';
43
+ export const COURT_TYPE_SOCCER_U12 = 'SOCCER_U12';
44
+ export const COURT_TYPE_SOCCER_U19 = 'SOCCER_U19';
45
+
46
+ export const COURT_TYPE_HOCKEY_NHL = 'HOCKEY_NHL';
47
+ export const COURT_TYPE_HOCKEY_INTERNATIONAL = 'HOCKEY_INTERNATIONAL';
48
+
49
+ export const COURT_TYPE_BASEBALL_HIGH_SCHOOL = 'BASEBALL_HIGH_SCHOOL';
50
+
51
+ export const COURT_TYPE_SOFTBALL_FP_COLLEGE = 'SOFTBALL_FP_COLLEGE';
52
+ export const COURT_TYPE_SOFTBALL_FP_HIGH_SCHOOL = 'SOFTBALL_FP_HS';
53
+
54
+ export const SHAPE_TYPE_CIRCLE = 'CIRCLE';
55
+ export const SHAPE_TYPE_SQUARE = 'SQUARE';
56
+ export const SHAPE_TYPE_TRIANGLE = 'TRIANGLE';
57
+ export const SHAPE_TYPE_FOV = 'FOV';
58
+ export const SHAPE_TYPE_XMARK = 'XMARK';
59
+ export const SHAPE_TYPE_STRAIGHT = 'STRAIGHT';
60
+ export const SHAPE_TYPE_CONE = 'CONE';
61
+ export const SHAPE_TYPE_LINE_CUT = 'LINE.CUT';
62
+ export const SHAPE_TYPE_LINE_SCREEN = 'LINE.SCREEN';
63
+ export const SHAPE_TYPE_LINE_DRIBBLE = 'LINE.DRIBBLE';
64
+ export const SHAPE_TYPE_LINE_PASS = 'LINE.PASS';
65
+ export const SHAPE_TYPE_LINE_HANDOFF = 'LINE.HANDOFF';
@@ -1,32 +1,169 @@
1
- import NoteModel from '../models/NoteModel';
2
- import ShapeModel from '../models/ShapeModel';
3
- import _ from 'lodash';
4
- import { FrameDataOptions } from '../models/FrameModel';
5
-
6
- export function computeAnimationAlpha(model: NoteModel | ShapeModel, options: FrameDataOptions) {
7
- let alpha = model.hideForStatic && !options.inDrawingState ? 0 : 1;
8
- if (options.animationGlobalProgress) {
9
- alpha = 0;
10
- const animationKeyTimeChunks = model.animations.map(a => {
11
- const [startPercent, endPercent] = a.keyTimes;
12
- return [startPercent, endPercent];
13
- });
14
- animationKeyTimeChunks.forEach(([start, end]) => {
15
- const fadeInDuration = 0.03;
16
- const fadeInStart = start - fadeInDuration;
17
- const fadeInEnd = start;
18
- const fadeOutStart = end;
19
- const fadeOutEnd = end + fadeInDuration;
20
- if (_.inRange(options.animationGlobalProgress, fadeInStart, fadeInEnd)) {
21
- alpha = (fadeInDuration - (fadeInEnd - options.animationGlobalProgress)) * 10;
22
- }
23
- if (_.inRange(options.animationGlobalProgress, start, end)) {
24
- alpha = 1;
25
- }
26
- if (_.inRange(options.animationGlobalProgress, fadeOutStart, fadeOutEnd)) {
27
- alpha = (fadeOutEnd - options.animationGlobalProgress) * 10;
28
- }
29
- });
30
- }
31
- return alpha;
32
- }
1
+ import NoteModel from '../models/NoteModel';
2
+ import ShapeModel from '../models/ShapeModel';
3
+ import _ from 'lodash';
4
+ import { FrameDataOptions } from '../models/FrameModel';
5
+ import { AnimationMode, Line, LinePart, LineType, Player } from '../types';
6
+ import Bezier from '../math/Bezier';
7
+ import { isBallTransferLineType } from './common';
8
+ import { BALL_TRANSFER_LINE_DURATION_DEFAULT } from '../constants';
9
+
10
+ export type TimelineKey = 'keyTimes' | 'keyTimesSeq';
11
+ export type HasKeyTimes = { keyTimes: number[]; keyTimesSeq?: number[] };
12
+
13
+ export const getTimelineKey = (mode: AnimationMode): TimelineKey => {
14
+ return mode === AnimationMode.SEQUENTIAL ? 'keyTimesSeq' : 'keyTimes';
15
+ };
16
+
17
+ export const getEffectiveTimeline = (anim: HasKeyTimes, timeline: TimelineKey): number[] => {
18
+ if (timeline === 'keyTimesSeq') return anim.keyTimesSeq?.length ? anim.keyTimesSeq : anim.keyTimes;
19
+ return anim.keyTimes;
20
+ };
21
+
22
+ export const keyTimesToChunks = (keyTimes: number[]): [number, number][] => {
23
+ const chunks: [number, number][] = [];
24
+ keyTimes.forEach((value, index) => {
25
+ if (index) chunks.push([keyTimes[index - 1], value]);
26
+ });
27
+ return chunks;
28
+ };
29
+
30
+ export function computeDefaultLineAnimationDuration(lineType: LineType, linePart: LinePart): number {
31
+ if (isBallTransferLineType(lineType)) return BALL_TRANSFER_LINE_DURATION_DEFAULT;
32
+ const [cp1, cp2, cp3, cp4] = linePart.controlPoints;
33
+ return new Bezier(cp1, cp2, cp3 ?? null, cp4 ?? null).length / 100;
34
+ }
35
+
36
+ const splitChunkByPartDurations = (params: {
37
+ start: number;
38
+ end: number;
39
+ lineParts: LinePart[];
40
+ lineType: LineType;
41
+ }): [number, number][] => {
42
+ const { start, end, lineParts, lineType } = params;
43
+
44
+ const total = end - start;
45
+ if (!Number.isFinite(total) || total <= 0) return [[start, end]];
46
+
47
+ const durations = lineParts.map(lp => computeDefaultLineAnimationDuration(lineType, lp));
48
+ const sum = durations.reduce((acc, v) => acc + (Number.isFinite(v) ? v : 0), 0);
49
+
50
+ // If geometry is degenerate, fall back to equal split.
51
+ const weights =
52
+ sum > 0 ? durations.map(d => (Number.isFinite(d) ? d / sum : 0)) : lineParts.map(() => 1 / lineParts.length);
53
+
54
+ const keyTimes: number[] = [start];
55
+ let cursor = start;
56
+
57
+ for (let i = 0; i < weights.length; i += 1) {
58
+ const isLast = i === weights.length - 1;
59
+ const dt = isLast ? end - cursor : total * weights[i];
60
+ cursor += dt;
61
+ keyTimes.push(isLast ? end : cursor);
62
+ }
63
+
64
+ return keyTimesToChunks(keyTimes);
65
+ };
66
+
67
+ export const deriveLineChunksWithFallback = (params: {
68
+ timeline: TimelineKey;
69
+ playerAnimation: HasKeyTimes;
70
+ lineAnimation: HasKeyTimes;
71
+ lineType: LineType;
72
+ lineParts: LinePart[];
73
+ legacySupport?: boolean;
74
+ }): [number, number][] => {
75
+ const { timeline, playerAnimation, lineAnimation, lineType, lineParts, legacySupport = true } = params;
76
+ const linePartsCount = lineParts.length ?? 0;
77
+
78
+ const chunks = keyTimesToChunks(getEffectiveTimeline(lineAnimation, timeline));
79
+ if (chunks.length === linePartsCount) return chunks;
80
+
81
+ // If there are no chunks (bad/partial data), just return as-is.
82
+ if (!chunks.length) return chunks;
83
+
84
+ // Prefer geometry-based splitting over player animation fallback.
85
+ // Typical legacy case: multi-part line stored as a single [start,end] chunk.
86
+ if (chunks.length === 1 && lineParts?.length) {
87
+ const [[start, end]] = chunks;
88
+ const split = splitChunkByPartDurations({ start, end, lineParts, lineType });
89
+ console.log('geometry based', { chunks, split, linePartsCount });
90
+ if (split.length === linePartsCount) return split;
91
+ }
92
+
93
+ console.warn('PLAYER FALLBACK ATTEMPT!!!', { chunks, linePartsCount });
94
+
95
+ // If legacy support is disabled, do not attempt reconstruction from player timeline.
96
+ if (!legacySupport) return chunks;
97
+
98
+ // Backward compatibility:
99
+ // legacy multi-part lines stored only [start,end] in the line animation timeline,
100
+ // so reconstruct per-part timings from the player animation timeline slice.
101
+ const [[start, end]] = chunks;
102
+ const playerTimeline = getEffectiveTimeline(playerAnimation, timeline);
103
+
104
+ const startIndex = playerTimeline.indexOf(start);
105
+ const endIndex = playerTimeline.indexOf(end);
106
+ if (startIndex < 0 || endIndex < 0) return chunks;
107
+
108
+ const sliced = playerTimeline.slice(startIndex, endIndex + 1);
109
+ return keyTimesToChunks(sliced);
110
+ };
111
+
112
+ export const getLineAnimationChunks = (params: {
113
+ mode: AnimationMode;
114
+ line: Line;
115
+ players: Player[];
116
+ }): [number, number][] => {
117
+ const { mode, line, players } = params;
118
+
119
+ const timeline = getTimelineKey(mode);
120
+
121
+ const lineAnimation = line.animations?.[0] as unknown as HasKeyTimes | undefined;
122
+ if (!lineAnimation) return [];
123
+
124
+ const linePlayer = players.find(p => p.position === line.playerPositionOrigin);
125
+ const playerAnimation = (linePlayer?.animations?.[0] as unknown as HasKeyTimes | undefined) ?? undefined;
126
+
127
+ // Fast path: no player timeline available, just chunk the line timeline as-is.
128
+ if (!playerAnimation) {
129
+ return keyTimesToChunks(getEffectiveTimeline(lineAnimation, timeline));
130
+ }
131
+
132
+ // Full path: handle chunking + legacy reconstruction.
133
+ return deriveLineChunksWithFallback({
134
+ timeline,
135
+ playerAnimation,
136
+ lineAnimation,
137
+ lineType: line.type,
138
+ lineParts: line.lineParts,
139
+ legacySupport: false
140
+ });
141
+ };
142
+
143
+ export function computeAnimationAlpha(model: NoteModel | ShapeModel, options: FrameDataOptions) {
144
+ let alpha = model.hideForStatic && !options.inDrawingState ? 0 : 1;
145
+ if (options.animationGlobalProgress) {
146
+ alpha = 0;
147
+ const animationKeyTimeChunks = model.animations.map(a => {
148
+ const [startPercent, endPercent] = a.keyTimes;
149
+ return [startPercent, endPercent];
150
+ });
151
+ animationKeyTimeChunks.forEach(([start, end]) => {
152
+ const fadeInDuration = 0.03;
153
+ const fadeInStart = start - fadeInDuration;
154
+ const fadeInEnd = start;
155
+ const fadeOutStart = end;
156
+ const fadeOutEnd = end + fadeInDuration;
157
+ if (_.inRange(options.animationGlobalProgress, fadeInStart, fadeInEnd)) {
158
+ alpha = (fadeInDuration - (fadeInEnd - options.animationGlobalProgress)) * 10;
159
+ }
160
+ if (_.inRange(options.animationGlobalProgress, start, end)) {
161
+ alpha = 1;
162
+ }
163
+ if (_.inRange(options.animationGlobalProgress, fadeOutStart, fadeOutEnd)) {
164
+ alpha = (fadeOutEnd - options.animationGlobalProgress) * 10;
165
+ }
166
+ });
167
+ }
168
+ return alpha;
169
+ }
@@ -1,6 +1,10 @@
1
- import { ShapeType } from '../types';
1
+ import { LineType, ShapeType } from '../types';
2
2
  import { ShapeModels } from '../index';
3
3
 
4
+ export function isBallTransferLineType(type: LineType) {
5
+ return ['PASS', 'HANDOFF', 'SHOT'].includes(type);
6
+ }
7
+
4
8
  export function animationProgress(globalProgress: number, start: number, end: number) {
5
9
  const max = end - start;
6
10
  const local = globalProgress - start;
package/src/index.ts CHANGED
@@ -11,6 +11,7 @@ import * as ShapeModels from './models/ShapeModels';
11
11
  import * as Constants from './constants';
12
12
  import * as LineDrawingMath from './math/LineDrawingMath';
13
13
  import * as Helpers from './helpers/common';
14
+ import * as AnimationHelpers from './helpers/animation';
14
15
  import Bezier from './math/Bezier';
15
16
 
16
17
  import {
@@ -34,5 +35,6 @@ export {
34
35
  CourtTypeConstants,
35
36
  SportConstants,
36
37
  SportCourtTypeMap,
37
- Helpers
38
+ Helpers,
39
+ AnimationHelpers
38
40
  };
@@ -1,50 +1,62 @@
1
- import _ from 'lodash';
2
- import { animationProgress } from '../../../helpers/common';
3
- import ActionLineLayer from '../base/ActionLineLayer';
4
- import DribbleLineTrait from '../../../traits/DribbleLineTrait';
5
- import { LinePart } from '../../../types';
6
- import { LinePartAdjusted } from '../../../models/LineModel';
7
-
8
- export default class DribbleLineLayer extends ActionLineLayer {
9
- // ================ DribbleLineTrait Methods ====================
10
- private convertLinePartsToDribble(lineParts: LinePart[]): LinePartAdjusted[] {
11
- return [];
12
- }
13
- // ==============================================================
14
-
15
- drawLineCap() {
16
- if (this.allowDrawLineCap()) this.drawArrowLineCap();
17
- }
18
-
19
- setLineOptions() {
20
- const lineParts = [...this.line.getLineParts()];
21
- const dribbleLineParts = this.convertLinePartsToDribble(lineParts);
22
-
23
- this.line.setLinePartsAdjusted([]);
24
-
25
- dribbleLineParts.forEach(lp => {
26
- const { controlPoints, lpIndex } = lp;
27
- const [firstPoint] = controlPoints;
28
-
29
- let animationSegment: LinePartAdjusted['animationSegment'] | undefined = undefined;
30
-
31
- if (this.options.animationGlobalProgress) {
32
- const [start, end] = this.line.animationKeyTimeChunks[lpIndex!];
33
-
34
- if (_.inRange(this.options.animationGlobalProgress, start, end)) {
35
- if (animationProgress(this.options.animationGlobalProgress, start, end) > firstPoint.time!) {
36
- animationSegment = 'processed';
37
- } else {
38
- animationSegment = 'active';
39
- }
40
- } else if (this.options.animationGlobalProgress > end) {
41
- animationSegment = 'processed';
42
- }
43
- }
44
-
45
- this.line.addLinePartAdjusted({ ...lp, controlPoints, animationSegment });
46
- });
47
- }
48
- }
49
-
50
- Object.assign(DribbleLineLayer.prototype, DribbleLineTrait);
1
+ import _ from 'lodash';
2
+ import { animationProgress } from '../../../helpers/common';
3
+ import ActionLineLayer from '../base/ActionLineLayer';
4
+ import DribbleLineTrait from '../../../traits/DribbleLineTrait';
5
+ import { LinePart, AnimationMode } from '../../../types';
6
+ import { LinePartAdjusted } from '../../../models/LineModel';
7
+ import { getLineAnimationChunks } from '../../../helpers/animation';
8
+
9
+ export default class DribbleLineLayer extends ActionLineLayer {
10
+ // ================ DribbleLineTrait Methods ====================
11
+ private convertLinePartsToDribble(lineParts: LinePart[]): LinePartAdjusted[] {
12
+ return [];
13
+ }
14
+ // ==============================================================
15
+
16
+ drawLineCap() {
17
+ if (this.allowDrawLineCap()) this.drawArrowLineCap();
18
+ }
19
+
20
+ setLineOptions() {
21
+ const lineParts = [...this.line.getLineParts()];
22
+ const dribbleLineParts = this.convertLinePartsToDribble(lineParts);
23
+
24
+ const mode = this.options.animationMode ?? AnimationMode.SEQUENTIAL;
25
+
26
+ const chunks = getLineAnimationChunks({
27
+ mode,
28
+ line: this.line.originalData,
29
+ players: this.playData.players
30
+ });
31
+
32
+ this.line.setLinePartsAdjusted([]);
33
+
34
+ dribbleLineParts.forEach(lp => {
35
+ const { controlPoints, lpIndex } = lp;
36
+ const [firstPoint] = controlPoints;
37
+
38
+ let animationSegment: LinePartAdjusted['animationSegment'] | undefined = undefined;
39
+
40
+ if (this.options.animationGlobalProgress && lpIndex != null) {
41
+ const chunk = chunks[lpIndex];
42
+ if (chunk) {
43
+ const [start, end] = chunk;
44
+
45
+ if (_.inRange(this.options.animationGlobalProgress, start, end)) {
46
+ if (animationProgress(this.options.animationGlobalProgress, start, end) > firstPoint.time!) {
47
+ animationSegment = 'processed';
48
+ } else {
49
+ animationSegment = 'active';
50
+ }
51
+ } else if (this.options.animationGlobalProgress > end) {
52
+ animationSegment = 'processed';
53
+ }
54
+ }
55
+ }
56
+
57
+ this.line.addLinePartAdjusted({ ...lp, controlPoints, animationSegment });
58
+ });
59
+ }
60
+ }
61
+
62
+ Object.assign(DribbleLineLayer.prototype, DribbleLineTrait);
@@ -1,6 +1,6 @@
1
1
  import _ from 'lodash';
2
2
  import Bezier from '../math/Bezier';
3
- import { animationProgress, transformShapeTypeToImportKey } from '../helpers/common';
3
+ import { animationProgress, isBallTransferLineType, transformShapeTypeToImportKey } from '../helpers/common';
4
4
  import { distanceBetweenPoints, splitBezierCurveAtTVal } from '../math/LineDrawingMath';
5
5
  import CourtLayer from '../layers/CourtLayer';
6
6
  import LineLayer from '../layers/LineLayer';
@@ -24,6 +24,7 @@ import {
24
24
  import PlayModel, { PlayModelOptions, PlayStaticData } from './PlayModel';
25
25
  import { AnimationMode, Court, CourtPoint, SportType } from '../types';
26
26
  import ShapeModel from './ShapeModel';
27
+ import { getEffectiveTimeline, getLineAnimationChunks, getTimelineKey, keyTimesToChunks } from '../helpers/animation';
27
28
 
28
29
  export type FrameData = {
29
30
  sport: SportType;
@@ -197,42 +198,73 @@ export default class FrameModel {
197
198
  }
198
199
 
199
200
  get animationProgressPlayers() {
200
- const passLines = this.prevAnimationLines.filter(l => ['PASS', 'HANDOFF', 'SHOT'].includes(l.type));
201
+ const ballTransferLines = this.prevAnimationLines.filter(l => isBallTransferLineType(l.type));
201
202
  const { animationMode } = this.play.options;
202
203
 
203
- // TODO MAYBE WE WILL SUSPEND PLAYER ANIMATIONS COMPLETELY AND RELY ONLY ON LINE ANIMATIONS TO HAVE SINGE SOURCE OF TRUTH
204
+ // During playback, rely ONLY on line animations as the single source of truth for player movement.
205
+ // Player.animations are ignored here as we treat them as legacy data.
206
+
207
+ // Only "movement" lines affect player location (origin === terminus === player position).
208
+ // Ball transfer lines (PASS/HANDOFF/SHOT) are used for possession only.
209
+ const movementLines = this.play.playData.lines.filter(l => {
210
+ if (isBallTransferLineType(l.type)) return false;
211
+ return !!l.playerPositionOrigin && l.playerPositionOrigin === l.playerPositionTerminus;
212
+ });
213
+
214
+ const movementLinesByPlayer = _.groupBy(movementLines, l => l.playerPositionOrigin);
204
215
 
205
216
  return this.play.playData.players.map(p => {
206
217
  const player = new PlayerModel(p);
207
- player.setPossession(passLines);
218
+ player.setPossession(ballTransferLines);
208
219
 
209
- if (!player.animations.length) return player;
220
+ const playerMovementLines = movementLinesByPlayer[player.position] ?? [];
221
+ if (!playerMovementLines.length) return player;
210
222
 
211
- player.animations.forEach(animation => {
212
- const keyTimes =
213
- animationMode === AnimationMode.SEQUENTIAL ? animation.keyTimesSeq || animation.keyTimes : animation.keyTimes;
223
+ // Find active movement line segment (if any), otherwise fall back to the last completed end point.
224
+ let activeLocation: CourtPoint | null = null;
214
225
 
215
- const keyTimesChunks: [number, number][] = [];
216
- keyTimes.forEach((value, index) => {
217
- if (index) keyTimesChunks.push([keyTimes[index - 1], value]);
226
+ for (const lineData of playerMovementLines) {
227
+ const chunks = getLineAnimationChunks({
228
+ mode: animationMode,
229
+ line: lineData,
230
+ players: this.play.playData.players
218
231
  });
219
232
 
220
- keyTimesChunks.forEach(([start, end], index) => {
221
- if (_.inRange(this.animationGlobalProgress, start, end)) {
222
- const animProgress = this.animationProgress(start, end);
223
- const bezier = new Bezier(
224
- ...(animation.lineParts[index].controlPoints as ConstructorParameters<typeof Bezier>)
225
- );
226
- player.location = {
227
- x: bezier.x(animProgress),
228
- y: bezier.y(animProgress)
229
- };
230
- }
231
- });
232
- });
233
+ if (!chunks.length) continue;
234
+
235
+ const lineModel = new LineModel(lineData);
236
+ const parts = lineModel.getLineParts();
233
237
 
234
- if (this.animationGlobalProgress >= player.lastAnimEndTime(animationMode)!) {
235
- player.location = player.lastAnimationLastLinePartControlPoint!;
238
+ for (let index = 0; index < chunks.length; index += 1) {
239
+ const [start, end] = chunks[index];
240
+ if (!_.inRange(this.animationGlobalProgress, start, end)) continue;
241
+
242
+ const part = parts[index];
243
+ if (!part) continue;
244
+
245
+ const t = this.animationProgress(start, end);
246
+ const bezier = new Bezier(...(part.controlPoints as ConstructorParameters<typeof Bezier>));
247
+
248
+ activeLocation = { x: bezier.x(t), y: bezier.y(t) };
249
+ break;
250
+ }
251
+
252
+ if (activeLocation) break;
253
+ }
254
+
255
+ if (activeLocation) {
256
+ player.location = activeLocation;
257
+ return player;
258
+ }
259
+
260
+ // No active segment: put player at the end of the latest movement line that has finished.
261
+ const finishedMovementLines = playerMovementLines
262
+ .map(l => new LineModel(l))
263
+ .filter(lm => this.animationGlobalProgress >= lm.lastAnimEndTime(animationMode)!);
264
+
265
+ const lastFinished = _.maxBy(finishedMovementLines, lm => lm.lastAnimEndTime(animationMode)!);
266
+ if (lastFinished?.lastLinePartControlPoint) {
267
+ player.location = lastFinished.lastLinePartControlPoint;
236
268
  }
237
269
 
238
270
  return player;
@@ -246,13 +278,14 @@ export default class FrameModel {
246
278
  .map(l => {
247
279
  const line = new LineModel(l);
248
280
 
249
- line.setAnimationKeyTimesChunked(
250
- this.play.playData.players.find(p => p.position === line.playerPositionOrigin)!,
251
- animationMode
252
- );
281
+ const chunks = getLineAnimationChunks({
282
+ mode: animationMode,
283
+ line: l,
284
+ players: this.play.playData.players
285
+ });
253
286
 
254
287
  const linePartsAdjusted: LinePartAdjusted[] = [];
255
- line.animationKeyTimeChunks.forEach(([start, end], index) => {
288
+ chunks.forEach(([start, end], index) => {
256
289
  if (this.animationGlobalProgressPrev < end && this.animationGlobalProgress >= end) {
257
290
  window.dispatchEvent(
258
291
  new CustomEvent('@luceosports/play-rendering:lineAnimationFinished', { detail: { id: line.id } })