@luceosports/play-rendering 2.5.5 → 2.5.7

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.
Files changed (39) hide show
  1. package/dist/play-rendering.js +23 -23
  2. package/dist/play-rendering.js.map +1 -1
  3. package/dist/types/constants.d.ts +3 -0
  4. package/dist/types/models/AnimationModel.d.ts +1 -0
  5. package/dist/types/models/LineModel.d.ts +1 -0
  6. package/dist/types/models/PlayModel.d.ts +4 -0
  7. package/dist/types/types/index.d.ts +3 -2
  8. package/package.json +1 -1
  9. package/src/assets/balls/baseball.svg +2 -0
  10. package/src/assets/balls/basketball.svg +2 -0
  11. package/src/assets/balls/football.svg +2 -0
  12. package/src/assets/balls/hockey.svg +13 -0
  13. package/src/assets/balls/lacrosse.svg +21 -0
  14. package/src/assets/balls/soccer.svg +34 -0
  15. package/src/assets/balls/volleyball.svg +37 -0
  16. package/src/assets/sand_bg.png +0 -0
  17. package/src/ballConfig.ts +46 -0
  18. package/src/constants.ts +4 -0
  19. package/src/helpers/draw.ts +48 -0
  20. package/src/layers/LineLayer.ts +1 -0
  21. package/src/layers/PlayerLayer.ts +27 -1
  22. package/src/layers/court/index.ts +278 -261
  23. package/src/layers/court/layers/BEACH_VOLLEYBALL/constants.ts +5 -0
  24. package/src/layers/court/layers/BEACH_VOLLEYBALL/courtTypes/BEACH_VOLLEYBALL_NCAA/constants.ts +2 -0
  25. package/src/layers/court/layers/BEACH_VOLLEYBALL/courtTypes/BEACH_VOLLEYBALL_RECREATIONAL/constants.ts +2 -0
  26. package/src/layers/court/layers/BEACH_VOLLEYBALL/layers/BorderRectLayer.ts +10 -0
  27. package/src/layers/court/layers/BEACH_VOLLEYBALL/layers/CenterLineLayer.ts +20 -0
  28. package/src/layers/court/layers/BEACH_VOLLEYBALL/layers/HashMarkLayer.ts +24 -0
  29. package/src/layers/court/layers/BEACH_VOLLEYBALL/layers/index.ts +5 -0
  30. package/src/layers/line/base/InternalLineLayer.ts +17 -2
  31. package/src/layers/line/layers/DribbleLineLayer.ts +13 -3
  32. package/src/layers/line/layers/ShotLineLayer.ts +13 -2
  33. package/src/models/AnimationModel.ts +19 -0
  34. package/src/models/FrameModel.ts +15 -3
  35. package/src/models/LineModel.ts +12 -1
  36. package/src/models/Play/Options.ts +4 -0
  37. package/src/models/PlayModel.ts +13 -0
  38. package/src/traits/LineDrawOperationsTrait.ts +58 -3
  39. package/src/types/index.ts +4 -0
@@ -2,7 +2,7 @@ import InternalBaseLayer from '../../base/InternalBaseLayer';
2
2
  import { adjustedBezierCurveWithExclusionZones } from '../../../math/LineDrawingMath';
3
3
  import LineDrawOperationsTrait from '../../../traits/LineDrawOperationsTrait';
4
4
  import LineLayer from '../../LineLayer';
5
- import LineModel from '../../../models/LineModel';
5
+ import LineModel, { LinePartAdjusted } from '../../../models/LineModel';
6
6
  import { CourtPoint, LinePart } from '../../../types';
7
7
 
8
8
  export type MaskSettings = {
@@ -19,6 +19,13 @@ export default class InternalLineLayer extends InternalBaseLayer {
19
19
  protected angleBetweenLastTwoPoints(): number {
20
20
  return 0;
21
21
  }
22
+ maybeDrawBallAtStartPoint(params: {
23
+ linePart: Pick<LinePartAdjusted, 'animationSegment'>;
24
+ controlPoints: CourtPoint[];
25
+ alreadyDrawn: boolean;
26
+ }): boolean {
27
+ return false;
28
+ }
22
29
  // ==============================================================
23
30
 
24
31
  protected lineWidth: number;
@@ -62,7 +69,15 @@ export default class InternalLineLayer extends InternalBaseLayer {
62
69
  const g = lineForPlayerInPosition ? 0 : Math.ceil(green * 255);
63
70
  const b = lineForPlayerInPosition ? 255 : Math.ceil(blue * 255);
64
71
 
65
- const color = `rgba(${r}, ${g}, ${b}, ${alphaOverride || alpha})`;
72
+ const hidePassLikeLinesDuringPlayback =
73
+ !!this.options.showBallMode &&
74
+ !!this.options.animationGlobalProgress &&
75
+ this.options.showPassLinesDuringPlayback === false &&
76
+ this.line.isBallTransferLine;
77
+
78
+ const effectiveAlpha = hidePassLikeLinesDuringPlayback ? 0 : alphaOverride ?? alpha;
79
+
80
+ const color = `rgba(${r}, ${g}, ${b}, ${effectiveAlpha})`;
66
81
 
67
82
  this.ctx.fillStyle = color;
68
83
  this.ctx.strokeStyle = color;
@@ -19,20 +19,30 @@ export default class DribbleLineLayer extends ActionLineLayer {
19
19
  setLineOptions() {
20
20
  const lineParts = [...this.line.getLineParts()];
21
21
  const dribbleLineParts = this.convertLinePartsToDribble(lineParts);
22
+
22
23
  this.line.setLinePartsAdjusted([]);
24
+
23
25
  dribbleLineParts.forEach(lp => {
24
26
  const { controlPoints, lpIndex } = lp;
25
27
  const [firstPoint] = controlPoints;
26
- let alpha = lineParts[lpIndex!].alpha || this.line.color.alpha;
28
+
29
+ let animationSegment: LinePartAdjusted['animationSegment'] | undefined = undefined;
30
+
27
31
  if (this.options.animationGlobalProgress) {
28
32
  const [start, end] = this.line.animationKeyTimeChunks[lpIndex!];
33
+
29
34
  if (_.inRange(this.options.animationGlobalProgress, start, end)) {
30
35
  if (animationProgress(this.options.animationGlobalProgress, start, end) > firstPoint.time!) {
31
- alpha = 0.1;
36
+ animationSegment = 'processed';
37
+ } else {
38
+ animationSegment = 'active';
32
39
  }
40
+ } else if (this.options.animationGlobalProgress > end) {
41
+ animationSegment = 'processed';
33
42
  }
34
43
  }
35
- this.line.addLinePartAdjusted({ ...lp, controlPoints, alpha });
44
+
45
+ this.line.addLinePartAdjusted({ ...lp, controlPoints, animationSegment });
36
46
  });
37
47
  }
38
48
  }
@@ -32,11 +32,13 @@ export default class ShotLineLayer extends ActionLineLayer {
32
32
 
33
33
  this.ctx.lineJoin = 'round';
34
34
 
35
+ let isBallDrawn = false;
36
+
35
37
  this.getProcessedLinePaths().forEach(linePart => {
36
38
  this.ctx.save();
37
39
 
38
- if (linePart.alpha) {
39
- this.setColor(linePart.alpha); // setting color for each line path (animation)
40
+ if (linePart.animationSegment === 'processed') {
41
+ this.setColor(0.1); // processed segment (before animation progress)
40
42
  }
41
43
 
42
44
  this.ctx.beginPath();
@@ -78,6 +80,15 @@ export default class ShotLineLayer extends ActionLineLayer {
78
80
  this.ctx.stroke();
79
81
  });
80
82
 
83
+ // Draw the ball AFTER the stroke so it overlaps the line.
84
+ if (!isBallDrawn) {
85
+ isBallDrawn = this.maybeDrawBallAtStartPoint({
86
+ linePart,
87
+ controlPoints: cp,
88
+ alreadyDrawn: isBallDrawn
89
+ });
90
+ }
91
+
81
92
  this.ctx.restore();
82
93
  });
83
94
 
@@ -30,6 +30,25 @@ export default class AnimationModel {
30
30
  this.timeElapsedSaved = 0;
31
31
  }
32
32
 
33
+ /**
34
+ * Extend current options (do not replace object) – same semantics as PlayModel.setOptions().
35
+ * This is needed because AnimationModel keeps an internal cloned PlayModel instance.
36
+ */
37
+ setOptions(options: Partial<PlayModel['options']>) {
38
+ this.play.options = { ...this.play.options, ...options };
39
+ this.playBase.options = { ...this.playBase.options, ...options };
40
+
41
+ // Recreate frame so any layers/models that cached options are refreshed immediately.
42
+ this.animationFrame = new FrameModel(this.play, this.currentPlayPhase, this.globalProgress).setContext(this.ctx);
43
+
44
+ // If paused/stopped, force immediate visual update.
45
+ if (!this.running) {
46
+ this.animationFrame.setPhase(this.currentPlayPhase).setAnimationGlobalProgress(this.globalProgress).draw();
47
+ }
48
+
49
+ return this;
50
+ }
51
+
33
52
  get animationDuration() {
34
53
  return this.play.playData.animationDuration / this.play.options.speed;
35
54
  }
@@ -252,14 +252,26 @@ export default class FrameModel {
252
252
  if (line.type === 'DRIBBLE') {
253
253
  return linePartsAdjusted.push({ ...line.getLineParts()[index] });
254
254
  }
255
+
255
256
  const linePathSplitted = splitBezierCurveAtTVal(
256
257
  line.getLineParts()[index].controlPoints,
257
258
  this.animationProgress(start, end)
258
259
  ) as LinePartAdjusted['controlPoints'][];
259
- linePartsAdjusted.push({ controlPoints: linePathSplitted[0], alpha: 0.1 });
260
- linePartsAdjusted.push({ controlPoints: linePathSplitted[1], alpha: line.color.alpha });
260
+
261
+ linePartsAdjusted.push({
262
+ controlPoints: linePathSplitted[0],
263
+ animationSegment: 'processed'
264
+ });
265
+
266
+ linePartsAdjusted.push({
267
+ controlPoints: linePathSplitted[1],
268
+ animationSegment: 'active'
269
+ });
261
270
  } else if (this.animationGlobalProgress > end) {
262
- linePartsAdjusted.push({ ...line.getLineParts()[index], alpha: 0.1 });
271
+ linePartsAdjusted.push({
272
+ ...line.getLineParts()[index],
273
+ animationSegment: 'processed'
274
+ });
263
275
  } else {
264
276
  linePartsAdjusted.push({ ...line.getLineParts()[index] });
265
277
  }
@@ -9,7 +9,14 @@ export type LinePartAdjusted = {
9
9
  | [CourtPointAdjusted, CourtPointAdjusted]
10
10
  | [CourtPointAdjusted, CourtPointAdjusted, CourtPointAdjusted]
11
11
  | [CourtPointAdjusted, CourtPointAdjusted, CourtPointAdjusted, CourtPointAdjusted];
12
- alpha?: number;
12
+
13
+ /**
14
+ * Indicates how this segment should be rendered during animation.
15
+ * - 'processed': segment is before current animation progress (dimmed)
16
+ * - 'active': segment is the currently "drawn" / normal colored segment
17
+ */
18
+ animationSegment?: 'processed' | 'active';
19
+
13
20
  lpIndex?: number;
14
21
  };
15
22
 
@@ -97,6 +104,10 @@ export default class LineModel extends Model<LineData, LineAdjusted> {
97
104
  return this._getAttr('type');
98
105
  }
99
106
 
107
+ get isBallTransferLine() {
108
+ return ['PASS', 'HANDOFF', 'SHOT'].includes(this.type);
109
+ }
110
+
100
111
  get phase() {
101
112
  return this._getAttr('phase');
102
113
  }
@@ -22,6 +22,10 @@ export function useDefaults(options?: Partial<PlayModelOptions>): PlayModelOptio
22
22
  flipPlayerLabels: false,
23
23
  legacyPrintStyle: false,
24
24
  playerTokenScale: 1,
25
+ showBallMode: false,
26
+ // showBallMode sub-options (defaults match current behavior)
27
+ highlightPlayerPuck: true,
28
+ showPassLinesDuringPlayback: true,
25
29
  // TODO: refactor NBA court type constants below
26
30
  showHalfCourtCircle: true,
27
31
  playersMap: [],
@@ -1,11 +1,13 @@
1
1
  import _ from 'lodash';
2
2
  import playerHatsConfig from '../playerHatsConfig';
3
3
  import shapesConfig from '../shapesConfig';
4
+ import ballConfig from '../ballConfig';
4
5
  import { useDefaults } from './Play/Options';
5
6
 
6
7
  import {
7
8
  SPORT_TYPE_BASEBALL,
8
9
  SPORT_TYPE_BASKETBALL,
10
+ SPORT_TYPE_BEACH_VOLLEYBALL,
9
11
  SPORT_TYPE_FOOTBALL,
10
12
  SPORT_TYPE_HOCKEY,
11
13
  SPORT_TYPE_LACROSSE,
@@ -21,6 +23,7 @@ import hardwoodImageData from '../assets/wood_bg.png';
21
23
  import grassImageData from '../assets/grass_bg.png';
22
24
  import iceImageData from '../assets/ice_bg.png';
23
25
  import concreteImageData from '../assets/concrete_bg.png';
26
+ import sandImageData from '../assets/sand_bg.png';
24
27
  import { loadImage } from '../helpers/common';
25
28
 
26
29
  const STORAGE_URL = 'https://playbooksstore.blob.core.windows.net/public';
@@ -44,6 +47,7 @@ export type PlayStaticData = {
44
47
  watermark: typeof PlayModel.watermark;
45
48
  playerHats: readonly ImageConfigItem[];
46
49
  shapes: readonly ImageConfigItem[];
50
+ balls: readonly ImageConfigItem[];
47
51
  playerHeadshots: typeof PlayModel.playerHeadshots;
48
52
  teamPlayers: typeof PlayModel.teamPlayers;
49
53
  };
@@ -88,6 +92,10 @@ export type PlayModelOptions = {
88
92
  playersMap: PlayersMapItem[];
89
93
  labelsOverrideType: 'Initials' | 'Jersey number' | 'Headshot' | null;
90
94
  inDrawingState: boolean;
95
+ showBallMode: boolean;
96
+ // sub-options for showBallMode
97
+ highlightPlayerPuck: boolean;
98
+ showPassLinesDuringPlayback: boolean;
91
99
  };
92
100
 
93
101
  export default class PlayModel {
@@ -98,6 +106,7 @@ export default class PlayModel {
98
106
  public static playerHeadshots: PlayerHeadshotItem[] = [];
99
107
  public static playerHats: readonly ImageConfigItem[];
100
108
  public static shapes: readonly ImageConfigItem[];
109
+ public static balls: readonly ImageConfigItem[];
101
110
  public static watermark: { LuceoSports: HTMLImageElement; TeamLogo: HTMLImageElement | null };
102
111
  public static backgroundOptions: Record<SportType, HTMLImageElement> & {
103
112
  Hardwood: HTMLImageElement;
@@ -113,17 +122,20 @@ export default class PlayModel {
113
122
  static async init({ teamLogoPath = '' } = {}) {
114
123
  PlayModel.playerHats = await playerHatsConfig();
115
124
  PlayModel.shapes = await shapesConfig();
125
+ PlayModel.balls = await ballConfig();
116
126
 
117
127
  const hardwoodImage = await loadImage(hardwoodImageData);
118
128
  const grassImage = await loadImage(grassImageData);
119
129
  const iceImage = await loadImage(iceImageData);
120
130
  const concreteImage = await loadImage(concreteImageData);
131
+ const sandImage = await loadImage(sandImageData);
121
132
 
122
133
  PlayModel.backgroundOptions = {
123
134
  Hardwood: hardwoodImage,
124
135
  Concrete: concreteImage,
125
136
  [SPORT_TYPE_BASKETBALL]: hardwoodImage,
126
137
  [SPORT_TYPE_VOLLEYBALL]: hardwoodImage,
138
+ [SPORT_TYPE_BEACH_VOLLEYBALL]: sandImage,
127
139
  [SPORT_TYPE_FOOTBALL]: grassImage,
128
140
  [SPORT_TYPE_LACROSSE]: grassImage,
129
141
  [SPORT_TYPE_LACROSSE_BOX]: grassImage,
@@ -190,6 +202,7 @@ export default class PlayModel {
190
202
  watermark: PlayModel.watermark,
191
203
  playerHats: PlayModel.playerHats,
192
204
  shapes: PlayModel.shapes,
205
+ balls: PlayModel.balls,
193
206
  playerHeadshots: PlayModel.playerHeadshots,
194
207
  teamPlayers: PlayModel.teamPlayers
195
208
  };
@@ -1,6 +1,8 @@
1
- import { CourtPoint, LinePart } from '../types';
1
+ import { CourtPoint, LinePart, PlayData } from '../types';
2
2
  import LineModel, { LinePartAdjusted } from '../models/LineModel';
3
3
  import ShapeModel from '../models/ShapeModel';
4
+ import { FrameDataOptions } from '../models/FrameModel';
5
+ import { drawBallObject } from '../helpers/draw';
4
6
 
5
7
  interface InheritedPropsAndMethods {
6
8
  ctx: CanvasRenderingContext2D;
@@ -8,6 +10,13 @@ interface InheritedPropsAndMethods {
8
10
  setColor: (alpha?: number) => void;
9
11
  line?: LineModel;
10
12
  shape?: ShapeModel;
13
+ // below present on layers using this trait (InternalBaseLayer)
14
+ options: FrameDataOptions;
15
+ playData: PlayData;
16
+ courtTypeConstants?: {
17
+ PLAYER_TOKEN_SCALE: number;
18
+ PLAYER_TOKEN_RADIUS: number;
19
+ };
11
20
  }
12
21
 
13
22
  interface LineDrawOperationsTrait extends InheritedPropsAndMethods {
@@ -21,9 +30,44 @@ interface LineDrawOperationsTrait extends InheritedPropsAndMethods {
21
30
  angleBetweenTwoPoints: (cpFrom: CourtPoint, cpTo: CourtPoint) => number;
22
31
  arrowTipPoint: () => CourtPoint;
23
32
  setLineOptions: () => void;
33
+ maybeDrawBallAtStartPoint: (params: {
34
+ linePart: Pick<LinePartAdjusted, 'animationSegment'>;
35
+ controlPoints: CourtPoint[];
36
+ alreadyDrawn: boolean;
37
+ }) => boolean;
24
38
  }
25
39
 
26
40
  export default {
41
+ maybeDrawBallAtStartPoint(params: {
42
+ linePart: Pick<LinePartAdjusted, 'animationSegment'>;
43
+ controlPoints: CourtPoint[];
44
+ alreadyDrawn: boolean;
45
+ }): boolean {
46
+ const { linePart, controlPoints, alreadyDrawn } = params;
47
+
48
+ if (!this.options?.showBallMode) return alreadyDrawn;
49
+ if (alreadyDrawn) return true;
50
+ if (linePart.animationSegment !== 'active') return false;
51
+ if (!this.line?.isBallTransferLine) return false;
52
+
53
+ const startPoint = controlPoints[0];
54
+ if (!startPoint || !this.courtTypeConstants) return false;
55
+
56
+ const playerScale = this.courtTypeConstants.PLAYER_TOKEN_SCALE * (this.options?.playerTokenScale ?? 1);
57
+ const puckRadiusBase = this.courtTypeConstants.PLAYER_TOKEN_RADIUS * playerScale;
58
+ const radiusMultiplier = this.options?.legacyPrintStyle ? 1.2 : 1;
59
+ const puckRadius = radiusMultiplier * puckRadiusBase;
60
+
61
+ return drawBallObject({
62
+ sport: this.playData.sport,
63
+ ctx: this.ctx,
64
+ staticData: this.options?.staticData,
65
+ center: { x: startPoint.x, y: startPoint.y },
66
+ puckRadius,
67
+ placement: 'center'
68
+ });
69
+ },
70
+
27
71
  drawLineFromControlPoints() {
28
72
  this.ctx.save();
29
73
 
@@ -31,11 +75,13 @@ export default {
31
75
 
32
76
  this.ctx.lineJoin = 'round';
33
77
 
78
+ let isBallDrawn = false;
79
+
34
80
  this.getProcessedLinePaths().forEach(linePart => {
35
81
  this.ctx.save();
36
82
 
37
- if (linePart.alpha) {
38
- this.setColor(linePart.alpha); // setting color for each line path (animation)
83
+ if (linePart.animationSegment === 'processed') {
84
+ this.setColor(0.1); // processed segment (before animation progress)
39
85
  }
40
86
 
41
87
  this.ctx.beginPath();
@@ -57,6 +103,15 @@ export default {
57
103
 
58
104
  this.ctx.stroke();
59
105
 
106
+ // Draw the ball AFTER the stroke so it overlaps the line.
107
+ if (!isBallDrawn) {
108
+ isBallDrawn = this.maybeDrawBallAtStartPoint({
109
+ linePart,
110
+ controlPoints: cp,
111
+ alreadyDrawn: isBallDrawn
112
+ });
113
+ }
114
+
60
115
  this.ctx.restore();
61
116
  });
62
117
 
@@ -18,6 +18,7 @@ export type SportType =
18
18
  | 'FOOTBALL'
19
19
  | 'BASKETBALL'
20
20
  | 'VOLLEYBALL'
21
+ | 'BEACH_VOLLEYBALL'
21
22
  | 'LACROSSE'
22
23
  | 'LACROSSE_BOX'
23
24
  | 'SOCCER'
@@ -37,6 +38,8 @@ export type CourtTypeSportBasketball =
37
38
 
38
39
  export type CourtTypeSportVolleyball = 'VOLLEYBALL_INDOOR';
39
40
 
41
+ export type CourtTypeSportBeachVolleyball = 'BEACH_VOLLEYBALL_NCAA' | 'BEACH_VOLLEYBALL_RECREATIONAL';
42
+
40
43
  export type CourtTypeSportSoccer =
41
44
  | 'SOCCER_FIFA'
42
45
  | 'SOCCER_NCAA'
@@ -62,6 +65,7 @@ export type CourtTypeSportFootballLegacy = 'FOOTBALL';
62
65
  export type CourtType =
63
66
  | CourtTypeSportBasketball
64
67
  | CourtTypeSportVolleyball
68
+ | CourtTypeSportBeachVolleyball
65
69
  | CourtTypeSportLacrosse
66
70
  | CourtTypeSportLacrosseBox
67
71
  | CourtTypeSportSoccer