diff --git a/docs/index.html b/docs/index.html index 892d8860..3d6d8737 100644 --- a/docs/index.html +++ b/docs/index.html @@ -65,6 +65,7 @@

Index

  • Methods @@ -343,6 +344,16 @@

    Methods

    +

    + addComments(...comments:formattedComment):void +

    +
    +

    drawCanvas(vpos:number)サイズ設定をミスるとコメントが正常に描画されません

    `, ], p_data: [ - `

    Please pass comment data.

    + `

    Please pass comment data or undefined.

    Please check the format for supported formats.

    `, - `

    コメントデータを渡してください

    + `

    コメントデータまたはundefinedを渡してください

    対応フォーマットはformatを確認してください

    `, ], p_config: [ @@ -69,6 +69,11 @@ const localize = {

    Supported formats are as follows

    + + + + + @@ -106,6 +111,11 @@ const localize = {
    NameTypeNote
    emptyundefinedFor dynamic additional comments
    niconicome XMLDocument
    + + + + + @@ -232,6 +242,14 @@ const localize = {

    デフォルト(null)の場合は描画を行いません

    指定されている場合は、背景に指定された動画を描画し、その上にコメントを描画します

    この機能を応用すると、Picture in Pictureにもコメントを表示できるようになります

    `, + ], + m_addComments: [ + `

    This is a feature to dynamically add comments, mainly for live broadcasts.

    +

    Comments added by this feature are placed based on a hit decision, but do not affect the position of subsequent comments that have already been placed.

    +

    Comments may overlap with each other when placed between already generated comments.

    `, + `

    主に生配信向けの、コメントを動的に追加する機能です

    +

    この機能によって追加されたコメントは当たり判定を考慮して配置されますが、すでに配置されているその後のコメントの位置には影響を及ぼしません

    +

    生成済みのコメントの間に配置した場合、コメント同士が重複する場合があります

    ` ], m_drawCanvas: [ `

    Draws a comment on the canvas based on vpos(currentTime*100 of the video)

    diff --git a/docs/style.css b/docs/style.css index c1077d24..3d36d0a7 100644 --- a/docs/style.css +++ b/docs/style.css @@ -45,11 +45,13 @@ main { aside { width: 30%; + max-width: 300px; min-height: 100vh; } main { width: 70%; + min-width: calc(100% - 300px); } section { diff --git a/package.json b/package.json index 2984386e..65953897 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@xpadev-net/niconicomments", - "version": "0.2.35", + "version": "0.2.36", "description": "NiconiComments is a comment drawing library that is somewhat compatible with the official Nico Nico Douga player.", "main": "dist/bundle.js", "types": "dist/bundle.d.ts", @@ -24,7 +24,12 @@ "url": "git+https://github.com/xpadev-net/niconicomments.git" }, "keywords": [ - "niconico" + "canvas", + "comment", + "danmaku", + "html5", + "niconico", + "nicovide" ], "author": "xpadev(xpadev.net)", "bugs": { diff --git a/src/@types/options.d.ts b/src/@types/options.d.ts index 3472bfc4..b9d65403 100644 --- a/src/@types/options.d.ts +++ b/src/@types/options.d.ts @@ -5,6 +5,7 @@ type inputFormatType = | "legacyOwner" | "owner" | "v1" + | "empty" | "default"; type inputFormat = | XMLDocument @@ -13,7 +14,8 @@ type inputFormat = | rawApiResponse[] | ownerComment[] | v1Thread[] - | string; + | string + | undefined; type modeType = "default" | "html5" | "flash"; type Options = { config: ConfigNullable; diff --git a/src/@types/types.d.ts b/src/@types/types.d.ts index 65259081..e42f4a38 100644 --- a/src/@types/types.d.ts +++ b/src/@types/types.d.ts @@ -55,7 +55,7 @@ type commentSize = "big" | "medium" | "small"; type commentLoc = "ue" | "naka" | "shita"; type collision = { [key in collisionPos]: collisionItem }; type collisionPos = "ue" | "shita" | "right" | "left"; -type collisionItem = { [p: number]: number[] }; +type collisionItem = { [p: number]: IComment[] }; type nicoScript = { reverse: nicoScriptReverse[]; ban: nicoScriptBan[]; diff --git a/src/inputParser.ts b/src/inputParser.ts index 0b843974..58013844 100644 --- a/src/inputParser.ts +++ b/src/inputParser.ts @@ -11,7 +11,9 @@ const convert2formattedComment = ( type: inputFormatType ): formattedComment[] => { let result: formattedComment[] = []; - if (type === "niconicome" && typeGuard.niconicome.xmlDocument(data)) { + if (type === "empty" && data === undefined) { + return []; + } else if (type === "niconicome" && typeGuard.niconicome.xmlDocument(data)) { result = fromNiconicome(data); } else if (type === "formatted" && typeGuard.formatted.legacyComments(data)) { result = fromFormatted(data); diff --git a/src/main.ts b/src/main.ts index c49879ab..97f24e24 100644 --- a/src/main.ts +++ b/src/main.ts @@ -31,12 +31,11 @@ class NiconiComments { public showFPS: boolean; public showCommentCount: boolean; public video: HTMLVideoElement | undefined; - private data: IComment[]; private lastVpos: number; private readonly canvas: HTMLCanvasElement; private readonly collision: collision; private readonly context: CanvasRenderingContext2D; - private readonly timeline: { [key: number]: number[] }; + private readonly timeline: { [key: number]: IComment[] }; /** * NiconiComments Constructor @@ -105,7 +104,6 @@ class NiconiComments { pv[value] = [] as collisionItem; return pv; }, {} as collision); - this.data = []; this.lastVpos = -1; this.preRendering(parsedData); @@ -115,14 +113,13 @@ class NiconiComments { /** * 事前に当たり判定を考慮してコメントの描画場所を決定する * @param {any[]} rawData - * ※読み込み時めちゃくちゃ重くなるので途中で絶対にカクついてほしくないという場合以外は非推奨 */ - preRendering(rawData: formattedComment[]) { + private preRendering(rawData: formattedComment[]) { const preRenderingStart = performance.now(); if (options.keepCA) { rawData = changeCALayer(rawData); } - const parsedData: IComment[] = this.getCommentPos( + this.getCommentPos( rawData.reduce((pv, val) => { if (isFlashComment(val)) { pv.push(new FlashComment(val, this.context)); @@ -132,16 +129,16 @@ class NiconiComments { return pv; }, [] as IComment[]) ); - this.data = this.sortComment(parsedData); + this.sortComment(); logger(`preRendering complete: ${performance.now() - preRenderingStart}ms`); } /** * 計算された描画サイズをもとに各コメントの配置位置を決定する */ - getCommentPos(data: IComment[]): IComment[] { + private getCommentPos(data: IComment[]): IComment[] { const getCommentPosStart = performance.now(); - data.forEach((comment, index) => { + data.forEach((comment) => { if (comment.invisible) return; if (comment.loc === "naka") { let posY = 0; @@ -167,8 +164,7 @@ class NiconiComments { const result = getPosY( posY, comment, - this.collision.right[vpos], - data + this.collision.right[vpos] ); posY = result.currentPos; isChanged = result.isChanged; @@ -182,8 +178,7 @@ class NiconiComments { const result = getPosY( posY, comment, - this.collision.left[vpos], - data + this.collision.left[vpos] ); posY = result.currentPos; isChanged = result.isChanged; @@ -199,18 +194,18 @@ class NiconiComments { for (let j = beforeVpos; j < comment.long + 125; j++) { const vpos = comment.vpos + j; const left_pos = getPosX(comment.width, j, comment.long); - arrayPush(this.timeline, vpos, index); + arrayPush(this.timeline, vpos, comment); if ( left_pos + comment.width >= config.collisionRange.right && left_pos <= config.collisionRange.right ) { - arrayPush(this.collision.right, vpos, index); + arrayPush(this.collision.right, vpos, comment); } if ( left_pos + comment.width >= config.collisionRange.left && left_pos <= config.collisionRange.left ) { - arrayPush(this.collision.left, vpos, index); + arrayPush(this.collision.left, vpos, comment); } } comment.posY = posY; @@ -228,12 +223,7 @@ class NiconiComments { isChanged = false; count++; for (let j = 0; j < comment.long; j++) { - const result = getPosY( - posY, - comment, - collision[comment.vpos + j], - data - ); + const result = getPosY(posY, comment, collision[comment.vpos + j]); posY = result.currentPos; isChanged = result.isChanged; if (result.isBreak) break; @@ -241,12 +231,12 @@ class NiconiComments { } for (let j = 0; j < comment.long; j++) { const vpos = comment.vpos + j; - arrayPush(this.timeline, vpos, index); + arrayPush(this.timeline, vpos, comment); if (j > comment.long - 20) continue; if (comment.loc === "ue") { - arrayPush(this.collision.ue, vpos, index); + arrayPush(this.collision.ue, vpos, comment); } else { - arrayPush(this.collision.shita, vpos, index); + arrayPush(this.collision.shita, vpos, comment); } } comment.posY = posY; @@ -261,24 +251,145 @@ class NiconiComments { /** * 投稿者コメントを前に移動 */ - sortComment(parsedData: IComment[]) { + private sortComment() { const sortCommentStart = performance.now(); for (const vpos of Object.keys(this.timeline)) { const item = this.timeline[Number(vpos)]; if (!item) continue; - const owner = [], - user = []; - for (const index of item) { - if (parsedData[index]?.owner) { - owner.push(index); + const owner: IComment[] = [], + user: IComment[] = []; + for (const comment of item) { + if (comment?.owner) { + owner.push(comment); } else { - user.push(index); + user.push(comment); } } this.timeline[Number(vpos)] = user.concat(owner); } logger(`parseData complete: ${performance.now() - sortCommentStart}ms`); - return parsedData; + } + + /** + * 動的にコメント追加 + */ + public addComments(...rawComments: formattedComment[]) { + const comments = rawComments.reduce((pv, val) => { + if (isFlashComment(val)) { + pv.push(new FlashComment(val, this.context)); + } else { + pv.push(new HTML5Comment(val, this.context)); + } + return pv; + }, [] as IComment[]); + for (const comment of comments) { + if (comment.invisible) continue; + if (comment.loc === "naka") { + let posY = 0; + const beforeVpos = + Math.round(-288 / ((1632 + comment.width) / (comment.long + 125))) - + 100; + if (config.canvasHeight < comment.height) { + posY = (comment.height - config.canvasHeight) / -2; + } else { + let isBreak = false, + isChanged = true, + count = 0; + while (isChanged && count < 10) { + isChanged = false; + count++; + for (let j = beforeVpos; j < comment.long + 125; j++) { + const vpos = comment.vpos + j; + const left_pos = getPosX(comment.width, j, comment.long); + if ( + left_pos + comment.width >= config.collisionRange.right && + left_pos <= config.collisionRange.right + ) { + const collision = this.collision.right[vpos]?.filter( + (val) => val.vpos <= comment.vpos + ); + const result = getPosY(posY, comment, collision); + posY = result.currentPos; + isChanged = result.isChanged; + isBreak = result.isBreak; + if (isBreak) break; + } + if ( + left_pos + comment.width >= config.collisionRange.left && + left_pos <= config.collisionRange.left + ) { + const collision = this.collision.left[vpos]?.filter( + (val) => val.vpos <= comment.vpos + ); + const result = getPosY(posY, comment, collision); + posY = result.currentPos; + isChanged = result.isChanged; + isBreak = result.isBreak; + if (isBreak) break; + } + } + if (isBreak) { + break; + } + } + } + for (let j = beforeVpos; j < comment.long + 125; j++) { + const vpos = comment.vpos + j; + const left_pos = getPosX(comment.width, j, comment.long); + arrayPush(this.timeline, vpos, comment); + if ( + left_pos + comment.width >= config.collisionRange.right && + left_pos <= config.collisionRange.right + ) { + arrayPush(this.collision.right, vpos, comment); + } + if ( + left_pos + comment.width >= config.collisionRange.left && + left_pos <= config.collisionRange.left + ) { + arrayPush(this.collision.left, vpos, comment); + } + } + comment.posY = posY; + } else { + let posY = 0, + isChanged = true, + count = 0, + collision: collisionItem; + if (comment.loc === "ue") { + collision = this.collision.ue; + } else { + collision = this.collision.shita; + } + while (isChanged && count < 10) { + isChanged = false; + count++; + for (let j = 0; j < comment.long; j++) { + const result = getPosY( + posY, + comment, + collision[comment.vpos + j]?.filter( + (val) => val.vpos <= comment.vpos + ) + ); + posY = result.currentPos; + isChanged = result.isChanged; + if (result.isBreak) break; + } + } + for (let j = 0; j < comment.long; j++) { + const vpos = comment.vpos + j; + arrayPush(this.timeline, vpos, comment); + if (j > comment.long - 20) continue; + if (comment.loc === "ue") { + arrayPush(this.collision.ue, vpos, comment); + } else { + arrayPush(this.collision.shita, vpos, comment); + } + } + comment.posY = posY; + } + } } /** @@ -286,7 +397,7 @@ class NiconiComments { * @param vpos - 動画の現在位置の100倍 ニコニコから吐き出されるコメントの位置情報は主にこれ * @param forceRendering */ - drawCanvas(vpos: number, forceRendering = false) { + public drawCanvas(vpos: number, forceRendering = false) { const drawCanvasStart = performance.now(); if (this.lastVpos === vpos && !forceRendering) return; this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); @@ -316,35 +427,30 @@ class NiconiComments { rightCollision = this.collision.right[vpos]; this.context.fillStyle = "red"; if (leftCollision) { - for (const index of leftCollision) { - const value = this.data[index]; - if (!value) continue; + for (const comment of leftCollision) { this.context.fillRect( config.collisionRange.left, - value.posY, + comment.posY, config.contextLineWidth, - value.height + comment.height ); } } if (rightCollision) { - for (const index of rightCollision) { - const value = this.data[index]; - if (!value) continue; + for (const comment of rightCollision) { this.context.fillRect( config.collisionRange.right, - value.posY, + comment.posY, config.contextLineWidth * -1, - value.height + comment.height ); } } } if (timelineRange) { - for (const index of timelineRange) { - const comment = this.data[index]; - if (!comment || comment.invisible) { + for (const comment of timelineRange) { + if (comment.invisible) { continue; } comment.draw(vpos, this.showCollision, isDebug); diff --git a/src/util.ts b/src/util.ts index 4f87a586..1cd9bf94 100644 --- a/src/util.ts +++ b/src/util.ts @@ -6,20 +6,16 @@ import typeGuard from "@/typeGuard"; * @param {number} currentPos * @param {parsedComment} targetComment * @param {number[]|undefined} collision - * @param {parsedComment[]} data */ const getPosY = ( currentPos: number, targetComment: IComment, - collision: number[] | undefined, - data: IComment[] + collision: IComment[] | undefined ): { currentPos: number; isChanged: boolean; isBreak: boolean } => { let isChanged = false, isBreak = false; if (!collision) return { currentPos, isChanged, isBreak }; - for (const index of collision) { - const collisionItem = data[index]; - if (!collisionItem) continue; + for (const collisionItem of collision) { if ( currentPos < collisionItem.posY + collisionItem.height && currentPos + targetComment.height > collisionItem.posY && @@ -86,9 +82,9 @@ const parseFont = (font: commentFont, size: string | number): string => { * @param push */ const arrayPush = ( - array: { [key: number]: number[] }, + array: { [key: number]: IComment[] }, key: string | number, - push: number + push: IComment ) => { if (!array) { array = {}; @@ -152,7 +148,8 @@ const changeCALayer = (rawData: formattedComment[]): formattedComment[] => { userList[value.user_id] += (value.content.match(/\r\n|\n|\r/g) || []).length / 2; } - const key = `${value.content}@@${Array.from(new Set([...value.mail].sort())) + const key = `${value.content}@@${[...value.mail] + .sort() .filter((e) => !e.match(/@[\d.]+|184|device:.+|patissier|ca/)) .join("")}`, lastComment = index[key];
    名前dataのtype備考
    emptyundefined動的追加コメント用
    niconicome XMLDocument