@luceosports/play-rendering 1.11.1 → 1.11.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luceosports/play-rendering",
3
- "version": "1.11.1",
3
+ "version": "1.11.2",
4
4
  "description": "",
5
5
  "main": "dist/play-rendering.js",
6
6
  "scripts": {
@@ -1,40 +1,7 @@
1
1
  const BaseLayer = require('./base/BaseLayer');
2
-
3
- const BOX_MAX_WIDTH = 250;
4
- const FONT_SIZE = 14;
5
- const LINE_HEIGHT_OFFSET = 4;
6
- const BOX_PADDING = 6;
7
- const PLAYER_TOKEN_RADIUS = 10;
2
+ const Note = require('../models/Note');
8
3
 
9
4
  class NoteLayer extends BaseLayer {
10
- get boxMaxWidth() {
11
- return this.scale(BOX_MAX_WIDTH);
12
- }
13
-
14
- get boxPadding() {
15
- return this.scale(BOX_PADDING);
16
- }
17
-
18
- get fontSize() {
19
- return this.scale(FONT_SIZE);
20
- }
21
-
22
- get spaceWidth() {
23
- return this.ctx.measureText(' ').width;
24
- }
25
-
26
- get lineHeight() {
27
- return this.scale(FONT_SIZE + LINE_HEIGHT_OFFSET);
28
- }
29
-
30
- get lineHeightOffset() {
31
- return this.scale(LINE_HEIGHT_OFFSET);
32
- }
33
-
34
- get playerTokenRadius() {
35
- return this.scale(PLAYER_TOKEN_RADIUS);
36
- }
37
-
38
5
  apply() {
39
6
  this.ctx.save();
40
7
 
@@ -43,14 +10,18 @@ class NoteLayer extends BaseLayer {
43
10
  this.setColor();
44
11
 
45
12
  this.playData.notes.forEach(note => {
13
+ this.ctx.save();
14
+
46
15
  const { location } = note;
47
16
  this.ctx.translate(location.x, location.y);
48
17
 
49
- this.setFont(note);
18
+ const { lines, box } = note.preProcessedTextForContext();
50
19
 
51
- const { lines, box } = this.preProcessText(note);
20
+ this.setFont(note);
52
21
  this.drawBubble(box);
53
22
  this.drawText(lines);
23
+
24
+ this.ctx.restore();
54
25
  });
55
26
 
56
27
  this.ctx.restore();
@@ -61,59 +32,9 @@ class NoteLayer extends BaseLayer {
61
32
  this.ctx.strokeStyle = `rgba(0, 133, 133, 1)`;
62
33
  }
63
34
 
64
- setFont() {
65
- this.ctx.font = `${this.fontSize}px Arial`;
66
- this.ctx.textBaseline = 'top';
67
- }
68
-
69
- preProcessText(note) {
70
- const fitWidth = this.boxMaxWidth - this.boxPadding * 2;
71
- const lines = this.getLines(note.text, fitWidth);
72
- const maxLineWidth = lines.reduce((a, b) => (a.width > b.width ? a : b)).width;
73
- return {
74
- lines,
75
- box: {
76
- width: maxLineWidth + this.boxPadding * 2,
77
- height: lines.length * this.lineHeight + this.boxPadding * 2 - this.lineHeightOffset
78
- }
79
- };
80
- }
81
-
82
- getLines(text, fitWidth) {
83
- const getLineInfo = inputWords => {
84
- let lineWidth = 0;
85
- const wordsResult = [];
86
- inputWords.forEach((w, i) => {
87
- const matches = [...w.matchAll(/(<player=(\d*)>)/gi)];
88
- const wordProcessed = {
89
- text: w,
90
- width: matches.length
91
- ? this.ctx.measureText(w.replace(/<player=(\d*)>/, '')).width + this.playerTokenRadius * 2
92
- : this.ctx.measureText(w).width
93
- };
94
- lineWidth += wordProcessed.width;
95
- if (i !== inputWords.length - 1) {
96
- lineWidth += this.spaceWidth;
97
- }
98
- wordsResult.push(wordProcessed);
99
- });
100
- return { words: wordsResult, width: lineWidth };
101
- };
102
-
103
- const lines = [];
104
- const words = text.split(' ');
105
- let wordIndex = 1;
106
- while (words.length > 0) {
107
- const wordsForIndex = words.slice(0, wordIndex);
108
- if (getLineInfo(wordsForIndex).width < fitWidth && wordIndex <= words.length) {
109
- wordIndex++;
110
- continue;
111
- }
112
- if (wordIndex !== 1) wordIndex--;
113
- lines.push(getLineInfo(words.splice(0, wordIndex)));
114
- wordIndex = 1;
115
- }
116
- return lines;
35
+ setFont(note) {
36
+ this.ctx.font = note.font;
37
+ this.ctx.textBaseline = note.textBaseline;
117
38
  }
118
39
 
119
40
  drawBubble({ width, height }) {
@@ -127,7 +48,7 @@ class NoteLayer extends BaseLayer {
127
48
  lines.forEach(({ words }, index) => {
128
49
  this.ctx.save();
129
50
 
130
- this.ctx.translate(this.boxPadding, this.boxPadding + this.lineHeight * index);
51
+ this.ctx.translate(Note.NOTE_WRAP_PADDING, Note.NOTE_WRAP_PADDING + Note.NOTE_LINE_HEIGHT * index);
131
52
 
132
53
  let wordStartX = 0;
133
54
  words.forEach((word, i) => {
@@ -135,7 +56,7 @@ class NoteLayer extends BaseLayer {
135
56
 
136
57
  if (matches.length && matches[0].index === 0) {
137
58
  this.drawPlayerToken(wordStartX, matches[0][2]);
138
- wordStartX += this.playerTokenRadius * 2;
59
+ wordStartX += Note.NOTE_PLAYER_TOKEN_RADIUS * 2;
139
60
  }
140
61
 
141
62
  this.ctx.fillStyle = '#000';
@@ -143,17 +64,17 @@ class NoteLayer extends BaseLayer {
143
64
  this.ctx.fillText(wordText, wordStartX, 0);
144
65
  wordStartX += word.width;
145
66
  if (matches.length) {
146
- wordStartX -= this.playerTokenRadius * 2;
67
+ wordStartX -= Note.NOTE_PLAYER_TOKEN_RADIUS * 2;
147
68
  }
148
69
 
149
70
  if (matches.length && matches[0].index > 0) {
150
71
  this.drawPlayerToken(wordStartX, matches[0][2]);
151
- wordStartX += this.playerTokenRadius * 2;
72
+ wordStartX += Note.NOTE_PLAYER_TOKEN_RADIUS * 2;
152
73
  }
153
74
 
154
75
  if (i !== words.length - 1) {
155
76
  this.ctx.fillText(' ', wordStartX, 0);
156
- wordStartX += this.spaceWidth;
77
+ wordStartX += this.ctx.measureText(' ').width;
157
78
  }
158
79
  });
159
80
 
@@ -164,12 +85,12 @@ class NoteLayer extends BaseLayer {
164
85
  drawPlayerToken(x, label) {
165
86
  this.ctx.save();
166
87
 
167
- this.ctx.translate(x + this.playerTokenRadius, 0);
88
+ this.ctx.translate(x + Note.NOTE_PLAYER_TOKEN_RADIUS, 0);
168
89
 
169
90
  this.ctx.beginPath();
170
91
  this.ctx.strokeStyle = '#ffffff';
171
92
  this.ctx.fillStyle = '#f36f21';
172
- this.ctx.arc(0, this.fontSize / 2, this.playerTokenRadius, 0, Math.PI * 2);
93
+ this.ctx.arc(0, Note.NOTE_FONT_SIZE / 2, Note.NOTE_PLAYER_TOKEN_RADIUS, 0, Math.PI * 2);
173
94
  this.ctx.fill();
174
95
  this.ctx.stroke();
175
96
 
@@ -240,7 +240,6 @@ class Frame {
240
240
  this.ctx.canvas.height = this.frameHeight;
241
241
 
242
242
  this.ctx.scale(this.play.scale, this.play.scale);
243
- // this.ctx.translate(Math.abs(this.court.courtRect.origin.x), Math.abs(this.court.courtRect.origin.y));
244
243
  this.ctx.translate(-this.court.courtRect.origin.x, -this.court.courtRect.origin.y);
245
244
  if (this.play.options.mirror) {
246
245
  this.ctx.scale(-1, 1);
@@ -276,6 +275,9 @@ class Frame {
276
275
  if (objectType === 'shape') {
277
276
  return this.closestShapeToPoint(courtPoint, filterCallback);
278
277
  }
278
+ if (objectType === 'note') {
279
+ return this.closestNoteToPoint(courtPoint, filterCallback);
280
+ }
279
281
  if (objectType === 'line') {
280
282
  return this.closestLineToPoint(courtPoint, filterCallback);
281
283
  }
@@ -369,6 +371,21 @@ class Frame {
369
371
  return _.minBy(shapeDistances, 'distance');
370
372
  }
371
373
 
374
+ closestNoteToPoint(courtPoint) {
375
+ const noteDistances = this.notes
376
+ .filter(note => note.noteWrapperContains(courtPoint))
377
+ .reduce((result, item) => {
378
+ const noteLocation = this.prepareCourtPoint(item.location);
379
+ result.push({
380
+ object: item,
381
+ objectPoint: noteLocation,
382
+ distance: distanceBetweenPoints(noteLocation, courtPoint)
383
+ });
384
+ return result;
385
+ }, []);
386
+ return noteDistances.length ? _.minBy(noteDistances, 'distance') : null;
387
+ }
388
+
372
389
  prepareCourtPoint(courtPoint) {
373
390
  const { mirror } = this.play.options;
374
391
  const { width } = this.play.playData.court.courtRect.size;
@@ -1,12 +1,37 @@
1
1
  const Model = require('./Base/InternalFrameModel');
2
2
 
3
3
  class Note extends Model {
4
- get id() {
5
- return this._getAttr('id');
4
+ constructor(data) {
5
+ super(data);
6
+ this.internalCanvas = document.createElement('canvas');
7
+ }
8
+
9
+ static get NOTE_WRAP_MAX_WIDTH() {
10
+ return 18;
11
+ }
12
+
13
+ static get NOTE_WRAP_PADDING() {
14
+ return 0.43;
15
+ }
16
+
17
+ static get NOTE_PLAYER_TOKEN_RADIUS() {
18
+ return 0.7;
19
+ }
20
+
21
+ static get NOTE_LINE_HEIGHT_OFFSET() {
22
+ return 0.3;
23
+ }
24
+
25
+ static get NOTE_FONT_SIZE() {
26
+ return 1;
6
27
  }
7
28
 
8
- get headerText() {
9
- return this._getAttr('headerText');
29
+ static get NOTE_LINE_HEIGHT() {
30
+ return Note.NOTE_FONT_SIZE + Note.NOTE_LINE_HEIGHT_OFFSET;
31
+ }
32
+
33
+ get id() {
34
+ return this._getAttr('id');
10
35
  }
11
36
 
12
37
  get text() {
@@ -16,6 +41,105 @@ class Note extends Model {
16
41
  get location() {
17
42
  return this._getAttr('location');
18
43
  }
44
+
45
+ get font() {
46
+ return `${Note.NOTE_FONT_SIZE}px Arial`;
47
+ }
48
+
49
+ get textBaseline() {
50
+ return 'top';
51
+ }
52
+
53
+ get internalCtx() {
54
+ // used only for measureText feature to get a text width
55
+ return this.internalCanvas.getContext('2d');
56
+ }
57
+
58
+ preProcessedTextForContext() {
59
+ const lines = this._getLines();
60
+ const maxLineWidth = lines.reduce((a, b) => (a.width > b.width ? a : b)).width;
61
+ return {
62
+ lines,
63
+ box: {
64
+ width: maxLineWidth + Note.NOTE_WRAP_PADDING * 2,
65
+ height: lines.length * Note.NOTE_LINE_HEIGHT + Note.NOTE_WRAP_PADDING * 2 - Note.NOTE_LINE_HEIGHT_OFFSET
66
+ }
67
+ };
68
+ }
69
+
70
+ _getLines() {
71
+ this.internalCtx.font = this.font;
72
+ this.internalCtx.textBaseline = this.textBaseline;
73
+
74
+ const fitWidth = Note.NOTE_WRAP_MAX_WIDTH - Note.NOTE_WRAP_PADDING * 2;
75
+
76
+ const lines = [];
77
+ const words = this.text.split(' ').reduce((result, item) => {
78
+ if (!item.match(/\n/)) {
79
+ result.push(item);
80
+ return result;
81
+ }
82
+ const nlSegments = item.split(/\n/);
83
+ nlSegments.forEach((segment, index) => {
84
+ if (!segment) return result.push('\n');
85
+ result.push(segment);
86
+ if (nlSegments[index + 1]) result.push('\n');
87
+ });
88
+ return result;
89
+ }, []);
90
+
91
+ let wordIndex = 1;
92
+ while (words.length > 0) {
93
+ const wordsForIndex = words.slice(0, wordIndex);
94
+ const lineInfo = this._getLineInfo(wordsForIndex);
95
+ if (lineInfo.width < fitWidth && wordIndex <= words.length && !lineInfo.hasLineBreak) {
96
+ wordIndex++;
97
+ continue;
98
+ }
99
+ if (wordIndex !== 1) wordIndex--;
100
+ lines.push(this._getLineInfo(words.splice(0, wordIndex)));
101
+ wordIndex = 1;
102
+ if (lineInfo.hasLineBreak) {
103
+ words.splice(0, 1);
104
+ }
105
+ }
106
+ return lines;
107
+ }
108
+
109
+ _getLineInfo(inputWords) {
110
+ let lineWidth = 0;
111
+ let hasLineBreak = false;
112
+ const wordsResult = [];
113
+ inputWords.forEach((w, i) => {
114
+ if (w === '\n') {
115
+ hasLineBreak = true;
116
+ return;
117
+ }
118
+ const matches = [...w.matchAll(/(<player=(\d*)>)/gi)];
119
+ const wordProcessed = {
120
+ text: w,
121
+ width: matches.length
122
+ ? this.internalCtx.measureText(w.replace(/<player=(\d*)>/, '')).width + Note.NOTE_PLAYER_TOKEN_RADIUS * 2
123
+ : this.internalCtx.measureText(w).width
124
+ };
125
+ lineWidth += wordProcessed.width;
126
+ if (i !== inputWords.length - 1) {
127
+ lineWidth += this.internalCtx.measureText(' ').width;
128
+ }
129
+ wordsResult.push(wordProcessed);
130
+ });
131
+ return { words: wordsResult, width: lineWidth, hasLineBreak };
132
+ }
133
+
134
+ noteWrapperContains({ x, y }) {
135
+ const { box } = this.preProcessedTextForContext();
136
+
137
+ const topLeft = this.location;
138
+ const topRight = { x: topLeft.x + box.width, y: topLeft.y };
139
+ const botRight = { x: topRight.x, y: topRight.y + box.height };
140
+
141
+ return topLeft.x <= x && x <= topRight.x && topLeft.y <= y && y <= botRight.y;
142
+ }
19
143
  }
20
144
 
21
145
  module.exports = Note;
@@ -17,6 +17,7 @@ class Play {
17
17
  linesHiddenIds = [],
18
18
  linesSelectedIds = [],
19
19
  shapeSelectedId = null,
20
+ noteSelectedId = null,
20
21
  playersHiddenPositions = [],
21
22
  background = '',
22
23
  watermark = null,
@@ -37,6 +38,7 @@ class Play {
37
38
  linesHiddenIds,
38
39
  linesSelectedIds,
39
40
  shapeSelectedId,
41
+ noteSelectedId,
40
42
  playersHiddenPositions,
41
43
  background,
42
44
  watermark,