(() => { return (settings, resources) => { class Danmaku { constructor(content, time, type, fontSize, color) { this.content = content; this.time = time; this.type = parseInt(type); this.fontSize = parseFloat(fontSize); this.color = parseInt(color); } } class XmlDanmaku extends Danmaku { constructor({ content, time, type, fontSize, color, timeStamp, pool, userHash, rowId }) { super(content, time, type, fontSize, color); this.timeStamp = parseInt(timeStamp); this.pool = parseInt(pool); this.userHash = userHash; this.rowId = parseInt(rowId); this.time = parseFloat(this.time); this.pDataArray = [time, type, fontSize, color, timeStamp, pool, userHash, rowId]; } text() { const pData = this.pDataArray.join(","); return `${this.content}`; } static parse(element) { const pData = element.getAttribute("p"); const [time, type, fontSize, color, timeStamp, pool, userHash, rowId] = pData.split(","); const content = element.innerHTML; return new XmlDanmaku({ content, time, type, fontSize, color, timeStamp, pool, userHash, rowId }); } } class XmlDanmakuDocument { constructor(xml) { this.xml = xml; const document = new DOMParser().parseFromString(xml, "application/xml").documentElement; this.danmakus = [...document.querySelectorAll("d[p]")].map(it => XmlDanmaku.parse(it)); } } class AssDanmaku extends Danmaku { constructor({ content, time, type, fontSize, color, typeTag, colorTag, endTime }) { super(content, time, type, fontSize, color); this.typeTag = typeTag; this.colorTag = colorTag; this.endTime = endTime; } text(fontStyles) { const styleName = fontStyles[this.fontSize].match(/Style:(.*?),/)[1].trim(); return `Dialogue: 0,${this.time},${this.endTime},${styleName},,0,0,0,,{${this.typeTag}${this.colorTag}}${this.content}`; } } class AssDanmakuDocument { constructor({ danmakus, title, fontStyles, blockTypes, resolution }) { this.danmakus = danmakus; this.title = title; this.fontStyles = fontStyles; this.blockTypes = blockTypes; this.resolution = resolution; } generateAss() { const meta = ` [Script Info] ; Script generated by Bilibili Evolved Danmaku Converter ; https://github.com/the1812/Bilibili-Evolved/ Title: ${this.title} ScriptType: v4.00+ PlayResX: ${this.resolution.x} PlayResY: ${this.resolution.y} Timer: 10.0000 WrapStyle: 2 ScaledBorderAndShadow: no [V4+ Styles] Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding ${Object.values(this.fontStyles).join("\n")} [Events] Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text `.trim(); return meta + "\n" + this.danmakus .map(it => it.text(this.fontStyles, this.blockTypes)) .filter(it => it !== "").join("\n"); } } class DanmakuStack { constructor(font, resolution, duration, bottomMarginPercent) { this.horizontalDanmakus = []; this.horizontalTrack = []; this.verticalDanmakus = []; this.verticalTrack = []; this.resolution = resolution; this.duration = duration; this.canvas = document.createElement("canvas"); this.context = this.canvas.getContext("2d"); // XML字体大小到实际大小的表 this.fontSizes = { 25: `52px ${font}`, 18: `36px ${font}`, }; this.danmakuType = { 1: "normal", 2: "normal", 3: "normal", 4: "bottom", 5: "top", 6: "reversed", 7: "special", 8: "special", }; this.bottomMarginPercent = bottomMarginPercent; this.margin = 4; this.nextDanmakuDelay = 0.05; this.generateTracks(); } generateTracks() { const height = 52; this.danmakuHeight = height; this.trackHeight = this.margin * 2 + height; this.trackCount = parseInt(fixed(this.resolution.y * (1 - this.bottomMarginPercent) / this.trackHeight, 0)); } getTextSize(danmaku) { this.context.font = this.fontSizes[danmaku.fontSize]; const metrics = this.context.measureText(danmaku.content); const x = metrics.width / 2; return [x, this.danmakuHeight / 2]; } getHorizonalTags(danmaku) { const [x, y] = this.getTextSize(danmaku); const width = x * 2; const time = this.duration(danmaku) * width / (this.resolution.x + width) + this.nextDanmakuDelay; let track = 0; let closestDanmaku = null; const isClosestDanmaku = it => { if (it.track !== track) { return false; } if (it.width < width) // 弹幕比前面的弹幕长 { // 必须等前面弹幕走完 return this.duration(danmaku) * this.resolution.x / (this.resolution.x + width) <= it.end - danmaku.time; //return it.end > danmaku.time; } else { return it.visible > danmaku.time; } }; // 寻找已发送弹幕中可能重叠的 do { closestDanmaku = this.horizontalTrack.find(isClosestDanmaku); track++; } while (closestDanmaku && track <= this.trackCount); // 如果弹幕过多, 此条就不显示了 if (track > this.trackCount) { return `\\pos(0,-999)`; } track--; // 减回最后的自增 this.horizontalTrack.push({ width: width, start: danmaku.time, visible: danmaku.time + time, end: danmaku.time + this.duration(danmaku), track: track }); return `\\move(${this.resolution.x + x},${track * this.trackHeight + this.margin + y},${-x},${track * this.trackHeight + this.margin + y},0,${this.duration(danmaku) * 1000})`; } getVerticalTags(danmaku) { const [, y] = this.getTextSize(danmaku); const isTop = this.danmakuType[danmaku.type] === "top"; let closestDanmaku = null; let track = isTop ? 0 : this.trackCount - 1; const nextTrack = isTop ? 1 : -1; const isClosestDanmaku = it => { if (it.track !== track) { return false; } return it.end > danmaku.time; }; do { closestDanmaku = this.verticalTrack.find(isClosestDanmaku); track += nextTrack; } while (closestDanmaku && track <= this.trackCount && track >= 0); if (track > this.trackCount || track < 0) { return `\\pos(0,-999)`; } track -= nextTrack; this.verticalTrack.push({ start: danmaku.time, end: danmaku.time + this.duration(danmaku), track: track }); if (isTop) { return `\\pos(${this.resolution.x / 2},${track * this.trackHeight + this.margin + y})`; } else { return `\\pos(${this.resolution.x / 2},${this.resolution.y - this.margin - y - (this.trackCount - 1 - track) * this.trackHeight})`; } } push(danmaku) { let tags = null; let stack = null; switch (this.danmakuType[danmaku.type]) { case "normal": case "reversed": // 反向先鸽了, 直接当正向了 { tags = this.getHorizonalTags(danmaku); stack = this.horizontalDanmakus; break; } case "top": case "bottom": { tags = this.getVerticalTags(danmaku); stack = this.verticalDanmakus; break; } case "special": // 高级弹幕也鸽了先 default: { return { tags: `\\pos(0,-999)`, }; } } const info = { tags }; stack.push(info); return info; } } class DanmakuConverter { constructor({ title, font, alpha, duration, blockTypes, resolution, bottomMarginPercent }) { this.title = title; this.font = font; this.alpha = Math.round(alpha * 100); this.duration = duration; this.blockTypes = blockTypes; this.resolution = resolution; this.danmakuStack = new DanmakuStack(font, resolution, duration, bottomMarginPercent); } get fontStyles() { return { 25: `Style: Medium,${this.font},52,&H${this.alpha}FFFFFF,&H${this.alpha}FFFFFF,&H${this.alpha}000000,&H${this.alpha}000000,0,0,0,0,100,100,0,0,1,1,0,5,0,0,0,0`, 18: `Style: Small,${this.font},36,&H${this.alpha}FFFFFF,&H${this.alpha}FFFFFF,&H${this.alpha}000000,&H${this.alpha}000000,0,0,0,0,100,100,0,0,1,1,0,5,0,0,0,0`, }; } convertToAssDocument(xml) { const xmlDanmakuDocument = new XmlDanmakuDocument(xml); const assDanmakus = []; for (const xmlDanmaku of xmlDanmakuDocument.danmakus.sort((a, b) => a.time - b.time)) { // 跳过设置为屏蔽的弹幕类型 if (this.blockTypes.indexOf(xmlDanmaku.type) !== -1) { continue; } const [startTime, endTime] = this.convertTime(xmlDanmaku.time, this.duration(xmlDanmaku)); assDanmakus.push(new AssDanmaku({ content: this.convertText(xmlDanmaku.content), time: startTime, endTime: endTime, type: xmlDanmaku.type, fontSize: xmlDanmaku.fontSize, color: xmlDanmaku.color, typeTag: this.convertType(xmlDanmaku), colorTag: this.convertColor(xmlDanmaku.color), })); } return new AssDanmakuDocument({ danmakus: assDanmakus, title: this.title, blockTypes: this.blockTypes, fontStyles: this.fontStyles, resolution: this.resolution }); } convertText(text) { const map = { "{": "{", "}": "}", "&": "&", "<": "<", ">": ">", """: '"', "'": "'", }; for (const [key, value] of Object.entries(map)) { text = text.replace(new RegExp(key, "g"), value); } return text; } convertType(danmaku) { return this.danmakuStack.push(danmaku).tags; } convertColor(decColor) { const white = 16777215; if (decColor === white) { return ""; } const hex = decColor.toString(16); const red = hex.substring(0, 2); const green = hex.substring(2, 4); const blue = hex.substring(4, 6); return `\\c&H${blue}${green}${red}&`; } convertTime(startTime, duration) { function round(number) { const [integer, decimal = "00"] = String(number).split("."); return `${integer.padStart(2, "0")}.${decimal.substr(0, 2).padEnd(2, "0")}`; } function secondsToTime(seconds) { let hours = 0; let minutes = 0; while (seconds >= 60) { seconds -= 60; minutes++; } while (minutes >= 60) { minutes -= 60; hours++; } return `${hours}:${String(minutes).padStart(2, "0")}:${round(seconds)}`; } return [secondsToTime(startTime), secondsToTime(startTime + duration)]; } } return { export: { AssDanmaku, AssDanmakuDocument, Danmaku, DanmakuConverter, DanmakuStack, XmlDanmaku, XmlDanmakuDocument, }, }; }; })();