From 0ecdd45cdb0c472ca059d3a39bdbb7f8787451b1 Mon Sep 17 00:00:00 2001 From: weizhenye Date: Tue, 11 Jun 2024 00:08:54 +0800 Subject: [PATCH] 0.1.0 --- dist/ass.d.ts | 91 ++ dist/ass.js | 3693 +++++++++++++++++++++++------------------------ dist/ass.min.js | 2 +- package.json | 2 +- 4 files changed, 1882 insertions(+), 1906 deletions(-) create mode 100644 dist/ass.d.ts diff --git a/dist/ass.d.ts b/dist/ass.d.ts new file mode 100644 index 0000000..f71b4b0 --- /dev/null +++ b/dist/ass.d.ts @@ -0,0 +1,91 @@ +/** + * @typedef {Object} ASSOption@typedef {Object} ASSOption + * @property {HTMLElement} [container] The container to display subtitles. + * Its style should be set with `position: relative` for subtitles will absolute to it. + * Defaults to `video.parentNode` + * @property {`${"video" | "script"}_${"width" | "height"}`} [resampling="video_height"] + * When script resolution(PlayResX and PlayResY) don't match the video resolution, this API defines how it behaves. + * However, drawings and clips will be always depending on script origin resolution. + * There are four valid values, we suppose video resolution is 1280x720 and script resolution is 640x480 in following situations: + * + `video_width`: Script resolution will set to video resolution based on video width. Script resolution will set to 640x360, and scale = 1280 / 640 = 2. + * + `video_height`(__default__): Script resolution will set to video resolution based on video height. Script resolution will set to 853.33x480, and scale = 720 / 480 = 1.5. + * + `script_width`: Script resolution will not change but scale is based on script width. So scale = 1280 / 640 = 2. This may causes top and bottom subs disappear from video area. + * + `script_height`: Script resolution will not change but scale is based on script height. So scale = 720 / 480 = 1.5. Script area will be centered in video area. + */ +declare class ASS { + /** + * Initialize an ASS instance + * @param {string} content ASS content + * @param {HTMLVideoElement} video The video element to be associated with + * @param {ASSOption} [option] + * @returns {ASS} + * @example + * + * HTML: + * ```html + *
+ * + * + *
+ * ``` + * + * JavaScript: + * ```js + * import ASS from 'assjs'; + * + * const content = await fetch('/path/to/example.ass').then((res) => res.text()); + * const ass = new ASS(content, document.querySelector('#video'), { + * container: document.querySelector('#container'), + * }); + * ``` + */ + constructor(content: string, video: HTMLVideoElement, { container, resampling }?: ASSOption); + set resampling(r: "video_width" | "video_height" | "script_width" | "script_height"); + /** @type {ASSOption['resampling']} */ + get resampling(): "video_width" | "video_height" | "script_width" | "script_height"; + /** + * Desctroy the ASS instance + * @returns {ASS} + */ + destroy(): ASS; + /** + * Show subtitles in the container + * @returns {ASS} + */ + show(): ASS; + /** + * Hide subtitles in the container + * @returns {ASS} + */ + hide(): ASS; + set delay(d: number); + /** @type {number} Subtitle delay in seconds. */ + get delay(): number; + #private; +} +export default ASS; + +export declare type ASSOption = { + /** + * The container to display subtitles. + * Its style should be set with `position: relative` for subtitles will absolute to it. + * Defaults to `video.parentNode` + */ + container?: HTMLElement; + /** + * When script resolution(PlayResX and PlayResY) don't match the video resolution, this API defines how it behaves. + * However, drawings and clips will be always depending on script origin resolution. + * There are four valid values, we suppose video resolution is 1280x720 and script resolution is 640x480 in following situations: + * + `video_width`: Script resolution will set to video resolution based on video width. Script resolution will set to 640x360, and scale = 1280 / 640 = 2. + * + `video_height`(__default__): Script resolution will set to video resolution based on video height. Script resolution will set to 853.33x480, and scale = 720 / 480 = 1.5. + * + `script_width`: Script resolution will not change but scale is based on script width. So scale = 1280 / 640 = 2. This may causes top and bottom subs disappear from video area. + * + `script_height`: Script resolution will not change but scale is based on script height. So scale = 720 / 480 = 1.5. Script area will be centered in video area. + */ + resampling?: `${"video" | "script"}_${"width" | "height"}`; +}; + +export { } diff --git a/dist/ass.js b/dist/ass.js index 0fcc8cf..f792816 100644 --- a/dist/ass.js +++ b/dist/ass.js @@ -1,2061 +1,1946 @@ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : - typeof define === 'function' && define.amd ? define(factory) : - (global = global || self, global.ASS = factory()); -}(this, (function () { 'use strict'; - - function parseEffect(text) { - var param = text - .toLowerCase() - .trim() - .split(/\s*;\s*/); - if (param[0] === 'banner') { - return { - name: param[0], - delay: param[1] * 1 || 0, - leftToRight: param[2] * 1 || 0, - fadeAwayWidth: param[3] * 1 || 0, - }; - } - if (/^scroll\s/.test(param[0])) { - return { - name: param[0], - y1: Math.min(param[1] * 1, param[2] * 1), - y2: Math.max(param[1] * 1, param[2] * 1), - delay: param[3] * 1 || 0, - fadeAwayHeight: param[4] * 1 || 0, - }; +function parseEffect(text) { + const param = text + .toLowerCase() + .trim() + .split(/\s*;\s*/); + if (param[0] === 'banner') { + return { + name: param[0], + delay: param[1] * 1 || 0, + leftToRight: param[2] * 1 || 0, + fadeAwayWidth: param[3] * 1 || 0, + }; + } + if (/^scroll\s/.test(param[0])) { + return { + name: param[0], + y1: Math.min(param[1] * 1, param[2] * 1), + y2: Math.max(param[1] * 1, param[2] * 1), + delay: param[3] * 1 || 0, + fadeAwayHeight: param[4] * 1 || 0, + }; + } + if (text !== '') { + return { name: text }; + } + return null; +} + +function parseDrawing(text) { + if (!text) return []; + return text + .toLowerCase() + // numbers + .replace(/([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?)/g, ' $1 ') + // commands + .replace(/([mnlbspc])/g, ' $1 ') + .trim() + .replace(/\s+/g, ' ') + .split(/\s(?=[mnlbspc])/) + .map((cmd) => ( + cmd.split(' ') + .filter((x, i) => !(i && Number.isNaN(x * 1))) + )); +} + +const 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', +]; + +const numRegexs = numTags.map((nt) => ({ 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]; + if (regex.test(text)) { + tag[name] = text.slice(name.length) * 1; + return tag; } - return null; } - - function parseDrawing(text) { - return text - .toLowerCase() - // numbers - .replace(/([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?)/g, ' $1 ') - // commands - .replace(/([mnlbspc])/g, ' $1 ') + if (/^fn/.test(text)) { + tag.fn = text.slice(2); + } else if (/^r/.test(text)) { + tag.r = text.slice(1); + } 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); + } 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); + } 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); + } else if (/^(?:pos|org|move|fad|fade)\([^)]+/.test(text)) { + const [, key, value] = text.match(/^(\w+)\((.*?)\)?$/); + tag[key] = value .trim() - .replace(/\s+/g, ' ') - .split(/\s(?=[mnlbspc])/) - .map(function (cmd) { return ( - cmd.split(' ') - .filter(function (x, i) { return !(i && Number.isNaN(x * 1)); }) - ); }); - } - - 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' ]; - - var numRegexs = numTags.map(function (nt) { return ({ name: nt, regex: new RegExp(("^" + nt + "-?\\d")) }); }); - - function parseTag(text) { - 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; - } + .split(/\s*,\s*/) + .map(Number); + } else if (/^i?clip\([^)]+/.test(text)) { + const p = text + .match(/^i?clip\((.*?)\)?$/)[1] + .trim() + .split(/\s*,\s*/); + tag.clip = { + inverse: /iclip/.test(text), + scale: 1, + drawing: null, + dots: null, + }; + if (p.length === 1) { + tag.clip.drawing = parseDrawing(p[0]); } - if (/^fn/.test(text)) { - tag.fn = text.slice(2); - } else if (/^r/.test(text)) { - tag.r = text.slice(1); - } else if (/^fs[\d+-]/.test(text)) { - tag.fs = text.slice(2); - } else if (/^\d?c&?H?[0-9a-f]+|^\d?c$/i.test(text)) { - 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-f]+/i.test(text)) { - var ref$2 = text.match(/^(\d)a&?H?(\w\w)/); - var num$1 = ref$2[1]; - var alpha = ref$2[2]; - tag[("a" + num$1)] = alpha; - } else if (/^alpha&?H?[0-9a-f]+/i.test(text)) { - (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)) { - 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)) { - var p = text - .match(/^i?clip\((.*?)\)?$/)[1] - .trim() - .split(/\s*,\s*/); - tag.clip = { - inverse: /iclip/.test(text), - scale: 1, - drawing: null, - dots: null, - }; - if (p.length === 1) { - tag.clip.drawing = parseDrawing(p[0]); - } - if (p.length === 2) { - tag.clip.scale = p[0] * 1; - tag.clip.drawing = parseDrawing(p[1]); - } - if (p.length === 4) { - tag.clip.dots = p.map(Number); - } - } else if (/^t\(/.test(text)) { - var p$1 = text - .match(/^t\((.*?)\)?$/)[1] - .trim() - .replace(/\\.*/, function (x) { return x.replace(/,/g, '\n'); }) - .split(/\s*,\s*/); - if (!p$1[0]) { return tag; } - tag.t = { - t1: 0, - t2: 0, - accel: 1, - tags: p$1[p$1.length - 1] - .replace(/\n/g, ',') - .split('\\') - .slice(1) - .map(parseTag), - }; - if (p$1.length === 2) { - tag.t.accel = p$1[0] * 1; - } - if (p$1.length === 3) { - tag.t.t1 = p$1[0] * 1; - tag.t.t2 = p$1[1] * 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; - } + if (p.length === 2) { + tag.clip.scale = p[0] * 1; + tag.clip.drawing = parseDrawing(p[1]); } - - return tag; - } - - function parseTags(text) { - var tags = []; - var depth = 0; - var str = ''; - for (var i = 0; i < text.length; i++) { - var x = text[i]; - if (x === '(') { depth++; } - if (x === ')') { depth--; } - if (depth < 0) { depth = 0; } - if (!depth && x === '\\') { - if (str) { - tags.push(str); - } - str = ''; - } else { - str += x; - } + if (p.length === 4) { + tag.clip.dots = p.map(Number); } - tags.push(str); - return tags.map(parseTag); - } - - function parseText(text) { - var pairs = text.split(/{([^{}]*?)}/); - var parsed = []; - if (pairs[0].length) { - parsed.push({ tags: [], text: pairs[0], drawing: [] }); + } else if (/^t\(/.test(text)) { + const p = text + .match(/^t\((.*?)\)?$/)[1] + .trim() + .replace(/\\.*/, (x) => x.replace(/,/g, '\n')) + .split(/\s*,\s*/); + if (!p[0]) return tag; + tag.t = { + t1: 0, + t2: 0, + accel: 1, + tags: p[p.length - 1] + .replace(/\n/g, ',') + .split('\\') + .slice(1) + .map(parseTag), + }; + if (p.length === 2) { + tag.t.accel = p[0] * 1; } - 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, - text: isDrawing ? '' : pairs[i + 1], - drawing: isDrawing ? parseDrawing(pairs[i + 1]) : [], - }); + if (p.length === 3) { + tag.t.t1 = p[0] * 1; + tag.t.t2 = p[1] * 1; } - return { - raw: text, - combined: parsed.map(function (frag) { return frag.text; }).join(''), - parsed: parsed, - }; - } - - function parseTime(time) { - var t = time.split(':'); - return t[0] * 3600 + t[1] * 60 + t[2] * 1; - } - - function parseDialogue(text, format) { - var fields = text.split(','); - if (fields.length > format.length) { - var textField = fields.slice(format.length - 1).join(); - fields = fields.slice(0, format.length - 1); - fields.push(textField); + if (p.length === 4) { + tag.t.t1 = p[0] * 1; + tag.t.t2 = p[1] * 1; + tag.t.accel = p[2] * 1; } + } - 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': - case 'MarginR': - case 'MarginV': - dia[fmt] = fld * 1; - break; - case 'Start': - case 'End': - dia[fmt] = parseTime(fld); - break; - case 'Effect': - dia[fmt] = parseEffect(fld); - break; - case 'Text': - dia[fmt] = parseText(fld); - break; - default: - dia[fmt] = fld; + return tag; +} + +function parseTags(text) { + const tags = []; + let depth = 0; + let 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; + if (!depth && x === '\\') { + if (str) { + tags.push(str); } + str = ''; + } else { + str += x; } - - return dia; } - - function parseFormat(text) { - return text.match(/Format\s*:\s*(.*)/i)[1].split(/\s*,\s*/); + return tags.map(parseTag); +} + +function parseText(text) { + const pairs = text.split(/{([^{}]*?)}/); + const parsed = []; + if (pairs[0].length) { + parsed.push({ tags: [], text: pairs[0], drawing: [] }); } - - function parseStyle(text) { - return text.match(/Style\s*:\s*(.*)/i)[1].split(/\s*,\s*/); + 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); + parsed.push({ + tags, + text: isDrawing ? '' : pairs[i + 1], + drawing: isDrawing ? parseDrawing(pairs[i + 1]) : [], + }); + } + return { + raw: text, + combined: parsed.map((frag) => frag.text).join(''), + parsed, + }; +} + +function parseTime(time) { + const t = time.split(':'); + return t[0] * 3600 + t[1] * 60 + t[2] * 1; +} + +function parseDialogue(text, format) { + let fields = text.split(','); + if (fields.length > format.length) { + const textField = fields.slice(format.length - 1).join(); + fields = fields.slice(0, format.length - 1); + fields.push(textField); } - function parse(text) { - var tree = { - info: {}, - styles: { format: [], style: [] }, - events: { format: [], comment: [], dialogue: [] }, - }; - 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)) { - var ref = line.match(/(.*?)\s*:\s*(.*)/); - var key = ref[1]; - var value = ref[2]; - tree.info[key] = value; - } - } - if (state === 2) { - if (/^Format\s*:/i.test(line)) { - tree.styles.format = parseFormat(line); - } - if (/^Style\s*:/i.test(line)) { - tree.styles.style.push(parseStyle(line)); - } - } - if (state === 3) { - if (/^Format\s*:/i.test(line)) { - tree.events.format = parseFormat(line); - } - if (/^(?:Comment|Dialogue)\s*:/i.test(line)) { - 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)); - } - } + const dia = {}; + for (let i = 0; i < fields.length; i++) { + const fmt = format[i]; + const fld = fields[i].trim(); + switch (fmt) { + case 'Layer': + case 'MarginL': + case 'MarginR': + case 'MarginV': + dia[fmt] = fld * 1; + break; + case 'Start': + case 'End': + dia[fmt] = parseTime(fld); + break; + case 'Effect': + dia[fmt] = parseEffect(fld); + break; + case 'Text': + dia[fmt] = parseText(fld); + break; + default: + dia[fmt] = fld; } - - return tree; } - var assign = Object.assign || ( - /* istanbul ignore next */ - function assign(target) { - var sources = [], len = arguments.length - 1; - while ( len-- > 0 ) sources[ len ] = arguments[ len + 1 ]; - - for (var i = 0; i < sources.length; i++) { - if (!sources[i]) { continue; } - var keys = Object.keys(sources[i]); - for (var j = 0; j < keys.length; j++) { - // eslint-disable-next-line no-param-reassign - target[keys[j]] = sources[i][keys[j]]; - } + return dia; +} + +const assign = Object.assign || ( + /* istanbul ignore next */ + function assign(target, ...sources) { + for (let i = 0; i < sources.length; i++) { + if (!sources[i]) continue; + const keys = Object.keys(sources[i]); + for (let j = 0; j < keys.length; j++) { + // eslint-disable-next-line no-param-reassign + target[keys[j]] = sources[i][keys[j]]; } - return target; - } - ); - - function createCommand(arr) { - var cmd = { - type: null, - prev: null, - next: null, - points: [], - }; - if (/[mnlbs]/.test(arr[0])) { - cmd.type = arr[0] - .toUpperCase() - .replace('N', 'L') - .replace('B', 'C'); - } - 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; + return target; } - - function isValid(cmd) { - if (!cmd.points.length || !cmd.type) { - return false; +); + +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']; + +function parseFormat(text) { + const 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()); + return caseField || field; + }); +} + +function parseStyle(text, format) { + const values = text.match(/Style\s*:\s*(.*)/i)[1].split(/\s*,\s*/); + return assign({}, ...format.map((fmt, idx) => ({ [fmt]: values[idx] }))); +} + +function parse(text) { + const 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; + if (state === 1) { + if (/:/.test(line)) { + const [, key, value] = line.match(/(.*?)\s*:\s*(.*)/); + tree.info[key] = value; + } } - if (/C|S/.test(cmd.type) && cmd.points.length < 3) { - return false; + if (state === 2) { + if (/^Format\s*:/i.test(line)) { + tree.styles.format = parseFormat(line); + } + if (/^Style\s*:/i.test(line)) { + tree.styles.style.push(parseStyle(line, tree.styles.format)); + } + } + if (state === 3) { + if (/^Format\s*:/i.test(line)) { + 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)); + } } - return true; } - function getViewBox(commands) { - 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 tree; +} - 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: minX, - minY: minY, - width: maxX - minX, - height: maxY - minY, - }; +function createCommand(arr) { + const cmd = { + type: null, + prev: null, + next: null, + points: [], + }; + if (/[mnlbs]/.test(arr[0])) { + cmd.type = arr[0] + .toUpperCase() + .replace('N', 'L') + .replace('B', 'C'); + } + for (let 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; +} - /** - * Convert S command to B command - * Reference from https://github.com/d3/d3/blob/v3.5.17/src/svg/line.js#L259 - * @param {Array} points points - * @param {String} prev type of previous command - * @param {String} next type of next command - * @return {Array} converted commands - */ - function s2b(points, prev, next) { - 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]; +function isValid(cmd) { + if (!cmd.points.length || !cmd.type) { + return false; + } + if (/C|S/.test(cmd.type) && cmd.points.length < 3) { + return false; + } + return true; +} + +function getViewBox(commands) { + let minX = Infinity; + let minY = Infinity; + let maxX = -Infinity; + let maxY = -Infinity; + [].concat(...commands.map(({ points }) => points)).forEach(({ x, y }) => { + minX = Math.min(minX, x); + minY = Math.min(minY, y); + maxX = Math.max(maxX, x); + maxY = Math.max(maxY, y); + }); + return { + minX, + minY, + width: maxX - minX, + height: maxY - minY, + }; +} + +/** + * Convert S command to B command + * Reference from https://github.com/d3/d3/blob/v3.5.17/src/svg/line.js#L259 + * @param {Array} points points + * @param {String} prev type of previous command + * @param {String} next type of next command + * @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]; + results.push({ + type: prev === 'M' ? 'M' : 'L', + points: [{ x: dot4(bb3, px), y: dot4(bb3, py) }], + }); + for (let 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({ - type: prev === 'M' ? 'M' : 'L', - points: [{ x: dot4(bb3, px), y: dot4(bb3, py) }], + type: 'C', + 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) }, + ], }); - 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({ - type: 'C', - 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) } ], - }); - } - if (next === 'L' || next === 'C') { - 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(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) { - var ref$1; - - var commands = []; - var i = 0; - while (i < rawCommands.length) { - var arr = rawCommands[i]; - var cmd = createCommand(arr); - if (isValid(cmd)) { - if (cmd.type === 'S') { - 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; - commands[i - 1].next = cmd.type; - } - commands.push(cmd); - i++; - } else { - if (i && commands[i - 1].type === 'S') { - 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(function (ref) { - var x = ref.x; - var y = ref.y; - - return ({ x: x, y: y }); - }) - ); - } - rawCommands.splice(i, 1); + if (next === 'L' || next === 'C') { + const 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(''); +} + +function compileDrawing(rawCommands) { + const commands = []; + let i = 0; + while (i < rawCommands.length) { + const arr = rawCommands[i]; + const 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 }); + } + if (i) { + cmd.prev = commands[i - 1].type; + commands[i - 1].next = cmd.type; + } + commands.push(cmd); + i++; + } else { + if (i && commands[i - 1].type === 'S') { + const 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 })), + ); } + rawCommands.splice(i, 1); } - 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: type, points: points } - ); - }) - ); - - return assign({ instructions: instructions, d: toSVGPath(instructions) }, getViewBox(commands)); } + const instructions = [].concat( + ...commands.map(({ type, points, prev, next }) => ( + type === 'S' + ? s2b(points, prev, next) + : { type, points } + )), + ); - var tTags = [ - 'fs', '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' ]; + return assign({ instructions, d: toSVGPath(instructions) }, getViewBox(commands)); +} - function compileTag(tag, key, presets) { - var obj, obj$1, obj$2; +const tTags = [ + 'fs', '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', +]; - if ( presets === void 0 ) presets = {}; - var value = tag[key]; - if (value === undefined) { - return null; - } - if (key === 'pos' || key === 'org') { - return value.length === 2 ? ( obj = {}, obj[key] = { x: value[0], y: value[1] }, obj ) : null; - } - if (key === 'move') { - 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: x1, y1: y1, x2: x2, y2: y2, t1: t1, t2: t2 } } - : null; - } - if (key === 'fad' || key === 'fade') { - if (value.length === 2) { - var t1$1 = value[0]; - var t2$1 = value[1]; - return { fade: { type: 'fad', t1: t1$1, t2: t2$1 } }; - } - if (value.length === 7) { - 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') { - var inverse = value.inverse; - var scale = value.scale; - var drawing = value.drawing; - var dots = value.dots; - if (drawing) { - return { clip: { inverse: inverse, scale: scale, drawing: compileDrawing(drawing), dots: dots } }; - } - if (dots) { - 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; - } - if (/^[xy]?(bord|shad)$/.test(key)) { - value = Math.max(value, 0); - } - if (key === 'bord') { - return { xbord: value, ybord: value }; - } - if (key === 'shad') { - return { xshad: value, yshad: value }; - } - if (/^c\d$/.test(key)) { - return ( obj$1 = {}, obj$1[key] = value || presets[key], obj$1 ); - } - if (key === 'alpha') { - return { a1: value, a2: value, a3: value, a4: value }; +function compileTag(tag, key, presets = {}) { + let 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; + } + if (key === 'move') { + const [x1, y1, x2, y2, t1 = 0, t2 = 0] = value; + return value.length === 4 || value.length === 6 + ? { move: { x1, y1, x2, y2, t1, t2 } } + : null; + } + if (key === 'fad' || key === 'fade') { + if (value.length === 2) { + const [t1, t2] = value; + return { fade: { type: 'fad', t1, t2 } }; } - if (key === 'fr') { - return { frz: value }; + if (value.length === 7) { + const [a1, a2, a3, t1, t2, t3, t4] = value; + return { fade: { type: 'fade', a1, a2, a3, t1, t2, t3, t4 } }; } - if (key === 'fs') { - return { - fs: /^\+|-/.test(value) - ? (value * 1 > -10 ? (1 + value / 10) : 1) * presets.fs - : value * 1, - }; + return null; + } + if (key === 'clip') { + const { inverse, scale, drawing, dots } = value; + if (drawing) { + return { clip: { inverse, scale, drawing: compileDrawing(drawing), dots } }; } - if (key === 't') { - 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)) { - assign(compiledTag, compileTag(t, k, presets)); - } - }); - return { t: { t1: t1$3, t2: t2$3, accel: accel, tag: compiledTag } }; + if (dots) { + const [x1, y1, x2, y2] = dots; + return { clip: { inverse, scale, drawing, dots: { x1, y1, x2, y2 } } }; } - return ( obj$2 = {}, obj$2[key] = value, obj$2 ); + return null; } - - var a2an = [ - null, 1, 2, 3, - null, 7, 8, 9, - null, 4, 5, 6 ]; - - var globalTags = ['r', 'a', 'an', 'pos', 'org', 'move', 'fade', 'fad', 'clip']; - - function createSlice(name, styles) { + if (/^[xy]?(bord|shad)$/.test(key)) { + value = Math.max(value, 0); + } + if (key === 'bord') { + return { xbord: value, ybord: value }; + } + if (key === 'shad') { + return { xshad: value, yshad: value }; + } + if (/^c\d$/.test(key)) { + return { [key]: value || presets[key] }; + } + if (key === 'alpha') { + return { a1: value, a2: value, a3: value, a4: value }; + } + if (key === 'fr') { + return { frz: value }; + } + if (key === 'fs') { return { - name: name, - borderStyle: styles[name].style.BorderStyle, - tag: styles[name].tag, - fragments: [], + fs: /^\+|-/.test(value) + ? (value * 1 > -10 ? (1 + value / 10) : 1) * presets.fs + : value * 1, }; } - - function compileText(ref) { - var styles = ref.styles; - var name = ref.name; - var parsed = ref.parsed; - var start = ref.start; - var end = ref.end; - - var alignment; - var pos; - var org; - var move; - var fade; - var clip; - var slices = []; - var slice = createSlice(name, styles); - 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; - } - var fragment = { - tag: reset === undefined ? JSON.parse(JSON.stringify(prevTag)) : {}, - text: text, - drawing: drawing.length ? compileDrawing(drawing) : null, - }; - 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; - 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)) { - var ref$2 = slice.tag; - var c1 = ref$2.c1; - var c2 = ref$2.c2; - var c3 = ref$2.c3; - var c4 = ref$2.c4; - var fs = prevTag.fs || slice.tag.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); - } else { - assign(fragment.tag, compiledTag); - } - } - } - prevTag = fragment.tag; - if (reset !== undefined) { - slices.push(slice); - slice = createSlice(styles[reset] ? reset : name, styles); + if (key === 'K') { + 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]; + if (~tTags.indexOf(k) && !(k === 'clip' && !t[k].dots)) { + assign(compiledTag, compileTag(t, k, presets)); } - if (fragment.text || fragment.drawing) { - 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; + }); + return { t: { t1, t2, accel, tag: compiledTag } }; + } + return { [key]: value }; +} + +const a2an = [ + null, 1, 2, 3, + null, 7, 8, 9, + null, 4, 5, 6, +]; + +const globalTags = ['r', 'a', 'an', 'pos', 'org', 'move', 'fade', 'fad', 'clip']; + +function inheritTag(pTag) { + return JSON.parse(JSON.stringify(assign({}, pTag, { + k: undefined, + kf: undefined, + ko: undefined, + kt: undefined, + }))); +} + +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]; + reset = tag.r === undefined ? reset : tag.r; + } + const fragment = { + tag: reset === undefined ? inheritTag(prevTag) : {}, + 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]; + 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 }); + if (key === 't') { + fragment.tag.t = fragment.tag.t || []; + fragment.tag.t.push(compiledTag.t); } else { - slice.fragments.push(fragment); + assign(fragment.tag, compiledTag); } } } - slices.push(slice); - - return assign({ alignment: alignment, slices: slices }, pos, org, move, fade, clip); - } - - 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'; - } - var stl = styles[dia.Style].style; - var compiledText = compileText({ - styles: styles, - name: dia.Style, - parsed: dia.Text.parsed, - start: dia.Start, - end: dia.End, - }); - var alignment = compiledText.alignment || stl.Alignment; - minLayer = Math.min(minLayer, dia.Layer); - results.push(assign({ - layer: dia.Layer, - start: dia.Start, - end: dia.End, - // reset style by `\r` will not effect margin and alignment - margin: { - left: dia.MarginL || stl.MarginL, - right: dia.MarginR || stl.MarginR, - vertical: dia.MarginV || stl.MarginV, - }, - effect: dia.Effect, - }, compiledText, { alignment: alignment })); - } - for (var i$1 = 0; i$1 < results.length; i$1++) { - results[i$1].layer -= minLayer; - } - 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 - var DEFAULT_STYLE = { - 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', - }; - - /** - * @param {String} color - * @returns {Array} [AA, BBGGRR] - */ - function parseStyleColor(color) { - if (/^(&|H|&H)[0-9a-f]{6,}/i.test(color)) { - 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]; + prevTag = fragment.tag; + if (reset !== undefined) { + slices.push(slice); + slice = { style: styles[reset] ? reset : style, fragments: [] }; } - var num = parseInt(color, 10); - if (!Number.isNaN(num)) { - var min = -2147483648; - var max = 2147483647; - if (num < min) { - return ['00', '000000']; + if (fragment.text || fragment.drawing) { + const 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; + } else { + slice.fragments.push(fragment); } - 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(ref) { - var info = ref.info; - var style = ref.style; - var format = ref.format; - var defaultStyle = ref.defaultStyle; - - var result = {}; - var styles = [ - assign({}, DEFAULT_STYLE, defaultStyle, { Name: 'Default' }) ].concat( style.map(function (stl) { - var s = {}; - for (var i = 0; i < format.length; i++) { - s[format[i]] = stl[i]; - } - return s; - }) ); - var loop = function ( i ) { - var s = styles[i]; - // this behavior is same as Aegisub by black-box testing - if (/^(\*+)Default$/.test(s.Name)) { - s.Name = 'Default'; - } - Object.keys(s).forEach(function (key) { - if (key !== 'Name' && key !== 'Fontname' && !/Colour/.test(key)) { - s[key] *= 1; - } - }); - 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: 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), - 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, - q: /^[0-3]$/.test(info.WrapStyle) ? info.WrapStyle * 1 : 2, - }; - result[s.Name] = { style: s, tag: tag }; - }; - - for (var i = 0; i < styles.length; i++) loop( i ); - return result; + slices.push(slice); + + return assign({ alignment, slices }, 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]; + if (dia.Start >= dia.End) { + continue; + } + if (!styles[dia.Style]) { + dia.Style = 'Default'; + } + const stl = styles[dia.Style].style; + const compiledText = compileText({ + styles, + style: dia.Style, + parsed: dia.Text.parsed, + start: dia.Start, + end: dia.End, + }); + const alignment = compiledText.alignment || stl.Alignment; + minLayer = Math.min(minLayer, dia.Layer); + results.push(assign({ + layer: dia.Layer, + start: dia.Start, + end: dia.End, + style: dia.Style, + name: dia.Name, + // reset style by `\r` will not effect margin and alignment + margin: { + left: dia.MarginL || stl.MarginL, + right: dia.MarginR || stl.MarginR, + vertical: dia.MarginV || stl.MarginV, + }, + effect: dia.Effect, + }, compiledText, { alignment })); } - - function compile(text, options) { - if ( options === void 0 ) options = {}; - - var tree = parse(text); - var styles = compileStyles({ - info: tree.info, - style: tree.styles.style, - format: tree.styles.format, - defaultStyle: options.defaultStyle || {}, + for (let i = 0; i < results.length; i++) { + results[i].layer -= minLayer; + } + return results.sort((a, b) => 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 = { + 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', +}; + +/** + * @param {String} color + * @returns {Array} [AA, BBGGRR] + */ +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); + return [a || '00', c]; + } + const num = parseInt(color, 10); + if (!Number.isNaN(num)) { + const min = -2147483648; + const max = 2147483647; + if (num < min) { + return ['00', '000000']; + } + const 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 = [assign({}, defaultStyle, { Name: 'Default' })].concat(style); + for (let i = 0; i < styles.length; i++) { + const s = 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) => { + if (key !== 'Name' && key !== 'Fontname' && !/Colour/.test(key)) { + s[key] *= 1; + } }); - return { - info: tree.info, - width: tree.info.PlayResX * 1 || null, - height: tree.info.PlayResY * 1 || null, - collisions: tree.info.Collisions || 'Normal', - styles: styles, - dialogues: compileDialogues({ - styles: styles, - dialogues: tree.events.dialogue, - }), + 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 = { + fn: s.Fontname, + fs: s.Fontsize, + c1, + a1, + c2, + a2, + c3, + a3, + c4, + a4, + 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(info.WrapStyle) ? info.WrapStyle * 1 : 2, }; + result[s.Name] = { style: s, tag }; } + return result; +} + +function compile(text, options = {}) { + const tree = parse(text); + const styles = compileStyles({ + info: tree.info, + style: tree.styles.style, + defaultStyle: options.defaultStyle || {}, + }); + return { + info: tree.info, + width: tree.info.PlayResX * 1 || null, + height: tree.info.PlayResY * 1 || null, + collisions: tree.info.Collisions || 'Normal', + styles, + dialogues: compileDialogues({ + styles, + dialogues: tree.events.dialogue, + }), + }; +} - var raf = ( - window.requestAnimationFrame - || window.mozRequestAnimationFrame - || window.webkitRequestAnimationFrame - || (function (cb) { return setTimeout(cb, 50 / 3); }) - ); +const $fixFontSize = document.createElement('div'); +$fixFontSize.className = 'ASS-fix-font-size'; +$fixFontSize.textContent = 'M'; - var caf = ( - window.cancelAnimationFrame - || window.mozCancelAnimationFrame - || window.webkitCancelAnimationFrame - || clearTimeout - ); +const cache = Object.create(null); - function color2rgba(c) { - var t = c.match(/(\w\w)(\w\w)(\w\w)(\w\w)/); - var a = 1 - ("0x" + (t[1])) / 255; - var b = +("0x" + (t[2])); - var g = +("0x" + (t[3])); - var r = +("0x" + (t[4])); - return ("rgba(" + r + "," + g + "," + b + "," + a + ")"); +function getRealFontSize(fn, fs) { + const key = `${fn}-${fs}`; + if (!cache[key]) { + $fixFontSize.style.cssText = `line-height:normal;font-size:${fs}px;font-family:"${fn}",Arial;`; + cache[key] = fs * fs / $fixFontSize.clientHeight; } - - function uuid() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { - var r = Math.random() * 16 | 0; - var v = c === 'x' ? r : (r & 0x3 | 0x8); - return v.toString(16); - }); + return cache[key]; +} + +function alpha2opacity(a) { + return 1 - `0x${a}` / 255; +} + +function color2rgba(c) { + const t = c.match(/(\w\w)(\w\w)(\w\w)(\w\w)/); + const a = alpha2opacity(t[1]); + const b = +`0x${t[2]}`; + const g = +`0x${t[3]}`; + const r = +`0x${t[4]}`; + return `rgba(${r},${g},${b},${a})`; +} + +function uuid() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = Math.trunc(Math.random() * 16); + const v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} + +/** + * @param {string} name SVG tag + * @param {[string, string][]} attrs + * @returns + */ +function createSVGEl(name, attrs = []) { + const $el = document.createElementNS('http://www.w3.org/2000/svg', name); + for (let i = 0; i < attrs.length; i += 1) { + const attr = attrs[i]; + $el.setAttributeNS( + attr[0] === 'xlink:href' ? 'http://www.w3.org/1999/xlink' : null, + attr[0], + attr[1], + ); } - - function createSVGEl(name, attrs) { - if ( attrs === void 0 ) attrs = []; - - var $el = document.createElementNS('http://www.w3.org/2000/svg', name); - for (var i = 0; i < attrs.length; i++) { - var attr = attrs[i]; - $el.setAttributeNS( - attr[0] === 'xlink:href' ? 'http://www.w3.org/1999/xlink' : null, - attr[0], - attr[1] - ); - } - return $el; + return $el; +} + +function getVendor(prop) { + const { style } = document.body; + const Prop = prop.replace(/^\w/, (x) => x.toUpperCase()); + if (prop in style) return ''; + if (`webkit${Prop}` in style) return '-webkit-'; + if (`moz${Prop}` in style) return '-moz-'; + return ''; +} + +const vendor = { + clipPath: getVendor('clipPath'), +}; + +const GLOBAL_CSS = '.ASS-box{overflow:hidden;pointer-events:none;position:absolute}.ASS-dialogue{font-size:0;position:absolute;z-index:0}.ASS-dialogue [data-stroke]{position:relative}.ASS-dialogue [data-stroke]::after,.ASS-dialogue [data-stroke]::before{content:attr(data-stroke);position:absolute;top:0;left:0;z-index:-1;filter:var(--ass-blur)}.ASS-dialogue [data-stroke]::before{color:var(--ass-shadow-color);transform:translate(var(--ass-shadow-offset));-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-stroke]::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-fix-font-size{position:absolute;visibility:hidden}.ASS-fix-objectBoundingBox{width:100%;height:100%;position:absolute;top:0;left:0}'; +/** + * @param {HTMLElement} container + */ +function addGlobalStyle(container) { + const rootNode = container.getRootNode() || document; + const styleRoot = rootNode === document ? document.head : rootNode; + let $style = styleRoot.querySelector('#ASS-global-style'); + if (!$style) { + $style = document.createElement('style'); + $style.type = 'text/css'; + $style.id = 'ASS-global-style'; + $style.append(document.createTextNode(GLOBAL_CSS)); + styleRoot.append($style); } - - function getVendor(prop) { - var ref = document.body; - var style = ref.style; - var Prop = prop.replace(/^\w/, function (x) { return x.toUpperCase(); }); - if (prop in style) { return ''; } - if (("webkit" + Prop) in style) { return '-webkit-'; } - if (("moz" + Prop) in style) { return '-moz-'; } - return ''; +} +const transformTags = ['fscx', 'fscy', 'frx', 'fry', 'frz', 'fax', 'fay']; + +function initAnimation($el, keyframes, options) { + const animation = $el.animate(keyframes, options); + animation.pause(); + return animation; +} + +function batchAnimate($el, action) { + ($el.animations || []).forEach((animation) => { + animation[action](); + }); +} + +function createClipPath(clip, store) { + const sw = store.scriptRes.width; + const sh = store.scriptRes.height; + 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`; } - - var vendor = { - transform: getVendor('transform'), - animation: getVendor('animation'), - clipPath: getVendor('clipPath'), - }; - - function getStyleRoot(container) { - var rootNode = container.getRootNode ? container.getRootNode() : document; - return rootNode === document ? rootNode.head : rootNode; - } - - var strokeTags = ['c3', 'a3', 'c4', 'a4', 'xbord', 'ybord', 'xshad', 'yshad', 'blur', 'be']; - var transformTags = ['fscx', 'fscy', 'frx', 'fry', 'frz', 'fax', 'fay']; - - function createClipPath(clip) { - var sw = this._.scriptRes.width; - var sh = this._.scriptRes.height; - var d = ''; - if (clip.dots !== null) { - var ref = clip.dots; - var x1 = ref.x1; - var y1 = ref.y1; - var x2 = ref.x2; - var y2 = ref.y2; - 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(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 / sw) + "," + (y / sh)); - }).join(',') - ); - }).join(''); - } - var scale = 1 / (1 << (clip.scale - 1)); - if (clip.inverse) { - d += "M0,0L0," + scale + "," + scale + "," + scale + "," + scale + ",0,0,0Z"; - } - var id = "ASS-" + (uuid()); - var $clipPath = createSVGEl('clipPath', [ - ['id', id], - ['clipPathUnits', 'objectBoundingBox'] ]); - $clipPath.appendChild(createSVGEl('path', [ - ['d', d], - ['transform', ("scale(" + scale + ")")], - ['clip-rule', 'evenodd'] ])); - this._.$defs.appendChild($clipPath); - return { - $clipPath: $clipPath, - cssText: ((vendor.clipPath) + "clip-path:url(#" + id + ");"), - }; + if (clip.drawing !== null) { + d = clip.drawing.instructions.map(({ type, points }) => ( + type + points.map(({ x, y }) => `${x / sw},${y / sh}`).join(',') + )).join(''); } - - function setClipPath(dialogue) { - if (!dialogue.clip) { - return; - } - var $fobb = document.createElement('div'); - this._.$stage.insertBefore($fobb, dialogue.$div); - $fobb.appendChild(dialogue.$div); - $fobb.className = 'ASS-fix-objectBoundingBox'; - var ref = createClipPath.call(this, dialogue.clip); - var cssText = ref.cssText; - var $clipPath = ref.$clipPath; - this._.$defs.appendChild($clipPath); - $fobb.style.cssText = cssText; - assign(dialogue, { $div: $fobb, $clipPath: $clipPath }); - } - - var $fixFontSize = document.createElement('div'); - $fixFontSize.className = 'ASS-fix-font-size'; - $fixFontSize.textContent = 'M'; - - var cache = Object.create(null); - - function getRealFontSize(fn, fs) { - var key = fn + "-" + fs; - if (!cache[key]) { - $fixFontSize.style.cssText = "line-height:normal;font-size:" + fs + "px;font-family:\"" + fn + "\",Arial;"; - cache[key] = fs * fs / $fixFontSize.clientHeight; - } - return cache[key]; + const scale = 1 / (1 << (clip.scale - 1)); + if (clip.inverse) { + d += `M0,0L0,${scale},${scale},${scale},${scale},0,0,0Z`; } - - function createSVGStroke(tag, id, scale) { - var hasBorder = tag.xbord || tag.ybord; - var hasShadow = tag.xshad || tag.yshad; - var isOpaque = tag.a1 !== 'FF'; - var blur = tag.blur || tag.be || 0; - var $filter = createSVGEl('filter', [['id', id]]); - $filter.appendChild(createSVGEl('feGaussianBlur', [ - ['stdDeviation', hasBorder ? 0 : blur], + const id = `ASS-${uuid()}`; + const $clipPath = createSVGEl('clipPath', [ + ['id', id], + ['clipPathUnits', 'objectBoundingBox'], + ]); + $clipPath.append(createSVGEl('path', [ + ['d', d], + ['transform', `scale(${scale})`], + ['clip-rule', 'evenodd'], + ])); + store.defs.append($clipPath); + return { + $clipPath, + cssText: `${vendor.clipPath}clip-path:url(#${id});`, + }; +} + +function getClipPath(dialogue, store) { + if (!dialogue.clip) return {}; + const $fobb = document.createElement('div'); + store.box.insertBefore($fobb, dialogue.$div); + $fobb.append(dialogue.$div); + $fobb.className = 'ASS-fix-objectBoundingBox'; + const { cssText, $clipPath } = createClipPath(dialogue.clip, store); + store.defs.append($clipPath); + $fobb.style.cssText = cssText; + return { $div: $fobb, $clipPath }; +} + +function createSVGStroke(tag, id, scale) { + const hasBorder = tag.xbord || tag.ybord; + const hasShadow = tag.xshad || tag.yshad; + const isOpaque = tag.a1 !== 'FF'; + const blur = tag.blur || tag.be || 0; + const $filter = createSVGEl('filter', [['id', id]]); + $filter.append(createSVGEl('feGaussianBlur', [ + ['stdDeviation', hasBorder ? 0 : blur], + ['in', 'SourceGraphic'], + ['result', 'sg_b'], + ])); + $filter.append(createSVGEl('feFlood', [ + ['flood-color', color2rgba(tag.a1 + tag.c1)], + ['result', 'c1'], + ])); + $filter.append(createSVGEl('feComposite', [ + ['operator', 'in'], + ['in', 'c1'], + ['in2', 'sg_b'], + ['result', 'main'], + ])); + if (hasBorder) { + $filter.append(createSVGEl('feMorphology', [ + ['radius', `${tag.xbord * scale} ${tag.ybord * scale}`], + ['operator', 'dilate'], ['in', 'SourceGraphic'], - ['result', 'sg_b'] ])); - $filter.appendChild(createSVGEl('feFlood', [ - ['flood-color', color2rgba(tag.a1 + tag.c1)], - ['result', 'c1'] ])); - $filter.appendChild(createSVGEl('feComposite', [ + ['result', 'dil'], + ])); + $filter.append(createSVGEl('feGaussianBlur', [ + ['stdDeviation', blur], + ['in', 'dil'], + ['result', 'dil_b'], + ])); + $filter.append(createSVGEl('feComposite', [ + ['operator', 'out'], + ['in', 'dil_b'], + ['in2', 'SourceGraphic'], + ['result', 'dil_b_o'], + ])); + $filter.append(createSVGEl('feFlood', [ + ['flood-color', color2rgba(tag.a3 + tag.c3)], + ['result', 'c3'], + ])); + $filter.append(createSVGEl('feComposite', [ ['operator', 'in'], - ['in', 'c1'], - ['in2', 'sg_b'], - ['result', 'main'] ])); - if (hasBorder) { - $filter.appendChild(createSVGEl('feMorphology', [ - ['radius', ((tag.xbord * scale) + " " + (tag.ybord * scale))], - ['operator', 'dilate'], - ['in', 'SourceGraphic'], - ['result', 'dil'] ])); - $filter.appendChild(createSVGEl('feGaussianBlur', [ - ['stdDeviation', blur], - ['in', 'dil'], - ['result', 'dil_b'] ])); - $filter.appendChild(createSVGEl('feComposite', [ - ['operator', 'out'], - ['in', 'dil_b'], - ['in2', 'SourceGraphic'], - ['result', 'dil_b_o'] ])); - $filter.appendChild(createSVGEl('feFlood', [ - ['flood-color', color2rgba(tag.a3 + tag.c3)], - ['result', 'c3'] ])); - $filter.appendChild(createSVGEl('feComposite', [ - ['operator', 'in'], - ['in', 'c3'], - ['in2', 'dil_b_o'], - ['result', 'border'] ])); - } - if (hasShadow && (hasBorder || isOpaque)) { - $filter.appendChild(createSVGEl('feOffset', [ + ['in', 'c3'], + ['in2', 'dil_b_o'], + ['result', 'border'], + ])); + } + if (hasShadow && (hasBorder || isOpaque)) { + $filter.append(createSVGEl('feOffset', [ + ['dx', tag.xshad * scale], + ['dy', tag.yshad * scale], + ['in', hasBorder ? 'dil' : 'SourceGraphic'], + ['result', 'off'], + ])); + $filter.append(createSVGEl('feGaussianBlur', [ + ['stdDeviation', blur], + ['in', 'off'], + ['result', 'off_b'], + ])); + if (!isOpaque) { + $filter.append(createSVGEl('feOffset', [ ['dx', tag.xshad * scale], ['dy', tag.yshad * scale], - ['in', hasBorder ? 'dil' : 'SourceGraphic'], - ['result', 'off'] ])); - $filter.appendChild(createSVGEl('feGaussianBlur', [ - ['stdDeviation', blur], - ['in', 'off'], - ['result', 'off_b'] ])); - if (!isOpaque) { - $filter.appendChild(createSVGEl('feOffset', [ - ['dx', tag.xshad * scale], - ['dy', tag.yshad * scale], - ['in', 'SourceGraphic'], - ['result', 'sg_off'] ])); - $filter.appendChild(createSVGEl('feComposite', [ - ['operator', 'out'], - ['in', 'off_b'], - ['in2', 'sg_off'], - ['result', 'off_b_o'] ])); - } - $filter.appendChild(createSVGEl('feFlood', [ - ['flood-color', color2rgba(tag.a4 + tag.c4)], - ['result', 'c4'] ])); - $filter.appendChild(createSVGEl('feComposite', [ - ['operator', 'in'], - ['in', 'c4'], - ['in2', isOpaque ? 'off_b' : 'off_b_o'], - ['result', 'shadow'] ])); - } - var $merge = createSVGEl('feMerge', []); - if (hasShadow && (hasBorder || isOpaque)) { - $merge.appendChild(createSVGEl('feMergeNode', [['in', 'shadow']])); - } - if (hasBorder) { - $merge.appendChild(createSVGEl('feMergeNode', [['in', 'border']])); - } - $merge.appendChild(createSVGEl('feMergeNode', [['in', 'main']])); - $filter.appendChild($merge); - return $filter; - } - - function createCSSStroke(tag, scale) { - var arr = []; - var oc = color2rgba(tag.a3 + tag.c3); - var ox = tag.xbord * scale; - var oy = tag.ybord * scale; - var sc = color2rgba(tag.a4 + tag.c4); - var sx = tag.xshad * scale; - var sy = tag.yshad * scale; - var blur = tag.blur || tag.be || 0; - if (!(ox + oy + sx + sy)) { return 'none'; } - if (ox || oy) { - for (var i = -1; i <= 1; i++) { - for (var j = -1; j <= 1; j++) { - for (var x = 1; x < ox; x++) { - for (var y = 1; y < oy; y++) { - if (i || j) { - arr.push((oc + " " + (i * x) + "px " + (j * y) + "px " + blur + "px")); - } - } - } - arr.push((oc + " " + (i * ox) + "px " + (j * oy) + "px " + blur + "px")); - } - } - } - if (sx || sy) { - var pnx = sx > 0 ? 1 : -1; - var pny = sy > 0 ? 1 : -1; - sx = Math.abs(sx); - sy = Math.abs(sy); - for (var x$1 = Math.max(ox, sx - ox); x$1 < sx + ox; x$1++) { - for (var y$1 = Math.max(oy, sy - oy); y$1 < sy + oy; y$1++) { - arr.push((sc + " " + (x$1 * pnx) + "px " + (y$1 * pny) + "px " + blur + "px")); - } - } - arr.push((sc + " " + ((sx + ox) * pnx) + "px " + ((sy + oy) * pny) + "px " + blur + "px")); - } - return arr.join(); - } - - function createTransform(tag) { - return [ - // TODO: I don't know why perspective is 314, it just performances well. - 'perspective(314px)', - ("rotateY(" + (tag.fry || 0) + "deg)"), - ("rotateX(" + (tag.frx || 0) + "deg)"), - ("rotateZ(" + (-tag.frz || 0) + "deg)"), - ("scale(" + (tag.p ? 1 : (tag.fscx || 100) / 100) + "," + (tag.p ? 1 : (tag.fscy || 100) / 100) + ")"), - ("skew(" + (tag.fax || 0) + "rad," + (tag.fay || 0) + "rad)") ].join(' '); - } - - function setTransformOrigin(dialogue) { - var alignment = dialogue.alignment; - var width = dialogue.width; - var height = dialogue.height; - var x = dialogue.x; - var y = dialogue.y; - var $div = dialogue.$div; - var org = dialogue.org; - if (!org) { - org = { x: 0, y: 0 }; - if (alignment % 3 === 1) { org.x = x; } - if (alignment % 3 === 2) { org.x = x + width / 2; } - if (alignment % 3 === 0) { org.x = x + width; } - if (alignment <= 3) { org.y = y + height; } - if (alignment >= 4 && alignment <= 6) { org.y = y + height / 2; } - if (alignment >= 7) { org.y = y; } - } - for (var i = $div.childNodes.length - 1; i >= 0; i--) { - var node = $div.childNodes[i]; - if (node.dataset.hasRotate === 'true') { - // It's not extremely precise for offsets are round the value to an integer. - var tox = org.x - x - node.offsetLeft; - var toy = org.y - y - node.offsetTop; - node.style.cssText += (vendor.transform) + "transform-origin:" + tox + "px " + toy + "px;"; - } - } + ['in', 'SourceGraphic'], + ['result', 'sg_off'], + ])); + $filter.append(createSVGEl('feComposite', [ + ['operator', 'out'], + ['in', 'off_b'], + ['in2', 'sg_off'], + ['result', 'off_b_o'], + ])); + } + $filter.append(createSVGEl('feFlood', [ + ['flood-color', color2rgba(tag.a4 + tag.c4)], + ['result', 'c4'], + ])); + $filter.append(createSVGEl('feComposite', [ + ['operator', 'in'], + ['in', 'c4'], + ['in2', isOpaque ? 'off_b' : 'off_b_o'], + ['result', 'shadow'], + ])); } - - function getKeyframeString(name, list) { - return ("@" + (vendor.animation) + "keyframes " + name + " {" + list + "}\n"); + const $merge = createSVGEl('feMerge', []); + if (hasShadow && (hasBorder || isOpaque)) { + $merge.append(createSVGEl('feMergeNode', [['in', 'shadow']])); } - - var KeyframeBlockList = function KeyframeBlockList() { - this.obj = {}; - }; - - KeyframeBlockList.prototype.set = function set (keyText, prop, value) { - if (!this.obj[keyText]) { this.obj[keyText] = {}; } - this.obj[keyText][prop] = value; + if (hasBorder) { + $merge.append(createSVGEl('feMergeNode', [['in', 'border']])); + } + $merge.append(createSVGEl('feMergeNode', [['in', 'main']])); + $filter.append($merge); + return $filter; +} + +function get4QuadrantPoints([x, y]) { + return [[0, 0], [0, 1], [1, 0], [1, 1]] + .filter(([i, j]) => (i || x) && (j || y)) + .map(([i, j]) => [(i || -1) * x, (j || -1) * y]); +} + +function getOffsets(x, y) { + if (x === y) return []; + const nx = Math.min(x, y); + const ny = Math.max(x, y); + // const offsets = [[nx, ny]]; + // for (let i = 0; i < nx; i++) { + // for (let j = Math.round(nx + 0.5); j < ny; j++) { + // offsets.push([i, j]); + // } + // } + // return [].concat(...offsets.map(get4QuadrantPoints)); + return Array.from({ length: Math.ceil(ny) - 1 }, (_, i) => i + 1).concat(ny) + .map((n) => [(ny - n) / ny * nx, n]) + .map(([i, j]) => (x > y ? [j, i] : [i, j])) + .flatMap(get4QuadrantPoints); +} + +// TODO: a1 === 'ff' +function createCSSStroke(tag, scale) { + const bc = color2rgba(`00${tag.c3}`); + const bx = tag.xbord * scale; + const by = tag.ybord * scale; + const sc = color2rgba(`00${tag.c4}`); + const sx = tag.xshad * scale; + const sy = tag.yshad * scale; + const blur = tag.blur || tag.be || 0; + const deltaOffsets = getOffsets(bx, by); + return [ + { key: 'border-width', value: `${Math.min(bx, by) * 2}px` }, + { key: 'border-color', value: bc }, + { key: 'border-opacity', value: alpha2opacity(tag.a3) }, + { key: 'border-delta', value: deltaOffsets.map(([x, y]) => `${x}px ${y}px ${bc}`).join(',') }, + { key: 'shadow-offset', value: `${sx}px, ${sy}px` }, + { key: 'shadow-color', value: sc }, + { key: 'shadow-opacity', value: alpha2opacity(tag.a4) }, + { key: 'shadow-delta', value: deltaOffsets.map(([x, y]) => `${x}px ${y}px ${sc}`).join(',') }, + { key: 'blur', value: `blur(${blur}px)` }, + ].map((kv) => Object.assign(kv, { key: `--ass-${kv.key}` })); +} + +function createDrawing(fragment, styleTag, store) { + if (!fragment.drawing.d) return null; + const { scale, info } = store; + const tag = { ...styleTag, ...fragment.tag }; + const { minX, minY, width, height } = fragment.drawing; + const baseScale = scale / (1 << (tag.p - 1)); + const scaleX = (tag.fscx ? tag.fscx / 100 : 1) * baseScale; + const scaleY = (tag.fscy ? tag.fscy / 100 : 1) * baseScale; + const blur = tag.blur || tag.be || 0; + const vbx = tag.xbord + (tag.xshad < 0 ? -tag.xshad : 0) + blur; + const vby = tag.ybord + (tag.yshad < 0 ? -tag.yshad : 0) + blur; + const vbw = width * scaleX + 2 * tag.xbord + Math.abs(tag.xshad) + 2 * blur; + const vbh = height * scaleY + 2 * tag.ybord + Math.abs(tag.yshad) + 2 * blur; + const $svg = createSVGEl('svg', [ + ['width', vbw], + ['height', vbh], + ['viewBox', `${-vbx} ${-vby} ${vbw} ${vbh}`], + ]); + const strokeScale = /yes/i.test(info.ScaledBorderAndShadow) ? scale : 1; + const filterId = `ASS-${uuid()}`; + const $defs = createSVGEl('defs'); + $defs.append(createSVGStroke(tag, filterId, strokeScale)); + $svg.append($defs); + const symbolId = `ASS-${uuid()}`; + const $symbol = createSVGEl('symbol', [ + ['id', symbolId], + ['viewBox', `${minX} ${minY} ${width} ${height}`], + ]); + $symbol.append(createSVGEl('path', [['d', fragment.drawing.d]])); + $svg.append($symbol); + $svg.append(createSVGEl('use', [ + ['width', width * scaleX], + ['height', height * scaleY], + ['xlink:href', `#${symbolId}`], + ['filter', `url(#${filterId})`], + ])); + $svg.style.cssText = ( + 'position:absolute;' + + `left:${minX * scaleX - vbx}px;` + + `top:${minY * scaleY - vby}px;` + ); + return { + $svg, + cssText: `position:relative;width:${width * scaleX}px;height:${height * scaleY}px;`, }; - - KeyframeBlockList.prototype.setT = function setT (ref) { - var t1 = ref.t1; - var t2 = ref.t2; - var duration = ref.duration; - var prop = ref.prop; - var from = ref.from; - var to = ref.to; - - this.set('0.000%', prop, from); - if (t1 < duration) { - this.set((((t1 / duration * 100).toFixed(3)) + "%"), prop, from); - } - if (t2 < duration) { - this.set((((t2 / duration * 100).toFixed(3)) + "%"), prop, to); +} + +function createTransform(tag) { + return [ + // TODO: I don't know why perspective is 314, it just performances well. + 'perspective(314px)', + `rotateY(${tag.fry || 0}deg)`, + `rotateX(${tag.frx || 0}deg)`, + `rotateZ(${-tag.frz || 0}deg)`, + `scale(${tag.p ? 1 : (tag.fscx || 100) / 100},${tag.p ? 1 : (tag.fscy || 100) / 100})`, + `skew(${tag.fax || 0}rad,${tag.fay || 0}rad)`, + ].join(' '); +} + +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.hasRotate === '') { + // 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;`; } - this.set('100.000%', prop, to); - }; - - KeyframeBlockList.prototype.toString = function toString () { - var this$1 = this; - - return Object.keys(this.obj) - .map(function (keyText) { return ( - (keyText + "{" + (Object.keys(this$1.obj[keyText]) - .map(function (prop) { return ("" + (vendor[prop] || '') + prop + ":" + (this$1.obj[keyText][prop]) + ";"); }) - .join('')) + "}") - ); }) - .join(''); + } +} + +function encodeText(text, q) { + return text + .replace(/\\h/g, ' ') + .replace(/\\N/g, '\n') + .replace(/\\n/g, q === 2 ? '\n' : ' '); +} + +function createDialogue(dialogue, store) { + const { video, styles, info } = store; + const $div = document.createElement('div'); + $div.className = 'ASS-dialogue'; + const df = document.createDocumentFragment(); + const { slices, start, end } = dialogue; + const animationOptions = { + duration: (end - start) * 1000, + delay: Math.min(0, start - (video.currentTime - store.delay)) * 1000, + fill: 'forwards', }; - - // TODO: multi \t can't be merged directly - function mergeT(ts) { - return ts.reduceRight(function (results, t) { - var merged = false; - return results - .map(function (r) { - merged = t.t1 === r.t1 && t.t2 === r.t2 && t.accel === r.accel; - return assign({}, r, merged ? { tag: assign({}, r.tag, t.tag) } : {}); - }) - .concat(merged ? [] : t); - }, []); - } - - function getKeyframes() { - var this$1 = this; - - var keyframes = ''; - this.dialogues.forEach(function (dialogue) { - var start = dialogue.start; - var end = dialogue.end; - var effect = dialogue.effect; - var move = dialogue.move; - var fade = dialogue.fade; - var slices = dialogue.slices; - var duration = (end - start) * 1000; - var diaKbl = new KeyframeBlockList(); - // TODO: when effect and move both exist, its behavior is weird, for now only move works. - if (effect && !move) { - var name = effect.name; - var delay = effect.delay; - var lefttoright = effect.lefttoright; - var y1 = effect.y1; - var y2 = effect.y2 || this$1._.resampledRes.height; - if (name === 'banner') { - var tx = this$1.scale * (duration / delay) * (lefttoright ? 1 : -1); - diaKbl.set('0.000%', 'transform', 'translateX(0)'); - diaKbl.set('100.000%', 'transform', ("translateX(" + tx + "px)")); + $div.animations = []; + slices.forEach((slice) => { + const sliceTag = styles[slice.style].tag; + const borderStyle = styles[slice.style].style.BorderStyle; + slice.fragments.forEach((fragment) => { + const { text, drawing } = fragment; + const tag = { ...sliceTag, ...fragment.tag }; + let cssText = 'display:inline-block;'; + const cssVars = []; + if (!drawing) { + cssText += `line-height:normal;font-family:"${tag.fn}",Arial;`; + cssText += `font-size:${store.scale * getRealFontSize(tag.fn, tag.fs)}px;`; + cssText += `color:${color2rgba(tag.a1 + tag.c1)};`; + const scale = /yes/i.test(info.ScaledBorderAndShadow) ? store.scale : 1; + if (borderStyle === 1) { + cssVars.push(...createCSSStroke(tag, scale)); } - if (/^scroll/.test(name)) { - var updown = /up/.test(name) ? -1 : 1; - var tFrom = "translateY(" + (this$1.scale * y1 * updown) + "px)"; - var tTo = "translateY(" + (this$1.scale * y2 * updown) + "px)"; - var dp = (y2 - y1) / (duration / delay) * 100; - diaKbl.set('0.000%', 'transform', tFrom); - if (dp < 100) { - diaKbl.set(((dp.toFixed(3)) + "%"), 'transform', tTo); - } - diaKbl.set('100.000%', 'transform', tTo); + if (borderStyle === 3) { + // TODO: \bord0\shad16 + const bc = color2rgba(tag.a3 + tag.c3); + const bx = tag.xbord * scale; + const by = tag.ybord * scale; + const sc = color2rgba(tag.a4 + tag.c4); + const sx = tag.xshad * scale; + const sy = tag.yshad * scale; + cssText += ( + `${bx || by ? `background-color:${bc};` : ''}` + + `border:0 solid ${bc};` + + `border-width:${bx}px ${by}px;` + + `margin:${-bx}px ${-by}px;` + + `box-shadow:${sx}px ${sy}px ${sc};` + ); + } + cssText += tag.b ? `font-weight:${tag.b === 1 ? 'bold' : tag.b};` : ''; + cssText += tag.i ? 'font-style:italic;' : ''; + cssText += (tag.u || tag.s) ? `text-decoration:${tag.u ? 'underline' : ''} ${tag.s ? 'line-through' : ''};` : ''; + cssText += tag.fsp ? `letter-spacing:${store.scale * tag.fsp}px;` : ''; + // TODO: q0 and q3 is same for now, at least better than nothing. + if (tag.q === 0 || tag.q === 3) { + cssText += 'text-wrap:balance;'; + } + if (tag.q === 1) { + cssText += 'word-break:break-all;white-space:normal;'; + } + if (tag.q === 2) { + cssText += 'word-break:normal;white-space:nowrap;'; } } - if (move) { - var x1 = move.x1; - var y1$1 = move.y1; - var x2 = move.x2; - var y2$1 = move.y2; - var t1 = move.t1; - var t2 = move.t2 || duration; - var pos = dialogue.pos || { x: 0, y: 0 }; - var values = [{ x: x1, y: y1$1 }, { x: x2, y: y2$1 }].map(function (ref) { - var x = ref.x; - var y = ref.y; - - return ( - ("translate(" + (this$1.scale * (x - pos.x)) + "px, " + (this$1.scale * (y - pos.y)) + "px)") - ); - }); - diaKbl.setT({ t1: t1, t2: t2, duration: duration, prop: 'transform', from: values[0], to: values[1] }); - } - if (fade) { - if (fade.type === 'fad') { - var t1$1 = fade.t1; - var t2$1 = fade.t2; - diaKbl.set('0.000%', 'opacity', 0); - if (t1$1 < duration) { - diaKbl.set((((t1$1 / duration * 100).toFixed(3)) + "%"), 'opacity', 1); - if (t1$1 + t2$1 < duration) { - diaKbl.set(((((duration - t2$1) / duration * 100).toFixed(3)) + "%"), 'opacity', 1); - } - diaKbl.set('100.000%', 'opacity', 0); - } else { - diaKbl.set('100.000%', 'opacity', duration / t1$1); - } - } else { - var a1 = fade.a1; - var a2 = fade.a2; - var a3 = fade.a3; - var t1$2 = fade.t1; - var t2$2 = fade.t2; - var t3 = fade.t3; - var t4 = fade.t4; - var keyTexts = [t1$2, t2$2, t3, t4].map(function (t) { return (((t / duration * 100).toFixed(3)) + "%"); }); - var values$1 = [a1, a2, a3].map(function (a) { return 1 - a / 255; }); - diaKbl.set('0.000%', 'opacity', values$1[0]); - if (t1$2 < duration) { diaKbl.set(keyTexts[0], 'opacity', values$1[0]); } - if (t2$2 < duration) { diaKbl.set(keyTexts[1], 'opacity', values$1[1]); } - if (t3 < duration) { diaKbl.set(keyTexts[2], 'opacity', values$1[1]); } - if (t4 < duration) { diaKbl.set(keyTexts[3], 'opacity', values$1[2]); } - diaKbl.set('100.000%', 'opacity', values$1[2]); + const hasTransfrom = transformTags.some((x) => ( + /^fsc[xy]$/.test(x) ? tag[x] !== 100 : !!tag[x] + )); + if (hasTransfrom) { + cssText += `transform:${createTransform(tag)};`; + if (!drawing) { + cssText += 'transform-style:preserve-3d;word-break:normal;white-space:nowrap;'; } } - var diaList = diaKbl.toString(); - if (diaList) { - assign(dialogue, { animationName: ("ASS-" + (uuid())) }); - keyframes += getKeyframeString(dialogue.animationName, diaList); + if (drawing && tag.pbo) { + const pbo = store.scale * -tag.pbo * (tag.fscy || 100) / 100; + cssText += `vertical-align:${pbo}px;`; } - slices.forEach(function (slice) { - slice.fragments.forEach(function (fragment) { - if (!fragment.tag.t || !fragment.tag.t.length) { - return; + + const hasRotate = /"fr[x-z]":[^0]/.test(JSON.stringify(tag)); + encodeText(text, tag.q).split('\n').forEach((content, idx) => { + const $span = document.createElement('span'); + if (hasRotate) { + $span.dataset.hasRotate = ''; + } + if (drawing) { + const obj = createDrawing(fragment, sliceTag, store); + if (!obj) return; + $span.style.cssText = obj.cssText; + $span.append(obj.$svg); + } else { + if (idx) { + df.append(document.createElement('br')); + } + if (!content) return; + $span.textContent = content; + if (tag.xbord || tag.ybord || tag.xshad || tag.yshad) { + $span.dataset.stroke = content; } - var kbl = new KeyframeBlockList(); - var fromTag = assign({}, slice.tag, fragment.tag); - // TODO: accel is not implemented yet - mergeT(fragment.tag.t).forEach(function (ref) { - var t1 = ref.t1; - var t2 = ref.t2; - var tag = ref.tag; - - if (tag.fs) { - var from = (this$1.scale * getRealFontSize(fromTag.fn, fromTag.fs)) + "px"; - var to = (this$1.scale * getRealFontSize(tag.fn, fromTag.fs)) + "px"; - kbl.setT({ t1: t1, t2: t2, duration: duration, prop: 'font-size', from: from, to: to }); - } - if (tag.fsp) { - var from$1 = (this$1.scale * fromTag.fsp) + "px"; - var to$1 = (this$1.scale * tag.fsp) + "px"; - kbl.setT({ t1: t1, t2: t2, duration: duration, prop: 'letter-spacing', from: from$1, to: to$1 }); - } - var hasAlpha = ( - tag.a1 !== undefined - && tag.a1 === tag.a2 - && tag.a2 === tag.a3 - && tag.a3 === tag.a4 - ); - if (tag.c1 || (tag.a1 && !hasAlpha)) { - var from$2 = color2rgba(fromTag.a1 + fromTag.c1); - var to$2 = color2rgba((tag.a1 || fromTag.a1) + (tag.c1 || fromTag.c1)); - kbl.setT({ t1: t1, t2: t2, duration: duration, prop: 'color', from: from$2, to: to$2 }); - } - if (hasAlpha) { - var from$3 = 1 - parseInt(fromTag.a1, 16) / 255; - var to$3 = 1 - parseInt(tag.a1, 16) / 255; - kbl.setT({ t1: t1, t2: t2, duration: duration, prop: 'opacity', from: from$3, to: to$3 }); - } - var hasStroke = strokeTags.some(function (x) { return ( - tag[x] !== undefined - && tag[x] !== (fragment.tag[x] || slice.tag[x]) - ); }); - if (hasStroke) { - var scale = /Yes/i.test(this$1.info.ScaledBorderAndShadow) ? this$1.scale : 1; - var from$4 = createCSSStroke(fromTag, scale); - var to$4 = createCSSStroke(assign({}, fromTag, tag), scale); - kbl.setT({ t1: t1, t2: t2, duration: duration, prop: 'text-shadow', from: from$4, to: to$4 }); - } - var hasTransfrom = transformTags.some(function (x) { return ( - tag[x] !== undefined - && tag[x] !== (fragment.tag[x] || slice.tag[x]) - ); }); - if (hasTransfrom) { - var toTag = assign({}, fromTag, tag); - if (fragment.drawing) { - // scales will be handled inside svg - assign(toTag, { - p: 0, - fscx: ((tag.fscx || fromTag.fscx) / fromTag.fscx) * 100, - fscy: ((tag.fscy || fromTag.fscy) / fromTag.fscy) * 100, - }); - assign(fromTag, { fscx: 100, fscy: 100 }); - } - var from$5 = createTransform(fromTag); - var to$5 = createTransform(toTag); - kbl.setT({ t1: t1, t2: t2, duration: duration, prop: 'transform', from: from$5, to: to$5 }); - } - }); - var list = kbl.toString(); - assign(fragment, { animationName: ("ASS-" + (uuid())) }); - keyframes += getKeyframeString(fragment.animationName, list); + } + // TODO: maybe it can be optimized + $span.style.cssText += cssText; + cssVars.forEach(({ key, value }) => { + $span.style.setProperty(key, value); }); + if (fragment.keyframes) { + const animation = initAnimation( + $span, + fragment.keyframes, + { ...animationOptions, duration: fragment.duration }, + ); + $div.animations.push(animation); + } + df.append($span); }); }); - return keyframes; + }); + if (dialogue.keyframes) { + $div.animations.push(initAnimation($div, dialogue.keyframes, animationOptions)); } - - function createAnimation(name, duration, delay) { - var va = vendor.animation; + $div.append(df); + return $div; +} + +function allocate(dialogue, store) { + const { video, space, scale } = store; + const { layer, margin, width, height, alignment, end } = dialogue; + const stageWidth = store.width - Math.trunc(scale * (margin.left + margin.right)); + const stageHeight = store.height; + const vertical = Math.trunc(scale * margin.vertical); + const vct = video.currentTime * 100; + space[layer] = space[layer] || { + left: { width: new Uint16Array(stageHeight + 1), end: new Uint32Array(stageHeight + 1) }, + center: { width: new Uint16Array(stageHeight + 1), end: new Uint32Array(stageHeight + 1) }, + right: { width: new Uint16Array(stageHeight + 1), end: new Uint32Array(stageHeight + 1) }, + }; + const channel = space[layer]; + const alignH = ['right', 'left', 'center'][alignment % 3]; + const willCollide = (y) => { + const lw = channel.left.width[y]; + const cw = channel.center.width[y]; + const rw = channel.right.width[y]; + const le = channel.left.end[y]; + const ce = channel.center.end[y]; + const re = channel.right.end[y]; return ( - va + "animation-name:" + name + ";" - + va + "animation-duration:" + duration + "s;" - + va + "animation-delay:" + delay + "s;" - + va + "animation-timing-function:linear;" - + va + "animation-iteration-count:1;" - + va + "animation-fill-mode:forwards;" + (alignH === 'left' && ( + (le > vct && lw) + || (ce > vct && cw && 2 * width + cw > stageWidth) + || (re > vct && rw && width + rw > stageWidth) + )) + || (alignH === 'center' && ( + (le > vct && lw && 2 * lw + width > stageWidth) + || (ce > vct && cw) + || (re > vct && rw && 2 * rw + width > stageWidth) + )) + || (alignH === 'right' && ( + (le > vct && lw && lw + width > stageWidth) + || (ce > vct && cw && 2 * width + cw > stageWidth) + || (re > vct && rw) + )) ); + }; + let count = 0; + let result = 0; + const find = (y) => { + count = willCollide(y) ? 0 : count + 1; + if (count >= height) { + result = y; + return true; + } + return false; + }; + if (alignment <= 3) { + result = stageHeight - vertical - 1; + for (let i = result; i > vertical; i -= 1) { + if (find(i)) break; + } + } else if (alignment >= 7) { + result = vertical + 1; + for (let i = result; i < stageHeight - vertical; i += 1) { + if (find(i)) break; + } + } else { + result = (stageHeight - height) >> 1; + for (let i = result; i < stageHeight - vertical; i += 1) { + if (find(i)) break; + } } - - function createDrawing(fragment, styleTag) { - var tag = assign({}, styleTag, fragment.tag); - var ref = fragment.drawing; - var minX = ref.minX; - var minY = ref.minY; - var width = ref.width; - var height = ref.height; - var baseScale = this.scale / (1 << (tag.p - 1)); - var scaleX = (tag.fscx ? tag.fscx / 100 : 1) * baseScale; - var scaleY = (tag.fscy ? tag.fscy / 100 : 1) * baseScale; - var blur = tag.blur || tag.be || 0; - var vbx = tag.xbord + (tag.xshad < 0 ? -tag.xshad : 0) + blur; - var vby = tag.ybord + (tag.yshad < 0 ? -tag.yshad : 0) + blur; - var vbw = width * scaleX + 2 * tag.xbord + Math.abs(tag.xshad) + 2 * blur; - var vbh = height * scaleY + 2 * tag.ybord + Math.abs(tag.yshad) + 2 * blur; - var $svg = createSVGEl('svg', [ - ['width', vbw], - ['height', vbh], - ['viewBox', ((-vbx) + " " + (-vby) + " " + vbw + " " + vbh)] ]); - var strokeScale = /Yes/i.test(this.info.ScaledBorderAndShadow) ? this.scale : 1; - var filterId = "ASS-" + (uuid()); - var $defs = createSVGEl('defs'); - $defs.appendChild(createSVGStroke(tag, filterId, strokeScale)); - $svg.appendChild($defs); - var symbolId = "ASS-" + (uuid()); - var $symbol = createSVGEl('symbol', [ - ['id', symbolId], - ['viewBox', (minX + " " + minY + " " + width + " " + height)] ]); - $symbol.appendChild(createSVGEl('path', [['d', fragment.drawing.d]])); - $svg.appendChild($symbol); - $svg.appendChild(createSVGEl('use', [ - ['width', width * scaleX], - ['height', height * scaleY], - ['xlink:href', ("#" + symbolId)], - ['filter', ("url(#" + filterId + ")")] ])); - $svg.style.cssText = ( - 'position:absolute;' - + "left:" + (minX * scaleX - vbx) + "px;" - + "top:" + (minY * scaleY - vby) + "px;" - ); - return { - $svg: $svg, - cssText: ("position:relative;width:" + (width * scaleX) + "px;height:" + (height * scaleY) + "px;"), - }; + if (alignment > 3) { + result -= height - 1; } - - function encodeText(text, q) { - return text - .replace(//g, '>') - .replace(/\s/g, ' ') - .replace(/\\h/g, ' ') - .replace(/\\N/g, '
') - .replace(/\\n/g, q === 2 ? '
' : ' '); - } - - function createDialogue(dialogue) { - var this$1 = this; - - var $div = document.createElement('div'); - $div.className = 'ASS-dialogue'; - var df = document.createDocumentFragment(); - var slices = dialogue.slices; - var start = dialogue.start; - var end = dialogue.end; - slices.forEach(function (slice) { - var borderStyle = slice.borderStyle; - slice.fragments.forEach(function (fragment) { - var text = fragment.text; - var drawing = fragment.drawing; - var animationName = fragment.animationName; - var tag = assign({}, slice.tag, fragment.tag); - var cssText = 'display:inline-block;'; - var vct = this$1.video.currentTime; - if (!drawing) { - cssText += "font-family:\"" + (tag.fn) + "\",Arial;"; - cssText += "font-size:" + (this$1.scale * getRealFontSize(tag.fn, tag.fs)) + "px;"; - cssText += "color:" + (color2rgba(tag.a1 + tag.c1)) + ";"; - var scale = /Yes/i.test(this$1.info.ScaledBorderAndShadow) ? this$1.scale : 1; - if (borderStyle === 1) { - cssText += "text-shadow:" + (createCSSStroke(tag, scale)) + ";"; - } - if (borderStyle === 3) { - cssText += ( - "background-color:" + (color2rgba(tag.a3 + tag.c3)) + ";" - + "box-shadow:" + (createCSSStroke(tag, scale)) + ";" - ); - } - cssText += tag.b ? ("font-weight:" + (tag.b === 1 ? 'bold' : tag.b) + ";") : ''; - cssText += tag.i ? 'font-style:italic;' : ''; - cssText += (tag.u || tag.s) ? ("text-decoration:" + (tag.u ? 'underline' : '') + " " + (tag.s ? 'line-through' : '') + ";") : ''; - cssText += tag.fsp ? ("letter-spacing:" + (tag.fsp) + "px;") : ''; - // TODO: (tag.q === 0) and (tag.q === 3) are not implemented yet, - // for now just handle it as (tag.q === 1) - if (tag.q === 1 || tag.q === 0 || tag.q === 3) { - cssText += 'word-break:break-all;white-space:normal;'; - } - if (tag.q === 2) { - cssText += 'word-break:normal;white-space:nowrap;'; - } - } - var hasTransfrom = transformTags.some(function (x) { return ( - /^fsc[xy]$/.test(x) ? tag[x] !== 100 : !!tag[x] - ); }); - if (hasTransfrom) { - cssText += (vendor.transform) + "transform:" + (createTransform(tag)) + ";"; - if (!drawing) { - cssText += 'transform-style:preserve-3d;word-break:normal;white-space:nowrap;'; - } - } - if (animationName) { - cssText += createAnimation(animationName, end - start, Math.min(0, start - vct)); - } - if (drawing && tag.pbo) { - var pbo = this$1.scale * -tag.pbo * (tag.fscy || 100) / 100; - cssText += "vertical-align:" + pbo + "px;"; - } - - var hasRotate = /"fr[xyz]":[^0]/.test(JSON.stringify(tag)); - encodeText(text, tag.q).split('
').forEach(function (html, idx) { - var $span = document.createElement('span'); - $span.dataset.hasRotate = hasRotate; - if (drawing) { - var obj = createDrawing.call(this$1, fragment, slice.tag); - $span.style.cssText = obj.cssText; - $span.appendChild(obj.$svg); - } else { - if (idx) { - df.appendChild(document.createElement('br')); - } - if (!html) { - return; - } - $span.innerHTML = html; - } - // TODO: maybe it can be optimized - $span.style.cssText += cssText; - df.appendChild($span); - }); - }); - }); - $div.appendChild(df); - return $div; - } - - function allocate(dialogue) { - var layer = dialogue.layer; - var margin = dialogue.margin; - var width = dialogue.width; - var height = dialogue.height; - var alignment = dialogue.alignment; - var end = dialogue.end; - var stageWidth = this.width - (this.scale * (margin.left + margin.right) | 0); - var stageHeight = this.height; - var vertical = this.scale * margin.vertical | 0; - var vct = this.video.currentTime * 100; - this._.space[layer] = this._.space[layer] || { - left: { width: new Uint16Array(stageHeight + 1), end: new Uint16Array(stageHeight + 1) }, - center: { width: new Uint16Array(stageHeight + 1), end: new Uint16Array(stageHeight + 1) }, - right: { width: new Uint16Array(stageHeight + 1), end: new Uint16Array(stageHeight + 1) }, - }; - var channel = this._.space[layer]; - var align = ['right', 'left', 'center'][alignment % 3]; - var willCollide = function (y) { - var lw = channel.left.width[y]; - var cw = channel.center.width[y]; - var rw = channel.right.width[y]; - var le = channel.left.end[y]; - var ce = channel.center.end[y]; - var re = channel.right.end[y]; - return ( - (align === 'left' && ( - (le > vct && lw) - || (ce > vct && cw && 2 * width + cw > stageWidth) - || (re > vct && rw && width + rw > stageWidth) - )) - || (align === 'center' && ( - (le > vct && lw && 2 * lw + width > stageWidth) - || (ce > vct && cw) - || (re > vct && rw && 2 * rw + width > stageWidth) - )) - || (align === 'right' && ( - (le > vct && lw && lw + width > stageWidth) - || (ce > vct && cw && 2 * width + cw > stageWidth) - || (re > vct && rw) - )) - ); - }; - var count = 0; - var result = 0; - var find = function (y) { - count = willCollide(y) ? 0 : count + 1; - if (count >= height) { - result = y; - return true; - } - return false; - }; - if (alignment <= 3) { - for (var i = stageHeight - vertical - 1; i > vertical; i--) { - if (find(i)) { break; } + for (let i = result; i < result + height; i += 1) { + channel[alignH].width[i] = width; + channel[alignH].end[i] = end * 100; + } + return result; +} + +function getPosition(dialogue, store) { + const { scale } = store; + const { effect, move, align, width, height, margin, slices } = dialogue; + let x = 0; + let y = 0; + if (effect) { + if (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) { + const pos = dialogue.pos || { x: 0, y: 0 }; + const sx = scale * pos.x; + const sy = scale * pos.y; + x = [sx, sx - width / 2, sx - width][align.h]; + y = [sy - height, sy - height / 2, sy][align.v]; + } else { + x = [ + 0, + (store.width - width) / 2, + store.width - width - scale * margin.right, + ][align.h]; + const hasT = slices.some((slice) => ( + slice.fragments.some(({ animationName }) => animationName) + )); + y = hasT + ? [ + store.height - height - margin.vertical, + (store.height - height) / 2, + margin.vertical, + ][align.v] + : allocate(dialogue, store); + } + return { x, y }; +} + +function createStyle(dialogue, store) { + const { layer, align, effect, pos, margin, width } = dialogue; + let cssText = ''; + if (layer) cssText += `z-index:${layer};`; + cssText += `text-align:${['left', 'center', 'right'][align.h]};`; + if (!effect) { + const mw = store.width - store.scale * (margin.left + margin.right); + cssText += `max-width:${mw}px;`; + if (!pos) { + if (align.h === 0) { + cssText += `margin-left:${store.scale * margin.left}px;`; } - } else if (alignment >= 7) { - for (var i$1 = vertical + 1; i$1 < stageHeight - vertical; i$1++) { - if (find(i$1)) { break; } + if (align.h === 2) { + cssText += `margin-right:${store.scale * margin.right}px;`; } - } else { - for (var i$2 = (stageHeight - height) >> 1; i$2 < stageHeight - vertical; i$2++) { - if (find(i$2)) { break; } + if (width > store.width - store.scale * (margin.left + margin.right)) { + cssText += `margin-left:${store.scale * margin.left}px;`; + cssText += `margin-right:${store.scale * margin.right}px;`; } } - if (alignment > 3) { - result -= height - 1; - } - for (var i$3 = result; i$3 < result + height; i$3++) { - channel[align].width[i$3] = width; - channel[align].end[i$3] = end * 100; + } + return cssText; +} + +function renderer(dialogue, store) { + const $div = createDialogue(dialogue, store); + Object.assign(dialogue, { $div }); + store.box.append($div); + const { width } = $div.getBoundingClientRect(); + Object.assign(dialogue, { width }); + $div.style.cssText += createStyle(dialogue, store); + // height may be changed after createStyle + const { height } = $div.getBoundingClientRect(); + 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;`; + setTransformOrigin(dialogue, store.scale); + Object.assign(dialogue, getClipPath(dialogue, store)); + return dialogue; +} + +// 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 }, store) { + // TODO: when effect and move both exist, its behavior is weird, for now only move works. + const { name, delay, lefttoright, y1 } = effect; + const y2 = effect.y2 || store.resampledRes.height; + if (name === 'banner') { + const tx = store.scale * (duration / delay) * (lefttoright ? 1 : -1); + return [0, `${tx}px`].map((x, i) => ({ + offset: i, + transform: `translateX(${x})`, + })); + } + if (name.startsWith('scroll')) { + const updown = /up/.test(name) ? -1 : 1; + const dp = (y2 - y1) / (duration / delay); + return [y1, y2] + .map((y) => store.scale * y * updown) + .map((y, i) => ({ + offset: Math.min(i, dp), + transform: `translateY${y}`, + })); + } + return []; +} + +function createMoveKeyframes({ move, duration, dialogue }, store) { + 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]) => [store.scale * (x - pos.x), store.scale * (y - pos.y)]) + .map(([x, y], index) => ({ + offset: Math.min(t[index] / duration, 1), + transform: `translate(${x}px, ${y}px)`, + })); +} + +function createFadeKeyframes(fade, duration) { + if (fade.type === 'fad') { + const { t1, t2 } = fade; + const kfs = []; + if (t1) { + kfs.push([0, 0]); } - return result; - } - - function getPosition(dialogue) { - var effect = dialogue.effect; - var move = dialogue.move; - var alignment = dialogue.alignment; - var width = dialogue.width; - var height = dialogue.height; - var margin = dialogue.margin; - var slices = dialogue.slices; - var x = 0; - var y = 0; - if (effect) { - if (effect.name === 'banner') { - if (alignment <= 3) { y = this.height - height - margin.vertical; } - if (alignment >= 4 && alignment <= 6) { y = (this.height - height) / 2; } - if (alignment >= 7) { y = margin.vertical; } - x = effect.lefttoright ? -width : this.width; + if (t1 < duration) { + if (t2 <= duration) { + kfs.push([t1 / duration, 1]); } - } else if (dialogue.pos || move) { - var pos = dialogue.pos || { x: 0, y: 0 }; - if (alignment % 3 === 1) { x = this.scale * pos.x; } - if (alignment % 3 === 2) { x = this.scale * pos.x - width / 2; } - if (alignment % 3 === 0) { x = this.scale * pos.x - width; } - if (alignment <= 3) { y = this.scale * pos.y - height; } - if (alignment >= 4 && alignment <= 6) { y = this.scale * pos.y - height / 2; } - if (alignment >= 7) { y = this.scale * pos.y; } - } else { - if (alignment % 3 === 1) { x = 0; } - if (alignment % 3 === 2) { x = (this.width - width) / 2; } - if (alignment % 3 === 0) { x = this.width - width - this.scale * margin.right; } - var hasT = slices.some(function (slice) { return ( - slice.fragments.some(function (ref) { - var animationName = ref.animationName; - - return animationName; - }) - ); }); - if (hasT) { - if (alignment <= 3) { y = this.height - height - margin.vertical; } - if (alignment >= 4 && alignment <= 6) { y = (this.height - height) / 2; } - if (alignment >= 7) { y = margin.vertical; } - } else { - y = allocate.call(this, dialogue); + if (t1 + t2 < duration) { + kfs.push([(duration - t2) / duration, 1]); } - } - return { x: x, y: y }; - } - - function createStyle(dialogue) { - var layer = dialogue.layer; - var start = dialogue.start; - var end = dialogue.end; - var alignment = dialogue.alignment; - var effect = dialogue.effect; - var pos = dialogue.pos; - var margin = dialogue.margin; - var animationName = dialogue.animationName; - var width = dialogue.width; - var height = dialogue.height; - var x = dialogue.x; - var y = dialogue.y; - var vct = this.video.currentTime; - var cssText = ''; - if (layer) { cssText += "z-index:" + layer + ";"; } - if (animationName) { - cssText += createAnimation(animationName, end - start, Math.min(0, start - vct)); - } - cssText += "text-align:" + (['right', 'left', 'center'][alignment % 3]) + ";"; - if (!effect) { - var mw = this.width - this.scale * (margin.left + margin.right); - cssText += "max-width:" + mw + "px;"; - if (!pos) { - if (alignment % 3 === 1) { - cssText += "margin-left:" + (this.scale * margin.left) + "px;"; - } - if (alignment % 3 === 0) { - cssText += "margin-right:" + (this.scale * margin.right) + "px;"; - } - if (width > this.width - this.scale * (margin.left + margin.right)) { - cssText += "margin-left:" + (this.scale * margin.left) + "px;"; - cssText += "margin-right:" + (this.scale * margin.right) + "px;"; - } + 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]); } - cssText += "width:" + width + "px;height:" + height + "px;left:" + x + "px;top:" + y + "px;"; - return cssText; - } - - function renderer(dialogue) { - var $div = createDialogue.call(this, dialogue); - assign(dialogue, { $div: $div }); - this._.$stage.appendChild($div); - var ref = $div.getBoundingClientRect(); - var width = ref.width; - var height = ref.height; - assign(dialogue, { width: width, height: height }); - assign(dialogue, getPosition.call(this, dialogue)); - $div.style.cssText = createStyle.call(this, dialogue); - setTransformOrigin(dialogue); - setClipPath.call(this, dialogue); - return dialogue; - } - - function framing() { - var vct = this.video.currentTime; - for (var i = this._.stagings.length - 1; i >= 0; i--) { - var dia = this._.stagings[i]; - var end = dia.end; - if (dia.effect && /scroll/.test(dia.effect.name)) { - var ref = dia.effect; - var y1 = ref.y1; - var y2 = ref.y2; - var delay = ref.delay; - var duration = ((y2 || this._.resampledRes.height) - y1) / (1000 / delay); - end = Math.min(end, dia.start + duration); + 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 { transform: createTransform(toTag) }; +} + +// 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 }, store) : []), + ...(move ? createMoveKeyframes({ move, duration, dialogue }, store) : []), + ...(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; } - if (end < vct) { - this._.$stage.removeChild(dia.$div); - if (dia.$clipPath) { - this._.$defs.removeChild(dia.$clipPath); - } - this._.stagings.splice(i, 1); + 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 }); } - } - var dias = this.dialogues; - while ( - this._.index < dias.length - && vct >= dias[this._.index].start - ) { - if (vct < dias[this._.index].end) { - var dia$1 = renderer.call(this, dias[this._.index]); - this._.stagings.push(dia$1); + tTags.reduce((prevTag, curr) => { + const tag = { ...prevTag, ...curr.tag }; + Object.assign(curr.tag, tag); + return tag; + }, {}); + const fDuration = Math.max(duration, ...tTags.map(({ t2 }) => t2)); + const kfs = tTags.map(({ t2, tag }) => { + const hasAlpha = ( + tag.a1 !== undefined + && tag.a1 === tag.a2 + && tag.a2 === tag.a3 + && tag.a3 === tag.a4 + ); + // TODO: border and shadow, should animate CSS vars + return { + offset: t2 / fDuration, + ...(tag.fs && { 'font-size': `${store.scale * getRealFontSize(tag.fn, tag.fs)}px` }), + ...(tag.fsp && { 'letter-spacing': `${store.scale * tag.fsp}px` }), + ...((tag.c1 || (tag.a1 && !hasAlpha)) && { + color: color2rgba((tag.a1 || fromTag.a1) + (tag.c1 || fromTag.c1)), + }), + ...(hasAlpha && { opacity: 1 - Number.parseInt(tag.a1, 16) / 255 }), + ...createTransformKeyframes({ fromTag, tag, fragment }), + }; + }).sort((a, b) => a.offset - b.offset); + if (kfs.length > 0) { + Object.assign(fragment, { keyframes: kfs, duration: fDuration }); } - ++this._.index; - } - } + }); + }); +} - function play() { - var this$1 = this; +/* eslint-disable no-param-reassign */ - var frame = function () { - framing.call(this$1); - this$1._.requestId = raf(frame); - }; - caf(this._.requestId); - this._.requestId = raf(frame); - this._.$stage.classList.remove('ASS-animation-paused'); - return this; +function clear(store) { + const { box, defs } = store; + while (box.lastChild) { + box.lastChild.remove(); } - - function pause() { - caf(this._.requestId); - this._.requestId = 0; - this._.$stage.classList.add('ASS-animation-paused'); - return this; + while (defs.lastChild) { + defs.lastChild.remove(); } - - function clear() { - while (this._.$stage.lastChild) { - this._.$stage.removeChild(this._.$stage.lastChild); + store.actives = []; + store.space = []; +} + +function framing(store) { + const { video, dialogues, actives, resampledRes } = store; + const vct = video.currentTime - store.delay; + for (let i = actives.length - 1; i >= 0; i -= 1) { + const dia = actives[i]; + let { end } = dia; + if (dia.effect && /scroll/.test(dia.effect.name)) { + const { y1, y2, delay } = dia.effect; + const duration = ((y2 || resampledRes.height) - y1) / (1000 / delay); + end = Math.min(end, dia.start + duration); + } + if (end < vct) { + dia.$div.remove(); + dia.$clipPath?.remove(); + actives.splice(i, 1); } - while (this._.$defs.lastChild) { - this._.$defs.removeChild(this._.$defs.lastChild); + } + while ( + store.index < dialogues.length + && vct >= dialogues[store.index].start + ) { + if (vct < dialogues[store.index].end) { + const dia = renderer(dialogues[store.index], store); + if (!video.paused) { + batchAnimate(dia.$div, 'play'); + } + actives.push(dia); } - this._.stagings = []; - this._.space = []; - } - - function seek() { - var vct = this.video.currentTime; - var dias = this.dialogues; - clear.call(this); - this._.index = (function () { - var from = 0; - var to = dias.length - 1; - while (from + 1 < to && vct > dias[(to + from) >> 1].end) { + store.index += 1; + } +} + +function createSeek(store) { + return function seek() { + clear(store); + const { video, dialogues } = store; + const vct = video.currentTime - store.delay; + store.index = (() => { + let from = 0; + const to = dialogues.length - 1; + while (from + 1 < to && vct > dialogues[(to + from) >> 1].end) { from = (to + from) >> 1; } - if (!from) { return 0; } - for (var i = from; i < to; i++) { + if (!from) return 0; + for (let i = from; i < to; i += 1) { if ( - dias[i].end > vct && vct >= dias[i].start - || (i && dias[i - 1].end < vct && vct < dias[i].start) + dialogues[i].end > vct && vct >= dialogues[i].start + || (i && dialogues[i - 1].end < vct && vct < dialogues[i].start) ) { return i; } } return to; })(); - framing.call(this); - } - - function bindEvents() { - var l = this._.listener; - l.play = play.bind(this); - l.pause = pause.bind(this); - l.seeking = seek.bind(this); - this.video.addEventListener('play', l.play); - this.video.addEventListener('pause', l.pause); - this.video.addEventListener('seeking', l.seeking); - } - - function unbindEvents() { - var l = this._.listener; - this.video.removeEventListener('play', l.play); - this.video.removeEventListener('pause', l.pause); - this.video.removeEventListener('seeking', l.seeking); - l.play = null; - l.pause = null; - l.seeking = null; - } - - function resize() { - var cw = this.video.clientWidth; - var ch = this.video.clientHeight; - var vw = this.video.videoWidth || cw; - var vh = this.video.videoHeight || ch; - var sw = this._.scriptRes.width; - var sh = this._.scriptRes.height; - var rw = sw; - var rh = sh; - var videoScale = Math.min(cw / vw, ch / vh); - if (this.resampling === 'video_width') { + framing(store); + }; +} + +function createPlay(store) { + return function play() { + const frame = () => { + framing(store); + store.requestId = requestAnimationFrame(frame); + }; + cancelAnimationFrame(store.requestId); + store.requestId = requestAnimationFrame(frame); + store.actives.forEach(({ $div }) => { + batchAnimate($div, 'play'); + }); + }; +} + +function createPause(store) { + return function pause() { + cancelAnimationFrame(store.requestId); + store.requestId = 0; + store.actives.forEach(({ $div }) => { + batchAnimate($div, 'pause'); + }); + }; +} + +function createResize(that, store) { + const { video, box, svg, dialogues } = store; + return function resize() { + const cw = video.clientWidth; + const ch = video.clientHeight; + const vw = video.videoWidth || cw; + const vh = video.videoHeight || ch; + const sw = store.scriptRes.width; + const sh = store.scriptRes.height; + let rw = sw; + let rh = sh; + const videoScale = Math.min(cw / vw, ch / vh); + if (that.resampling === 'video_width') { rh = sw / vw * vh; } - if (this.resampling === 'video_height') { + if (that.resampling === 'video_height') { rw = sh / vh * vw; } - this.scale = Math.min(cw / rw, ch / rh); - if (this.resampling === 'script_width') { - this.scale = videoScale * (vw / rw); + store.scale = Math.min(cw / rw, ch / rh); + if (that.resampling === 'script_width') { + store.scale = videoScale * (vw / rw); } - if (this.resampling === 'script_height') { - this.scale = videoScale * (vh / rh); + if (that.resampling === 'script_height') { + store.scale = videoScale * (vh / rh); } - this.width = this.scale * rw; - this.height = this.scale * rh; - this._.resampledRes = { width: rw, height: rh }; - - this.container.style.cssText = "width:" + cw + "px;height:" + ch + "px;"; - var cssText = ( - "width:" + (this.width) + "px;" - + "height:" + (this.height) + "px;" - + "top:" + ((ch - this.height) / 2) + "px;" - + "left:" + ((cw - this.width) / 2) + "px;" + const bw = store.scale * rw; + const bh = store.scale * rh; + store.width = bw; + 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;` ); - this._.$stage.style.cssText = cssText; - this._.$svg.style.cssText = cssText; - this._.$svg.setAttributeNS(null, 'viewBox', ("0 0 " + sw + " " + sh)); + box.style.cssText = cssText; + svg.style.cssText = cssText; + svg.setAttributeNS(null, 'viewBox', `0 0 ${sw} ${sh}`); - this._.$animation.innerHTML = getKeyframes.call(this); - seek.call(this); + dialogues.forEach((dialogue) => { + setKeyframes(dialogue, store); + }); - return this; - } + createSeek(store)(); + }; +} + +/* eslint-disable max-len */ + +/** + * @typedef {Object} ASSOption + * @property {HTMLElement} [container] The container to display subtitles. + * Its style should be set with `position: relative` for subtitles will absolute to it. + * Defaults to `video.parentNode` + * @property {`${"video" | "script"}_${"width" | "height"}`} [resampling="video_height"] + * When script resolution(PlayResX and PlayResY) don't match the video resolution, this API defines how it behaves. + * However, drawings and clips will be always depending on script origin resolution. + * There are four valid values, we suppose video resolution is 1280x720 and script resolution is 640x480 in following situations: + * + `video_width`: Script resolution will set to video resolution based on video width. Script resolution will set to 640x360, and scale = 1280 / 640 = 2. + * + `video_height`(__default__): Script resolution will set to video resolution based on video height. Script resolution will set to 853.33x480, and scale = 720 / 480 = 1.5. + * + `script_width`: Script resolution will not change but scale is based on script width. So scale = 1280 / 640 = 2. This may causes top and bottom subs disappear from video area. + * + `script_height`: Script resolution will not change but scale is based on script height. So scale = 720 / 480 = 1.5. Script area will be centered in video area. + */ + +class ASS { + #store = { + /** @type {HTMLVideoElement} */ + video: null, + /** the box to display subtitles */ + box: document.createElement('div'), + // TODO: 是否可以动态添加 + /** use for \clip */ + svg: createSVGEl('svg'), + /** use for \clip */ + defs: createSVGEl('defs'), + /** + * video resize observer + * @type {ResizeObserver} + */ + observer: null, + scale: 1, + width: 0, + height: 0, + /** resolution from ASS file, it's PlayResX and PlayResY */ + scriptRes: {}, + /** resolution after resampling */ + resampledRes: {}, + /** current index of dialogues to match currentTime */ + index: 0, + /** @type {import('ass-compiler').ScriptInfo} */ + info: {}, + /** @type {import('ass-compiler').CompiledASSStyle} */ + styles: {}, + /** @type {import('ass-compiler').Dialogue[]} */ + dialogues: [], + /** + * active dialogues + * @type {import('ass-compiler').Dialogue[]} + */ + actives: [], + /** record dialogues' position */ + space: [], + requestId: 0, + delay: 0, + }; - var GLOBAL_CSS = '.ASS-container,.ASS-stage{position:relative;overflow:hidden}.ASS-container video{position:absolute;top:0;left:0}.ASS-stage{pointer-events:none;position:absolute}.ASS-dialogue{font-size:0;position:absolute}.ASS-fix-font-size{position:absolute;visibility:hidden}.ASS-fix-objectBoundingBox{width:100%;height:100%;position:absolute;top:0;left:0}.ASS-animation-paused *{-webkit-animation-play-state:paused!important;animation-play-state:paused!important}'; + #play; - function init(source, video, options) { - if ( options === void 0 ) options = {}; + #pause; - this.scale = 1; + #seek; - // private variables - this._ = { - index: 0, - stagings: [], - space: [], - listener: {}, - $svg: createSVGEl('svg'), - $defs: createSVGEl('defs'), - $stage: document.createElement('div'), - $animation: document.createElement('style'), - }; - this._.$svg.appendChild(this._.$defs); - this._.$stage.className = 'ASS-stage ASS-animation-paused'; - - this._.resampling = options.resampling || 'video_height'; - - this.container = options.container || document.createElement('div'); - this.container.classList.add('ASS-container'); - this.container.appendChild($fixFontSize); - this.container.appendChild(this._.$svg); - this._.hasInitContainer = !!options.container; - - this.video = video; - bindEvents.call(this); - if (!this._.hasInitContainer) { - var isPlaying = !video.paused; - video.parentNode.insertBefore(this.container, video); - this.container.appendChild(video); - if (isPlaying && video.paused) { - video.play(); - } - } - this.container.appendChild(this._.$stage); - - var ref = compile(source); - var info = ref.info; - var width = ref.width; - var height = ref.height; - var dialogues = ref.dialogues; - this.info = info; - this._.scriptRes = { - width: width || video.videoWidth, - height: height || video.videoHeight, + #resize; + + /** + * Initialize an ASS instance + * @param {string} content ASS content + * @param {HTMLVideoElement} video The video element to be associated with + * @param {ASSOption} [option] + * @returns {ASS} + * @example + * + * HTML: + * ```html + *
+ * + * + *
+ * ``` + * + * JavaScript: + * ```js + * import ASS from 'assjs'; + * + * const content = await fetch('/path/to/example.ass').then((res) => res.text()); + * const ass = new ASS(content, document.querySelector('#video'), { + * container: document.querySelector('#container'), + * }); + * ``` + */ + constructor(content, video, { container = video.parentNode, resampling } = {}) { + this.#store.video = video; + if (!container) throw new Error('Missing container.'); + + const { info, width, height, styles, dialogues } = compile(content); + this.#store.info = info; + this.#store.scriptRes = { + width: width || video.videoWidth || video.clientWidth, + height: height || video.videoHeight || video.clientHeight, }; - this.dialogues = dialogues; - - var styleRoot = getStyleRoot(this.container); - var $style = styleRoot.querySelector('#ASS-global-style'); - if (!$style) { - $style = document.createElement('style'); - $style.type = 'text/css'; - $style.id = 'ASS-global-style'; - $style.appendChild(document.createTextNode(GLOBAL_CSS)); - styleRoot.appendChild($style); - } - this._.$animation.type = 'text/css'; - this._.$animation.className = 'ASS-animation'; - styleRoot.appendChild(this._.$animation); + this.#store.styles = styles; + this.#store.dialogues = dialogues.map((dia) => Object.assign(dia, { + align: { + // 0: left, 1: center, 2: right + h: (dia.alignment + 2) % 3, + // 0: top, 1: center, 2: bottom + v: Math.trunc((dia.alignment - 1) / 3), + }, + })); - resize.call(this); + container.append($fixFontSize); - if (!this.video.paused) { - seek.call(this); - play.call(this); - } + const { svg, defs, box } = this.#store; - return this; - } + svg.append(defs); + container.append(svg); - function show() { - this._.$stage.style.visibility = 'visible'; - return this; - } + box.className = 'ASS-box'; + container.append(box); - function hide() { - this._.$stage.style.visibility = 'hidden'; - return this; - } + addGlobalStyle(container); - function destroy() { - pause.call(this); - clear.call(this); - unbindEvents.call(this, this._.listener); + this.#play = createPlay(this.#store); + this.#pause = createPause(this.#store); + this.#seek = createSeek(this.#store); + video.addEventListener('play', this.#play); + video.addEventListener('pause', this.#pause); + video.addEventListener('playing', this.#play); + video.addEventListener('waiting', this.#pause); + video.addEventListener('seeking', this.#seek); - var styleRoot = getStyleRoot(this.container); - if (!this._.hasInitContainer) { - var isPlay = !this.video.paused; - this.container.parentNode.insertBefore(this.video, this.container); - this.container.parentNode.removeChild(this.container); - if (isPlay && this.video.paused) { - this.video.play(); - } - } - styleRoot.removeChild(this._.$animation); + this.#resize = createResize(this, this.#store); + this.#resize(); + this.resampling = resampling; - // eslint-disable-next-line no-restricted-syntax - for (var key in this) { - if (Object.prototype.hasOwnProperty.call(this, key)) { - this[key] = null; - } - } + const observer = new ResizeObserver(this.#resize); + observer.observe(video); + this.#store.observer = observer; return this; } - var regex = /^(video|script)_(width|height)$/; + /** + * Desctroy the ASS instance + * @returns {ASS} + */ + destroy() { + const { video, box, svg, observer } = this.#store; + this.#pause(); + clear(this.#store); + video.removeEventListener('play', this.#play); + video.removeEventListener('pause', this.#pause); + video.removeEventListener('playing', this.#play); + video.removeEventListener('waiting', this.#pause); + video.removeEventListener('seeking', this.#seek); + + $fixFontSize.remove(); + svg.remove(); + box.remove(); + observer.unobserve(this.#store.video); + + this.#store.styles = {}; + this.#store.dialogues = []; - function getter() { - return regex.test(this._.resampling) ? this._.resampling : 'video_height'; + return this; } - function setter(r) { - if (r === this._.resampling) { return r; } - if (regex.test(r)) { - this._.resampling = r; - this.resize(); - } - return this._.resampling; + /** + * Show subtitles in the container + * @returns {ASS} + */ + show() { + this.#store.box.style.visibility = 'visible'; + return this; } - var ASS = function ASS(source, video, options) { - if (typeof source !== 'string') { - return this; - } - return init.call(this, source, video, options); - }; - - var prototypeAccessors = { resampling: { configurable: true } }; - - ASS.prototype.resize = function resize$1 () { - return resize.call(this); - }; - - ASS.prototype.show = function show$1 () { - return show.call(this); - }; + /** + * Hide subtitles in the container + * @returns {ASS} + */ + hide() { + this.#store.box.style.visibility = 'hidden'; + return this; + } - ASS.prototype.hide = function hide$1 () { - return hide.call(this); - }; + #resampling = 'video_height'; - ASS.prototype.destroy = function destroy$1 () { - return destroy.call(this); - }; + /** @type {ASSOption['resampling']} */ + get resampling() { + return this.#resampling; + } - prototypeAccessors.resampling.get = function () { - return getter.call(this); - }; + set resampling(r) { + if (r === this.#resampling) return; + if (/^(video|script)_(width|height)$/.test(r)) { + this.#resampling = r; + this.#resize(); + } + } - prototypeAccessors.resampling.set = function (r) { - return setter.call(this, r); - }; + /** @type {number} Subtitle delay in seconds. */ + get delay() { + return this.#store.delay; + } - Object.defineProperties( ASS.prototype, prototypeAccessors ); + set delay(d) { + if (typeof d !== 'number') return; + this.#store.delay = d; + this.#seek(); + } - return ASS; + // addDialogue(dialogue) { + // } +} -}))); +export { ASS as default }; diff --git a/dist/ass.min.js b/dist/ass.min.js index 66825fd..ef23fed 100644 --- a/dist/ass.min.js +++ b/dist/ass.min.js @@ -1 +1 @@ -(function(t,e){typeof exports==="object"&&typeof module!=="undefined"?module.exports=e():typeof define==="function"&&define.amd?define(e):(t=t||self,t.ASS=e())})(this,function(){"use strict";function l(t){var e=t.toLowerCase().trim().split(/\s*;\s*/);if(e[0]==="banner"){return{name:e[0],delay:e[1]*1||0,leftToRight:e[2]*1||0,fadeAwayWidth:e[3]*1||0}}if(/^scroll\s/.test(e[0])){return{name:e[0],y1:Math.min(e[1]*1,e[2]*1),y2:Math.max(e[1]*1,e[2]*1),delay:e[3]*1||0,fadeAwayHeight:e[4]*1||0}}return null}function y(t){return 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&&Number.isNaN(t*1))})})}var t=["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"];var x=t.map(function(t){return{name:t,regex:new RegExp("^"+t+"-?\\d")}});function b(t){var e;var a={};for(var r=0;re.length){var r=a.slice(e.length-1).join();a=a.slice(0,e.length-1);a.push(r)}var i={};for(var n=0;n0)a[r]=arguments[r+1];for(var i=0;i-10?1+s/10:1)*a.fs:s*1}}if(e==="t"){var T=s.t1;var E=s.accel;var F=s.tags;var j=s.t2||(a.end-a.start)*1e3;var L={};F.forEach(function(t){var e=Object.keys(t)[0];if(~O.indexOf(e)&&!(e==="clip"&&!t[e].dots)){q(L,R(t,e,a))}});return{t:{t1:T,t2:j,accel:E,tag:L}}}return n={},n[e]=s,n}var z=[null,1,2,3,null,7,8,9,null,4,5,6];var H=["r","a","an","pos","org","move","fade","fad","clip"];function I(t,e){return{name:t,borderStyle:e[t].style.BorderStyle,tag:e[t].tag,fragments:[]}}function S(t){var e=t.styles;var a=t.name;var r=t.parsed;var i=t.start;var n=t.end;var s;var o;var l;var f;var h;var v;var c=[];var d=I(a,e);var p={};for(var u=0;u=s.End){continue}if(!e[s.Style]){s.Style="Default"}var o=e[s.Style].style;var l=S({styles:e,name:s.Style,parsed:s.Text.parsed,start:s.Start,end:s.End});var f=l.alignment||o.Alignment;r=Math.min(r,s.Layer);i.push(q({layer:s.Layer,start:s.Start,end:s.End,margin:{left:s.MarginL||o.MarginL,right:s.MarginR||o.MarginR,vertical:s.MarginV||o.MarginV},effect:s.Effect},l,{alignment:f}))}for(var h=0;h0?1:-1;var u=l>0?1:-1;o=Math.abs(o);l=Math.abs(l);for(var g=Math.max(i,o-i);g=4&&e<=6){o.y=n+r/2}if(e>=7){o.y=n}}for(var l=s.childNodes.length-1;l>=0;l--){var f=s.childNodes[l];if(f.dataset.hasRotate==="true"){var h=o.x-i-f.offsetLeft;var v=o.y-n-f.offsetTop;f.style.cssText+=M.transform+"transform-origin:"+h+"px "+v+"px;"}}}function V(t,e){return"@"+M.animation+"keyframes "+t+" {"+e+"}\n"}var J=function t(){this.obj={}};J.prototype.set=function t(e,a,r){if(!this.obj[e]){this.obj[e]={}}this.obj[e][a]=r};J.prototype.setT=function t(e){var a=e.t1;var r=e.t2;var i=e.duration;var n=e.prop;var s=e.from;var o=e.to;this.set("0.000%",n,s);if(a/g,">").replace(/\s/g," ").replace(/\\h/g," ").replace(/\\N/g,"
").replace(/\\n/g,e===2?"
":" ")}function at(t){var d=this;var e=document.createElement("div");e.className="ASS-dialogue";var p=document.createDocumentFragment();var a=t.slices;var u=t.start;var g=t.end;a.forEach(function(v){var c=v.borderStyle;v.fragments.forEach(function(i){var t=i.text;var n=i.drawing;var e=i.animationName;var a=q({},v.tag,i.tag);var s="display:inline-block;";var r=d.video.currentTime;if(!n){s+='font-family:"'+a.fn+'",Arial;';s+="font-size:"+d.scale*G(a.fn,a.fs)+"px;";s+="color:"+P(a.a1+a.c1)+";";var o=/Yes/i.test(d.info.ScaledBorderAndShadow)?d.scale:1;if(c===1){s+="text-shadow:"+X(a,o)+";"}if(c===3){s+="background-color:"+P(a.a3+a.c3)+";"+"box-shadow:"+X(a,o)+";"}s+=a.b?"font-weight:"+(a.b===1?"bold":a.b)+";":"";s+=a.i?"font-style:italic;":"";s+=a.u||a.s?"text-decoration:"+(a.u?"underline":"")+" "+(a.s?"line-through":"")+";":"";s+=a.fsp?"letter-spacing:"+a.fsp+"px;":"";if(a.q===1||a.q===0||a.q===3){s+="word-break:break-all;white-space:normal;"}if(a.q===2){s+="word-break:normal;white-space:nowrap;"}}var l=D.some(function(t){return/^fsc[xy]$/.test(t)?a[t]!==100:!!a[t]});if(l){s+=M.transform+"transform:"+W(a)+";";if(!n){s+="transform-style:preserve-3d;word-break:normal;white-space:nowrap;"}}if(e){s+=Q(e,g-u,Math.min(0,u-r))}if(n&&a.pbo){var f=d.scale*-a.pbo*(a.fscy||100)/100;s+="vertical-align:"+f+"px;"}var h=/"fr[xyz]":[^0]/.test(JSON.stringify(a));et(t,a.q).split("
").forEach(function(t,e){var a=document.createElement("span");a.dataset.hasRotate=h;if(n){var r=tt.call(d,i,v.tag);a.style.cssText=r.cssText;a.appendChild(r.$svg)}else{if(e){p.appendChild(document.createElement("br"))}if(!t){return}a.innerHTML=t}a.style.cssText+=s;p.appendChild(a)})})});e.appendChild(p);return e}function rt(t){var e=t.layer;var a=t.margin;var o=t.width;var r=t.height;var i=t.alignment;var n=t.end;var l=this.width-(this.scale*(a.left+a.right)|0);var s=this.height;var f=this.scale*a.vertical|0;var h=this.video.currentTime*100;this._.space[e]=this._.space[e]||{left:{width:new Uint16Array(s+1),end:new Uint16Array(s+1)},center:{width:new Uint16Array(s+1),end:new Uint16Array(s+1)},right:{width:new Uint16Array(s+1),end:new Uint16Array(s+1)}};var v=this._.space[e];var c=["right","left","center"][i%3];var d=function(t){var e=v.left.width[t];var a=v.center.width[t];var r=v.right.width[t];var i=v.left.end[t];var n=v.center.end[t];var s=v.right.end[t];return c==="left"&&(i>h&&e||n>h&&a&&2*o+a>l||s>h&&r&&o+r>l)||c==="center"&&(i>h&&e&&2*e+o>l||n>h&&a||s>h&&r&&2*r+o>l)||c==="right"&&(i>h&&e&&e+o>l||n>h&&a&&2*o+a>l||s>h&&r)};var p=0;var u=0;var g=function(t){p=d(t)?0:p+1;if(p>=r){u=t;return true}return false};if(i<=3){for(var m=s-f-1;m>f;m--){if(g(m)){break}}}else if(i>=7){for(var y=f+1;y>1;x3){u-=r-1}for(var b=u;b=4&&r<=6){f=(this.height-n)/2}if(r>=7){f=s.vertical}l=e.lefttoright?-i:this.width}}else if(t.pos||a){var h=t.pos||{x:0,y:0};if(r%3===1){l=this.scale*h.x}if(r%3===2){l=this.scale*h.x-i/2}if(r%3===0){l=this.scale*h.x-i}if(r<=3){f=this.scale*h.y-n}if(r>=4&&r<=6){f=this.scale*h.y-n/2}if(r>=7){f=this.scale*h.y}}else{if(r%3===1){l=0}if(r%3===2){l=(this.width-i)/2}if(r%3===0){l=this.width-i-this.scale*s.right}var v=o.some(function(t){return t.fragments.some(function(t){var e=t.animationName;return e})});if(v){if(r<=3){f=this.height-n-s.vertical}if(r>=4&&r<=6){f=(this.height-n)/2}if(r>=7){f=s.vertical}}else{f=rt.call(this,t)}}return{x:l,y:f}}function nt(t){var e=t.layer;var a=t.start;var r=t.end;var i=t.alignment;var n=t.effect;var s=t.pos;var o=t.margin;var l=t.animationName;var f=t.width;var h=t.height;var v=t.x;var c=t.y;var d=this.video.currentTime;var p="";if(e){p+="z-index:"+e+";"}if(l){p+=Q(l,r-a,Math.min(0,a-d))}p+="text-align:"+["right","left","center"][i%3]+";";if(!n){var u=this.width-this.scale*(o.left+o.right);p+="max-width:"+u+"px;";if(!s){if(i%3===1){p+="margin-left:"+this.scale*o.left+"px;"}if(i%3===0){p+="margin-right:"+this.scale*o.right+"px;"}if(f>this.width-this.scale*(o.left+o.right)){p+="margin-left:"+this.scale*o.left+"px;";p+="margin-right:"+this.scale*o.right+"px;"}}}p+="width:"+f+"px;height:"+h+"px;left:"+v+"px;top:"+c+"px;";return p}function st(t){var e=at.call(this,t);q(t,{$div:e});this._.$stage.appendChild(e);var a=e.getBoundingClientRect();var r=a.width;var i=a.height;q(t,{width:r,height:i});q(t,it.call(this,t));e.style.cssText=nt.call(this,t);L(t);T.call(this,t);return t}function ot(){var t=this.video.currentTime;for(var e=this._.stagings.length-1;e>=0;e--){var a=this._.stagings[e];var r=a.end;if(a.effect&&/scroll/.test(a.effect.name)){var i=a.effect;var n=i.y1;var s=i.y2;var o=i.delay;var l=((s||this._.resampledRes.height)-n)/(1e3/o);r=Math.min(r,a.start+l)}if(r=f[this._.index].start){if(ti[e+t>>1].end){t=e+t>>1}if(!t){return 0}for(var a=t;ar&&r>=i[a].start||a&&i[a-1].endt.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 n(t){const a={};for(let e=0;et.replace(/,/g,"\n"))).split(/\s*,\s*/);if(!e[0])return a;a.t={t1:0,t2:0,accel:1,tags:e[e.length-1].replace(/\n/g,",").split("\\").slice(1).map(n)},2===e.length&&(a.t.accel=1*e[0]),3===e.length&&(a.t.t1=1*e[0],a.t.t2=1*e[1]),4===e.length&&(a.t.t1=1*e[0],a.t.t2=1*e[1],a.t.accel=1*e[2])}return a}function a(t){const e=[];let s=0,a="";const i=t.split("\\").slice(1).concat("").join("\\");for(let t=0;tvoid 0===e.p?t:!!e.p),!1);n.push({tags:i,text:r?"":s[t+1],drawing:r?e(s[t+1]):[]})}return{raw:t,combined:n.map((t=>t.text)).join(""),parsed:n}}function r(t){const e=t.split(":");return 3600*e[0]+60*e[1]+1*e[2]}function o(e,s){let n=e.split(",");if(n.length>s.length){const t=n.slice(s.length-1).join();n=n.slice(0,s.length-1),n.push(t)}const a={};for(let e=0;ee.find((e=>e.toLowerCase()===t.toLowerCase()))||t))}function p(t,e){const s=t.match(/Style\s*:\s*(.*)/i)[1].split(/\s*,\s*/);return l({},...e.map(((t,e)=>({[t]:s[e]}))))}function h(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),n=1;nt+e.map((({x:t,y:e})=>`${t},${e}`)).join(","))).join("")}function y(t){const e=[];let s=0;for(;s({x:t,y:e}))))}t.splice(s,1)}}const n=[].concat(...e.map((({type:t,points:e,prev:s,next:n})=>"S"===t?function(t,e,s){const n=[],a=[0,2/3,1/3,0],i=[0,1/3,2/3,0],r=[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];n.push({type:"M"===e?"M":"L",points:[{x:o(r,l),y:o(r,c)}]});for(let e=3;et))).forEach((({x:t,y:i})=>{e=Math.min(e,t),s=Math.min(s,i),n=Math.max(n,t),a=Math.max(a,i)})),{minX:e,minY:s,width:n-e,height:a-s}}(e))}const m=["fs","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 x(t,e,s={}){let n=t[e];if(void 0===n)return null;if("pos"===e||"org"===e)return 2===n.length?{[e]:{x:n[0],y:n[1]}}:null;if("move"===e){const[t,e,s,a,i=0,r=0]=n;return 4===n.length||6===n.length?{move:{x1:t,y1:e,x2:s,y2:a,t1:i,t2:r}}:null}if("fad"===e||"fade"===e){if(2===n.length){const[t,e]=n;return{fade:{type:"fad",t1:t,t2:e}}}if(7===n.length){const[t,e,s,a,i,r,o]=n;return{fade:{type:"fade",a1:t,a2:e,a3:s,t1:a,t2:i,t3:r,t4:o}}}return null}if("clip"===e){const{inverse:t,scale:e,drawing:s,dots:a}=n;if(s)return{clip:{inverse:t,scale:e,drawing:y(s),dots:a}};if(a){const[n,i,r,o]=a;return{clip:{inverse:t,scale:e,drawing:s,dots:{x1:n,y1:i,x2:r,y2:o}}}}return null}if(/^[xy]?(bord|shad)$/.test(e)&&(n=Math.max(n,0)),"bord"===e)return{xbord:n,ybord:n};if("shad"===e)return{xshad:n,yshad:n};if(/^c\d$/.test(e))return{[e]:n||s[e]};if("alpha"===e)return{a1:n,a2:n,a3:n,a4:n};if("fr"===e)return{frz:n};if("fs"===e)return{fs:/^\+|-/.test(n)?(1*n>-10?1+n/10:1)*s.fs:1*n};if("K"===e)return{kf:n};if("t"===e){const{t1:t,accel:e,tags:a}=n,i=n.t2||1e3*(s.end-s.start),r={};return a.forEach((t=>{const e=Object.keys(t)[0];~m.indexOf(e)&&("clip"!==e||t[e].dots)&&l(r,x(t,e,s))})),{t:{t1:t,t2:i,accel:e,tag:r}}}return{[e]:n}}const b=[null,1,2,3,null,7,8,9,null,4,5,6],v=["r","a","an","pos","org","move","fade","fad","clip"];function $({styles:t,style:e,parsed:s,start:n,end:a}){let i,r,o,c,d,f;const p=[];let h={style:e,fragments:[]},u={};for(let m=0;m=i.End)continue;t[i.Style]||(i.Style="Default");const r=t[i.Style].style,o=$({styles:t,style:i.Style,parsed:i.Text.parsed,start:i.Start,end:i.End}),c=o.alignment||r.Alignment;s=Math.min(s,i.Layer),n.push(l({layer:i.Layer,start:i.Start,end:i.End,style:i.Style,name:i.Name,margin:{left:i.MarginL||r.MarginL,right:i.MarginR||r.MarginR,vertical:i.MarginV||r.MarginV},effect:i.Effect},o,{alignment:c}))}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 k(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[i,r]=k(s.PrimaryColour),[o,c]=k(s.SecondaryColour),[d,f]=k(s.OutlineColour),[p,h]=k(s.BackColour),u={fn:s.Fontname,fs:s.Fontsize,c1:r,a1:i,c2:c,a2:o,c3:f,a3:d,c4:h,a4:p,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};n[s.Name]={style:s,tag:u}}return n}({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:n,dialogues:w({styles:n,dialogues:s.events.dialogue})}}const A=document.createElement("div");A.className="ASS-fix-font-size",A.textContent="M";const E=Object.create(null);function N(t,e){const s=`${t}-${e}`;return E[s]||(A.style.cssText=`line-height:normal;font-size:${e}px;font-family:"${t}",Arial;`,E[s]=e*e/A.clientHeight),E[s]}function C(t){return 1-`0x${t}`/255}function O(t){const e=t.match(/(\w\w)(\w\w)(\w\w)(\w\w)/),s=C(e[1]),n=+`0x${e[2]}`,a=+`0x${e[3]}`;return`rgba(${+`0x${e[4]}`},${a},${n},${s})`}function j(){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 L(t,e=[]){const s=document.createElementNS("http://www.w3.org/2000/svg",t);for(let t=0;tt.toUpperCase()));return t in e?"":`webkit${s}`in e?"-webkit-":`moz${s}`in e?"-moz-":""}("clipPath")};const z=["fscx","fscy","frx","fry","frz","fax","fay"];function B(t,e,s){const n=t.animate(e,s);return n.pause(),n}function R(t,e){(t.animations||[]).forEach((t=>{t[e]()}))}function T(t,e){if(!t.clip)return{};const s=document.createElement("div");e.box.insertBefore(s,t.$div),s.append(t.$div),s.className="ASS-fix-objectBoundingBox";const{cssText:n,$clipPath:a}=function(t,e){const s=e.scriptRes.width,n=e.scriptRes.height;let a="";if(null!==t.dots){let{x1:e,y1:i,x2:r,y2:o}=t.dots;e/=s,i/=n,r/=s,o/=n,a=`M${e},${i}L${e},${o},${r},${o},${r},${i}Z`}null!==t.drawing&&(a=t.drawing.instructions.map((({type:t,points:e})=>t+e.map((({x:t,y:e})=>`${t/s},${e/n}`)).join(","))).join(""));const i=1/(1<(s||t)&&(n||e))).map((([s,n])=>[(s||-1)*t,(n||-1)*e]))}function H(t,e){const s=O(`00${t.c3}`),n=t.xbord*e,a=t.ybord*e,i=O(`00${t.c4}`),r=t.xshad*e,o=t.yshad*e,l=t.blur||t.be||0,c=function(t,e){if(t===e)return[];const s=Math.min(t,e),n=Math.max(t,e);return Array.from({length:Math.ceil(n)-1},((t,e)=>e+1)).concat(n).map((t=>[(n-t)/n*s,t])).map((([s,n])=>t>e?[n,s]:[s,n])).flatMap(_)}(n,a);return[{key:"border-width",value:2*Math.min(n,a)+"px"},{key:"border-color",value:s},{key:"border-opacity",value:C(t.a3)},{key:"border-delta",value:c.map((([t,e])=>`${t}px ${e}px ${s}`)).join(",")},{key:"shadow-offset",value:`${r}px, ${o}px`},{key:"shadow-color",value:i},{key:"shadow-opacity",value:C(t.a4)},{key:"shadow-delta",value:c.map((([t,e])=>`${t}px ${e}px ${i}`)).join(",")},{key:"blur",value:`blur(${l}px)`}].map((t=>Object.assign(t,{key:`--ass-${t.key}`})))}function q(t,e,s){if(!t.drawing.d)return null;const{scale:n,info:a}=s,i={...e,...t.tag},{minX:r,minY:o,width:l,height:c}=t.drawing,d=n/(1<(m=(t=>{const e=g.left.width[t],s=g.center.width[t],n=g.right.width[t],a=g.left.end[t],i=g.center.end[t],r=g.right.end[t];return"left"===y&&(a>u&&e||i>u&&s&&2*o+s>f||r>u&&n&&o+n>f)||"center"===y&&(a>u&&e&&2*e+o>f||i>u&&s||r>u&&n&&2*n+o>f)||"right"===y&&(a>u&&e&&e+o>f||i>u&&s&&2*o+s>f||r>u&&n)})(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{const s=n[t.style].tag,o=n[t.style].style.BorderStyle;t.fragments.forEach((t=>{const{text:n,drawing:l}=t,c={...s,...t.tag};let f="display:inline-block;";const p=[];if(!l){f+=`line-height:normal;font-family:"${c.fn}",Arial;`,f+=`font-size:${e.scale*N(c.fn,c.fs)}px;`,f+=`color:${O(c.a1+c.c1)};`;const t=/yes/i.test(a.ScaledBorderAndShadow)?e.scale:1;if(1===o&&p.push(...H(c,t)),3===o){const e=O(c.a3+c.c3),s=c.xbord*t,n=c.ybord*t,a=O(c.a4+c.c4),i=c.xshad*t,r=c.yshad*t;f+=(s||n?`background-color:${e};`:"")+`border:0 solid ${e};`+`border-width:${s}px ${n}px;`+`margin:${-s}px ${-n}px;`+`box-shadow:${i}px ${r}px ${a};`}f+=c.b?`font-weight:${1===c.b?"bold":c.b};`:"",f+=c.i?"font-style:italic;":"",f+=c.u||c.s?`text-decoration:${c.u?"underline":""} ${c.s?"line-through":""};`:"",f+=c.fsp?`letter-spacing:${e.scale*c.fsp}px;`:"",0!==c.q&&3!==c.q||(f+="text-wrap:balance;"),1===c.q&&(f+="word-break:break-all;white-space:normal;"),2===c.q&&(f+="word-break:normal;white-space:nowrap;")}if(z.some((t=>/^fsc[xy]$/.test(t)?100!==c[t]:!!c[t]))&&(f+=`transform:${P(c)};`,l||(f+="transform-style:preserve-3d;word-break:normal;white-space:nowrap;")),l&&c.pbo){const t=e.scale*-c.pbo*(c.fscy||100)/100;f+=`vertical-align:${t}px;`}const h=/"fr[x-z]":[^0]/.test(JSON.stringify(c));(function(t,e){return t.replace(/\\h/g," ").replace(/\\N/g,"\n").replace(/\\n/g,2===e?"\n":" ")})(n,c.q).split("\n").forEach(((n,a)=>{const o=document.createElement("span");if(h&&(o.dataset.hasRotate=""),l){const n=q(t,s,e);if(!n)return;o.style.cssText=n.cssText,o.append(n.$svg)}else{if(a&&r.append(document.createElement("br")),!n)return;o.textContent=n,(c.xbord||c.ybord||c.xshad||c.yshad)&&(o.dataset.stroke=n)}if(o.style.cssText+=f,p.forEach((({key:t,value:e})=>{o.style.setProperty(t,e)})),t.keyframes){const e=B(o,t.keyframes,{...d,duration:t.duration});i.animations.push(e)}r.append(o)}))}))})),t.keyframes&&i.animations.push(B(i,t.keyframes,d)),i.append(r),i}(t,e);Object.assign(t,{$div:s}),e.box.append(s);const{width:n}=s.getBoundingClientRect();Object.assign(t,{width:n}),s.style.cssText+=function(t,e){const{layer:s,align:n,effect:a,pos:i,margin:r,width:o}=t;let l="";s&&(l+=`z-index:${s};`),l+=`text-align:${["left","center","right"][n.h]};`,a||(l+=`max-width:${e.width-e.scale*(r.left+r.right)}px;`,i||(0===n.h&&(l+=`margin-left:${e.scale*r.left}px;`),2===n.h&&(l+=`margin-right:${e.scale*r.right}px;`),o>e.width-e.scale*(r.left+r.right)&&(l+=`margin-left:${e.scale*r.left}px;`,l+=`margin-right:${e.scale*r.right}px;`)));return l}(t,e);const{height:a}=s.getBoundingClientRect();Object.assign(t,{height:a});const{x:i,y:r}=function(t,e){const{scale:s}=e,{effect:n,move:a,align:i,width:r,height:o,margin:l,slices:c}=t;let d=0,f=0;if(n)"banner"===n.name&&(d=n.lefttoright?-r:e.width,f=[e.height-o-l.vertical,(e.height-o)/2,l.vertical][i.v]);else if(t.pos||a){const e=t.pos||{x:0,y:0},n=s*e.x,a=s*e.y;d=[n,n-r/2,n-r][i.h],f=[a-o,a-o/2,a][i.v]}else d=[0,(e.width-r)/2,e.width-r-s*l.right][i.h],f=c.some((t=>t.fragments.some((({animationName:t})=>t))))?[e.height-o-l.vertical,(e.height-o)/2,l.vertical][i.v]:I(t,e);return{x:d,y:f}}(t,e);return Object.assign(t,{x:i,y:r}),s.style.cssText+=`width:${n}px;height:${a}px;left:${i}px;top:${r}px;`,function(t,e){const{align:s,width:n,height:a,x:i,y:r,$div:o}=t,l={};t.org?(l.x=t.org.x*e,l.y=t.org.y*e):(l.x=[i,i+n/2,i+n][s.h],l.y=[r+a,r+a/2,r][s.v]);for(let t=o.childNodes.length-1;t>=0;t-=1){const e=o.childNodes[t];if(""===e.dataset.hasRotate){const t=l.x-i-e.offsetLeft,s=l.y-r-e.offsetTop;e.style.cssText+=`transform-origin:${t}px ${s}px;`}}}(t,e.scale),Object.assign(t,T(t,e)),t}function D({effect:t,duration:e},s){const{name:n,delay:a,lefttoright:i,y1:r}=t,o=t.y2||s.resampledRes.height;if("banner"===n){return[0,`${s.scale*(e/a)*(i?1:-1)}px`].map(((t,e)=>({offset:e,transform:`translateX(${t})`})))}if(n.startsWith("scroll")){const t=/up/.test(n)?-1:1,i=(o-r)/(e/a);return[r,o].map((e=>s.scale*e*t)).map(((t,e)=>({offset:Math.min(e,i),transform:`translateY${t}`})))}return[]}function G({move:t,duration:e,dialogue:s},n){const{x1:a,y1:i,x2:r,y2:o,t1:l,t2:c}=t,d=[l,c||e],f=s.pos||{x:0,y:0};return[[a,i],[r,o]].map((([t,e])=>[n.scale*(t-f.x),n.scale*(e-f.y)])).map((([t,s],n)=>({offset:Math.min(d[n]/e,1),transform:`translate(${t}px, ${s}px)`})))}function W(t,e){if("fad"===t.type){const{t1:s,t2:n}=t,a=[];return s&&a.push([0,0]),se?a.push([0,(n-e)/n]):s+n>e&&a.push([(s+.5)/e,1-(s+n-e)/n]),n&&a.push([1,0])):a.push([1,e/s]),a.map((([t,e])=>({offset:t,opacity:e})))}const{a1:s,a2:n,a3:a,t1:i,t2:r,t3:o,t4:l}=t,c=[s,n,a].map((t=>1-t/255));return[0,i,r,o,l,e].map((t=>t/e)).map(((t,e)=>({offset:t,opacity:c[e>>1]}))).filter((({offset:t})=>t<=1))}function X({fromTag:t,tag:e,fragment:s}){const n={...t,...e};return s.drawing&&(Object.assign(n,{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})),{transform:P(n)}}function Y(t){const{box:e,defs:s}=t;for(;e.lastChild;)e.lastChild.remove();for(;s.lastChild;)s.lastChild.remove();t.actives=[],t.space=[]}function V(t){const{video:e,dialogues:s,actives:n,resampledRes:a}=t,i=e.currentTime-t.delay;for(let t=n.length-1;t>=0;t-=1){const e=n[t];let{end:s}=e;if(e.effect&&/scroll/.test(e.effect.name)){const{y1:t,y2:n,delay:i}=e.effect,r=((n||a.height)-t)/(1e3/i);s=Math.min(s,e.start+r)}s=s[t.index].start;){if(i{let t=0;const e=s.length-1;for(;t+1s[e+t>>1].end;)t=e+t>>1;if(!t)return 0;for(let a=t;an&&n>=s[a].start||a&&s[a-1].end{!function(t,e){const{start:s,end:n,effect:a,move:i,fade:r,slices:o}=t,l=1e3*(n-s),c=[...a&&!i?D({effect:a,duration:l},e):[],...i?G({move:i,duration:l,dialogue:t},e):[],...r?W(r,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 n={...s,...t.tag},a=(i=t.tag.t,i.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 i;a[0].t1>0&&a.unshift({t1:0,t2:a[0].t1,tag:n}),a.reduce(((t,e)=>{const s={...t,...e.tag};return Object.assign(e.tag,s),s}),{});const r=Math.max(l,...a.map((({t2:t})=>t))),o=a.map((({t2:s,tag:a})=>{const i=void 0!==a.a1&&a.a1===a.a2&&a.a2===a.a3&&a.a3===a.a4;return{offset:s/r,...a.fs&&{"font-size":e.scale*N(a.fn,a.fs)+"px"},...a.fsp&&{"letter-spacing":e.scale*a.fsp+"px"},...(a.c1||a.a1&&!i)&&{color:O((a.a1||n.a1)+(a.c1||n.c1))},...i&&{opacity:1-Number.parseInt(a.a1,16)/255},...X({fromTag:n,tag:a,fragment:t})}})).sort(((t,e)=>t.offset-e.offset));o.length>0&&Object.assign(t,{keyframes:o,duration:r})}))}))}(t,e)})),J(e)()}}class K{#t={video:null,box:document.createElement("div"),svg:L("svg"),defs:L("defs"),observer:null,scale:1,width:0,height:0,scriptRes:{},resampledRes:{},index:0,info:{},styles:{},dialogues:[],actives:[],space:[],requestId:0,delay:0};#e;#s;#n;#a;constructor(t,e,{container:s=e.parentNode,resampling:n}={}){if(this.#t.video=e,!s)throw new Error("Missing container.");const{info:a,width:i,height:r,styles:o,dialogues:l}=M(t);this.#t.info=a,this.#t.scriptRes={width:i||e.videoWidth||e.clientWidth,height:r||e.videoHeight||e.clientHeight},this.#t.styles=o,this.#t.dialogues=l.map((t=>Object.assign(t,{align:{h:(t.alignment+2)%3,v:Math.trunc((t.alignment-1)/3)}}))),s.append(A);const{svg:c,defs:d,box:f}=this.#t;var p;c.append(d),s.append(c),f.className="ASS-box",s.append(f),function(t){const e=t.getRootNode()||document,s=e===document?document.head:e;let n=s.querySelector("#ASS-global-style");n||(n=document.createElement("style"),n.type="text/css",n.id="ASS-global-style",n.append(document.createTextNode(".ASS-box{overflow:hidden;pointer-events:none;position:absolute}.ASS-dialogue{font-size:0;position:absolute;z-index:0}.ASS-dialogue [data-stroke]{position:relative}.ASS-dialogue [data-stroke]::after,.ASS-dialogue [data-stroke]::before{content:attr(data-stroke);position:absolute;top:0;left:0;z-index:-1;filter:var(--ass-blur)}.ASS-dialogue [data-stroke]::before{color:var(--ass-shadow-color);transform:translate(var(--ass-shadow-offset));-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-stroke]::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-fix-font-size{position:absolute;visibility:hidden}.ASS-fix-objectBoundingBox{width:100%;height:100%;position:absolute;top:0;left:0}")),s.append(n))}(s),this.#e=(p=this.#t,function(){const t=()=>{V(p),p.requestId=requestAnimationFrame(t)};cancelAnimationFrame(p.requestId),p.requestId=requestAnimationFrame(t),p.actives.forEach((({$div:t})=>{R(t,"play")}))}),this.#s=function(t){return function(){cancelAnimationFrame(t.requestId),t.requestId=0,t.actives.forEach((({$div:t})=>{R(t,"pause")}))}}(this.#t),this.#n=J(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.#n),this.#a=Z(this,this.#t),this.#a(),this.resampling=n;const h=new ResizeObserver(this.#a);return h.observe(e),this.#t.observer=h,this}destroy(){const{video:t,box:e,svg:s,observer:n}=this.#t;return this.#s(),Y(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.#n),A.remove(),s.remove(),e.remove(),n.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}#i="video_height";get resampling(){return this.#i}set resampling(t){t!==this.#i&&/^(video|script)_(width|height)$/.test(t)&&(this.#i=t,this.#a())}get delay(){return this.#t.delay}set delay(t){"number"==typeof t&&(this.#t.delay=t,this.#n())}}export{K as default}; diff --git a/package.json b/package.json index 19cf30c..088f331 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "assjs", - "version": "0.0.11", + "version": "0.1.0", "type": "module", "description": "A JavaScript ASS subtitle format renderer", "main": "dist/ass.js",