Acfun爱咔-ASS字幕加载

AI生成的爱咔加载ass字幕脚本 视频右键选择ass字幕文件

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Acfun爱咔-ASS字幕加载
// @namespace    http://tampermonkey.net/
// @version      1.3.3
// @description  AI生成的爱咔加载ass字幕脚本 视频右键选择ass字幕文件
// @author       You
// @icon         https://www.google.com/s2/favicons?sz=64&domain=acfun.cn
// @match        https://onvideo.kuaishou.com/vangogh/editor/*?source=ac
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';
//============================================
//https://cdn.jsdelivr.net/npm/[email protected]/dist/ass.global.js
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 end = `translate(-100%, ${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));
        const tags = [tag, ...(tag.t || []).map((t) => t.tag)];
        const hasRotate = rotateTags.some((x) => tags.some((t) => t[x]));
        const hasScale = scaleTags.some((x) => tags.some((t) => t[x] !== undefined && t[x] !== 100));
        const hasSkew = skewTags.some((x) => tags.some((t) => t[x]));

        encodeText(text, dialogue.q).split('\n').forEach((content, idx) => {
          const $span = document.createElement('span');
          const $ssspan = document.createElement('span');
          if (hasScale || hasSkew) {
            if (hasScale) {
              $ssspan.dataset.scale = '';
            }
            if (hasSkew) {
              $ssspan.dataset.skew = '';
            }
            $ssspan.textContent = content;
          }
          if (hasRotate) {
            $span.dataset.rotate = '';
          }
          if (drawing) {
            $span.dataset.drawing = '';
            const obj = createDrawing(fragment, sliceTag, store);
            if (!obj) return;
            $span.style.cssText = obj.cssText;
            $span.append(obj.$svg);
          } else {
            if (idx) {
              const br = document.createElement('div');
              br.dataset.is = 'br';
              br.style.setProperty('--ass-tag-fs', tag.fs);
              df.append(br);
            }
            if (!content) return;
            if (hasScale || hasSkew) {
              $span.append($ssspan);
            } else {
              $span.textContent = content;
            }
            const el = hasScale || hasSkew ? $ssspan : $span;
            el.dataset.text = content;
            if (hasStroke) {
              el.dataset.borderStyle = borderStyle;
              el.dataset.stroke = 'css';
            }
            if (stroke) {
              el.dataset.stroke = 'svg';
              // TODO: it doesn't support animation
              el.style.filter = `url(#${stroke.id})`;
              el.append(stroke.el);
            }
          }
          $span.style.cssText += cssText;
          cssVars.forEach(([k, v]) => {
            $span.style.setProperty(k, v);
          });
          animations.push(...createTagAnimations($span, fragment, sliceTag));
          df.append($span);
        });
      });
    });
    animations.push(...createDialogueAnimations($div, dialogue));
    $div.append(df);
    return { $div, animations };
  }

  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 (
        (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;
      }
    }
    if (alignment > 3) {
      result -= height - 1;
    }
    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 { move, align, width, height, margin, slices } = dialogue;
    let x = 0;
    let y = 0;
    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(({ keyframes }) => keyframes?.length)
      ));
      y = hasT
        ? [
          store.height - height - margin.vertical,
          (store.height - height) / 2,
          margin.vertical,
        ][align.v]
        : allocate(dialogue, store);
    }
    return {
      x: x + [0, width / 2, width][align.h],
      y: y + [height, height / 2, 0][align.v],
    };
  }

  function createStyle(dialogue) {
    const { layer, align, effect, pos, margin, q } = dialogue;
    let cssText = '';
    if (layer) cssText += `z-index:${layer};`;
    cssText += `text-align:${['left', 'center', 'right'][align.h]};`;
    if (!effect) {
      if (q !== 2) {
        cssText += `max-width:calc(100% - var(--ass-scale) * ${margin.left + margin.right}px);`;
      }
      if (!pos) {
        if (align.h !== 0) {
          cssText += `padding-right:calc(var(--ass-scale) * ${margin.right}px);`;
        }
        if (align.h !== 2) {
          cssText += `padding-left:calc(var(--ass-scale) * ${margin.left}px);`;
        }
      }
    }
    return cssText;
  }

  function setEffect(dialogue, store) {
    const $area = document.createElement('div');
    $area.className = 'ASS-effect-area';
    store.box.insertBefore($area, dialogue.$div);
    $area.append(dialogue.$div);
    const { width, height } = store.scriptRes;
    const { name, y1, y2, leftToRight, fadeAwayWidth, fadeAwayHeight } = dialogue.effect;
    const min = Math.min(y1, y2);
    const max = Math.max(y1, y2);
    $area.dataset.effect = name;
    if (name === 'banner') {
      $area.style.alignItems = leftToRight ? 'flex-start' : 'flex-end';
      $area.style.justifyContent = ['flex-end', 'center', 'flex-start'][dialogue.align.v];
    }
    if (name.startsWith('scroll')) {
      const top = min / height * 100;
      const bottom = (height - max) / height * 100;
      $area.style.cssText = `top:${top}%;bottom:${bottom}%;`;
      $area.style.justifyContent = ['flex-start', 'center', 'flex-end'][dialogue.align.h];
    }
    if (fadeAwayHeight) {
      const p = fadeAwayHeight / (max - min) * 100;
      $area.style.maskImage = [
        `linear-gradient(#000 ${100 - p}%, transparent)`,
        `linear-gradient(transparent, #000 ${p}%)`,
      ].join(',');
    }
    if (fadeAwayWidth) {
      const p = fadeAwayWidth / width * 100;
      // only left side has fade away effect in VSFilter
      $area.style.maskImage = `linear-gradient(90deg, transparent, #000 ${p}%)`;
    }
    return $area;
  }

  function renderer(dialogue, store) {
    const { $div, animations } = createDialogue(dialogue, store);
    Object.assign(dialogue, { $div, animations });
    store.box.append($div);
    const { width } = $div.getBoundingClientRect();
    Object.assign(dialogue, { width });
    $div.style.cssText += createStyle(dialogue);
    // 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 += `left:${x}px;top:${y}px;`;
    setTransformOrigin(dialogue, store.scale);
    // TODO: refactor to create .clip-area or .effect-area wrappers in `createDialogue`
    Object.assign(dialogue, getClipPath(dialogue, store));
    if (dialogue.effect) {
      Object.assign(dialogue, { $div: setEffect(dialogue, store) });
    }
    return dialogue;
  }

  /* eslint-disable no-param-reassign */

  function clear(store) {
    const { box } = store;
    while (box.lastChild) {
      box.lastChild.remove();
    }
    store.actives = [];
    store.space = [];
  }

  function framing(store, mediaTime) {
    const { dialogues, actives } = store;
    const vct = mediaTime - store.delay;
    for (let i = actives.length - 1; i >= 0; i -= 1) {
      const dia = actives[i];
      const { end } = dia;
      if (end < vct) {
        dia.$div.remove();
        actives.splice(i, 1);
      }
    }
    while (
      store.index < dialogues.length
      && vct >= dialogues[store.index].start
    ) {
      if (vct < dialogues[store.index].end) {
        const dia = renderer(dialogues[store.index], store);
        (dia.animations || []).forEach((animation) => {
          animation.currentTime = (vct - dia.start) * 1000;
        });
        actives.push(dia);
        if (!store.video.paused) {
          batchAnimate(dia, 'play');
        }
      }
      store.index += 1;
    }
  }

  function createSeek(store) {
    return function seek() {
      clear(store);
      const { video, dialogues } = store;
      const vct = video.currentTime - store.delay;
      store.index = (() => {
        for (let i = 0; i < dialogues.length; i += 1) {
          if (vct < dialogues[i].end) {
            return i;
          }
        }
        return (dialogues.length || 1) - 1;
      })();
      framing(store, video.currentTime);
    };
  }

  function createFrame(video) {
    const useVFC = video.requestVideoFrameCallback;
    return [
      useVFC ? video.requestVideoFrameCallback.bind(video) : requestAnimationFrame,
      useVFC ? video.cancelVideoFrameCallback.bind(video) : cancelAnimationFrame,
    ];
  }

  function createPlay(store) {
    const { video } = store;
    const [requestFrame, cancelFrame] = createFrame(video);
    return function play() {
      const frame = (now, metadata) => {
        framing(store, metadata?.mediaTime || video.currentTime);
        store.requestId = requestFrame(frame);
      };
      cancelFrame(store.requestId);
      store.requestId = requestFrame(frame);
      store.actives.forEach((dia) => {
        batchAnimate(dia, 'play');
      });
    };
  }

  function createPause(store) {
    const [, cancelFrame] = createFrame(store.video);
    return function pause() {
      cancelFrame(store.requestId);
      store.requestId = 0;
      store.actives.forEach((dia) => {
        batchAnimate(dia, 'pause');
      });
    };
  }

  function createResize(that, store) {
    const { video, box, layoutRes } = store;
    return function resize() {
      const cw = video.clientWidth;
      const ch = video.clientHeight;
      const vw = layoutRes.width || video.videoWidth || cw;
      const vh = layoutRes.height || 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 (that.resampling === 'video_height') {
        rw = sh / vh * vw;
      }
      store.scale = Math.min(cw / rw, ch / rh);
      if (that.resampling === 'script_width') {
        store.scale = videoScale * (vw / rw);
      }
      if (that.resampling === 'script_height') {
        store.scale = videoScale * (vh / rh);
      }
      const bw = store.scale * rw;
      const bh = store.scale * rh;
      store.width = bw;
      store.height = bh;
      store.resampledRes = { width: rw, height: rh };

      box.style.cssText = `width:${bw}px;height:${bh}px;top:${(ch - bh) / 2}px;left:${(cw - bw) / 2}px;`;
      box.style.setProperty('--ass-scale', store.scale);
      box.style.setProperty('--ass-scale-stroke', store.sbas ? store.scale : 1);

      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'),
      /**
       * video resize observer
       * @type {ResizeObserver}
       */
      observer: null,
      scale: 1,
      width: 0,
      height: 0,
      /** resolution from ASS file, it's PlayResX and PlayResY */
      scriptRes: {},
      /** resolution from ASS file, it's LayoutResX and LayoutResY */
      layoutRes: {},
      /** resolution after resampling */
      resampledRes: {},
      /** current index of dialogues to match currentTime */
      index: 0,
      /** @type {boolean} ScaledBorderAndShadow */
      sbas: true,
      /** @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,
    };

    #play;

    #pause;

    #seek;

    #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
     * <div id="container" style="position: relative;">
     *   <video
     *     id="video"
     *     src="./example.mp4"
     *     style="position: absolute; width: 100%; height: 100%;"
     *   ></video>
     *   <!-- ASS will be added here -->
     * </div>
     * ```
     *
     * 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.sbas = /yes/i.test(info.ScaledBorderAndShadow);
      this.#store.layoutRes = {
        width: info.LayoutResX * 1 || video.videoWidth || video.clientWidth,
        height: info.LayoutResY * 1 || video.videoHeight || video.clientHeight,
      };
      this.#store.scriptRes = {
        width: width || this.#store.layoutRes.width,
        height: height || this.#store.layoutRes.height,
      };
      this.#store.styles = styles;
      this.#store.dialogues = dialogues.map((dia) => Object.assign(dia, {
        effect: ['banner', 'scroll up', 'scroll down'].includes(dia.effect?.name) ? dia.effect : null,
        align: {
          // 0: left, 1: center, 2: right
          h: (dia.alignment + 2) % 3,
          // 0: bottom, 1: center, 2: top
          v: Math.trunc((dia.alignment - 1) / 3),
        },
      }));

      if ($fixFontSize) {
        container.append($fixFontSize);
      }

      const { box } = this.#store;
      box.className = 'ASS-box';
      container.append(box);

      addGlobalStyle(container);

      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);

      this.#resize = createResize(this, this.#store);
      this.#resize();
      this.resampling = resampling;

      const observer = new ResizeObserver(this.#resize);
      observer.observe(video);
      this.#store.observer = observer;

      return this;
    }

    /**
     * Desctroy the ASS instance
     * @returns {ASS}
     */
    destroy() {
      const { video, box, 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);

      if ($fixFontSize) {
        $fixFontSize.remove();
      }
      box.remove();
      observer.unobserve(this.#store.video);

      this.#store.styles = {};
      this.#store.dialogues = [];

      return this;
    }

    /**
     * Show subtitles in the container
     * @returns {ASS}
     */
    show() {
      this.#store.box.style.visibility = 'visible';
      return this;
    }

    /**
     * Hide subtitles in the container
     * @returns {ASS}
     */
    hide() {
      this.#store.box.style.visibility = 'hidden';
      return this;
    }

    #resampling = 'video_height';

    /** @type {ASSOption['resampling']} */
    get resampling() {
      return this.#resampling;
    }

    set resampling(r) {
      if (r === this.#resampling) return;
      if (/^(video|script)_(width|height)$/.test(r)) {
        this.#resampling = r;
        this.#resize();
      }
    }

    /** @type {number} Subtitle delay in seconds. */
    get delay() {
      return this.#store.delay;
    }

    set delay(d) {
      if (typeof d !== 'number') return;
      this.#store.delay = d;
      this.#seek();
    }

    // addDialogue(dialogue) {
    // }
  }

  return ASS;

})();
//============================================
    let ass = null;
    let content = '';
    let currentVideo = null;
    let subtitleContainer = null;
    const config = {
        fontSize: 1.0,
        timeOffset: 0,
        enabled: true,
        resampling: 'video_height'
    };

    // 初始化
    function init() {
        createUI();
        observeVideoElements();
        registerMenuCommands();
        console.log('ASS字幕播放器已初始化');
    }

    // 创建UI界面
    function createUI() {
        const style = document.createElement('style');
        style.textContent = `
            .ass-control-panel {
                position: fixed;
                top: 20px;
                right: 20px;
                background: rgba(0, 0, 0, 0.95);
                color: white;
                padding: 20px;
                border-radius: 10px;
                z-index: 2147483647;
                font-family: Arial, sans-serif;
                font-size: 14px;
                min-width: 300px;
                backdrop-filter: blur(10px);
                border: 1px solid rgba(255, 255, 255, 0.2);
                display: none;
                box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
            }

            .ass-drop-area {
                border: 2px dashed #666;
                border-radius: 8px;
                padding: 20px;
                text-align: center;
                margin-bottom: 15px;
                cursor: pointer;
                transition: border-color 0.3s;
            }

            .ass-drop-area:hover {
                border-color: #4fc3f7;
            }

            .ass-drop-area input {
                display: none;
            }

            .ass-controls {
                display: grid;
                gap: 10px;
            }

            .ass-btn {
                padding: 10px;
                border: none;
                border-radius: 5px;
                cursor: pointer;
                font-weight: bold;
                transition: background-color 0.3s;
            }

            .ass-btn-primary {
                background: #2196f3;
                color: white;
            }

            .ass-btn-secondly {
                background: #8521f3;
                color: white;
            }

            .ass-btn-success {
                background: #4caf50;
                color: white;
            }

            .ass-btn-danger {
                background: #f44336;
                color: white;
            }

            .ass-slider-group {
                margin-bottom: 15px;
            }

            .ass-slider-label {
                display: block;
                margin-bottom: 5px;
                color: #bbb;
            }

            .ass-close-btn {
                position: absolute;
                top: 10px;
                right: 10px;
                background: none;
                border: none;
                color: #ccc;
                font-size: 20px;
                cursor: pointer;
                width: 30px;
                height: 30px;
            }
        `;
        document.head.appendChild(style);

        const panel = document.createElement('div');
        panel.className = 'ass-control-panel';
        panel.id = 'ass-control-panel';
        panel.innerHTML = `
            <h3 style="margin: 0 0 15px 0; color: #4fc3f7;">ASS字幕控制</h3>

<!--             <div class="ass-drop-area" id="drop-video">
                <div>拖放视频文件或点击选择</div>
                <input type="file" accept="video/*">
            </div> -->

            <div class="ass-drop-area" id="drop-ass">
                <div>拖放ASS字幕文件或点击选择</div>
                <input type="file" accept=".ass,.srt,.vtt">
            </div>

            <div class="ass-controls">
<!--                 <div class="ass-slider-group">
                    <label class="ass-slider-label">字体大小: <span id="font-size-value">${config.fontSize.toFixed(1)}x</span></label>
                    <input type="range" id="font-size-slider" min="0.5" max="3.0" step="0.1" value="${config.fontSize}" style="width: 100%;">
                </div> -->

                <div class="ass-slider-group">
                    <label class="ass-slider-label">时间偏移: <span id="offset-value">${config.timeOffset}秒</span></label>
                    <input type="range" id="time-offset-slider" min="-30" max="30" step="1" value="${config.timeOffset}" style="width: 100%;">
                </div>

                <div style="display: flex; gap: 10px;">
                    <button class="ass-btn ass-btn-primary" id="ass-show">显示字幕</button>
                    <button class="ass-btn ass-btn-secondly" id="ass-hide">隐藏字幕</button>
                </div>

                <div style="display: flex; gap: 10px;">
<!--                     <button class="ass-btn ass-btn-success" id="ass-resize">调整大小</button> -->
                    <button class="ass-btn ass-btn-danger" id="ass-destroy">销毁实例</button>
                </div>

                <div>
                    <label class="ass-slider-label">重采样模式:</label>
                    <div style="display: flex; gap: 10px; margin-top: 5px;">
                        <label><input type="radio" name="resampling" value="video_width" ${config.resampling === 'video_width' ? 'checked' : ''}> 视频宽度</label>
                        <label><input type="radio" name="resampling" value="video_height" ${config.resampling === 'video_height' ? 'checked' : ''}> 视频高度</label>
                    </div>
                </div>
            </div>

            <button class="ass-close-btn" id="ass-close">×</button>
        `;
        document.body.appendChild(panel);

        setupEventListeners();
    }

    function setupEventListeners() {
        const panel = $('#ass-control-panel')[0];

        // 拖放区域事件
        // setupDropArea($('#drop-video')[0], loadVideoFile);
        setupDropArea($('#drop-ass')[0], loadASSFile);

        // 控制按钮事件
        $('#ass-show')[0].addEventListener('click', () => {
            if (ass) ass.show();
        });

        $('#ass-hide')[0].addEventListener('click', () => {
            if (ass) ass.hide();
        });

        // $('#ass-resize')[0].addEventListener('click', () => {
        //     const main = $('#main')[0];
        //     if (main) {
        //         main.dataset.size = main.dataset.size === 'lg' ? 'sm' : 'lg';
        //     }
        // });

        $('#ass-destroy')[0].addEventListener('click', () => {
            if (ass) {
                ass.destroy();
                ass = null;
            }
        });

        $('#ass-close')[0].addEventListener('click', () => {
            panel.style.display = 'none';
        });

        // // 滑块事件
        // $('#font-size-slider')[0].addEventListener('input', (e) => {
        //     config.fontSize = parseFloat(e.target.value);
        //     $('#font-size-value')[0].textContent = config.fontSize.toFixed(1) + 'x';
        //     updateFontSize();
        // });

        $('#time-offset-slider')[0].addEventListener('input', (e) => {
            config.timeOffset = parseInt(e.target.value);
            $('#offset-value')[0].textContent = config.timeOffset + '秒';
            if (ass) ass.delay = config.timeOffset;
        });

        // 重采样模式
        document.querySelectorAll('input[name="resampling"]').forEach(radio => {
            radio.addEventListener('change', (e) => {
                config.resampling = e.target.value;
                if (ass) ass.resampling = config.resampling;
            });
        });
    }

    function setupDropArea(element, callback) {
        const input = element.querySelector('input');

        element.addEventListener('click', () => {
            input.click();
        });

        element.addEventListener('dragover', (e) => {
            e.preventDefault();
            element.style.borderColor = '#4fc3f7';
        });

        element.addEventListener('dragleave', () => {
            element.style.borderColor = '#666';
        });

        element.addEventListener('drop', (e) => {
            e.preventDefault();
            element.style.borderColor = '#666';
            if (e.dataTransfer.files.length > 0) {
                callback(e.dataTransfer.files[0]);
            }
        });

        input.addEventListener('change', (e) => {
            if (e.target.files.length > 0) {
                callback(e.target.files[0]);
            }
        });
    }

//     function loadVideoFile(file) {
//         if (!file.type.startsWith('video/')) {
//             alert('请选择视频文件');
//             return;
//         }

//         const url = URL.createObjectURL(file);
//         setupVideo(url);
//     }

    function loadASSFile(file) {
        if (!file.name.toLowerCase().endsWith('.ass')) {
            alert('请选择ASS字幕文件');
            return;
        }

        file.text().then(text => {
            content = text;
            //$('#info')[0].textContent = content;
            initASS();
        }).catch(error => {
            console.error('读取ASS文件失败:', error);
            alert('读取字幕文件失败');
        });
    }

//     function setupVideo(src) {
//         // 清理现有视频
//         if (currentVideo) {
//             currentVideo.remove();
//         }

//         currentVideo = document.createElement('video');
//         currentVideo.controls = true;
//         currentVideo.muted = true;
//         currentVideo.src = src;

//         // 创建视频容器
//         const container = document.createElement('div');
//         container.style.position = 'relative';
//         container.style.display = 'inline-block';
//         container.id = 'video-container';

//         // 创建字幕容器
//         const subtitleContainer = document.createElement('div');
//         subtitleContainer.className = 'ass-subtitle-container';
//         subtitleContainer.style.cssText = `
//             position: absolute;
//             top: 0;
//             left: 0;
//             width: 100%;
//             height: 100%;
//             pointer-events: none;
//             z-index: 2147483646;
//         `;

//         container.appendChild(currentVideo);
//         container.appendChild(subtitleContainer);
//         document.body.appendChild(container);

//         currentVideo.addEventListener('loadedmetadata', () => {
//             console.log('视频加载完成');
//             if (content) {
//                 initASS();
//             }
//         });

//         currentVideo.addEventListener('error', () => {
//             console.error('视频加载失败');
//         });
//     }

    function initASS() {
        if (!currentVideo || !content) return;

        // 清理现有ASS实例
        if (ass) {
            try {
                ass.destroy();
            } catch (e) {
                console.warn('清理现有ASS实例时出错:', e);
            }
        }

        if (!subtitleContainer) {
            console.error('未找到字幕容器');
            return;
        }

        try {
            ass = new ASS(content, currentVideo, {
                container: subtitleContainer,
                resampling: config.resampling
            });

            // 应用配置
            ass.delay = config.timeOffset;
            updateFontSize();

            if (config.enabled) {
                ass.show();
            } else {
                ass.hide();
            }

            console.log('ASS实例创建成功', ass);

        } catch (error) {
            console.error('创建ASS实例失败:', error);
            alert('初始化字幕失败: ' + error.message);
        }
    }

    function updateFontSize() {
        if (!ass || !ass._store || !ass._store.box) return;

        try {
            ass._store.box.style.setProperty('--ass-scale', config.fontSize);
        } catch (error) {
            console.error('更新字体大小失败:', error);
        }
    }

    function observeVideoElements() {
        const observer = new MutationObserver((mutations) => {
            mutations.forEach((mutation) => {
                mutation.addedNodes.forEach((node) => {
                    if (node.nodeType === 1) {
                        const videos = node.querySelectorAll ? node.querySelectorAll('video') : [];
                        videos.forEach(video => {
                            if(currentVideo) return;
                            video.addEventListener('contextmenu', (e) => {
                                e.preventDefault();
                                $('#ass-control-panel')[0].style.display = 'block';
                            });
                            currentVideo = video;
                            currentVideo.parentNode
                            subtitleContainer = document.createElement('div');
                            subtitleContainer.className = 'ass-subtitle-container';
                            Object.assign(subtitleContainer.style, {
                                position: 'absolute',
                                top: '0',
                                left: '0',
                                width: '100%',
                                height: '100%',
                                pointerEvents: 'none',
                                zIndex: '2147483647'
                            });
                            currentVideo.parentNode.appendChild(subtitleContainer);
                        });
                    }
                });
            });
        });

        observer.observe(document.body, { childList: true, subtree: true });
    }

    function registerMenuCommands() {
        if (typeof GM_registerMenuCommand === 'function') {
            GM_registerMenuCommand('打开ASS控制面板', () => {
                $('#ass-control-panel')[0].style.display = 'block';
            });

            GM_registerMenuCommand('加载示例字幕', () => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: 'https://raw.githubusercontent.com/Aegisub/Aegisub/master/docs/specs/ass-format-tests.ass',
                    onload: function(response) {
                        content = response.responseText;
                        $('#info')[0].textContent = content;
                        if (currentVideo) {
                            initASS();
                        }
                    },
                    onerror: function(error) {
                        console.error('加载示例字幕失败:', error);
                    }
                });
            });
        }
    }

    // 工具函数
    function $(selector) {
        return document.querySelectorAll(selector);
    }

    // 添加快捷键
    document.addEventListener('keydown', (e) => {
        if (e.ctrlKey && e.key === ';') {
            e.preventDefault();
            const panel = $('#ass-control-panel')[0];
            panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
        }

        if (ass && currentVideo && e.code === 'Space') {
            e.preventDefault();
            if (currentVideo.paused) {
                currentVideo.play();
            } else {
                currentVideo.pause();
            }
        }
    });

    // 初始化
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();