@luceosports/play-rendering 2.0.1 → 2.0.3
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/.babelrc +2 -1
- package/dist/play-rendering.js +4 -4
- package/dist/play-rendering.js.map +1 -1
- package/dist/types/models/PlayModel.d.ts +50 -39
- package/dist/types/models/PlayerModel.d.ts +2 -1
- package/dist/types/models/ShapeModels/index.d.ts +2 -2
- package/package.json +2 -1
- package/src/helpers/common.ts +1 -1
- package/src/layers/CourtLayer.ts +9 -8
- package/src/layers/LineLayer.ts +2 -2
- package/src/layers/NoteLayer.ts +5 -3
- package/src/layers/PlayerLayer.ts +20 -1
- package/src/layers/ShapeLayer.ts +2 -2
- package/src/layers/court/layers/BASEBALL/courtTypes/BASEBALL_HIGH_SCHOOL/layers/ADirtLayer.ts +2 -1
- package/src/layers/court/layers/BASEBALL/courtTypes/BASEBALL_HIGH_SCHOOL/layers/BaseLineLayer.ts +1 -1
- package/src/layers/court/layers/BASKETBALL/common/LaneMarkingNBATrait.ts +13 -1
- package/src/layers/court/layers/BASKETBALL/common/LaneMarkingNCAATrait.ts +13 -1
- package/src/layers/court/layers/BASKETBALL/courtTypes/BIG3/layers/Big3Layer.ts +1 -1
- package/src/layers/court/layers/FOOTBALL/layers/FieldNumberLayer.ts +1 -1
- package/src/layers/court/layers/LACROSSE/courtTypes/LACROSSE_US_W/layers/FanLineLayer.ts +2 -1
- package/src/layers/line/base/InternalLineLayer.ts +5 -5
- package/src/layers/line/layers/DribbleLineLayer.ts +4 -4
- package/src/layers/shape/base/InternalShapeLayer.ts +1 -1
- package/src/layers/shape/layers/line/DribbleLineShapeLayer.ts +1 -1
- package/src/layers/shape/layers/line/base/InternalLineShapeLayer.ts +1 -1
- package/src/math/Bezier.ts +7 -5
- package/src/math/LineDrawingMath.ts +15 -15
- package/src/models/AnimationModel.ts +11 -11
- package/src/models/Base/InternalFrameModel.ts +2 -2
- package/src/models/FrameModel.ts +18 -15
- package/src/models/LineModel.ts +2 -2
- package/src/models/NoteModel.ts +7 -7
- package/src/models/Play/Options.ts +3 -1
- package/src/models/PlayModel.ts +57 -24
- package/src/models/PlayerModel.ts +9 -5
- package/src/models/ShapeModel.ts +3 -6
- package/src/models/ShapeModels/CircleShape.ts +4 -0
- package/src/models/ShapeModels/LineShape.ts +7 -2
- package/src/traits/DribbleLineTrait.ts +17 -2
- package/src/traits/LineDrawOperationsTrait.ts +31 -7
- package/src/types/index.ts +0 -1
- package/tsconfig.json +3 -1
|
@@ -12,8 +12,8 @@ export default class AnimationModel {
|
|
|
12
12
|
private running: boolean;
|
|
13
13
|
private framesLoadTime: number[];
|
|
14
14
|
private timeStart: number | null;
|
|
15
|
-
private timeElapsed: number;
|
|
16
|
-
private timeElapsedSaved: number;
|
|
15
|
+
private timeElapsed: number | null;
|
|
16
|
+
private timeElapsedSaved: number | null;
|
|
17
17
|
|
|
18
18
|
constructor(private ctx: CanvasRenderingContext2D, play: PlayModel) {
|
|
19
19
|
this.play = _.cloneDeep(play);
|
|
@@ -35,7 +35,7 @@ export default class AnimationModel {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
get globalProgress() {
|
|
38
|
-
return (this.timeElapsed * 100) / (this.animationDuration * 1000) / 100;
|
|
38
|
+
return ((this.timeElapsed || 0) * 100) / (this.animationDuration * 1000) / 100;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
get currentPlayPhase() {
|
|
@@ -43,7 +43,7 @@ export default class AnimationModel {
|
|
|
43
43
|
return _.inRange(this.globalProgress, interval.min, interval.max);
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
-
return parseInt(phaseInterval ? phaseInterval[0] : Object.keys(this.linesPhaseIntervals).pop());
|
|
46
|
+
return parseInt(phaseInterval ? phaseInterval[0] : Object.keys(this.linesPhaseIntervals).pop()!);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
get linesPhaseIntervals(): Record<number, { min: number; max: number }> {
|
|
@@ -52,7 +52,7 @@ export default class AnimationModel {
|
|
|
52
52
|
if (!result[line.phase]) result[line.phase] = { min: 0, max: 0 };
|
|
53
53
|
line.animations.forEach(({ keyTimes }) => {
|
|
54
54
|
const [from] = keyTimes;
|
|
55
|
-
const to = [...keyTimes].pop()
|
|
55
|
+
const to = [...keyTimes].pop()!;
|
|
56
56
|
if (from < result[line.phase].min || (!result[line.phase].min && line.phase > 1)) result[line.phase].min = from;
|
|
57
57
|
if (to > result[line.phase].max) result[line.phase].max = to;
|
|
58
58
|
});
|
|
@@ -61,9 +61,9 @@ export default class AnimationModel {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
get lastLineAnimationMax() {
|
|
64
|
-
return this.play.playData.lines.reduce((result, line) => {
|
|
64
|
+
return this.play.playData.lines.reduce<number>((result, line) => {
|
|
65
65
|
const lineModel = new LineModel(line);
|
|
66
|
-
return result > lineModel.lastAnimEndTime ? result : lineModel.lastAnimEndTime
|
|
66
|
+
return result > lineModel.lastAnimEndTime! ? result : lineModel.lastAnimEndTime!;
|
|
67
67
|
}, 0);
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -78,7 +78,7 @@ export default class AnimationModel {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
pause() {
|
|
81
|
-
cancelAnimationFrame(this.loopId);
|
|
81
|
+
cancelAnimationFrame(this.loopId!);
|
|
82
82
|
this.running = false;
|
|
83
83
|
this.timeStart = null;
|
|
84
84
|
this.timeElapsedSaved = this.timeElapsed;
|
|
@@ -117,7 +117,7 @@ export default class AnimationModel {
|
|
|
117
117
|
if (this.globalProgress >= this.lastLineAnimationMax) {
|
|
118
118
|
progressCallback(this.lastLineAnimationMax);
|
|
119
119
|
this.reset();
|
|
120
|
-
cancelAnimationFrame(this.loopId);
|
|
120
|
+
cancelAnimationFrame(this.loopId!);
|
|
121
121
|
setTimeout(() => {
|
|
122
122
|
finishCallback();
|
|
123
123
|
}, 1000);
|
|
@@ -128,10 +128,10 @@ export default class AnimationModel {
|
|
|
128
128
|
|
|
129
129
|
if (!this.timeStart) this.timeStart = timestamp;
|
|
130
130
|
|
|
131
|
-
const frameTime = timestamp - this.timeStart + this.timeElapsedSaved - this.timeElapsed;
|
|
131
|
+
const frameTime = timestamp - this.timeStart + (this.timeElapsedSaved || 0) - (this.timeElapsed || 0);
|
|
132
132
|
this.framesLoadTime.push(frameTime);
|
|
133
133
|
|
|
134
|
-
this.timeElapsed = timestamp - this.timeStart + this.timeElapsedSaved;
|
|
134
|
+
this.timeElapsed = timestamp - this.timeStart + (this.timeElapsedSaved || 0);
|
|
135
135
|
|
|
136
136
|
if (debug) {
|
|
137
137
|
console.log(
|
|
@@ -10,11 +10,11 @@ export default class Model<M extends Line | Player | Note | Shape, A extends Lin
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
_getAttrOriginal<K extends keyof M>(attr: K) {
|
|
13
|
-
return
|
|
13
|
+
return this.originalData[attr];
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
_getAttr<K extends keyof A>(attr: K) {
|
|
17
|
-
return
|
|
17
|
+
return this.adjustedData[attr];
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
_setAttr<K extends keyof A>(attr: K, value: A[K]) {
|
package/src/models/FrameModel.ts
CHANGED
|
@@ -9,7 +9,7 @@ import ShapeLayer from '../layers/ShapeLayer';
|
|
|
9
9
|
import NoteLayer from '../layers/NoteLayer';
|
|
10
10
|
import LineControlPointLayer from '../layers/LineControlPointLayer';
|
|
11
11
|
import ShapeControlPointLayer from '../layers/ShapeControlPointLayer';
|
|
12
|
-
import LineModel from './LineModel';
|
|
12
|
+
import LineModel, { LinePartAdjusted } from './LineModel';
|
|
13
13
|
import PlayerModel from './PlayerModel';
|
|
14
14
|
import NoteModel from './NoteModel';
|
|
15
15
|
import * as ShapeModels from './ShapeModels';
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
} from '../constants';
|
|
23
23
|
|
|
24
24
|
import PlayModel, { PlayModelOptions, PlayStaticData } from './PlayModel';
|
|
25
|
-
import { Court, CourtPoint, SportType } from '../types';
|
|
25
|
+
import { Court, CourtPoint, ShapeType, SportType } from '../types';
|
|
26
26
|
import ShapeModel from './ShapeModel';
|
|
27
27
|
|
|
28
28
|
export type FrameData = {
|
|
@@ -56,11 +56,12 @@ type InferEntityFromObjectType<T> = T extends 'player'
|
|
|
56
56
|
: LineModel;
|
|
57
57
|
|
|
58
58
|
export default class FrameModel {
|
|
59
|
-
private ctx: CanvasRenderingContext2D;
|
|
59
|
+
private ctx: CanvasRenderingContext2D | null;
|
|
60
60
|
private animationGlobalProgressPrev: number;
|
|
61
61
|
|
|
62
62
|
constructor(private play: PlayModel, private phase = 1, private animationGlobalProgress = 0) {
|
|
63
63
|
this.animationGlobalProgressPrev = animationGlobalProgress;
|
|
64
|
+
this.ctx = null;
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
get sport(): SportType {
|
|
@@ -109,9 +110,9 @@ export default class FrameModel {
|
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
get playAnimationDuration() {
|
|
112
|
-
return this.play.playData.lines.reduce((result, l) => {
|
|
113
|
+
return this.play.playData.lines.reduce<number>((result, l) => {
|
|
113
114
|
const line = new LineModel(l);
|
|
114
|
-
const lineLastAnimEndTime = line.lastAnimEndTime
|
|
115
|
+
const lineLastAnimEndTime = line.lastAnimEndTime!;
|
|
115
116
|
return result > lineLastAnimEndTime ? result : lineLastAnimEndTime;
|
|
116
117
|
}, 0);
|
|
117
118
|
}
|
|
@@ -128,10 +129,10 @@ export default class FrameModel {
|
|
|
128
129
|
if (!this.play.playData.shapes) return [];
|
|
129
130
|
return this.play.playData.shapes
|
|
130
131
|
.filter(s => {
|
|
131
|
-
return ShapeModels[transformShapeTypeToImportKey(s.type)] !== undefined;
|
|
132
|
+
return ShapeModels[transformShapeTypeToImportKey(s.type) as keyof typeof ShapeModels] !== undefined;
|
|
132
133
|
})
|
|
133
134
|
.map(s => {
|
|
134
|
-
return new ShapeModels[transformShapeTypeToImportKey(s.type)](s);
|
|
135
|
+
return new ShapeModels[transformShapeTypeToImportKey(s.type) as keyof typeof ShapeModels](s);
|
|
135
136
|
});
|
|
136
137
|
}
|
|
137
138
|
|
|
@@ -203,7 +204,7 @@ export default class FrameModel {
|
|
|
203
204
|
if (!player.animations.length) return player;
|
|
204
205
|
|
|
205
206
|
player.animations.forEach(animation => {
|
|
206
|
-
const keyTimesChunks = [];
|
|
207
|
+
const keyTimesChunks: [number, number][] = [];
|
|
207
208
|
animation.keyTimes.forEach((value, index) => {
|
|
208
209
|
if (index) keyTimesChunks.push([animation.keyTimes[index - 1], value]);
|
|
209
210
|
});
|
|
@@ -222,8 +223,8 @@ export default class FrameModel {
|
|
|
222
223
|
});
|
|
223
224
|
});
|
|
224
225
|
|
|
225
|
-
if (this.animationGlobalProgress >= player.lastAnimEndTime) {
|
|
226
|
-
player.location = player.lastAnimationLastLinePartControlPoint
|
|
226
|
+
if (this.animationGlobalProgress >= player.lastAnimEndTime!) {
|
|
227
|
+
player.location = player.lastAnimationLastLinePartControlPoint!;
|
|
227
228
|
}
|
|
228
229
|
|
|
229
230
|
return player;
|
|
@@ -237,10 +238,10 @@ export default class FrameModel {
|
|
|
237
238
|
const line = new LineModel(l);
|
|
238
239
|
|
|
239
240
|
line.setAnimationKeyTimesChunked(
|
|
240
|
-
this.play.playData.players.find(p => p.position === line.playerPositionOrigin)
|
|
241
|
+
this.play.playData.players.find(p => p.position === line.playerPositionOrigin)!
|
|
241
242
|
);
|
|
242
243
|
|
|
243
|
-
const linePartsAdjusted = [];
|
|
244
|
+
const linePartsAdjusted: LinePartAdjusted[] = [];
|
|
244
245
|
line.animationKeyTimeChunks.forEach(([start, end], index) => {
|
|
245
246
|
if (this.animationGlobalProgressPrev < end && this.animationGlobalProgress >= end) {
|
|
246
247
|
window.dispatchEvent(
|
|
@@ -254,7 +255,7 @@ export default class FrameModel {
|
|
|
254
255
|
const linePathSplitted = splitBezierCurveAtTVal(
|
|
255
256
|
line.getLineParts()[index].controlPoints,
|
|
256
257
|
this.animationProgress(start, end)
|
|
257
|
-
);
|
|
258
|
+
) as LinePartAdjusted['controlPoints'][];
|
|
258
259
|
linePartsAdjusted.push({ controlPoints: linePathSplitted[0], alpha: 0.1 });
|
|
259
260
|
linePartsAdjusted.push({ controlPoints: linePathSplitted[1], alpha: line.color.alpha });
|
|
260
261
|
} else if (this.animationGlobalProgress > end) {
|
|
@@ -265,7 +266,7 @@ export default class FrameModel {
|
|
|
265
266
|
});
|
|
266
267
|
line.setLinePartsAdjusted(linePartsAdjusted);
|
|
267
268
|
|
|
268
|
-
if (this.animationGlobalProgress > line.lastAnimEndTime) {
|
|
269
|
+
if (this.animationGlobalProgress > line.lastAnimEndTime!) {
|
|
269
270
|
line.color = { ...line.color, alpha: 0.1 }; // To adjust line cap opacity
|
|
270
271
|
}
|
|
271
272
|
|
|
@@ -292,7 +293,7 @@ export default class FrameModel {
|
|
|
292
293
|
}
|
|
293
294
|
|
|
294
295
|
get prevAnimationLines() {
|
|
295
|
-
return this.playDataLines.filter(l => this.animationGlobalProgress >= l.lastAnimEndTime);
|
|
296
|
+
return this.playDataLines.filter(l => this.animationGlobalProgress >= l.lastAnimEndTime!);
|
|
296
297
|
}
|
|
297
298
|
|
|
298
299
|
draw() {
|
|
@@ -319,6 +320,8 @@ export default class FrameModel {
|
|
|
319
320
|
}
|
|
320
321
|
|
|
321
322
|
_init() {
|
|
323
|
+
if (!this.ctx) throw new Error('Canvas context is not provided. Please use setContext() method.');
|
|
324
|
+
|
|
322
325
|
this.ctx.canvas.width = this.frameWidth;
|
|
323
326
|
this.ctx.canvas.height = this.frameHeight;
|
|
324
327
|
|
package/src/models/LineModel.ts
CHANGED
|
@@ -48,7 +48,7 @@ export default class LineModel extends Model<LineData, LineAdjusted> {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
setAnimationKeyTimesChunked(linePlayer: Player) {
|
|
51
|
-
let keyTimesChunks = [];
|
|
51
|
+
let keyTimesChunks: [number, number][] = [];
|
|
52
52
|
this.animations[0].keyTimes.forEach((value, index) => {
|
|
53
53
|
if (index) keyTimesChunks.push([this.animations[0].keyTimes[index - 1], value]);
|
|
54
54
|
});
|
|
@@ -142,6 +142,6 @@ export default class LineModel extends Model<LineData, LineAdjusted> {
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
get lastLinePartControlPoint() {
|
|
145
|
-
return [...[...this.lineParts].pop()
|
|
145
|
+
return [...[...this.lineParts].pop()!.controlPoints].pop()!;
|
|
146
146
|
}
|
|
147
147
|
}
|
package/src/models/NoteModel.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Model from './Base/InternalFrameModel';
|
|
2
|
-
import { Note as NoteData } from '../types';
|
|
2
|
+
import { CourtPoint, Note as NoteData } from '../types';
|
|
3
3
|
|
|
4
4
|
const NOTE_GREY_COLOR = { red: 0.502, green: 0.502, blue: 0.502, alpha: 0.35 };
|
|
5
5
|
|
|
@@ -84,9 +84,9 @@ export default class NoteModel extends Model<NoteData, NoteData> {
|
|
|
84
84
|
return 'top';
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
get internalCtx() {
|
|
87
|
+
get internalCtx(): CanvasRenderingContext2D {
|
|
88
88
|
// used only for measureText feature to get a text width
|
|
89
|
-
return this.internalCanvas.getContext('2d')
|
|
89
|
+
return this.internalCanvas.getContext('2d')!;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
preProcessedTextForContext() {
|
|
@@ -108,13 +108,13 @@ export default class NoteModel extends Model<NoteData, NoteData> {
|
|
|
108
108
|
const fitWidth = NoteModel.NOTE_WRAP_MAX_WIDTH - NoteModel.NOTE_WRAP_PADDING * 2;
|
|
109
109
|
|
|
110
110
|
const lines = [];
|
|
111
|
-
const words = this.text.split(' ').reduce((result, word) => {
|
|
111
|
+
const words = this.text.split(' ').reduce<string[]>((result, word) => {
|
|
112
112
|
if (!word.match(/\n/)) {
|
|
113
113
|
result.push(word);
|
|
114
114
|
return result;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
const nlSegments = [];
|
|
117
|
+
const nlSegments: string[] = [];
|
|
118
118
|
let curSeg = '';
|
|
119
119
|
for (let i = 0; i < word.length; i++) {
|
|
120
120
|
const char = word[i];
|
|
@@ -164,7 +164,7 @@ export default class NoteModel extends Model<NoteData, NoteData> {
|
|
|
164
164
|
_getLineInfo(inputWords: string[]) {
|
|
165
165
|
let lineWidth = 0;
|
|
166
166
|
let hasLineBreak = false;
|
|
167
|
-
const wordsResult = [];
|
|
167
|
+
const wordsResult: { text: string; width: number }[] = [];
|
|
168
168
|
inputWords.forEach((w, i) => {
|
|
169
169
|
if (w === '\n') {
|
|
170
170
|
hasLineBreak = true;
|
|
@@ -186,7 +186,7 @@ export default class NoteModel extends Model<NoteData, NoteData> {
|
|
|
186
186
|
return { words: wordsResult, width: lineWidth, hasLineBreak };
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
noteWrapperContains({ x, y }) {
|
|
189
|
+
noteWrapperContains({ x, y }: CourtPoint) {
|
|
190
190
|
const { box } = this.preProcessedTextForContext();
|
|
191
191
|
|
|
192
192
|
const topLeft = this.location;
|
|
@@ -22,7 +22,9 @@ export function useDefaults(options?: Partial<PlayModelOptions>): PlayModelOptio
|
|
|
22
22
|
legacyPrintStyle: false,
|
|
23
23
|
playerTokenScale: 1,
|
|
24
24
|
// TODO: refactor NBA court type constants below
|
|
25
|
-
showHalfCourtCircle: true
|
|
25
|
+
showHalfCourtCircle: true,
|
|
26
|
+
playersMap: [],
|
|
27
|
+
labelsOverrideType: null
|
|
26
28
|
};
|
|
27
29
|
return {
|
|
28
30
|
...defaults,
|
package/src/models/PlayModel.ts
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
SPORT_TYPE_VOLLEYBALL
|
|
14
14
|
} from '../constants';
|
|
15
15
|
|
|
16
|
-
import { PlayConstructData, PlayData, SportType } from '../types';
|
|
16
|
+
import { PlayConstructData, PlayData, Position, SportType } from '../types';
|
|
17
17
|
|
|
18
18
|
import hardwoodImageData from '../assets/wood_bg.png';
|
|
19
19
|
import grassImageData from '../assets/grass_bg.png';
|
|
@@ -31,49 +31,65 @@ export type ImageConfigItem = {
|
|
|
31
31
|
reverse?: boolean;
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
+
export type PlayerHeadshotItem = {
|
|
35
|
+
id: string;
|
|
36
|
+
image: HTMLImageElement;
|
|
37
|
+
};
|
|
38
|
+
|
|
34
39
|
export type PlayStaticData = {
|
|
35
40
|
backgroundOptions: typeof PlayModel.backgroundOptions;
|
|
36
41
|
watermark: typeof PlayModel.watermark;
|
|
37
42
|
playerHats: readonly ImageConfigItem[];
|
|
38
43
|
shapes: readonly ImageConfigItem[];
|
|
44
|
+
playerHeadshots: typeof PlayModel.playerHeadshots;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type PlayersMapItem = {
|
|
48
|
+
position: Position;
|
|
49
|
+
textOverride: string | null;
|
|
50
|
+
teamPlayerId: string | null;
|
|
51
|
+
playerHatKey?: string | null;
|
|
39
52
|
};
|
|
40
53
|
|
|
41
54
|
export type PlayModelOptions = {
|
|
42
|
-
width
|
|
43
|
-
lineColor
|
|
44
|
-
linesDisplay
|
|
45
|
-
linesSelectedIds
|
|
46
|
-
shapeSelectedId
|
|
47
|
-
noteSelectedId
|
|
48
|
-
playersHiddenPositions
|
|
49
|
-
background
|
|
50
|
-
watermark
|
|
51
|
-
mirror
|
|
52
|
-
speed
|
|
53
|
-
position
|
|
54
|
-
huddleMode
|
|
55
|
-
magnetMode
|
|
56
|
-
playerTokenScale
|
|
57
|
-
flipPlayerLabels
|
|
58
|
-
linesDisplayOnMoveOnly
|
|
59
|
-
linesHiddenIds
|
|
60
|
-
legacyPrintStyle
|
|
61
|
-
showHalfCourtCircle
|
|
55
|
+
width: number;
|
|
56
|
+
lineColor: string;
|
|
57
|
+
linesDisplay: boolean;
|
|
58
|
+
linesSelectedIds: string[];
|
|
59
|
+
shapeSelectedId: string | null;
|
|
60
|
+
noteSelectedId: string | null;
|
|
61
|
+
playersHiddenPositions: string[];
|
|
62
|
+
background: string;
|
|
63
|
+
watermark: string | null;
|
|
64
|
+
mirror: boolean;
|
|
65
|
+
speed: number;
|
|
66
|
+
position: string | null;
|
|
67
|
+
huddleMode: boolean;
|
|
68
|
+
magnetMode: boolean;
|
|
69
|
+
playerTokenScale: number;
|
|
70
|
+
flipPlayerLabels: boolean;
|
|
71
|
+
linesDisplayOnMoveOnly: boolean;
|
|
72
|
+
linesHiddenIds: string[];
|
|
73
|
+
legacyPrintStyle: boolean;
|
|
74
|
+
showHalfCourtCircle: boolean;
|
|
75
|
+
playersMap: PlayersMapItem[];
|
|
76
|
+
labelsOverrideType: 'Headshot' | null;
|
|
62
77
|
};
|
|
63
78
|
|
|
64
79
|
export default class PlayModel {
|
|
65
80
|
public name: string;
|
|
66
81
|
public playData: PlayData;
|
|
67
82
|
public options: PlayModelOptions;
|
|
83
|
+
public static playerHeadshots: PlayerHeadshotItem[] = [];
|
|
68
84
|
public static playerHats: readonly ImageConfigItem[];
|
|
69
85
|
public static shapes: readonly ImageConfigItem[];
|
|
70
|
-
public static watermark: { LuceoSports: HTMLImageElement; TeamLogo: HTMLImageElement };
|
|
86
|
+
public static watermark: { LuceoSports: HTMLImageElement; TeamLogo: HTMLImageElement | null };
|
|
71
87
|
public static backgroundOptions: Record<SportType, HTMLImageElement> & { Hardwood: HTMLImageElement };
|
|
72
88
|
|
|
73
89
|
constructor(data: PlayConstructData, options?: Partial<PlayModelOptions>) {
|
|
74
90
|
this.name = data.name;
|
|
75
91
|
this.options = useDefaults(options);
|
|
76
|
-
this.
|
|
92
|
+
this.playData = data.playData;
|
|
77
93
|
}
|
|
78
94
|
|
|
79
95
|
static async init({ teamLogoPath = '' } = {}) {
|
|
@@ -103,6 +119,22 @@ export default class PlayModel {
|
|
|
103
119
|
};
|
|
104
120
|
}
|
|
105
121
|
|
|
122
|
+
static async loadPlayerHeadshots(data: { id: string; headshotUrl?: string | null }[]) {
|
|
123
|
+
await Promise.all(
|
|
124
|
+
data.map(async ({ id, headshotUrl }) => {
|
|
125
|
+
try {
|
|
126
|
+
if (headshotUrl) {
|
|
127
|
+
const image = await loadImage(headshotUrl);
|
|
128
|
+
PlayModel.playerHeadshots.push({ id, image });
|
|
129
|
+
}
|
|
130
|
+
} catch (e) {
|
|
131
|
+
console.error(e);
|
|
132
|
+
}
|
|
133
|
+
return true;
|
|
134
|
+
})
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
106
138
|
get totalPhasesCount() {
|
|
107
139
|
const distinctPhases = _.uniq(_.map(this.playData.lines, 'phase'));
|
|
108
140
|
return distinctPhases.length || 1;
|
|
@@ -125,7 +157,8 @@ export default class PlayModel {
|
|
|
125
157
|
backgroundOptions: PlayModel.backgroundOptions,
|
|
126
158
|
watermark: PlayModel.watermark,
|
|
127
159
|
playerHats: PlayModel.playerHats,
|
|
128
|
-
shapes: PlayModel.shapes
|
|
160
|
+
shapes: PlayModel.shapes,
|
|
161
|
+
playerHeadshots: PlayModel.playerHeadshots
|
|
129
162
|
};
|
|
130
163
|
}
|
|
131
164
|
|
|
@@ -7,6 +7,14 @@ export default class PlayerModel extends Model<PlayerData, PlayerData> {
|
|
|
7
7
|
return this._getAttr('id');
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
get textOverride() {
|
|
11
|
+
return this._getAttr('textOverride');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
set textOverride(data) {
|
|
15
|
+
this._setAttr('textOverride', data);
|
|
16
|
+
}
|
|
17
|
+
|
|
10
18
|
get textLabel() {
|
|
11
19
|
return this._getAttr('textOverride') || this.position;
|
|
12
20
|
}
|
|
@@ -39,10 +47,6 @@ export default class PlayerModel extends Model<PlayerData, PlayerData> {
|
|
|
39
47
|
return this._getAttr('color');
|
|
40
48
|
}
|
|
41
49
|
|
|
42
|
-
get playerHatKey() {
|
|
43
|
-
return this._getAttr('playerHatKey');
|
|
44
|
-
}
|
|
45
|
-
|
|
46
50
|
get animations() {
|
|
47
51
|
return this._getAttr('animations');
|
|
48
52
|
}
|
|
@@ -60,7 +64,7 @@ export default class PlayerModel extends Model<PlayerData, PlayerData> {
|
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
get lastAnimationLastLinePartControlPoint() {
|
|
63
|
-
return [...[...this.lastAnimation
|
|
67
|
+
return [...[...this.lastAnimation!.lineParts].pop()!.controlPoints].pop();
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
setPossession(prevPassLines: Line[]) {
|
package/src/models/ShapeModel.ts
CHANGED
|
@@ -8,7 +8,7 @@ const WRAP_POINT_TOP_RIGHT_INDEX = 1;
|
|
|
8
8
|
const WRAP_POINT_BOT_RIGHT_INDEX = 2;
|
|
9
9
|
const WRAP_POINT_BOT_LEFT_INDEX = 3;
|
|
10
10
|
|
|
11
|
-
export default class ShapeModel extends Model<ShapeData, ShapeData> {
|
|
11
|
+
export default abstract class ShapeModel extends Model<ShapeData, ShapeData> {
|
|
12
12
|
get id() {
|
|
13
13
|
return this._getAttr('id');
|
|
14
14
|
}
|
|
@@ -76,10 +76,7 @@ export default class ShapeModel extends Model<ShapeData, ShapeData> {
|
|
|
76
76
|
return +(aDistance * bDistance).toFixed(2);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
get shapeControlPoints()
|
|
80
|
-
// Override this function in a subclass if needed
|
|
81
|
-
return [];
|
|
82
|
-
}
|
|
79
|
+
abstract get shapeControlPoints(): CourtPoint[];
|
|
83
80
|
|
|
84
81
|
rectWrapperContains(point: CourtPoint) {
|
|
85
82
|
// https://stackoverflow.com/questions/17136084/checking-if-a-point-is-inside-a-rotated-rectangle
|
|
@@ -102,7 +99,7 @@ export default class ShapeModel extends Model<ShapeData, ShapeData> {
|
|
|
102
99
|
return trianglesAreaSum <= this.rectWrapAreaTranslated;
|
|
103
100
|
}
|
|
104
101
|
|
|
105
|
-
computeCenterForRect([topLeft, topRight, botRight, botLeft]) {
|
|
102
|
+
computeCenterForRect([topLeft, topRight, botRight, botLeft]: [CourtPoint, CourtPoint, CourtPoint, CourtPoint]) {
|
|
106
103
|
return {
|
|
107
104
|
x: (topLeft.x + botRight.x) / 2,
|
|
108
105
|
y: (topRight.y + botLeft.y) / 2
|
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
import ShapeModel from '../ShapeModel';
|
|
2
2
|
import { rotatePoint } from '../../helpers/common';
|
|
3
3
|
import { bezierBoundingBox } from '../../math/LineDrawingMath';
|
|
4
|
+
import { CourtPoint } from '../../types';
|
|
4
5
|
|
|
5
6
|
export default class LineShape extends ShapeModel {
|
|
6
7
|
get rectWrapPointsPure() {
|
|
7
|
-
const { controlPoints } = this.linePart
|
|
8
|
+
const { controlPoints } = this.linePart!;
|
|
8
9
|
return bezierBoundingBox(controlPoints);
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
get controlPointsTranslated() {
|
|
12
|
-
return this.linePart
|
|
13
|
+
return this.linePart!.controlPoints.map(point => {
|
|
13
14
|
const scaled = { x: point.x * this.scale.x, y: point.y * this.scale.y };
|
|
14
15
|
const rotated = rotatePoint(scaled.x, scaled.y, 0, 0, this.angle);
|
|
15
16
|
return { x: rotated.x + this.location.x, y: rotated.y + this.location.y };
|
|
16
17
|
});
|
|
17
18
|
}
|
|
19
|
+
|
|
20
|
+
get shapeControlPoints() {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
18
23
|
}
|
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
import Bezier from '../math/Bezier';
|
|
2
|
-
import { LinePart } from '../types';
|
|
2
|
+
import { CourtPoint, LinePart } from '../types';
|
|
3
3
|
import { CourtPointAdjusted, LinePartAdjusted } from '../models/LineModel';
|
|
4
4
|
|
|
5
|
+
interface InheritedPropsAndMethods {}
|
|
6
|
+
|
|
7
|
+
interface DribbleLineTrait extends InheritedPropsAndMethods {
|
|
8
|
+
convertLinePartsToDribble: (lineParts: LinePart[]) => LinePartAdjusted[];
|
|
9
|
+
drawArrowLineCap: () => void;
|
|
10
|
+
drawPerpendicularLineCap: () => void;
|
|
11
|
+
drawPerpendicularLineAtCourtPoint: (point: CourtPoint, angle: number) => void;
|
|
12
|
+
handOffLineCap: (baseLineParts: LinePart[]) => void;
|
|
13
|
+
calcMidPoint: (point1: CourtPoint, point2: CourtPoint) => CourtPoint;
|
|
14
|
+
angleBetweenLastTwoPoints: () => number;
|
|
15
|
+
angleBetweenTwoPoints: (cpFrom: CourtPoint, cpTo: CourtPoint) => number;
|
|
16
|
+
arrowTipPoint: () => CourtPoint;
|
|
17
|
+
setLineOptions: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
5
20
|
export default {
|
|
6
21
|
convertLinePartsToDribble(lineParts: LinePart[]): LinePartAdjusted[] {
|
|
7
22
|
const processedLineParts: LinePartAdjusted[] = [];
|
|
@@ -56,4 +71,4 @@ export default {
|
|
|
56
71
|
|
|
57
72
|
return processedLineParts;
|
|
58
73
|
}
|
|
59
|
-
};
|
|
74
|
+
} as DribbleLineTrait;
|
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
import { CourtPoint, LinePart } from '../types';
|
|
2
|
-
import { LinePartAdjusted } from '../models/LineModel';
|
|
2
|
+
import LineModel, { LinePartAdjusted } from '../models/LineModel';
|
|
3
|
+
import ShapeModel from '../models/ShapeModel';
|
|
4
|
+
|
|
5
|
+
interface InheritedPropsAndMethods {
|
|
6
|
+
ctx: CanvasRenderingContext2D;
|
|
7
|
+
getProcessedLinePaths: () => LinePartAdjusted[];
|
|
8
|
+
setColor: (alpha?: number) => void;
|
|
9
|
+
line?: LineModel;
|
|
10
|
+
shape?: ShapeModel;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface LineDrawOperationsTrait extends InheritedPropsAndMethods {
|
|
14
|
+
drawLineFromControlPoints: () => void;
|
|
15
|
+
drawArrowLineCap: () => void;
|
|
16
|
+
drawPerpendicularLineCap: () => void;
|
|
17
|
+
drawPerpendicularLineAtCourtPoint: (point: CourtPoint, angle: number) => void;
|
|
18
|
+
handOffLineCap: (baseLineParts: LinePart[]) => void;
|
|
19
|
+
calcMidPoint: (point1: CourtPoint, point2: CourtPoint) => CourtPoint;
|
|
20
|
+
angleBetweenLastTwoPoints: () => number;
|
|
21
|
+
angleBetweenTwoPoints: (cpFrom: CourtPoint, cpTo: CourtPoint) => number;
|
|
22
|
+
arrowTipPoint: () => CourtPoint;
|
|
23
|
+
setLineOptions: () => void;
|
|
24
|
+
}
|
|
3
25
|
|
|
4
26
|
export default {
|
|
5
27
|
drawLineFromControlPoints() {
|
|
@@ -9,7 +31,7 @@ export default {
|
|
|
9
31
|
|
|
10
32
|
this.ctx.lineJoin = 'round';
|
|
11
33
|
|
|
12
|
-
|
|
34
|
+
this.getProcessedLinePaths().forEach(linePart => {
|
|
13
35
|
this.ctx.save();
|
|
14
36
|
|
|
15
37
|
if (linePart.alpha) {
|
|
@@ -23,7 +45,7 @@ export default {
|
|
|
23
45
|
|
|
24
46
|
if (cp.length === 2) {
|
|
25
47
|
// TODO refactor next line to avoid access to line/shape model
|
|
26
|
-
if ((this.line || this.shape).type === 'DRIBBLE') this.ctx.lineCap = 'round'; // fix last straight line cap segment
|
|
48
|
+
if ((this.line || this.shape || {}).type === 'DRIBBLE') this.ctx.lineCap = 'round'; // fix last straight line cap segment
|
|
27
49
|
this.ctx.lineTo(cp[1].x, cp[1].y);
|
|
28
50
|
}
|
|
29
51
|
if (cp.length === 3) {
|
|
@@ -41,7 +63,9 @@ export default {
|
|
|
41
63
|
this.ctx.restore();
|
|
42
64
|
},
|
|
43
65
|
|
|
44
|
-
drawArrowLineCap(
|
|
66
|
+
drawArrowLineCap(point?: CourtPoint) {
|
|
67
|
+
const arrowTipPoint = point || this.arrowTipPoint();
|
|
68
|
+
|
|
45
69
|
this.ctx.save();
|
|
46
70
|
|
|
47
71
|
const arrowTipLength = 1.4;
|
|
@@ -135,7 +159,7 @@ export default {
|
|
|
135
159
|
|
|
136
160
|
angleBetweenLastTwoPoints() {
|
|
137
161
|
const lastLinePart = [...this.getProcessedLinePaths()].pop();
|
|
138
|
-
const cp = lastLinePart
|
|
162
|
+
const cp = lastLinePart!.controlPoints;
|
|
139
163
|
|
|
140
164
|
const cpTo = cp[cp.length - 1];
|
|
141
165
|
const cpFrom = cp[cp.length - 2];
|
|
@@ -150,7 +174,7 @@ export default {
|
|
|
150
174
|
},
|
|
151
175
|
|
|
152
176
|
arrowTipPoint() {
|
|
153
|
-
const lastLinePart
|
|
177
|
+
const lastLinePart = [...this.getProcessedLinePaths()].pop() as LinePartAdjusted;
|
|
154
178
|
const cp = lastLinePart.controlPoints;
|
|
155
179
|
return cp[cp.length - 1];
|
|
156
180
|
},
|
|
@@ -158,4 +182,4 @@ export default {
|
|
|
158
182
|
setLineOptions() {
|
|
159
183
|
// Override this method in a subclass
|
|
160
184
|
}
|
|
161
|
-
};
|
|
185
|
+
} as LineDrawOperationsTrait;
|
package/src/types/index.ts
CHANGED