From cadc662ebd83a247d2a899335a0fe253d6a9de21 Mon Sep 17 00:00:00 2001 From: weizhenye Date: Mon, 2 Sep 2024 01:43:29 +0800 Subject: [PATCH] 0.1.2 --- dist/ass.global.js | 1292 ++++++++++++++++++++++------------------ dist/ass.global.min.js | 2 +- dist/ass.js | 1292 ++++++++++++++++++++++------------------ dist/ass.min.js | 2 +- package.json | 2 +- 5 files changed, 1411 insertions(+), 1179 deletions(-) diff --git a/dist/ass.global.js b/dist/ass.global.js index dc1d84f..b784a89 100644 --- a/dist/ass.global.js +++ b/dist/ass.global.js @@ -2,7 +2,7 @@ var ASS = (function () { 'use strict'; function parseEffect(text) { - const param = text + var param = text .toLowerCase() .trim() .split(/\s*;\s*/); @@ -30,7 +30,7 @@ var ASS = (function () { } function parseDrawing(text) { - if (!text) return []; + if (!text) { return []; } return text .toLowerCase() // numbers @@ -40,26 +40,29 @@ var ASS = (function () { .trim() .replace(/\s+/g, ' ') .split(/\s(?=[mnlbspc])/) - .map((cmd) => ( + .map(function (cmd) { return ( cmd.split(' ') - .filter((x, i) => !(i && Number.isNaN(x * 1))) - )); + .filter(function (x, i) { return !(i && isNaN(x * 1)); }) + ); }); } - const numTags = [ + var numTags = [ 'b', 'i', 'u', 's', 'fsp', 'k', 'K', 'kf', 'ko', 'kt', 'fe', 'q', 'p', 'pbo', 'a', 'an', 'fscx', 'fscy', 'fax', 'fay', 'frx', 'fry', 'frz', 'fr', - 'be', 'blur', 'bord', 'xbord', 'ybord', 'shad', 'xshad', 'yshad', - ]; + 'be', 'blur', 'bord', 'xbord', 'ybord', 'shad', 'xshad', 'yshad' ]; - const numRegexs = numTags.map((nt) => ({ name: nt, regex: new RegExp(`^${nt}-?\\d`) })); + var numRegexs = numTags.map(function (nt) { return ({ name: nt, regex: new RegExp(("^" + nt + "-?\\d")) }); }); function parseTag(text) { - const tag = {}; - for (let i = 0; i < numRegexs.length; i++) { - const { name, regex } = numRegexs[i]; + var assign; + + var tag = {}; + for (var i = 0; i < numRegexs.length; i++) { + var ref = numRegexs[i]; + var name = ref.name; + var regex = ref.regex; if (regex.test(text)) { tag[name] = text.slice(name.length) * 1; return tag; @@ -72,22 +75,28 @@ var ASS = (function () { } else if (/^fs[\d+-]/.test(text)) { tag.fs = text.slice(2); } else if (/^\d?c&?H?[0-9a-fA-F]+|^\d?c$/.test(text)) { - const [, num, color] = text.match(/^(\d?)c&?H?(\w*)/); - tag[`c${num || 1}`] = color && `000000${color}`.slice(-6); + var ref$1 = text.match(/^(\d?)c&?H?(\w*)/); + var num = ref$1[1]; + var color = ref$1[2]; + tag[("c" + (num || 1))] = color && ("000000" + color).slice(-6); } else if (/^\da&?H?[0-9a-fA-F]+/.test(text)) { - const [, num, alpha] = text.match(/^(\d)a&?H?([0-9a-f]+)/i); - tag[`a${num}`] = `00${alpha}`.slice(-2); + var ref$2 = text.match(/^(\d)a&?H?([0-9a-f]+)/i); + var num$1 = ref$2[1]; + var alpha = ref$2[2]; + tag[("a" + num$1)] = ("00" + alpha).slice(-2); } else if (/^alpha&?H?[0-9a-fA-F]+/.test(text)) { - [, tag.alpha] = text.match(/^alpha&?H?([0-9a-f]+)/i); - tag.alpha = `00${tag.alpha}`.slice(-2); + (assign = text.match(/^alpha&?H?([0-9a-f]+)/i), tag.alpha = assign[1]); + tag.alpha = ("00" + (tag.alpha)).slice(-2); } else if (/^(?:pos|org|move|fad|fade)\([^)]+/.test(text)) { - const [, key, value] = text.match(/^(\w+)\((.*?)\)?$/); + var ref$3 = text.match(/^(\w+)\((.*?)\)?$/); + var key = ref$3[1]; + var value = ref$3[2]; tag[key] = value .trim() .split(/\s*,\s*/) .map(Number); } else if (/^i?clip\([^)]+/.test(text)) { - const p = text + var p = text .match(/^i?clip\((.*?)\)?$/)[1] .trim() .split(/\s*,\s*/); @@ -108,33 +117,33 @@ var ASS = (function () { tag.clip.dots = p.map(Number); } } else if (/^t\(/.test(text)) { - const p = text + var p$1 = text .match(/^t\((.*?)\)?$/)[1] .trim() - .replace(/\\.*/, (x) => x.replace(/,/g, '\n')) + .replace(/\\.*/, function (x) { return x.replace(/,/g, '\n'); }) .split(/\s*,\s*/); - if (!p[0]) return tag; + if (!p$1[0]) { return tag; } tag.t = { t1: 0, t2: 0, accel: 1, - tags: p[p.length - 1] + tags: p$1[p$1.length - 1] .replace(/\n/g, ',') .split('\\') .slice(1) .map(parseTag), }; - if (p.length === 2) { - tag.t.accel = p[0] * 1; + if (p$1.length === 2) { + tag.t.accel = p$1[0] * 1; } - if (p.length === 3) { - tag.t.t1 = p[0] * 1; - tag.t.t2 = p[1] * 1; + if (p$1.length === 3) { + tag.t.t1 = p$1[0] * 1; + tag.t.t2 = p$1[1] * 1; } - if (p.length === 4) { - tag.t.t1 = p[0] * 1; - tag.t.t2 = p[1] * 1; - tag.t.accel = p[2] * 1; + if (p$1.length === 4) { + tag.t.t1 = p$1[0] * 1; + tag.t.t2 = p$1[1] * 1; + tag.t.accel = p$1[2] * 1; } } @@ -142,17 +151,17 @@ var ASS = (function () { } function parseTags(text) { - const tags = []; - let depth = 0; - let str = ''; + var tags = []; + var depth = 0; + var str = ''; // `\b\c` -> `b\c\` // `a\b\c` -> `b\c\` - const transText = text.split('\\').slice(1).concat('').join('\\'); - for (let i = 0; i < transText.length; i++) { - const x = transText[i]; - if (x === '(') depth++; - if (x === ')') depth--; - if (depth < 0) depth = 0; + var transText = text.split('\\').slice(1).concat('').join('\\'); + for (var i = 0; i < transText.length; i++) { + var x = transText[i]; + if (x === '(') { depth++; } + if (x === ')') { depth--; } + if (depth < 0) { depth = 0; } if (!depth && x === '\\') { if (str) { tags.push(str); @@ -166,44 +175,44 @@ var ASS = (function () { } function parseText(text) { - const pairs = text.split(/{([^{}]*?)}/); - const parsed = []; + var pairs = text.split(/{([^{}]*?)}/); + var parsed = []; if (pairs[0].length) { parsed.push({ tags: [], text: pairs[0], drawing: [] }); } - for (let i = 1; i < pairs.length; i += 2) { - const tags = parseTags(pairs[i]); - const isDrawing = tags.reduce((v, tag) => (tag.p === undefined ? v : !!tag.p), false); + for (var i = 1; i < pairs.length; i += 2) { + var tags = parseTags(pairs[i]); + var isDrawing = tags.reduce(function (v, tag) { return (tag.p === undefined ? v : !!tag.p); }, false); parsed.push({ - tags, + tags: tags, text: isDrawing ? '' : pairs[i + 1], drawing: isDrawing ? parseDrawing(pairs[i + 1]) : [], }); } return { raw: text, - combined: parsed.map((frag) => frag.text).join(''), - parsed, + combined: parsed.map(function (frag) { return frag.text; }).join(''), + parsed: parsed, }; } function parseTime(time) { - const t = time.split(':'); + var t = time.split(':'); return t[0] * 3600 + t[1] * 60 + t[2] * 1; } function parseDialogue(text, format) { - let fields = text.split(','); + var fields = text.split(','); if (fields.length > format.length) { - const textField = fields.slice(format.length - 1).join(); + var textField = fields.slice(format.length - 1).join(); fields = fields.slice(0, format.length - 1); fields.push(textField); } - const dia = {}; - for (let i = 0; i < fields.length; i++) { - const fmt = format[i]; - const fld = fields[i].trim(); + var dia = {}; + for (var i = 0; i < fields.length; i++) { + var fmt = format[i]; + var fld = fields[i].trim(); switch (fmt) { case 'Layer': case 'MarginL': @@ -229,45 +238,51 @@ var ASS = (function () { return dia; } - const stylesFormat = ['Name', 'Fontname', 'Fontsize', 'PrimaryColour', 'SecondaryColour', 'OutlineColour', 'BackColour', 'Bold', 'Italic', 'Underline', 'StrikeOut', 'ScaleX', 'ScaleY', 'Spacing', 'Angle', 'BorderStyle', 'Outline', 'Shadow', 'Alignment', 'MarginL', 'MarginR', 'MarginV', 'Encoding']; - const eventsFormat = ['Layer', 'Start', 'End', 'Style', 'Name', 'MarginL', 'MarginR', 'MarginV', 'Effect', 'Text']; + var stylesFormat = ['Name', 'Fontname', 'Fontsize', 'PrimaryColour', 'SecondaryColour', 'OutlineColour', 'BackColour', 'Bold', 'Italic', 'Underline', 'StrikeOut', 'ScaleX', 'ScaleY', 'Spacing', 'Angle', 'BorderStyle', 'Outline', 'Shadow', 'Alignment', 'MarginL', 'MarginR', 'MarginV', 'Encoding']; + var eventsFormat = ['Layer', 'Start', 'End', 'Style', 'Name', 'MarginL', 'MarginR', 'MarginV', 'Effect', 'Text']; function parseFormat(text) { - const fields = stylesFormat.concat(eventsFormat); + var fields = stylesFormat.concat(eventsFormat); return text.match(/Format\s*:\s*(.*)/i)[1] .split(/\s*,\s*/) - .map((field) => { - const caseField = fields.find((f) => f.toLowerCase() === field.toLowerCase()); + .map(function (field) { + var caseField = fields.find(function (f) { return f.toLowerCase() === field.toLowerCase(); }); return caseField || field; }); } function parseStyle(text, format) { - const values = text.match(/Style\s*:\s*(.*)/i)[1].split(/\s*,\s*/); - return Object.assign({}, ...format.map((fmt, idx) => ({ [fmt]: values[idx] }))); + var values = text.match(/Style\s*:\s*(.*)/i)[1].split(/\s*,\s*/); + return Object.assign.apply(Object, [ {} ].concat( format.map(function (fmt, idx) { + var obj; + + return (( obj = {}, obj[fmt] = values[idx], obj )); + }) )); } function parse(text) { - const tree = { + var tree = { info: {}, styles: { format: [], style: [] }, events: { format: [], comment: [], dialogue: [] }, }; - const lines = text.split(/\r?\n/); - let state = 0; - for (let i = 0; i < lines.length; i++) { - const line = lines[i].trim(); - if (/^;/.test(line)) continue; - - if (/^\[Script Info\]/i.test(line)) state = 1; - else if (/^\[V4\+? Styles\]/i.test(line)) state = 2; - else if (/^\[Events\]/i.test(line)) state = 3; - else if (/^\[.*\]/.test(line)) state = 0; - - if (state === 0) continue; + var lines = text.split(/\r?\n/); + var state = 0; + for (var i = 0; i < lines.length; i++) { + var line = lines[i].trim(); + if (/^;/.test(line)) { continue; } + + if (/^\[Script Info\]/i.test(line)) { state = 1; } + else if (/^\[V4\+? Styles\]/i.test(line)) { state = 2; } + else if (/^\[Events\]/i.test(line)) { state = 3; } + else if (/^\[.*\]/.test(line)) { state = 0; } + + if (state === 0) { continue; } if (state === 1) { if (/:/.test(line)) { - const [, key, value] = line.match(/(.*?)\s*:\s*(.*)/); + var ref = line.match(/(.*?)\s*:\s*(.*)/); + var key = ref[1]; + var value = ref[2]; tree.info[key] = value; } } @@ -284,8 +299,10 @@ var ASS = (function () { tree.events.format = parseFormat(line); } if (/^(?:Comment|Dialogue)\s*:/i.test(line)) { - const [, key, value] = line.match(/^(\w+?)\s*:\s*(.*)/i); - tree.events[key.toLowerCase()].push(parseDialogue(value, tree.events.format)); + var ref$1 = line.match(/^(\w+?)\s*:\s*(.*)/i); + var key$1 = ref$1[1]; + var value$1 = ref$1[2]; + tree.events[key$1.toLowerCase()].push(parseDialogue(value$1, tree.events.format)); } } } @@ -294,7 +311,7 @@ var ASS = (function () { } function createCommand(arr) { - const cmd = { + var cmd = { type: null, prev: null, next: null, @@ -306,7 +323,7 @@ var ASS = (function () { .replace('N', 'L') .replace('B', 'C'); } - for (let len = arr.length - !(arr.length & 1), i = 1; i < len; i += 2) { + for (var len = arr.length - !(arr.length & 1), i = 1; i < len; i += 2) { cmd.points.push({ x: arr[i] * 1, y: arr[i + 1] * 1 }); } return cmd; @@ -323,19 +340,28 @@ var ASS = (function () { } function getViewBox(commands) { - let minX = Infinity; - let minY = Infinity; - let maxX = -Infinity; - let maxY = -Infinity; - [].concat(...commands.map(({ points }) => points)).forEach(({ x, y }) => { + var ref; + + var minX = Infinity; + var minY = Infinity; + var maxX = -Infinity; + var maxY = -Infinity; + (ref = []).concat.apply(ref, commands.map(function (ref) { + var points = ref.points; + + return points; + })).forEach(function (ref) { + var x = ref.x; + var y = ref.y; + minX = Math.min(minX, x); minY = Math.min(minY, y); maxX = Math.max(maxX, x); maxY = Math.max(maxY, y); }); return { - minX, - minY, + minX: minX, + minY: minY, width: maxX - minX, height: maxY - minY, }; @@ -350,18 +376,18 @@ var ASS = (function () { * @return {Array} converted commands */ function s2b(points, prev, next) { - const results = []; - const bb1 = [0, 2 / 3, 1 / 3, 0]; - const bb2 = [0, 1 / 3, 2 / 3, 0]; - const bb3 = [0, 1 / 6, 2 / 3, 1 / 6]; - const dot4 = (a, b) => (a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3]); - let px = [points[points.length - 1].x, points[0].x, points[1].x, points[2].x]; - let py = [points[points.length - 1].y, points[0].y, points[1].y, points[2].y]; + var results = []; + var bb1 = [0, 2 / 3, 1 / 3, 0]; + var bb2 = [0, 1 / 3, 2 / 3, 0]; + var bb3 = [0, 1 / 6, 2 / 3, 1 / 6]; + var dot4 = function (a, b) { return (a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3]); }; + var px = [points[points.length - 1].x, points[0].x, points[1].x, points[2].x]; + var py = [points[points.length - 1].y, points[0].y, points[1].y, points[2].y]; results.push({ type: prev === 'M' ? 'M' : 'L', points: [{ x: dot4(bb3, px), y: dot4(bb3, py) }], }); - for (let i = 3; i < points.length; i++) { + for (var i = 3; i < points.length; i++) { px = [points[i - 3].x, points[i - 2].x, points[i - 1].x, points[i].x]; py = [points[i - 3].y, points[i - 2].y, points[i - 1].y, points[i].y]; results.push({ @@ -369,33 +395,46 @@ var ASS = (function () { points: [ { x: dot4(bb1, px), y: dot4(bb1, py) }, { x: dot4(bb2, px), y: dot4(bb2, py) }, - { x: dot4(bb3, px), y: dot4(bb3, py) }, - ], + { x: dot4(bb3, px), y: dot4(bb3, py) } ], }); } if (next === 'L' || next === 'C') { - const last = points[points.length - 1]; + var last = points[points.length - 1]; results.push({ type: 'L', points: [{ x: last.x, y: last.y }] }); } return results; } function toSVGPath(instructions) { - return instructions.map(({ type, points }) => ( - type + points.map(({ x, y }) => `${x},${y}`).join(',') - )).join(''); + return instructions.map(function (ref) { + var type = ref.type; + var points = ref.points; + + return ( + type + points.map(function (ref) { + var x = ref.x; + var y = ref.y; + + return (x + "," + y); + }).join(',') + ); + }).join(''); } function compileDrawing(rawCommands) { - const commands = []; - let i = 0; + var ref$1; + + var commands = []; + var i = 0; while (i < rawCommands.length) { - const arr = rawCommands[i]; - const cmd = createCommand(arr); + var arr = rawCommands[i]; + var cmd = createCommand(arr); if (isValid(cmd)) { if (cmd.type === 'S') { - const { x, y } = (commands[i - 1] || { points: [{ x: 0, y: 0 }] }).points.slice(-1)[0]; - cmd.points.unshift({ x, y }); + var ref = (commands[i - 1] || { points: [{ x: 0, y: 0 }] }).points.slice(-1)[0]; + var x = ref.x; + var y = ref.y; + cmd.points.unshift({ x: x, y: y }); } if (i) { cmd.prev = commands[i - 1].type; @@ -405,68 +444,100 @@ var ASS = (function () { i++; } else { if (i && commands[i - 1].type === 'S') { - const additionPoints = { + var additionPoints = { p: cmd.points, c: commands[i - 1].points.slice(0, 3), }; commands[i - 1].points = commands[i - 1].points.concat( - (additionPoints[arr[0]] || []).map(({ x, y }) => ({ x, y })), + (additionPoints[arr[0]] || []).map(function (ref) { + var x = ref.x; + var y = ref.y; + + return ({ x: x, y: y }); + }) ); } rawCommands.splice(i, 1); } } - const instructions = [].concat( - ...commands.map(({ type, points, prev, next }) => ( + var instructions = (ref$1 = []).concat.apply( + ref$1, commands.map(function (ref) { + var type = ref.type; + var points = ref.points; + var prev = ref.prev; + var next = ref.next; + + return ( type === 'S' ? s2b(points, prev, next) - : { type, points } - )), + : { type: type, points: points } + ); + }) ); - return Object.assign({ instructions, d: toSVGPath(instructions) }, getViewBox(commands)); + return Object.assign({ instructions: instructions, d: toSVGPath(instructions) }, getViewBox(commands)); } - const tTags = [ + var tTags = [ 'fs', 'fsp', 'clip', 'c1', 'c2', 'c3', 'c4', 'a1', 'a2', 'a3', 'a4', 'alpha', 'fscx', 'fscy', 'fax', 'fay', 'frx', 'fry', 'frz', 'fr', - 'be', 'blur', 'bord', 'xbord', 'ybord', 'shad', 'xshad', 'yshad', - ]; + 'be', 'blur', 'bord', 'xbord', 'ybord', 'shad', 'xshad', 'yshad' ]; + + function compileTag(tag, key, presets) { + var obj, obj$1, obj$2; - function compileTag(tag, key, presets = {}) { - let value = tag[key]; + if ( presets === void 0 ) presets = {}; + var value = tag[key]; if (value === undefined) { return null; } if (key === 'pos' || key === 'org') { - return value.length === 2 ? { [key]: { x: value[0], y: value[1] } } : null; + return value.length === 2 ? ( obj = {}, obj[key] = { x: value[0], y: value[1] }, obj ) : null; } if (key === 'move') { - const [x1, y1, x2, y2, t1 = 0, t2 = 0] = value; + var x1 = value[0]; + var y1 = value[1]; + var x2 = value[2]; + var y2 = value[3]; + var t1 = value[4]; if ( t1 === void 0 ) t1 = 0; + var t2 = value[5]; if ( t2 === void 0 ) t2 = 0; return value.length === 4 || value.length === 6 - ? { move: { x1, y1, x2, y2, t1, t2 } } + ? { move: { x1: x1, y1: y1, x2: x2, y2: y2, t1: t1, t2: t2 } } : null; } if (key === 'fad' || key === 'fade') { if (value.length === 2) { - const [t1, t2] = value; - return { fade: { type: 'fad', t1, t2 } }; + var t1$1 = value[0]; + var t2$1 = value[1]; + return { fade: { type: 'fad', t1: t1$1, t2: t2$1 } }; } if (value.length === 7) { - const [a1, a2, a3, t1, t2, t3, t4] = value; - return { fade: { type: 'fade', a1, a2, a3, t1, t2, t3, t4 } }; + var a1 = value[0]; + var a2 = value[1]; + var a3 = value[2]; + var t1$2 = value[3]; + var t2$2 = value[4]; + var t3 = value[5]; + var t4 = value[6]; + return { fade: { type: 'fade', a1: a1, a2: a2, a3: a3, t1: t1$2, t2: t2$2, t3: t3, t4: t4 } }; } return null; } if (key === 'clip') { - const { inverse, scale, drawing, dots } = value; + var inverse = value.inverse; + var scale = value.scale; + var drawing = value.drawing; + var dots = value.dots; if (drawing) { - return { clip: { inverse, scale, drawing: compileDrawing(drawing), dots } }; + return { clip: { inverse: inverse, scale: scale, drawing: compileDrawing(drawing), dots: dots } }; } if (dots) { - const [x1, y1, x2, y2] = dots; - return { clip: { inverse, scale, drawing, dots: { x1, y1, x2, y2 } } }; + var x1$1 = dots[0]; + var y1$1 = dots[1]; + var x2$1 = dots[2]; + var y2$1 = dots[3]; + return { clip: { inverse: inverse, scale: scale, drawing: drawing, dots: { x1: x1$1, y1: y1$1, x2: x2$1, y2: y2$1 } } }; } return null; } @@ -480,7 +551,7 @@ var ASS = (function () { return { xshad: value, yshad: value }; } if (/^c\d$/.test(key)) { - return { [key]: value || presets[key] }; + return ( obj$1 = {}, obj$1[key] = value || presets[key], obj$1 ); } if (key === 'alpha') { return { a1: value, a2: value, a3: value, a4: value }; @@ -499,27 +570,28 @@ var ASS = (function () { return { kf: value }; } if (key === 't') { - const { t1, accel, tags } = value; - const t2 = value.t2 || (presets.end - presets.start) * 1e3; - const compiledTag = {}; - tags.forEach((t) => { - const k = Object.keys(t)[0]; + var t1$3 = value.t1; + var accel = value.accel; + var tags = value.tags; + var t2$3 = value.t2 || (presets.end - presets.start) * 1e3; + var compiledTag = {}; + tags.forEach(function (t) { + var k = Object.keys(t)[0]; if (~tTags.indexOf(k) && !(k === 'clip' && !t[k].dots)) { Object.assign(compiledTag, compileTag(t, k, presets)); } }); - return { t: { t1, t2, accel, tag: compiledTag } }; + return { t: { t1: t1$3, t2: t2$3, accel: accel, tag: compiledTag } }; } - return { [key]: value }; + return ( obj$2 = {}, obj$2[key] = value, obj$2 ); } - const a2an = [ + var a2an = [ null, 1, 2, 3, null, 7, 8, 9, - null, 4, 5, 6, - ]; + null, 4, 5, 6 ]; - const globalTags = ['r', 'a', 'an', 'pos', 'org', 'move', 'fade', 'fad', 'clip']; + var globalTags = ['r', 'a', 'an', 'pos', 'org', 'move', 'fade', 'fad', 'clip']; function inheritTag(pTag) { return JSON.parse(JSON.stringify(Object.assign({}, pTag, { @@ -530,42 +602,56 @@ var ASS = (function () { }))); } - function compileText({ styles, style, parsed, start, end }) { - let alignment; - let pos; - let org; - let move; - let fade; - let clip; - const slices = []; - let slice = { style, fragments: [] }; - let prevTag = {}; - for (let i = 0; i < parsed.length; i++) { - const { tags, text, drawing } = parsed[i]; - let reset; - for (let j = 0; j < tags.length; j++) { - const tag = tags[j]; + function compileText(ref) { + var styles = ref.styles; + var style = ref.style; + var parsed = ref.parsed; + var start = ref.start; + var end = ref.end; + + var alignment; + var q = { q: styles[style].tag.q }; + var pos; + var org; + var move; + var fade; + var clip; + var slices = []; + var slice = { style: style, fragments: [] }; + var prevTag = {}; + for (var i = 0; i < parsed.length; i++) { + var ref$1 = parsed[i]; + var tags = ref$1.tags; + var text = ref$1.text; + var drawing = ref$1.drawing; + var reset = (void 0); + for (var j = 0; j < tags.length; j++) { + var tag = tags[j]; reset = tag.r === undefined ? reset : tag.r; } - const fragment = { + var fragment = { tag: reset === undefined ? inheritTag(prevTag) : {}, - text, + text: text, drawing: drawing.length ? compileDrawing(drawing) : null, }; - for (let j = 0; j < tags.length; j++) { - const tag = tags[j]; - alignment = alignment || a2an[tag.a || 0] || tag.an; - pos = pos || compileTag(tag, 'pos'); - org = org || compileTag(tag, 'org'); - move = move || compileTag(tag, 'move'); - fade = fade || compileTag(tag, 'fade') || compileTag(tag, 'fad'); - clip = compileTag(tag, 'clip') || clip; - const key = Object.keys(tag)[0]; + for (var j$1 = 0; j$1 < tags.length; j$1++) { + var tag$1 = tags[j$1]; + alignment = alignment || a2an[tag$1.a || 0] || tag$1.an; + q = compileTag(tag$1, 'q') || q; + pos = pos || compileTag(tag$1, 'pos'); + org = org || compileTag(tag$1, 'org'); + move = move || compileTag(tag$1, 'move'); + fade = fade || compileTag(tag$1, 'fade') || compileTag(tag$1, 'fad'); + clip = compileTag(tag$1, 'clip') || clip; + var key = Object.keys(tag$1)[0]; if (key && !~globalTags.indexOf(key)) { - const sliceTag = styles[style].tag; - const { c1, c2, c3, c4 } = sliceTag; - const fs = prevTag.fs || sliceTag.fs; - const compiledTag = compileTag(tag, key, { start, end, c1, c2, c3, c4, fs }); + var sliceTag = styles[style].tag; + var c1 = sliceTag.c1; + var c2 = sliceTag.c2; + var c3 = sliceTag.c3; + var c4 = sliceTag.c4; + var fs = prevTag.fs || sliceTag.fs; + var compiledTag = compileTag(tag$1, key, { start: start, end: end, c1: c1, c2: c2, c3: c3, c4: c4, fs: fs }); if (key === 't') { fragment.tag.t = fragment.tag.t || []; fragment.tag.t.push(compiledTag.t); @@ -580,7 +666,7 @@ var ASS = (function () { slice = { style: styles[reset] ? reset : style, fragments: [] }; } if (fragment.text || fragment.drawing) { - const prev = slice.fragments[slice.fragments.length - 1] || {}; + var prev = slice.fragments[slice.fragments.length - 1] || {}; if (prev.text && fragment.text && !Object.keys(fragment.tag).length) { // merge fragment to previous if its tag is empty prev.text += fragment.text; @@ -591,29 +677,32 @@ var ASS = (function () { } slices.push(slice); - return Object.assign({ alignment, slices }, pos, org, move, fade, clip); + return Object.assign({ alignment: alignment, slices: slices }, q, pos, org, move, fade, clip); } - function compileDialogues({ styles, dialogues }) { - let minLayer = Infinity; - const results = []; - for (let i = 0; i < dialogues.length; i++) { - const dia = dialogues[i]; + function compileDialogues(ref) { + var styles = ref.styles; + var dialogues = ref.dialogues; + + var minLayer = Infinity; + var results = []; + for (var i = 0; i < dialogues.length; i++) { + var dia = dialogues[i]; if (dia.Start >= dia.End) { continue; } if (!styles[dia.Style]) { dia.Style = 'Default'; } - const stl = styles[dia.Style].style; - const compiledText = compileText({ - styles, + var stl = styles[dia.Style].style; + var compiledText = compileText({ + styles: styles, style: dia.Style, parsed: dia.Text.parsed, start: dia.Start, end: dia.End, }); - const alignment = compiledText.alignment || stl.Alignment; + var alignment = compiledText.alignment || stl.Alignment; minLayer = Math.min(minLayer, dia.Layer); results.push(Object.assign({ layer: dia.Layer, @@ -628,17 +717,17 @@ var ASS = (function () { vertical: dia.MarginV || stl.MarginV, }, effect: dia.Effect, - }, compiledText, { alignment })); + }, compiledText, { alignment: alignment })); } - for (let i = 0; i < results.length; i++) { - results[i].layer -= minLayer; + for (var i$1 = 0; i$1 < results.length; i$1++) { + results[i$1].layer -= minLayer; } - return results.sort((a, b) => a.start - b.start || a.end - b.end); + return results.sort(function (a, b) { return a.start - b.start || a.end - b.end; }); } // same as Aegisub // https://github.com/Aegisub/Aegisub/blob/master/src/ass_style.h - const DEFAULT_STYLE = { + var DEFAULT_STYLE = { Name: 'Default', Fontname: 'Arial', Fontsize: '20', @@ -670,53 +759,67 @@ var ASS = (function () { */ function parseStyleColor(color) { if (/^(&|H|&H)[0-9a-f]{6,}/i.test(color)) { - const [, a, c] = color.match(/&?H?([0-9a-f]{2})?([0-9a-f]{6})/i); + var ref = color.match(/&?H?([0-9a-f]{2})?([0-9a-f]{6})/i); + var a = ref[1]; + var c = ref[2]; return [a || '00', c]; } - const num = parseInt(color, 10); - if (!Number.isNaN(num)) { - const min = -2147483648; - const max = 2147483647; + var num = parseInt(color, 10); + if (!isNaN(num)) { + var min = -2147483648; + var max = 2147483647; if (num < min) { return ['00', '000000']; } - const aabbggrr = (min <= num && num <= max) - ? `00000000${(num < 0 ? num + 4294967296 : num).toString(16)}`.slice(-8) + var aabbggrr = (min <= num && num <= max) + ? ("00000000" + ((num < 0 ? num + 4294967296 : num).toString(16))).slice(-8) : String(num).slice(0, 8); return [aabbggrr.slice(0, 2), aabbggrr.slice(2)]; } return ['00', '000000']; } - function compileStyles({ info, style, defaultStyle }) { - const result = {}; - const styles = [Object.assign({}, defaultStyle, { Name: 'Default' })].concat(style); - for (let i = 0; i < styles.length; i++) { - const s = Object.assign({}, DEFAULT_STYLE, styles[i]); + function compileStyles(ref) { + var info = ref.info; + var style = ref.style; + var defaultStyle = ref.defaultStyle; + + var result = {}; + var styles = [Object.assign({}, defaultStyle, { Name: 'Default' })].concat(style); + var loop = function ( i ) { + var s = Object.assign({}, DEFAULT_STYLE, styles[i]); // this behavior is same as Aegisub by black-box testing if (/^(\*+)Default$/.test(s.Name)) { s.Name = 'Default'; } - Object.keys(s).forEach((key) => { + Object.keys(s).forEach(function (key) { if (key !== 'Name' && key !== 'Fontname' && !/Colour/.test(key)) { s[key] *= 1; } }); - const [a1, c1] = parseStyleColor(s.PrimaryColour); - const [a2, c2] = parseStyleColor(s.SecondaryColour); - const [a3, c3] = parseStyleColor(s.OutlineColour); - const [a4, c4] = parseStyleColor(s.BackColour); - const tag = { + var ref$1 = parseStyleColor(s.PrimaryColour); + var a1 = ref$1[0]; + var c1 = ref$1[1]; + var ref$2 = parseStyleColor(s.SecondaryColour); + var a2 = ref$2[0]; + var c2 = ref$2[1]; + var ref$3 = parseStyleColor(s.OutlineColour); + var a3 = ref$3[0]; + var c3 = ref$3[1]; + var ref$4 = parseStyleColor(s.BackColour); + var a4 = ref$4[0]; + var c4 = ref$4[1]; + var tag = { fn: s.Fontname, fs: s.Fontsize, - c1, - a1, - c2, - a2, - c3, - a3, - c4, - a4, + c1: c1, + a1: a1, + c2: c2, + a2: a2, + c3: c3, + a3: a3, + c4: c4, + a4: a4, b: Math.abs(s.Bold), i: Math.abs(s.Italic), u: Math.abs(s.Underline), @@ -730,16 +833,21 @@ var ASS = (function () { xshad: s.Shadow, yshad: s.Shadow, fe: s.Encoding, + // TODO: [breaking change] remove `q` from style q: /^[0-3]$/.test(info.WrapStyle) ? info.WrapStyle * 1 : 2, }; - result[s.Name] = { style: s, tag }; - } + result[s.Name] = { style: s, tag: tag }; + }; + + for (var i = 0; i < styles.length; i++) loop( i ); return result; } - function compile(text, options = {}) { - const tree = parse(text); - const styles = compileStyles({ + function compile(text, options) { + if ( options === void 0 ) options = {}; + + var tree = parse(text); + var styles = compileStyles({ info: tree.info, style: tree.styles.style, defaultStyle: options.defaultStyle || {}, @@ -748,15 +856,50 @@ var ASS = (function () { info: tree.info, width: tree.info.PlayResX * 1 || null, height: tree.info.PlayResY * 1 || null, + wrapStyle: /^[0-3]$/.test(tree.info.WrapStyle) ? tree.info.WrapStyle * 1 : 2, collisions: tree.info.Collisions || 'Normal', - styles, + styles: styles, dialogues: compileDialogues({ - styles, + styles: styles, dialogues: tree.events.dialogue, }), }; } + // https://github.com/weizhenye/ASS/wiki/Font-Size-in-ASS + + const useTextMetrics = 'fontBoundingBoxAscent' in TextMetrics.prototype; + + // It seems max line-height is 1200px in Firefox. + const isFirefox = navigator.userAgent.toLowerCase().includes('firefox'); + const unitsPerEm = !useTextMetrics && isFirefox ? 512 : 2048; + const lineSpacing = Object.create(null); + + const ctx = document.createElement('canvas').getContext('2d'); + + const $div = document.createElement('div'); + $div.className = 'ASS-fix-font-size'; + $div.style.fontSize = `${unitsPerEm}px`; + const $span = document.createElement('span'); + $span.textContent = '0'; + $div.append($span); + + const $fixFontSize = useTextMetrics ? null : $div; + + function getRealFontSize(fn, fs) { + if (!lineSpacing[fn]) { + if (useTextMetrics) { + ctx.font = `${unitsPerEm}px "${fn}"`; + const tm = ctx.measureText(''); + lineSpacing[fn] = tm.fontBoundingBoxAscent + tm.fontBoundingBoxDescent; + } else { + $span.style.fontFamily = `"${fn}"`; + lineSpacing[fn] = $span.clientHeight; + } + } + return fs * unitsPerEm / lineSpacing[fn]; + } + function alpha2opacity(a) { return 1 - `0x${a}` / 255; } @@ -796,7 +939,7 @@ var ASS = (function () { return $el; } - const GLOBAL_CSS = '.ASS-box{font-family:Arial;overflow:hidden;pointer-events:none;position:absolute}.ASS-dialogue{font-size:0;position:absolute;z-index:0}.ASS-dialogue span{display:inline-block}.ASS-dialogue [data-text]{display:inline-block;color:var(--ass-fill-color);font-size:calc(var(--ass-scale)*var(--ass-real-fs)*1px);line-height:calc(var(--ass-scale)*var(--ass-tag-fs)*1px);letter-spacing:calc(var(--ass-scale)*var(--ass-tag-fsp)*1px)}.ASS-dialogue [data-wrap-style="0"],.ASS-dialogue [data-wrap-style="3"]{text-wrap:balance}.ASS-dialogue [data-wrap-style="1"]{word-break:break-word;white-space:normal}.ASS-dialogue [data-wrap-style="2"]{word-break:normal;white-space:nowrap}.ASS-dialogue [data-border-style="1"]{position:relative}.ASS-dialogue [data-border-style="1"]::after,.ASS-dialogue [data-border-style="1"]::before{content:attr(data-text);position:absolute;top:0;left:0;z-index:-1;filter:blur(calc(var(--ass-tag-blur)*1px))}.ASS-dialogue [data-border-style="1"]::before{color:var(--ass-shadow-color);transform:translate(calc(var(--ass-scale-stroke)*var(--ass-tag-xshad)*1px),calc(var(--ass-scale-stroke)*var(--ass-tag-yshad)*1px));-webkit-text-stroke:var(--ass-border-width) var(--ass-shadow-color);text-shadow:var(--ass-shadow-delta);opacity:var(--ass-shadow-opacity)}.ASS-dialogue [data-border-style="1"]::after{color:transparent;-webkit-text-stroke:var(--ass-border-width) var(--ass-border-color);text-shadow:var(--ass-border-delta);opacity:var(--ass-border-opacity)}.ASS-dialogue [data-border-style="3"]{padding:calc(var(--ass-scale-stroke)*var(--ass-tag-xbord)*1px) calc(var(--ass-scale-stroke)*var(--ass-tag-ybord)*1px);position:relative;filter:blur(calc(var(--ass-tag-blur)*1px))}.ASS-dialogue [data-border-style="3"]::after,.ASS-dialogue [data-border-style="3"]::before{content:"";width:100%;height:100%;position:absolute;z-index:-1}.ASS-dialogue [data-border-style="3"]::before{background-color:var(--ass-shadow-color);left:calc(var(--ass-scale-stroke)*var(--ass-tag-xshad)*1px);top:calc(var(--ass-scale-stroke)*var(--ass-tag-yshad)*1px)}.ASS-dialogue [data-border-style="3"]::after{background-color:var(--ass-border-color);left:0;top:0}@container style(--ass-tag-xbord: 0) and style(--ass-tag-ybord: 0){.ASS-dialogue [data-border-style="3"]::after{background-color:transparent}}@container style(--ass-tag-xshad: 0) and style(--ass-tag-yshad: 0){.ASS-dialogue [data-border-style="3"]::before{background-color:transparent}}.ASS-dialogue [data-rotate]{transform:perspective(312.5px) rotateY(calc(var(--ass-tag-fry)*1deg)) rotateX(calc(var(--ass-tag-frx)*1deg)) rotateZ(calc(var(--ass-tag-frz)*-1deg))}.ASS-dialogue [data-text][data-rotate]{transform-style:preserve-3d;word-break:normal;white-space:nowrap}.ASS-dialogue [data-scale],.ASS-dialogue [data-skew]{display:inline-block;transform:scale(var(--ass-tag-fscx),var(--ass-tag-fscy)) skew(calc(var(--ass-tag-fax)*1rad),calc(var(--ass-tag-fay)*1rad));transform-origin:var(--ass-align-h) var(--ass-align-v)}.ASS-fix-font-size{font-size:2048px;font-family:Arial;line-height:normal;width:0;height:0;position:absolute;visibility:hidden;overflow:hidden}.ASS-clip-area,.ASS-fix-font-size span{position:absolute}.ASS-clip-area{width:100%;height:100%;top:0;left:0}.ASS-scroll-area{position:absolute;width:100%;overflow:hidden}'; + const GLOBAL_CSS = '.ASS-box{font-family:Arial;overflow:hidden;pointer-events:none;position:absolute}.ASS-dialogue{font-size:0;width:max-content;position:absolute;z-index:0;transform:translate(calc(var(--ass-align-h)*-1),calc(var(--ass-align-v)*-1))}.ASS-dialogue span{display:inline-block}.ASS-dialogue [data-text]{display:inline-block;color:var(--ass-fill-color);font-size:calc(var(--ass-scale)*var(--ass-real-fs)*1px);line-height:calc(var(--ass-scale)*var(--ass-tag-fs)*1px);letter-spacing:calc(var(--ass-scale)*var(--ass-tag-fsp)*1px)}.ASS-dialogue[data-wrap-style="0"],.ASS-dialogue[data-wrap-style="3"]{text-wrap:balance;white-space:pre-wrap}.ASS-dialogue[data-wrap-style="1"]{word-break:break-word;white-space:normal}.ASS-dialogue[data-wrap-style="2"]{word-break:normal;white-space:nowrap}.ASS-dialogue [data-border-style="1"]{position:relative;filter:blur(calc(var(--ass-tag-blur)*calc(1 - sign(var(--ass-tag-xbord)))*calc(1 - sign(var(--ass-tag-ybord)))*1px))}.ASS-dialogue [data-border-style="1"]::after,.ASS-dialogue [data-border-style="1"]::before{content:attr(data-text);position:absolute;top:0;left:0;z-index:-1;filter:blur(calc(var(--ass-tag-blur)*1px))}.ASS-dialogue [data-border-style="1"]::before{color:var(--ass-shadow-color);transform:translate(calc(var(--ass-scale-stroke)*var(--ass-tag-xshad)*1px),calc(var(--ass-scale-stroke)*var(--ass-tag-yshad)*1px));-webkit-text-stroke:var(--ass-border-width) var(--ass-shadow-color);text-shadow:var(--ass-shadow-delta);opacity:var(--ass-shadow-opacity)}.ASS-dialogue [data-border-style="1"]::after{color:transparent;-webkit-text-stroke:var(--ass-border-width) var(--ass-border-color);text-shadow:var(--ass-border-delta);opacity:var(--ass-border-opacity)}.ASS-dialogue [data-border-style="3"]{padding:calc(var(--ass-scale-stroke)*var(--ass-tag-xbord)*1px) calc(var(--ass-scale-stroke)*var(--ass-tag-ybord)*1px);position:relative;filter:blur(calc(var(--ass-tag-blur)*1px))}.ASS-dialogue [data-border-style="3"]::after,.ASS-dialogue [data-border-style="3"]::before{content:"";width:100%;height:100%;position:absolute;z-index:-1}.ASS-dialogue [data-border-style="3"]::before{background-color:var(--ass-shadow-color);left:calc(var(--ass-scale-stroke)*var(--ass-tag-xshad)*1px);top:calc(var(--ass-scale-stroke)*var(--ass-tag-yshad)*1px)}.ASS-dialogue [data-border-style="3"]::after{background-color:var(--ass-border-color);left:0;top:0}@container style(--ass-tag-xbord: 0) and style(--ass-tag-ybord: 0){.ASS-dialogue [data-border-style="3"]::after{background-color:transparent}}@container style(--ass-tag-xshad: 0) and style(--ass-tag-yshad: 0){.ASS-dialogue [data-border-style="3"]::before{background-color:transparent}}.ASS-dialogue [data-rotate]{transform:perspective(312.5px) rotateY(calc(var(--ass-tag-fry)*1deg)) rotateX(calc(var(--ass-tag-frx)*1deg)) rotateZ(calc(var(--ass-tag-frz)*-1deg))}.ASS-dialogue [data-text][data-rotate]{transform-style:preserve-3d;word-break:normal;white-space:nowrap}.ASS-dialogue [data-scale],.ASS-dialogue [data-skew]{display:inline-block;transform:scale(var(--ass-tag-fscx),var(--ass-tag-fscy)) skew(calc(var(--ass-tag-fax)*1rad),calc(var(--ass-tag-fay)*1rad));transform-origin:var(--ass-align-h) var(--ass-align-v)}.ASS-fix-font-size{font-family:Arial;line-height:normal;width:0;height:0;position:absolute;visibility:hidden;overflow:hidden}.ASS-clip-area,.ASS-fix-font-size span{position:absolute}.ASS-clip-area{width:100%;height:100%;top:0;left:0}.ASS-effect-area{position:absolute;display:flex;width:100%;height:fit-content;overflow:hidden;mask-composite:intersect}.ASS-effect-area[data-effect=banner]{flex-direction:column;height:100%}.ASS-effect-area .ASS-dialogue{position:static;transform:none}'; /** * @param {HTMLElement} container */ @@ -825,23 +968,232 @@ var ASS = (function () { }); } - // https://github.com/weizhenye/ASS/wiki/Font-Size-in-ASS + function createEffect(effect, duration) { + // TODO: when effect and move both exist, its behavior is weird, for now only move works. + const { name, delay, leftToRight } = effect; + const translate = name === 'banner' ? 'X' : 'Y'; + const dir = ({ + X: leftToRight ? 1 : -1, + Y: /up/.test(name) ? -1 : 1, + })[translate]; + const start = -100 * dir; + // speed is 1000px/s when delay=1 + const distance = (duration / (delay || 1)) * dir; + const keyframes = [ + { offset: 0, transform: `translate${translate}(${start}%)` }, + { offset: 1, transform: `translate${translate}(calc(${start}% + var(--ass-scale) * ${distance}px))` }, + ]; + return [keyframes, { duration, fill: 'forwards' }]; + } - const $fixFontSize = document.createElement('div'); - $fixFontSize.className = 'ASS-fix-font-size'; - const $span = document.createElement('span'); - $span.textContent = '0'; - $fixFontSize.append($span); + function multiplyScale(v) { + return `calc(var(--ass-scale) * ${v}px)`; + } - const unitsPerEm = 2048; - const lineSpacing = Object.create(null); + function createMove(move, duration) { + const { x1, y1, x2, y2, t1, t2 } = move; + const start = `translate(${multiplyScale(x1)}, ${multiplyScale(y1)})`; + const end = `translate(${multiplyScale(x2)}, ${multiplyScale(y2)})`; + const moveDuration = Math.max(t2, duration); + const keyframes = [ + { offset: 0, transform: start }, + t1 > 0 ? { offset: t1 / moveDuration, transform: start } : null, + (t2 > 0 && t2 < duration) ? { offset: t2 / moveDuration, transform: end } : null, + { offset: 1, transform: end }, + ].filter(Boolean); + const options = { duration: moveDuration, fill: 'forwards' }; + return [keyframes, options]; + } + + function createFadeList(fade, duration) { + const { type, a1, a2, a3, t1, t2, t3, t4 } = fade; + // \fad(, ) + if (type === 'fad') { + // For example dialogue starts at 0 and ends at 5000 with \fad(4000, 4000) + // * means opacity from 0 to 1 in (0, 4000) + // * means opacity from 1 to 0 in (1000, 5000) + // and are overlaped in (1000, 4000), will take affect + // so the result is: + // * opacity from 0 to 1 in (0, 4000) + // * opacity from 0.25 to 0 in (4000, 5000) + const t1Keyframes = [{ offset: 0, opacity: 0 }, { offset: 1, opacity: 1 }]; + const t2Keyframes = [{ offset: 0, opacity: 1 }, { offset: 1, opacity: 0 }]; + return [ + [t2Keyframes, { duration: t2, delay: duration - t2, fill: 'forwards' }], + [t1Keyframes, { duration: t1, composite: 'replace' }], + ]; + } + // \fade(, , , , , , ) + const fadeDuration = Math.max(duration, t4); + const opacities = [a1, a2, a3].map((a) => 1 - a / 255); + const offsets = [0, t1, t2, t3, t4].map((t) => t / fadeDuration); + const keyframes = offsets.map((t, i) => ({ offset: t, opacity: opacities[i >> 1] })); + return [ + [keyframes, { duration: fadeDuration, fill: 'forwards' }], + ]; + } - function getRealFontSize(fn, fs) { - if (!lineSpacing[fn]) { - $span.style.fontFamily = fn; - lineSpacing[fn] = $span.clientHeight; + function createAnimatableVars(tag) { + return [ + ['real-fs', getRealFontSize(tag.fn, tag.fs)], + ['tag-fs', tag.fs], + ['tag-fsp', tag.fsp], + ['fill-color', color2rgba(tag.a1 + tag.c1)], + ] + .filter(([, v]) => v) + .map(([k, v]) => [`--ass-${k}`, v]); + } + + if (window.CSS.registerProperty) { + ['real-fs', 'tag-fs', 'tag-fsp'].forEach((k) => { + window.CSS.registerProperty({ + name: `--ass-${k}`, + syntax: '', + inherits: true, + initialValue: '0', + }); + }); + window.CSS.registerProperty({ + name: '--ass-fill-color', + syntax: '', + inherits: true, + initialValue: 'transparent', + }); + } + + // use linear() to simulate accel + function getEasing(duration, accel) { + if (accel === 1) return 'linear'; + // 60fps + const frames = Math.ceil(duration / 1000 * 60); + const points = Array.from({ length: frames + 1 }) + .map((_, i) => (i / frames) ** accel); + return `linear(${points.join(',')})`; + } + + function createDialogueAnimations(el, dialogue) { + const { start, end, effect, move, fade } = dialogue; + const duration = (end - start) * 1000; + return [ + effect && !move ? createEffect(effect, duration) : null, + move ? createMove(move, duration) : null, + ...(fade ? createFadeList(fade, duration) : []), + ] + .filter(Boolean) + .map(([keyframes, options]) => initAnimation(el, keyframes, options)); + } + + function createTagKeyframes(fromTag, tag, key) { + const value = tag[key]; + if (value === undefined) return []; + if (key === 'clip') return []; + if (key === 'a1' || key === 'c1') { + return [['fill-color', color2rgba((tag.a1 || fromTag.a1) + (tag.c1 || fromTag.c1))]]; } - return fs * unitsPerEm / lineSpacing[fn]; + if (key === 'c3') { + return [['border-color', color2rgba(`00${tag.c3}`)]]; + } + if (key === 'a3') { + return [['border-opacity', alpha2opacity(tag.a3)]]; + } + if (key === 'c4') { + return [['shadow-color', color2rgba(`00${tag.c4}`)]]; + } + if (key === 'a4') { + return [['shadow-opacity', alpha2opacity(tag.a4)]]; + } + if (key === 'fs') { + return [ + ['real-fs', getRealFontSize(tag.fn || fromTag.fn, tag.fs)], + ['tag-fs', value], + ]; + } + if (key === 'fscx' || key === 'fscy') { + return [[`tag-${key}`, (value || 100) / 100]]; + } + return [[`tag-${key}`, value]]; + } + + function createTagAnimations(el, fragment, sliceTag) { + const fromTag = { ...sliceTag, ...fragment.tag }; + return (fragment.tag.t || []).map(({ t1, t2, accel, tag }) => { + const keyframe = Object.fromEntries( + Object.keys(tag) + .flatMap((key) => createTagKeyframes(fromTag, tag, key)) + .map(([k, v]) => [`--ass-${k}`, v]) + // .concat(tag.clip ? [['clipPath', ]] : []) + .concat([['offset', 1]]), + ); + const duration = Math.max(0, t2 - t1); + return initAnimation(el, [keyframe], { + duration, + delay: t1, + fill: 'forwards', + easing: getEasing(duration, accel), + }); + }); + } + + function createClipAnimations(el, dialogue, store) { + return dialogue.slices + .flatMap((slice) => slice.fragments) + .flatMap((fragment) => fragment.tag.t || []) + .filter(({ tag }) => tag.clip) + .map(({ t1, t2, accel, tag }) => { + const keyframe = { + offset: 1, + clipPath: createRectClip(tag.clip, store.scriptRes.width, store.scriptRes.height), + }; + const duration = Math.max(0, t2 - t1); + return initAnimation(el, [keyframe], { + duration, + delay: t1, + fill: 'forwards', + easing: getEasing(duration, accel), + }); + }); + } + + // eslint-disable-next-line import/no-cycle + + function createRectClip(clip, sw, sh) { + if (!clip.dots) return ''; + const { x1, y1, x2, y2 } = clip.dots; + const polygon = [[x1, y1], [x1, y2], [x2, y2], [x2, y1], [x1, y1]] + .map(([x, y]) => [x / sw, y / sh]) + .concat(clip.inverse ? [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]] : []) + .map((pair) => pair.map((n) => `${n * 100}%`).join(' ')) + .join(','); + return `polygon(evenodd, ${polygon})`; + } + + function createPathClip(clip, sw, sh, store) { + if (!clip.drawing) return ''; + const scale = store.scale / (1 << (clip.scale - 1)); + let d = clip.drawing.instructions.map(({ type, points }) => ( + type + points.map(({ x, y }) => `${x * scale},${y * scale}`).join(',') + )).join(''); + if (clip.inverse) { + d += `M0,0L0,${sh},${sw},${sh},${sw},0,0,0Z`; + } + return `path(evenodd, "${d}")`; + } + + function getClipPath(dialogue, store) { + const { clip, animations } = dialogue; + if (!clip) return {}; + const { width, height } = store.scriptRes; + const $clipArea = document.createElement('div'); + store.box.insertBefore($clipArea, dialogue.$div); + $clipArea.append(dialogue.$div); + $clipArea.className = 'ASS-clip-area'; + $clipArea.style.zIndex = dialogue.$div.style.zIndex; + $clipArea.style.clipPath = clip.dots + ? createRectClip(clip, width, height) + : createPathClip(clip, width, height, store); + animations.push(...createClipAnimations($clipArea, dialogue, store)); + + return { $div: $clipArea }; } function createSVGStroke(tag, id, scale) { @@ -1024,276 +1376,6 @@ var ASS = (function () { }); } - const rotateTags = ['frx', 'fry', 'frz']; - const scaleTags = ['fscx', 'fscy']; - const skewTags = ['fax', 'fay']; - - if (window.CSS.registerProperty) { - [...rotateTags, ...skewTags].forEach((tag) => { - window.CSS.registerProperty({ - name: `--ass-tag-${tag}`, - syntax: '', - inherits: true, - initialValue: 0, - }); - }); - scaleTags.forEach((tag) => { - window.CSS.registerProperty({ - name: `--ass-tag-${tag}`, - syntax: '', - inherits: true, - initialValue: 1, - }); - }); - } - - function createTransform(tag) { - return [ - ...[...rotateTags, ...skewTags].map((x) => ([`--ass-tag-${x}`, `${tag[x] || 0}`])), - ...scaleTags.map((x) => ([`--ass-tag-${x}`, tag.p ? 1 : (tag[x] || 100) / 100])), - ]; - } - - function setTransformOrigin(dialogue, scale) { - const { align, width, height, x, y, $div } = dialogue; - const org = {}; - if (dialogue.org) { - org.x = dialogue.org.x * scale; - org.y = dialogue.org.y * scale; - } else { - org.x = [x, x + width / 2, x + width][align.h]; - org.y = [y + height, y + height / 2, y][align.v]; - } - for (let i = $div.childNodes.length - 1; i >= 0; i -= 1) { - const node = $div.childNodes[i]; - if (node.dataset.rotate === '') { - // It's not extremely precise for offsets are round the value to an integer. - const tox = org.x - x - node.offsetLeft; - const toy = org.y - y - node.offsetTop; - node.style.cssText += `transform-origin:${tox}px ${toy}px;`; - } - } - } - - // TODO: multi \t can't be merged directly - function mergeT(ts) { - return ts.reduceRight((results, t) => { - let merged = false; - return results - .map((r) => { - merged = t.t1 === r.t1 && t.t2 === r.t2 && t.accel === r.accel; - return { ...r, ...(merged ? { tag: { ...r.tag, ...t.tag } } : {}) }; - }) - .concat(merged ? [] : t); - }, []); - } - - function createEffectKeyframes({ effect, duration }) { - // TODO: when effect and move both exist, its behavior is weird, for now only move works. - const { name, delay, leftToRight } = effect; - if (name === 'banner') { - const tx = (duration / (delay || 1)) * (leftToRight ? 1 : -1); - return [0, `calc(var(--ass-scale) * ${tx}px)`].map((x, i) => ({ - offset: i, - transform: `translateX(${x})`, - })); - } - if (name.startsWith('scroll')) { - // speed is 1000px/s when delay=1 - const updown = /up/.test(name) ? -1 : 1; - const y = duration / (delay || 1) * updown; - return [ - { offset: 0, transform: 'translateY(-100%)' }, - { offset: 1, transform: `translateY(calc(var(--ass-scale) * ${y}px))` }, - ]; - } - return []; - } - - function createMoveKeyframes({ move, duration, dialogue }) { - const { x1, y1, x2, y2, t1, t2 } = move; - const t = [t1, t2 || duration]; - const pos = dialogue.pos || { x: 0, y: 0 }; - return [[x1, y1], [x2, y2]] - .map(([x, y]) => [(x - pos.x), (y - pos.y)]) - .map(([x, y], index) => ({ - offset: Math.min(t[index] / duration, 1), - transform: `translate(calc(var(--ass-scale) * ${x}px), calc(var(--ass-scale) * ${y}px))`, - })); - } - - function createFadeKeyframes(fade, duration) { - if (fade.type === 'fad') { - const { t1, t2 } = fade; - const kfs = []; - if (t1) { - kfs.push([0, 0]); - } - if (t1 < duration) { - if (t2 <= duration) { - kfs.push([t1 / duration, 1]); - } - if (t1 + t2 < duration) { - kfs.push([(duration - t2) / duration, 1]); - } - if (t2 > duration) { - kfs.push([0, (t2 - duration) / t2]); - } else if (t1 + t2 > duration) { - kfs.push([(t1 + 0.5) / duration, 1 - (t1 + t2 - duration) / t2]); - } - if (t2) { - kfs.push([1, 0]); - } - } else { - kfs.push([1, duration / t1]); - } - return kfs.map(([offset, opacity]) => ({ offset, opacity })); - } - const { a1, a2, a3, t1, t2, t3, t4 } = fade; - const opacities = [a1, a2, a3].map((a) => 1 - a / 255); - return [0, t1, t2, t3, t4, duration] - .map((t) => t / duration) - .map((t, i) => ({ offset: t, opacity: opacities[i >> 1] })) - .filter(({ offset }) => offset <= 1); - } - - function createTransformKeyframes({ fromTag, tag, fragment }) { - const toTag = { ...fromTag, ...tag }; - if (fragment.drawing) { - // scales will be handled inside svg - Object.assign(toTag, { - p: 0, - fscx: ((tag.fscx || fromTag.fscx) / fromTag.fscx) * 100, - fscy: ((tag.fscy || fromTag.fscy) / fromTag.fscy) * 100, - }); - Object.assign(fromTag, { fscx: 100, fscy: 100 }); - } - return Object.fromEntries(createTransform(toTag)); - } - - function createAnimatableVars(tag) { - return [ - ['real-fs', getRealFontSize(tag.fn, tag.fs)], - ['tag-fs', tag.fs], - ['tag-fsp', tag.fsp], - ['fill-color', color2rgba(tag.a1 + tag.c1)], - ] - .filter(([, v]) => v) - .map(([k, v]) => [`--ass-${k}`, v]); - } - - if (window.CSS.registerProperty) { - ['real-fs', 'tag-fs', 'tag-fsp'].forEach((k) => { - window.CSS.registerProperty({ - name: `--ass-${k}`, - syntax: '', - inherits: true, - initialValue: '0', - }); - }); - window.CSS.registerProperty({ - name: '--ass-fill-color', - syntax: '', - inherits: true, - initialValue: 'transparent', - }); - } - - // TODO: accel is not implemented yet, maybe it can be simulated by cubic-bezier? - function setKeyframes(dialogue, store) { - const { start, end, effect, move, fade, slices } = dialogue; - const duration = (end - start) * 1000; - const keyframes = [ - ...(effect && !move ? createEffectKeyframes({ effect, duration }) : []), - ...(move ? createMoveKeyframes({ move, duration, dialogue }) : []), - ...(fade ? createFadeKeyframes(fade, duration) : []), - ].sort((a, b) => a.offset - b.offset); - if (keyframes.length > 0) { - Object.assign(dialogue, { keyframes }); - } - slices.forEach((slice) => { - const sliceTag = store.styles[slice.style].tag; - slice.fragments.forEach((fragment) => { - if (!fragment.tag.t || fragment.tag.t.length === 0) { - return; - } - const fromTag = { ...sliceTag, ...fragment.tag }; - const tTags = mergeT(fragment.tag.t).sort((a, b) => a.t2 - b.t2 || a.t1 - b.t1); - if (tTags[0].t1 > 0) { - tTags.unshift({ t1: 0, t2: tTags[0].t1, tag: fromTag }); - } - tTags.reduce((prevTag, curr) => { - const tag = { ...prevTag, ...curr.tag }; - tag.t = null; - Object.assign(curr.tag, tag); - return tag; - }, {}); - const fDuration = Math.max(duration, ...tTags.map(({ t2 }) => t2)); - const kfs = tTags.map(({ t2, tag }) => ({ - offset: t2 / fDuration, - ...Object.fromEntries(createAnimatableVars({ - ...tag, - a1: tag.a1 || fromTag.a1, - c1: tag.c1 || fromTag.c1, - })), - ...Object.fromEntries(createCSSStroke( - { ...fromTag, ...tag }, - store.sbas ? store.scale : 1, - )), - ...createTransformKeyframes({ fromTag, tag, fragment }), - })).sort((a, b) => a.offset - b.offset); - if (kfs.length > 0) { - Object.assign(fragment, { keyframes: kfs, duration: fDuration }); - } - }); - }); - } - - function addClipPath($defs, clip, id, sw, sh) { - if ($defs.querySelector(`#${id}`)) return; - let d = ''; - if (clip.dots !== null) { - let { x1, y1, x2, y2 } = clip.dots; - x1 /= sw; - y1 /= sh; - x2 /= sw; - y2 /= sh; - d = `M${x1},${y1}L${x1},${y2},${x2},${y2},${x2},${y1}Z`; - } - if (clip.drawing !== null) { - d = clip.drawing.instructions.map(({ type, points }) => ( - type + points.map(({ x, y }) => `${x / sw},${y / sh}`).join(',') - )).join(''); - } - const scale = 1 / (1 << (clip.scale - 1)); - if (clip.inverse) { - d += `M0,0L0,${scale},${scale},${scale},${scale},0,0,0Z`; - } - const $clipPath = createSVGEl('clipPath', [ - ['id', id], - ['clipPathUnits', 'objectBoundingBox'], - ]); - $clipPath.append(createSVGEl('path', [ - ['d', d], - ['transform', `scale(${scale})`], - ['clip-rule', 'evenodd'], - ])); - $defs.append($clipPath); - } - - function getClipPath(dialogue, store) { - const { id, clip } = dialogue; - if (!clip) return {}; - const { width, height } = store.scriptRes; - addClipPath(store.defs, clip, id, width, height); - const $clipArea = document.createElement('div'); - store.box.insertBefore($clipArea, dialogue.$div); - $clipArea.append(dialogue.$div); - $clipArea.className = 'ASS-clip-area'; - $clipArea.style.clipPath = `url(#${id})`; - return { $div: $clipArea }; - } - function createDrawing(fragment, styleTag, store) { if (!fragment.drawing.d) return null; const tag = { ...styleTag, ...fragment.tag }; @@ -1340,6 +1422,51 @@ var ASS = (function () { }; } + const rotateTags = ['frx', 'fry', 'frz']; + const scaleTags = ['fscx', 'fscy']; + const skewTags = ['fax', 'fay']; + + if (window.CSS.registerProperty) { + [...rotateTags, ...skewTags].forEach((tag) => { + window.CSS.registerProperty({ + name: `--ass-tag-${tag}`, + syntax: '', + inherits: true, + initialValue: 0, + }); + }); + scaleTags.forEach((tag) => { + window.CSS.registerProperty({ + name: `--ass-tag-${tag}`, + syntax: '', + inherits: true, + initialValue: 1, + }); + }); + } + + function createTransform(tag) { + return [ + ...[...rotateTags, ...skewTags].map((x) => ([`--ass-tag-${x}`, `${tag[x] || 0}`])), + ...scaleTags.map((x) => ([`--ass-tag-${x}`, tag.p ? 1 : (tag[x] || 100) / 100])), + ]; + } + + function setTransformOrigin(dialogue, scale) { + const { align, width, height, x, y, $div } = dialogue; + const orgX = (dialogue.org ? dialogue.org.x * scale : x) + [0, width / 2, width][align.h]; + const orgY = (dialogue.org ? dialogue.org.y * scale : y) + [height, height / 2, 0][align.v]; + for (let i = $div.childNodes.length - 1; i >= 0; i -= 1) { + const node = $div.childNodes[i]; + if (node.dataset.rotate === '') { + // It's not extremely precise for offsets are round the value to an integer. + const tox = orgX - x - node.offsetLeft; + const toy = orgY - y - node.offsetTop; + node.style.cssText += `transform-origin:${tox}px ${toy}px;`; + } + } + } + function encodeText(text, q) { return text .replace(/\\h/g, ' ') @@ -1348,22 +1475,18 @@ var ASS = (function () { } function createDialogue(dialogue, store) { - const { video, styles } = store; + const { styles } = store; const $div = document.createElement('div'); $div.className = 'ASS-dialogue'; + $div.dataset.wrapStyle = dialogue.q; const df = document.createDocumentFragment(); - const { align, slices, start, end } = dialogue; + const { align, slices } = dialogue; [ - ['--ass-align-h', ['left', 'center', 'right'][align.h]], - ['--ass-align-v', ['bottom', 'center', 'top'][align.v]], + ['--ass-align-h', ['0%', '50%', '100%'][align.h]], + ['--ass-align-v', ['100%', '50%', '0%'][align.v]], ].forEach(([k, v]) => { $div.style.setProperty(k, v); }); - const animationOptions = { - duration: (end - start) * 1000, - delay: Math.min(0, start - (video.currentTime - store.delay)) * 1000, - fill: 'forwards', - }; const animations = []; slices.forEach((slice) => { const sliceTag = styles[slice.style].tag; @@ -1397,8 +1520,6 @@ var ASS = (function () { encodeText(text, tag.q).split('\n').forEach((content, idx) => { const $span = document.createElement('span'); const $ssspan = document.createElement('span'); - $span.dataset.wrapStyle = tag.q; - $span.dataset.borderStyle = borderStyle; if (hasScale || hasSkew) { if (hasScale) { $ssspan.dataset.scale = ''; @@ -1418,7 +1539,6 @@ var ASS = (function () { $span.style.cssText = obj.cssText; $span.append(obj.$svg); } else { - $span.dataset.text = ''; if (idx) { df.append(document.createElement('br')); } @@ -1429,29 +1549,21 @@ var ASS = (function () { $span.textContent = content; } const el = hasScale || hasSkew ? $ssspan : $span; + el.dataset.text = content; if (tag.xbord || tag.ybord || tag.xshad || tag.yshad) { - el.dataset.text = content; + el.dataset.borderStyle = borderStyle; } } $span.style.cssText += cssText; cssVars.forEach(([k, v]) => { $span.style.setProperty(k, v); }); - if (fragment.keyframes) { - const animation = initAnimation( - $span, - fragment.keyframes, - { ...animationOptions, duration: fragment.duration }, - ); - animations.push(animation); - } + animations.push(...createTagAnimations($span, fragment, sliceTag)); df.append($span); }); }); }); - if (dialogue.keyframes) { - animations.push(initAnimation($div, dialogue.keyframes, animationOptions)); - } + animations.push(...createDialogueAnimations($div, dialogue)); $div.append(df); return { $div, animations }; } @@ -1533,17 +1645,10 @@ var ASS = (function () { function getPosition(dialogue, store) { const { scale } = store; - const { effect, move, align, width, height, margin, slices } = dialogue; + const { move, align, width, height, margin, slices } = dialogue; let x = 0; let y = 0; - if (effect && effect.name === 'banner') { - x = effect.lefttoright ? -width : store.width; - y = [ - store.height - height - margin.vertical, - (store.height - height) / 2, - margin.vertical, - ][align.v]; - } else if (dialogue.pos || move) { + if (dialogue.pos || move) { const pos = dialogue.pos || { x: 0, y: 0 }; const sx = scale * pos.x; const sy = scale * pos.y; @@ -1566,47 +1671,66 @@ var ASS = (function () { ][align.v] : allocate(dialogue, store); } - // TODO: use % for x and y - return { x, y }; + return { + x: x + [0, width / 2, width][align.h], + y: y + [height, height / 2, 0][align.v], + }; } function createStyle(dialogue) { - const { layer, align, effect, pos, margin } = dialogue; + const { layer, align, effect, pos, margin, q } = dialogue; let cssText = ''; if (layer) cssText += `z-index:${layer};`; cssText += `text-align:${['left', 'center', 'right'][align.h]};`; if (!effect) { - cssText += `max-width:calc(100% - var(--ass-scale) * ${margin.left + margin.right}px);`; + if (q !== 2) { + cssText += `max-width:calc(100% - var(--ass-scale) * ${margin.left + margin.right}px);`; + } if (!pos) { if (align.h !== 0) { - cssText += `margin-right:calc(var(--ass-scale) * ${margin.right}px);`; + cssText += `padding-right:calc(var(--ass-scale) * ${margin.right}px);`; } if (align.h !== 2) { - cssText += `margin-left:calc(var(--ass-scale) * ${margin.left}px);`; + cssText += `padding-left:calc(var(--ass-scale) * ${margin.left}px);`; } } } return cssText; } - function getScrollEffect(dialogue, store) { - const $scrollArea = document.createElement('div'); - $scrollArea.className = 'ASS-scroll-area'; - store.box.insertBefore($scrollArea, dialogue.$div); - $scrollArea.append(dialogue.$div); - const { height } = store.scriptRes; - const { name, y1, y2 } = dialogue.effect; + function setEffect(dialogue, store) { + const $area = document.createElement('div'); + $area.className = 'ASS-effect-area'; + store.box.insertBefore($area, dialogue.$div); + $area.append(dialogue.$div); + const { width, height } = store.scriptRes; + const { name, y1, y2, leftToRight, fadeAwayWidth, fadeAwayHeight } = dialogue.effect; const min = Math.min(y1, y2); const max = Math.max(y1, y2); - const top = min / height * 100; - const bottom = (height - max) / height * 100; - $scrollArea.style.cssText += `top:${top}%;bottom:${bottom}%;`; - const up = /up/.test(name); - // eslint-disable-next-line no-param-reassign - dialogue.$div.style.cssText += up ? 'top:100%;' : 'top:0%;'; - return { - $div: $scrollArea, - }; + $area.dataset.effect = name; + if (name === 'banner') { + $area.style.alignItems = leftToRight ? 'flex-start' : 'flex-end'; + $area.style.justifyContent = ['flex-end', 'center', 'flex-start'][dialogue.align.v]; + } + if (name.startsWith('scroll')) { + const top = min / height * 100; + const bottom = (height - max) / height * 100; + $area.style.cssText = `top:${top}%;bottom:${bottom}%;`; + $area.style.justifyContent = ['flex-start', 'center', 'flex-end'][dialogue.align.h]; + } + if (fadeAwayHeight) { + const p = fadeAwayHeight / (max - min) * 100; + $area.style.maskImage = [ + `linear-gradient(#000 ${100 - p}%, transparent)`, + `linear-gradient(transparent, #000 ${p}%)`, + ].join(','); + } + if (fadeAwayWidth) { + const p = fadeAwayWidth / width * 100; + // only left side has fade away effect in VSFilter + $area.style.maskImage = `linear-gradient(90deg, transparent, #000 ${p}%)`; + } + return $area; } function renderer(dialogue, store) { @@ -1621,11 +1745,12 @@ var ASS = (function () { Object.assign(dialogue, { height }); const { x, y } = getPosition(dialogue, store); Object.assign(dialogue, { x, y }); - $div.style.cssText += `width:${width}px;height:${height}px;left:${x}px;top:${y}px;`; + $div.style.cssText += `left:${x}px;top:${y}px;`; setTransformOrigin(dialogue, store.scale); + // TODO: refactor to create .clip-area or .effect-area wrappers in `createDialogue` Object.assign(dialogue, getClipPath(dialogue, store)); - if (dialogue.effect?.name?.startsWith('scroll')) { - Object.assign(dialogue, getScrollEffect(dialogue, store)); + if (dialogue.effect) { + Object.assign(dialogue, { $div: setEffect(dialogue, store) }); } return dialogue; } @@ -1658,6 +1783,9 @@ var ASS = (function () { ) { if (vct < dialogues[store.index].end) { const dia = renderer(dialogues[store.index], store); + (dia.animations || []).forEach((animation) => { + animation.currentTime = (vct - dia.start) * 1000; + }); if (!video.paused) { batchAnimate(dia, 'play'); } @@ -1709,7 +1837,7 @@ var ASS = (function () { } function createResize(that, store) { - const { video, box, svg, layoutRes } = store; + const { video, box, layoutRes } = store; return function resize() { const cw = video.clientWidth; const ch = video.clientHeight; @@ -1739,11 +1867,9 @@ var ASS = (function () { store.height = bh; store.resampledRes = { width: rw, height: rh }; - const cssText = `width:${bw}px;height:${bh}px;top:${(ch - bh) / 2}px;left:${(cw - bw) / 2}px;`; - box.style.cssText = cssText; + box.style.cssText = `width:${bw}px;height:${bh}px;top:${(ch - bh) / 2}px;left:${(cw - bw) / 2}px;`; box.style.setProperty('--ass-scale', store.scale); box.style.setProperty('--ass-scale-stroke', store.sbas ? store.scale : 1); - svg.style.cssText = cssText; createSeek(store)(); }; @@ -1772,10 +1898,6 @@ var ASS = (function () { video: null, /** the box to display subtitles */ box: document.createElement('div'), - /** use for \clip */ - svg: createSVGEl('svg'), - /** use for \clip */ - defs: createSVGEl('defs'), /** * video resize observer * @type {ResizeObserver} @@ -1863,7 +1985,7 @@ var ASS = (function () { }; this.#store.styles = styles; this.#store.dialogues = dialogues.map((dia) => Object.assign(dia, { - id: `ASS-${uuid()}`, + effect: ['banner', 'scroll up', 'scroll down'].includes(dia.effect?.name) ? dia.effect : null, align: { // 0: left, 1: center, 2: right h: (dia.alignment + 2) % 3, @@ -1872,14 +1994,11 @@ var ASS = (function () { }, })); - container.append($fixFontSize); - - const { svg, defs, scriptRes, box } = this.#store; - svg.setAttributeNS(null, 'viewBox', `0 0 ${scriptRes.width} ${scriptRes.height}`); - - svg.append(defs); - container.append(svg); + if ($fixFontSize) { + container.append($fixFontSize); + } + const { box } = this.#store; box.className = 'ASS-box'; container.append(box); @@ -1898,10 +2017,6 @@ var ASS = (function () { this.#resize(); this.resampling = resampling; - dialogues.forEach((dialogue) => { - setKeyframes(dialogue, this.#store); - }); - const observer = new ResizeObserver(this.#resize); observer.observe(video); this.#store.observer = observer; @@ -1914,7 +2029,7 @@ var ASS = (function () { * @returns {ASS} */ destroy() { - const { video, box, svg, observer } = this.#store; + const { video, box, observer } = this.#store; this.#pause(); clear(this.#store); video.removeEventListener('play', this.#play); @@ -1923,8 +2038,9 @@ var ASS = (function () { video.removeEventListener('waiting', this.#pause); video.removeEventListener('seeking', this.#seek); - $fixFontSize.remove(); - svg.remove(); + if ($fixFontSize) { + $fixFontSize.remove(); + } box.remove(); observer.unobserve(this.#store.video); diff --git a/dist/ass.global.min.js b/dist/ass.global.min.js index 6ab5b17..967a25a 100644 --- a/dist/ass.global.min.js +++ b/dist/ass.global.min.js @@ -1 +1 @@ -var ASS=function(){"use strict";function t(t){const e=t.toLowerCase().trim().split(/\s*;\s*/);return"banner"===e[0]?{name:e[0],delay:1*e[1]||0,leftToRight:1*e[2]||0,fadeAwayWidth:1*e[3]||0}:/^scroll\s/.test(e[0])?{name:e[0],y1:Math.min(1*e[1],1*e[2]),y2:Math.max(1*e[1],1*e[2]),delay:1*e[3]||0,fadeAwayHeight:1*e[4]||0}:""!==t?{name:t}:null}function e(t){return t?t.toLowerCase().replace(/([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?)/g," $1 ").replace(/([mnlbspc])/g," $1 ").trim().replace(/\s+/g," ").split(/\s(?=[mnlbspc])/).map((t=>t.split(" ").filter(((t,e)=>!(e&&Number.isNaN(1*t)))))):[]}const s=["b","i","u","s","fsp","k","K","kf","ko","kt","fe","q","p","pbo","a","an","fscx","fscy","fax","fay","frx","fry","frz","fr","be","blur","bord","xbord","ybord","shad","xshad","yshad"].map((t=>({name:t,regex:new RegExp(`^${t}-?\\d`)})));function a(t){const n={};for(let e=0;et.replace(/,/g,"\n"))).split(/\s*,\s*/);if(!e[0])return n;n.t={t1:0,t2:0,accel:1,tags:e[e.length-1].replace(/\n/g,",").split("\\").slice(1).map(a)},2===e.length&&(n.t.accel=1*e[0]),3===e.length&&(n.t.t1=1*e[0],n.t.t2=1*e[1]),4===e.length&&(n.t.t1=1*e[0],n.t.t2=1*e[1],n.t.accel=1*e[2])}return n}function n(t){const e=[];let s=0,n="";const r=t.split("\\").slice(1).concat("").join("\\");for(let t=0;tvoid 0===e.p?t:!!e.p),!1);a.push({tags:r,text:i?"":s[t+1],drawing:i?e(s[t+1]):[]})}return{raw:t,combined:a.map((t=>t.text)).join(""),parsed:a}}function i(t){const e=t.split(":");return 3600*e[0]+60*e[1]+1*e[2]}function o(e,s){let a=e.split(",");if(a.length>s.length){const t=a.slice(s.length-1).join();a=a.slice(0,s.length-1),a.push(t)}const n={};for(let e=0;ee.find((e=>e.toLowerCase()===t.toLowerCase()))||t))}function f(t,e){const s=t.match(/Style\s*:\s*(.*)/i)[1].split(/\s*,\s*/);return Object.assign({},...e.map(((t,e)=>({[t]:s[e]}))))}function p(t){const e={type:null,prev:null,next:null,points:[]};/[mnlbs]/.test(t[0])&&(e.type=t[0].toUpperCase().replace("N","L").replace("B","C"));for(let s=t.length-!(1&t.length),a=1;at+e.map((({x:t,y:e})=>`${t},${e}`)).join(","))).join("")}function g(t){const e=[];let s=0;for(;s({x:t,y:e}))))}t.splice(s,1)}}const a=[].concat(...e.map((({type:t,points:e,prev:s,next:a})=>"S"===t?function(t,e,s){const a=[],n=[0,2/3,1/3,0],r=[0,1/3,2/3,0],i=[0,1/6,2/3,1/6],o=(t,e)=>t[0]*e[0]+t[1]*e[1]+t[2]*e[2]+t[3]*e[3];let l=[t[t.length-1].x,t[0].x,t[1].x,t[2].x],c=[t[t.length-1].y,t[0].y,t[1].y,t[2].y];a.push({type:"M"===e?"M":"L",points:[{x:o(i,l),y:o(i,c)}]});for(let e=3;et))).forEach((({x:t,y:r})=>{e=Math.min(e,t),s=Math.min(s,r),a=Math.max(a,t),n=Math.max(n,r)})),{minX:e,minY:s,width:a-e,height:n-s}}(e))}const y=["fs","fsp","clip","c1","c2","c3","c4","a1","a2","a3","a4","alpha","fscx","fscy","fax","fay","frx","fry","frz","fr","be","blur","bord","xbord","ybord","shad","xshad","yshad"];function m(t,e,s={}){let a=t[e];if(void 0===a)return null;if("pos"===e||"org"===e)return 2===a.length?{[e]:{x:a[0],y:a[1]}}:null;if("move"===e){const[t,e,s,n,r=0,i=0]=a;return 4===a.length||6===a.length?{move:{x1:t,y1:e,x2:s,y2:n,t1:r,t2:i}}:null}if("fad"===e||"fade"===e){if(2===a.length){const[t,e]=a;return{fade:{type:"fad",t1:t,t2:e}}}if(7===a.length){const[t,e,s,n,r,i,o]=a;return{fade:{type:"fade",a1:t,a2:e,a3:s,t1:n,t2:r,t3:i,t4:o}}}return null}if("clip"===e){const{inverse:t,scale:e,drawing:s,dots:n}=a;if(s)return{clip:{inverse:t,scale:e,drawing:g(s),dots:n}};if(n){const[a,r,i,o]=n;return{clip:{inverse:t,scale:e,drawing:s,dots:{x1:a,y1:r,x2:i,y2:o}}}}return null}if(/^[xy]?(bord|shad)$/.test(e)&&(a=Math.max(a,0)),"bord"===e)return{xbord:a,ybord:a};if("shad"===e)return{xshad:a,yshad:a};if(/^c\d$/.test(e))return{[e]:a||s[e]};if("alpha"===e)return{a1:a,a2:a,a3:a,a4:a};if("fr"===e)return{frz:a};if("fs"===e)return{fs:/^\+|-/.test(a)?(1*a>-10?1+a/10:1)*s.fs:1*a};if("K"===e)return{kf:a};if("t"===e){const{t1:t,accel:e,tags:n}=a,r=a.t2||1e3*(s.end-s.start),i={};return n.forEach((t=>{const e=Object.keys(t)[0];~y.indexOf(e)&&("clip"!==e||t[e].dots)&&Object.assign(i,m(t,e,s))})),{t:{t1:t,t2:r,accel:e,tag:i}}}return{[e]:a}}const x=[null,1,2,3,null,7,8,9,null,4,5,6],b=["r","a","an","pos","org","move","fade","fad","clip"];function v({styles:t,style:e,parsed:s,start:a,end:n}){let r,i,o,l,c,d;const f=[];let p={style:e,fragments:[]},h={};for(let y=0;y=r.End)continue;t[r.Style]||(r.Style="Default");const i=t[r.Style].style,o=v({styles:t,style:r.Style,parsed:r.Text.parsed,start:r.Start,end:r.End}),l=o.alignment||i.Alignment;s=Math.min(s,r.Layer),a.push(Object.assign({layer:r.Layer,start:r.Start,end:r.End,style:r.Style,name:r.Name,margin:{left:r.MarginL||i.MarginL,right:r.MarginR||i.MarginR,vertical:r.MarginV||i.MarginV},effect:r.Effect},o,{alignment:l}))}for(let t=0;tt.start-e.start||t.end-e.end))}const S={Name:"Default",Fontname:"Arial",Fontsize:"20",PrimaryColour:"&H00FFFFFF&",SecondaryColour:"&H000000FF&",OutlineColour:"&H00000000&",BackColour:"&H00000000&",Bold:"0",Italic:"0",Underline:"0",StrikeOut:"0",ScaleX:"100",ScaleY:"100",Spacing:"0",Angle:"0",BorderStyle:"1",Outline:"2",Shadow:"2",Alignment:"2",MarginL:"10",MarginR:"10",MarginV:"10",Encoding:"1"};function $(t){if(/^(&|H|&H)[0-9a-f]{6,}/i.test(t)){const[,e,s]=t.match(/&?H?([0-9a-f]{2})?([0-9a-f]{6})/i);return[e||"00",s]}const e=parseInt(t,10);if(!Number.isNaN(e)){const t=-2147483648;if(e{"Name"===t||"Fontname"===t||/Colour/.test(t)||(s[t]*=1)}));const[r,i]=$(s.PrimaryColour),[o,l]=$(s.SecondaryColour),[c,d]=$(s.OutlineColour),[f,p]=$(s.BackColour),h={fn:s.Fontname,fs:s.Fontsize,c1:i,a1:r,c2:l,a2:o,c3:d,a3:c,c4:p,a4:f,b:Math.abs(s.Bold),i:Math.abs(s.Italic),u:Math.abs(s.Underline),s:Math.abs(s.StrikeOut),fscx:s.ScaleX,fscy:s.ScaleY,fsp:s.Spacing,frz:s.Angle,xbord:s.Outline,ybord:s.Outline,xshad:s.Shadow,yshad:s.Shadow,fe:s.Encoding,q:/^[0-3]$/.test(t.WrapStyle)?1*t.WrapStyle:2};a[s.Name]={style:s,tag:h}}return a}({info:s.info,style:s.styles.style,defaultStyle:e.defaultStyle||{}});return{info:s.info,width:1*s.info.PlayResX||null,height:1*s.info.PlayResY||null,collisions:s.info.Collisions||"Normal",styles:a,dialogues:w({styles:a,dialogues:s.events.dialogue})}}function k(t){return 1-`0x${t}`/255}function M(t){const e=t.match(/(\w\w)(\w\w)(\w\w)(\w\w)/),s=k(e[1]),a=+`0x${e[2]}`,n=+`0x${e[3]}`;return`rgba(${+`0x${e[4]}`},${n},${a},${s})`}function E(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,(t=>{const e=Math.trunc(16*Math.random());return("x"===t?e:3&e|8).toString(16)}))}function C(t,e=[]){const s=document.createElementNS("http://www.w3.org/2000/svg",t);for(let t=0;t{t[e]()}))}const N=document.createElement("div");N.className="ASS-fix-font-size";const L=document.createElement("span");L.textContent="0",N.append(L);const F=2048,R=Object.create(null);function z([t,e]){return[[0,0],[0,1],[1,0],[1,1]].filter((([s,a])=>(s||t)&&(a||e))).map((([s,a])=>[(s||-1)*t,(a||-1)*e]))}function T(t,e){const s=M(`00${t.c3}`),a=t.xbord*e,n=t.ybord*e,r=M(`00${t.c4}`),i=t.blur||t.be||0,o=function(t,e){if(t===e)return[];const s=Math.min(t,e),a=Math.max(t,e);return Array.from({length:Math.ceil(a)-1},((t,e)=>e+1)).concat(a).map((t=>[(a-t)/a*s,t])).map((([s,a])=>t>e?[a,s]:[s,a])).flatMap(z)}(a,n);return[["border-width",2*Math.min(a,n)+"px"],["border-color",s],["border-opacity",k(t.a3)],["border-delta",o.map((([t,e])=>`${t}px ${e}px ${s}`)).join(",")],["shadow-color",r],["shadow-opacity",k(t.a4)],["shadow-delta",o.map((([t,e])=>`${t}px ${e}px ${r}`)).join(",")],["tag-blur",i],["tag-xbord",t.xbord],["tag-ybord",t.ybord],["tag-xshad",t.xshad],["tag-yshad",t.yshad]].map((([t,e])=>[`--ass-${t}`,e]))}window.CSS.registerProperty&&(window.CSS.registerProperty({name:"--ass-border-width",syntax:"",inherits:!0,initialValue:"0px"}),["border-color","shadow-color"].forEach((t=>{window.CSS.registerProperty({name:`--ass-${t}`,syntax:"",inherits:!0,initialValue:"transparent"})})),["border-opacity","shadow-opacity"].forEach((t=>{window.CSS.registerProperty({name:`--ass-${t}`,syntax:"",inherits:!0,initialValue:"1"})})),["blur","xbord","ybord","xshad","yshad"].forEach((t=>{window.CSS.registerProperty({name:`--ass-tag-${t}`,syntax:"",inherits:!0,initialValue:"0"})})));const B=["frx","fry","frz"],P=["fscx","fscy"],_=["fax","fay"];function H(t){return[...[...B,..._].map((e=>[`--ass-tag-${e}`,`${t[e]||0}`])),...P.map((e=>[`--ass-tag-${e}`,t.p?1:(t[e]||100)/100]))]}function V({effect:t,duration:e}){const{name:s,delay:a,leftToRight:n}=t;if("banner"===s){return[0,`calc(var(--ass-scale) * ${e/(a||1)*(n?1:-1)}px)`].map(((t,e)=>({offset:e,transform:`translateX(${t})`})))}if(s.startsWith("scroll")){return[{offset:0,transform:"translateY(-100%)"},{offset:1,transform:`translateY(calc(var(--ass-scale) * ${e/(a||1)*(/up/.test(s)?-1:1)}px))`}]}return[]}function q({move:t,duration:e,dialogue:s}){const{x1:a,y1:n,x2:r,y2:i,t1:o,t2:l}=t,c=[o,l||e],d=s.pos||{x:0,y:0};return[[a,n],[r,i]].map((([t,e])=>[t-d.x,e-d.y])).map((([t,s],a)=>({offset:Math.min(c[a]/e,1),transform:`translate(calc(var(--ass-scale) * ${t}px), calc(var(--ass-scale) * ${s}px))`})))}function I(t,e){if("fad"===t.type){const{t1:s,t2:a}=t,n=[];return s&&n.push([0,0]),se?n.push([0,(a-e)/a]):s+a>e&&n.push([(s+.5)/e,1-(s+a-e)/a]),a&&n.push([1,0])):n.push([1,e/s]),n.map((([t,e])=>({offset:t,opacity:e})))}const{a1:s,a2:a,a3:n,t1:r,t2:i,t3:o,t4:l}=t,c=[s,a,n].map((t=>1-t/255));return[0,r,i,o,l,e].map((t=>t/e)).map(((t,e)=>({offset:t,opacity:c[e>>1]}))).filter((({offset:t})=>t<=1))}function U({fromTag:t,tag:e,fragment:s}){const a={...t,...e};return s.drawing&&(Object.assign(a,{p:0,fscx:(e.fscx||t.fscx)/t.fscx*100,fscy:(e.fscy||t.fscy)/t.fscy*100}),Object.assign(t,{fscx:100,fscy:100})),Object.fromEntries(H(a))}function D(t){return[["real-fs",(e=t.fn,s=t.fs,R[e]||(L.style.fontFamily=e,R[e]=L.clientHeight),s*F/R[e])],["tag-fs",t.fs],["tag-fsp",t.fsp],["fill-color",M(t.a1+t.c1)]].filter((([,t])=>t)).map((([t,e])=>[`--ass-${t}`,e]));var e,s}function Y(t,e){const{id:s,clip:a}=t;if(!a)return{};const{width:n,height:r}=e.scriptRes;!function(t,e,s,a,n){if(t.querySelector(`#${s}`))return;let r="";if(null!==e.dots){let{x1:t,y1:s,x2:i,y2:o}=e.dots;t/=a,s/=n,i/=a,o/=n,r=`M${t},${s}L${t},${o},${i},${o},${i},${s}Z`}null!==e.drawing&&(r=e.drawing.instructions.map((({type:t,points:e})=>t+e.map((({x:t,y:e})=>`${t/a},${e/n}`)).join(","))).join(""));const i=1/(1<(m=(t=>{const e=g.left.width[t],s=g.center.width[t],a=g.right.width[t],n=g.left.end[t],r=g.center.end[t],i=g.right.end[t];return"left"===y&&(n>u&&e||r>u&&s&&2*o+s>f||i>u&&a&&o+a>f)||"center"===y&&(n>u&&e&&2*e+o>f||r>u&&s||i>u&&a&&2*a+o>f)||"right"===y&&(n>u&&e&&e+o>f||r>u&&s&&2*o+s>f||i>u&&a)})(t)?0:m+1,m>=l&&(x=t,!0));if(c<=3){x=p-h-1;for(let t=x;t>h&&!b(t);t-=1);}else if(c>=7){x=h+1;for(let t=x;t>1;for(let t=x;t3&&(x-=l-1);for(let t=x;t{n.style.setProperty(t,e)}));const d={duration:1e3*(c-l),delay:1e3*Math.min(0,l-(s.currentTime-e.delay)),fill:"forwards"},f=[];return o.forEach((t=>{const s=a[t.style].tag,n=a[t.style].style.BorderStyle;t.fragments.forEach((t=>{const{text:a,drawing:i}=t,o={...s,...t.tag};let l="";const c=[];if(!i){c.push(...D(o));const t=e.sbas?e.scale:1;c.push(...T(o,t)),l+=`font-family:"${o.fn}";`,l+=o.b?`font-weight:${1===o.b?"bold":o.b};`:"",l+=o.i?"font-style:italic;":"",l+=o.u||o.s?`text-decoration:${o.u?"underline":""} ${o.s?"line-through":""};`:""}if(i&&o.pbo){const t=-o.pbo*(o.fscy||100)/100;l+=`vertical-align:calc(var(--ass-scale) * ${t}px);`}c.push(...H(o));const p=[o,...(o.t||[]).map((t=>t.tag))],h=B.some((t=>p.some((e=>e[t])))),u=P.some((t=>p.some((e=>void 0!==e[t]&&100!==e[t])))),g=_.some((t=>p.some((e=>e[t]))));(function(t,e){return t.replace(/\\h/g," ").replace(/\\N/g,"\n").replace(/\\n/g,2===e?"\n":" ")})(a,o.q).split("\n").forEach(((a,p)=>{const y=document.createElement("span"),m=document.createElement("span");if(y.dataset.wrapStyle=o.q,y.dataset.borderStyle=n,(u||g)&&(u&&(m.dataset.scale=""),g&&(m.dataset.skew=""),m.textContent=a),h&&(y.dataset.rotate=""),i){y.dataset.drawing="";const a=W(t,s,e);if(!a)return;y.style.cssText=a.cssText,y.append(a.$svg)}else{if(y.dataset.text="",p&&r.append(document.createElement("br")),!a)return;u||g?y.append(m):y.textContent=a;const t=u||g?m:y;(o.xbord||o.ybord||o.xshad||o.yshad)&&(t.dataset.text=a)}if(y.style.cssText+=l,c.forEach((([t,e])=>{y.style.setProperty(t,e)})),t.keyframes){const e=O(y,t.keyframes,{...d,duration:t.duration});f.push(e)}r.append(y)}))}))})),t.keyframes&&f.push(O(n,t.keyframes,d)),n.append(r),{$div:n,animations:f}}(t,e);Object.assign(t,{$div:s,animations:a}),e.box.append(s);const{width:n}=s.getBoundingClientRect();Object.assign(t,{width:n}),s.style.cssText+=function(t){const{layer:e,align:s,effect:a,pos:n,margin:r}=t;let i="";return e&&(i+=`z-index:${e};`),i+=`text-align:${["left","center","right"][s.h]};`,a||(i+=`max-width:calc(100% - var(--ass-scale) * ${r.left+r.right}px);`,n||(0!==s.h&&(i+=`margin-right:calc(var(--ass-scale) * ${r.right}px);`),2!==s.h&&(i+=`margin-left:calc(var(--ass-scale) * ${r.left}px);`))),i}(t);const{height:r}=s.getBoundingClientRect();Object.assign(t,{height:r});const{x:i,y:o}=function(t,e){const{scale:s}=e,{effect:a,move:n,align:r,width:i,height:o,margin:l,slices:c}=t;let d=0,f=0;if(a&&"banner"===a.name)d=a.lefttoright?-i:e.width,f=[e.height-o-l.vertical,(e.height-o)/2,l.vertical][r.v];else if(t.pos||n){const e=t.pos||{x:0,y:0},a=s*e.x,n=s*e.y;d=[a,a-i/2,a-i][r.h],f=[n-o,n-o/2,n][r.v]}else d=[0,(e.width-i)/2,e.width-i-s*l.right][r.h],f=c.some((t=>t.fragments.some((({keyframes:t})=>t?.length))))?[e.height-o-l.vertical,(e.height-o)/2,l.vertical][r.v]:X(t,e);return{x:d,y:f}}(t,e);return Object.assign(t,{x:i,y:o}),s.style.cssText+=`width:${n}px;height:${r}px;left:${i}px;top:${o}px;`,function(t,e){const{align:s,width:a,height:n,x:r,y:i,$div:o}=t,l={};t.org?(l.x=t.org.x*e,l.y=t.org.y*e):(l.x=[r,r+a/2,r+a][s.h],l.y=[i+n,i+n/2,i][s.v]);for(let t=o.childNodes.length-1;t>=0;t-=1){const e=o.childNodes[t];if(""===e.dataset.rotate){const t=l.x-r-e.offsetLeft,s=l.y-i-e.offsetTop;e.style.cssText+=`transform-origin:${t}px ${s}px;`}}}(t,e.scale),Object.assign(t,Y(t,e)),t.effect?.name?.startsWith("scroll")&&Object.assign(t,function(t,e){const s=document.createElement("div");s.className="ASS-scroll-area",e.box.insertBefore(s,t.$div),s.append(t.$div);const{height:a}=e.scriptRes,{name:n,y1:r,y2:i}=t.effect,o=Math.min(r,i)/a*100,l=(a-Math.max(r,i))/a*100;s.style.cssText+=`top:${o}%;bottom:${l}%;`;const c=/up/.test(n);return t.$div.style.cssText+=c?"top:100%;":"top:0%;",{$div:s}}(t,e)),t}function Z(t){const{box:e}=t;for(;e.lastChild;)e.lastChild.remove();t.actives=[],t.space=[]}function J(t){const{video:e,dialogues:s,actives:a}=t,n=e.currentTime-t.delay;for(let t=a.length-1;t>=0;t-=1){const e=a[t],{end:s}=e;s=s[t.index].start;){if(n{for(let t=0;t{window.CSS.registerProperty({name:`--ass-tag-${t}`,syntax:"",inherits:!0,initialValue:0})})),P.forEach((t=>{window.CSS.registerProperty({name:`--ass-tag-${t}`,syntax:"",inherits:!0,initialValue:1})}))),window.CSS.registerProperty&&(["real-fs","tag-fs","tag-fsp"].forEach((t=>{window.CSS.registerProperty({name:`--ass-${t}`,syntax:"",inherits:!0,initialValue:"0"})})),window.CSS.registerProperty({name:"--ass-fill-color",syntax:"",inherits:!0,initialValue:"transparent"}));return class{#t={video:null,box:document.createElement("div"),svg:C("svg"),defs:C("defs"),observer:null,scale:1,width:0,height:0,scriptRes:{},layoutRes:{},resampledRes:{},index:0,sbas:!0,styles:{},dialogues:[],actives:[],space:[],requestId:0,delay:0};#e;#s;#a;#n;constructor(t,e,{container:s=e.parentNode,resampling:a}={}){if(this.#t.video=e,!s)throw new Error("Missing container.");const{info:n,width:r,height:i,styles:o,dialogues:l}=A(t);this.#t.sbas=/yes/i.test(n.ScaledBorderAndShadow),this.#t.layoutRes={width:1*n.LayoutResX||e.videoWidth||e.clientWidth,height:1*n.LayoutResY||e.videoHeight||e.clientHeight},this.#t.scriptRes={width:r||this.#t.layoutRes.width,height:i||this.#t.layoutRes.height},this.#t.styles=o,this.#t.dialogues=l.map((t=>Object.assign(t,{id:`ASS-${E()}`,align:{h:(t.alignment+2)%3,v:Math.trunc((t.alignment-1)/3)}}))),s.append(N);const{svg:c,defs:d,scriptRes:f,box:p}=this.#t;var h;c.setAttributeNS(null,"viewBox",`0 0 ${f.width} ${f.height}`),c.append(d),s.append(c),p.className="ASS-box",s.append(p),function(t){const e=t.getRootNode()||document,s=e===document?document.head:e;let a=s.querySelector("#ASS-global-style");a||(a=document.createElement("style"),a.type="text/css",a.id="ASS-global-style",a.append(document.createTextNode('.ASS-box{font-family:Arial;overflow:hidden;pointer-events:none;position:absolute}.ASS-dialogue{font-size:0;position:absolute;z-index:0}.ASS-dialogue span{display:inline-block}.ASS-dialogue [data-text]{display:inline-block;color:var(--ass-fill-color);font-size:calc(var(--ass-scale)*var(--ass-real-fs)*1px);line-height:calc(var(--ass-scale)*var(--ass-tag-fs)*1px);letter-spacing:calc(var(--ass-scale)*var(--ass-tag-fsp)*1px)}.ASS-dialogue [data-wrap-style="0"],.ASS-dialogue [data-wrap-style="3"]{text-wrap:balance}.ASS-dialogue [data-wrap-style="1"]{word-break:break-word;white-space:normal}.ASS-dialogue [data-wrap-style="2"]{word-break:normal;white-space:nowrap}.ASS-dialogue [data-border-style="1"]{position:relative}.ASS-dialogue [data-border-style="1"]::after,.ASS-dialogue [data-border-style="1"]::before{content:attr(data-text);position:absolute;top:0;left:0;z-index:-1;filter:blur(calc(var(--ass-tag-blur)*1px))}.ASS-dialogue [data-border-style="1"]::before{color:var(--ass-shadow-color);transform:translate(calc(var(--ass-scale-stroke)*var(--ass-tag-xshad)*1px),calc(var(--ass-scale-stroke)*var(--ass-tag-yshad)*1px));-webkit-text-stroke:var(--ass-border-width) var(--ass-shadow-color);text-shadow:var(--ass-shadow-delta);opacity:var(--ass-shadow-opacity)}.ASS-dialogue [data-border-style="1"]::after{color:transparent;-webkit-text-stroke:var(--ass-border-width) var(--ass-border-color);text-shadow:var(--ass-border-delta);opacity:var(--ass-border-opacity)}.ASS-dialogue [data-border-style="3"]{padding:calc(var(--ass-scale-stroke)*var(--ass-tag-xbord)*1px) calc(var(--ass-scale-stroke)*var(--ass-tag-ybord)*1px);position:relative;filter:blur(calc(var(--ass-tag-blur)*1px))}.ASS-dialogue [data-border-style="3"]::after,.ASS-dialogue [data-border-style="3"]::before{content:"";width:100%;height:100%;position:absolute;z-index:-1}.ASS-dialogue [data-border-style="3"]::before{background-color:var(--ass-shadow-color);left:calc(var(--ass-scale-stroke)*var(--ass-tag-xshad)*1px);top:calc(var(--ass-scale-stroke)*var(--ass-tag-yshad)*1px)}.ASS-dialogue [data-border-style="3"]::after{background-color:var(--ass-border-color);left:0;top:0}@container style(--ass-tag-xbord: 0) and style(--ass-tag-ybord: 0){.ASS-dialogue [data-border-style="3"]::after{background-color:transparent}}@container style(--ass-tag-xshad: 0) and style(--ass-tag-yshad: 0){.ASS-dialogue [data-border-style="3"]::before{background-color:transparent}}.ASS-dialogue [data-rotate]{transform:perspective(312.5px) rotateY(calc(var(--ass-tag-fry)*1deg)) rotateX(calc(var(--ass-tag-frx)*1deg)) rotateZ(calc(var(--ass-tag-frz)*-1deg))}.ASS-dialogue [data-text][data-rotate]{transform-style:preserve-3d;word-break:normal;white-space:nowrap}.ASS-dialogue [data-scale],.ASS-dialogue [data-skew]{display:inline-block;transform:scale(var(--ass-tag-fscx),var(--ass-tag-fscy)) skew(calc(var(--ass-tag-fax)*1rad),calc(var(--ass-tag-fay)*1rad));transform-origin:var(--ass-align-h) var(--ass-align-v)}.ASS-fix-font-size{font-size:2048px;font-family:Arial;line-height:normal;width:0;height:0;position:absolute;visibility:hidden;overflow:hidden}.ASS-clip-area,.ASS-fix-font-size span{position:absolute}.ASS-clip-area{width:100%;height:100%;top:0;left:0}.ASS-scroll-area{position:absolute;width:100%;overflow:hidden}')),s.append(a))}(s),this.#e=(h=this.#t,function(){const t=()=>{J(h),h.requestId=requestAnimationFrame(t)};cancelAnimationFrame(h.requestId),h.requestId=requestAnimationFrame(t),h.actives.forEach((t=>{j(t,"play")}))}),this.#s=function(t){return function(){cancelAnimationFrame(t.requestId),t.requestId=0,t.actives.forEach((t=>{j(t,"pause")}))}}(this.#t),this.#a=K(this.#t),e.addEventListener("play",this.#e),e.addEventListener("pause",this.#s),e.addEventListener("playing",this.#e),e.addEventListener("waiting",this.#s),e.addEventListener("seeking",this.#a),this.#n=function(t,e){const{video:s,box:a,svg:n,layoutRes:r}=e;return function(){const i=s.clientWidth,o=s.clientHeight,l=r.width||s.videoWidth||i,c=r.height||s.videoHeight||o,d=e.scriptRes.width,f=e.scriptRes.height;let p=d,h=f;const u=Math.min(i/l,o/c);"video_width"===t.resampling&&(h=d/l*c),"video_height"===t.resampling&&(p=f/c*l),e.scale=Math.min(i/p,o/h),"script_width"===t.resampling&&(e.scale=u*(l/p)),"script_height"===t.resampling&&(e.scale=u*(c/h));const g=e.scale*p,y=e.scale*h;e.width=g,e.height=y,e.resampledRes={width:p,height:h};const m=`width:${g}px;height:${y}px;top:${(o-y)/2}px;left:${(i-g)/2}px;`;a.style.cssText=m,a.style.setProperty("--ass-scale",e.scale),a.style.setProperty("--ass-scale-stroke",e.sbas?e.scale:1),n.style.cssText=m,K(e)()}}(this,this.#t),this.#n(),this.resampling=a,l.forEach((t=>{!function(t,e){const{start:s,end:a,effect:n,move:r,fade:i,slices:o}=t,l=1e3*(a-s),c=[...n&&!r?V({effect:n,duration:l}):[],...r?q({move:r,duration:l,dialogue:t}):[],...i?I(i,l):[]].sort(((t,e)=>t.offset-e.offset));c.length>0&&Object.assign(t,{keyframes:c}),o.forEach((t=>{const s=e.styles[t.style].tag;t.fragments.forEach((t=>{if(!t.tag.t||0===t.tag.t.length)return;const a={...s,...t.tag},n=(r=t.tag.t,r.reduceRight(((t,e)=>{let s=!1;return t.map((t=>(s=e.t1===t.t1&&e.t2===t.t2&&e.accel===t.accel,{...t,...s?{tag:{...t.tag,...e.tag}}:{}}))).concat(s?[]:e)}),[])).sort(((t,e)=>t.t2-e.t2||t.t1-e.t1));var r;n[0].t1>0&&n.unshift({t1:0,t2:n[0].t1,tag:a}),n.reduce(((t,e)=>{const s={...t,...e.tag};return s.t=null,Object.assign(e.tag,s),s}),{});const i=Math.max(l,...n.map((({t2:t})=>t))),o=n.map((({t2:s,tag:n})=>({offset:s/i,...Object.fromEntries(D({...n,a1:n.a1||a.a1,c1:n.c1||a.c1})),...Object.fromEntries(T({...a,...n},e.sbas?e.scale:1)),...U({fromTag:a,tag:n,fragment:t})}))).sort(((t,e)=>t.offset-e.offset));o.length>0&&Object.assign(t,{keyframes:o,duration:i})}))}))}(t,this.#t)}));const u=new ResizeObserver(this.#n);return u.observe(e),this.#t.observer=u,this}destroy(){const{video:t,box:e,svg:s,observer:a}=this.#t;return this.#s(),Z(this.#t),t.removeEventListener("play",this.#e),t.removeEventListener("pause",this.#s),t.removeEventListener("playing",this.#e),t.removeEventListener("waiting",this.#s),t.removeEventListener("seeking",this.#a),N.remove(),s.remove(),e.remove(),a.unobserve(this.#t.video),this.#t.styles={},this.#t.dialogues=[],this}show(){return this.#t.box.style.visibility="visible",this}hide(){return this.#t.box.style.visibility="hidden",this}#r="video_height";get resampling(){return this.#r}set resampling(t){t!==this.#r&&/^(video|script)_(width|height)$/.test(t)&&(this.#r=t,this.#n())}get delay(){return this.#t.delay}set delay(t){"number"==typeof t&&(this.#t.delay=t,this.#a())}}}(); +var ASS=function(){"use strict";function t(t){var e=t.toLowerCase().trim().split(/\s*;\s*/);return"banner"===e[0]?{name:e[0],delay:1*e[1]||0,leftToRight:1*e[2]||0,fadeAwayWidth:1*e[3]||0}:/^scroll\s/.test(e[0])?{name:e[0],y1:Math.min(1*e[1],1*e[2]),y2:Math.max(1*e[1],1*e[2]),delay:1*e[3]||0,fadeAwayHeight:1*e[4]||0}:""!==t?{name:t}:null}function e(t){return t?t.toLowerCase().replace(/([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?)/g," $1 ").replace(/([mnlbspc])/g," $1 ").trim().replace(/\s+/g," ").split(/\s(?=[mnlbspc])/).map((function(t){return t.split(" ").filter((function(t,e){return!(e&&isNaN(1*t))}))})):[]}var a=["b","i","u","s","fsp","k","K","kf","ko","kt","fe","q","p","pbo","a","an","fscx","fscy","fax","fay","frx","fry","frz","fr","be","blur","bord","xbord","ybord","shad","xshad","yshad"].map((function(t){return{name:t,regex:new RegExp("^"+t+"-?\\d")}}));function r(t){for(var s,n={},i=0;ia.length){var s=r.slice(a.length-1).join();(r=r.slice(0,a.length-1)).push(s)}for(var i,o={},l=0;l-10?1+i/10:1)*a.fs:1*i};if("K"===e)return{kf:i};if("t"===e){var b=i.t1,w=i.accel,S=i.tags,$=i.t2||1e3*(a.end-a.start),A={};return S.forEach((function(t){var e=Object.keys(t)[0];~g.indexOf(e)&&("clip"!==e||t[e].dots)&&Object.assign(A,y(t,e,a))})),{t:{t1:b,t2:$,accel:w,tag:A}}}return(n={})[e]=i,n}var m=[null,1,2,3,null,7,8,9,null,4,5,6],x=["r","a","an","pos","org","move","fade","fad","clip"];function v(t){for(var e,a,r,s,n,i,o,l=t.styles,c=t.style,d=t.parsed,f=t.start,p=t.end,u={q:l[c].tag.q},g=[],v={style:c,fragments:[]},b={},w=0;w=i.End)){e[i.Style]||(i.Style="Default");var o=e[i.Style].style,l=v({styles:e,style:i.Style,parsed:i.Text.parsed,start:i.Start,end:i.End}),c=l.alignment||o.Alignment;r=Math.min(r,i.Layer),s.push(Object.assign({layer:i.Layer,start:i.Start,end:i.End,style:i.Style,name:i.Name,margin:{left:i.MarginL||o.MarginL,right:i.MarginR||o.MarginR,vertical:i.MarginV||o.MarginV},effect:i.Effect},l,{alignment:c}))}}for(var d=0;d{const e=Math.trunc(16*Math.random());return("x"===t?e:3&e|8).toString(16)}))}function z(t,e=[]){const a=document.createElementNS("http://www.w3.org/2000/svg",t);for(let t=0;t{t[e]()}))}function P(t,e){const{name:a,delay:r,leftToRight:s}=t,n="banner"===a?"X":"Y",i={X:s?1:-1,Y:/up/.test(a)?-1:1}[n],o=-100*i;return[[{offset:0,transform:`translate${n}(${o}%)`},{offset:1,transform:`translate${n}(calc(${o}% + var(--ass-scale) * ${e/(r||1)*i}px))`}],{duration:e,fill:"forwards"}]}function H(t){return`calc(var(--ass-scale) * ${t}px)`}function q(t,e){const{x1:a,y1:r,x2:s,y2:n,t1:i,t2:o}=t,l=`translate(${H(a)}, ${H(r)})`,c=`translate(${H(s)}, ${H(n)})`,d=Math.max(o,e);return[[{offset:0,transform:l},i>0?{offset:i/d,transform:l}:null,o>0&&o1-t/255));return[[[0,i,o,l,c].map((t=>t/d)).map(((t,e)=>({offset:t,opacity:f[e>>1]}))),{duration:d,fill:"forwards"}]]}function V(t,e){if(1===e)return"linear";const a=Math.ceil(t/1e3*60);return`linear(${Array.from({length:a+1}).map(((t,r)=>(r/a)**e)).join(",")})`}function D(t,e,a){const r={...a,...e.tag};return(e.tag.t||[]).map((({t1:e,t2:a,accel:s,tag:n})=>{const i=Object.fromEntries(Object.keys(n).flatMap((t=>function(t,e,a){const r=e[a];return void 0===r||"clip"===a?[]:"a1"===a||"c1"===a?[["fill-color",F((e.a1||t.a1)+(e.c1||t.c1))]]:"c3"===a?[["border-color",F(`00${e.c3}`)]]:"a3"===a?[["border-opacity",R(e.a3)]]:"c4"===a?[["shadow-color",F(`00${e.c4}`)]]:"a4"===a?[["shadow-opacity",R(e.a4)]]:"fs"===a?[["real-fs",L(e.fn||t.fn,e.fs)],["tag-fs",r]]:"fscx"===a||"fscy"===a?[[`tag-${a}`,(r||100)/100]]:[[`tag-${a}`,r]]}(r,n,t))).map((([t,e])=>[`--ass-${t}`,e])).concat([["offset",1]])),o=Math.max(0,a-e);return T(t,[i],{duration:o,delay:e,fill:"forwards",easing:V(o,s)})}))}function W(t,e,a){if(!t.dots)return"";const{x1:r,y1:s,x2:n,y2:i}=t.dots;return`polygon(evenodd, ${[[r,s],[r,i],[n,i],[n,s],[r,s]].map((([t,r])=>[t/e,r/a])).concat(t.inverse?[[0,0],[0,1],[1,1],[1,0],[0,0]]:[]).map((t=>t.map((t=>100*t+"%")).join(" "))).join(",")})`}function U(t,e){const{clip:a,animations:r}=t;if(!a)return{};const{width:s,height:n}=e.scriptRes,i=document.createElement("div");return e.box.insertBefore(i,t.$div),i.append(t.$div),i.className="ASS-clip-area",i.style.zIndex=t.$div.style.zIndex,i.style.clipPath=a.dots?W(a,s,n):function(t,e,a,r){if(!t.drawing)return"";const s=r.scale/(1<t+e.map((({x:t,y:e})=>`${t*s},${e*s}`)).join(","))).join("");return t.inverse&&(n+=`M0,0L0,${a},${e},${a},${e},0,0,0Z`),`path(evenodd, "${n}")`}(a,s,n,e),r.push(...function(t,e,a){return e.slices.flatMap((t=>t.fragments)).flatMap((t=>t.tag.t||[])).filter((({tag:t})=>t.clip)).map((({t1:e,t2:r,accel:s,tag:n})=>{const i={offset:1,clipPath:W(n.clip,a.scriptRes.width,a.scriptRes.height)},o=Math.max(0,r-e);return T(t,[i],{duration:o,delay:e,fill:"forwards",easing:V(o,s)})}))}(i,t,e)),{$div:i}}function X([t,e]){return[[0,0],[0,1],[1,0],[1,1]].filter((([a,r])=>(a||t)&&(r||e))).map((([a,r])=>[(a||-1)*t,(r||-1)*e]))}function Y(t,e){const a=F(`00${t.c3}`),r=t.xbord*e,s=t.ybord*e,n=F(`00${t.c4}`),i=t.blur||t.be||0,o=function(t,e){if(t===e)return[];const a=Math.min(t,e),r=Math.max(t,e);return Array.from({length:Math.ceil(r)-1},((t,e)=>e+1)).concat(r).map((t=>[(r-t)/r*a,t])).map((([a,r])=>t>e?[r,a]:[a,r])).flatMap(X)}(r,s);return[["border-width",2*Math.min(r,s)+"px"],["border-color",a],["border-opacity",R(t.a3)],["border-delta",o.map((([t,e])=>`${t}px ${e}px ${a}`)).join(",")],["shadow-color",n],["shadow-opacity",R(t.a4)],["shadow-delta",o.map((([t,e])=>`${t}px ${e}px ${n}`)).join(",")],["tag-blur",i],["tag-xbord",t.xbord],["tag-ybord",t.ybord],["tag-xshad",t.xshad],["tag-yshad",t.yshad]].map((([t,e])=>[`--ass-${t}`,e]))}function G(t,e,a){if(!t.drawing.d)return null;const r={...e,...t.tag},{minX:s,minY:n,width:i,height:o}=t.drawing,l=a.scale/(1<{window.CSS.registerProperty({name:`--ass-${t}`,syntax:"",inherits:!0,initialValue:"0"})})),window.CSS.registerProperty({name:"--ass-fill-color",syntax:"",inherits:!0,initialValue:"transparent"})),window.CSS.registerProperty&&(window.CSS.registerProperty({name:"--ass-border-width",syntax:"",inherits:!0,initialValue:"0px"}),["border-color","shadow-color"].forEach((t=>{window.CSS.registerProperty({name:`--ass-${t}`,syntax:"",inherits:!0,initialValue:"transparent"})})),["border-opacity","shadow-opacity"].forEach((t=>{window.CSS.registerProperty({name:`--ass-${t}`,syntax:"",inherits:!0,initialValue:"1"})})),["blur","xbord","ybord","xshad","yshad"].forEach((t=>{window.CSS.registerProperty({name:`--ass-tag-${t}`,syntax:"",inherits:!0,initialValue:"0"})})));const J=["frx","fry","frz"],K=["fscx","fscy"],Z=["fax","fay"];function Q(t,e){const{styles:a}=e,r=document.createElement("div");r.className="ASS-dialogue",r.dataset.wrapStyle=t.q;const s=document.createDocumentFragment(),{align:n,slices:i}=t;[["--ass-align-h",["0%","50%","100%"][n.h]],["--ass-align-v",["100%","50%","0%"][n.v]]].forEach((([t,e])=>{r.style.setProperty(t,e)}));const o=[];return i.forEach((t=>{const r=a[t.style].tag,n=a[t.style].style.BorderStyle;t.fragments.forEach((t=>{const{text:a,drawing:i}=t,l={...r,...t.tag};let c="";const d=[];if(!i){d.push(...function(t){return[["real-fs",L(t.fn,t.fs)],["tag-fs",t.fs],["tag-fsp",t.fsp],["fill-color",F(t.a1+t.c1)]].filter((([,t])=>t)).map((([t,e])=>[`--ass-${t}`,e]))}(l));const t=e.sbas?e.scale:1;d.push(...Y(l,t)),c+=`font-family:"${l.fn}";`,c+=l.b?`font-weight:${1===l.b?"bold":l.b};`:"",c+=l.i?"font-style:italic;":"",c+=l.u||l.s?`text-decoration:${l.u?"underline":""} ${l.s?"line-through":""};`:""}if(i&&l.pbo){const t=-l.pbo*(l.fscy||100)/100;c+=`vertical-align:calc(var(--ass-scale) * ${t}px);`}d.push(...function(t){return[...[...J,...Z].map((e=>[`--ass-tag-${e}`,`${t[e]||0}`])),...K.map((e=>[`--ass-tag-${e}`,t.p?1:(t[e]||100)/100]))]}(l));const f=[l,...(l.t||[]).map((t=>t.tag))],p=J.some((t=>f.some((e=>e[t])))),u=K.some((t=>f.some((e=>void 0!==e[t]&&100!==e[t])))),h=Z.some((t=>f.some((e=>e[t]))));(function(t,e){return t.replace(/\\h/g," ").replace(/\\N/g,"\n").replace(/\\n/g,2===e?"\n":" ")})(a,l.q).split("\n").forEach(((a,f)=>{const g=document.createElement("span"),y=document.createElement("span");if((u||h)&&(u&&(y.dataset.scale=""),h&&(y.dataset.skew=""),y.textContent=a),p&&(g.dataset.rotate=""),i){g.dataset.drawing="";const a=G(t,r,e);if(!a)return;g.style.cssText=a.cssText,g.append(a.$svg)}else{if(f&&s.append(document.createElement("br")),!a)return;u||h?g.append(y):g.textContent=a;const t=u||h?y:g;t.dataset.text=a,(l.xbord||l.ybord||l.xshad||l.yshad)&&(t.dataset.borderStyle=n)}g.style.cssText+=c,d.forEach((([t,e])=>{g.style.setProperty(t,e)})),o.push(...D(g,t,r)),s.append(g)}))}))})),o.push(...function(t,e){const{start:a,end:r,effect:s,move:n,fade:i}=e,o=1e3*(r-a);return[s&&!n?P(s,o):null,n?q(n,o):null,...i?I(i,o):[]].filter(Boolean).map((([e,a])=>T(t,e,a)))}(r,t)),r.append(s),{$div:r,animations:o}}function tt(t,e){const{video:a,space:r,scale:s}=e,{layer:n,margin:i,width:o,height:l,alignment:c,end:d}=t,f=e.width-Math.trunc(s*(i.left+i.right)),p=e.height,u=Math.trunc(s*i.vertical),h=100*a.currentTime;r[n]=r[n]||{left:{width:new Uint16Array(p+1),end:new Uint32Array(p+1)},center:{width:new Uint16Array(p+1),end:new Uint32Array(p+1)},right:{width:new Uint16Array(p+1),end:new Uint32Array(p+1)}};const g=r[n],y=["right","left","center"][c%3];let m=0,x=0;const v=t=>(m=(t=>{const e=g.left.width[t],a=g.center.width[t],r=g.right.width[t],s=g.left.end[t],n=g.center.end[t],i=g.right.end[t];return"left"===y&&(s>h&&e||n>h&&a&&2*o+a>f||i>h&&r&&o+r>f)||"center"===y&&(s>h&&e&&2*e+o>f||n>h&&a||i>h&&r&&2*r+o>f)||"right"===y&&(s>h&&e&&e+o>f||n>h&&a&&2*o+a>f||i>h&&r)})(t)?0:m+1,m>=l&&(x=t,!0));if(c<=3){x=p-u-1;for(let t=x;t>u&&!v(t);t-=1);}else if(c>=7){x=u+1;for(let t=x;t>1;for(let t=x;t3&&(x-=l-1);for(let t=x;tt.fragments.some((({keyframes:t})=>t?.length))))?[e.height-i-o.vertical,(e.height-i)/2,o.vertical][s.v]:tt(t,e);return{x:c+[0,n/2,n][s.h],y:d+[i,i/2,0][s.v]}}(t,e);return Object.assign(t,{x:i,y:o}),a.style.cssText+=`left:${i}px;top:${o}px;`,function(t,e){const{align:a,width:r,height:s,x:n,y:i,$div:o}=t,l=(t.org?t.org.x*e:n)+[0,r/2,r][a.h],c=(t.org?t.org.y*e:i)+[s,s/2,0][a.v];for(let t=o.childNodes.length-1;t>=0;t-=1){const e=o.childNodes[t];if(""===e.dataset.rotate){const t=l-n-e.offsetLeft,a=c-i-e.offsetTop;e.style.cssText+=`transform-origin:${t}px ${a}px;`}}}(t,e.scale),Object.assign(t,U(t,e)),t.effect&&Object.assign(t,{$div:et(t,e)}),t}function rt(t){const{box:e}=t;for(;e.lastChild;)e.lastChild.remove();t.actives=[],t.space=[]}function st(t){const{video:e,dialogues:a,actives:r}=t,s=e.currentTime-t.delay;for(let t=r.length-1;t>=0;t-=1){const e=r[t],{end:a}=e;a=a[t.index].start;){if(s{t.currentTime=1e3*(s-n.start)})),e.paused||_(n,"play"),r.push(n)}t.index+=1}}function nt(t){return function(){rt(t);const{video:e,dialogues:a}=t,r=e.currentTime-t.delay;t.index=(()=>{for(let t=0;t{window.CSS.registerProperty({name:`--ass-tag-${t}`,syntax:"",inherits:!0,initialValue:0})})),K.forEach((t=>{window.CSS.registerProperty({name:`--ass-tag-${t}`,syntax:"",inherits:!0,initialValue:1})})));return class{#t={video:null,box:document.createElement("div"),observer:null,scale:1,width:0,height:0,scriptRes:{},layoutRes:{},resampledRes:{},index:0,sbas:!0,styles:{},dialogues:[],actives:[],space:[],requestId:0,delay:0};#e;#a;#r;#s;constructor(t,e,{container:a=e.parentNode,resampling:r}={}){if(this.#t.video=e,!a)throw new Error("Missing container.");const{info:s,width:n,height:i,styles:o,dialogues:l}=$(t);this.#t.sbas=/yes/i.test(s.ScaledBorderAndShadow),this.#t.layoutRes={width:1*s.LayoutResX||e.videoWidth||e.clientWidth,height:1*s.LayoutResY||e.videoHeight||e.clientHeight},this.#t.scriptRes={width:n||this.#t.layoutRes.width,height:i||this.#t.layoutRes.height},this.#t.styles=o,this.#t.dialogues=l.map((t=>Object.assign(t,{effect:["banner","scroll up","scroll down"].includes(t.effect?.name)?t.effect:null,align:{h:(t.alignment+2)%3,v:Math.trunc((t.alignment-1)/3)}}))),N&&a.append(N);const{box:c}=this.#t;var d;c.className="ASS-box",a.append(c),function(t){const e=t.getRootNode()||document,a=e===document?document.head:e;let r=a.querySelector("#ASS-global-style");r||(r=document.createElement("style"),r.type="text/css",r.id="ASS-global-style",r.append(document.createTextNode('.ASS-box{font-family:Arial;overflow:hidden;pointer-events:none;position:absolute}.ASS-dialogue{font-size:0;width:max-content;position:absolute;z-index:0;transform:translate(calc(var(--ass-align-h)*-1),calc(var(--ass-align-v)*-1))}.ASS-dialogue span{display:inline-block}.ASS-dialogue [data-text]{display:inline-block;color:var(--ass-fill-color);font-size:calc(var(--ass-scale)*var(--ass-real-fs)*1px);line-height:calc(var(--ass-scale)*var(--ass-tag-fs)*1px);letter-spacing:calc(var(--ass-scale)*var(--ass-tag-fsp)*1px)}.ASS-dialogue[data-wrap-style="0"],.ASS-dialogue[data-wrap-style="3"]{text-wrap:balance;white-space:pre-wrap}.ASS-dialogue[data-wrap-style="1"]{word-break:break-word;white-space:normal}.ASS-dialogue[data-wrap-style="2"]{word-break:normal;white-space:nowrap}.ASS-dialogue [data-border-style="1"]{position:relative;filter:blur(calc(var(--ass-tag-blur)*calc(1 - sign(var(--ass-tag-xbord)))*calc(1 - sign(var(--ass-tag-ybord)))*1px))}.ASS-dialogue [data-border-style="1"]::after,.ASS-dialogue [data-border-style="1"]::before{content:attr(data-text);position:absolute;top:0;left:0;z-index:-1;filter:blur(calc(var(--ass-tag-blur)*1px))}.ASS-dialogue [data-border-style="1"]::before{color:var(--ass-shadow-color);transform:translate(calc(var(--ass-scale-stroke)*var(--ass-tag-xshad)*1px),calc(var(--ass-scale-stroke)*var(--ass-tag-yshad)*1px));-webkit-text-stroke:var(--ass-border-width) var(--ass-shadow-color);text-shadow:var(--ass-shadow-delta);opacity:var(--ass-shadow-opacity)}.ASS-dialogue [data-border-style="1"]::after{color:transparent;-webkit-text-stroke:var(--ass-border-width) var(--ass-border-color);text-shadow:var(--ass-border-delta);opacity:var(--ass-border-opacity)}.ASS-dialogue [data-border-style="3"]{padding:calc(var(--ass-scale-stroke)*var(--ass-tag-xbord)*1px) calc(var(--ass-scale-stroke)*var(--ass-tag-ybord)*1px);position:relative;filter:blur(calc(var(--ass-tag-blur)*1px))}.ASS-dialogue [data-border-style="3"]::after,.ASS-dialogue [data-border-style="3"]::before{content:"";width:100%;height:100%;position:absolute;z-index:-1}.ASS-dialogue [data-border-style="3"]::before{background-color:var(--ass-shadow-color);left:calc(var(--ass-scale-stroke)*var(--ass-tag-xshad)*1px);top:calc(var(--ass-scale-stroke)*var(--ass-tag-yshad)*1px)}.ASS-dialogue [data-border-style="3"]::after{background-color:var(--ass-border-color);left:0;top:0}@container style(--ass-tag-xbord: 0) and style(--ass-tag-ybord: 0){.ASS-dialogue [data-border-style="3"]::after{background-color:transparent}}@container style(--ass-tag-xshad: 0) and style(--ass-tag-yshad: 0){.ASS-dialogue [data-border-style="3"]::before{background-color:transparent}}.ASS-dialogue [data-rotate]{transform:perspective(312.5px) rotateY(calc(var(--ass-tag-fry)*1deg)) rotateX(calc(var(--ass-tag-frx)*1deg)) rotateZ(calc(var(--ass-tag-frz)*-1deg))}.ASS-dialogue [data-text][data-rotate]{transform-style:preserve-3d;word-break:normal;white-space:nowrap}.ASS-dialogue [data-scale],.ASS-dialogue [data-skew]{display:inline-block;transform:scale(var(--ass-tag-fscx),var(--ass-tag-fscy)) skew(calc(var(--ass-tag-fax)*1rad),calc(var(--ass-tag-fay)*1rad));transform-origin:var(--ass-align-h) var(--ass-align-v)}.ASS-fix-font-size{font-family:Arial;line-height:normal;width:0;height:0;position:absolute;visibility:hidden;overflow:hidden}.ASS-clip-area,.ASS-fix-font-size span{position:absolute}.ASS-clip-area{width:100%;height:100%;top:0;left:0}.ASS-effect-area{position:absolute;display:flex;width:100%;height:fit-content;overflow:hidden;mask-composite:intersect}.ASS-effect-area[data-effect=banner]{flex-direction:column;height:100%}.ASS-effect-area .ASS-dialogue{position:static;transform:none}')),a.append(r))}(a),this.#e=(d=this.#t,function(){const t=()=>{st(d),d.requestId=requestAnimationFrame(t)};cancelAnimationFrame(d.requestId),d.requestId=requestAnimationFrame(t),d.actives.forEach((t=>{_(t,"play")}))}),this.#a=function(t){return function(){cancelAnimationFrame(t.requestId),t.requestId=0,t.actives.forEach((t=>{_(t,"pause")}))}}(this.#t),this.#r=nt(this.#t),e.addEventListener("play",this.#e),e.addEventListener("pause",this.#a),e.addEventListener("playing",this.#e),e.addEventListener("waiting",this.#a),e.addEventListener("seeking",this.#r),this.#s=function(t,e){const{video:a,box:r,layoutRes:s}=e;return function(){const n=a.clientWidth,i=a.clientHeight,o=s.width||a.videoWidth||n,l=s.height||a.videoHeight||i,c=e.scriptRes.width,d=e.scriptRes.height;let f=c,p=d;const u=Math.min(n/o,i/l);"video_width"===t.resampling&&(p=c/o*l),"video_height"===t.resampling&&(f=d/l*o),e.scale=Math.min(n/f,i/p),"script_width"===t.resampling&&(e.scale=u*(o/f)),"script_height"===t.resampling&&(e.scale=u*(l/p));const h=e.scale*f,g=e.scale*p;e.width=h,e.height=g,e.resampledRes={width:f,height:p},r.style.cssText=`width:${h}px;height:${g}px;top:${(i-g)/2}px;left:${(n-h)/2}px;`,r.style.setProperty("--ass-scale",e.scale),r.style.setProperty("--ass-scale-stroke",e.sbas?e.scale:1),nt(e)()}}(this,this.#t),this.#s(),this.resampling=r;const f=new ResizeObserver(this.#s);return f.observe(e),this.#t.observer=f,this}destroy(){const{video:t,box:e,observer:a}=this.#t;return this.#a(),rt(this.#t),t.removeEventListener("play",this.#e),t.removeEventListener("pause",this.#a),t.removeEventListener("playing",this.#e),t.removeEventListener("waiting",this.#a),t.removeEventListener("seeking",this.#r),N&&N.remove(),e.remove(),a.unobserve(this.#t.video),this.#t.styles={},this.#t.dialogues=[],this}show(){return this.#t.box.style.visibility="visible",this}hide(){return this.#t.box.style.visibility="hidden",this}#n="video_height";get resampling(){return this.#n}set resampling(t){t!==this.#n&&/^(video|script)_(width|height)$/.test(t)&&(this.#n=t,this.#s())}get delay(){return this.#t.delay}set delay(t){"number"==typeof t&&(this.#t.delay=t,this.#r())}}}(); diff --git a/dist/ass.js b/dist/ass.js index 7f00d4a..14c1758 100644 --- a/dist/ass.js +++ b/dist/ass.js @@ -1,5 +1,5 @@ function parseEffect(text) { - const param = text + var param = text .toLowerCase() .trim() .split(/\s*;\s*/); @@ -27,7 +27,7 @@ function parseEffect(text) { } function parseDrawing(text) { - if (!text) return []; + if (!text) { return []; } return text .toLowerCase() // numbers @@ -37,26 +37,29 @@ function parseDrawing(text) { .trim() .replace(/\s+/g, ' ') .split(/\s(?=[mnlbspc])/) - .map((cmd) => ( + .map(function (cmd) { return ( cmd.split(' ') - .filter((x, i) => !(i && Number.isNaN(x * 1))) - )); + .filter(function (x, i) { return !(i && isNaN(x * 1)); }) + ); }); } -const numTags = [ +var numTags = [ 'b', 'i', 'u', 's', 'fsp', 'k', 'K', 'kf', 'ko', 'kt', 'fe', 'q', 'p', 'pbo', 'a', 'an', 'fscx', 'fscy', 'fax', 'fay', 'frx', 'fry', 'frz', 'fr', - 'be', 'blur', 'bord', 'xbord', 'ybord', 'shad', 'xshad', 'yshad', -]; + 'be', 'blur', 'bord', 'xbord', 'ybord', 'shad', 'xshad', 'yshad' ]; -const numRegexs = numTags.map((nt) => ({ name: nt, regex: new RegExp(`^${nt}-?\\d`) })); +var numRegexs = numTags.map(function (nt) { return ({ name: nt, regex: new RegExp(("^" + nt + "-?\\d")) }); }); function parseTag(text) { - const tag = {}; - for (let i = 0; i < numRegexs.length; i++) { - const { name, regex } = numRegexs[i]; + var assign; + + var tag = {}; + for (var i = 0; i < numRegexs.length; i++) { + var ref = numRegexs[i]; + var name = ref.name; + var regex = ref.regex; if (regex.test(text)) { tag[name] = text.slice(name.length) * 1; return tag; @@ -69,22 +72,28 @@ function parseTag(text) { } else if (/^fs[\d+-]/.test(text)) { tag.fs = text.slice(2); } else if (/^\d?c&?H?[0-9a-fA-F]+|^\d?c$/.test(text)) { - const [, num, color] = text.match(/^(\d?)c&?H?(\w*)/); - tag[`c${num || 1}`] = color && `000000${color}`.slice(-6); + var ref$1 = text.match(/^(\d?)c&?H?(\w*)/); + var num = ref$1[1]; + var color = ref$1[2]; + tag[("c" + (num || 1))] = color && ("000000" + color).slice(-6); } else if (/^\da&?H?[0-9a-fA-F]+/.test(text)) { - const [, num, alpha] = text.match(/^(\d)a&?H?([0-9a-f]+)/i); - tag[`a${num}`] = `00${alpha}`.slice(-2); + var ref$2 = text.match(/^(\d)a&?H?([0-9a-f]+)/i); + var num$1 = ref$2[1]; + var alpha = ref$2[2]; + tag[("a" + num$1)] = ("00" + alpha).slice(-2); } else if (/^alpha&?H?[0-9a-fA-F]+/.test(text)) { - [, tag.alpha] = text.match(/^alpha&?H?([0-9a-f]+)/i); - tag.alpha = `00${tag.alpha}`.slice(-2); + (assign = text.match(/^alpha&?H?([0-9a-f]+)/i), tag.alpha = assign[1]); + tag.alpha = ("00" + (tag.alpha)).slice(-2); } else if (/^(?:pos|org|move|fad|fade)\([^)]+/.test(text)) { - const [, key, value] = text.match(/^(\w+)\((.*?)\)?$/); + var ref$3 = text.match(/^(\w+)\((.*?)\)?$/); + var key = ref$3[1]; + var value = ref$3[2]; tag[key] = value .trim() .split(/\s*,\s*/) .map(Number); } else if (/^i?clip\([^)]+/.test(text)) { - const p = text + var p = text .match(/^i?clip\((.*?)\)?$/)[1] .trim() .split(/\s*,\s*/); @@ -105,33 +114,33 @@ function parseTag(text) { tag.clip.dots = p.map(Number); } } else if (/^t\(/.test(text)) { - const p = text + var p$1 = text .match(/^t\((.*?)\)?$/)[1] .trim() - .replace(/\\.*/, (x) => x.replace(/,/g, '\n')) + .replace(/\\.*/, function (x) { return x.replace(/,/g, '\n'); }) .split(/\s*,\s*/); - if (!p[0]) return tag; + if (!p$1[0]) { return tag; } tag.t = { t1: 0, t2: 0, accel: 1, - tags: p[p.length - 1] + tags: p$1[p$1.length - 1] .replace(/\n/g, ',') .split('\\') .slice(1) .map(parseTag), }; - if (p.length === 2) { - tag.t.accel = p[0] * 1; + if (p$1.length === 2) { + tag.t.accel = p$1[0] * 1; } - if (p.length === 3) { - tag.t.t1 = p[0] * 1; - tag.t.t2 = p[1] * 1; + if (p$1.length === 3) { + tag.t.t1 = p$1[0] * 1; + tag.t.t2 = p$1[1] * 1; } - if (p.length === 4) { - tag.t.t1 = p[0] * 1; - tag.t.t2 = p[1] * 1; - tag.t.accel = p[2] * 1; + if (p$1.length === 4) { + tag.t.t1 = p$1[0] * 1; + tag.t.t2 = p$1[1] * 1; + tag.t.accel = p$1[2] * 1; } } @@ -139,17 +148,17 @@ function parseTag(text) { } function parseTags(text) { - const tags = []; - let depth = 0; - let str = ''; + var tags = []; + var depth = 0; + var str = ''; // `\b\c` -> `b\c\` // `a\b\c` -> `b\c\` - const transText = text.split('\\').slice(1).concat('').join('\\'); - for (let i = 0; i < transText.length; i++) { - const x = transText[i]; - if (x === '(') depth++; - if (x === ')') depth--; - if (depth < 0) depth = 0; + var transText = text.split('\\').slice(1).concat('').join('\\'); + for (var i = 0; i < transText.length; i++) { + var x = transText[i]; + if (x === '(') { depth++; } + if (x === ')') { depth--; } + if (depth < 0) { depth = 0; } if (!depth && x === '\\') { if (str) { tags.push(str); @@ -163,44 +172,44 @@ function parseTags(text) { } function parseText(text) { - const pairs = text.split(/{([^{}]*?)}/); - const parsed = []; + var pairs = text.split(/{([^{}]*?)}/); + var parsed = []; if (pairs[0].length) { parsed.push({ tags: [], text: pairs[0], drawing: [] }); } - for (let i = 1; i < pairs.length; i += 2) { - const tags = parseTags(pairs[i]); - const isDrawing = tags.reduce((v, tag) => (tag.p === undefined ? v : !!tag.p), false); + for (var i = 1; i < pairs.length; i += 2) { + var tags = parseTags(pairs[i]); + var isDrawing = tags.reduce(function (v, tag) { return (tag.p === undefined ? v : !!tag.p); }, false); parsed.push({ - tags, + tags: tags, text: isDrawing ? '' : pairs[i + 1], drawing: isDrawing ? parseDrawing(pairs[i + 1]) : [], }); } return { raw: text, - combined: parsed.map((frag) => frag.text).join(''), - parsed, + combined: parsed.map(function (frag) { return frag.text; }).join(''), + parsed: parsed, }; } function parseTime(time) { - const t = time.split(':'); + var t = time.split(':'); return t[0] * 3600 + t[1] * 60 + t[2] * 1; } function parseDialogue(text, format) { - let fields = text.split(','); + var fields = text.split(','); if (fields.length > format.length) { - const textField = fields.slice(format.length - 1).join(); + var textField = fields.slice(format.length - 1).join(); fields = fields.slice(0, format.length - 1); fields.push(textField); } - const dia = {}; - for (let i = 0; i < fields.length; i++) { - const fmt = format[i]; - const fld = fields[i].trim(); + var dia = {}; + for (var i = 0; i < fields.length; i++) { + var fmt = format[i]; + var fld = fields[i].trim(); switch (fmt) { case 'Layer': case 'MarginL': @@ -226,45 +235,51 @@ function parseDialogue(text, format) { return dia; } -const stylesFormat = ['Name', 'Fontname', 'Fontsize', 'PrimaryColour', 'SecondaryColour', 'OutlineColour', 'BackColour', 'Bold', 'Italic', 'Underline', 'StrikeOut', 'ScaleX', 'ScaleY', 'Spacing', 'Angle', 'BorderStyle', 'Outline', 'Shadow', 'Alignment', 'MarginL', 'MarginR', 'MarginV', 'Encoding']; -const eventsFormat = ['Layer', 'Start', 'End', 'Style', 'Name', 'MarginL', 'MarginR', 'MarginV', 'Effect', 'Text']; +var stylesFormat = ['Name', 'Fontname', 'Fontsize', 'PrimaryColour', 'SecondaryColour', 'OutlineColour', 'BackColour', 'Bold', 'Italic', 'Underline', 'StrikeOut', 'ScaleX', 'ScaleY', 'Spacing', 'Angle', 'BorderStyle', 'Outline', 'Shadow', 'Alignment', 'MarginL', 'MarginR', 'MarginV', 'Encoding']; +var eventsFormat = ['Layer', 'Start', 'End', 'Style', 'Name', 'MarginL', 'MarginR', 'MarginV', 'Effect', 'Text']; function parseFormat(text) { - const fields = stylesFormat.concat(eventsFormat); + var fields = stylesFormat.concat(eventsFormat); return text.match(/Format\s*:\s*(.*)/i)[1] .split(/\s*,\s*/) - .map((field) => { - const caseField = fields.find((f) => f.toLowerCase() === field.toLowerCase()); + .map(function (field) { + var caseField = fields.find(function (f) { return f.toLowerCase() === field.toLowerCase(); }); return caseField || field; }); } function parseStyle(text, format) { - const values = text.match(/Style\s*:\s*(.*)/i)[1].split(/\s*,\s*/); - return Object.assign({}, ...format.map((fmt, idx) => ({ [fmt]: values[idx] }))); + var values = text.match(/Style\s*:\s*(.*)/i)[1].split(/\s*,\s*/); + return Object.assign.apply(Object, [ {} ].concat( format.map(function (fmt, idx) { + var obj; + + return (( obj = {}, obj[fmt] = values[idx], obj )); + }) )); } function parse(text) { - const tree = { + var tree = { info: {}, styles: { format: [], style: [] }, events: { format: [], comment: [], dialogue: [] }, }; - const lines = text.split(/\r?\n/); - let state = 0; - for (let i = 0; i < lines.length; i++) { - const line = lines[i].trim(); - if (/^;/.test(line)) continue; - - if (/^\[Script Info\]/i.test(line)) state = 1; - else if (/^\[V4\+? Styles\]/i.test(line)) state = 2; - else if (/^\[Events\]/i.test(line)) state = 3; - else if (/^\[.*\]/.test(line)) state = 0; - - if (state === 0) continue; + var lines = text.split(/\r?\n/); + var state = 0; + for (var i = 0; i < lines.length; i++) { + var line = lines[i].trim(); + if (/^;/.test(line)) { continue; } + + if (/^\[Script Info\]/i.test(line)) { state = 1; } + else if (/^\[V4\+? Styles\]/i.test(line)) { state = 2; } + else if (/^\[Events\]/i.test(line)) { state = 3; } + else if (/^\[.*\]/.test(line)) { state = 0; } + + if (state === 0) { continue; } if (state === 1) { if (/:/.test(line)) { - const [, key, value] = line.match(/(.*?)\s*:\s*(.*)/); + var ref = line.match(/(.*?)\s*:\s*(.*)/); + var key = ref[1]; + var value = ref[2]; tree.info[key] = value; } } @@ -281,8 +296,10 @@ function parse(text) { tree.events.format = parseFormat(line); } if (/^(?:Comment|Dialogue)\s*:/i.test(line)) { - const [, key, value] = line.match(/^(\w+?)\s*:\s*(.*)/i); - tree.events[key.toLowerCase()].push(parseDialogue(value, tree.events.format)); + var ref$1 = line.match(/^(\w+?)\s*:\s*(.*)/i); + var key$1 = ref$1[1]; + var value$1 = ref$1[2]; + tree.events[key$1.toLowerCase()].push(parseDialogue(value$1, tree.events.format)); } } } @@ -291,7 +308,7 @@ function parse(text) { } function createCommand(arr) { - const cmd = { + var cmd = { type: null, prev: null, next: null, @@ -303,7 +320,7 @@ function createCommand(arr) { .replace('N', 'L') .replace('B', 'C'); } - for (let len = arr.length - !(arr.length & 1), i = 1; i < len; i += 2) { + for (var len = arr.length - !(arr.length & 1), i = 1; i < len; i += 2) { cmd.points.push({ x: arr[i] * 1, y: arr[i + 1] * 1 }); } return cmd; @@ -320,19 +337,28 @@ function isValid(cmd) { } function getViewBox(commands) { - let minX = Infinity; - let minY = Infinity; - let maxX = -Infinity; - let maxY = -Infinity; - [].concat(...commands.map(({ points }) => points)).forEach(({ x, y }) => { + var ref; + + var minX = Infinity; + var minY = Infinity; + var maxX = -Infinity; + var maxY = -Infinity; + (ref = []).concat.apply(ref, commands.map(function (ref) { + var points = ref.points; + + return points; + })).forEach(function (ref) { + var x = ref.x; + var y = ref.y; + minX = Math.min(minX, x); minY = Math.min(minY, y); maxX = Math.max(maxX, x); maxY = Math.max(maxY, y); }); return { - minX, - minY, + minX: minX, + minY: minY, width: maxX - minX, height: maxY - minY, }; @@ -347,18 +373,18 @@ function getViewBox(commands) { * @return {Array} converted commands */ function s2b(points, prev, next) { - const results = []; - const bb1 = [0, 2 / 3, 1 / 3, 0]; - const bb2 = [0, 1 / 3, 2 / 3, 0]; - const bb3 = [0, 1 / 6, 2 / 3, 1 / 6]; - const dot4 = (a, b) => (a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3]); - let px = [points[points.length - 1].x, points[0].x, points[1].x, points[2].x]; - let py = [points[points.length - 1].y, points[0].y, points[1].y, points[2].y]; + var results = []; + var bb1 = [0, 2 / 3, 1 / 3, 0]; + var bb2 = [0, 1 / 3, 2 / 3, 0]; + var bb3 = [0, 1 / 6, 2 / 3, 1 / 6]; + var dot4 = function (a, b) { return (a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3]); }; + var px = [points[points.length - 1].x, points[0].x, points[1].x, points[2].x]; + var py = [points[points.length - 1].y, points[0].y, points[1].y, points[2].y]; results.push({ type: prev === 'M' ? 'M' : 'L', points: [{ x: dot4(bb3, px), y: dot4(bb3, py) }], }); - for (let i = 3; i < points.length; i++) { + for (var i = 3; i < points.length; i++) { px = [points[i - 3].x, points[i - 2].x, points[i - 1].x, points[i].x]; py = [points[i - 3].y, points[i - 2].y, points[i - 1].y, points[i].y]; results.push({ @@ -366,33 +392,46 @@ function s2b(points, prev, next) { points: [ { x: dot4(bb1, px), y: dot4(bb1, py) }, { x: dot4(bb2, px), y: dot4(bb2, py) }, - { x: dot4(bb3, px), y: dot4(bb3, py) }, - ], + { x: dot4(bb3, px), y: dot4(bb3, py) } ], }); } if (next === 'L' || next === 'C') { - const last = points[points.length - 1]; + var last = points[points.length - 1]; results.push({ type: 'L', points: [{ x: last.x, y: last.y }] }); } return results; } function toSVGPath(instructions) { - return instructions.map(({ type, points }) => ( - type + points.map(({ x, y }) => `${x},${y}`).join(',') - )).join(''); + return instructions.map(function (ref) { + var type = ref.type; + var points = ref.points; + + return ( + type + points.map(function (ref) { + var x = ref.x; + var y = ref.y; + + return (x + "," + y); + }).join(',') + ); + }).join(''); } function compileDrawing(rawCommands) { - const commands = []; - let i = 0; + var ref$1; + + var commands = []; + var i = 0; while (i < rawCommands.length) { - const arr = rawCommands[i]; - const cmd = createCommand(arr); + var arr = rawCommands[i]; + var cmd = createCommand(arr); if (isValid(cmd)) { if (cmd.type === 'S') { - const { x, y } = (commands[i - 1] || { points: [{ x: 0, y: 0 }] }).points.slice(-1)[0]; - cmd.points.unshift({ x, y }); + var ref = (commands[i - 1] || { points: [{ x: 0, y: 0 }] }).points.slice(-1)[0]; + var x = ref.x; + var y = ref.y; + cmd.points.unshift({ x: x, y: y }); } if (i) { cmd.prev = commands[i - 1].type; @@ -402,68 +441,100 @@ function compileDrawing(rawCommands) { i++; } else { if (i && commands[i - 1].type === 'S') { - const additionPoints = { + var additionPoints = { p: cmd.points, c: commands[i - 1].points.slice(0, 3), }; commands[i - 1].points = commands[i - 1].points.concat( - (additionPoints[arr[0]] || []).map(({ x, y }) => ({ x, y })), + (additionPoints[arr[0]] || []).map(function (ref) { + var x = ref.x; + var y = ref.y; + + return ({ x: x, y: y }); + }) ); } rawCommands.splice(i, 1); } } - const instructions = [].concat( - ...commands.map(({ type, points, prev, next }) => ( + var instructions = (ref$1 = []).concat.apply( + ref$1, commands.map(function (ref) { + var type = ref.type; + var points = ref.points; + var prev = ref.prev; + var next = ref.next; + + return ( type === 'S' ? s2b(points, prev, next) - : { type, points } - )), + : { type: type, points: points } + ); + }) ); - return Object.assign({ instructions, d: toSVGPath(instructions) }, getViewBox(commands)); + return Object.assign({ instructions: instructions, d: toSVGPath(instructions) }, getViewBox(commands)); } -const tTags = [ +var tTags = [ 'fs', 'fsp', 'clip', 'c1', 'c2', 'c3', 'c4', 'a1', 'a2', 'a3', 'a4', 'alpha', 'fscx', 'fscy', 'fax', 'fay', 'frx', 'fry', 'frz', 'fr', - 'be', 'blur', 'bord', 'xbord', 'ybord', 'shad', 'xshad', 'yshad', -]; + 'be', 'blur', 'bord', 'xbord', 'ybord', 'shad', 'xshad', 'yshad' ]; + +function compileTag(tag, key, presets) { + var obj, obj$1, obj$2; -function compileTag(tag, key, presets = {}) { - let value = tag[key]; + if ( presets === void 0 ) presets = {}; + var value = tag[key]; if (value === undefined) { return null; } if (key === 'pos' || key === 'org') { - return value.length === 2 ? { [key]: { x: value[0], y: value[1] } } : null; + return value.length === 2 ? ( obj = {}, obj[key] = { x: value[0], y: value[1] }, obj ) : null; } if (key === 'move') { - const [x1, y1, x2, y2, t1 = 0, t2 = 0] = value; + var x1 = value[0]; + var y1 = value[1]; + var x2 = value[2]; + var y2 = value[3]; + var t1 = value[4]; if ( t1 === void 0 ) t1 = 0; + var t2 = value[5]; if ( t2 === void 0 ) t2 = 0; return value.length === 4 || value.length === 6 - ? { move: { x1, y1, x2, y2, t1, t2 } } + ? { move: { x1: x1, y1: y1, x2: x2, y2: y2, t1: t1, t2: t2 } } : null; } if (key === 'fad' || key === 'fade') { if (value.length === 2) { - const [t1, t2] = value; - return { fade: { type: 'fad', t1, t2 } }; + var t1$1 = value[0]; + var t2$1 = value[1]; + return { fade: { type: 'fad', t1: t1$1, t2: t2$1 } }; } if (value.length === 7) { - const [a1, a2, a3, t1, t2, t3, t4] = value; - return { fade: { type: 'fade', a1, a2, a3, t1, t2, t3, t4 } }; + var a1 = value[0]; + var a2 = value[1]; + var a3 = value[2]; + var t1$2 = value[3]; + var t2$2 = value[4]; + var t3 = value[5]; + var t4 = value[6]; + return { fade: { type: 'fade', a1: a1, a2: a2, a3: a3, t1: t1$2, t2: t2$2, t3: t3, t4: t4 } }; } return null; } if (key === 'clip') { - const { inverse, scale, drawing, dots } = value; + var inverse = value.inverse; + var scale = value.scale; + var drawing = value.drawing; + var dots = value.dots; if (drawing) { - return { clip: { inverse, scale, drawing: compileDrawing(drawing), dots } }; + return { clip: { inverse: inverse, scale: scale, drawing: compileDrawing(drawing), dots: dots } }; } if (dots) { - const [x1, y1, x2, y2] = dots; - return { clip: { inverse, scale, drawing, dots: { x1, y1, x2, y2 } } }; + var x1$1 = dots[0]; + var y1$1 = dots[1]; + var x2$1 = dots[2]; + var y2$1 = dots[3]; + return { clip: { inverse: inverse, scale: scale, drawing: drawing, dots: { x1: x1$1, y1: y1$1, x2: x2$1, y2: y2$1 } } }; } return null; } @@ -477,7 +548,7 @@ function compileTag(tag, key, presets = {}) { return { xshad: value, yshad: value }; } if (/^c\d$/.test(key)) { - return { [key]: value || presets[key] }; + return ( obj$1 = {}, obj$1[key] = value || presets[key], obj$1 ); } if (key === 'alpha') { return { a1: value, a2: value, a3: value, a4: value }; @@ -496,27 +567,28 @@ function compileTag(tag, key, presets = {}) { return { kf: value }; } if (key === 't') { - const { t1, accel, tags } = value; - const t2 = value.t2 || (presets.end - presets.start) * 1e3; - const compiledTag = {}; - tags.forEach((t) => { - const k = Object.keys(t)[0]; + var t1$3 = value.t1; + var accel = value.accel; + var tags = value.tags; + var t2$3 = value.t2 || (presets.end - presets.start) * 1e3; + var compiledTag = {}; + tags.forEach(function (t) { + var k = Object.keys(t)[0]; if (~tTags.indexOf(k) && !(k === 'clip' && !t[k].dots)) { Object.assign(compiledTag, compileTag(t, k, presets)); } }); - return { t: { t1, t2, accel, tag: compiledTag } }; + return { t: { t1: t1$3, t2: t2$3, accel: accel, tag: compiledTag } }; } - return { [key]: value }; + return ( obj$2 = {}, obj$2[key] = value, obj$2 ); } -const a2an = [ +var a2an = [ null, 1, 2, 3, null, 7, 8, 9, - null, 4, 5, 6, -]; + null, 4, 5, 6 ]; -const globalTags = ['r', 'a', 'an', 'pos', 'org', 'move', 'fade', 'fad', 'clip']; +var globalTags = ['r', 'a', 'an', 'pos', 'org', 'move', 'fade', 'fad', 'clip']; function inheritTag(pTag) { return JSON.parse(JSON.stringify(Object.assign({}, pTag, { @@ -527,42 +599,56 @@ function inheritTag(pTag) { }))); } -function compileText({ styles, style, parsed, start, end }) { - let alignment; - let pos; - let org; - let move; - let fade; - let clip; - const slices = []; - let slice = { style, fragments: [] }; - let prevTag = {}; - for (let i = 0; i < parsed.length; i++) { - const { tags, text, drawing } = parsed[i]; - let reset; - for (let j = 0; j < tags.length; j++) { - const tag = tags[j]; +function compileText(ref) { + var styles = ref.styles; + var style = ref.style; + var parsed = ref.parsed; + var start = ref.start; + var end = ref.end; + + var alignment; + var q = { q: styles[style].tag.q }; + var pos; + var org; + var move; + var fade; + var clip; + var slices = []; + var slice = { style: style, fragments: [] }; + var prevTag = {}; + for (var i = 0; i < parsed.length; i++) { + var ref$1 = parsed[i]; + var tags = ref$1.tags; + var text = ref$1.text; + var drawing = ref$1.drawing; + var reset = (void 0); + for (var j = 0; j < tags.length; j++) { + var tag = tags[j]; reset = tag.r === undefined ? reset : tag.r; } - const fragment = { + var fragment = { tag: reset === undefined ? inheritTag(prevTag) : {}, - text, + text: text, drawing: drawing.length ? compileDrawing(drawing) : null, }; - for (let j = 0; j < tags.length; j++) { - const tag = tags[j]; - alignment = alignment || a2an[tag.a || 0] || tag.an; - pos = pos || compileTag(tag, 'pos'); - org = org || compileTag(tag, 'org'); - move = move || compileTag(tag, 'move'); - fade = fade || compileTag(tag, 'fade') || compileTag(tag, 'fad'); - clip = compileTag(tag, 'clip') || clip; - const key = Object.keys(tag)[0]; + for (var j$1 = 0; j$1 < tags.length; j$1++) { + var tag$1 = tags[j$1]; + alignment = alignment || a2an[tag$1.a || 0] || tag$1.an; + q = compileTag(tag$1, 'q') || q; + pos = pos || compileTag(tag$1, 'pos'); + org = org || compileTag(tag$1, 'org'); + move = move || compileTag(tag$1, 'move'); + fade = fade || compileTag(tag$1, 'fade') || compileTag(tag$1, 'fad'); + clip = compileTag(tag$1, 'clip') || clip; + var key = Object.keys(tag$1)[0]; if (key && !~globalTags.indexOf(key)) { - const sliceTag = styles[style].tag; - const { c1, c2, c3, c4 } = sliceTag; - const fs = prevTag.fs || sliceTag.fs; - const compiledTag = compileTag(tag, key, { start, end, c1, c2, c3, c4, fs }); + var sliceTag = styles[style].tag; + var c1 = sliceTag.c1; + var c2 = sliceTag.c2; + var c3 = sliceTag.c3; + var c4 = sliceTag.c4; + var fs = prevTag.fs || sliceTag.fs; + var compiledTag = compileTag(tag$1, key, { start: start, end: end, c1: c1, c2: c2, c3: c3, c4: c4, fs: fs }); if (key === 't') { fragment.tag.t = fragment.tag.t || []; fragment.tag.t.push(compiledTag.t); @@ -577,7 +663,7 @@ function compileText({ styles, style, parsed, start, end }) { slice = { style: styles[reset] ? reset : style, fragments: [] }; } if (fragment.text || fragment.drawing) { - const prev = slice.fragments[slice.fragments.length - 1] || {}; + var prev = slice.fragments[slice.fragments.length - 1] || {}; if (prev.text && fragment.text && !Object.keys(fragment.tag).length) { // merge fragment to previous if its tag is empty prev.text += fragment.text; @@ -588,29 +674,32 @@ function compileText({ styles, style, parsed, start, end }) { } slices.push(slice); - return Object.assign({ alignment, slices }, pos, org, move, fade, clip); + return Object.assign({ alignment: alignment, slices: slices }, q, pos, org, move, fade, clip); } -function compileDialogues({ styles, dialogues }) { - let minLayer = Infinity; - const results = []; - for (let i = 0; i < dialogues.length; i++) { - const dia = dialogues[i]; +function compileDialogues(ref) { + var styles = ref.styles; + var dialogues = ref.dialogues; + + var minLayer = Infinity; + var results = []; + for (var i = 0; i < dialogues.length; i++) { + var dia = dialogues[i]; if (dia.Start >= dia.End) { continue; } if (!styles[dia.Style]) { dia.Style = 'Default'; } - const stl = styles[dia.Style].style; - const compiledText = compileText({ - styles, + var stl = styles[dia.Style].style; + var compiledText = compileText({ + styles: styles, style: dia.Style, parsed: dia.Text.parsed, start: dia.Start, end: dia.End, }); - const alignment = compiledText.alignment || stl.Alignment; + var alignment = compiledText.alignment || stl.Alignment; minLayer = Math.min(minLayer, dia.Layer); results.push(Object.assign({ layer: dia.Layer, @@ -625,17 +714,17 @@ function compileDialogues({ styles, dialogues }) { vertical: dia.MarginV || stl.MarginV, }, effect: dia.Effect, - }, compiledText, { alignment })); + }, compiledText, { alignment: alignment })); } - for (let i = 0; i < results.length; i++) { - results[i].layer -= minLayer; + for (var i$1 = 0; i$1 < results.length; i$1++) { + results[i$1].layer -= minLayer; } - return results.sort((a, b) => a.start - b.start || a.end - b.end); + return results.sort(function (a, b) { return a.start - b.start || a.end - b.end; }); } // same as Aegisub // https://github.com/Aegisub/Aegisub/blob/master/src/ass_style.h -const DEFAULT_STYLE = { +var DEFAULT_STYLE = { Name: 'Default', Fontname: 'Arial', Fontsize: '20', @@ -667,53 +756,67 @@ const DEFAULT_STYLE = { */ function parseStyleColor(color) { if (/^(&|H|&H)[0-9a-f]{6,}/i.test(color)) { - const [, a, c] = color.match(/&?H?([0-9a-f]{2})?([0-9a-f]{6})/i); + var ref = color.match(/&?H?([0-9a-f]{2})?([0-9a-f]{6})/i); + var a = ref[1]; + var c = ref[2]; return [a || '00', c]; } - const num = parseInt(color, 10); - if (!Number.isNaN(num)) { - const min = -2147483648; - const max = 2147483647; + var num = parseInt(color, 10); + if (!isNaN(num)) { + var min = -2147483648; + var max = 2147483647; if (num < min) { return ['00', '000000']; } - const aabbggrr = (min <= num && num <= max) - ? `00000000${(num < 0 ? num + 4294967296 : num).toString(16)}`.slice(-8) + var aabbggrr = (min <= num && num <= max) + ? ("00000000" + ((num < 0 ? num + 4294967296 : num).toString(16))).slice(-8) : String(num).slice(0, 8); return [aabbggrr.slice(0, 2), aabbggrr.slice(2)]; } return ['00', '000000']; } -function compileStyles({ info, style, defaultStyle }) { - const result = {}; - const styles = [Object.assign({}, defaultStyle, { Name: 'Default' })].concat(style); - for (let i = 0; i < styles.length; i++) { - const s = Object.assign({}, DEFAULT_STYLE, styles[i]); +function compileStyles(ref) { + var info = ref.info; + var style = ref.style; + var defaultStyle = ref.defaultStyle; + + var result = {}; + var styles = [Object.assign({}, defaultStyle, { Name: 'Default' })].concat(style); + var loop = function ( i ) { + var s = Object.assign({}, DEFAULT_STYLE, styles[i]); // this behavior is same as Aegisub by black-box testing if (/^(\*+)Default$/.test(s.Name)) { s.Name = 'Default'; } - Object.keys(s).forEach((key) => { + Object.keys(s).forEach(function (key) { if (key !== 'Name' && key !== 'Fontname' && !/Colour/.test(key)) { s[key] *= 1; } }); - const [a1, c1] = parseStyleColor(s.PrimaryColour); - const [a2, c2] = parseStyleColor(s.SecondaryColour); - const [a3, c3] = parseStyleColor(s.OutlineColour); - const [a4, c4] = parseStyleColor(s.BackColour); - const tag = { + var ref$1 = parseStyleColor(s.PrimaryColour); + var a1 = ref$1[0]; + var c1 = ref$1[1]; + var ref$2 = parseStyleColor(s.SecondaryColour); + var a2 = ref$2[0]; + var c2 = ref$2[1]; + var ref$3 = parseStyleColor(s.OutlineColour); + var a3 = ref$3[0]; + var c3 = ref$3[1]; + var ref$4 = parseStyleColor(s.BackColour); + var a4 = ref$4[0]; + var c4 = ref$4[1]; + var tag = { fn: s.Fontname, fs: s.Fontsize, - c1, - a1, - c2, - a2, - c3, - a3, - c4, - a4, + c1: c1, + a1: a1, + c2: c2, + a2: a2, + c3: c3, + a3: a3, + c4: c4, + a4: a4, b: Math.abs(s.Bold), i: Math.abs(s.Italic), u: Math.abs(s.Underline), @@ -727,16 +830,21 @@ function compileStyles({ info, style, defaultStyle }) { xshad: s.Shadow, yshad: s.Shadow, fe: s.Encoding, + // TODO: [breaking change] remove `q` from style q: /^[0-3]$/.test(info.WrapStyle) ? info.WrapStyle * 1 : 2, }; - result[s.Name] = { style: s, tag }; - } + result[s.Name] = { style: s, tag: tag }; + }; + + for (var i = 0; i < styles.length; i++) loop( i ); return result; } -function compile(text, options = {}) { - const tree = parse(text); - const styles = compileStyles({ +function compile(text, options) { + if ( options === void 0 ) options = {}; + + var tree = parse(text); + var styles = compileStyles({ info: tree.info, style: tree.styles.style, defaultStyle: options.defaultStyle || {}, @@ -745,15 +853,50 @@ function compile(text, options = {}) { info: tree.info, width: tree.info.PlayResX * 1 || null, height: tree.info.PlayResY * 1 || null, + wrapStyle: /^[0-3]$/.test(tree.info.WrapStyle) ? tree.info.WrapStyle * 1 : 2, collisions: tree.info.Collisions || 'Normal', - styles, + styles: styles, dialogues: compileDialogues({ - styles, + styles: styles, dialogues: tree.events.dialogue, }), }; } +// https://github.com/weizhenye/ASS/wiki/Font-Size-in-ASS + +const useTextMetrics = 'fontBoundingBoxAscent' in TextMetrics.prototype; + +// It seems max line-height is 1200px in Firefox. +const isFirefox = navigator.userAgent.toLowerCase().includes('firefox'); +const unitsPerEm = !useTextMetrics && isFirefox ? 512 : 2048; +const lineSpacing = Object.create(null); + +const ctx = document.createElement('canvas').getContext('2d'); + +const $div = document.createElement('div'); +$div.className = 'ASS-fix-font-size'; +$div.style.fontSize = `${unitsPerEm}px`; +const $span = document.createElement('span'); +$span.textContent = '0'; +$div.append($span); + +const $fixFontSize = useTextMetrics ? null : $div; + +function getRealFontSize(fn, fs) { + if (!lineSpacing[fn]) { + if (useTextMetrics) { + ctx.font = `${unitsPerEm}px "${fn}"`; + const tm = ctx.measureText(''); + lineSpacing[fn] = tm.fontBoundingBoxAscent + tm.fontBoundingBoxDescent; + } else { + $span.style.fontFamily = `"${fn}"`; + lineSpacing[fn] = $span.clientHeight; + } + } + return fs * unitsPerEm / lineSpacing[fn]; +} + function alpha2opacity(a) { return 1 - `0x${a}` / 255; } @@ -793,7 +936,7 @@ function createSVGEl(name, attrs = []) { return $el; } -const GLOBAL_CSS = '.ASS-box{font-family:Arial;overflow:hidden;pointer-events:none;position:absolute}.ASS-dialogue{font-size:0;position:absolute;z-index:0}.ASS-dialogue span{display:inline-block}.ASS-dialogue [data-text]{display:inline-block;color:var(--ass-fill-color);font-size:calc(var(--ass-scale)*var(--ass-real-fs)*1px);line-height:calc(var(--ass-scale)*var(--ass-tag-fs)*1px);letter-spacing:calc(var(--ass-scale)*var(--ass-tag-fsp)*1px)}.ASS-dialogue [data-wrap-style="0"],.ASS-dialogue [data-wrap-style="3"]{text-wrap:balance}.ASS-dialogue [data-wrap-style="1"]{word-break:break-word;white-space:normal}.ASS-dialogue [data-wrap-style="2"]{word-break:normal;white-space:nowrap}.ASS-dialogue [data-border-style="1"]{position:relative}.ASS-dialogue [data-border-style="1"]::after,.ASS-dialogue [data-border-style="1"]::before{content:attr(data-text);position:absolute;top:0;left:0;z-index:-1;filter:blur(calc(var(--ass-tag-blur)*1px))}.ASS-dialogue [data-border-style="1"]::before{color:var(--ass-shadow-color);transform:translate(calc(var(--ass-scale-stroke)*var(--ass-tag-xshad)*1px),calc(var(--ass-scale-stroke)*var(--ass-tag-yshad)*1px));-webkit-text-stroke:var(--ass-border-width) var(--ass-shadow-color);text-shadow:var(--ass-shadow-delta);opacity:var(--ass-shadow-opacity)}.ASS-dialogue [data-border-style="1"]::after{color:transparent;-webkit-text-stroke:var(--ass-border-width) var(--ass-border-color);text-shadow:var(--ass-border-delta);opacity:var(--ass-border-opacity)}.ASS-dialogue [data-border-style="3"]{padding:calc(var(--ass-scale-stroke)*var(--ass-tag-xbord)*1px) calc(var(--ass-scale-stroke)*var(--ass-tag-ybord)*1px);position:relative;filter:blur(calc(var(--ass-tag-blur)*1px))}.ASS-dialogue [data-border-style="3"]::after,.ASS-dialogue [data-border-style="3"]::before{content:"";width:100%;height:100%;position:absolute;z-index:-1}.ASS-dialogue [data-border-style="3"]::before{background-color:var(--ass-shadow-color);left:calc(var(--ass-scale-stroke)*var(--ass-tag-xshad)*1px);top:calc(var(--ass-scale-stroke)*var(--ass-tag-yshad)*1px)}.ASS-dialogue [data-border-style="3"]::after{background-color:var(--ass-border-color);left:0;top:0}@container style(--ass-tag-xbord: 0) and style(--ass-tag-ybord: 0){.ASS-dialogue [data-border-style="3"]::after{background-color:transparent}}@container style(--ass-tag-xshad: 0) and style(--ass-tag-yshad: 0){.ASS-dialogue [data-border-style="3"]::before{background-color:transparent}}.ASS-dialogue [data-rotate]{transform:perspective(312.5px) rotateY(calc(var(--ass-tag-fry)*1deg)) rotateX(calc(var(--ass-tag-frx)*1deg)) rotateZ(calc(var(--ass-tag-frz)*-1deg))}.ASS-dialogue [data-text][data-rotate]{transform-style:preserve-3d;word-break:normal;white-space:nowrap}.ASS-dialogue [data-scale],.ASS-dialogue [data-skew]{display:inline-block;transform:scale(var(--ass-tag-fscx),var(--ass-tag-fscy)) skew(calc(var(--ass-tag-fax)*1rad),calc(var(--ass-tag-fay)*1rad));transform-origin:var(--ass-align-h) var(--ass-align-v)}.ASS-fix-font-size{font-size:2048px;font-family:Arial;line-height:normal;width:0;height:0;position:absolute;visibility:hidden;overflow:hidden}.ASS-clip-area,.ASS-fix-font-size span{position:absolute}.ASS-clip-area{width:100%;height:100%;top:0;left:0}.ASS-scroll-area{position:absolute;width:100%;overflow:hidden}'; +const GLOBAL_CSS = '.ASS-box{font-family:Arial;overflow:hidden;pointer-events:none;position:absolute}.ASS-dialogue{font-size:0;width:max-content;position:absolute;z-index:0;transform:translate(calc(var(--ass-align-h)*-1),calc(var(--ass-align-v)*-1))}.ASS-dialogue span{display:inline-block}.ASS-dialogue [data-text]{display:inline-block;color:var(--ass-fill-color);font-size:calc(var(--ass-scale)*var(--ass-real-fs)*1px);line-height:calc(var(--ass-scale)*var(--ass-tag-fs)*1px);letter-spacing:calc(var(--ass-scale)*var(--ass-tag-fsp)*1px)}.ASS-dialogue[data-wrap-style="0"],.ASS-dialogue[data-wrap-style="3"]{text-wrap:balance;white-space:pre-wrap}.ASS-dialogue[data-wrap-style="1"]{word-break:break-word;white-space:normal}.ASS-dialogue[data-wrap-style="2"]{word-break:normal;white-space:nowrap}.ASS-dialogue [data-border-style="1"]{position:relative;filter:blur(calc(var(--ass-tag-blur)*calc(1 - sign(var(--ass-tag-xbord)))*calc(1 - sign(var(--ass-tag-ybord)))*1px))}.ASS-dialogue [data-border-style="1"]::after,.ASS-dialogue [data-border-style="1"]::before{content:attr(data-text);position:absolute;top:0;left:0;z-index:-1;filter:blur(calc(var(--ass-tag-blur)*1px))}.ASS-dialogue [data-border-style="1"]::before{color:var(--ass-shadow-color);transform:translate(calc(var(--ass-scale-stroke)*var(--ass-tag-xshad)*1px),calc(var(--ass-scale-stroke)*var(--ass-tag-yshad)*1px));-webkit-text-stroke:var(--ass-border-width) var(--ass-shadow-color);text-shadow:var(--ass-shadow-delta);opacity:var(--ass-shadow-opacity)}.ASS-dialogue [data-border-style="1"]::after{color:transparent;-webkit-text-stroke:var(--ass-border-width) var(--ass-border-color);text-shadow:var(--ass-border-delta);opacity:var(--ass-border-opacity)}.ASS-dialogue [data-border-style="3"]{padding:calc(var(--ass-scale-stroke)*var(--ass-tag-xbord)*1px) calc(var(--ass-scale-stroke)*var(--ass-tag-ybord)*1px);position:relative;filter:blur(calc(var(--ass-tag-blur)*1px))}.ASS-dialogue [data-border-style="3"]::after,.ASS-dialogue [data-border-style="3"]::before{content:"";width:100%;height:100%;position:absolute;z-index:-1}.ASS-dialogue [data-border-style="3"]::before{background-color:var(--ass-shadow-color);left:calc(var(--ass-scale-stroke)*var(--ass-tag-xshad)*1px);top:calc(var(--ass-scale-stroke)*var(--ass-tag-yshad)*1px)}.ASS-dialogue [data-border-style="3"]::after{background-color:var(--ass-border-color);left:0;top:0}@container style(--ass-tag-xbord: 0) and style(--ass-tag-ybord: 0){.ASS-dialogue [data-border-style="3"]::after{background-color:transparent}}@container style(--ass-tag-xshad: 0) and style(--ass-tag-yshad: 0){.ASS-dialogue [data-border-style="3"]::before{background-color:transparent}}.ASS-dialogue [data-rotate]{transform:perspective(312.5px) rotateY(calc(var(--ass-tag-fry)*1deg)) rotateX(calc(var(--ass-tag-frx)*1deg)) rotateZ(calc(var(--ass-tag-frz)*-1deg))}.ASS-dialogue [data-text][data-rotate]{transform-style:preserve-3d;word-break:normal;white-space:nowrap}.ASS-dialogue [data-scale],.ASS-dialogue [data-skew]{display:inline-block;transform:scale(var(--ass-tag-fscx),var(--ass-tag-fscy)) skew(calc(var(--ass-tag-fax)*1rad),calc(var(--ass-tag-fay)*1rad));transform-origin:var(--ass-align-h) var(--ass-align-v)}.ASS-fix-font-size{font-family:Arial;line-height:normal;width:0;height:0;position:absolute;visibility:hidden;overflow:hidden}.ASS-clip-area,.ASS-fix-font-size span{position:absolute}.ASS-clip-area{width:100%;height:100%;top:0;left:0}.ASS-effect-area{position:absolute;display:flex;width:100%;height:fit-content;overflow:hidden;mask-composite:intersect}.ASS-effect-area[data-effect=banner]{flex-direction:column;height:100%}.ASS-effect-area .ASS-dialogue{position:static;transform:none}'; /** * @param {HTMLElement} container */ @@ -822,23 +965,232 @@ function batchAnimate(dia, action) { }); } -// https://github.com/weizhenye/ASS/wiki/Font-Size-in-ASS +function createEffect(effect, duration) { + // TODO: when effect and move both exist, its behavior is weird, for now only move works. + const { name, delay, leftToRight } = effect; + const translate = name === 'banner' ? 'X' : 'Y'; + const dir = ({ + X: leftToRight ? 1 : -1, + Y: /up/.test(name) ? -1 : 1, + })[translate]; + const start = -100 * dir; + // speed is 1000px/s when delay=1 + const distance = (duration / (delay || 1)) * dir; + const keyframes = [ + { offset: 0, transform: `translate${translate}(${start}%)` }, + { offset: 1, transform: `translate${translate}(calc(${start}% + var(--ass-scale) * ${distance}px))` }, + ]; + return [keyframes, { duration, fill: 'forwards' }]; +} -const $fixFontSize = document.createElement('div'); -$fixFontSize.className = 'ASS-fix-font-size'; -const $span = document.createElement('span'); -$span.textContent = '0'; -$fixFontSize.append($span); +function multiplyScale(v) { + return `calc(var(--ass-scale) * ${v}px)`; +} -const unitsPerEm = 2048; -const lineSpacing = Object.create(null); +function createMove(move, duration) { + const { x1, y1, x2, y2, t1, t2 } = move; + const start = `translate(${multiplyScale(x1)}, ${multiplyScale(y1)})`; + const end = `translate(${multiplyScale(x2)}, ${multiplyScale(y2)})`; + const moveDuration = Math.max(t2, duration); + const keyframes = [ + { offset: 0, transform: start }, + t1 > 0 ? { offset: t1 / moveDuration, transform: start } : null, + (t2 > 0 && t2 < duration) ? { offset: t2 / moveDuration, transform: end } : null, + { offset: 1, transform: end }, + ].filter(Boolean); + const options = { duration: moveDuration, fill: 'forwards' }; + return [keyframes, options]; +} + +function createFadeList(fade, duration) { + const { type, a1, a2, a3, t1, t2, t3, t4 } = fade; + // \fad(, ) + if (type === 'fad') { + // For example dialogue starts at 0 and ends at 5000 with \fad(4000, 4000) + // * means opacity from 0 to 1 in (0, 4000) + // * means opacity from 1 to 0 in (1000, 5000) + // and are overlaped in (1000, 4000), will take affect + // so the result is: + // * opacity from 0 to 1 in (0, 4000) + // * opacity from 0.25 to 0 in (4000, 5000) + const t1Keyframes = [{ offset: 0, opacity: 0 }, { offset: 1, opacity: 1 }]; + const t2Keyframes = [{ offset: 0, opacity: 1 }, { offset: 1, opacity: 0 }]; + return [ + [t2Keyframes, { duration: t2, delay: duration - t2, fill: 'forwards' }], + [t1Keyframes, { duration: t1, composite: 'replace' }], + ]; + } + // \fade(, , , , , , ) + const fadeDuration = Math.max(duration, t4); + const opacities = [a1, a2, a3].map((a) => 1 - a / 255); + const offsets = [0, t1, t2, t3, t4].map((t) => t / fadeDuration); + const keyframes = offsets.map((t, i) => ({ offset: t, opacity: opacities[i >> 1] })); + return [ + [keyframes, { duration: fadeDuration, fill: 'forwards' }], + ]; +} -function getRealFontSize(fn, fs) { - if (!lineSpacing[fn]) { - $span.style.fontFamily = fn; - lineSpacing[fn] = $span.clientHeight; +function createAnimatableVars(tag) { + return [ + ['real-fs', getRealFontSize(tag.fn, tag.fs)], + ['tag-fs', tag.fs], + ['tag-fsp', tag.fsp], + ['fill-color', color2rgba(tag.a1 + tag.c1)], + ] + .filter(([, v]) => v) + .map(([k, v]) => [`--ass-${k}`, v]); +} + +if (window.CSS.registerProperty) { + ['real-fs', 'tag-fs', 'tag-fsp'].forEach((k) => { + window.CSS.registerProperty({ + name: `--ass-${k}`, + syntax: '', + inherits: true, + initialValue: '0', + }); + }); + window.CSS.registerProperty({ + name: '--ass-fill-color', + syntax: '', + inherits: true, + initialValue: 'transparent', + }); +} + +// use linear() to simulate accel +function getEasing(duration, accel) { + if (accel === 1) return 'linear'; + // 60fps + const frames = Math.ceil(duration / 1000 * 60); + const points = Array.from({ length: frames + 1 }) + .map((_, i) => (i / frames) ** accel); + return `linear(${points.join(',')})`; +} + +function createDialogueAnimations(el, dialogue) { + const { start, end, effect, move, fade } = dialogue; + const duration = (end - start) * 1000; + return [ + effect && !move ? createEffect(effect, duration) : null, + move ? createMove(move, duration) : null, + ...(fade ? createFadeList(fade, duration) : []), + ] + .filter(Boolean) + .map(([keyframes, options]) => initAnimation(el, keyframes, options)); +} + +function createTagKeyframes(fromTag, tag, key) { + const value = tag[key]; + if (value === undefined) return []; + if (key === 'clip') return []; + if (key === 'a1' || key === 'c1') { + return [['fill-color', color2rgba((tag.a1 || fromTag.a1) + (tag.c1 || fromTag.c1))]]; } - return fs * unitsPerEm / lineSpacing[fn]; + if (key === 'c3') { + return [['border-color', color2rgba(`00${tag.c3}`)]]; + } + if (key === 'a3') { + return [['border-opacity', alpha2opacity(tag.a3)]]; + } + if (key === 'c4') { + return [['shadow-color', color2rgba(`00${tag.c4}`)]]; + } + if (key === 'a4') { + return [['shadow-opacity', alpha2opacity(tag.a4)]]; + } + if (key === 'fs') { + return [ + ['real-fs', getRealFontSize(tag.fn || fromTag.fn, tag.fs)], + ['tag-fs', value], + ]; + } + if (key === 'fscx' || key === 'fscy') { + return [[`tag-${key}`, (value || 100) / 100]]; + } + return [[`tag-${key}`, value]]; +} + +function createTagAnimations(el, fragment, sliceTag) { + const fromTag = { ...sliceTag, ...fragment.tag }; + return (fragment.tag.t || []).map(({ t1, t2, accel, tag }) => { + const keyframe = Object.fromEntries( + Object.keys(tag) + .flatMap((key) => createTagKeyframes(fromTag, tag, key)) + .map(([k, v]) => [`--ass-${k}`, v]) + // .concat(tag.clip ? [['clipPath', ]] : []) + .concat([['offset', 1]]), + ); + const duration = Math.max(0, t2 - t1); + return initAnimation(el, [keyframe], { + duration, + delay: t1, + fill: 'forwards', + easing: getEasing(duration, accel), + }); + }); +} + +function createClipAnimations(el, dialogue, store) { + return dialogue.slices + .flatMap((slice) => slice.fragments) + .flatMap((fragment) => fragment.tag.t || []) + .filter(({ tag }) => tag.clip) + .map(({ t1, t2, accel, tag }) => { + const keyframe = { + offset: 1, + clipPath: createRectClip(tag.clip, store.scriptRes.width, store.scriptRes.height), + }; + const duration = Math.max(0, t2 - t1); + return initAnimation(el, [keyframe], { + duration, + delay: t1, + fill: 'forwards', + easing: getEasing(duration, accel), + }); + }); +} + +// eslint-disable-next-line import/no-cycle + +function createRectClip(clip, sw, sh) { + if (!clip.dots) return ''; + const { x1, y1, x2, y2 } = clip.dots; + const polygon = [[x1, y1], [x1, y2], [x2, y2], [x2, y1], [x1, y1]] + .map(([x, y]) => [x / sw, y / sh]) + .concat(clip.inverse ? [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]] : []) + .map((pair) => pair.map((n) => `${n * 100}%`).join(' ')) + .join(','); + return `polygon(evenodd, ${polygon})`; +} + +function createPathClip(clip, sw, sh, store) { + if (!clip.drawing) return ''; + const scale = store.scale / (1 << (clip.scale - 1)); + let d = clip.drawing.instructions.map(({ type, points }) => ( + type + points.map(({ x, y }) => `${x * scale},${y * scale}`).join(',') + )).join(''); + if (clip.inverse) { + d += `M0,0L0,${sh},${sw},${sh},${sw},0,0,0Z`; + } + return `path(evenodd, "${d}")`; +} + +function getClipPath(dialogue, store) { + const { clip, animations } = dialogue; + if (!clip) return {}; + const { width, height } = store.scriptRes; + const $clipArea = document.createElement('div'); + store.box.insertBefore($clipArea, dialogue.$div); + $clipArea.append(dialogue.$div); + $clipArea.className = 'ASS-clip-area'; + $clipArea.style.zIndex = dialogue.$div.style.zIndex; + $clipArea.style.clipPath = clip.dots + ? createRectClip(clip, width, height) + : createPathClip(clip, width, height, store); + animations.push(...createClipAnimations($clipArea, dialogue, store)); + + return { $div: $clipArea }; } function createSVGStroke(tag, id, scale) { @@ -1021,276 +1373,6 @@ if (window.CSS.registerProperty) { }); } -const rotateTags = ['frx', 'fry', 'frz']; -const scaleTags = ['fscx', 'fscy']; -const skewTags = ['fax', 'fay']; - -if (window.CSS.registerProperty) { - [...rotateTags, ...skewTags].forEach((tag) => { - window.CSS.registerProperty({ - name: `--ass-tag-${tag}`, - syntax: '', - inherits: true, - initialValue: 0, - }); - }); - scaleTags.forEach((tag) => { - window.CSS.registerProperty({ - name: `--ass-tag-${tag}`, - syntax: '', - inherits: true, - initialValue: 1, - }); - }); -} - -function createTransform(tag) { - return [ - ...[...rotateTags, ...skewTags].map((x) => ([`--ass-tag-${x}`, `${tag[x] || 0}`])), - ...scaleTags.map((x) => ([`--ass-tag-${x}`, tag.p ? 1 : (tag[x] || 100) / 100])), - ]; -} - -function setTransformOrigin(dialogue, scale) { - const { align, width, height, x, y, $div } = dialogue; - const org = {}; - if (dialogue.org) { - org.x = dialogue.org.x * scale; - org.y = dialogue.org.y * scale; - } else { - org.x = [x, x + width / 2, x + width][align.h]; - org.y = [y + height, y + height / 2, y][align.v]; - } - for (let i = $div.childNodes.length - 1; i >= 0; i -= 1) { - const node = $div.childNodes[i]; - if (node.dataset.rotate === '') { - // It's not extremely precise for offsets are round the value to an integer. - const tox = org.x - x - node.offsetLeft; - const toy = org.y - y - node.offsetTop; - node.style.cssText += `transform-origin:${tox}px ${toy}px;`; - } - } -} - -// TODO: multi \t can't be merged directly -function mergeT(ts) { - return ts.reduceRight((results, t) => { - let merged = false; - return results - .map((r) => { - merged = t.t1 === r.t1 && t.t2 === r.t2 && t.accel === r.accel; - return { ...r, ...(merged ? { tag: { ...r.tag, ...t.tag } } : {}) }; - }) - .concat(merged ? [] : t); - }, []); -} - -function createEffectKeyframes({ effect, duration }) { - // TODO: when effect and move both exist, its behavior is weird, for now only move works. - const { name, delay, leftToRight } = effect; - if (name === 'banner') { - const tx = (duration / (delay || 1)) * (leftToRight ? 1 : -1); - return [0, `calc(var(--ass-scale) * ${tx}px)`].map((x, i) => ({ - offset: i, - transform: `translateX(${x})`, - })); - } - if (name.startsWith('scroll')) { - // speed is 1000px/s when delay=1 - const updown = /up/.test(name) ? -1 : 1; - const y = duration / (delay || 1) * updown; - return [ - { offset: 0, transform: 'translateY(-100%)' }, - { offset: 1, transform: `translateY(calc(var(--ass-scale) * ${y}px))` }, - ]; - } - return []; -} - -function createMoveKeyframes({ move, duration, dialogue }) { - const { x1, y1, x2, y2, t1, t2 } = move; - const t = [t1, t2 || duration]; - const pos = dialogue.pos || { x: 0, y: 0 }; - return [[x1, y1], [x2, y2]] - .map(([x, y]) => [(x - pos.x), (y - pos.y)]) - .map(([x, y], index) => ({ - offset: Math.min(t[index] / duration, 1), - transform: `translate(calc(var(--ass-scale) * ${x}px), calc(var(--ass-scale) * ${y}px))`, - })); -} - -function createFadeKeyframes(fade, duration) { - if (fade.type === 'fad') { - const { t1, t2 } = fade; - const kfs = []; - if (t1) { - kfs.push([0, 0]); - } - if (t1 < duration) { - if (t2 <= duration) { - kfs.push([t1 / duration, 1]); - } - if (t1 + t2 < duration) { - kfs.push([(duration - t2) / duration, 1]); - } - if (t2 > duration) { - kfs.push([0, (t2 - duration) / t2]); - } else if (t1 + t2 > duration) { - kfs.push([(t1 + 0.5) / duration, 1 - (t1 + t2 - duration) / t2]); - } - if (t2) { - kfs.push([1, 0]); - } - } else { - kfs.push([1, duration / t1]); - } - return kfs.map(([offset, opacity]) => ({ offset, opacity })); - } - const { a1, a2, a3, t1, t2, t3, t4 } = fade; - const opacities = [a1, a2, a3].map((a) => 1 - a / 255); - return [0, t1, t2, t3, t4, duration] - .map((t) => t / duration) - .map((t, i) => ({ offset: t, opacity: opacities[i >> 1] })) - .filter(({ offset }) => offset <= 1); -} - -function createTransformKeyframes({ fromTag, tag, fragment }) { - const toTag = { ...fromTag, ...tag }; - if (fragment.drawing) { - // scales will be handled inside svg - Object.assign(toTag, { - p: 0, - fscx: ((tag.fscx || fromTag.fscx) / fromTag.fscx) * 100, - fscy: ((tag.fscy || fromTag.fscy) / fromTag.fscy) * 100, - }); - Object.assign(fromTag, { fscx: 100, fscy: 100 }); - } - return Object.fromEntries(createTransform(toTag)); -} - -function createAnimatableVars(tag) { - return [ - ['real-fs', getRealFontSize(tag.fn, tag.fs)], - ['tag-fs', tag.fs], - ['tag-fsp', tag.fsp], - ['fill-color', color2rgba(tag.a1 + tag.c1)], - ] - .filter(([, v]) => v) - .map(([k, v]) => [`--ass-${k}`, v]); -} - -if (window.CSS.registerProperty) { - ['real-fs', 'tag-fs', 'tag-fsp'].forEach((k) => { - window.CSS.registerProperty({ - name: `--ass-${k}`, - syntax: '', - inherits: true, - initialValue: '0', - }); - }); - window.CSS.registerProperty({ - name: '--ass-fill-color', - syntax: '', - inherits: true, - initialValue: 'transparent', - }); -} - -// TODO: accel is not implemented yet, maybe it can be simulated by cubic-bezier? -function setKeyframes(dialogue, store) { - const { start, end, effect, move, fade, slices } = dialogue; - const duration = (end - start) * 1000; - const keyframes = [ - ...(effect && !move ? createEffectKeyframes({ effect, duration }) : []), - ...(move ? createMoveKeyframes({ move, duration, dialogue }) : []), - ...(fade ? createFadeKeyframes(fade, duration) : []), - ].sort((a, b) => a.offset - b.offset); - if (keyframes.length > 0) { - Object.assign(dialogue, { keyframes }); - } - slices.forEach((slice) => { - const sliceTag = store.styles[slice.style].tag; - slice.fragments.forEach((fragment) => { - if (!fragment.tag.t || fragment.tag.t.length === 0) { - return; - } - const fromTag = { ...sliceTag, ...fragment.tag }; - const tTags = mergeT(fragment.tag.t).sort((a, b) => a.t2 - b.t2 || a.t1 - b.t1); - if (tTags[0].t1 > 0) { - tTags.unshift({ t1: 0, t2: tTags[0].t1, tag: fromTag }); - } - tTags.reduce((prevTag, curr) => { - const tag = { ...prevTag, ...curr.tag }; - tag.t = null; - Object.assign(curr.tag, tag); - return tag; - }, {}); - const fDuration = Math.max(duration, ...tTags.map(({ t2 }) => t2)); - const kfs = tTags.map(({ t2, tag }) => ({ - offset: t2 / fDuration, - ...Object.fromEntries(createAnimatableVars({ - ...tag, - a1: tag.a1 || fromTag.a1, - c1: tag.c1 || fromTag.c1, - })), - ...Object.fromEntries(createCSSStroke( - { ...fromTag, ...tag }, - store.sbas ? store.scale : 1, - )), - ...createTransformKeyframes({ fromTag, tag, fragment }), - })).sort((a, b) => a.offset - b.offset); - if (kfs.length > 0) { - Object.assign(fragment, { keyframes: kfs, duration: fDuration }); - } - }); - }); -} - -function addClipPath($defs, clip, id, sw, sh) { - if ($defs.querySelector(`#${id}`)) return; - let d = ''; - if (clip.dots !== null) { - let { x1, y1, x2, y2 } = clip.dots; - x1 /= sw; - y1 /= sh; - x2 /= sw; - y2 /= sh; - d = `M${x1},${y1}L${x1},${y2},${x2},${y2},${x2},${y1}Z`; - } - if (clip.drawing !== null) { - d = clip.drawing.instructions.map(({ type, points }) => ( - type + points.map(({ x, y }) => `${x / sw},${y / sh}`).join(',') - )).join(''); - } - const scale = 1 / (1 << (clip.scale - 1)); - if (clip.inverse) { - d += `M0,0L0,${scale},${scale},${scale},${scale},0,0,0Z`; - } - const $clipPath = createSVGEl('clipPath', [ - ['id', id], - ['clipPathUnits', 'objectBoundingBox'], - ]); - $clipPath.append(createSVGEl('path', [ - ['d', d], - ['transform', `scale(${scale})`], - ['clip-rule', 'evenodd'], - ])); - $defs.append($clipPath); -} - -function getClipPath(dialogue, store) { - const { id, clip } = dialogue; - if (!clip) return {}; - const { width, height } = store.scriptRes; - addClipPath(store.defs, clip, id, width, height); - const $clipArea = document.createElement('div'); - store.box.insertBefore($clipArea, dialogue.$div); - $clipArea.append(dialogue.$div); - $clipArea.className = 'ASS-clip-area'; - $clipArea.style.clipPath = `url(#${id})`; - return { $div: $clipArea }; -} - function createDrawing(fragment, styleTag, store) { if (!fragment.drawing.d) return null; const tag = { ...styleTag, ...fragment.tag }; @@ -1337,6 +1419,51 @@ function createDrawing(fragment, styleTag, store) { }; } +const rotateTags = ['frx', 'fry', 'frz']; +const scaleTags = ['fscx', 'fscy']; +const skewTags = ['fax', 'fay']; + +if (window.CSS.registerProperty) { + [...rotateTags, ...skewTags].forEach((tag) => { + window.CSS.registerProperty({ + name: `--ass-tag-${tag}`, + syntax: '', + inherits: true, + initialValue: 0, + }); + }); + scaleTags.forEach((tag) => { + window.CSS.registerProperty({ + name: `--ass-tag-${tag}`, + syntax: '', + inherits: true, + initialValue: 1, + }); + }); +} + +function createTransform(tag) { + return [ + ...[...rotateTags, ...skewTags].map((x) => ([`--ass-tag-${x}`, `${tag[x] || 0}`])), + ...scaleTags.map((x) => ([`--ass-tag-${x}`, tag.p ? 1 : (tag[x] || 100) / 100])), + ]; +} + +function setTransformOrigin(dialogue, scale) { + const { align, width, height, x, y, $div } = dialogue; + const orgX = (dialogue.org ? dialogue.org.x * scale : x) + [0, width / 2, width][align.h]; + const orgY = (dialogue.org ? dialogue.org.y * scale : y) + [height, height / 2, 0][align.v]; + for (let i = $div.childNodes.length - 1; i >= 0; i -= 1) { + const node = $div.childNodes[i]; + if (node.dataset.rotate === '') { + // It's not extremely precise for offsets are round the value to an integer. + const tox = orgX - x - node.offsetLeft; + const toy = orgY - y - node.offsetTop; + node.style.cssText += `transform-origin:${tox}px ${toy}px;`; + } + } +} + function encodeText(text, q) { return text .replace(/\\h/g, ' ') @@ -1345,22 +1472,18 @@ function encodeText(text, q) { } function createDialogue(dialogue, store) { - const { video, styles } = store; + const { styles } = store; const $div = document.createElement('div'); $div.className = 'ASS-dialogue'; + $div.dataset.wrapStyle = dialogue.q; const df = document.createDocumentFragment(); - const { align, slices, start, end } = dialogue; + const { align, slices } = dialogue; [ - ['--ass-align-h', ['left', 'center', 'right'][align.h]], - ['--ass-align-v', ['bottom', 'center', 'top'][align.v]], + ['--ass-align-h', ['0%', '50%', '100%'][align.h]], + ['--ass-align-v', ['100%', '50%', '0%'][align.v]], ].forEach(([k, v]) => { $div.style.setProperty(k, v); }); - const animationOptions = { - duration: (end - start) * 1000, - delay: Math.min(0, start - (video.currentTime - store.delay)) * 1000, - fill: 'forwards', - }; const animations = []; slices.forEach((slice) => { const sliceTag = styles[slice.style].tag; @@ -1394,8 +1517,6 @@ function createDialogue(dialogue, store) { encodeText(text, tag.q).split('\n').forEach((content, idx) => { const $span = document.createElement('span'); const $ssspan = document.createElement('span'); - $span.dataset.wrapStyle = tag.q; - $span.dataset.borderStyle = borderStyle; if (hasScale || hasSkew) { if (hasScale) { $ssspan.dataset.scale = ''; @@ -1415,7 +1536,6 @@ function createDialogue(dialogue, store) { $span.style.cssText = obj.cssText; $span.append(obj.$svg); } else { - $span.dataset.text = ''; if (idx) { df.append(document.createElement('br')); } @@ -1426,29 +1546,21 @@ function createDialogue(dialogue, store) { $span.textContent = content; } const el = hasScale || hasSkew ? $ssspan : $span; + el.dataset.text = content; if (tag.xbord || tag.ybord || tag.xshad || tag.yshad) { - el.dataset.text = content; + el.dataset.borderStyle = borderStyle; } } $span.style.cssText += cssText; cssVars.forEach(([k, v]) => { $span.style.setProperty(k, v); }); - if (fragment.keyframes) { - const animation = initAnimation( - $span, - fragment.keyframes, - { ...animationOptions, duration: fragment.duration }, - ); - animations.push(animation); - } + animations.push(...createTagAnimations($span, fragment, sliceTag)); df.append($span); }); }); }); - if (dialogue.keyframes) { - animations.push(initAnimation($div, dialogue.keyframes, animationOptions)); - } + animations.push(...createDialogueAnimations($div, dialogue)); $div.append(df); return { $div, animations }; } @@ -1530,17 +1642,10 @@ function allocate(dialogue, store) { function getPosition(dialogue, store) { const { scale } = store; - const { effect, move, align, width, height, margin, slices } = dialogue; + const { move, align, width, height, margin, slices } = dialogue; let x = 0; let y = 0; - if (effect && effect.name === 'banner') { - x = effect.lefttoright ? -width : store.width; - y = [ - store.height - height - margin.vertical, - (store.height - height) / 2, - margin.vertical, - ][align.v]; - } else if (dialogue.pos || move) { + if (dialogue.pos || move) { const pos = dialogue.pos || { x: 0, y: 0 }; const sx = scale * pos.x; const sy = scale * pos.y; @@ -1563,47 +1668,66 @@ function getPosition(dialogue, store) { ][align.v] : allocate(dialogue, store); } - // TODO: use % for x and y - return { x, y }; + return { + x: x + [0, width / 2, width][align.h], + y: y + [height, height / 2, 0][align.v], + }; } function createStyle(dialogue) { - const { layer, align, effect, pos, margin } = dialogue; + const { layer, align, effect, pos, margin, q } = dialogue; let cssText = ''; if (layer) cssText += `z-index:${layer};`; cssText += `text-align:${['left', 'center', 'right'][align.h]};`; if (!effect) { - cssText += `max-width:calc(100% - var(--ass-scale) * ${margin.left + margin.right}px);`; + if (q !== 2) { + cssText += `max-width:calc(100% - var(--ass-scale) * ${margin.left + margin.right}px);`; + } if (!pos) { if (align.h !== 0) { - cssText += `margin-right:calc(var(--ass-scale) * ${margin.right}px);`; + cssText += `padding-right:calc(var(--ass-scale) * ${margin.right}px);`; } if (align.h !== 2) { - cssText += `margin-left:calc(var(--ass-scale) * ${margin.left}px);`; + cssText += `padding-left:calc(var(--ass-scale) * ${margin.left}px);`; } } } return cssText; } -function getScrollEffect(dialogue, store) { - const $scrollArea = document.createElement('div'); - $scrollArea.className = 'ASS-scroll-area'; - store.box.insertBefore($scrollArea, dialogue.$div); - $scrollArea.append(dialogue.$div); - const { height } = store.scriptRes; - const { name, y1, y2 } = dialogue.effect; +function setEffect(dialogue, store) { + const $area = document.createElement('div'); + $area.className = 'ASS-effect-area'; + store.box.insertBefore($area, dialogue.$div); + $area.append(dialogue.$div); + const { width, height } = store.scriptRes; + const { name, y1, y2, leftToRight, fadeAwayWidth, fadeAwayHeight } = dialogue.effect; const min = Math.min(y1, y2); const max = Math.max(y1, y2); - const top = min / height * 100; - const bottom = (height - max) / height * 100; - $scrollArea.style.cssText += `top:${top}%;bottom:${bottom}%;`; - const up = /up/.test(name); - // eslint-disable-next-line no-param-reassign - dialogue.$div.style.cssText += up ? 'top:100%;' : 'top:0%;'; - return { - $div: $scrollArea, - }; + $area.dataset.effect = name; + if (name === 'banner') { + $area.style.alignItems = leftToRight ? 'flex-start' : 'flex-end'; + $area.style.justifyContent = ['flex-end', 'center', 'flex-start'][dialogue.align.v]; + } + if (name.startsWith('scroll')) { + const top = min / height * 100; + const bottom = (height - max) / height * 100; + $area.style.cssText = `top:${top}%;bottom:${bottom}%;`; + $area.style.justifyContent = ['flex-start', 'center', 'flex-end'][dialogue.align.h]; + } + if (fadeAwayHeight) { + const p = fadeAwayHeight / (max - min) * 100; + $area.style.maskImage = [ + `linear-gradient(#000 ${100 - p}%, transparent)`, + `linear-gradient(transparent, #000 ${p}%)`, + ].join(','); + } + if (fadeAwayWidth) { + const p = fadeAwayWidth / width * 100; + // only left side has fade away effect in VSFilter + $area.style.maskImage = `linear-gradient(90deg, transparent, #000 ${p}%)`; + } + return $area; } function renderer(dialogue, store) { @@ -1618,11 +1742,12 @@ function renderer(dialogue, store) { Object.assign(dialogue, { height }); const { x, y } = getPosition(dialogue, store); Object.assign(dialogue, { x, y }); - $div.style.cssText += `width:${width}px;height:${height}px;left:${x}px;top:${y}px;`; + $div.style.cssText += `left:${x}px;top:${y}px;`; setTransformOrigin(dialogue, store.scale); + // TODO: refactor to create .clip-area or .effect-area wrappers in `createDialogue` Object.assign(dialogue, getClipPath(dialogue, store)); - if (dialogue.effect?.name?.startsWith('scroll')) { - Object.assign(dialogue, getScrollEffect(dialogue, store)); + if (dialogue.effect) { + Object.assign(dialogue, { $div: setEffect(dialogue, store) }); } return dialogue; } @@ -1655,6 +1780,9 @@ function framing(store) { ) { if (vct < dialogues[store.index].end) { const dia = renderer(dialogues[store.index], store); + (dia.animations || []).forEach((animation) => { + animation.currentTime = (vct - dia.start) * 1000; + }); if (!video.paused) { batchAnimate(dia, 'play'); } @@ -1706,7 +1834,7 @@ function createPause(store) { } function createResize(that, store) { - const { video, box, svg, layoutRes } = store; + const { video, box, layoutRes } = store; return function resize() { const cw = video.clientWidth; const ch = video.clientHeight; @@ -1736,11 +1864,9 @@ function createResize(that, store) { store.height = bh; store.resampledRes = { width: rw, height: rh }; - const cssText = `width:${bw}px;height:${bh}px;top:${(ch - bh) / 2}px;left:${(cw - bw) / 2}px;`; - box.style.cssText = cssText; + box.style.cssText = `width:${bw}px;height:${bh}px;top:${(ch - bh) / 2}px;left:${(cw - bw) / 2}px;`; box.style.setProperty('--ass-scale', store.scale); box.style.setProperty('--ass-scale-stroke', store.sbas ? store.scale : 1); - svg.style.cssText = cssText; createSeek(store)(); }; @@ -1769,10 +1895,6 @@ class ASS { video: null, /** the box to display subtitles */ box: document.createElement('div'), - /** use for \clip */ - svg: createSVGEl('svg'), - /** use for \clip */ - defs: createSVGEl('defs'), /** * video resize observer * @type {ResizeObserver} @@ -1860,7 +1982,7 @@ class ASS { }; this.#store.styles = styles; this.#store.dialogues = dialogues.map((dia) => Object.assign(dia, { - id: `ASS-${uuid()}`, + effect: ['banner', 'scroll up', 'scroll down'].includes(dia.effect?.name) ? dia.effect : null, align: { // 0: left, 1: center, 2: right h: (dia.alignment + 2) % 3, @@ -1869,14 +1991,11 @@ class ASS { }, })); - container.append($fixFontSize); - - const { svg, defs, scriptRes, box } = this.#store; - svg.setAttributeNS(null, 'viewBox', `0 0 ${scriptRes.width} ${scriptRes.height}`); - - svg.append(defs); - container.append(svg); + if ($fixFontSize) { + container.append($fixFontSize); + } + const { box } = this.#store; box.className = 'ASS-box'; container.append(box); @@ -1895,10 +2014,6 @@ class ASS { this.#resize(); this.resampling = resampling; - dialogues.forEach((dialogue) => { - setKeyframes(dialogue, this.#store); - }); - const observer = new ResizeObserver(this.#resize); observer.observe(video); this.#store.observer = observer; @@ -1911,7 +2026,7 @@ class ASS { * @returns {ASS} */ destroy() { - const { video, box, svg, observer } = this.#store; + const { video, box, observer } = this.#store; this.#pause(); clear(this.#store); video.removeEventListener('play', this.#play); @@ -1920,8 +2035,9 @@ class ASS { video.removeEventListener('waiting', this.#pause); video.removeEventListener('seeking', this.#seek); - $fixFontSize.remove(); - svg.remove(); + if ($fixFontSize) { + $fixFontSize.remove(); + } box.remove(); observer.unobserve(this.#store.video); diff --git a/dist/ass.min.js b/dist/ass.min.js index ac53fd8..a834d97 100644 --- a/dist/ass.min.js +++ b/dist/ass.min.js @@ -1 +1 @@ -function t(t){const e=t.toLowerCase().trim().split(/\s*;\s*/);return"banner"===e[0]?{name:e[0],delay:1*e[1]||0,leftToRight:1*e[2]||0,fadeAwayWidth:1*e[3]||0}:/^scroll\s/.test(e[0])?{name:e[0],y1:Math.min(1*e[1],1*e[2]),y2:Math.max(1*e[1],1*e[2]),delay:1*e[3]||0,fadeAwayHeight:1*e[4]||0}:""!==t?{name:t}:null}function e(t){return t?t.toLowerCase().replace(/([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?)/g," $1 ").replace(/([mnlbspc])/g," $1 ").trim().replace(/\s+/g," ").split(/\s(?=[mnlbspc])/).map((t=>t.split(" ").filter(((t,e)=>!(e&&Number.isNaN(1*t)))))):[]}const s=["b","i","u","s","fsp","k","K","kf","ko","kt","fe","q","p","pbo","a","an","fscx","fscy","fax","fay","frx","fry","frz","fr","be","blur","bord","xbord","ybord","shad","xshad","yshad"].map((t=>({name:t,regex:new RegExp(`^${t}-?\\d`)})));function a(t){const n={};for(let e=0;et.replace(/,/g,"\n"))).split(/\s*,\s*/);if(!e[0])return n;n.t={t1:0,t2:0,accel:1,tags:e[e.length-1].replace(/\n/g,",").split("\\").slice(1).map(a)},2===e.length&&(n.t.accel=1*e[0]),3===e.length&&(n.t.t1=1*e[0],n.t.t2=1*e[1]),4===e.length&&(n.t.t1=1*e[0],n.t.t2=1*e[1],n.t.accel=1*e[2])}return n}function n(t){const e=[];let s=0,n="";const r=t.split("\\").slice(1).concat("").join("\\");for(let t=0;tvoid 0===e.p?t:!!e.p),!1);a.push({tags:r,text:i?"":s[t+1],drawing:i?e(s[t+1]):[]})}return{raw:t,combined:a.map((t=>t.text)).join(""),parsed:a}}function i(t){const e=t.split(":");return 3600*e[0]+60*e[1]+1*e[2]}function o(e,s){let a=e.split(",");if(a.length>s.length){const t=a.slice(s.length-1).join();a=a.slice(0,s.length-1),a.push(t)}const n={};for(let e=0;ee.find((e=>e.toLowerCase()===t.toLowerCase()))||t))}function f(t,e){const s=t.match(/Style\s*:\s*(.*)/i)[1].split(/\s*,\s*/);return Object.assign({},...e.map(((t,e)=>({[t]:s[e]}))))}function p(t){const e={type:null,prev:null,next:null,points:[]};/[mnlbs]/.test(t[0])&&(e.type=t[0].toUpperCase().replace("N","L").replace("B","C"));for(let s=t.length-!(1&t.length),a=1;at+e.map((({x:t,y:e})=>`${t},${e}`)).join(","))).join("")}function g(t){const e=[];let s=0;for(;s({x:t,y:e}))))}t.splice(s,1)}}const a=[].concat(...e.map((({type:t,points:e,prev:s,next:a})=>"S"===t?function(t,e,s){const a=[],n=[0,2/3,1/3,0],r=[0,1/3,2/3,0],i=[0,1/6,2/3,1/6],o=(t,e)=>t[0]*e[0]+t[1]*e[1]+t[2]*e[2]+t[3]*e[3];let l=[t[t.length-1].x,t[0].x,t[1].x,t[2].x],c=[t[t.length-1].y,t[0].y,t[1].y,t[2].y];a.push({type:"M"===e?"M":"L",points:[{x:o(i,l),y:o(i,c)}]});for(let e=3;et))).forEach((({x:t,y:r})=>{e=Math.min(e,t),s=Math.min(s,r),a=Math.max(a,t),n=Math.max(n,r)})),{minX:e,minY:s,width:a-e,height:n-s}}(e))}const y=["fs","fsp","clip","c1","c2","c3","c4","a1","a2","a3","a4","alpha","fscx","fscy","fax","fay","frx","fry","frz","fr","be","blur","bord","xbord","ybord","shad","xshad","yshad"];function m(t,e,s={}){let a=t[e];if(void 0===a)return null;if("pos"===e||"org"===e)return 2===a.length?{[e]:{x:a[0],y:a[1]}}:null;if("move"===e){const[t,e,s,n,r=0,i=0]=a;return 4===a.length||6===a.length?{move:{x1:t,y1:e,x2:s,y2:n,t1:r,t2:i}}:null}if("fad"===e||"fade"===e){if(2===a.length){const[t,e]=a;return{fade:{type:"fad",t1:t,t2:e}}}if(7===a.length){const[t,e,s,n,r,i,o]=a;return{fade:{type:"fade",a1:t,a2:e,a3:s,t1:n,t2:r,t3:i,t4:o}}}return null}if("clip"===e){const{inverse:t,scale:e,drawing:s,dots:n}=a;if(s)return{clip:{inverse:t,scale:e,drawing:g(s),dots:n}};if(n){const[a,r,i,o]=n;return{clip:{inverse:t,scale:e,drawing:s,dots:{x1:a,y1:r,x2:i,y2:o}}}}return null}if(/^[xy]?(bord|shad)$/.test(e)&&(a=Math.max(a,0)),"bord"===e)return{xbord:a,ybord:a};if("shad"===e)return{xshad:a,yshad:a};if(/^c\d$/.test(e))return{[e]:a||s[e]};if("alpha"===e)return{a1:a,a2:a,a3:a,a4:a};if("fr"===e)return{frz:a};if("fs"===e)return{fs:/^\+|-/.test(a)?(1*a>-10?1+a/10:1)*s.fs:1*a};if("K"===e)return{kf:a};if("t"===e){const{t1:t,accel:e,tags:n}=a,r=a.t2||1e3*(s.end-s.start),i={};return n.forEach((t=>{const e=Object.keys(t)[0];~y.indexOf(e)&&("clip"!==e||t[e].dots)&&Object.assign(i,m(t,e,s))})),{t:{t1:t,t2:r,accel:e,tag:i}}}return{[e]:a}}const x=[null,1,2,3,null,7,8,9,null,4,5,6],b=["r","a","an","pos","org","move","fade","fad","clip"];function v({styles:t,style:e,parsed:s,start:a,end:n}){let r,i,o,l,c,d;const f=[];let p={style:e,fragments:[]},h={};for(let y=0;y=r.End)continue;t[r.Style]||(r.Style="Default");const i=t[r.Style].style,o=v({styles:t,style:r.Style,parsed:r.Text.parsed,start:r.Start,end:r.End}),l=o.alignment||i.Alignment;s=Math.min(s,r.Layer),a.push(Object.assign({layer:r.Layer,start:r.Start,end:r.End,style:r.Style,name:r.Name,margin:{left:r.MarginL||i.MarginL,right:r.MarginR||i.MarginR,vertical:r.MarginV||i.MarginV},effect:r.Effect},o,{alignment:l}))}for(let t=0;tt.start-e.start||t.end-e.end))}const S={Name:"Default",Fontname:"Arial",Fontsize:"20",PrimaryColour:"&H00FFFFFF&",SecondaryColour:"&H000000FF&",OutlineColour:"&H00000000&",BackColour:"&H00000000&",Bold:"0",Italic:"0",Underline:"0",StrikeOut:"0",ScaleX:"100",ScaleY:"100",Spacing:"0",Angle:"0",BorderStyle:"1",Outline:"2",Shadow:"2",Alignment:"2",MarginL:"10",MarginR:"10",MarginV:"10",Encoding:"1"};function $(t){if(/^(&|H|&H)[0-9a-f]{6,}/i.test(t)){const[,e,s]=t.match(/&?H?([0-9a-f]{2})?([0-9a-f]{6})/i);return[e||"00",s]}const e=parseInt(t,10);if(!Number.isNaN(e)){const t=-2147483648;if(e{"Name"===t||"Fontname"===t||/Colour/.test(t)||(s[t]*=1)}));const[r,i]=$(s.PrimaryColour),[o,l]=$(s.SecondaryColour),[c,d]=$(s.OutlineColour),[f,p]=$(s.BackColour),h={fn:s.Fontname,fs:s.Fontsize,c1:i,a1:r,c2:l,a2:o,c3:d,a3:c,c4:p,a4:f,b:Math.abs(s.Bold),i:Math.abs(s.Italic),u:Math.abs(s.Underline),s:Math.abs(s.StrikeOut),fscx:s.ScaleX,fscy:s.ScaleY,fsp:s.Spacing,frz:s.Angle,xbord:s.Outline,ybord:s.Outline,xshad:s.Shadow,yshad:s.Shadow,fe:s.Encoding,q:/^[0-3]$/.test(t.WrapStyle)?1*t.WrapStyle:2};a[s.Name]={style:s,tag:h}}return a}({info:s.info,style:s.styles.style,defaultStyle:e.defaultStyle||{}});return{info:s.info,width:1*s.info.PlayResX||null,height:1*s.info.PlayResY||null,collisions:s.info.Collisions||"Normal",styles:a,dialogues:w({styles:a,dialogues:s.events.dialogue})}}function k(t){return 1-`0x${t}`/255}function M(t){const e=t.match(/(\w\w)(\w\w)(\w\w)(\w\w)/),s=k(e[1]),a=+`0x${e[2]}`,n=+`0x${e[3]}`;return`rgba(${+`0x${e[4]}`},${n},${a},${s})`}function E(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,(t=>{const e=Math.trunc(16*Math.random());return("x"===t?e:3&e|8).toString(16)}))}function C(t,e=[]){const s=document.createElementNS("http://www.w3.org/2000/svg",t);for(let t=0;t{t[e]()}))}const N=document.createElement("div");N.className="ASS-fix-font-size";const L=document.createElement("span");L.textContent="0",N.append(L);const F=2048,R=Object.create(null);function z([t,e]){return[[0,0],[0,1],[1,0],[1,1]].filter((([s,a])=>(s||t)&&(a||e))).map((([s,a])=>[(s||-1)*t,(a||-1)*e]))}function T(t,e){const s=M(`00${t.c3}`),a=t.xbord*e,n=t.ybord*e,r=M(`00${t.c4}`),i=t.blur||t.be||0,o=function(t,e){if(t===e)return[];const s=Math.min(t,e),a=Math.max(t,e);return Array.from({length:Math.ceil(a)-1},((t,e)=>e+1)).concat(a).map((t=>[(a-t)/a*s,t])).map((([s,a])=>t>e?[a,s]:[s,a])).flatMap(z)}(a,n);return[["border-width",2*Math.min(a,n)+"px"],["border-color",s],["border-opacity",k(t.a3)],["border-delta",o.map((([t,e])=>`${t}px ${e}px ${s}`)).join(",")],["shadow-color",r],["shadow-opacity",k(t.a4)],["shadow-delta",o.map((([t,e])=>`${t}px ${e}px ${r}`)).join(",")],["tag-blur",i],["tag-xbord",t.xbord],["tag-ybord",t.ybord],["tag-xshad",t.xshad],["tag-yshad",t.yshad]].map((([t,e])=>[`--ass-${t}`,e]))}window.CSS.registerProperty&&(window.CSS.registerProperty({name:"--ass-border-width",syntax:"",inherits:!0,initialValue:"0px"}),["border-color","shadow-color"].forEach((t=>{window.CSS.registerProperty({name:`--ass-${t}`,syntax:"",inherits:!0,initialValue:"transparent"})})),["border-opacity","shadow-opacity"].forEach((t=>{window.CSS.registerProperty({name:`--ass-${t}`,syntax:"",inherits:!0,initialValue:"1"})})),["blur","xbord","ybord","xshad","yshad"].forEach((t=>{window.CSS.registerProperty({name:`--ass-tag-${t}`,syntax:"",inherits:!0,initialValue:"0"})})));const B=["frx","fry","frz"],P=["fscx","fscy"],_=["fax","fay"];function H(t){return[...[...B,..._].map((e=>[`--ass-tag-${e}`,`${t[e]||0}`])),...P.map((e=>[`--ass-tag-${e}`,t.p?1:(t[e]||100)/100]))]}function V({effect:t,duration:e}){const{name:s,delay:a,leftToRight:n}=t;if("banner"===s){return[0,`calc(var(--ass-scale) * ${e/(a||1)*(n?1:-1)}px)`].map(((t,e)=>({offset:e,transform:`translateX(${t})`})))}if(s.startsWith("scroll")){return[{offset:0,transform:"translateY(-100%)"},{offset:1,transform:`translateY(calc(var(--ass-scale) * ${e/(a||1)*(/up/.test(s)?-1:1)}px))`}]}return[]}function q({move:t,duration:e,dialogue:s}){const{x1:a,y1:n,x2:r,y2:i,t1:o,t2:l}=t,c=[o,l||e],d=s.pos||{x:0,y:0};return[[a,n],[r,i]].map((([t,e])=>[t-d.x,e-d.y])).map((([t,s],a)=>({offset:Math.min(c[a]/e,1),transform:`translate(calc(var(--ass-scale) * ${t}px), calc(var(--ass-scale) * ${s}px))`})))}function I(t,e){if("fad"===t.type){const{t1:s,t2:a}=t,n=[];return s&&n.push([0,0]),se?n.push([0,(a-e)/a]):s+a>e&&n.push([(s+.5)/e,1-(s+a-e)/a]),a&&n.push([1,0])):n.push([1,e/s]),n.map((([t,e])=>({offset:t,opacity:e})))}const{a1:s,a2:a,a3:n,t1:r,t2:i,t3:o,t4:l}=t,c=[s,a,n].map((t=>1-t/255));return[0,r,i,o,l,e].map((t=>t/e)).map(((t,e)=>({offset:t,opacity:c[e>>1]}))).filter((({offset:t})=>t<=1))}function U({fromTag:t,tag:e,fragment:s}){const a={...t,...e};return s.drawing&&(Object.assign(a,{p:0,fscx:(e.fscx||t.fscx)/t.fscx*100,fscy:(e.fscy||t.fscy)/t.fscy*100}),Object.assign(t,{fscx:100,fscy:100})),Object.fromEntries(H(a))}function D(t){return[["real-fs",(e=t.fn,s=t.fs,R[e]||(L.style.fontFamily=e,R[e]=L.clientHeight),s*F/R[e])],["tag-fs",t.fs],["tag-fsp",t.fsp],["fill-color",M(t.a1+t.c1)]].filter((([,t])=>t)).map((([t,e])=>[`--ass-${t}`,e]));var e,s}function Y(t,e){const{id:s,clip:a}=t;if(!a)return{};const{width:n,height:r}=e.scriptRes;!function(t,e,s,a,n){if(t.querySelector(`#${s}`))return;let r="";if(null!==e.dots){let{x1:t,y1:s,x2:i,y2:o}=e.dots;t/=a,s/=n,i/=a,o/=n,r=`M${t},${s}L${t},${o},${i},${o},${i},${s}Z`}null!==e.drawing&&(r=e.drawing.instructions.map((({type:t,points:e})=>t+e.map((({x:t,y:e})=>`${t/a},${e/n}`)).join(","))).join(""));const i=1/(1<(m=(t=>{const e=g.left.width[t],s=g.center.width[t],a=g.right.width[t],n=g.left.end[t],r=g.center.end[t],i=g.right.end[t];return"left"===y&&(n>u&&e||r>u&&s&&2*o+s>f||i>u&&a&&o+a>f)||"center"===y&&(n>u&&e&&2*e+o>f||r>u&&s||i>u&&a&&2*a+o>f)||"right"===y&&(n>u&&e&&e+o>f||r>u&&s&&2*o+s>f||i>u&&a)})(t)?0:m+1,m>=l&&(x=t,!0));if(c<=3){x=p-h-1;for(let t=x;t>h&&!b(t);t-=1);}else if(c>=7){x=h+1;for(let t=x;t>1;for(let t=x;t3&&(x-=l-1);for(let t=x;t{n.style.setProperty(t,e)}));const d={duration:1e3*(c-l),delay:1e3*Math.min(0,l-(s.currentTime-e.delay)),fill:"forwards"},f=[];return o.forEach((t=>{const s=a[t.style].tag,n=a[t.style].style.BorderStyle;t.fragments.forEach((t=>{const{text:a,drawing:i}=t,o={...s,...t.tag};let l="";const c=[];if(!i){c.push(...D(o));const t=e.sbas?e.scale:1;c.push(...T(o,t)),l+=`font-family:"${o.fn}";`,l+=o.b?`font-weight:${1===o.b?"bold":o.b};`:"",l+=o.i?"font-style:italic;":"",l+=o.u||o.s?`text-decoration:${o.u?"underline":""} ${o.s?"line-through":""};`:""}if(i&&o.pbo){const t=-o.pbo*(o.fscy||100)/100;l+=`vertical-align:calc(var(--ass-scale) * ${t}px);`}c.push(...H(o));const p=[o,...(o.t||[]).map((t=>t.tag))],h=B.some((t=>p.some((e=>e[t])))),u=P.some((t=>p.some((e=>void 0!==e[t]&&100!==e[t])))),g=_.some((t=>p.some((e=>e[t]))));(function(t,e){return t.replace(/\\h/g," ").replace(/\\N/g,"\n").replace(/\\n/g,2===e?"\n":" ")})(a,o.q).split("\n").forEach(((a,p)=>{const y=document.createElement("span"),m=document.createElement("span");if(y.dataset.wrapStyle=o.q,y.dataset.borderStyle=n,(u||g)&&(u&&(m.dataset.scale=""),g&&(m.dataset.skew=""),m.textContent=a),h&&(y.dataset.rotate=""),i){y.dataset.drawing="";const a=W(t,s,e);if(!a)return;y.style.cssText=a.cssText,y.append(a.$svg)}else{if(y.dataset.text="",p&&r.append(document.createElement("br")),!a)return;u||g?y.append(m):y.textContent=a;const t=u||g?m:y;(o.xbord||o.ybord||o.xshad||o.yshad)&&(t.dataset.text=a)}if(y.style.cssText+=l,c.forEach((([t,e])=>{y.style.setProperty(t,e)})),t.keyframes){const e=O(y,t.keyframes,{...d,duration:t.duration});f.push(e)}r.append(y)}))}))})),t.keyframes&&f.push(O(n,t.keyframes,d)),n.append(r),{$div:n,animations:f}}(t,e);Object.assign(t,{$div:s,animations:a}),e.box.append(s);const{width:n}=s.getBoundingClientRect();Object.assign(t,{width:n}),s.style.cssText+=function(t){const{layer:e,align:s,effect:a,pos:n,margin:r}=t;let i="";return e&&(i+=`z-index:${e};`),i+=`text-align:${["left","center","right"][s.h]};`,a||(i+=`max-width:calc(100% - var(--ass-scale) * ${r.left+r.right}px);`,n||(0!==s.h&&(i+=`margin-right:calc(var(--ass-scale) * ${r.right}px);`),2!==s.h&&(i+=`margin-left:calc(var(--ass-scale) * ${r.left}px);`))),i}(t);const{height:r}=s.getBoundingClientRect();Object.assign(t,{height:r});const{x:i,y:o}=function(t,e){const{scale:s}=e,{effect:a,move:n,align:r,width:i,height:o,margin:l,slices:c}=t;let d=0,f=0;if(a&&"banner"===a.name)d=a.lefttoright?-i:e.width,f=[e.height-o-l.vertical,(e.height-o)/2,l.vertical][r.v];else if(t.pos||n){const e=t.pos||{x:0,y:0},a=s*e.x,n=s*e.y;d=[a,a-i/2,a-i][r.h],f=[n-o,n-o/2,n][r.v]}else d=[0,(e.width-i)/2,e.width-i-s*l.right][r.h],f=c.some((t=>t.fragments.some((({keyframes:t})=>t?.length))))?[e.height-o-l.vertical,(e.height-o)/2,l.vertical][r.v]:X(t,e);return{x:d,y:f}}(t,e);return Object.assign(t,{x:i,y:o}),s.style.cssText+=`width:${n}px;height:${r}px;left:${i}px;top:${o}px;`,function(t,e){const{align:s,width:a,height:n,x:r,y:i,$div:o}=t,l={};t.org?(l.x=t.org.x*e,l.y=t.org.y*e):(l.x=[r,r+a/2,r+a][s.h],l.y=[i+n,i+n/2,i][s.v]);for(let t=o.childNodes.length-1;t>=0;t-=1){const e=o.childNodes[t];if(""===e.dataset.rotate){const t=l.x-r-e.offsetLeft,s=l.y-i-e.offsetTop;e.style.cssText+=`transform-origin:${t}px ${s}px;`}}}(t,e.scale),Object.assign(t,Y(t,e)),t.effect?.name?.startsWith("scroll")&&Object.assign(t,function(t,e){const s=document.createElement("div");s.className="ASS-scroll-area",e.box.insertBefore(s,t.$div),s.append(t.$div);const{height:a}=e.scriptRes,{name:n,y1:r,y2:i}=t.effect,o=Math.min(r,i)/a*100,l=(a-Math.max(r,i))/a*100;s.style.cssText+=`top:${o}%;bottom:${l}%;`;const c=/up/.test(n);return t.$div.style.cssText+=c?"top:100%;":"top:0%;",{$div:s}}(t,e)),t}function Z(t){const{box:e}=t;for(;e.lastChild;)e.lastChild.remove();t.actives=[],t.space=[]}function J(t){const{video:e,dialogues:s,actives:a}=t,n=e.currentTime-t.delay;for(let t=a.length-1;t>=0;t-=1){const e=a[t],{end:s}=e;s=s[t.index].start;){if(n{for(let t=0;t{window.CSS.registerProperty({name:`--ass-tag-${t}`,syntax:"",inherits:!0,initialValue:0})})),P.forEach((t=>{window.CSS.registerProperty({name:`--ass-tag-${t}`,syntax:"",inherits:!0,initialValue:1})}))),window.CSS.registerProperty&&(["real-fs","tag-fs","tag-fsp"].forEach((t=>{window.CSS.registerProperty({name:`--ass-${t}`,syntax:"",inherits:!0,initialValue:"0"})})),window.CSS.registerProperty({name:"--ass-fill-color",syntax:"",inherits:!0,initialValue:"transparent"}));class Q{#t={video:null,box:document.createElement("div"),svg:C("svg"),defs:C("defs"),observer:null,scale:1,width:0,height:0,scriptRes:{},layoutRes:{},resampledRes:{},index:0,sbas:!0,styles:{},dialogues:[],actives:[],space:[],requestId:0,delay:0};#e;#s;#a;#n;constructor(t,e,{container:s=e.parentNode,resampling:a}={}){if(this.#t.video=e,!s)throw new Error("Missing container.");const{info:n,width:r,height:i,styles:o,dialogues:l}=A(t);this.#t.sbas=/yes/i.test(n.ScaledBorderAndShadow),this.#t.layoutRes={width:1*n.LayoutResX||e.videoWidth||e.clientWidth,height:1*n.LayoutResY||e.videoHeight||e.clientHeight},this.#t.scriptRes={width:r||this.#t.layoutRes.width,height:i||this.#t.layoutRes.height},this.#t.styles=o,this.#t.dialogues=l.map((t=>Object.assign(t,{id:`ASS-${E()}`,align:{h:(t.alignment+2)%3,v:Math.trunc((t.alignment-1)/3)}}))),s.append(N);const{svg:c,defs:d,scriptRes:f,box:p}=this.#t;var h;c.setAttributeNS(null,"viewBox",`0 0 ${f.width} ${f.height}`),c.append(d),s.append(c),p.className="ASS-box",s.append(p),function(t){const e=t.getRootNode()||document,s=e===document?document.head:e;let a=s.querySelector("#ASS-global-style");a||(a=document.createElement("style"),a.type="text/css",a.id="ASS-global-style",a.append(document.createTextNode('.ASS-box{font-family:Arial;overflow:hidden;pointer-events:none;position:absolute}.ASS-dialogue{font-size:0;position:absolute;z-index:0}.ASS-dialogue span{display:inline-block}.ASS-dialogue [data-text]{display:inline-block;color:var(--ass-fill-color);font-size:calc(var(--ass-scale)*var(--ass-real-fs)*1px);line-height:calc(var(--ass-scale)*var(--ass-tag-fs)*1px);letter-spacing:calc(var(--ass-scale)*var(--ass-tag-fsp)*1px)}.ASS-dialogue [data-wrap-style="0"],.ASS-dialogue [data-wrap-style="3"]{text-wrap:balance}.ASS-dialogue [data-wrap-style="1"]{word-break:break-word;white-space:normal}.ASS-dialogue [data-wrap-style="2"]{word-break:normal;white-space:nowrap}.ASS-dialogue [data-border-style="1"]{position:relative}.ASS-dialogue [data-border-style="1"]::after,.ASS-dialogue [data-border-style="1"]::before{content:attr(data-text);position:absolute;top:0;left:0;z-index:-1;filter:blur(calc(var(--ass-tag-blur)*1px))}.ASS-dialogue [data-border-style="1"]::before{color:var(--ass-shadow-color);transform:translate(calc(var(--ass-scale-stroke)*var(--ass-tag-xshad)*1px),calc(var(--ass-scale-stroke)*var(--ass-tag-yshad)*1px));-webkit-text-stroke:var(--ass-border-width) var(--ass-shadow-color);text-shadow:var(--ass-shadow-delta);opacity:var(--ass-shadow-opacity)}.ASS-dialogue [data-border-style="1"]::after{color:transparent;-webkit-text-stroke:var(--ass-border-width) var(--ass-border-color);text-shadow:var(--ass-border-delta);opacity:var(--ass-border-opacity)}.ASS-dialogue [data-border-style="3"]{padding:calc(var(--ass-scale-stroke)*var(--ass-tag-xbord)*1px) calc(var(--ass-scale-stroke)*var(--ass-tag-ybord)*1px);position:relative;filter:blur(calc(var(--ass-tag-blur)*1px))}.ASS-dialogue [data-border-style="3"]::after,.ASS-dialogue [data-border-style="3"]::before{content:"";width:100%;height:100%;position:absolute;z-index:-1}.ASS-dialogue [data-border-style="3"]::before{background-color:var(--ass-shadow-color);left:calc(var(--ass-scale-stroke)*var(--ass-tag-xshad)*1px);top:calc(var(--ass-scale-stroke)*var(--ass-tag-yshad)*1px)}.ASS-dialogue [data-border-style="3"]::after{background-color:var(--ass-border-color);left:0;top:0}@container style(--ass-tag-xbord: 0) and style(--ass-tag-ybord: 0){.ASS-dialogue [data-border-style="3"]::after{background-color:transparent}}@container style(--ass-tag-xshad: 0) and style(--ass-tag-yshad: 0){.ASS-dialogue [data-border-style="3"]::before{background-color:transparent}}.ASS-dialogue [data-rotate]{transform:perspective(312.5px) rotateY(calc(var(--ass-tag-fry)*1deg)) rotateX(calc(var(--ass-tag-frx)*1deg)) rotateZ(calc(var(--ass-tag-frz)*-1deg))}.ASS-dialogue [data-text][data-rotate]{transform-style:preserve-3d;word-break:normal;white-space:nowrap}.ASS-dialogue [data-scale],.ASS-dialogue [data-skew]{display:inline-block;transform:scale(var(--ass-tag-fscx),var(--ass-tag-fscy)) skew(calc(var(--ass-tag-fax)*1rad),calc(var(--ass-tag-fay)*1rad));transform-origin:var(--ass-align-h) var(--ass-align-v)}.ASS-fix-font-size{font-size:2048px;font-family:Arial;line-height:normal;width:0;height:0;position:absolute;visibility:hidden;overflow:hidden}.ASS-clip-area,.ASS-fix-font-size span{position:absolute}.ASS-clip-area{width:100%;height:100%;top:0;left:0}.ASS-scroll-area{position:absolute;width:100%;overflow:hidden}')),s.append(a))}(s),this.#e=(h=this.#t,function(){const t=()=>{J(h),h.requestId=requestAnimationFrame(t)};cancelAnimationFrame(h.requestId),h.requestId=requestAnimationFrame(t),h.actives.forEach((t=>{j(t,"play")}))}),this.#s=function(t){return function(){cancelAnimationFrame(t.requestId),t.requestId=0,t.actives.forEach((t=>{j(t,"pause")}))}}(this.#t),this.#a=K(this.#t),e.addEventListener("play",this.#e),e.addEventListener("pause",this.#s),e.addEventListener("playing",this.#e),e.addEventListener("waiting",this.#s),e.addEventListener("seeking",this.#a),this.#n=function(t,e){const{video:s,box:a,svg:n,layoutRes:r}=e;return function(){const i=s.clientWidth,o=s.clientHeight,l=r.width||s.videoWidth||i,c=r.height||s.videoHeight||o,d=e.scriptRes.width,f=e.scriptRes.height;let p=d,h=f;const u=Math.min(i/l,o/c);"video_width"===t.resampling&&(h=d/l*c),"video_height"===t.resampling&&(p=f/c*l),e.scale=Math.min(i/p,o/h),"script_width"===t.resampling&&(e.scale=u*(l/p)),"script_height"===t.resampling&&(e.scale=u*(c/h));const g=e.scale*p,y=e.scale*h;e.width=g,e.height=y,e.resampledRes={width:p,height:h};const m=`width:${g}px;height:${y}px;top:${(o-y)/2}px;left:${(i-g)/2}px;`;a.style.cssText=m,a.style.setProperty("--ass-scale",e.scale),a.style.setProperty("--ass-scale-stroke",e.sbas?e.scale:1),n.style.cssText=m,K(e)()}}(this,this.#t),this.#n(),this.resampling=a,l.forEach((t=>{!function(t,e){const{start:s,end:a,effect:n,move:r,fade:i,slices:o}=t,l=1e3*(a-s),c=[...n&&!r?V({effect:n,duration:l}):[],...r?q({move:r,duration:l,dialogue:t}):[],...i?I(i,l):[]].sort(((t,e)=>t.offset-e.offset));c.length>0&&Object.assign(t,{keyframes:c}),o.forEach((t=>{const s=e.styles[t.style].tag;t.fragments.forEach((t=>{if(!t.tag.t||0===t.tag.t.length)return;const a={...s,...t.tag},n=(r=t.tag.t,r.reduceRight(((t,e)=>{let s=!1;return t.map((t=>(s=e.t1===t.t1&&e.t2===t.t2&&e.accel===t.accel,{...t,...s?{tag:{...t.tag,...e.tag}}:{}}))).concat(s?[]:e)}),[])).sort(((t,e)=>t.t2-e.t2||t.t1-e.t1));var r;n[0].t1>0&&n.unshift({t1:0,t2:n[0].t1,tag:a}),n.reduce(((t,e)=>{const s={...t,...e.tag};return s.t=null,Object.assign(e.tag,s),s}),{});const i=Math.max(l,...n.map((({t2:t})=>t))),o=n.map((({t2:s,tag:n})=>({offset:s/i,...Object.fromEntries(D({...n,a1:n.a1||a.a1,c1:n.c1||a.c1})),...Object.fromEntries(T({...a,...n},e.sbas?e.scale:1)),...U({fromTag:a,tag:n,fragment:t})}))).sort(((t,e)=>t.offset-e.offset));o.length>0&&Object.assign(t,{keyframes:o,duration:i})}))}))}(t,this.#t)}));const u=new ResizeObserver(this.#n);return u.observe(e),this.#t.observer=u,this}destroy(){const{video:t,box:e,svg:s,observer:a}=this.#t;return this.#s(),Z(this.#t),t.removeEventListener("play",this.#e),t.removeEventListener("pause",this.#s),t.removeEventListener("playing",this.#e),t.removeEventListener("waiting",this.#s),t.removeEventListener("seeking",this.#a),N.remove(),s.remove(),e.remove(),a.unobserve(this.#t.video),this.#t.styles={},this.#t.dialogues=[],this}show(){return this.#t.box.style.visibility="visible",this}hide(){return this.#t.box.style.visibility="hidden",this}#r="video_height";get resampling(){return this.#r}set resampling(t){t!==this.#r&&/^(video|script)_(width|height)$/.test(t)&&(this.#r=t,this.#n())}get delay(){return this.#t.delay}set delay(t){"number"==typeof t&&(this.#t.delay=t,this.#a())}}export{Q as default}; +function t(t){var e=t.toLowerCase().trim().split(/\s*;\s*/);return"banner"===e[0]?{name:e[0],delay:1*e[1]||0,leftToRight:1*e[2]||0,fadeAwayWidth:1*e[3]||0}:/^scroll\s/.test(e[0])?{name:e[0],y1:Math.min(1*e[1],1*e[2]),y2:Math.max(1*e[1],1*e[2]),delay:1*e[3]||0,fadeAwayHeight:1*e[4]||0}:""!==t?{name:t}:null}function e(t){return t?t.toLowerCase().replace(/([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?)/g," $1 ").replace(/([mnlbspc])/g," $1 ").trim().replace(/\s+/g," ").split(/\s(?=[mnlbspc])/).map((function(t){return t.split(" ").filter((function(t,e){return!(e&&isNaN(1*t))}))})):[]}var a=["b","i","u","s","fsp","k","K","kf","ko","kt","fe","q","p","pbo","a","an","fscx","fscy","fax","fay","frx","fry","frz","fr","be","blur","bord","xbord","ybord","shad","xshad","yshad"].map((function(t){return{name:t,regex:new RegExp("^"+t+"-?\\d")}}));function s(t){for(var r,n={},i=0;ia.length){var r=s.slice(a.length-1).join();(s=s.slice(0,a.length-1)).push(r)}for(var i,o={},l=0;l-10?1+i/10:1)*a.fs:1*i};if("K"===e)return{kf:i};if("t"===e){var b=i.t1,w=i.accel,S=i.tags,$=i.t2||1e3*(a.end-a.start),A={};return S.forEach((function(t){var e=Object.keys(t)[0];~g.indexOf(e)&&("clip"!==e||t[e].dots)&&Object.assign(A,y(t,e,a))})),{t:{t1:b,t2:$,accel:w,tag:A}}}return(n={})[e]=i,n}var m=[null,1,2,3,null,7,8,9,null,4,5,6],x=["r","a","an","pos","org","move","fade","fad","clip"];function v(t){for(var e,a,s,r,n,i,o,l=t.styles,c=t.style,d=t.parsed,f=t.start,p=t.end,u={q:l[c].tag.q},g=[],v={style:c,fragments:[]},b={},w=0;w=i.End)){e[i.Style]||(i.Style="Default");var o=e[i.Style].style,l=v({styles:e,style:i.Style,parsed:i.Text.parsed,start:i.Start,end:i.End}),c=l.alignment||o.Alignment;s=Math.min(s,i.Layer),r.push(Object.assign({layer:i.Layer,start:i.Start,end:i.End,style:i.Style,name:i.Name,margin:{left:i.MarginL||o.MarginL,right:i.MarginR||o.MarginR,vertical:i.MarginV||o.MarginV},effect:i.Effect},l,{alignment:c}))}}for(var d=0;d{const e=Math.trunc(16*Math.random());return("x"===t?e:3&e|8).toString(16)}))}function z(t,e=[]){const a=document.createElementNS("http://www.w3.org/2000/svg",t);for(let t=0;t{t[e]()}))}function P(t,e){const{name:a,delay:s,leftToRight:r}=t,n="banner"===a?"X":"Y",i={X:r?1:-1,Y:/up/.test(a)?-1:1}[n],o=-100*i;return[[{offset:0,transform:`translate${n}(${o}%)`},{offset:1,transform:`translate${n}(calc(${o}% + var(--ass-scale) * ${e/(s||1)*i}px))`}],{duration:e,fill:"forwards"}]}function H(t){return`calc(var(--ass-scale) * ${t}px)`}function q(t,e){const{x1:a,y1:s,x2:r,y2:n,t1:i,t2:o}=t,l=`translate(${H(a)}, ${H(s)})`,c=`translate(${H(r)}, ${H(n)})`,d=Math.max(o,e);return[[{offset:0,transform:l},i>0?{offset:i/d,transform:l}:null,o>0&&o1-t/255));return[[[0,i,o,l,c].map((t=>t/d)).map(((t,e)=>({offset:t,opacity:f[e>>1]}))),{duration:d,fill:"forwards"}]]}function V(t,e){if(1===e)return"linear";const a=Math.ceil(t/1e3*60);return`linear(${Array.from({length:a+1}).map(((t,s)=>(s/a)**e)).join(",")})`}function D(t,e,a){const s={...a,...e.tag};return(e.tag.t||[]).map((({t1:e,t2:a,accel:r,tag:n})=>{const i=Object.fromEntries(Object.keys(n).flatMap((t=>function(t,e,a){const s=e[a];return void 0===s||"clip"===a?[]:"a1"===a||"c1"===a?[["fill-color",F((e.a1||t.a1)+(e.c1||t.c1))]]:"c3"===a?[["border-color",F(`00${e.c3}`)]]:"a3"===a?[["border-opacity",R(e.a3)]]:"c4"===a?[["shadow-color",F(`00${e.c4}`)]]:"a4"===a?[["shadow-opacity",R(e.a4)]]:"fs"===a?[["real-fs",L(e.fn||t.fn,e.fs)],["tag-fs",s]]:"fscx"===a||"fscy"===a?[[`tag-${a}`,(s||100)/100]]:[[`tag-${a}`,s]]}(s,n,t))).map((([t,e])=>[`--ass-${t}`,e])).concat([["offset",1]])),o=Math.max(0,a-e);return T(t,[i],{duration:o,delay:e,fill:"forwards",easing:V(o,r)})}))}function W(t,e,a){if(!t.dots)return"";const{x1:s,y1:r,x2:n,y2:i}=t.dots;return`polygon(evenodd, ${[[s,r],[s,i],[n,i],[n,r],[s,r]].map((([t,s])=>[t/e,s/a])).concat(t.inverse?[[0,0],[0,1],[1,1],[1,0],[0,0]]:[]).map((t=>t.map((t=>100*t+"%")).join(" "))).join(",")})`}function U(t,e){const{clip:a,animations:s}=t;if(!a)return{};const{width:r,height:n}=e.scriptRes,i=document.createElement("div");return e.box.insertBefore(i,t.$div),i.append(t.$div),i.className="ASS-clip-area",i.style.zIndex=t.$div.style.zIndex,i.style.clipPath=a.dots?W(a,r,n):function(t,e,a,s){if(!t.drawing)return"";const r=s.scale/(1<t+e.map((({x:t,y:e})=>`${t*r},${e*r}`)).join(","))).join("");return t.inverse&&(n+=`M0,0L0,${a},${e},${a},${e},0,0,0Z`),`path(evenodd, "${n}")`}(a,r,n,e),s.push(...function(t,e,a){return e.slices.flatMap((t=>t.fragments)).flatMap((t=>t.tag.t||[])).filter((({tag:t})=>t.clip)).map((({t1:e,t2:s,accel:r,tag:n})=>{const i={offset:1,clipPath:W(n.clip,a.scriptRes.width,a.scriptRes.height)},o=Math.max(0,s-e);return T(t,[i],{duration:o,delay:e,fill:"forwards",easing:V(o,r)})}))}(i,t,e)),{$div:i}}function X([t,e]){return[[0,0],[0,1],[1,0],[1,1]].filter((([a,s])=>(a||t)&&(s||e))).map((([a,s])=>[(a||-1)*t,(s||-1)*e]))}function Y(t,e){const a=F(`00${t.c3}`),s=t.xbord*e,r=t.ybord*e,n=F(`00${t.c4}`),i=t.blur||t.be||0,o=function(t,e){if(t===e)return[];const a=Math.min(t,e),s=Math.max(t,e);return Array.from({length:Math.ceil(s)-1},((t,e)=>e+1)).concat(s).map((t=>[(s-t)/s*a,t])).map((([a,s])=>t>e?[s,a]:[a,s])).flatMap(X)}(s,r);return[["border-width",2*Math.min(s,r)+"px"],["border-color",a],["border-opacity",R(t.a3)],["border-delta",o.map((([t,e])=>`${t}px ${e}px ${a}`)).join(",")],["shadow-color",n],["shadow-opacity",R(t.a4)],["shadow-delta",o.map((([t,e])=>`${t}px ${e}px ${n}`)).join(",")],["tag-blur",i],["tag-xbord",t.xbord],["tag-ybord",t.ybord],["tag-xshad",t.xshad],["tag-yshad",t.yshad]].map((([t,e])=>[`--ass-${t}`,e]))}function G(t,e,a){if(!t.drawing.d)return null;const s={...e,...t.tag},{minX:r,minY:n,width:i,height:o}=t.drawing,l=a.scale/(1<{window.CSS.registerProperty({name:`--ass-${t}`,syntax:"",inherits:!0,initialValue:"0"})})),window.CSS.registerProperty({name:"--ass-fill-color",syntax:"",inherits:!0,initialValue:"transparent"})),window.CSS.registerProperty&&(window.CSS.registerProperty({name:"--ass-border-width",syntax:"",inherits:!0,initialValue:"0px"}),["border-color","shadow-color"].forEach((t=>{window.CSS.registerProperty({name:`--ass-${t}`,syntax:"",inherits:!0,initialValue:"transparent"})})),["border-opacity","shadow-opacity"].forEach((t=>{window.CSS.registerProperty({name:`--ass-${t}`,syntax:"",inherits:!0,initialValue:"1"})})),["blur","xbord","ybord","xshad","yshad"].forEach((t=>{window.CSS.registerProperty({name:`--ass-tag-${t}`,syntax:"",inherits:!0,initialValue:"0"})})));const J=["frx","fry","frz"],K=["fscx","fscy"],Z=["fax","fay"];function Q(t,e){const{styles:a}=e,s=document.createElement("div");s.className="ASS-dialogue",s.dataset.wrapStyle=t.q;const r=document.createDocumentFragment(),{align:n,slices:i}=t;[["--ass-align-h",["0%","50%","100%"][n.h]],["--ass-align-v",["100%","50%","0%"][n.v]]].forEach((([t,e])=>{s.style.setProperty(t,e)}));const o=[];return i.forEach((t=>{const s=a[t.style].tag,n=a[t.style].style.BorderStyle;t.fragments.forEach((t=>{const{text:a,drawing:i}=t,l={...s,...t.tag};let c="";const d=[];if(!i){d.push(...function(t){return[["real-fs",L(t.fn,t.fs)],["tag-fs",t.fs],["tag-fsp",t.fsp],["fill-color",F(t.a1+t.c1)]].filter((([,t])=>t)).map((([t,e])=>[`--ass-${t}`,e]))}(l));const t=e.sbas?e.scale:1;d.push(...Y(l,t)),c+=`font-family:"${l.fn}";`,c+=l.b?`font-weight:${1===l.b?"bold":l.b};`:"",c+=l.i?"font-style:italic;":"",c+=l.u||l.s?`text-decoration:${l.u?"underline":""} ${l.s?"line-through":""};`:""}if(i&&l.pbo){const t=-l.pbo*(l.fscy||100)/100;c+=`vertical-align:calc(var(--ass-scale) * ${t}px);`}d.push(...function(t){return[...[...J,...Z].map((e=>[`--ass-tag-${e}`,`${t[e]||0}`])),...K.map((e=>[`--ass-tag-${e}`,t.p?1:(t[e]||100)/100]))]}(l));const f=[l,...(l.t||[]).map((t=>t.tag))],p=J.some((t=>f.some((e=>e[t])))),u=K.some((t=>f.some((e=>void 0!==e[t]&&100!==e[t])))),h=Z.some((t=>f.some((e=>e[t]))));(function(t,e){return t.replace(/\\h/g," ").replace(/\\N/g,"\n").replace(/\\n/g,2===e?"\n":" ")})(a,l.q).split("\n").forEach(((a,f)=>{const g=document.createElement("span"),y=document.createElement("span");if((u||h)&&(u&&(y.dataset.scale=""),h&&(y.dataset.skew=""),y.textContent=a),p&&(g.dataset.rotate=""),i){g.dataset.drawing="";const a=G(t,s,e);if(!a)return;g.style.cssText=a.cssText,g.append(a.$svg)}else{if(f&&r.append(document.createElement("br")),!a)return;u||h?g.append(y):g.textContent=a;const t=u||h?y:g;t.dataset.text=a,(l.xbord||l.ybord||l.xshad||l.yshad)&&(t.dataset.borderStyle=n)}g.style.cssText+=c,d.forEach((([t,e])=>{g.style.setProperty(t,e)})),o.push(...D(g,t,s)),r.append(g)}))}))})),o.push(...function(t,e){const{start:a,end:s,effect:r,move:n,fade:i}=e,o=1e3*(s-a);return[r&&!n?P(r,o):null,n?q(n,o):null,...i?I(i,o):[]].filter(Boolean).map((([e,a])=>T(t,e,a)))}(s,t)),s.append(r),{$div:s,animations:o}}function tt(t,e){const{video:a,space:s,scale:r}=e,{layer:n,margin:i,width:o,height:l,alignment:c,end:d}=t,f=e.width-Math.trunc(r*(i.left+i.right)),p=e.height,u=Math.trunc(r*i.vertical),h=100*a.currentTime;s[n]=s[n]||{left:{width:new Uint16Array(p+1),end:new Uint32Array(p+1)},center:{width:new Uint16Array(p+1),end:new Uint32Array(p+1)},right:{width:new Uint16Array(p+1),end:new Uint32Array(p+1)}};const g=s[n],y=["right","left","center"][c%3];let m=0,x=0;const v=t=>(m=(t=>{const e=g.left.width[t],a=g.center.width[t],s=g.right.width[t],r=g.left.end[t],n=g.center.end[t],i=g.right.end[t];return"left"===y&&(r>h&&e||n>h&&a&&2*o+a>f||i>h&&s&&o+s>f)||"center"===y&&(r>h&&e&&2*e+o>f||n>h&&a||i>h&&s&&2*s+o>f)||"right"===y&&(r>h&&e&&e+o>f||n>h&&a&&2*o+a>f||i>h&&s)})(t)?0:m+1,m>=l&&(x=t,!0));if(c<=3){x=p-u-1;for(let t=x;t>u&&!v(t);t-=1);}else if(c>=7){x=u+1;for(let t=x;t>1;for(let t=x;t3&&(x-=l-1);for(let t=x;tt.fragments.some((({keyframes:t})=>t?.length))))?[e.height-i-o.vertical,(e.height-i)/2,o.vertical][r.v]:tt(t,e);return{x:c+[0,n/2,n][r.h],y:d+[i,i/2,0][r.v]}}(t,e);return Object.assign(t,{x:i,y:o}),a.style.cssText+=`left:${i}px;top:${o}px;`,function(t,e){const{align:a,width:s,height:r,x:n,y:i,$div:o}=t,l=(t.org?t.org.x*e:n)+[0,s/2,s][a.h],c=(t.org?t.org.y*e:i)+[r,r/2,0][a.v];for(let t=o.childNodes.length-1;t>=0;t-=1){const e=o.childNodes[t];if(""===e.dataset.rotate){const t=l-n-e.offsetLeft,a=c-i-e.offsetTop;e.style.cssText+=`transform-origin:${t}px ${a}px;`}}}(t,e.scale),Object.assign(t,U(t,e)),t.effect&&Object.assign(t,{$div:et(t,e)}),t}function st(t){const{box:e}=t;for(;e.lastChild;)e.lastChild.remove();t.actives=[],t.space=[]}function rt(t){const{video:e,dialogues:a,actives:s}=t,r=e.currentTime-t.delay;for(let t=s.length-1;t>=0;t-=1){const e=s[t],{end:a}=e;a=a[t.index].start;){if(r{t.currentTime=1e3*(r-n.start)})),e.paused||_(n,"play"),s.push(n)}t.index+=1}}function nt(t){return function(){st(t);const{video:e,dialogues:a}=t,s=e.currentTime-t.delay;t.index=(()=>{for(let t=0;t{window.CSS.registerProperty({name:`--ass-tag-${t}`,syntax:"",inherits:!0,initialValue:0})})),K.forEach((t=>{window.CSS.registerProperty({name:`--ass-tag-${t}`,syntax:"",inherits:!0,initialValue:1})})));class it{#t={video:null,box:document.createElement("div"),observer:null,scale:1,width:0,height:0,scriptRes:{},layoutRes:{},resampledRes:{},index:0,sbas:!0,styles:{},dialogues:[],actives:[],space:[],requestId:0,delay:0};#e;#a;#s;#r;constructor(t,e,{container:a=e.parentNode,resampling:s}={}){if(this.#t.video=e,!a)throw new Error("Missing container.");const{info:r,width:n,height:i,styles:o,dialogues:l}=$(t);this.#t.sbas=/yes/i.test(r.ScaledBorderAndShadow),this.#t.layoutRes={width:1*r.LayoutResX||e.videoWidth||e.clientWidth,height:1*r.LayoutResY||e.videoHeight||e.clientHeight},this.#t.scriptRes={width:n||this.#t.layoutRes.width,height:i||this.#t.layoutRes.height},this.#t.styles=o,this.#t.dialogues=l.map((t=>Object.assign(t,{effect:["banner","scroll up","scroll down"].includes(t.effect?.name)?t.effect:null,align:{h:(t.alignment+2)%3,v:Math.trunc((t.alignment-1)/3)}}))),N&&a.append(N);const{box:c}=this.#t;var d;c.className="ASS-box",a.append(c),function(t){const e=t.getRootNode()||document,a=e===document?document.head:e;let s=a.querySelector("#ASS-global-style");s||(s=document.createElement("style"),s.type="text/css",s.id="ASS-global-style",s.append(document.createTextNode('.ASS-box{font-family:Arial;overflow:hidden;pointer-events:none;position:absolute}.ASS-dialogue{font-size:0;width:max-content;position:absolute;z-index:0;transform:translate(calc(var(--ass-align-h)*-1),calc(var(--ass-align-v)*-1))}.ASS-dialogue span{display:inline-block}.ASS-dialogue [data-text]{display:inline-block;color:var(--ass-fill-color);font-size:calc(var(--ass-scale)*var(--ass-real-fs)*1px);line-height:calc(var(--ass-scale)*var(--ass-tag-fs)*1px);letter-spacing:calc(var(--ass-scale)*var(--ass-tag-fsp)*1px)}.ASS-dialogue[data-wrap-style="0"],.ASS-dialogue[data-wrap-style="3"]{text-wrap:balance;white-space:pre-wrap}.ASS-dialogue[data-wrap-style="1"]{word-break:break-word;white-space:normal}.ASS-dialogue[data-wrap-style="2"]{word-break:normal;white-space:nowrap}.ASS-dialogue [data-border-style="1"]{position:relative;filter:blur(calc(var(--ass-tag-blur)*calc(1 - sign(var(--ass-tag-xbord)))*calc(1 - sign(var(--ass-tag-ybord)))*1px))}.ASS-dialogue [data-border-style="1"]::after,.ASS-dialogue [data-border-style="1"]::before{content:attr(data-text);position:absolute;top:0;left:0;z-index:-1;filter:blur(calc(var(--ass-tag-blur)*1px))}.ASS-dialogue [data-border-style="1"]::before{color:var(--ass-shadow-color);transform:translate(calc(var(--ass-scale-stroke)*var(--ass-tag-xshad)*1px),calc(var(--ass-scale-stroke)*var(--ass-tag-yshad)*1px));-webkit-text-stroke:var(--ass-border-width) var(--ass-shadow-color);text-shadow:var(--ass-shadow-delta);opacity:var(--ass-shadow-opacity)}.ASS-dialogue [data-border-style="1"]::after{color:transparent;-webkit-text-stroke:var(--ass-border-width) var(--ass-border-color);text-shadow:var(--ass-border-delta);opacity:var(--ass-border-opacity)}.ASS-dialogue [data-border-style="3"]{padding:calc(var(--ass-scale-stroke)*var(--ass-tag-xbord)*1px) calc(var(--ass-scale-stroke)*var(--ass-tag-ybord)*1px);position:relative;filter:blur(calc(var(--ass-tag-blur)*1px))}.ASS-dialogue [data-border-style="3"]::after,.ASS-dialogue [data-border-style="3"]::before{content:"";width:100%;height:100%;position:absolute;z-index:-1}.ASS-dialogue [data-border-style="3"]::before{background-color:var(--ass-shadow-color);left:calc(var(--ass-scale-stroke)*var(--ass-tag-xshad)*1px);top:calc(var(--ass-scale-stroke)*var(--ass-tag-yshad)*1px)}.ASS-dialogue [data-border-style="3"]::after{background-color:var(--ass-border-color);left:0;top:0}@container style(--ass-tag-xbord: 0) and style(--ass-tag-ybord: 0){.ASS-dialogue [data-border-style="3"]::after{background-color:transparent}}@container style(--ass-tag-xshad: 0) and style(--ass-tag-yshad: 0){.ASS-dialogue [data-border-style="3"]::before{background-color:transparent}}.ASS-dialogue [data-rotate]{transform:perspective(312.5px) rotateY(calc(var(--ass-tag-fry)*1deg)) rotateX(calc(var(--ass-tag-frx)*1deg)) rotateZ(calc(var(--ass-tag-frz)*-1deg))}.ASS-dialogue [data-text][data-rotate]{transform-style:preserve-3d;word-break:normal;white-space:nowrap}.ASS-dialogue [data-scale],.ASS-dialogue [data-skew]{display:inline-block;transform:scale(var(--ass-tag-fscx),var(--ass-tag-fscy)) skew(calc(var(--ass-tag-fax)*1rad),calc(var(--ass-tag-fay)*1rad));transform-origin:var(--ass-align-h) var(--ass-align-v)}.ASS-fix-font-size{font-family:Arial;line-height:normal;width:0;height:0;position:absolute;visibility:hidden;overflow:hidden}.ASS-clip-area,.ASS-fix-font-size span{position:absolute}.ASS-clip-area{width:100%;height:100%;top:0;left:0}.ASS-effect-area{position:absolute;display:flex;width:100%;height:fit-content;overflow:hidden;mask-composite:intersect}.ASS-effect-area[data-effect=banner]{flex-direction:column;height:100%}.ASS-effect-area .ASS-dialogue{position:static;transform:none}')),a.append(s))}(a),this.#e=(d=this.#t,function(){const t=()=>{rt(d),d.requestId=requestAnimationFrame(t)};cancelAnimationFrame(d.requestId),d.requestId=requestAnimationFrame(t),d.actives.forEach((t=>{_(t,"play")}))}),this.#a=function(t){return function(){cancelAnimationFrame(t.requestId),t.requestId=0,t.actives.forEach((t=>{_(t,"pause")}))}}(this.#t),this.#s=nt(this.#t),e.addEventListener("play",this.#e),e.addEventListener("pause",this.#a),e.addEventListener("playing",this.#e),e.addEventListener("waiting",this.#a),e.addEventListener("seeking",this.#s),this.#r=function(t,e){const{video:a,box:s,layoutRes:r}=e;return function(){const n=a.clientWidth,i=a.clientHeight,o=r.width||a.videoWidth||n,l=r.height||a.videoHeight||i,c=e.scriptRes.width,d=e.scriptRes.height;let f=c,p=d;const u=Math.min(n/o,i/l);"video_width"===t.resampling&&(p=c/o*l),"video_height"===t.resampling&&(f=d/l*o),e.scale=Math.min(n/f,i/p),"script_width"===t.resampling&&(e.scale=u*(o/f)),"script_height"===t.resampling&&(e.scale=u*(l/p));const h=e.scale*f,g=e.scale*p;e.width=h,e.height=g,e.resampledRes={width:f,height:p},s.style.cssText=`width:${h}px;height:${g}px;top:${(i-g)/2}px;left:${(n-h)/2}px;`,s.style.setProperty("--ass-scale",e.scale),s.style.setProperty("--ass-scale-stroke",e.sbas?e.scale:1),nt(e)()}}(this,this.#t),this.#r(),this.resampling=s;const f=new ResizeObserver(this.#r);return f.observe(e),this.#t.observer=f,this}destroy(){const{video:t,box:e,observer:a}=this.#t;return this.#a(),st(this.#t),t.removeEventListener("play",this.#e),t.removeEventListener("pause",this.#a),t.removeEventListener("playing",this.#e),t.removeEventListener("waiting",this.#a),t.removeEventListener("seeking",this.#s),N&&N.remove(),e.remove(),a.unobserve(this.#t.video),this.#t.styles={},this.#t.dialogues=[],this}show(){return this.#t.box.style.visibility="visible",this}hide(){return this.#t.box.style.visibility="hidden",this}#n="video_height";get resampling(){return this.#n}set resampling(t){t!==this.#n&&/^(video|script)_(width|height)$/.test(t)&&(this.#n=t,this.#r())}get delay(){return this.#t.delay}set delay(t){"number"==typeof t&&(this.#t.delay=t,this.#s())}}export{it as default}; diff --git a/package.json b/package.json index 9d7b769..27075ca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "assjs", - "version": "0.1.1", + "version": "0.1.2", "type": "module", "description": "A JavaScript ASS subtitle format renderer", "main": "dist/ass.js",