@xpadev-net/niconicomments 0.1.10 → 0.2.0

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 (3) hide show
  1. package/LICENSE +18 -4
  2. package/dist/bundle.js +1066 -1049
  3. package/package.json +2 -1
package/dist/bundle.js CHANGED
@@ -1,1058 +1,1075 @@
1
1
  /*!
2
- niconicomments.js v0.1.10
2
+ niconicomments.js v0.2.0
3
3
  (c) 2021 xpadev-net https://xpadev.net
4
4
  Released under the MIT License.
5
5
  */
6
6
  (function (global, factory) {
7
- typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
8
- typeof define === 'function' && define.amd ? define(factory) :
9
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.NiconiComments = factory());
7
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
8
+ typeof define === 'function' && define.amd ? define(factory) :
9
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.NiconiComments = factory());
10
10
  })(this, (function () { 'use strict';
11
11
 
12
- class NiconiComments {
13
- /**
14
- * NiconiComments Constructor
15
- * @param {HTMLCanvasElement} canvas - 描画対象のキャンバス
16
- * @param {[]} data - 描画用のコメント
17
- * @param {{useLegacy: boolean, formatted: boolean, video: HTMLVideoElement|null}} options - 細かい設定類
18
- */
19
- constructor(canvas, data, options = {
20
- useLegacy: false,
21
- formatted: false,
22
- video: null
23
- }) {
24
- this.canvas = canvas;
25
- this.context = canvas.getContext("2d");
26
- this.context.strokeStyle = "rgba(0,0,0,0.7)";
27
- this.context.textAlign = "left";
28
- this.context.textBaseline = "top";
29
- this.context.lineWidth = 4;
30
-
31
- if (options.useLegacy) {
32
- this.commentYOffset = 0.25;
33
- } else {
34
- this.commentYOffset = 0.2;
35
- }
36
-
37
- this.commentYMarginTop = 0.08;
38
- this.fontSize = {
39
- "small": {
40
- "default": 47,
41
- "resized": 27.5
42
- },
43
- "medium": {
44
- "default": 76,
45
- "resized": 39
46
- },
47
- "big": {
48
- "default": 118,
49
- "resized": 62.5
50
- }
51
- };
52
- this.defaultCommandValue = {
53
- loc: "naka",
54
- size: "medium",
55
- fontSize: this.fontSize.medium.default,
56
- color: "#ffffff",
57
- font: 'defont',
58
- full: false,
59
- ender: false,
60
- _live: false,
61
- invisible: false
62
- };
63
- this.doubleResizeMaxWidth = {
64
- full: {
65
- legacy: 3020,
66
- default: 3220
67
- },
68
- normal: {
69
- legacy: 2540,
70
- default: 2740
71
- }
72
- };
73
-
74
- if (options.formatted) {
75
- this.data = data;
76
- } else {
77
- this.data = this.parseData(data);
78
- }
79
-
80
- this.video = options.video ? options.video : null;
81
- this.showCollision = false;
82
- this.showFPS = false;
83
- this.showCommentCount = false;
84
- this.timeline = {};
85
- this.nicoScripts = {
86
- "reverse": [],
87
- "default": []
88
- };
89
- this.collision_right = {};
90
- this.collision_left = {};
91
- this.collision_ue = {};
92
- this.collision_shita = {};
93
- this.useLegacy = options.useLegacy;
94
- this.preRendering();
95
- this.fpsCount = 0;
96
- this.fps = 0;
97
- this.fpsClock = setInterval(() => {
98
- this.fps = this.fpsCount * 2;
99
- this.fpsCount = 0;
100
- }, 500);
101
- }
102
- /**
103
- * ニコニコが吐き出したデータを処理しやすいように変換する
104
- * @param {[]} data - ニコニコが吐き出したコメントデータ
105
- * @returns {*[]} - 独自フォーマットのコメントデータ
106
- */
107
-
108
-
109
- parseData(data) {
110
- let data_ = [];
111
-
112
- for (let i = 0; i < data.length; i++) {
113
- for (let key in data[i]) {
114
- let value = data[i][key];
115
-
116
- if (key === "chat" && value["deleted"] !== 1 && !value["content"].startsWith("/")) {
117
- let tmpParam = {
118
- "id": value["no"],
119
- "vpos": value["vpos"],
120
- "content": value["content"],
121
- "date": value["date"],
122
- "date_usec": value["date_usec"],
123
- "owner": !value["user_id"],
124
- "premium": value["premium"] === 1
125
- };
126
-
127
- if (value["mail"]) {
128
- tmpParam["mail"] = value["mail"].split(/[\s ]/g);
129
- } else {
130
- tmpParam["mail"] = [];
131
- }
132
-
133
- data_.push(tmpParam);
134
- }
135
- }
136
- }
137
-
138
- data_.sort((a, b) => {
139
- if (a.vpos < b.vpos) return -1;
140
- if (a.vpos > b.vpos) return 1;
141
- if (a.date < b.date) return -1;
142
- if (a.date > b.date) return 1;
143
- if (a.date_usec < b.date_usec) return -1;
144
- if (a.date_usec > b.date_usec) return 1;
145
- return 0;
146
- });
147
- return data_;
148
- }
149
- /**
150
- * 事前に当たり判定を考慮してコメントの描画場所を決定する
151
- */
152
-
153
-
154
- preRendering() {
155
- this.getFont();
156
- this.getCommentSize();
157
- this.getCommentPos();
158
- this.sortComment();
159
- }
160
- /**
161
- * コマンドをもとに各コメントに適用するフォントを決定する
162
- */
163
-
164
-
165
- getFont() {
166
- for (let i in this.data) {
167
- let comment = this.data[i];
168
- let command = this.parseCommandAndNicoscript(comment);
169
- this.data[i].loc = command.loc;
170
- this.data[i].size = command.size;
171
- this.data[i].fontSize = command.fontSize;
172
- this.data[i].font = command.font;
173
- this.data[i].color = command.color;
174
- this.data[i].full = command.full;
175
- this.data[i].ender = command.ender;
176
- this.data[i]._live = command._live;
177
- this.data[i].long = command.long;
178
- this.data[i].invisible = command.invisible;
179
- this.data[i].content = this.data[i].content.replaceAll("\t", " ");
180
- }
181
- }
182
- /**
183
- * コメントの描画サイズを計算する
184
- */
185
-
186
-
187
- getCommentSize() {
188
- let tmpData = groupBy(this.data, "font", "fontSize");
189
-
190
- for (let i in tmpData) {
191
- for (let j in tmpData[i]) {
192
- this.context.font = parseFont(i, j, this.useLegacy);
193
-
194
- for (let k in tmpData[i][j]) {
195
- let comment = tmpData[i][j][k];
196
-
197
- if (comment.invisible) {
198
- continue;
199
- }
200
-
201
- let measure = this.measureText(comment);
202
- this.data[comment.index].height = measure.height;
203
- this.data[comment.index].width = measure.width;
204
- this.data[comment.index].width_max = measure.width_max;
205
- this.data[comment.index].width_min = measure.width_min;
206
-
207
- if (measure.resized) {
208
- this.data[comment.index].fontSize = measure.fontSize;
209
- this.context.font = parseFont(i, j, this.useLegacy);
210
- }
211
- }
212
- }
213
- }
214
- }
215
- /**
216
- * 計算された描画サイズをもとに各コメントの配置位置を決定する
217
- */
218
-
219
-
220
- getCommentPos() {
221
- let data = this.data;
222
-
223
- for (let i in data) {
224
- let comment = data[i];
225
-
226
- if (comment.invisible) {
227
- continue;
228
- }
229
-
230
- for (let j = 0; j < 500; j++) {
231
- if (!this.timeline[comment.vpos + j]) {
232
- this.timeline[comment.vpos + j] = [];
233
- }
234
-
235
- if (!this.collision_right[comment.vpos + j]) {
236
- this.collision_right[comment.vpos + j] = [];
237
- }
238
-
239
- if (!this.collision_left[comment.vpos + j]) {
240
- this.collision_left[comment.vpos + j] = [];
241
- }
242
-
243
- if (!this.collision_ue[comment.vpos + j]) {
244
- this.collision_ue[comment.vpos + j] = [];
245
- }
246
-
247
- if (!this.collision_shita[comment.vpos + j]) {
248
- this.collision_shita[comment.vpos + j] = [];
249
- }
250
- }
251
-
252
- if (comment.loc === "naka") {
253
- comment.vpos -= 70;
254
- this.data[i].vpos -= 70;
255
- let posY = 0,
256
- is_break = false,
257
- is_change = true,
258
- count = 0;
259
-
260
- if (1080 < comment.height) {
261
- posY = (comment.height - 1080) / -2;
262
- } else {
263
- while (is_change && count < 10) {
264
- is_change = false;
265
- count++;
266
-
267
- for (let j = 0; j < 500; j++) {
268
- let vpos = comment.vpos + j;
269
- let left_pos = 1920 - (1920 + comment.width_max) * j / 500;
270
-
271
- if (left_pos + comment.width_max >= 1880) {
272
- for (let k in this.collision_right[vpos]) {
273
- let l = this.collision_right[vpos][k];
274
-
275
- if (posY < data[l].posY + data[l].height && posY + comment.height > data[l].posY && data[l].owner === comment.owner) {
276
- if (data[l].posY + data[l].height > posY) {
277
- posY = data[l].posY + data[l].height;
278
- is_change = true;
279
- }
280
-
281
- if (posY + comment.height > 1080) {
282
- if (1080 < comment.height) {
283
- posY = (comment.height - 1080) / -2;
284
- } else {
285
- posY = Math.floor(Math.random() * (1080 - comment.height));
286
- }
287
-
288
- is_break = true;
289
- break;
290
- }
291
- }
292
- }
293
-
294
- if (is_break) {
295
- break;
296
- }
297
- }
298
-
299
- if (left_pos <= 40 && is_break === false) {
300
- for (let k in this.collision_left[vpos]) {
301
- let l = this.collision_left[vpos][k];
302
-
303
- if (posY < data[l].posY + data[l].height && posY + comment.height > data[l].posY && data[l].owner === comment.owner) {
304
- if (data[l].posY + data[l].height > posY) {
305
- posY = data[l].posY + data[l].height;
306
- is_change = true;
307
- }
308
-
309
- if (posY + comment.height > 1080) {
310
- if (1080 < comment.height) {
311
- posY = 0;
312
- } else {
313
- posY = Math.random() * (1080 - comment.height);
314
- }
315
-
316
- is_break = true;
317
- break;
318
- }
319
- }
320
- }
321
-
322
- if (is_break) {
323
- break;
324
- }
325
- }
326
-
327
- if (is_break) {
328
- break;
329
- }
330
- }
331
- }
332
- }
333
-
334
- for (let j = 0; j < 500; j++) {
335
- let vpos = comment.vpos + j;
336
- let left_pos = 1920 - (1920 + comment.width_max) * j / 500;
337
- arrayPush(this.timeline, vpos, i);
338
-
339
- if (left_pos + comment.width_max >= 1880) {
340
- arrayPush(this.collision_right, vpos, i);
341
- }
342
-
343
- if (left_pos <= 40) {
344
- arrayPush(this.collision_left, vpos, i);
345
- }
346
- }
347
-
348
- this.data[i].posY = posY;
349
- } else {
350
- let posY = 0,
351
- is_break = false,
352
- is_change = true,
353
- count = 0,
354
- collision;
355
-
356
- if (comment.loc === "ue") {
357
- collision = this.collision_ue;
358
- } else if (comment.loc === "shita") {
359
- collision = this.collision_shita;
360
- }
361
-
362
- while (is_change && count < 10) {
363
- is_change = false;
364
- count++;
365
-
366
- for (let j = 0; j < 300; j++) {
367
- let vpos = comment.vpos + j;
368
-
369
- for (let k in collision[vpos]) {
370
- let l = collision[vpos][k];
371
-
372
- if (posY < data[l].posY + data[l].height && posY + comment.height > data[l].posY && data[l].owner === comment.owner) {
373
- if (data[l].posY + data[l].height > posY) {
374
- posY = data[l].posY + data[l].height;
375
- is_change = true;
376
- }
377
-
378
- if (posY + comment.height > 1080) {
379
- if (1000 <= comment.height) {
380
- posY = 0;
381
- } else {
382
- posY = Math.floor(Math.random() * (1080 - comment.height));
383
- }
384
-
385
- is_break = true;
386
- break;
387
- }
388
- }
389
- }
390
-
391
- if (is_break) {
392
- break;
393
- }
394
- }
395
- }
396
-
397
- for (let j = 0; j < comment.long; j++) {
398
- let vpos = comment.vpos + j;
399
- arrayPush(this.timeline, vpos, i);
400
-
401
- if (comment.loc === "ue") {
402
- arrayPush(this.collision_ue, vpos, i);
403
- } else {
404
- arrayPush(this.collision_shita, vpos, i);
405
- }
406
- }
407
-
408
- this.data[i].posY = posY;
409
- }
410
- }
411
- }
412
-
413
- sortComment() {
414
- for (let vpos in this.timeline) {
415
- this.timeline[vpos].sort((a, b) => {
416
- const A = this.data[a];
417
- const B = this.data[b];
418
-
419
- if (!A.owner && B.owner) {
420
- return -1;
421
- } else if (A.owner && !B.owner) {
422
- return 1;
423
- } else {
424
- return 0;
425
- }
426
- });
427
- }
428
- }
429
- /**
430
- * context.measureTextの複数行対応版
431
- * 画面外にはみ出すコメントの縮小も行う
432
- * @param comment - 独自フォーマットのコメントデータ
433
- * @returns {{resized: boolean, width: number, width_max: number, fontSize: number, width_min: number, height: number}} - 描画サイズとリサイズの情報
434
- */
435
-
436
-
437
- measureText(comment) {
438
- let width,
439
- width_max,
440
- width_min,
441
- height,
442
- width_arr = [],
443
- lines = comment.content.split("\n");
444
-
445
- if (!comment.resized && !comment.ender) {
446
- if (comment.size === "big" && lines.length > 2) {
447
- comment.fontSize = this.fontSize.big.resized;
448
- comment.resized = true;
449
- comment.tateRisized = true;
450
- this.context.font = parseFont(comment.font, comment.fontSize, this.useLegacy);
451
- } else if (comment.size === "medium" && lines.length > 4) {
452
- comment.fontSize = this.fontSize.medium.resized;
453
- comment.resized = true;
454
- comment.tateRisized = true;
455
- this.context.font = parseFont(comment.font, comment.fontSize, this.useLegacy);
456
- } else if (comment.size === "small" && lines.length > 6) {
457
- comment.fontSize = this.fontSize.small.resized;
458
- comment.resized = true;
459
- comment.tateRisized = true;
460
- this.context.font = parseFont(comment.font, comment.fontSize, this.useLegacy);
461
- }
462
- }
463
-
464
- for (let i = 0; i < lines.length; i++) {
465
- let measure = this.context.measureText(lines[i]);
466
- width_arr.push(measure.width);
467
- }
468
-
469
- width = width_arr.reduce((p, c) => p + c, 0) / width_arr.length;
470
- width_max = Math.max(...width_arr);
471
- width_min = Math.min(...width_arr);
472
- height = (comment.fontSize + this.commentYMarginTop * comment.fontSize) * lines.length;
473
-
474
- if (comment.loc !== "naka" && !comment.tateRisized) {
475
- if (comment.full && width_max > 1920) {
476
- comment.fontSize -= 1;
477
- comment.resized = true;
478
- comment.yokoResized = true;
479
- this.context.font = parseFont(comment.font, comment.fontSize, this.useLegacy);
480
- return this.measureText(comment);
481
- } else if (!comment.full && width_max > 1440) {
482
- comment.fontSize -= 1;
483
- comment.resized = true;
484
- comment.yokoResized = true;
485
- this.context.font = parseFont(comment.font, comment.fontSize, this.useLegacy);
486
- return this.measureText(comment);
487
- }
488
- } else if (comment.loc !== "naka" && comment.tateRisized && (comment.full && width_max > 1920 || !comment.full && width_max > 1440) && !comment.yokoResized) {
489
- comment.fontSize = this.fontSize[comment.size].default;
490
- comment.resized = true;
491
- comment.yokoResized = true;
492
- this.context.font = parseFont(comment.font, comment.fontSize, this.useLegacy);
493
- return this.measureText(comment);
494
- } else if (comment.loc !== "naka" && comment.tateRisized && comment.yokoResized) {
495
- if (comment.full && width_max > this.doubleResizeMaxWidth.full[this.useLegacy ? "legacy" : "default"]) {
496
- comment.fontSize -= 1;
497
- this.context.font = parseFont(comment.font, comment.fontSize, this.useLegacy);
498
- return this.measureText(comment);
499
- } else if (!comment.full && width_max > this.doubleResizeMaxWidth.normal[this.useLegacy ? "legacy" : "default"]) {
500
- comment.fontSize -= 1.;
501
- this.context.font = parseFont(comment.font, comment.fontSize, this.useLegacy);
502
- return this.measureText(comment);
503
- }
504
- }
505
-
506
- return {
507
- "width": width,
508
- "width_max": width_max,
509
- "width_min": width_min,
510
- "height": height,
511
- "resized": comment.resized,
512
- "fontSize": comment.fontSize
513
- };
514
- }
515
- /**
516
- * コマンドをもとに所定の位置にコメントを表示する
517
- * @param comment - 独自フォーマットのコメントデータ
518
- * @param {number} vpos - 動画の現在位置の100倍 ニコニコから吐き出されるコメントの位置情報は主にこれ
519
- */
520
-
521
-
522
- drawText(comment, vpos) {
523
- let reverse = false;
524
-
525
- for (let i in this.nicoScripts.reverse) {
526
- let range = this.nicoScripts.reverse[i];
527
-
528
- if (range.target === "コメ" && comment.owner || range.target === "投コメ" && !comment.owner) {
529
- break;
530
- }
531
-
532
- if (range.start < vpos && vpos < range.end) {
533
- reverse = true;
534
- }
535
- }
536
-
537
- let lines = comment.content.split("\n"),
538
- posX = (1920 - comment.width_max) / 2;
539
-
540
- if (comment.loc === "naka") {
541
- if (reverse) {
542
- posX = (1920 + comment.width_max) * (vpos - comment.vpos) / 500 - comment.width_max;
543
- } else {
544
- posX = 1920 - (1920 + comment.width_max) * (vpos - comment.vpos) / 500;
545
- }
546
- }
547
-
548
- if (this.showCollision) {
549
- this.context.strokeStyle = "rgba(0,255,255,1)";
550
-
551
- if (comment.loc === "shita") {
552
- this.context.strokeRect(posX, 1080 - comment.posY - comment.height, comment.width_max, comment.height);
553
- } else {
554
- this.context.strokeRect(posX, comment.posY, comment.width_max, comment.height);
555
- }
556
-
557
- if (comment.color === "#000000") {
558
- this.context.strokeStyle = "rgba(255,255,255,0.7)";
559
- } else {
560
- this.context.strokeStyle = "rgba(0,0,0,0.7)";
561
- }
562
- }
563
-
564
- for (let i in lines) {
565
- let line = lines[i],
566
- posY;
567
-
568
- if (comment.loc === "shita") {
569
- posY = 1080 - comment.posY + i * (comment.fontSize + this.commentYMarginTop * comment.fontSize) - comment.height + this.commentYOffset * comment.fontSize;
570
- } else {
571
- posY = comment.posY + i * (comment.fontSize + this.commentYMarginTop * comment.fontSize) + this.commentYOffset * comment.fontSize;
572
- }
573
-
574
- this.context.strokeText(line, posX, posY);
575
- this.context.fillText(line, posX, posY);
576
-
577
- if (this.showCollision) {
578
- this.context.strokeStyle = "rgba(255,255,0,0.5)";
579
- this.context.strokeRect(posX, posY, comment.width_max, comment.fontSize);
580
-
581
- if (comment.color === "#000000") {
582
- this.context.strokeStyle = "rgba(255,255,255,0.7)";
583
- } else {
584
- this.context.strokeStyle = "rgba(0,0,0,0.7)";
585
- }
586
- }
587
- }
588
- }
589
- /**
590
- * コメントに含まれるコマンドを解釈する
591
- * @param comment- 独自フォーマットのコメントデータ
592
- * @returns {{loc: string|null, size: string|null, color: string|null, fontSize: number|null, ender: boolean, font: string|null, full: boolean, _live: boolean, invisible: boolean, long:number|null}}
593
- */
594
-
595
-
596
- parseCommand(comment) {
597
- let metadata = comment.mail,
598
- loc = null,
599
- size = null,
600
- fontSize = null,
601
- color = null,
602
- font = null,
603
- full = false,
604
- ender = false,
605
- _live = false,
606
- invisible = false,
607
- long = null;
608
-
609
- for (let i in metadata) {
610
- let command = metadata[i].toLowerCase();
611
- const match = command.match(/^@([0-9.]+)/);
612
-
613
- if (match) {
614
- long = match[1];
615
- }
616
-
617
- if (loc === null) {
618
- switch (command) {
619
- case "ue":
620
- loc = "ue";
621
- break;
622
-
623
- case "shita":
624
- loc = "shita";
625
- break;
626
- }
627
- }
628
-
629
- if (size === null) {
630
- switch (command) {
631
- case "big":
632
- size = "big";
633
- fontSize = this.fontSize.big.default;
634
- break;
635
-
636
- case "small":
637
- size = "small";
638
- fontSize = this.fontSize.small.default;
639
- break;
640
- }
641
- }
642
-
643
- if (color === null) {
644
- switch (command) {
645
- case "white":
646
- color = "#FFFFFF";
647
- break;
648
-
649
- case "red":
650
- color = "#FF0000";
651
- break;
652
-
653
- case "pink":
654
- color = "#FF8080";
655
- break;
656
-
657
- case "orange":
658
- color = "#FFC000";
659
- break;
660
-
661
- case "yellow":
662
- color = "#FFFF00";
663
- break;
664
-
665
- case "green":
666
- color = "#00FF00";
667
- break;
668
-
669
- case "cyan":
670
- color = "#00FFFF";
671
- break;
672
-
673
- case "blue":
674
- color = "#0000FF";
675
- break;
676
-
677
- case "purple":
678
- color = "#C000FF";
679
- break;
680
-
681
- case "black":
682
- color = "#000000";
683
- break;
684
-
685
- case "white2":
686
- case "niconicowhite":
687
- color = "#CCCC99";
688
- break;
689
-
690
- case "red2":
691
- case "truered":
692
- color = "#CC0033";
693
- break;
694
-
695
- case "pink2":
696
- color = "#FF33CC";
697
- break;
698
-
699
- case "orange2":
700
- case "passionorange":
701
- color = "#FF6600";
702
- break;
703
-
704
- case "yellow2":
705
- case "madyellow":
706
- color = "#999900";
707
- break;
708
-
709
- case "green2":
710
- case "elementalgreen":
711
- color = "#00CC66";
712
- break;
713
-
714
- case "cyan2":
715
- color = "#00CCCC";
716
- break;
717
-
718
- case "blue2":
719
- case "marineblue":
720
- color = "#3399FF";
721
- break;
722
-
723
- case "purple2":
724
- case "nobleviolet":
725
- color = "#6633CC";
726
- break;
727
-
728
- case "black2":
729
- color = "#666666";
730
- break;
731
-
732
- default:
733
- const match = command.match(/#[0-9a-z]{3,6}/);
734
-
735
- if (match && comment.premium) {
736
- color = match[0].toUpperCase();
737
- }
738
-
739
- break;
740
- }
741
- }
742
-
743
- if (font === null) {
744
- switch (command) {
745
- case "gothic":
746
- font = "gothic";
747
- break;
748
-
749
- case "mincho":
750
- font = "mincho";
751
- break;
752
- }
753
- }
754
-
755
- switch (command) {
756
- case "full":
757
- full = true;
758
- break;
759
-
760
- case "ender":
761
- ender = true;
762
- break;
763
-
764
- case "_live":
765
- _live = true;
766
- break;
767
-
768
- case "invisible":
769
- invisible = true;
770
- break;
771
- }
772
- }
773
-
774
- return {
775
- loc,
776
- size,
777
- fontSize,
778
- color,
779
- font,
780
- full,
781
- ender,
782
- _live,
783
- invisible,
784
- long
785
- };
786
- }
787
-
788
- parseCommandAndNicoscript(comment) {
789
- let data = this.parseCommand(comment),
790
- nicoscript = comment.content.match(/^@(デフォルト|置換|逆|コメント禁止|シーク禁止|ジャンプ)/);
791
-
792
- if (nicoscript) {
793
- switch (nicoscript[1]) {
794
- case "デフォルト":
795
- this.nicoScripts.default.push({
796
- start: comment.vpos,
797
- long: data.long === null ? null : Math.floor(data.long * 100),
798
- color: data.color,
799
- size: data.size,
800
- font: data.font,
801
- loc: data.loc
802
- });
803
- break;
804
-
805
- case "逆":
806
- let reverse = comment.content.match(/^@逆 ?(全|コメ|投コメ)?/);
807
-
808
- if (!reverse[1]) {
809
- reverse[1] = "全";
810
- }
811
-
812
- if (data.long === null) {
813
- data.long = 30;
814
- }
815
-
816
- this.nicoScripts.reverse.push({
817
- "start": comment.vpos,
818
- "end": comment.vpos + data.long * 100,
819
- "target": reverse[1]
820
- });
821
- break;
822
- }
823
-
824
- data.invisible = true;
825
- }
826
-
827
- let color = "#FFFFFF",
828
- size = "medium",
829
- font = "defont",
830
- loc = "naka";
831
-
832
- for (let i in this.nicoScripts.default) {
833
- if (this.nicoScripts.default[i].long !== null && this.nicoScripts.default[i].start + this.nicoScripts.default[i].long < comment.vpos) {
834
- this.nicoScripts.default = this.nicoScripts.default.splice(Number(i), 1);
835
- continue;
836
- }
837
-
838
- if (this.nicoScripts.default[i].loc) {
839
- loc = this.nicoScripts.default[i].loc;
840
- }
841
-
842
- if (this.nicoScripts.default[i].color) {
843
- color = this.nicoScripts.default[i].color;
844
- }
845
-
846
- if (this.nicoScripts.default[i].size) {
847
- size = this.nicoScripts.default[i].size;
848
- }
849
-
850
- if (this.nicoScripts.default[i].font) {
851
- font = this.nicoScripts.default[i].font;
852
- }
853
- }
854
-
855
- if (!data.loc) {
856
- data.loc = loc;
857
- }
858
-
859
- if (!data.color) {
860
- data.color = color;
861
- }
862
-
863
- if (!data.size) {
864
- data.size = size;
865
- data.fontSize = this.fontSize[data.size].default;
866
- }
867
-
868
- if (!data.font) {
869
- data.font = font;
870
- }
871
-
872
- if (data.loc !== "naka") {
873
- if (!data.long) {
874
- data.long = 300;
875
- } else {
876
- data.long = Math.floor(data.long * 100);
877
- }
878
- }
879
-
880
- return data;
881
- }
882
- /**
883
- * キャンバスを描画する
884
- * @param vpos - 動画の現在位置の100倍 ニコニコから吐き出されるコメントの位置情報は主にこれ
885
- */
886
-
887
-
888
- drawCanvas(vpos) {
889
- this.fpsCount++;
890
- this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
891
-
892
- if (this.video) {
893
- let offsetX,
894
- offsetY,
895
- scale,
896
- height = this.canvas.height / this.video.videoHeight,
897
- width = this.canvas.width / this.video.videoWidth;
898
-
899
- if (height > width) {
900
- scale = width;
901
- } else {
902
- scale = height;
903
- }
904
-
905
- offsetX = (this.canvas.width - this.video.videoWidth * scale) * 0.5;
906
- offsetY = (this.canvas.height - this.video.videoHeight * scale) * 0.5;
907
- this.context.drawImage(this.video, offsetX, offsetY, this.video.videoWidth * scale, this.video.videoHeight * scale);
908
- }
909
-
910
- if (this.timeline[vpos]) {
911
- for (let index in this.timeline[vpos]) {
912
- let comment = this.data[this.timeline[vpos][index]];
913
-
914
- if (comment.invisible) {
915
- continue;
916
- }
917
-
918
- this.context.font = parseFont(comment.font, comment.fontSize, this.useLegacy);
919
-
920
- if (comment._live) {
921
- let rgb = hex2rgb(comment.color);
922
- this.context.fillStyle = `rgba(${rgb[0]},${rgb[1]},${rgb[2]},0.5)`;
923
- } else {
924
- this.context.fillStyle = comment.color;
925
- }
926
-
927
- if (comment.color === "#000000") {
928
- this.context.strokeStyle = "rgba(255,255,255,0.7)";
929
- }
930
-
931
- this.drawText(comment, vpos);
932
-
933
- if (comment.color === "#000000") {
934
- this.context.strokeStyle = "rgba(0,0,0,0.7)";
935
- }
936
- }
937
- }
938
-
939
- if (this.showFPS) {
940
- this.context.font = parseFont("defont", 60, this.useLegacy);
941
- this.context.fillStyle = "#00FF00";
942
- this.context.strokeText("FPS:" + this.fps, 100, 100);
943
- this.context.fillText("FPS:" + this.fps, 100, 100);
944
- }
945
-
946
- if (this.showCommentCount) {
947
- this.context.font = parseFont("defont", 60, this.useLegacy);
948
- this.context.fillStyle = "#00FF00";
949
-
950
- if (this.timeline[vpos]) {
951
- this.context.strokeText("Count:" + this.timeline[vpos].length, 100, 200);
952
- this.context.fillText("Count:" + this.timeline[vpos].length, 100, 200);
953
- } else {
954
- this.context.strokeText("Count:0", 100, 200);
955
- this.context.fillText("Count:0", 100, 200);
956
- }
957
- }
958
- }
959
- /**
960
- * キャンバスを消去する
961
- */
962
-
963
-
964
- clear() {
965
- this.context.clearRect(0, 0, 1920, 1080);
966
- }
967
-
968
- }
969
- /**
970
- * 配列を複数のキーでグループ化する
971
- * @param {{}} array
972
- * @param {string} key
973
- * @param {string} key2
974
- * @returns {{}}
975
- */
976
-
977
-
978
- const groupBy = (array, key, key2) => {
979
- let data = {};
980
-
981
- for (let i in array) {
982
- if (!data[array[i][key]]) {
983
- data[array[i][key]] = {};
984
- }
985
-
986
- if (!data[array[i][key]][array[i][key2]]) {
987
- data[array[i][key]][array[i][key2]] = [];
988
- }
989
-
990
- array[i].index = i;
991
- data[array[i][key]][array[i][key2]].push(array[i]);
992
- }
993
-
994
- return data;
995
- };
996
- /**
997
- * フォント名とサイズをもとにcontextで使えるフォントを生成する
998
- * @param {string} font
999
- * @param {number} size
1000
- * @param {boolean} useLegacy
1001
- * @returns {string}
1002
- */
1003
-
1004
-
1005
- const parseFont = (font, size, useLegacy) => {
1006
- switch (font) {
1007
- case "gothic":
1008
- return `normal 400 ${size}px "游ゴシック体", "游ゴシック", "Yu Gothic", YuGothic, yugothic, YuGo-Medium`;
1009
-
1010
- case "mincho":
1011
- return `normal 400 ${size}px "游明朝体", "游明朝", "Yu Mincho", YuMincho, yumincho, YuMin-Medium`;
1012
-
1013
- default:
1014
- if (useLegacy) {
1015
- return `normal 600 ${size}px Arial, "MS Pゴシック", "MS PGothic", MSPGothic, MS-PGothic`;
1016
- } else {
1017
- return `normal 600 ${size}px sans-serif, Arial, "MS Pゴシック", "MS PGothic", MSPGothic, MS-PGothic`;
1018
- }
1019
-
1020
- }
1021
- };
1022
- /**
1023
- * phpのarray_push的なあれ
1024
- * @param array
1025
- * @param {string} key
1026
- * @param push
1027
- */
1028
-
1029
-
1030
- const arrayPush = (array, key, push) => {
1031
- if (!array) {
1032
- array = {};
1033
- }
1034
-
1035
- if (!array[key]) {
1036
- array[key] = [];
1037
- }
1038
-
1039
- array[key].push(push);
1040
- };
1041
- /**
1042
- * Hexからrgbに変換する(_live用)
1043
- * @param {string} hex
1044
- * @return {array} RGB
1045
- */
1046
-
1047
-
1048
- const hex2rgb = hex => {
1049
- if (hex.slice(0, 1) === "#") hex = hex.slice(1);
1050
- if (hex.length === 3) hex = hex.slice(0, 1) + hex.slice(0, 1) + hex.slice(1, 2) + hex.slice(1, 2) + hex.slice(2, 3) + hex.slice(2, 3);
1051
- return [hex.slice(0, 2), hex.slice(2, 4), hex.slice(4, 6)].map(function (str) {
1052
- return parseInt(str, 16);
1053
- });
1054
- };
1055
-
1056
- return NiconiComments;
12
+ class NiconiComments {
13
+ /**
14
+ * NiconiComments Constructor
15
+ * @param {HTMLCanvasElement} canvas - 描画対象のキャンバス
16
+ * @param {[]} data - 描画用のコメント
17
+ * @param {{useLegacy: boolean, formatted: boolean, video: HTMLVideoElement|null}, showCollision: boolean, showFPS: boolean, showCommentCount: boolean, drawAllImageOnLoad: boolean} options - 細かい設定類
18
+ */
19
+ constructor(canvas, data, options = {
20
+ useLegacy: false,
21
+ formatted: false,
22
+ video: null,
23
+ showCollision: false,
24
+ showFPS: false,
25
+ showCommentCount: false,
26
+ drawAllImageOnLoad: false
27
+ }) {
28
+ this.canvas = canvas;
29
+ this.context = canvas.getContext("2d");
30
+ this.context.strokeStyle = "rgba(0,0,0,0.7)";
31
+ this.context.textAlign = "start";
32
+ this.context.textBaseline = "alphabetic";
33
+ this.context.lineWidth = 4;
34
+ this.commentYPaddingTop = 0.08;
35
+ this.commentYMarginBottom = 0.24;
36
+ this.fontSize = {
37
+ "small": {
38
+ "default": 47,
39
+ "resized": 26.1
40
+ },
41
+ "medium": {
42
+ "default": 74,
43
+ "resized": 38.7
44
+ },
45
+ "big": {
46
+ "default": 111,
47
+ "resized": 61
48
+ }
49
+ };
50
+ this.doubleResizeMaxWidth = {
51
+ full: {
52
+ legacy: 3020,
53
+ default: 3220
54
+ },
55
+ normal: {
56
+ legacy: 2540,
57
+ default: 2740
58
+ }
59
+ };
60
+
61
+ if (options.formatted) {
62
+ this.data = data;
63
+ } else {
64
+ this.data = this.parseData(data);
65
+ }
66
+
67
+ this.video = options.video ? options.video : null;
68
+ this.showCollision = options.showCollision;
69
+ this.showFPS = options.showFPS;
70
+ this.showCommentCount = options.showCommentCount;
71
+ this.timeline = {};
72
+ this.nicoScripts = {
73
+ "reverse": [],
74
+ "default": []
75
+ };
76
+ this.collision_right = {};
77
+ this.collision_left = {};
78
+ this.collision_ue = {};
79
+ this.collision_shita = {};
80
+ this.lastVpos = null;
81
+ this.useLegacy = options.useLegacy;
82
+ this.preRendering(options.drawAllImageOnLoad);
83
+ this.fpsCount = 0;
84
+ this.fps = 0;
85
+ this.fpsClock = setInterval(() => {
86
+ this.fps = this.fpsCount * 2;
87
+ this.fpsCount = 0;
88
+ }, 500);
89
+ }
90
+ /**
91
+ * ニコニコが吐き出したデータを処理しやすいように変換する
92
+ * @param {[]} data - ニコニコが吐き出したコメントデータ
93
+ * @returns {*[]} - 独自フォーマットのコメントデータ
94
+ */
95
+
96
+
97
+ parseData(data) {
98
+ let data_ = [];
99
+
100
+ for (let i = 0; i < data.length; i++) {
101
+ for (let key in data[i]) {
102
+ let value = data[i][key];
103
+
104
+ if (key === "chat" && value["deleted"] !== 1 && !value["content"].startsWith("/")) {
105
+ let tmpParam = {
106
+ "id": value["no"],
107
+ "vpos": value["vpos"],
108
+ "content": value["content"],
109
+ "date": value["date"],
110
+ "date_usec": value["date_usec"],
111
+ "owner": !value["user_id"],
112
+ "premium": value["premium"] === 1
113
+ };
114
+
115
+ if (value["mail"]) {
116
+ tmpParam["mail"] = value["mail"].split(/[\s ]/g);
117
+ } else {
118
+ tmpParam["mail"] = [];
119
+ }
120
+
121
+ data_.push(tmpParam);
122
+ }
123
+ }
124
+ }
125
+
126
+ data_.sort((a, b) => {
127
+ if (a.vpos < b.vpos) return -1;
128
+ if (a.vpos > b.vpos) return 1;
129
+ if (a.date < b.date) return -1;
130
+ if (a.date > b.date) return 1;
131
+ if (a.date_usec < b.date_usec) return -1;
132
+ if (a.date_usec > b.date_usec) return 1;
133
+ return 0;
134
+ });
135
+ return data_;
136
+ }
137
+ /**
138
+ * 事前に当たり判定を考慮してコメントの描画場所を決定する
139
+ * @param {boolean} drawAll - 読み込み時にすべてのコメント画像を生成する
140
+ * ※読み込み時めちゃくちゃ重くなるので途中で絶対にカクついてほしくないという場合以外は非推奨
141
+ */
142
+
143
+
144
+ preRendering(drawAll) {
145
+ this.getFont();
146
+ this.getCommentSize();
147
+ this.getCommentPos();
148
+ this.sortComment();
149
+
150
+ if (drawAll) {
151
+ for (let i in this.data) {
152
+ this.getTextImage(i);
153
+ }
154
+ }
155
+ }
156
+ /**
157
+ * コマンドをもとに各コメントに適用するフォントを決定する
158
+ */
159
+
160
+
161
+ getFont() {
162
+ for (let i in this.data) {
163
+ let comment = this.data[i];
164
+ let command = this.parseCommandAndNicoscript(comment);
165
+ this.data[i].loc = command.loc;
166
+ this.data[i].size = command.size;
167
+ this.data[i].fontSize = command.fontSize;
168
+ this.data[i].font = command.font;
169
+ this.data[i].color = command.color;
170
+ this.data[i].full = command.full;
171
+ this.data[i].ender = command.ender;
172
+ this.data[i]._live = command._live;
173
+ this.data[i].long = command.long;
174
+ this.data[i].invisible = command.invisible;
175
+ this.data[i].content = this.data[i].content.replaceAll("\t", "  ");
176
+ }
177
+ }
178
+ /**
179
+ * コメントの描画サイズを計算する
180
+ */
181
+
182
+
183
+ getCommentSize() {
184
+ let tmpData = groupBy(this.data, "font", "fontSize");
185
+
186
+ for (let i in tmpData) {
187
+ for (let j in tmpData[i]) {
188
+ this.context.font = parseFont(i, j, this.useLegacy);
189
+
190
+ for (let k in tmpData[i][j]) {
191
+ let comment = tmpData[i][j][k];
192
+
193
+ if (comment.invisible) {
194
+ continue;
195
+ }
196
+
197
+ let measure = this.measureText(comment);
198
+ this.data[comment.index].height = measure.height;
199
+ this.data[comment.index].width = measure.width;
200
+ this.data[comment.index].width_max = measure.width_max;
201
+ this.data[comment.index].width_min = measure.width_min;
202
+
203
+ if (measure.resized) {
204
+ this.data[comment.index].fontSize = measure.fontSize;
205
+ this.context.font = parseFont(i, j, this.useLegacy);
206
+ }
207
+ }
208
+ }
209
+ }
210
+ }
211
+ /**
212
+ * 計算された描画サイズをもとに各コメントの配置位置を決定する
213
+ */
214
+
215
+
216
+ getCommentPos() {
217
+ let data = this.data;
218
+
219
+ for (let i in data) {
220
+ let comment = data[i];
221
+
222
+ if (comment.invisible) {
223
+ continue;
224
+ }
225
+
226
+ for (let j = 0; j < 500; j++) {
227
+ if (!this.timeline[comment.vpos + j]) {
228
+ this.timeline[comment.vpos + j] = [];
229
+ }
230
+
231
+ if (!this.collision_right[comment.vpos + j]) {
232
+ this.collision_right[comment.vpos + j] = [];
233
+ }
234
+
235
+ if (!this.collision_left[comment.vpos + j]) {
236
+ this.collision_left[comment.vpos + j] = [];
237
+ }
238
+
239
+ if (!this.collision_ue[comment.vpos + j]) {
240
+ this.collision_ue[comment.vpos + j] = [];
241
+ }
242
+
243
+ if (!this.collision_shita[comment.vpos + j]) {
244
+ this.collision_shita[comment.vpos + j] = [];
245
+ }
246
+ }
247
+
248
+ if (comment.loc === "naka") {
249
+ comment.vpos -= 70;
250
+ this.data[i].vpos -= 70;
251
+ let posY = 0,
252
+ is_break = false,
253
+ is_change = true,
254
+ count = 0;
255
+
256
+ if (1080 < comment.height) {
257
+ posY = (comment.height - 1080) / -2;
258
+ } else {
259
+ while (is_change && count < 10) {
260
+ is_change = false;
261
+ count++;
262
+
263
+ for (let j = 0; j < 500; j++) {
264
+ let vpos = comment.vpos + j;
265
+ let left_pos = 1920 - (1920 + comment.width_max) * j / 500;
266
+
267
+ if (left_pos + comment.width_max >= 1880) {
268
+ for (let k in this.collision_right[vpos]) {
269
+ let l = this.collision_right[vpos][k];
270
+
271
+ if (posY < data[l].posY + data[l].height && posY + comment.height > data[l].posY && data[l].owner === comment.owner) {
272
+ if (data[l].posY + data[l].height > posY) {
273
+ posY = data[l].posY + data[l].height;
274
+ is_change = true;
275
+ }
276
+
277
+ if (posY + comment.height > 1080) {
278
+ if (1080 < comment.height) {
279
+ posY = (comment.height - 1080) / -2;
280
+ } else {
281
+ posY = Math.floor(Math.random() * (1080 - comment.height));
282
+ }
283
+
284
+ is_break = true;
285
+ break;
286
+ }
287
+ }
288
+ }
289
+
290
+ if (is_break) {
291
+ break;
292
+ }
293
+ }
294
+
295
+ if (left_pos <= 40 && is_break === false) {
296
+ for (let k in this.collision_left[vpos]) {
297
+ let l = this.collision_left[vpos][k];
298
+
299
+ if (posY < data[l].posY + data[l].height && posY + comment.height > data[l].posY && data[l].owner === comment.owner) {
300
+ if (data[l].posY + data[l].height > posY) {
301
+ posY = data[l].posY + data[l].height;
302
+ is_change = true;
303
+ }
304
+
305
+ if (posY + comment.height > 1080) {
306
+ if (1080 < comment.height) {
307
+ posY = 0;
308
+ } else {
309
+ posY = Math.random() * (1080 - comment.height);
310
+ }
311
+
312
+ is_break = true;
313
+ break;
314
+ }
315
+ }
316
+ }
317
+
318
+ if (is_break) {
319
+ break;
320
+ }
321
+ }
322
+
323
+ if (is_break) {
324
+ break;
325
+ }
326
+ }
327
+ }
328
+ }
329
+
330
+ for (let j = 0; j < 500; j++) {
331
+ let vpos = comment.vpos + j;
332
+ let left_pos = 1920 - (1920 + comment.width_max) * j / 500;
333
+ arrayPush(this.timeline, vpos, i);
334
+
335
+ if (left_pos + comment.width_max >= 1880) {
336
+ arrayPush(this.collision_right, vpos, i);
337
+ }
338
+
339
+ if (left_pos <= 40) {
340
+ arrayPush(this.collision_left, vpos, i);
341
+ }
342
+ }
343
+
344
+ this.data[i].posY = posY;
345
+ } else {
346
+ let posY = 0,
347
+ is_break = false,
348
+ is_change = true,
349
+ count = 0,
350
+ collision;
351
+
352
+ if (comment.loc === "ue") {
353
+ collision = this.collision_ue;
354
+ } else if (comment.loc === "shita") {
355
+ collision = this.collision_shita;
356
+ }
357
+
358
+ while (is_change && count < 10) {
359
+ is_change = false;
360
+ count++;
361
+
362
+ for (let j = 0; j < 300; j++) {
363
+ let vpos = comment.vpos + j;
364
+
365
+ for (let k in collision[vpos]) {
366
+ let l = collision[vpos][k];
367
+
368
+ if (posY < data[l].posY + data[l].height && posY + comment.height > data[l].posY && data[l].owner === comment.owner) {
369
+ if (data[l].posY + data[l].height > posY) {
370
+ posY = data[l].posY + data[l].height;
371
+ is_change = true;
372
+ }
373
+
374
+ if (posY + comment.height > 1080) {
375
+ if (1000 <= comment.height) {
376
+ posY = 0;
377
+ } else {
378
+ posY = Math.floor(Math.random() * (1080 - comment.height));
379
+ }
380
+
381
+ is_break = true;
382
+ break;
383
+ }
384
+ }
385
+ }
386
+
387
+ if (is_break) {
388
+ break;
389
+ }
390
+ }
391
+ }
392
+
393
+ for (let j = 0; j < comment.long; j++) {
394
+ let vpos = comment.vpos + j;
395
+ arrayPush(this.timeline, vpos, i);
396
+
397
+ if (comment.loc === "ue") {
398
+ arrayPush(this.collision_ue, vpos, i);
399
+ } else {
400
+ arrayPush(this.collision_shita, vpos, i);
401
+ }
402
+ }
403
+
404
+ this.data[i].posY = posY;
405
+ }
406
+ }
407
+ }
408
+ /**
409
+ * 投稿者コメントを前に移動
410
+ */
411
+
412
+
413
+ sortComment() {
414
+ for (let vpos in this.timeline) {
415
+ this.timeline[vpos].sort((a, b) => {
416
+ const A = this.data[a];
417
+ const B = this.data[b];
418
+
419
+ if (!A.owner && B.owner) {
420
+ return -1;
421
+ } else if (A.owner && !B.owner) {
422
+ return 1;
423
+ } else {
424
+ return 0;
425
+ }
426
+ });
427
+ }
428
+ }
429
+ /**
430
+ * context.measureTextの複数行対応版
431
+ * 画面外にはみ出すコメントの縮小も行う
432
+ * @param comment - 独自フォーマットのコメントデータ
433
+ * @returns {{resized: boolean, width: number, width_max: number, fontSize: number, width_min: number, height: number}} - 描画サイズとリサイズの情報
434
+ */
435
+
436
+
437
+ measureText(comment) {
438
+ let width,
439
+ width_max,
440
+ width_min,
441
+ height,
442
+ width_arr = [],
443
+ lines = comment.content.split("\n");
444
+
445
+ if (!comment.resized && !comment.ender) {
446
+ if (comment.size === "big" && lines.length > 2) {
447
+ comment.fontSize = this.fontSize.big.resized;
448
+ comment.resized = true;
449
+ comment.tateRisized = true;
450
+ this.context.font = parseFont(comment.font, comment.fontSize, this.useLegacy);
451
+ } else if (comment.size === "medium" && lines.length > 4) {
452
+ comment.fontSize = this.fontSize.medium.resized;
453
+ comment.resized = true;
454
+ comment.tateRisized = true;
455
+ this.context.font = parseFont(comment.font, comment.fontSize, this.useLegacy);
456
+ } else if (comment.size === "small" && lines.length > 6) {
457
+ comment.fontSize = this.fontSize.small.resized;
458
+ comment.resized = true;
459
+ comment.tateRisized = true;
460
+ this.context.font = parseFont(comment.font, comment.fontSize, this.useLegacy);
461
+ }
462
+ }
463
+
464
+ for (let i = 0; i < lines.length; i++) {
465
+ let measure = this.context.measureText(lines[i]);
466
+ width_arr.push(measure.width);
467
+ }
468
+
469
+ width = width_arr.reduce((p, c) => p + c, 0) / width_arr.length;
470
+ width_max = Math.max(...width_arr);
471
+ width_min = Math.min(...width_arr);
472
+ height = comment.fontSize * (1 + this.commentYPaddingTop) * lines.length + this.commentYMarginBottom * comment.fontSize;
473
+
474
+ if (comment.loc !== "naka" && !comment.tateRisized) {
475
+ if (comment.full && width_max > 1840) {
476
+ comment.fontSize -= 1;
477
+ comment.resized = true;
478
+ comment.yokoResized = true;
479
+ this.context.font = parseFont(comment.font, comment.fontSize, this.useLegacy);
480
+ return this.measureText(comment);
481
+ } else if (!comment.full && width_max > 1440) {
482
+ comment.fontSize -= 1;
483
+ comment.resized = true;
484
+ comment.yokoResized = true;
485
+ this.context.font = parseFont(comment.font, comment.fontSize, this.useLegacy);
486
+ return this.measureText(comment);
487
+ }
488
+ } else if (comment.loc !== "naka" && comment.tateRisized && (comment.full && width_max > 1920 || !comment.full && width_max > 1440) && !comment.yokoResized) {
489
+ comment.fontSize = this.fontSize[comment.size].default;
490
+ comment.resized = true;
491
+ comment.yokoResized = true;
492
+ this.context.font = parseFont(comment.font, comment.fontSize, this.useLegacy);
493
+ return this.measureText(comment);
494
+ } else if (comment.loc !== "naka" && comment.tateRisized && comment.yokoResized) {
495
+ if (comment.full && width_max > this.doubleResizeMaxWidth.full[this.useLegacy ? "legacy" : "default"]) {
496
+ comment.fontSize -= 1;
497
+ this.context.font = parseFont(comment.font, comment.fontSize, this.useLegacy);
498
+ return this.measureText(comment);
499
+ } else if (!comment.full && width_max > this.doubleResizeMaxWidth.normal[this.useLegacy ? "legacy" : "default"]) {
500
+ comment.fontSize -= 1.;
501
+ this.context.font = parseFont(comment.font, comment.fontSize, this.useLegacy);
502
+ return this.measureText(comment);
503
+ }
504
+ }
505
+
506
+ return {
507
+ "width": width,
508
+ "width_max": width_max,
509
+ "width_min": width_min,
510
+ "height": height,
511
+ "resized": comment.resized,
512
+ "fontSize": comment.fontSize
513
+ };
514
+ }
515
+ /**
516
+ * コマンドをもとに所定の位置に事前に生成したコメントを表示する
517
+ * @param comment - 独自フォーマットのコメントデータ
518
+ * @param {number} vpos - 動画の現在位置の100倍 ニコニコから吐き出されるコメントの位置情報は主にこれ
519
+ */
520
+
521
+
522
+ drawText(comment, vpos) {
523
+ let reverse = false;
524
+
525
+ for (let i in this.nicoScripts.reverse) {
526
+ let range = this.nicoScripts.reverse[i];
527
+
528
+ if (range.target === "コメ" && comment.owner || range.target === "投コメ" && !comment.owner) {
529
+ break;
530
+ }
531
+
532
+ if (range.start < vpos && vpos < range.end) {
533
+ reverse = true;
534
+ }
535
+ }
536
+
537
+ let posX = (1920 - comment.width_max) / 2;
538
+
539
+ if (comment.loc === "naka") {
540
+ if (reverse) {
541
+ posX = (1920 + comment.width_max) * (vpos - comment.vpos) / 500 - comment.width_max;
542
+ } else {
543
+ posX = 1920 - (1920 + comment.width_max) * (vpos - comment.vpos) / 500;
544
+ }
545
+ }
546
+
547
+ this.context.drawImage(comment.image, posX, comment.posY);
548
+ }
549
+ /**
550
+ * drawTextで毎回fill/strokeすると重いので画像化して再利用できるようにする
551
+ * @param {number} i - コメントデータのインデックス
552
+ */
553
+
554
+
555
+ getTextImage(i) {
556
+ let value = this.data[i];
557
+
558
+ if (value.invisible) {
559
+ return;
560
+ }
561
+
562
+ let image = document.createElement("canvas");
563
+ image.width = value.width_max;
564
+ image.height = value.height;
565
+ let context = image.getContext("2d");
566
+ context.strokeStyle = "rgba(0,0,0,0.7)";
567
+ context.textAlign = "start";
568
+ context.textBaseline = "alphabetic";
569
+ context.lineWidth = 4;
570
+ context.font = parseFont(value.font, value.fontSize, this.useLegacy);
571
+
572
+ if (value._live) {
573
+ let rgb = hex2rgb(value.color);
574
+ context.fillStyle = `rgba(${rgb[0]},${rgb[1]},${rgb[2]},0.5)`;
575
+ } else {
576
+ context.fillStyle = value.color;
577
+ }
578
+
579
+ if (value.color === "#000000") {
580
+ context.strokeStyle = "rgba(255,255,255,0.7)";
581
+ }
582
+
583
+ if (this.showCollision) {
584
+ context.strokeStyle = "rgba(0,255,255,1)";
585
+ context.strokeRect(0, 0, value.width_max, value.height);
586
+
587
+ if (value.color === "#000000") {
588
+ context.strokeStyle = "rgba(255,255,255,0.7)";
589
+ } else {
590
+ context.strokeStyle = "rgba(0,0,0,0.7)";
591
+ }
592
+ }
593
+
594
+ let lines = value.content.split("\n");
595
+
596
+ for (let i in lines) {
597
+ let line = lines[i],
598
+ posY;
599
+ posY = (Number(i) + 1) * value.fontSize * (1 + this.commentYPaddingTop);
600
+ context.strokeText(line, 0, posY);
601
+ context.fillText(line, 0, posY);
602
+
603
+ if (this.showCollision) {
604
+ context.strokeStyle = "rgba(255,255,0,0.5)";
605
+ context.strokeRect(0, posY, value.width_max, value.fontSize * -1);
606
+
607
+ if (value.color === "#000000") {
608
+ context.strokeStyle = "rgba(255,255,255,0.7)";
609
+ } else {
610
+ context.strokeStyle = "rgba(0,0,0,0.7)";
611
+ }
612
+ }
613
+ }
614
+
615
+ this.data[i].image = image;
616
+ }
617
+ /**
618
+ * コメントに含まれるコマンドを解釈する
619
+ * @param comment- 独自フォーマットのコメントデータ
620
+ * @returns {{loc: string|null, size: string|null, color: string|null, fontSize: number|null, ender: boolean, font: string|null, full: boolean, _live: boolean, invisible: boolean, long:number|null}}
621
+ */
622
+
623
+
624
+ parseCommand(comment) {
625
+ let metadata = comment.mail,
626
+ loc = null,
627
+ size = null,
628
+ fontSize = null,
629
+ color = null,
630
+ font = null,
631
+ full = false,
632
+ ender = false,
633
+ _live = false,
634
+ invisible = false,
635
+ long = null;
636
+
637
+ for (let i in metadata) {
638
+ let command = metadata[i].toLowerCase();
639
+ const match = command.match(/^@([0-9.]+)/);
640
+
641
+ if (match) {
642
+ long = match[1];
643
+ }
644
+
645
+ if (loc === null) {
646
+ switch (command) {
647
+ case "ue":
648
+ loc = "ue";
649
+ break;
650
+
651
+ case "shita":
652
+ loc = "shita";
653
+ break;
654
+ }
655
+ }
656
+
657
+ if (size === null) {
658
+ switch (command) {
659
+ case "big":
660
+ size = "big";
661
+ fontSize = this.fontSize.big.default;
662
+ break;
663
+
664
+ case "small":
665
+ size = "small";
666
+ fontSize = this.fontSize.small.default;
667
+ break;
668
+ }
669
+ }
670
+
671
+ if (color === null) {
672
+ switch (command) {
673
+ case "white":
674
+ color = "#FFFFFF";
675
+ break;
676
+
677
+ case "red":
678
+ color = "#FF0000";
679
+ break;
680
+
681
+ case "pink":
682
+ color = "#FF8080";
683
+ break;
684
+
685
+ case "orange":
686
+ color = "#FFC000";
687
+ break;
688
+
689
+ case "yellow":
690
+ color = "#FFFF00";
691
+ break;
692
+
693
+ case "green":
694
+ color = "#00FF00";
695
+ break;
696
+
697
+ case "cyan":
698
+ color = "#00FFFF";
699
+ break;
700
+
701
+ case "blue":
702
+ color = "#0000FF";
703
+ break;
704
+
705
+ case "purple":
706
+ color = "#C000FF";
707
+ break;
708
+
709
+ case "black":
710
+ color = "#000000";
711
+ break;
712
+
713
+ case "white2":
714
+ case "niconicowhite":
715
+ color = "#CCCC99";
716
+ break;
717
+
718
+ case "red2":
719
+ case "truered":
720
+ color = "#CC0033";
721
+ break;
722
+
723
+ case "pink2":
724
+ color = "#FF33CC";
725
+ break;
726
+
727
+ case "orange2":
728
+ case "passionorange":
729
+ color = "#FF6600";
730
+ break;
731
+
732
+ case "yellow2":
733
+ case "madyellow":
734
+ color = "#999900";
735
+ break;
736
+
737
+ case "green2":
738
+ case "elementalgreen":
739
+ color = "#00CC66";
740
+ break;
741
+
742
+ case "cyan2":
743
+ color = "#00CCCC";
744
+ break;
745
+
746
+ case "blue2":
747
+ case "marineblue":
748
+ color = "#3399FF";
749
+ break;
750
+
751
+ case "purple2":
752
+ case "nobleviolet":
753
+ color = "#6633CC";
754
+ break;
755
+
756
+ case "black2":
757
+ color = "#666666";
758
+ break;
759
+
760
+ default:
761
+ const match = command.match(/#[0-9a-z]{3,6}/);
762
+
763
+ if (match && comment.premium) {
764
+ color = match[0].toUpperCase();
765
+ }
766
+
767
+ break;
768
+ }
769
+ }
770
+
771
+ if (font === null) {
772
+ switch (command) {
773
+ case "gothic":
774
+ font = "gothic";
775
+ break;
776
+
777
+ case "mincho":
778
+ font = "mincho";
779
+ break;
780
+ }
781
+ }
782
+
783
+ switch (command) {
784
+ case "full":
785
+ full = true;
786
+ break;
787
+
788
+ case "ender":
789
+ ender = true;
790
+ break;
791
+
792
+ case "_live":
793
+ _live = true;
794
+ break;
795
+
796
+ case "invisible":
797
+ invisible = true;
798
+ break;
799
+ }
800
+ }
801
+
802
+ return {
803
+ loc,
804
+ size,
805
+ fontSize,
806
+ color,
807
+ font,
808
+ full,
809
+ ender,
810
+ _live,
811
+ invisible,
812
+ long
813
+ };
814
+ }
815
+
816
+ parseCommandAndNicoscript(comment) {
817
+ let data = this.parseCommand(comment),
818
+ nicoscript = comment.content.match(/^@(デフォルト|置換|逆|コメント禁止|シーク禁止|ジャンプ)/);
819
+
820
+ if (nicoscript) {
821
+ switch (nicoscript[1]) {
822
+ case "デフォルト":
823
+ this.nicoScripts.default.push({
824
+ start: comment.vpos,
825
+ long: data.long === null ? null : Math.floor(data.long * 100),
826
+ color: data.color,
827
+ size: data.size,
828
+ font: data.font,
829
+ loc: data.loc
830
+ });
831
+ break;
832
+
833
+ case "逆":
834
+ let reverse = comment.content.match(/^@逆 ?(全|コメ|投コメ)?/);
835
+
836
+ if (!reverse[1]) {
837
+ reverse[1] = "全";
838
+ }
839
+
840
+ if (data.long === null) {
841
+ data.long = 30;
842
+ }
843
+
844
+ this.nicoScripts.reverse.push({
845
+ "start": comment.vpos,
846
+ "end": comment.vpos + data.long * 100,
847
+ "target": reverse[1]
848
+ });
849
+ break;
850
+ }
851
+
852
+ data.invisible = true;
853
+ }
854
+
855
+ let color = "#FFFFFF",
856
+ size = "medium",
857
+ font = "defont",
858
+ loc = "naka";
859
+
860
+ for (let i in this.nicoScripts.default) {
861
+ if (this.nicoScripts.default[i].long !== null && this.nicoScripts.default[i].start + this.nicoScripts.default[i].long < comment.vpos) {
862
+ this.nicoScripts.default = this.nicoScripts.default.splice(Number(i), 1);
863
+ continue;
864
+ }
865
+
866
+ if (this.nicoScripts.default[i].loc) {
867
+ loc = this.nicoScripts.default[i].loc;
868
+ }
869
+
870
+ if (this.nicoScripts.default[i].color) {
871
+ color = this.nicoScripts.default[i].color;
872
+ }
873
+
874
+ if (this.nicoScripts.default[i].size) {
875
+ size = this.nicoScripts.default[i].size;
876
+ }
877
+
878
+ if (this.nicoScripts.default[i].font) {
879
+ font = this.nicoScripts.default[i].font;
880
+ }
881
+ }
882
+
883
+ if (!data.loc) {
884
+ data.loc = loc;
885
+ }
886
+
887
+ if (!data.color) {
888
+ data.color = color;
889
+ }
890
+
891
+ if (!data.size) {
892
+ data.size = size;
893
+ data.fontSize = this.fontSize[data.size].default;
894
+ }
895
+
896
+ if (!data.font) {
897
+ data.font = font;
898
+ }
899
+
900
+ if (data.loc !== "naka") {
901
+ if (!data.long) {
902
+ data.long = 300;
903
+ } else {
904
+ data.long = Math.floor(data.long * 100);
905
+ }
906
+ }
907
+
908
+ return data;
909
+ }
910
+ /**
911
+ * キャンバスを描画する
912
+ * @param vpos - 動画の現在位置の100倍 ニコニコから吐き出されるコメントの位置情報は主にこれ
913
+ */
914
+
915
+
916
+ drawCanvas(vpos) {
917
+ if (this.lastVpos === vpos) return;
918
+ this.lastVpos = vpos;
919
+ this.fpsCount++;
920
+ this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
921
+
922
+ if (this.video) {
923
+ let offsetX,
924
+ offsetY,
925
+ scale,
926
+ height = this.canvas.height / this.video.videoHeight,
927
+ width = this.canvas.width / this.video.videoWidth;
928
+
929
+ if (height > width) {
930
+ scale = width;
931
+ } else {
932
+ scale = height;
933
+ }
934
+
935
+ offsetX = (this.canvas.width - this.video.videoWidth * scale) * 0.5;
936
+ offsetY = (this.canvas.height - this.video.videoHeight * scale) * 0.5;
937
+ this.context.drawImage(this.video, offsetX, offsetY, this.video.videoWidth * scale, this.video.videoHeight * scale);
938
+ }
939
+
940
+ if (this.timeline[vpos]) {
941
+ for (let index in this.timeline[vpos]) {
942
+ let comment = this.data[this.timeline[vpos][index]];
943
+
944
+ if (comment.invisible) {
945
+ continue;
946
+ }
947
+
948
+ if (!comment.image) {
949
+ this.getTextImage(this.timeline[vpos][index]);
950
+ }
951
+
952
+ this.drawText(comment, vpos);
953
+ }
954
+ }
955
+
956
+ if (this.showFPS) {
957
+ this.context.font = parseFont("defont", 60, this.useLegacy);
958
+ this.context.fillStyle = "#00FF00";
959
+ this.context.strokeText("FPS:" + this.fps, 100, 100);
960
+ this.context.fillText("FPS:" + this.fps, 100, 100);
961
+ }
962
+
963
+ if (this.showCommentCount) {
964
+ this.context.font = parseFont("defont", 60, this.useLegacy);
965
+ this.context.fillStyle = "#00FF00";
966
+
967
+ if (this.timeline[vpos]) {
968
+ this.context.strokeText("Count:" + this.timeline[vpos].length, 100, 200);
969
+ this.context.fillText("Count:" + this.timeline[vpos].length, 100, 200);
970
+ } else {
971
+ this.context.strokeText("Count:0", 100, 200);
972
+ this.context.fillText("Count:0", 100, 200);
973
+ }
974
+ }
975
+ }
976
+ /**
977
+ * キャンバスを消去する
978
+ */
979
+
980
+
981
+ clear() {
982
+ this.context.clearRect(0, 0, 1920, 1080);
983
+ }
984
+
985
+ }
986
+ /**
987
+ * 配列を複数のキーでグループ化する
988
+ * @param {{}} array
989
+ * @param {string} key
990
+ * @param {string} key2
991
+ * @returns {{}}
992
+ */
993
+
994
+
995
+ const groupBy = (array, key, key2) => {
996
+ let data = {};
997
+
998
+ for (let i in array) {
999
+ if (!data[array[i][key]]) {
1000
+ data[array[i][key]] = {};
1001
+ }
1002
+
1003
+ if (!data[array[i][key]][array[i][key2]]) {
1004
+ data[array[i][key]][array[i][key2]] = [];
1005
+ }
1006
+
1007
+ array[i].index = i;
1008
+ data[array[i][key]][array[i][key2]].push(array[i]);
1009
+ }
1010
+
1011
+ return data;
1012
+ };
1013
+ /**
1014
+ * フォント名とサイズをもとにcontextで使えるフォントを生成する
1015
+ * @param {string} font
1016
+ * @param {number} size
1017
+ * @param {boolean} useLegacy
1018
+ * @returns {string}
1019
+ */
1020
+
1021
+
1022
+ const parseFont = (font, size, useLegacy) => {
1023
+ switch (font) {
1024
+ case "gothic":
1025
+ return `normal 400 ${size}px "游ゴシック体", "游ゴシック", "Yu Gothic", YuGothic, yugothic, YuGo-Medium`;
1026
+
1027
+ case "mincho":
1028
+ return `normal 400 ${size}px "游明朝体", "游明朝", "Yu Mincho", YuMincho, yumincho, YuMin-Medium`;
1029
+
1030
+ default:
1031
+ if (useLegacy) {
1032
+ return `normal 600 ${size}px Arial, "MS Pゴシック", "MS PGothic", MSPGothic, MS-PGothic`;
1033
+ } else {
1034
+ return `normal 600 ${size}px sans-serif, Arial, "MS Pゴシック", "MS PGothic", MSPGothic, MS-PGothic`;
1035
+ }
1036
+
1037
+ }
1038
+ };
1039
+ /**
1040
+ * phpのarray_push的なあれ
1041
+ * @param array
1042
+ * @param {string} key
1043
+ * @param push
1044
+ */
1045
+
1046
+
1047
+ const arrayPush = (array, key, push) => {
1048
+ if (!array) {
1049
+ array = {};
1050
+ }
1051
+
1052
+ if (!array[key]) {
1053
+ array[key] = [];
1054
+ }
1055
+
1056
+ array[key].push(push);
1057
+ };
1058
+ /**
1059
+ * Hexからrgbに変換する(_live用)
1060
+ * @param {string} hex
1061
+ * @return {array} RGB
1062
+ */
1063
+
1064
+
1065
+ const hex2rgb = hex => {
1066
+ if (hex.slice(0, 1) === "#") hex = hex.slice(1);
1067
+ if (hex.length === 3) hex = hex.slice(0, 1) + hex.slice(0, 1) + hex.slice(1, 2) + hex.slice(1, 2) + hex.slice(2, 3) + hex.slice(2, 3);
1068
+ return [hex.slice(0, 2), hex.slice(2, 4), hex.slice(4, 6)].map(function (str) {
1069
+ return parseInt(str, 16);
1070
+ });
1071
+ };
1072
+
1073
+ return NiconiComments;
1057
1074
 
1058
1075
  }));