@luceosports/play-rendering 2.6.1 → 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.
- package/dist/play-rendering.js +23 -23
- package/dist/play-rendering.js.map +1 -1
- package/dist/types/helpers/animation.d.ts +4 -3
- package/package.json +1 -1
- package/src/constants.ts +65 -63
- package/src/helpers/animation.ts +61 -8
- package/src/helpers/common.ts +5 -1
- package/src/models/FrameModel.ts +570 -536
- package/src/models/LineModel.ts +2 -1
package/src/models/FrameModel.ts
CHANGED
|
@@ -1,536 +1,570 @@
|
|
|
1
|
-
import _ from 'lodash';
|
|
2
|
-
import Bezier from '../math/Bezier';
|
|
3
|
-
import { animationProgress, transformShapeTypeToImportKey } from '../helpers/common';
|
|
4
|
-
import { distanceBetweenPoints, splitBezierCurveAtTVal } from '../math/LineDrawingMath';
|
|
5
|
-
import CourtLayer from '../layers/CourtLayer';
|
|
6
|
-
import LineLayer from '../layers/LineLayer';
|
|
7
|
-
import PlayerLayer from '../layers/PlayerLayer';
|
|
8
|
-
import ShapeLayer from '../layers/ShapeLayer';
|
|
9
|
-
import NoteLayer from '../layers/NoteLayer';
|
|
10
|
-
import LineControlPointLayer from '../layers/LineControlPointLayer';
|
|
11
|
-
import ShapeControlPointLayer from '../layers/ShapeControlPointLayer';
|
|
12
|
-
import LineModel, { LinePartAdjusted } from './LineModel';
|
|
13
|
-
import PlayerModel from './PlayerModel';
|
|
14
|
-
import NoteModel from './NoteModel';
|
|
15
|
-
import * as ShapeModels from './ShapeModels';
|
|
16
|
-
|
|
17
|
-
import {
|
|
18
|
-
COURT_TYPE_FOOTBALL_HIGH_SCHOOL,
|
|
19
|
-
COURT_TYPE_FOOTBALL_HIGH_SCHOOL_LEGACY,
|
|
20
|
-
SPORT_TYPE_BASKETBALL,
|
|
21
|
-
SPORT_TYPE_FOOTBALL
|
|
22
|
-
} from '../constants';
|
|
23
|
-
|
|
24
|
-
import PlayModel, { PlayModelOptions, PlayStaticData } from './PlayModel';
|
|
25
|
-
import { AnimationMode, Court, CourtPoint, SportType } from '../types';
|
|
26
|
-
import ShapeModel from './ShapeModel';
|
|
27
|
-
import { getEffectiveTimeline, getLineAnimationChunks, getTimelineKey, keyTimesToChunks } from '../helpers/animation';
|
|
28
|
-
|
|
29
|
-
export type FrameData = {
|
|
30
|
-
sport: SportType;
|
|
31
|
-
court: Court;
|
|
32
|
-
lines: LineModel[];
|
|
33
|
-
players: PlayerModel[];
|
|
34
|
-
shapes: ShapeModel[];
|
|
35
|
-
notes: NoteModel[];
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export type FrameDataOptions = PlayModelOptions & {
|
|
39
|
-
staticData: PlayStaticData;
|
|
40
|
-
scale: number;
|
|
41
|
-
animationGlobalProgress: number;
|
|
42
|
-
playAnimationDuration: number;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
type ClosestObjectDistance<T extends PlayerModel | ShapeModel | NoteModel | LineModel> = {
|
|
46
|
-
object: T;
|
|
47
|
-
objectPoint: CourtPoint;
|
|
48
|
-
distance: number;
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
type InferEntityFromObjectType<T> = T extends 'player'
|
|
52
|
-
? PlayerModel
|
|
53
|
-
: T extends 'note'
|
|
54
|
-
? NoteModel
|
|
55
|
-
: T extends 'shape'
|
|
56
|
-
? ShapeModel
|
|
57
|
-
: LineModel;
|
|
58
|
-
|
|
59
|
-
export default class FrameModel {
|
|
60
|
-
private ctx: CanvasRenderingContext2D | null;
|
|
61
|
-
private animationGlobalProgressPrev: number;
|
|
62
|
-
|
|
63
|
-
constructor(private play: PlayModel, private phase = 1, private animationGlobalProgress = 0) {
|
|
64
|
-
this.animationGlobalProgressPrev = animationGlobalProgress;
|
|
65
|
-
this.ctx = null;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
get sport(): SportType {
|
|
69
|
-
const { court, sport } = this.play.playData;
|
|
70
|
-
if (sport) return sport;
|
|
71
|
-
return [COURT_TYPE_FOOTBALL_HIGH_SCHOOL_LEGACY, COURT_TYPE_FOOTBALL_HIGH_SCHOOL].includes(court.type)
|
|
72
|
-
? SPORT_TYPE_FOOTBALL
|
|
73
|
-
: SPORT_TYPE_BASKETBALL;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
get court(): Court {
|
|
77
|
-
const { court } = this.play.playData;
|
|
78
|
-
return {
|
|
79
|
-
...court,
|
|
80
|
-
type: court.type === COURT_TYPE_FOOTBALL_HIGH_SCHOOL_LEGACY ? COURT_TYPE_FOOTBALL_HIGH_SCHOOL : court.type
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
get frameWidth() {
|
|
85
|
-
return this.play.width;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
get frameHeight() {
|
|
89
|
-
return this.play.height;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
get frameData() {
|
|
93
|
-
return {
|
|
94
|
-
sport: this.sport,
|
|
95
|
-
court: this.court,
|
|
96
|
-
lines: this.lines,
|
|
97
|
-
players: this.players,
|
|
98
|
-
shapes: this.shapes,
|
|
99
|
-
notes: this.notes
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
get frameDataOptions(): FrameDataOptions {
|
|
104
|
-
return {
|
|
105
|
-
...this.play.options,
|
|
106
|
-
staticData: this.play.staticData,
|
|
107
|
-
scale: this.play.scale,
|
|
108
|
-
animationGlobalProgress: this.animationGlobalProgress,
|
|
109
|
-
playAnimationDuration: this.playAnimationDuration
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
get playAnimationDuration() {
|
|
114
|
-
const mode = this.play.options.animationMode;
|
|
115
|
-
return this.play.playData.lines.reduce<number>((result, l) => {
|
|
116
|
-
const line = new LineModel(l);
|
|
117
|
-
const lineLastAnimEndTime = line.lastAnimEndTime(mode)!;
|
|
118
|
-
return result > lineLastAnimEndTime ? result : lineLastAnimEndTime;
|
|
119
|
-
}, 0);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
get lines() {
|
|
123
|
-
return this.animationGlobalProgress ? this.animationProgressLines : this.currentPhaseLines;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
get players() {
|
|
127
|
-
return this.animationGlobalProgress ? this.animationProgressPlayers : this.currentPhasePlayers;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
get shapes(): ShapeModel[] {
|
|
131
|
-
if (!this.play.playData.shapes) return [];
|
|
132
|
-
return this.play.playData.shapes
|
|
133
|
-
.filter(s => {
|
|
134
|
-
return ShapeModels[transformShapeTypeToImportKey(s.type) as keyof typeof ShapeModels] !== undefined;
|
|
135
|
-
})
|
|
136
|
-
.map(s => {
|
|
137
|
-
return new ShapeModels[transformShapeTypeToImportKey(s.type) as keyof typeof ShapeModels](s);
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
get notes(): NoteModel[] {
|
|
142
|
-
if (!this.play.playData.notes) return [];
|
|
143
|
-
return this.play.playData.notes
|
|
144
|
-
.filter(n => {
|
|
145
|
-
return !n.displayModes || (n.displayModes as string[]).includes('onCourt');
|
|
146
|
-
})
|
|
147
|
-
.map(n => {
|
|
148
|
-
return new NoteModel(n);
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
get currentPhasePlayers() {
|
|
153
|
-
const { currentPhaseLines } = this;
|
|
154
|
-
const { huddleMode, magnetMode, animationMode } = this.play.options;
|
|
155
|
-
|
|
156
|
-
const lines = huddleMode || magnetMode ? this.playDataLines : this.prevPhaseLines;
|
|
157
|
-
|
|
158
|
-
const passLines = lines.filter(l => ['PASS', 'HANDOFF'].includes(l.type));
|
|
159
|
-
|
|
160
|
-
return this.play.playData.players.map(p => {
|
|
161
|
-
const player = new PlayerModel(p);
|
|
162
|
-
player.setPossession(passLines);
|
|
163
|
-
|
|
164
|
-
const playerPrevLines = lines
|
|
165
|
-
.filter(l => {
|
|
166
|
-
if (!huddleMode) return true;
|
|
167
|
-
if (this.phase < l.phase) return false;
|
|
168
|
-
return !currentPhaseLines.find(item => item.id === l.id); // exclude last 2 lines in huddle mode
|
|
169
|
-
})
|
|
170
|
-
.filter(l => l.playerPositionOrigin === player.position && l.playerPositionTerminus === player.position);
|
|
171
|
-
|
|
172
|
-
const playerCurrentPhaseLines = currentPhaseLines.filter(
|
|
173
|
-
l => magnetMode && l.playerPositionOrigin === player.position && l.playerPositionTerminus === player.position
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
const playerLines = playerCurrentPhaseLines.length ? playerCurrentPhaseLines : playerPrevLines;
|
|
177
|
-
if (playerLines.length) {
|
|
178
|
-
const maxLine = _.maxBy(playerLines, l => l.lastAnimEndTime(animationMode));
|
|
179
|
-
if (maxLine) {
|
|
180
|
-
const lastPoint = maxLine.lastLinePartControlPoint;
|
|
181
|
-
if (lastPoint) {
|
|
182
|
-
player.location = lastPoint;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
return player;
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
get currentPhaseLines() {
|
|
191
|
-
let lines = this.play.playData.lines.filter(l => l.phase === this.phase);
|
|
192
|
-
if (this.play.options.huddleMode) {
|
|
193
|
-
lines = _.takeRight(lines, 2);
|
|
194
|
-
}
|
|
195
|
-
return lines.map(l => {
|
|
196
|
-
return new LineModel(l);
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
get animationProgressPlayers() {
|
|
201
|
-
const
|
|
202
|
-
const { animationMode } = this.play.options;
|
|
203
|
-
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if (
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
return this
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
this.
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
this.
|
|
378
|
-
this.
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
return
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
return
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
return
|
|
476
|
-
})
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
return
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
}
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
import Bezier from '../math/Bezier';
|
|
3
|
+
import { animationProgress, isBallTransferLineType, transformShapeTypeToImportKey } from '../helpers/common';
|
|
4
|
+
import { distanceBetweenPoints, splitBezierCurveAtTVal } from '../math/LineDrawingMath';
|
|
5
|
+
import CourtLayer from '../layers/CourtLayer';
|
|
6
|
+
import LineLayer from '../layers/LineLayer';
|
|
7
|
+
import PlayerLayer from '../layers/PlayerLayer';
|
|
8
|
+
import ShapeLayer from '../layers/ShapeLayer';
|
|
9
|
+
import NoteLayer from '../layers/NoteLayer';
|
|
10
|
+
import LineControlPointLayer from '../layers/LineControlPointLayer';
|
|
11
|
+
import ShapeControlPointLayer from '../layers/ShapeControlPointLayer';
|
|
12
|
+
import LineModel, { LinePartAdjusted } from './LineModel';
|
|
13
|
+
import PlayerModel from './PlayerModel';
|
|
14
|
+
import NoteModel from './NoteModel';
|
|
15
|
+
import * as ShapeModels from './ShapeModels';
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
COURT_TYPE_FOOTBALL_HIGH_SCHOOL,
|
|
19
|
+
COURT_TYPE_FOOTBALL_HIGH_SCHOOL_LEGACY,
|
|
20
|
+
SPORT_TYPE_BASKETBALL,
|
|
21
|
+
SPORT_TYPE_FOOTBALL
|
|
22
|
+
} from '../constants';
|
|
23
|
+
|
|
24
|
+
import PlayModel, { PlayModelOptions, PlayStaticData } from './PlayModel';
|
|
25
|
+
import { AnimationMode, Court, CourtPoint, SportType } from '../types';
|
|
26
|
+
import ShapeModel from './ShapeModel';
|
|
27
|
+
import { getEffectiveTimeline, getLineAnimationChunks, getTimelineKey, keyTimesToChunks } from '../helpers/animation';
|
|
28
|
+
|
|
29
|
+
export type FrameData = {
|
|
30
|
+
sport: SportType;
|
|
31
|
+
court: Court;
|
|
32
|
+
lines: LineModel[];
|
|
33
|
+
players: PlayerModel[];
|
|
34
|
+
shapes: ShapeModel[];
|
|
35
|
+
notes: NoteModel[];
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type FrameDataOptions = PlayModelOptions & {
|
|
39
|
+
staticData: PlayStaticData;
|
|
40
|
+
scale: number;
|
|
41
|
+
animationGlobalProgress: number;
|
|
42
|
+
playAnimationDuration: number;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type ClosestObjectDistance<T extends PlayerModel | ShapeModel | NoteModel | LineModel> = {
|
|
46
|
+
object: T;
|
|
47
|
+
objectPoint: CourtPoint;
|
|
48
|
+
distance: number;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
type InferEntityFromObjectType<T> = T extends 'player'
|
|
52
|
+
? PlayerModel
|
|
53
|
+
: T extends 'note'
|
|
54
|
+
? NoteModel
|
|
55
|
+
: T extends 'shape'
|
|
56
|
+
? ShapeModel
|
|
57
|
+
: LineModel;
|
|
58
|
+
|
|
59
|
+
export default class FrameModel {
|
|
60
|
+
private ctx: CanvasRenderingContext2D | null;
|
|
61
|
+
private animationGlobalProgressPrev: number;
|
|
62
|
+
|
|
63
|
+
constructor(private play: PlayModel, private phase = 1, private animationGlobalProgress = 0) {
|
|
64
|
+
this.animationGlobalProgressPrev = animationGlobalProgress;
|
|
65
|
+
this.ctx = null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get sport(): SportType {
|
|
69
|
+
const { court, sport } = this.play.playData;
|
|
70
|
+
if (sport) return sport;
|
|
71
|
+
return [COURT_TYPE_FOOTBALL_HIGH_SCHOOL_LEGACY, COURT_TYPE_FOOTBALL_HIGH_SCHOOL].includes(court.type)
|
|
72
|
+
? SPORT_TYPE_FOOTBALL
|
|
73
|
+
: SPORT_TYPE_BASKETBALL;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
get court(): Court {
|
|
77
|
+
const { court } = this.play.playData;
|
|
78
|
+
return {
|
|
79
|
+
...court,
|
|
80
|
+
type: court.type === COURT_TYPE_FOOTBALL_HIGH_SCHOOL_LEGACY ? COURT_TYPE_FOOTBALL_HIGH_SCHOOL : court.type
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
get frameWidth() {
|
|
85
|
+
return this.play.width;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
get frameHeight() {
|
|
89
|
+
return this.play.height;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
get frameData() {
|
|
93
|
+
return {
|
|
94
|
+
sport: this.sport,
|
|
95
|
+
court: this.court,
|
|
96
|
+
lines: this.lines,
|
|
97
|
+
players: this.players,
|
|
98
|
+
shapes: this.shapes,
|
|
99
|
+
notes: this.notes
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
get frameDataOptions(): FrameDataOptions {
|
|
104
|
+
return {
|
|
105
|
+
...this.play.options,
|
|
106
|
+
staticData: this.play.staticData,
|
|
107
|
+
scale: this.play.scale,
|
|
108
|
+
animationGlobalProgress: this.animationGlobalProgress,
|
|
109
|
+
playAnimationDuration: this.playAnimationDuration
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
get playAnimationDuration() {
|
|
114
|
+
const mode = this.play.options.animationMode;
|
|
115
|
+
return this.play.playData.lines.reduce<number>((result, l) => {
|
|
116
|
+
const line = new LineModel(l);
|
|
117
|
+
const lineLastAnimEndTime = line.lastAnimEndTime(mode)!;
|
|
118
|
+
return result > lineLastAnimEndTime ? result : lineLastAnimEndTime;
|
|
119
|
+
}, 0);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
get lines() {
|
|
123
|
+
return this.animationGlobalProgress ? this.animationProgressLines : this.currentPhaseLines;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
get players() {
|
|
127
|
+
return this.animationGlobalProgress ? this.animationProgressPlayers : this.currentPhasePlayers;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
get shapes(): ShapeModel[] {
|
|
131
|
+
if (!this.play.playData.shapes) return [];
|
|
132
|
+
return this.play.playData.shapes
|
|
133
|
+
.filter(s => {
|
|
134
|
+
return ShapeModels[transformShapeTypeToImportKey(s.type) as keyof typeof ShapeModels] !== undefined;
|
|
135
|
+
})
|
|
136
|
+
.map(s => {
|
|
137
|
+
return new ShapeModels[transformShapeTypeToImportKey(s.type) as keyof typeof ShapeModels](s);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
get notes(): NoteModel[] {
|
|
142
|
+
if (!this.play.playData.notes) return [];
|
|
143
|
+
return this.play.playData.notes
|
|
144
|
+
.filter(n => {
|
|
145
|
+
return !n.displayModes || (n.displayModes as string[]).includes('onCourt');
|
|
146
|
+
})
|
|
147
|
+
.map(n => {
|
|
148
|
+
return new NoteModel(n);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
get currentPhasePlayers() {
|
|
153
|
+
const { currentPhaseLines } = this;
|
|
154
|
+
const { huddleMode, magnetMode, animationMode } = this.play.options;
|
|
155
|
+
|
|
156
|
+
const lines = huddleMode || magnetMode ? this.playDataLines : this.prevPhaseLines;
|
|
157
|
+
|
|
158
|
+
const passLines = lines.filter(l => ['PASS', 'HANDOFF'].includes(l.type));
|
|
159
|
+
|
|
160
|
+
return this.play.playData.players.map(p => {
|
|
161
|
+
const player = new PlayerModel(p);
|
|
162
|
+
player.setPossession(passLines);
|
|
163
|
+
|
|
164
|
+
const playerPrevLines = lines
|
|
165
|
+
.filter(l => {
|
|
166
|
+
if (!huddleMode) return true;
|
|
167
|
+
if (this.phase < l.phase) return false;
|
|
168
|
+
return !currentPhaseLines.find(item => item.id === l.id); // exclude last 2 lines in huddle mode
|
|
169
|
+
})
|
|
170
|
+
.filter(l => l.playerPositionOrigin === player.position && l.playerPositionTerminus === player.position);
|
|
171
|
+
|
|
172
|
+
const playerCurrentPhaseLines = currentPhaseLines.filter(
|
|
173
|
+
l => magnetMode && l.playerPositionOrigin === player.position && l.playerPositionTerminus === player.position
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const playerLines = playerCurrentPhaseLines.length ? playerCurrentPhaseLines : playerPrevLines;
|
|
177
|
+
if (playerLines.length) {
|
|
178
|
+
const maxLine = _.maxBy(playerLines, l => l.lastAnimEndTime(animationMode));
|
|
179
|
+
if (maxLine) {
|
|
180
|
+
const lastPoint = maxLine.lastLinePartControlPoint;
|
|
181
|
+
if (lastPoint) {
|
|
182
|
+
player.location = lastPoint;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return player;
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
get currentPhaseLines() {
|
|
191
|
+
let lines = this.play.playData.lines.filter(l => l.phase === this.phase);
|
|
192
|
+
if (this.play.options.huddleMode) {
|
|
193
|
+
lines = _.takeRight(lines, 2);
|
|
194
|
+
}
|
|
195
|
+
return lines.map(l => {
|
|
196
|
+
return new LineModel(l);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
get animationProgressPlayers() {
|
|
201
|
+
const ballTransferLines = this.prevAnimationLines.filter(l => isBallTransferLineType(l.type));
|
|
202
|
+
const { animationMode } = this.play.options;
|
|
203
|
+
|
|
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);
|
|
215
|
+
|
|
216
|
+
return this.play.playData.players.map(p => {
|
|
217
|
+
const player = new PlayerModel(p);
|
|
218
|
+
player.setPossession(ballTransferLines);
|
|
219
|
+
|
|
220
|
+
const playerMovementLines = movementLinesByPlayer[player.position] ?? [];
|
|
221
|
+
if (!playerMovementLines.length) return player;
|
|
222
|
+
|
|
223
|
+
// Find active movement line segment (if any), otherwise fall back to the last completed end point.
|
|
224
|
+
let activeLocation: CourtPoint | null = null;
|
|
225
|
+
|
|
226
|
+
for (const lineData of playerMovementLines) {
|
|
227
|
+
const chunks = getLineAnimationChunks({
|
|
228
|
+
mode: animationMode,
|
|
229
|
+
line: lineData,
|
|
230
|
+
players: this.play.playData.players
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
if (!chunks.length) continue;
|
|
234
|
+
|
|
235
|
+
const lineModel = new LineModel(lineData);
|
|
236
|
+
const parts = lineModel.getLineParts();
|
|
237
|
+
|
|
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;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return player;
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
get animationProgressLines() {
|
|
275
|
+
const { linesDisplayOnMoveOnly, animationMode } = this.play.options;
|
|
276
|
+
|
|
277
|
+
return this.play.playData.lines
|
|
278
|
+
.map(l => {
|
|
279
|
+
const line = new LineModel(l);
|
|
280
|
+
|
|
281
|
+
const chunks = getLineAnimationChunks({
|
|
282
|
+
mode: animationMode,
|
|
283
|
+
line: l,
|
|
284
|
+
players: this.play.playData.players
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const linePartsAdjusted: LinePartAdjusted[] = [];
|
|
288
|
+
chunks.forEach(([start, end], index) => {
|
|
289
|
+
if (this.animationGlobalProgressPrev < end && this.animationGlobalProgress >= end) {
|
|
290
|
+
window.dispatchEvent(
|
|
291
|
+
new CustomEvent('@luceosports/play-rendering:lineAnimationFinished', { detail: { id: line.id } })
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (_.inRange(this.animationGlobalProgress, start, end)) {
|
|
296
|
+
if (line.type === 'DRIBBLE') {
|
|
297
|
+
return linePartsAdjusted.push({ ...line.getLineParts()[index] });
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const linePathSplitted = splitBezierCurveAtTVal(
|
|
301
|
+
line.getLineParts()[index].controlPoints,
|
|
302
|
+
this.animationProgress(start, end)
|
|
303
|
+
) as LinePartAdjusted['controlPoints'][];
|
|
304
|
+
|
|
305
|
+
linePartsAdjusted.push({
|
|
306
|
+
controlPoints: linePathSplitted[0],
|
|
307
|
+
animationSegment: 'processed'
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
linePartsAdjusted.push({
|
|
311
|
+
controlPoints: linePathSplitted[1],
|
|
312
|
+
animationSegment: 'active'
|
|
313
|
+
});
|
|
314
|
+
} else if (this.animationGlobalProgress > end) {
|
|
315
|
+
linePartsAdjusted.push({
|
|
316
|
+
...line.getLineParts()[index],
|
|
317
|
+
animationSegment: 'processed'
|
|
318
|
+
});
|
|
319
|
+
} else {
|
|
320
|
+
linePartsAdjusted.push({ ...line.getLineParts()[index] });
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
line.setLinePartsAdjusted(linePartsAdjusted);
|
|
325
|
+
|
|
326
|
+
if (this.animationGlobalProgress > line.lastAnimEndTime(animationMode)!) {
|
|
327
|
+
line.color = { ...line.color, alpha: 0.1 }; // To adjust line cap opacity
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return line;
|
|
331
|
+
})
|
|
332
|
+
.filter(l => {
|
|
333
|
+
const [firstAnimation] = l.animations;
|
|
334
|
+
const keyTimes =
|
|
335
|
+
animationMode === AnimationMode.SEQUENTIAL
|
|
336
|
+
? firstAnimation.keyTimesSeq || firstAnimation.keyTimes
|
|
337
|
+
: firstAnimation.keyTimes;
|
|
338
|
+
|
|
339
|
+
const [start] = keyTimes;
|
|
340
|
+
const [end] = [...keyTimes].reverse();
|
|
341
|
+
|
|
342
|
+
const lineOnMove = _.inRange(this.animationGlobalProgress, start, end);
|
|
343
|
+
if (linesDisplayOnMoveOnly) return lineOnMove;
|
|
344
|
+
return lineOnMove || this.animationGlobalProgress > end || l.phase === this.phase;
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
get playDataLines() {
|
|
349
|
+
return this.play.playData.lines.map(l => {
|
|
350
|
+
return new LineModel(l);
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
get prevPhaseLines() {
|
|
355
|
+
return this.playDataLines.filter(l => l.phase < this.phase);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
get prevAnimationLines() {
|
|
359
|
+
const mode = this.play.options.animationMode;
|
|
360
|
+
return this.playDataLines.filter(l => this.animationGlobalProgress >= l.lastAnimEndTime(mode)!);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
draw() {
|
|
364
|
+
if (!this.ctx) throw new Error('Canvas context is not provided. Please use setContext() method.');
|
|
365
|
+
|
|
366
|
+
// console.time('draw');
|
|
367
|
+
|
|
368
|
+
this.ctx.clearRect(0, 0, this.frameWidth, this.frameHeight);
|
|
369
|
+
|
|
370
|
+
this._init();
|
|
371
|
+
|
|
372
|
+
const { frameData, frameDataOptions } = this;
|
|
373
|
+
new CourtLayer(this.ctx, frameData, frameDataOptions).apply();
|
|
374
|
+
new ShapeLayer(this.ctx, frameData, frameDataOptions).apply();
|
|
375
|
+
new LineLayer(this.ctx, frameData, frameDataOptions).apply();
|
|
376
|
+
new PlayerLayer(this.ctx, frameData, frameDataOptions).apply();
|
|
377
|
+
new LineControlPointLayer(this.ctx, frameData, frameDataOptions).apply();
|
|
378
|
+
new ShapeControlPointLayer(this.ctx, frameData, frameDataOptions).apply();
|
|
379
|
+
new NoteLayer(this.ctx, frameData, frameDataOptions).apply();
|
|
380
|
+
|
|
381
|
+
// console.timeEnd('draw');
|
|
382
|
+
|
|
383
|
+
return this;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
_init() {
|
|
387
|
+
if (!this.ctx) throw new Error('Canvas context is not provided. Please use setContext() method.');
|
|
388
|
+
|
|
389
|
+
this.ctx.canvas.width = this.frameWidth;
|
|
390
|
+
this.ctx.canvas.height = this.frameHeight;
|
|
391
|
+
|
|
392
|
+
this.ctx.scale(this.play.scale, this.play.scale);
|
|
393
|
+
this.ctx.translate(-this.court.courtRect.origin.x, -this.court.courtRect.origin.y);
|
|
394
|
+
if (this.play.options.mirror) {
|
|
395
|
+
this.ctx.scale(-1, 1);
|
|
396
|
+
this.ctx.translate(Math.abs(this.court.courtRect.origin.x) * 2 - this.court.courtRect.size.width, 0);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
setContext(ctx: CanvasRenderingContext2D) {
|
|
401
|
+
this.ctx = ctx;
|
|
402
|
+
return this;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
setPhase(newPhase: number) {
|
|
406
|
+
this.phase = newPhase;
|
|
407
|
+
return this;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
setAnimationGlobalProgress(newProgress: number) {
|
|
411
|
+
this.animationGlobalProgressPrev = this.animationGlobalProgress;
|
|
412
|
+
this.animationGlobalProgress = newProgress;
|
|
413
|
+
return this;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
animationProgress(start: number, end: number) {
|
|
417
|
+
return animationProgress(this.animationGlobalProgress, start, end);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
closestObjectToPoint<T extends 'player' | 'shape' | 'note' | 'line' | 'lineVertex'>(
|
|
421
|
+
viewPoint: CourtPoint,
|
|
422
|
+
objectType: T,
|
|
423
|
+
needConversion = true,
|
|
424
|
+
filterCallback?: (subject: InferEntityFromObjectType<T>) => boolean
|
|
425
|
+
): ClosestObjectDistance<InferEntityFromObjectType<T>> | null {
|
|
426
|
+
const courtPoint = needConversion ? this.convertViewPointToCourtPoint(viewPoint) : viewPoint;
|
|
427
|
+
if (objectType === 'player') {
|
|
428
|
+
return this.closestPlayerToPoint(
|
|
429
|
+
courtPoint,
|
|
430
|
+
filterCallback as (subject: PlayerModel) => boolean
|
|
431
|
+
) as ClosestObjectDistance<InferEntityFromObjectType<T>>;
|
|
432
|
+
}
|
|
433
|
+
if (objectType === 'shape') {
|
|
434
|
+
return this.closestShapeToPoint(courtPoint) as ClosestObjectDistance<InferEntityFromObjectType<T>>;
|
|
435
|
+
}
|
|
436
|
+
if (objectType === 'note') {
|
|
437
|
+
return this.closestNoteToPoint(courtPoint) as ClosestObjectDistance<InferEntityFromObjectType<T>>;
|
|
438
|
+
}
|
|
439
|
+
if (objectType === 'line') {
|
|
440
|
+
return this.closestLineToPoint(
|
|
441
|
+
courtPoint,
|
|
442
|
+
filterCallback as (subject: LineModel) => boolean
|
|
443
|
+
) as ClosestObjectDistance<InferEntityFromObjectType<T>>;
|
|
444
|
+
}
|
|
445
|
+
if (objectType === 'lineVertex') {
|
|
446
|
+
return this.closestLineVertexToPoint(
|
|
447
|
+
courtPoint,
|
|
448
|
+
filterCallback as (subject: LineModel) => boolean
|
|
449
|
+
) as ClosestObjectDistance<InferEntityFromObjectType<T>>;
|
|
450
|
+
}
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
closestLineToPoint(courtPoint: CourtPoint, filterCallback?: (subject: LineModel) => boolean) {
|
|
455
|
+
const lineDistances = this.lines
|
|
456
|
+
.filter(item => {
|
|
457
|
+
return filterCallback ? filterCallback(item) : true;
|
|
458
|
+
})
|
|
459
|
+
.reduce<ClosestObjectDistance<LineModel>[]>((result, item) => {
|
|
460
|
+
item.lineParts.forEach(linePart => {
|
|
461
|
+
const bezier = new Bezier(...(linePart.controlPoints as ConstructorParameters<typeof Bezier>));
|
|
462
|
+
for (let i = 1; i <= 100; i++) {
|
|
463
|
+
const tVal = i / 100;
|
|
464
|
+
const pointAtTVal = this.prepareCourtPoint({
|
|
465
|
+
x: bezier.x(tVal),
|
|
466
|
+
y: bezier.y(tVal)
|
|
467
|
+
});
|
|
468
|
+
result.push({
|
|
469
|
+
object: item,
|
|
470
|
+
objectPoint: pointAtTVal,
|
|
471
|
+
distance: distanceBetweenPoints(pointAtTVal, courtPoint)
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
return result;
|
|
476
|
+
}, []);
|
|
477
|
+
|
|
478
|
+
return _.minBy(lineDistances, 'distance');
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
closestLineVertexToPoint(courtPoint: CourtPoint, filterCallback?: (subject: LineModel) => boolean) {
|
|
482
|
+
const lineVertexDistances = this.lines
|
|
483
|
+
.filter(item => {
|
|
484
|
+
return filterCallback ? filterCallback(item) : true;
|
|
485
|
+
})
|
|
486
|
+
.reduce<ClosestObjectDistance<LineModel>[]>((result, item) => {
|
|
487
|
+
if (!item.id.length) return result;
|
|
488
|
+
|
|
489
|
+
const firstVertex = this.prepareCourtPoint(item.firstLinePartControlPoint);
|
|
490
|
+
const lastVertex = this.prepareCourtPoint(item.lastLinePartControlPoint);
|
|
491
|
+
result.push({
|
|
492
|
+
object: item,
|
|
493
|
+
objectPoint: firstVertex,
|
|
494
|
+
distance: distanceBetweenPoints(firstVertex, courtPoint)
|
|
495
|
+
});
|
|
496
|
+
result.push({
|
|
497
|
+
object: item,
|
|
498
|
+
objectPoint: lastVertex,
|
|
499
|
+
distance: distanceBetweenPoints(lastVertex, courtPoint)
|
|
500
|
+
});
|
|
501
|
+
return result;
|
|
502
|
+
}, []);
|
|
503
|
+
return _.minBy(lineVertexDistances, 'distance');
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
closestPlayerToPoint(courtPoint: CourtPoint, filterCallback?: (subject: PlayerModel) => boolean) {
|
|
507
|
+
const playerDistances = this.players
|
|
508
|
+
.filter(item => {
|
|
509
|
+
return filterCallback ? filterCallback(item) : true;
|
|
510
|
+
})
|
|
511
|
+
.reduce<ClosestObjectDistance<PlayerModel>[]>((result, item) => {
|
|
512
|
+
const playerLocation = this.prepareCourtPoint(item.location);
|
|
513
|
+
result.push({
|
|
514
|
+
object: item,
|
|
515
|
+
objectPoint: playerLocation,
|
|
516
|
+
distance: distanceBetweenPoints(playerLocation, courtPoint)
|
|
517
|
+
});
|
|
518
|
+
return result;
|
|
519
|
+
}, []);
|
|
520
|
+
return _.minBy(playerDistances, 'distance');
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
closestShapeToPoint(courtPoint: CourtPoint) {
|
|
524
|
+
const shapeDistances = this.shapes
|
|
525
|
+
.filter(shape => shape.rectWrapperContains(courtPoint))
|
|
526
|
+
.reduce<ClosestObjectDistance<ShapeModel>[]>((result, item) => {
|
|
527
|
+
const shapeLocation = this.prepareCourtPoint(item.location);
|
|
528
|
+
result.push({
|
|
529
|
+
object: item,
|
|
530
|
+
objectPoint: shapeLocation,
|
|
531
|
+
distance: distanceBetweenPoints(shapeLocation, courtPoint)
|
|
532
|
+
});
|
|
533
|
+
return result;
|
|
534
|
+
}, []);
|
|
535
|
+
return _.minBy(shapeDistances, 'distance');
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
closestNoteToPoint(courtPoint: CourtPoint) {
|
|
539
|
+
const noteDistances = this.notes
|
|
540
|
+
.filter(note => note.noteWrapperContains(courtPoint))
|
|
541
|
+
.reduce<ClosestObjectDistance<NoteModel>[]>((result, item) => {
|
|
542
|
+
const noteLocation = this.prepareCourtPoint(item.location);
|
|
543
|
+
result.push({
|
|
544
|
+
object: item,
|
|
545
|
+
objectPoint: noteLocation,
|
|
546
|
+
distance: distanceBetweenPoints(noteLocation, courtPoint)
|
|
547
|
+
});
|
|
548
|
+
return result;
|
|
549
|
+
}, []);
|
|
550
|
+
return noteDistances.length ? _.minBy(noteDistances, 'distance') : null;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
prepareCourtPoint(courtPoint: CourtPoint) {
|
|
554
|
+
const { mirror } = this.play.options;
|
|
555
|
+
const { width } = this.play.playData.court.courtRect.size;
|
|
556
|
+
const { origin } = this.play.playData.court.courtRect;
|
|
557
|
+
return {
|
|
558
|
+
x: mirror ? width + origin.x - (courtPoint.x - origin.x) : courtPoint.x,
|
|
559
|
+
y: courtPoint.y
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
convertViewPointToCourtPoint(viewPoint: CourtPoint) {
|
|
564
|
+
const { x: ox, y: oy } = this.play.playData.court.courtRect.origin;
|
|
565
|
+
return {
|
|
566
|
+
x: viewPoint.x / this.play.scale + ox,
|
|
567
|
+
y: viewPoint.y / this.play.scale + oy
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
}
|