assjs
Version:
A lightweight JavaScript ASS subtitle renderer
1,457 lines (1,361 loc) • 69.9 kB
JavaScript
var ASS = (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,
};
}
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(function (cmd) { return (
cmd.split(' ')
.filter(function (x, i) { return !(i && 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;
}
}
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)) {
var ref$1 = text.match(/^(\d?)c&?H?(\w*)/);
var num = ref$1[1];
var color = ref$1[2];
tag[("c" + (num || 1))] = color && ("000000" + color).slice(-6);
} else if (/^\da&?H?[0-9a-fA-F]+/.test(text)) {
var ref$2 = text.match(/^(\d)a&?H?([0-9a-f]+)/i);
var num$1 = ref$2[1];
var alpha = ref$2[2];
tag[("a" + num$1)] = ("00" + alpha).slice(-2);
} else if (/^alpha&?H?[0-9a-fA-F]+/.test(text)) {
(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;
}
}
return tag;
}
function parseTags(text) {
var tags = [];
var depth = 0;
var str = '';
// `\b\c` -> `b\c\`
// `a\b\c` -> `b\c\`
var transText = text.split('\\').slice(1).concat('').join('\\');
for (var i = 0; i < transText.length; i++) {
var x = transText[i];
if (x === '(') { depth++; }
if (x === ')') { depth--; }
if (depth < 0) { depth = 0; }
if (!depth && x === '\\') {
if (str) {
tags.push(str);
}
str = '';
} else {
str += x;
}
}
return tags.map(parseTag);
}
function parseText(text) {
var pairs = text.split(/{(.*?)}/);
var parsed = [];
if (pairs[0].length) {
parsed.push({ tags: [], text: pairs[0], drawing: [] });
}
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]) : [],
});
}
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);
}
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 dia;
}
var stylesFormat = ['Name', 'Fontname', 'Fontsize', 'PrimaryColour', 'SecondaryColour', 'OutlineColour', 'BackColour', 'Bold', 'Italic', 'Underline', 'StrikeOut', 'ScaleX', 'ScaleY', 'Spacing', 'Angle', 'BorderStyle', 'Outline', 'Shadow', 'Alignment', 'MarginL', 'MarginR', 'MarginV', 'Encoding'];
var eventsFormat = ['Layer', 'Start', 'End', 'Style', 'Name', 'MarginL', 'MarginR', 'MarginV', 'Effect', 'Text'];
function parseFormat(text) {
var fields = stylesFormat.concat(eventsFormat);
return text.match(/Format\s*:\s*(.*)/i)[1]
.split(/\s*,\s*/)
.map(function (field) {
var caseField = fields.find(function (f) { return f.toLowerCase() === field.toLowerCase(); });
return caseField || field;
});
}
function parseStyle(text, format) {
var values = text.match(/Style\s*:\s*(.*)/i)[1].split(/\s*,\s*/);
return Object.assign.apply(Object, [ {} ].concat( format.map(function (fmt, idx) {
var obj;
return (( obj = {}, obj[fmt] = values[idx], obj ));
}) ));
}
function parse(text) {
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, tree.styles.format));
}
}
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));
}
}
}
return tree;
}
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;
}
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) {
var ref;
var minX = Infinity;
var minY = Infinity;
var maxX = -Infinity;
var maxY = -Infinity;
(ref = []).concat.apply(ref, commands.map(function (ref) {
var points = ref.points;
return points;
})).forEach(function (ref) {
var x = ref.x;
var y = ref.y;
minX = Math.min(minX, x);
minY = Math.min(minY, y);
maxX = Math.max(maxX, x);
maxY = Math.max(maxY, y);
});
return {
minX: minX,
minY: 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) {
var results = [];
var bb1 = [0, 2 / 3, 1 / 3, 0];
var bb2 = [0, 1 / 3, 2 / 3, 0];
var bb3 = [0, 1 / 6, 2 / 3, 1 / 6];
var dot4 = function (a, b) { return (a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3]); };
var px = [points[points.length - 1].x, points[0].x, points[1].x, points[2].x];
var py = [points[points.length - 1].y, points[0].y, points[1].y, points[2].y];
results.push({
type: prev === 'M' ? 'M' : 'L',
points: [{ x: dot4(bb3, px), y: dot4(bb3, py) }],
});
for (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);
}
}
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 Object.assign({ instructions: instructions, d: toSVGPath(instructions) }, getViewBox(commands));
}
var tTags = [
'fs', 'fsp', 'clip',
'c1', 'c2', 'c3', 'c4', 'a1', 'a2', 'a3', 'a4', 'alpha',
'fscx', 'fscy', 'fax', 'fay', 'frx', 'fry', 'frz', 'fr',
'be', 'blur', 'bord', 'xbord', 'ybord', 'shad', 'xshad', 'yshad' ];
function compileTag(tag, key, presets) {
var obj, obj$1, obj$2;
if ( presets === undefined ) 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 === undefined ) t1 = 0;
var t2 = value[5]; if ( t2 === undefined ) 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 };
}
if (key === 'fr') {
return { frz: value };
}
if (key === 'fs') {
return {
fs: /^\+|-/.test(value)
? (value * 1 > -10 ? (1 + value / 10) : 1) * presets.fs
: value * 1,
};
}
if (key === 'K') {
return { kf: value };
}
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)) {
Object.assign(compiledTag, compileTag(t, k, presets));
}
});
return { t: { t1: t1$3, t2: t2$3, accel: accel, tag: compiledTag } };
}
return ( obj$2 = {}, obj$2[key] = value, obj$2 );
}
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 inheritTag(pTag) {
return JSON.parse(JSON.stringify(Object.assign({}, pTag, {
k: undefined,
kf: undefined,
ko: undefined,
kt: undefined,
})));
}
function compileText(ref) {
var styles = ref.styles;
var style = ref.style;
var parsed = ref.parsed;
var start = ref.start;
var end = ref.end;
var alignment;
var q = { q: styles[style].tag.q };
var pos;
var org;
var move;
var fade;
var clip;
var slices = [];
var slice = { style: style, fragments: [] };
var prevTag = {};
for (var i = 0; i < parsed.length; i++) {
var ref$1 = parsed[i];
var tags = ref$1.tags;
var text = ref$1.text;
var drawing = ref$1.drawing;
var reset = (undefined);
for (var j = 0; j < tags.length; j++) {
var tag = tags[j];
reset = tag.r === undefined ? reset : tag.r;
}
var fragment = {
tag: reset === undefined ? inheritTag(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;
q = compileTag(tag$1, 'q') || q;
if (!move) {
pos = pos || compileTag(tag$1, 'pos');
}
org = org || compileTag(tag$1, 'org');
if (!pos) {
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 sliceTag = styles[style].tag;
var c1 = sliceTag.c1;
var c2 = sliceTag.c2;
var c3 = sliceTag.c3;
var c4 = sliceTag.c4;
var fs = prevTag.fs || sliceTag.fs;
var compiledTag = compileTag(tag$1, key, { start: start, end: end, c1: c1, c2: c2, c3: c3, c4: c4, fs: fs });
if (key === 't') {
fragment.tag.t = fragment.tag.t || [];
fragment.tag.t.push(compiledTag.t);
} else {
Object.assign(fragment.tag, compiledTag);
}
}
}
prevTag = fragment.tag;
if (reset !== undefined) {
slices.push(slice);
slice = { style: styles[reset] ? reset : style, fragments: [] };
}
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;
} else {
slice.fragments.push(fragment);
}
}
}
slices.push(slice);
return Object.assign({ alignment: alignment, slices: slices }, q, 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,
style: 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(Object.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: 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];
}
var num = parseInt(color, 10);
if (!isNaN(num)) {
var min = -2147483648;
var max = 2147483647;
if (num < min) {
return ['00', '000000'];
}
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 defaultStyle = ref.defaultStyle;
var result = {};
var styles = [Object.assign({}, defaultStyle, { Name: 'Default' })].concat(style);
var loop = function ( i ) {
var s = Object.assign({}, DEFAULT_STYLE, styles[i]);
// this behavior is same as Aegisub by black-box testing
if (/^(\*+)Default$/.test(s.Name)) {
s.Name = 'Default';
}
Object.keys(s).forEach(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,
fe: s.Encoding,
// TODO: [breaking change] remove `q` from style
q: /^[0-3]$/.test(info.WrapStyle) ? info.WrapStyle * 1 : 2,
};
result[s.Name] = { style: s, tag: tag };
};
for (var i = 0; i < styles.length; i++) loop( i );
return result;
}
function compile(text, options) {
if ( options === undefined ) options = {};
var tree = parse(text);
var info = Object.assign(options.defaultInfo || {}, tree.info);
var styles = compileStyles({
info: info,
style: tree.styles.style,
defaultStyle: options.defaultStyle || {},
});
return {
info: info,
width: info.PlayResX * 1 || null,
height: info.PlayResY * 1 || null,
wrapStyle: /^[0-3]$/.test(info.WrapStyle) ? info.WrapStyle * 1 : 2,
collisions: info.Collisions || 'Normal',
styles: styles,
dialogues: compileDialogues({
styles: styles,
dialogues: tree.events.dialogue,
}),
};
}
// https://github.com/weizhenye/ASS/wiki/Font-Size-in-ASS
const useTextMetrics = 'fontBoundingBoxAscent' in TextMetrics.prototype;
// It seems max line-height is 1200px in Firefox.
const isFirefox = navigator.userAgent.toLowerCase().includes('firefox');
const unitsPerEm = !useTextMetrics && isFirefox ? 512 : 2048;
const lineSpacing = Object.create(null);
const ctx = document.createElement('canvas').getContext('2d');
const $div = document.createElement('div');
$div.className = 'ASS-fix-font-size';
$div.style.fontSize = `${unitsPerEm}px`;
const $span = document.createElement('span');
$span.textContent = '0';
$div.append($span);
const $fixFontSize = useTextMetrics ? null : $div;
function getRealFontSize(fn, fs) {
if (!lineSpacing[fn]) {
if (useTextMetrics) {
ctx.font = `${unitsPerEm}px "${fn}"`;
const tm = ctx.measureText('');
lineSpacing[fn] = tm.fontBoundingBoxAscent + tm.fontBoundingBoxDescent;
} else {
$span.style.fontFamily = `"${fn}"`;
lineSpacing[fn] = $span.clientHeight;
}
}
return fs * unitsPerEm / lineSpacing[fn];
}
var GLOBAL_CSS = '.ASS-box{pointer-events:none;font-family:Arial;position:absolute;overflow:hidden}.ASS-dialogue{z-index:0;width:max-content;transform:translate(calc(var(--ass-align-h)*-1),calc(var(--ass-align-v)*-1));font-size:0;position:absolute}.ASS-dialogue span{display:inline-block}.ASS-dialogue [data-text]{color:var(--ass-fill-color);font-size:calc(var(--ass-scale)*var(--ass-real-fs)*1px);line-height:calc(var(--ass-scale)*var(--ass-tag-fs)*1px);letter-spacing:calc(var(--ass-scale)*var(--ass-tag-fsp)*1px);filter:blur(calc(var(--ass-scale-stroke)*var(--ass-tag-blur)*(1 - round(up,sin(var(--ass-tag-xbord))*sin(var(--ass-tag-xbord))))*(1 - round(up,sin(var(--ass-tag-ybord))*sin(var(--ass-tag-ybord))))*1px));display:inline-block}.ASS-dialogue [data-is=br]+[data-is=br]{height:calc(var(--ass-scale)*var(--ass-tag-fs)*1px/2)}.ASS-dialogue[data-wrap-style="0"],.ASS-dialogue[data-wrap-style="3"]{text-wrap:balance;white-space:pre-wrap}.ASS-dialogue[data-wrap-style="1"]{word-break:break-word;white-space:pre-wrap}.ASS-dialogue[data-wrap-style="2"]{word-break:normal;white-space:pre}.ASS-dialogue [data-border-style="1"]{position:relative}.ASS-dialogue [data-border-style="1"]:before,.ASS-dialogue [data-border-style="1"]:after{content:attr(data-text);z-index:-1;filter:blur(calc(var(--ass-scale-stroke)*var(--ass-tag-blur)*1px));position:absolute;top:0;left:0}.ASS-dialogue [data-border-style="1"]:before{color:var(--ass-shadow-color);-webkit-text-stroke:calc(var(--ass-scale-stroke)*var(--ass-border-width)*1px)var(--ass-shadow-color);transform:translate(calc(var(--ass-scale-stroke)*var(--ass-tag-xshad)*1px),calc(var(--ass-scale-stroke)*var(--ass-tag-yshad)*1px))}.ASS-dialogue [data-border-style="1"]:after{color:var(--ass-border-color);-webkit-text-stroke:calc(var(--ass-scale-stroke)*var(--ass-border-width)*1px)var(--ass-border-color)}.ASS-dialogue [data-border-style="1"][data-stroke=svg]{color:#000}.ASS-dialogue [data-border-style="1"][data-stroke=svg]:before,.ASS-dialogue [data-border-style="1"][data-stroke=svg]:after{opacity:0}@container style(--ass-tag-xbord:0) and style(--ass-tag-ybord:0){.ASS-dialogue [data-border-style="1"]:after{display:none}}@container style(--ass-tag-xshad:0) and style(--ass-tag-yshad:0){.ASS-dialogue [data-border-style="1"]:before{display:none}}.ASS-dialogue [data-border-style="3"]{padding:calc(var(--ass-scale-stroke)*var(--ass-tag-xbord)*1px)calc(var(--ass-scale-stroke)*var(--ass-tag-ybord)*1px);filter:blur(calc(var(--ass-scale-stroke)*var(--ass-tag-blur)*1px));position:relative}.ASS-dialogue [data-border-style="3"]:before,.ASS-dialogue [data-border-style="3"]:after{content:"";z-index:-1;width:100%;height:100%;position:absolute}.ASS-dialogue [data-border-style="3"]:before{background-color:var(--ass-shadow-color);left:calc(var(--ass-scale-stroke)*var(--ass-tag-xshad)*1px);top:calc(var(--ass-scale-stroke)*var(--ass-tag-yshad)*1px)}.ASS-dialogue [data-border-style="3"]:after{background-color:var(--ass-border-color);top:0;left:0}@container style(--ass-tag-xbord:0) and style(--ass-tag-ybord:0){.ASS-dialogue [data-border-style="3"]:after{background-color:#0000}}@container style(--ass-tag-xshad:0) and style(--ass-tag-yshad:0){.ASS-dialogue [data-border-style="3"]:before{background-color:#0000}}.ASS-dialogue [data-rotate]{transform:perspective(312.5px)rotateY(calc(var(--ass-tag-fry)*1deg))rotateX(calc(var(--ass-tag-frx)*1deg))rotateZ(calc(var(--ass-tag-frz)*-1deg))}.ASS-dialogue [data-rotate][data-text]{transform-style:preserve-3d;word-break:normal;white-space:nowrap}.ASS-dialogue [data-scale],.ASS-dialogue [data-skew]{transform:scale(var(--ass-tag-fscx),var(--ass-tag-fscy))skew(calc(var(--ass-tag-fax)*57.2958deg),calc(var(--ass-tag-fay)*57.2958deg));transform-origin:var(--ass-align-h)var(--ass-align-v);display:inline-block}.ASS-fix-font-size{visibility:hidden;width:0;height:0;font-family:Arial;line-height:normal;position:absolute;overflow:hidden}.ASS-fix-font-size span{position:absolute}.ASS-clip-area{width:100%;height:100%;position:absolute;top:0;left:0}.ASS-effect-area{width:100%;height:fit-content;display:flex;position:absolute;overflow:hidden;mask-composite:intersect}.ASS-effect-area[data-effect=banner]{flex-direction:column;height:100%}.ASS-effect-area .ASS-dialogue{position:static;transform:none}';
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],
);
}
return $el;
}
/**
* @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 initAnimation($el, keyframes, options) {
const animation = $el.animate(keyframes, options);
animation.pause();
return animation;
}
function batchAnimate(dia, action) {
(dia.animations || []).forEach((animation) => {
animation[action]();
});
}
const rotateTags = ['frx', 'fry', 'frz'];
const scaleTags = ['fscx', 'fscy'];
const skewTags = ['fax', 'fay'];
function createTransform(tag) {
return [
...[...rotateTags, ...skewTags].map((x) => ([`--ass-tag-${x}`, `${tag[x] || 0}`])),
...scaleTags.map((x) => ([`--ass-tag-${x}`, tag.p ? 1 : (tag[x] || 100) / 100])),
];
}
function setTransformOrigin(dialogue, scale) {
const { align, width, height, x, y, $div } = dialogue;
const orgX = (dialogue.org ? dialogue.org.x * scale : x) + [0, width / 2, width][align.h];
const orgY = (dialogue.org ? dialogue.org.y * scale : y) + [height, height / 2, 0][align.v];
for (let i = $div.childNodes.length - 1; i >= 0; i -= 1) {
const node = $div.childNodes[i];
if (node.dataset.rotate === '') {
// It's not extremely precise for offsets are round the value to an integer.
const tox = orgX - x - node.offsetLeft;
const toy = orgY - y - node.offsetTop;
node.style.cssText += `transform-origin:${tox}px ${toy}px;`;
}
}
}
const strokeTags = ['blur', 'xbord', 'ybord', 'xshad', 'yshad'];
if (window.CSS.registerProperty) {
[
'real-fs', 'tag-fs', 'tag-fsp', 'border-width',
...[...strokeTags, ...rotateTags, ...skewTags].map((tag) => `tag-${tag}`),
].forEach((k) => {
window.CSS.registerProperty({
name: `--ass-${k}`,
syntax: '<number>',
inherits: true,
initialValue: 0,
});
});
[
'border-opacity', 'shadow-opacity',
...scaleTags.map((tag) => `tag-${tag}`),
].forEach((k) => {
window.CSS.registerProperty({
name: `--ass-${k}`,
syntax: '<number>',
inherits: true,
initialValue: 1,
});
});
['fill-color', 'border-color', 'shadow-color'].forEach((k) => {
window.CSS.registerProperty({
name: `--ass-${k}`,
syntax: '<color>',
inherits: true,
initialValue: 'transparent',
});
});
}
function createEffect(effect, duration) {
// TODO: when effect and move both exist, its behavior is weird, for now only move works.
const { name, delay, leftToRight } = effect;
const translate = name === 'banner' ? 'X' : 'Y';
const dir = ({
X: leftToRight ? 1 : -1,
Y: /up/.test(name) ? -1 : 1,
})[translate];
const start = -100 * dir;
// speed is 1000px/s when delay=1
const distance = (duration / (delay || 1)) * dir;
const keyframes = [
{ offset: 0, transform: `translate${translate}(${start}%)` },
{ offset: 1, transform: `translate${translate}(calc(${start}% + var(--ass-scale) * ${distance}px))` },
];
return [keyframes, { duration, fill: 'forwards' }];
}
function multiplyScale(v) {
return `calc(var(--ass-scale) * ${v}px)`;
}
function createMove(move, duration) {
const { x1, y1, x2, y2, t1, t2 } = move;
const start = `translate(${multiplyScale(x1)}, ${multiplyScale(y1)})`;
const end = `translate(${multiplyScale(x2)}, ${multiplyScale(y2)})`;
const moveDuration = Math.max(t2, duration);
const keyframes = [
{ offset: 0, transform: start },
t1 > 0 ? { offset: t1 / moveDuration, transform: start } : null,
(t2 > 0 && t2 < duration) ? { offset: t2 / moveDuration, transform: end } : null,
{ offset: 1, transform: end },
].filter(Boolean);
const options = { duration: moveDuration, fill: 'forwards' };
return [keyframes, options];
}
function createFadeList(fade, duration) {
const { type, a1, a2, a3, t1, t2, t3, t4 } = fade;
// \fad(<t1>, <t2>)
if (type === 'fad') {
// For example dialogue starts at 0 and ends at 5000 with \fad(4000, 4000)
// * <t1> means opacity from 0 to 1 in (0, 4000)
// * <t2> means opacity from 1 to 0 in (1000, 5000)
// <t1> and <t2> are overlaped in (1000, 4000), <t1> will take affect
// so the result is:
// * opacity from 0 to 1 in (0, 4000)
// * opacity from 0.25 to 0 in (4000, 5000)
const t1Keyframes = [{ offset: 0, opacity: 0 }, { offset: 1, opacity: 1 }];
const t2Keyframes = [{ offset: 0, opacity: 1 }, { offset: 1, opacity: 0 }];
return [
[t2Keyframes, { duration: t2, delay: duration - t2, fill: 'forwards' }],
[t1Keyframes, { duration: t1, composite: 'replace' }],
];
}
// \fade(<a1>, <a2>, <a3>, <t1>, <t2>, <t3>, <t4>)
const fadeDuration = Math.max(duration, t4);
const opacities = [a1, a2, a3].map((a) => 1 - a / 255);
const offsets = [0, t1, t2, t3, t4].map((t) => t / fadeDuration);
const keyframes = offsets.map((t, i) => ({ offset: t, opacity: opacities[i >> 1] }));
return [
[keyframes, { duration: fadeDuration, fill: 'forwards' }],
];
}
function createAnimatableVars(tag) {
return [
['real-fs', getRealFontSize(tag.fn, tag.fs)],
['tag-fs', tag.fs],
['tag-fsp', tag.fsp],
['fill-color', color2rgba(tag.a1 + tag.c1)],
]
.filter(([, v]) => v)
.map(([k, v]) => [`--ass-${k}`, v]);
}
// use linear() to simulate accel
function getEasing(duration, accel) {
if (accel === 1) return 'linear';
// 60fps
const frames = Math.ceil(duration / 1000 * 60);
const points = Array.from({ length: frames + 1 })
.map((_, i) => (i / frames) ** accel);
return `linear(${points.join(',')})`;
}
function createDialogueAnimations(el, dialogue) {
const { start, end, effect, move, fade } = dialogue;
const duration = (end - start) * 1000;
return [
effect && !move ? createEffect(effect, duration) : null,
move ? createMove(move, duration) : null,
...(fade ? createFadeList(fade, duration) : []),
]
.filter(Boolean)
.map(([keyframes, options]) => initAnimation(el, keyframes, options));
}
function createTagKeyframes(fromTag, tag, key) {
const value = tag[key];
if (value === undefined) return [];
if (key === 'clip') return [];
if (key === 'a1' || key === 'c1') {
return [['fill-color', color2rgba((tag.a1 || fromTag.a1) + (tag.c1 || fromTag.c1))]];
}
if (key === 'a3' || key === 'c3') {
return [['border-color', color2rgba((tag.a3 || fromTag.a3) + (tag.c3 || fromTag.c3))]];
}
if (key === 'a4' || key === 'c4') {
return [['shadow-color', color2rgba((tag.a4 || fromTag.a4) + (tag.c4 || fromTag.c4))]];
}
if (key === 'fs') {
return [
['real-fs', getRealFontSize(tag.fn || fromTag.fn, tag.fs)],
['tag-fs', value],
];
}
if (key === 'fscx' || key === 'fscy') {
return [[`tag-${key}`, (value || 100) / 100]];
}
if (key === 'xbord' || key === 'ybord') {
return [['border-width', value * 2]];
}
return [[`tag-${key}`, value]];
}
function createTagAnimations(el, fragment, sliceTag) {
const fromTag = { ...sliceTag, ...fragment.tag };
return (fragment.tag.t || []).map(({ t1, t2, accel, tag }) => {
const keyframe = Object.fromEntries(
Object.keys(tag)
.flatMap((key) => createTagKeyframes(fromTag, tag, key))
.map(([k, v]) => [`--ass-${k}`, v])
// .concat(tag.clip ? [['clipPath', ]] : [])
.concat([['offset', 1]]),
);
const duration = Math.max(0, t2 - t1);
return initAnimation(el, [keyframe], {
duration,
delay: t1,
fill: 'forwards',
easing: getEasing(duration, accel),
});
});
}
function createClipAnimations(el, dialogue, store) {
return dialogue.slices
.flatMap((slice) => slice.fragments)
.flatMap((fragment) => fragment.tag.t || [])
.filter(({ tag }) => tag.clip)
.map(({ t1, t2, accel, tag }) => {
const keyframe = {
offset: 1,
clipPath: createRectClip(tag.clip, store.scriptRes.width, store.scriptRes.height),
};
const duration = Math.max(0, t2 - t1);
return initAnimation(el, [keyframe], {
duration,
delay: t1,
fill: 'forwards',
easing: getEasing(duration, accel),
});
});
}
// eslint-disable-next-line import/no-cycle
function createRectClip(clip, sw, sh) {
if (!clip.dots) return '';
const { x1, y1, x2, y2 } = clip.dots;
const polygon = [[x1, y1], [x1, y2], [x2, y2], [x2, y1], [x1, y1]]
.map(([x, y]) => [x / sw, y / sh])
.concat(clip.inverse ? [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]] : [])
.map((pair) => pair.map((n) => `${n * 100}%`).join(' '))
.join(',');
return `polygon(evenodd, ${polygon})`;
}
function createPathClip(clip, sw, sh, store) {
if (!clip.drawing) return '';
const scale = store.scale / (1 << (clip.scale - 1));
let d = clip.drawing.instructions.map(({ type, points }) => (
type + points.map(({ x, y }) => `${x * scale},${y * scale}`).join(',')
)).join('');
if (clip.inverse) {
d += `M0,0L0,${sh},${sw},${sh},${sw},0,0,0Z`;
}
return `path(evenodd, "${d}")`;
}
function getClipPath(dialogue, store) {
const { clip, animations } = dialogue;
if (!clip) return {};
const { width, height } = store.scriptRes;
const $clipArea = document.createElement('div');
store.box.insertBefore($clipArea, dialogue.$div);
$clipArea.append(dialogue.$div);
$clipArea.className = 'ASS-clip-area';
$clipArea.style.zIndex = dialogue.$div.style.zIndex;
$clipArea.style.clipPath = clip.dots
? createRectClip(clip, width, height)
: createPathClip(clip, width, height, store);
animations.push(...createClipAnimations($clipArea, dialogue, store));
return { $div: $clipArea };
}
function createStrokeFilter(tag, scale) {
const id = `ASS-${uuid()}`;
const hasBorder = tag.xbord || tag.ybord;
const hasShadow = tag.xshad || tag.yshad;
const isOpaque = (tag.a1 || '00').toLowerCase() !== 'ff';
const blur = (tag.blur || tag.be || 0) * scale;
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', 'var(--ass-fill-color)'],
['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', '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', 'var(--ass-border-color)'],
['result', 'c3'],
]));
$filter.append(createSVGEl('feComposite', [
['operator', 'in'],
['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 ? (isOpaque ? 'dil' : 'dil_b_o') : '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', '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', 'var(--ass-shadow-color)'],
['result', 'c4'],
]));
$filter.append(createSVGEl('feComposite', [
['operator', 'in'],
['in', 'c4'],
['in2', isOpaque ? 'off_b' : 'off_b_o'],
['result', 'shadow'],
]));
}
const $merge = createSVGEl('feMerge', []);
if (hasShadow && (hasBorder || isOpaque)) {
$merge.append(createSVGEl('feMergeNode', [['in', 'shadow']]));
}
if (hasBorder) {
$merge.append(createSVGEl('feMergeNode', [['in', 'border']]));
}
$merge.append(createSVGEl('feMergeNode', [['in', 'main']]));
$filter.append($merge);
return { id, el: $filter };
}
function createStrokeVars(tag) {
return [
['border-width', tag.xbord * 2],
['border-color', color2rgba(`${tag.a3}${tag.c3}`)],
['shadow-color', color2rgba(`${tag.a4}${tag.c4}`)],
['tag-blur', tag.blur || tag.be || 0],
['tag-xbord', tag.xbord],
['tag-ybord', tag.ybord],
['tag-xshad', tag.xshad],
['tag-yshad', tag.yshad],
].map(([k, v]) => [`--ass-${k}`, v]);
}
function createDrawing(fragment, styleTag, store) {
if (!fragment.drawing.d) return null;
const tag = { ...styleTag, ...fragment.tag };
const { minX, minY, width, height } = fragment.drawing;
const baseScale = store.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 = store.sbas ? store.scale : 1;
const $defs = createSVGEl('defs');
const filter = createStrokeFilter(tag, strokeScale);
$defs.append(filter.el);
$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(#${filter.id})`],
]));
$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;`,
};
}
function encodeText(text, q) {
return text
.replace(/\\h/g, ' ')
.replace(/\\N/g, '\n')
.replace(/\\n/g, q === 2 ? '\n' : ' ');
}
function createDialogue(dialogue, store) {
const { styles } = store;
const $div = document.createElement('div');
$div.className = 'ASS-dialogue';
$div.dataset.wrapStyle = dialogue.q;
const df = document.createDocumentFragment();
const { align, slices } = dialogue;
[
['--ass-align-h', ['0%', '50%', '100%'][align.h]],
['--ass-align-v', ['100%', '50%', '0%'][align.v]],
].forEach(([k, v]) => {
$div.style.setProperty(k, v);
});
const 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 = '';
const cssVars = [];
cssVars.push(...createStrokeVars(tag));
let stroke = null;
const hasStroke = tag.xbord || tag.ybord || tag.xshad || tag.yshad;
if (hasStroke && (drawing || tag.a1 !== '00' || tag.xbord !== tag.ybord)) {
const filter = createStrokeFilter(tag, store.sbas ? store.scale : 1);
const svg = createSVGEl('svg', [['width', 0], ['height', 0]]);
svg.append(filter.el);
stroke = { id: filter.id, el: svg };
}
cssVars.push(...createAnimatableVars(tag));
if (!drawing) {
cssText += `font-family:"${tag.fn}";`;
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' : ''};` : '';
}
if (drawing && tag.pbo) {
const pbo = -tag.pbo * (tag.fscy || 100) / 100;
cssText += `vertical-align:calc(var(--ass-scale) * ${pbo}px);`;
}
cssVars.push(...createTransform(tag));
cons