@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/dist/play-rendering.js +3 -3
- package/dist/play-rendering.js.map +1 -1
- package/package.json +1 -1
- package/src/layers/NoteLayer.js +17 -96
- package/src/models/Frame.js +18 -1
- package/src/models/Note.js +128 -4
- package/src/models/Play.js +2 -0
package/package.json
CHANGED
package/src/layers/NoteLayer.js
CHANGED
|
@@ -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
|
-
|
|
18
|
+
const { lines, box } = note.preProcessedTextForContext();
|
|
50
19
|
|
|
51
|
-
|
|
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 =
|
|
66
|
-
this.ctx.textBaseline =
|
|
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(
|
|
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 +=
|
|
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 -=
|
|
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 +=
|
|
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.
|
|
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 +
|
|
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,
|
|
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
|
|
package/src/models/Frame.js
CHANGED
|
@@ -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;
|
package/src/models/Note.js
CHANGED
|
@@ -1,12 +1,37 @@
|
|
|
1
1
|
const Model = require('./Base/InternalFrameModel');
|
|
2
2
|
|
|
3
3
|
class Note extends Model {
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
9
|
-
return
|
|
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;
|
package/src/models/Play.js
CHANGED
|
@@ -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,
|