diff --git a/.eslintrc.json b/.eslintrc.json
index 2cfb4eb..f63d71e 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -2,18 +2,33 @@
"env": {
"browser": true
},
- "extends": "airbnb-base",
+ "parserOptions": {
+ "ecmaVersion": "latest"
+ },
+ "extends": [
+ "airbnb-base",
+ "plugin:unicorn/recommended"
+ ],
+ "plugins": [
+ "unicorn"
+ ],
"rules": {
"no-bitwise": 0,
+ "no-constructor-return": 0,
"no-mixed-operators": 0,
- "no-plusplus": 0,
- "no-underscore-dangle": 0,
"object-curly-newline": ["error", {
"ObjectExpression": { "multiline": true, "consistent": true },
"ObjectPattern": { "multiline": true, "consistent": true }
}],
"import/extensions": ["error", "ignorePackages"],
"import/no-extraneous-dependencies": 0,
- "import/prefer-default-export": 0
+ "import/prefer-default-export": 0,
+ "unicorn/no-array-callback-reference": 0,
+ "unicorn/no-array-for-each": 0,
+ "unicorn/no-array-reduce": 0,
+ "unicorn/no-for-loop": 0,
+ "unicorn/no-null": 0,
+ "unicorn/prefer-string-replace-all": 0,
+ "unicorn/prevent-abbreviations": 0
}
}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 9e5bcae..98896c2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -12,7 +12,7 @@ jobs:
- name: Use Node.js
uses: actions/setup-node@v1
with:
- node-version: '14.x'
+ node-version: '22.x'
- name: Install Dependencies
run: npm i
- name: Build
diff --git a/.gitignore b/.gitignore
index aeb2ade..b9f9df1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
npm-debug.log*
node_modules
coverage
+types
package-lock.json
diff --git a/README.md b/README.md
index 5e3eb2d..0a78afb 100644
--- a/README.md
+++ b/README.md
@@ -7,8 +7,6 @@
[![File size](https://badgen.net/bundlephobia/minzip/assjs?icon=https://api.iconify.design/ant-design:file-zip-outline.svg?color=white)](https://bundlephobia.com/result?p=assjs)
[![jsDelivr](https://badgen.net/jsdelivr/hits/npm/assjs?icon=https://api.iconify.design/simple-icons:jsdelivr.svg?color=white)](https://www.jsdelivr.com/package/npm/assjs)
-[![Browser compatibility](https://saucelabs.com/browser-matrix/assjs.svg)](https://saucelabs.com/u/assjs)
-
ASS.js uses [ass-compiler](https://github.com/weizhenye/ass-compiler) to parse ASS subtitle file format, and then renders subtitles on HTML5 video.
[Demo](https://ass.js.org/)
@@ -23,28 +21,26 @@ npm install assjs
CDN: [jsDelivr](https://www.jsdelivr.com/package/npm/assjs), [unpkg](https://unpkg.com/assjs/)
-ASS.js only targets for latest modern browsers, if you need support IE 10 or 11, please use [v0.0.11](https://github.com/weizhenye/ASS/tree/v0.0.11).
-
## Usage
```html
-
-
+
+
+
-
-
-
-
+```
+
+```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'),
+});
```
## API
@@ -52,9 +48,8 @@ fetch('/path/to/example.ass')
#### Initialization
```js
-const ass = new ASS(text, video, {
+const ass = new ASS(content, video, {
// Subtitles will display in the container.
- // The container will be created automatically if it's not provided.
container: document.getElementById('my-container'),
// see resampling API below
@@ -62,14 +57,6 @@ const ass = new ASS(text, video, {
});
```
-#### Resize
-
-If you change the size of video, you should call it.
-
-```js
-ass.resize();
-```
-
#### Show
```js
diff --git a/api-extractor.json b/api-extractor.json
new file mode 100644
index 0000000..e1b5850
--- /dev/null
+++ b/api-extractor.json
@@ -0,0 +1,41 @@
+{
+ "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
+
+ "mainEntryPointFilePath": "
/types/index.d.ts",
+ "bundledPackages": ["ass-compiler"],
+
+ "apiReport": {
+ "enabled": false
+ },
+
+ "docModel": {
+ "enabled": false
+ },
+
+ "dtsRollup": {
+ "enabled": true,
+ "untrimmedFilePath": "/dist/ass.d.ts"
+ },
+
+ "tsdocMetadata": {
+ "enabled": false
+ },
+
+ "messages": {
+ "compilerMessageReporting": {
+ "default": {
+ "logLevel": "warning"
+ }
+ },
+ "extractorMessageReporting": {
+ "default": {
+ "logLevel": "none"
+ }
+ },
+ "tsdocMessageReporting": {
+ "default": {
+ "logLevel": "none"
+ }
+ }
+ }
+}
diff --git a/dist/ass.esm.js b/dist/ass.esm.js
deleted file mode 100644
index 3c387a1..0000000
--- a/dist/ass.esm.js
+++ /dev/null
@@ -1,2053 +0,0 @@
-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,
- };
- }
- return null;
-}
-
-function parseDrawing(text) {
- 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 && Number.isNaN(x * 1)); })
- ); });
-}
-
-var numTags = [
- 'b', 'i', 'u', 's', 'fsp',
- 'k', 'K', 'kf', 'ko', 'kt',
- 'fe', 'q', 'p', 'pbo', 'a', 'an',
- 'fscx', 'fscy', 'fax', 'fay', 'frx', 'fry', 'frz', 'fr',
- 'be', 'blur', 'bord', 'xbord', 'ybord', 'shad', 'xshad', 'yshad' ];
-
-var numRegexs = numTags.map(function (nt) { return ({ name: nt, regex: new RegExp(("^" + nt + "-?\\d")) }); });
-
-function parseTag(text) {
- var assign;
-
- var tag = {};
- for (var i = 0; i < numRegexs.length; i++) {
- var ref = numRegexs[i];
- var name = ref.name;
- var regex = ref.regex;
- if (regex.test(text)) {
- tag[name] = text.slice(name.length) * 1;
- return tag;
- }
- }
- if (/^fn/.test(text)) {
- tag.fn = text.slice(2);
- } else if (/^r/.test(text)) {
- tag.r = text.slice(1);
- } else if (/^fs[\d+-]/.test(text)) {
- tag.fs = text.slice(2);
- } else if (/^\d?c&?H?[0-9a-f]+|^\d?c$/i.test(text)) {
- var ref$1 = text.match(/^(\d?)c&?H?(\w*)/);
- var num = ref$1[1];
- var color = ref$1[2];
- tag[("c" + (num || 1))] = color && ("000000" + color).slice(-6);
- } else if (/^\da&?H?[0-9a-f]+/i.test(text)) {
- var ref$2 = text.match(/^(\d)a&?H?(\w\w)/);
- var num$1 = ref$2[1];
- var alpha = ref$2[2];
- tag[("a" + num$1)] = alpha;
- } else if (/^alpha&?H?[0-9a-f]+/i.test(text)) {
- (assign = text.match(/^alpha&?H?([0-9a-f]+)/i), tag.alpha = assign[1]);
- tag.alpha = ("00" + (tag.alpha)).slice(-2);
- } else if (/^(?:pos|org|move|fad|fade)\(/.test(text)) {
- var ref$3 = text.match(/^(\w+)\((.*?)\)?$/);
- var key = ref$3[1];
- var value = ref$3[2];
- tag[key] = value
- .trim()
- .split(/\s*,\s*/)
- .map(Number);
- } else if (/^i?clip/.test(text)) {
- var p = text
- .match(/^i?clip\((.*?)\)?$/)[1]
- .trim()
- .split(/\s*,\s*/);
- tag.clip = {
- inverse: /iclip/.test(text),
- scale: 1,
- drawing: null,
- dots: null,
- };
- if (p.length === 1) {
- tag.clip.drawing = parseDrawing(p[0]);
- }
- if (p.length === 2) {
- tag.clip.scale = p[0] * 1;
- tag.clip.drawing = parseDrawing(p[1]);
- }
- if (p.length === 4) {
- tag.clip.dots = p.map(Number);
- }
- } else if (/^t\(/.test(text)) {
- var p$1 = text
- .match(/^t\((.*?)\)?$/)[1]
- .trim()
- .replace(/\\.*/, function (x) { return x.replace(/,/g, '\n'); })
- .split(/\s*,\s*/);
- if (!p$1[0]) { return tag; }
- tag.t = {
- t1: 0,
- t2: 0,
- accel: 1,
- tags: p$1[p$1.length - 1]
- .replace(/\n/g, ',')
- .split('\\')
- .slice(1)
- .map(parseTag),
- };
- if (p$1.length === 2) {
- tag.t.accel = p$1[0] * 1;
- }
- if (p$1.length === 3) {
- tag.t.t1 = p$1[0] * 1;
- tag.t.t2 = p$1[1] * 1;
- }
- if (p$1.length === 4) {
- tag.t.t1 = p$1[0] * 1;
- tag.t.t2 = p$1[1] * 1;
- tag.t.accel = p$1[2] * 1;
- }
- }
-
- return tag;
-}
-
-function parseTags(text) {
- var tags = [];
- var depth = 0;
- var str = '';
- for (var i = 0; i < text.length; i++) {
- var x = text[i];
- if (x === '(') { depth++; }
- if (x === ')') { depth--; }
- if (depth < 0) { depth = 0; }
- if (!depth && x === '\\') {
- if (str) {
- tags.push(str);
- }
- str = '';
- } else {
- str += x;
- }
- }
- tags.push(str);
- return tags.map(parseTag);
-}
-
-function parseText(text) {
- var pairs = text.split(/{([^{}]*?)}/);
- var parsed = [];
- if (pairs[0].length) {
- parsed.push({ tags: [], text: pairs[0], drawing: [] });
- }
- 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;
-}
-
-function parseFormat(text) {
- return text.match(/Format\s*:\s*(.*)/i)[1].split(/\s*,\s*/);
-}
-
-function parseStyle(text) {
- return text.match(/Style\s*:\s*(.*)/i)[1].split(/\s*,\s*/);
-}
-
-function parse(text) {
- var tree = {
- info: {},
- styles: { format: [], style: [] },
- events: { format: [], comment: [], dialogue: [] },
- };
- var lines = text.split(/\r?\n/);
- var state = 0;
- for (var i = 0; i < lines.length; i++) {
- var line = lines[i].trim();
- if (/^;/.test(line)) { continue; }
-
- if (/^\[Script Info\]/i.test(line)) { state = 1; }
- else if (/^\[V4\+? Styles\]/i.test(line)) { state = 2; }
- else if (/^\[Events\]/i.test(line)) { state = 3; }
- else if (/^\[.*\]/.test(line)) { state = 0; }
-
- if (state === 0) { continue; }
- if (state === 1) {
- if (/:/.test(line)) {
- var ref = line.match(/(.*?)\s*:\s*(.*)/);
- var key = ref[1];
- var value = ref[2];
- tree.info[key] = value;
- }
- }
- if (state === 2) {
- if (/^Format\s*:/i.test(line)) {
- tree.styles.format = parseFormat(line);
- }
- if (/^Style\s*:/i.test(line)) {
- tree.styles.style.push(parseStyle(line));
- }
- }
- if (state === 3) {
- if (/^Format\s*:/i.test(line)) {
- tree.events.format = parseFormat(line);
- }
- if (/^(?:Comment|Dialogue)\s*:/i.test(line)) {
- var ref$1 = line.match(/^(\w+?)\s*:\s*(.*)/i);
- var key$1 = ref$1[1];
- var value$1 = ref$1[2];
- tree.events[key$1.toLowerCase()].push(parseDialogue(value$1, tree.events.format));
- }
- }
- }
-
- return tree;
-}
-
-var assign = Object.assign || (
- /* istanbul ignore next */
- function assign(target) {
- var sources = [], len = arguments.length - 1;
- while ( len-- > 0 ) sources[ len ] = arguments[ len + 1 ];
-
- for (var i = 0; i < sources.length; i++) {
- if (!sources[i]) { continue; }
- var keys = Object.keys(sources[i]);
- for (var j = 0; j < keys.length; j++) {
- // eslint-disable-next-line no-param-reassign
- target[keys[j]] = sources[i][keys[j]];
- }
- }
- return target;
- }
-);
-
-function createCommand(arr) {
- var cmd = {
- type: null,
- prev: null,
- next: null,
- points: [],
- };
- if (/[mnlbs]/.test(arr[0])) {
- cmd.type = arr[0]
- .toUpperCase()
- .replace('N', 'L')
- .replace('B', 'C');
- }
- for (var len = arr.length - !(arr.length & 1), i = 1; i < len; i += 2) {
- cmd.points.push({ x: arr[i] * 1, y: arr[i + 1] * 1 });
- }
- return cmd;
-}
-
-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 assign({ instructions: instructions, d: toSVGPath(instructions) }, getViewBox(commands));
-}
-
-var tTags = [
- 'fs', 'clip',
- 'c1', 'c2', 'c3', 'c4', 'a1', 'a2', 'a3', 'a4', 'alpha',
- 'fscx', 'fscy', 'fax', 'fay', 'frx', 'fry', 'frz', 'fr',
- 'be', 'blur', 'bord', 'xbord', 'ybord', 'shad', 'xshad', 'yshad' ];
-
-function compileTag(tag, key, presets) {
- var obj, obj$1, obj$2;
-
- if ( presets === void 0 ) presets = {};
- var value = tag[key];
- if (value === undefined) {
- return null;
- }
- if (key === 'pos' || key === 'org') {
- return value.length === 2 ? ( obj = {}, obj[key] = { x: value[0], y: value[1] }, obj ) : null;
- }
- if (key === 'move') {
- var x1 = value[0];
- var y1 = value[1];
- var x2 = value[2];
- var y2 = value[3];
- var t1 = value[4]; if ( t1 === void 0 ) t1 = 0;
- var t2 = value[5]; if ( t2 === void 0 ) t2 = 0;
- return value.length === 4 || value.length === 6
- ? { move: { x1: x1, y1: y1, x2: x2, y2: y2, t1: t1, t2: t2 } }
- : null;
- }
- if (key === 'fad' || key === 'fade') {
- if (value.length === 2) {
- var t1$1 = value[0];
- var t2$1 = value[1];
- return { fade: { type: 'fad', t1: t1$1, t2: t2$1 } };
- }
- if (value.length === 7) {
- var a1 = value[0];
- var a2 = value[1];
- var a3 = value[2];
- var t1$2 = value[3];
- var t2$2 = value[4];
- var t3 = value[5];
- var t4 = value[6];
- return { fade: { type: 'fade', a1: a1, a2: a2, a3: a3, t1: t1$2, t2: t2$2, t3: t3, t4: t4 } };
- }
- return null;
- }
- if (key === 'clip') {
- var inverse = value.inverse;
- var scale = value.scale;
- var drawing = value.drawing;
- var dots = value.dots;
- if (drawing) {
- return { clip: { inverse: inverse, scale: scale, drawing: compileDrawing(drawing), dots: dots } };
- }
- if (dots) {
- var x1$1 = dots[0];
- var y1$1 = dots[1];
- var x2$1 = dots[2];
- var y2$1 = dots[3];
- return { clip: { inverse: inverse, scale: scale, drawing: drawing, dots: { x1: x1$1, y1: y1$1, x2: x2$1, y2: y2$1 } } };
- }
- return null;
- }
- if (/^[xy]?(bord|shad)$/.test(key)) {
- value = Math.max(value, 0);
- }
- if (key === 'bord') {
- return { xbord: value, ybord: value };
- }
- if (key === 'shad') {
- return { xshad: value, yshad: value };
- }
- if (/^c\d$/.test(key)) {
- return ( obj$1 = {}, obj$1[key] = value || presets[key], obj$1 );
- }
- if (key === 'alpha') {
- return { a1: value, a2: value, a3: value, a4: value };
- }
- 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 === 't') {
- var t1$3 = value.t1;
- var accel = value.accel;
- var tags = value.tags;
- var t2$3 = value.t2 || (presets.end - presets.start) * 1e3;
- var compiledTag = {};
- tags.forEach(function (t) {
- var k = Object.keys(t)[0];
- if (~tTags.indexOf(k) && !(k === 'clip' && !t[k].dots)) {
- assign(compiledTag, compileTag(t, k, presets));
- }
- });
- return { t: { t1: t1$3, t2: t2$3, accel: accel, tag: compiledTag } };
- }
- 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 createSlice(name, styles) {
- return {
- name: name,
- borderStyle: styles[name].style.BorderStyle,
- tag: styles[name].tag,
- fragments: [],
- };
-}
-
-function compileText(ref) {
- var styles = ref.styles;
- var name = ref.name;
- var parsed = ref.parsed;
- var start = ref.start;
- var end = ref.end;
-
- var alignment;
- var pos;
- var org;
- var move;
- var fade;
- var clip;
- var slices = [];
- var slice = createSlice(name, styles);
- var prevTag = {};
- for (var i = 0; i < parsed.length; i++) {
- var ref$1 = parsed[i];
- var tags = ref$1.tags;
- var text = ref$1.text;
- var drawing = ref$1.drawing;
- var reset = (void 0);
- for (var j = 0; j < tags.length; j++) {
- var tag = tags[j];
- reset = tag.r === undefined ? reset : tag.r;
- }
- var fragment = {
- tag: reset === undefined ? JSON.parse(JSON.stringify(prevTag)) : {},
- text: text,
- drawing: drawing.length ? compileDrawing(drawing) : null,
- };
- for (var j$1 = 0; j$1 < tags.length; j$1++) {
- var tag$1 = tags[j$1];
- alignment = alignment || a2an[tag$1.a || 0] || tag$1.an;
- pos = pos || compileTag(tag$1, 'pos');
- org = org || compileTag(tag$1, 'org');
- move = move || compileTag(tag$1, 'move');
- fade = fade || compileTag(tag$1, 'fade') || compileTag(tag$1, 'fad');
- clip = compileTag(tag$1, 'clip') || clip;
- var key = Object.keys(tag$1)[0];
- if (key && !~globalTags.indexOf(key)) {
- var ref$2 = slice.tag;
- var c1 = ref$2.c1;
- var c2 = ref$2.c2;
- var c3 = ref$2.c3;
- var c4 = ref$2.c4;
- var fs = prevTag.fs || slice.tag.fs;
- var compiledTag = compileTag(tag$1, key, { start: start, end: end, c1: c1, c2: c2, c3: c3, c4: c4, fs: fs });
- if (key === 't') {
- fragment.tag.t = fragment.tag.t || [];
- fragment.tag.t.push(compiledTag.t);
- } else {
- assign(fragment.tag, compiledTag);
- }
- }
- }
- prevTag = fragment.tag;
- if (reset !== undefined) {
- slices.push(slice);
- slice = createSlice(styles[reset] ? reset : name, styles);
- }
- if (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 assign({ alignment: alignment, slices: slices }, pos, org, move, fade, clip);
-}
-
-function compileDialogues(ref) {
- var styles = ref.styles;
- var dialogues = ref.dialogues;
-
- var minLayer = Infinity;
- var results = [];
- for (var i = 0; i < dialogues.length; i++) {
- var dia = dialogues[i];
- if (dia.Start >= dia.End) {
- continue;
- }
- if (!styles[dia.Style]) {
- dia.Style = 'Default';
- }
- var stl = styles[dia.Style].style;
- var compiledText = compileText({
- styles: styles,
- name: dia.Style,
- parsed: dia.Text.parsed,
- start: dia.Start,
- end: dia.End,
- });
- var alignment = compiledText.alignment || stl.Alignment;
- minLayer = Math.min(minLayer, dia.Layer);
- results.push(assign({
- layer: dia.Layer,
- start: dia.Start,
- end: dia.End,
- // reset style by `\r` will not effect margin and alignment
- margin: {
- left: dia.MarginL || stl.MarginL,
- right: dia.MarginR || stl.MarginR,
- vertical: dia.MarginV || stl.MarginV,
- },
- effect: dia.Effect,
- }, compiledText, { alignment: alignment }));
- }
- for (var i$1 = 0; i$1 < results.length; i$1++) {
- results[i$1].layer -= minLayer;
- }
- return results.sort(function (a, b) { return a.start - b.start || a.end - b.end; });
-}
-
-// same as Aegisub
-// https://github.com/Aegisub/Aegisub/blob/master/src/ass_style.h
-var DEFAULT_STYLE = {
- Name: 'Default',
- Fontname: 'Arial',
- Fontsize: '20',
- PrimaryColour: '&H00FFFFFF&',
- SecondaryColour: '&H000000FF&',
- OutlineColour: '&H00000000&',
- BackColour: '&H00000000&',
- Bold: '0',
- Italic: '0',
- Underline: '0',
- StrikeOut: '0',
- ScaleX: '100',
- ScaleY: '100',
- Spacing: '0',
- Angle: '0',
- BorderStyle: '1',
- Outline: '2',
- Shadow: '2',
- Alignment: '2',
- MarginL: '10',
- MarginR: '10',
- MarginV: '10',
- Encoding: '1',
-};
-
-/**
- * @param {String} color
- * @returns {Array} [AA, BBGGRR]
- */
-function parseStyleColor(color) {
- if (/^(&|H|&H)[0-9a-f]{6,}/i.test(color)) {
- var ref = color.match(/&?H?([0-9a-f]{2})?([0-9a-f]{6})/i);
- var a = ref[1];
- var c = ref[2];
- return [a || '00', c];
- }
- var num = parseInt(color, 10);
- if (!Number.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 format = ref.format;
- var defaultStyle = ref.defaultStyle;
-
- var result = {};
- var styles = [
- assign({}, DEFAULT_STYLE, defaultStyle, { Name: 'Default' }) ].concat( style.map(function (stl) {
- var s = {};
- for (var i = 0; i < format.length; i++) {
- s[format[i]] = stl[i];
- }
- return s;
- }) );
- var loop = function ( i ) {
- var s = styles[i];
- // this behavior is same as Aegisub by black-box testing
- if (/^(\*+)Default$/.test(s.Name)) {
- s.Name = 'Default';
- }
- Object.keys(s).forEach(function (key) {
- if (key !== 'Name' && key !== 'Fontname' && !/Colour/.test(key)) {
- s[key] *= 1;
- }
- });
- var ref$1 = parseStyleColor(s.PrimaryColour);
- var a1 = ref$1[0];
- var c1 = ref$1[1];
- var ref$2 = parseStyleColor(s.SecondaryColour);
- var a2 = ref$2[0];
- var c2 = ref$2[1];
- var ref$3 = parseStyleColor(s.OutlineColour);
- var a3 = ref$3[0];
- var c3 = ref$3[1];
- var ref$4 = parseStyleColor(s.BackColour);
- var a4 = ref$4[0];
- var c4 = ref$4[1];
- var tag = {
- fn: s.Fontname,
- fs: s.Fontsize,
- c1: c1,
- a1: a1,
- c2: c2,
- a2: a2,
- c3: c3,
- a3: a3,
- c4: c4,
- a4: a4,
- b: Math.abs(s.Bold),
- i: Math.abs(s.Italic),
- u: Math.abs(s.Underline),
- s: Math.abs(s.StrikeOut),
- fscx: s.ScaleX,
- fscy: s.ScaleY,
- fsp: s.Spacing,
- frz: s.Angle,
- xbord: s.Outline,
- ybord: s.Outline,
- xshad: s.Shadow,
- yshad: s.Shadow,
- q: /^[0-3]$/.test(info.WrapStyle) ? info.WrapStyle * 1 : 2,
- };
- result[s.Name] = { style: s, tag: tag };
- };
-
- for (var i = 0; i < styles.length; i++) loop( i );
- return result;
-}
-
-function compile(text, options) {
- if ( options === void 0 ) options = {};
-
- var tree = parse(text);
- var styles = compileStyles({
- info: tree.info,
- style: tree.styles.style,
- format: tree.styles.format,
- defaultStyle: options.defaultStyle || {},
- });
- return {
- info: tree.info,
- width: tree.info.PlayResX * 1 || null,
- height: tree.info.PlayResY * 1 || null,
- collisions: tree.info.Collisions || 'Normal',
- styles: styles,
- dialogues: compileDialogues({
- styles: styles,
- dialogues: tree.events.dialogue,
- }),
- };
-}
-
-var raf = (
- window.requestAnimationFrame
- || window.mozRequestAnimationFrame
- || window.webkitRequestAnimationFrame
- || (function (cb) { return setTimeout(cb, 50 / 3); })
-);
-
-var caf = (
- window.cancelAnimationFrame
- || window.mozCancelAnimationFrame
- || window.webkitCancelAnimationFrame
- || clearTimeout
-);
-
-function color2rgba(c) {
- var t = c.match(/(\w\w)(\w\w)(\w\w)(\w\w)/);
- var a = 1 - ("0x" + (t[1])) / 255;
- var b = +("0x" + (t[2]));
- var g = +("0x" + (t[3]));
- var r = +("0x" + (t[4]));
- return ("rgba(" + r + "," + g + "," + b + "," + a + ")");
-}
-
-function uuid() {
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
- var r = Math.random() * 16 | 0;
- var v = c === 'x' ? r : (r & 0x3 | 0x8);
- return v.toString(16);
- });
-}
-
-function createSVGEl(name, attrs) {
- if ( attrs === void 0 ) attrs = [];
-
- var $el = document.createElementNS('http://www.w3.org/2000/svg', name);
- for (var i = 0; i < attrs.length; i++) {
- var attr = attrs[i];
- $el.setAttributeNS(
- attr[0] === 'xlink:href' ? 'http://www.w3.org/1999/xlink' : null,
- attr[0],
- attr[1]
- );
- }
- return $el;
-}
-
-function getVendor(prop) {
- var ref = document.body;
- var style = ref.style;
- var Prop = prop.replace(/^\w/, function (x) { return x.toUpperCase(); });
- if (prop in style) { return ''; }
- if (("webkit" + Prop) in style) { return '-webkit-'; }
- if (("moz" + Prop) in style) { return '-moz-'; }
- return '';
-}
-
-var vendor = {
- transform: getVendor('transform'),
- animation: getVendor('animation'),
- clipPath: getVendor('clipPath'),
-};
-
-function getStyleRoot(container) {
- var rootNode = container.getRootNode ? container.getRootNode() : document;
- return rootNode === document ? rootNode.head : rootNode;
-}
-
-var strokeTags = ['c3', 'a3', 'c4', 'a4', 'xbord', 'ybord', 'xshad', 'yshad', 'blur', 'be'];
-var transformTags = ['fscx', 'fscy', 'frx', 'fry', 'frz', 'fax', 'fay'];
-
-function createClipPath(clip) {
- var sw = this._.scriptRes.width;
- var sh = this._.scriptRes.height;
- var d = '';
- if (clip.dots !== null) {
- var ref = clip.dots;
- var x1 = ref.x1;
- var y1 = ref.y1;
- var x2 = ref.x2;
- var y2 = ref.y2;
- x1 /= sw;
- y1 /= sh;
- x2 /= sw;
- y2 /= sh;
- d = "M" + x1 + "," + y1 + "L" + x1 + "," + y2 + "," + x2 + "," + y2 + "," + x2 + "," + y1 + "Z";
- }
- if (clip.drawing !== null) {
- d = clip.drawing.instructions.map(function (ref) {
- var type = ref.type;
- var points = ref.points;
-
- return (
- type + points.map(function (ref) {
- var x = ref.x;
- var y = ref.y;
-
- return ((x / sw) + "," + (y / sh));
- }).join(',')
- );
- }).join('');
- }
- var scale = 1 / (1 << (clip.scale - 1));
- if (clip.inverse) {
- d += "M0,0L0," + scale + "," + scale + "," + scale + "," + scale + ",0,0,0Z";
- }
- var id = "ASS-" + (uuid());
- var $clipPath = createSVGEl('clipPath', [
- ['id', id],
- ['clipPathUnits', 'objectBoundingBox'] ]);
- $clipPath.appendChild(createSVGEl('path', [
- ['d', d],
- ['transform', ("scale(" + scale + ")")],
- ['clip-rule', 'evenodd'] ]));
- this._.$defs.appendChild($clipPath);
- return {
- $clipPath: $clipPath,
- cssText: ((vendor.clipPath) + "clip-path:url(#" + id + ");"),
- };
-}
-
-function setClipPath(dialogue) {
- if (!dialogue.clip) {
- return;
- }
- var $fobb = document.createElement('div');
- this._.$stage.insertBefore($fobb, dialogue.$div);
- $fobb.appendChild(dialogue.$div);
- $fobb.className = 'ASS-fix-objectBoundingBox';
- var ref = createClipPath.call(this, dialogue.clip);
- var cssText = ref.cssText;
- var $clipPath = ref.$clipPath;
- this._.$defs.appendChild($clipPath);
- $fobb.style.cssText = cssText;
- assign(dialogue, { $div: $fobb, $clipPath: $clipPath });
-}
-
-var $fixFontSize = document.createElement('div');
-$fixFontSize.className = 'ASS-fix-font-size';
-$fixFontSize.textContent = 'M';
-
-var cache = Object.create(null);
-
-function getRealFontSize(fn, fs) {
- var key = fn + "-" + fs;
- if (!cache[key]) {
- $fixFontSize.style.cssText = "line-height:normal;font-size:" + fs + "px;font-family:\"" + fn + "\",Arial;";
- cache[key] = fs * fs / $fixFontSize.clientHeight;
- }
- return cache[key];
-}
-
-function createSVGStroke(tag, id, scale) {
- var hasBorder = tag.xbord || tag.ybord;
- var hasShadow = tag.xshad || tag.yshad;
- var isOpaque = tag.a1 !== 'FF';
- var blur = tag.blur || tag.be || 0;
- var $filter = createSVGEl('filter', [['id', id]]);
- $filter.appendChild(createSVGEl('feGaussianBlur', [
- ['stdDeviation', hasBorder ? 0 : blur],
- ['in', 'SourceGraphic'],
- ['result', 'sg_b'] ]));
- $filter.appendChild(createSVGEl('feFlood', [
- ['flood-color', color2rgba(tag.a1 + tag.c1)],
- ['result', 'c1'] ]));
- $filter.appendChild(createSVGEl('feComposite', [
- ['operator', 'in'],
- ['in', 'c1'],
- ['in2', 'sg_b'],
- ['result', 'main'] ]));
- if (hasBorder) {
- $filter.appendChild(createSVGEl('feMorphology', [
- ['radius', ((tag.xbord * scale) + " " + (tag.ybord * scale))],
- ['operator', 'dilate'],
- ['in', 'SourceGraphic'],
- ['result', 'dil'] ]));
- $filter.appendChild(createSVGEl('feGaussianBlur', [
- ['stdDeviation', blur],
- ['in', 'dil'],
- ['result', 'dil_b'] ]));
- $filter.appendChild(createSVGEl('feComposite', [
- ['operator', 'out'],
- ['in', 'dil_b'],
- ['in2', 'SourceGraphic'],
- ['result', 'dil_b_o'] ]));
- $filter.appendChild(createSVGEl('feFlood', [
- ['flood-color', color2rgba(tag.a3 + tag.c3)],
- ['result', 'c3'] ]));
- $filter.appendChild(createSVGEl('feComposite', [
- ['operator', 'in'],
- ['in', 'c3'],
- ['in2', 'dil_b_o'],
- ['result', 'border'] ]));
- }
- if (hasShadow && (hasBorder || isOpaque)) {
- $filter.appendChild(createSVGEl('feOffset', [
- ['dx', tag.xshad * scale],
- ['dy', tag.yshad * scale],
- ['in', hasBorder ? 'dil' : 'SourceGraphic'],
- ['result', 'off'] ]));
- $filter.appendChild(createSVGEl('feGaussianBlur', [
- ['stdDeviation', blur],
- ['in', 'off'],
- ['result', 'off_b'] ]));
- if (!isOpaque) {
- $filter.appendChild(createSVGEl('feOffset', [
- ['dx', tag.xshad * scale],
- ['dy', tag.yshad * scale],
- ['in', 'SourceGraphic'],
- ['result', 'sg_off'] ]));
- $filter.appendChild(createSVGEl('feComposite', [
- ['operator', 'out'],
- ['in', 'off_b'],
- ['in2', 'sg_off'],
- ['result', 'off_b_o'] ]));
- }
- $filter.appendChild(createSVGEl('feFlood', [
- ['flood-color', color2rgba(tag.a4 + tag.c4)],
- ['result', 'c4'] ]));
- $filter.appendChild(createSVGEl('feComposite', [
- ['operator', 'in'],
- ['in', 'c4'],
- ['in2', isOpaque ? 'off_b' : 'off_b_o'],
- ['result', 'shadow'] ]));
- }
- var $merge = createSVGEl('feMerge', []);
- if (hasShadow && (hasBorder || isOpaque)) {
- $merge.appendChild(createSVGEl('feMergeNode', [['in', 'shadow']]));
- }
- if (hasBorder) {
- $merge.appendChild(createSVGEl('feMergeNode', [['in', 'border']]));
- }
- $merge.appendChild(createSVGEl('feMergeNode', [['in', 'main']]));
- $filter.appendChild($merge);
- return $filter;
-}
-
-function createCSSStroke(tag, scale) {
- var arr = [];
- var oc = color2rgba(tag.a3 + tag.c3);
- var ox = tag.xbord * scale;
- var oy = tag.ybord * scale;
- var sc = color2rgba(tag.a4 + tag.c4);
- var sx = tag.xshad * scale;
- var sy = tag.yshad * scale;
- var blur = tag.blur || tag.be || 0;
- if (!(ox + oy + sx + sy)) { return 'none'; }
- if (ox || oy) {
- for (var i = -1; i <= 1; i++) {
- for (var j = -1; j <= 1; j++) {
- for (var x = 1; x < ox; x++) {
- for (var y = 1; y < oy; y++) {
- if (i || j) {
- arr.push((oc + " " + (i * x) + "px " + (j * y) + "px " + blur + "px"));
- }
- }
- }
- arr.push((oc + " " + (i * ox) + "px " + (j * oy) + "px " + blur + "px"));
- }
- }
- }
- if (sx || sy) {
- var pnx = sx > 0 ? 1 : -1;
- var pny = sy > 0 ? 1 : -1;
- sx = Math.abs(sx);
- sy = Math.abs(sy);
- for (var x$1 = Math.max(ox, sx - ox); x$1 < sx + ox; x$1++) {
- for (var y$1 = Math.max(oy, sy - oy); y$1 < sy + oy; y$1++) {
- arr.push((sc + " " + (x$1 * pnx) + "px " + (y$1 * pny) + "px " + blur + "px"));
- }
- }
- arr.push((sc + " " + ((sx + ox) * pnx) + "px " + ((sy + oy) * pny) + "px " + blur + "px"));
- }
- return arr.join();
-}
-
-function createTransform(tag) {
- return [
- // TODO: I don't know why perspective is 314, it just performances well.
- 'perspective(314px)',
- ("rotateY(" + (tag.fry || 0) + "deg)"),
- ("rotateX(" + (tag.frx || 0) + "deg)"),
- ("rotateZ(" + (-tag.frz || 0) + "deg)"),
- ("scale(" + (tag.p ? 1 : (tag.fscx || 100) / 100) + "," + (tag.p ? 1 : (tag.fscy || 100) / 100) + ")"),
- ("skew(" + (tag.fax || 0) + "rad," + (tag.fay || 0) + "rad)") ].join(' ');
-}
-
-function setTransformOrigin(dialogue) {
- var alignment = dialogue.alignment;
- var width = dialogue.width;
- var height = dialogue.height;
- var x = dialogue.x;
- var y = dialogue.y;
- var $div = dialogue.$div;
- var org = dialogue.org;
- if (!org) {
- org = { x: 0, y: 0 };
- if (alignment % 3 === 1) { org.x = x; }
- if (alignment % 3 === 2) { org.x = x + width / 2; }
- if (alignment % 3 === 0) { org.x = x + width; }
- if (alignment <= 3) { org.y = y + height; }
- if (alignment >= 4 && alignment <= 6) { org.y = y + height / 2; }
- if (alignment >= 7) { org.y = y; }
- }
- for (var i = $div.childNodes.length - 1; i >= 0; i--) {
- var node = $div.childNodes[i];
- if (node.dataset.hasRotate === 'true') {
- // It's not extremely precise for offsets are round the value to an integer.
- var tox = org.x - x - node.offsetLeft;
- var toy = org.y - y - node.offsetTop;
- node.style.cssText += (vendor.transform) + "transform-origin:" + tox + "px " + toy + "px;";
- }
- }
-}
-
-function getKeyframeString(name, list) {
- return ("@" + (vendor.animation) + "keyframes " + name + " {" + list + "}\n");
-}
-
-var KeyframeBlockList = function KeyframeBlockList() {
- this.obj = {};
-};
-
-KeyframeBlockList.prototype.set = function set (keyText, prop, value) {
- if (!this.obj[keyText]) { this.obj[keyText] = {}; }
- this.obj[keyText][prop] = value;
-};
-
-KeyframeBlockList.prototype.setT = function setT (ref) {
- var t1 = ref.t1;
- var t2 = ref.t2;
- var duration = ref.duration;
- var prop = ref.prop;
- var from = ref.from;
- var to = ref.to;
-
- this.set('0.000%', prop, from);
- if (t1 < duration) {
- this.set((((t1 / duration * 100).toFixed(3)) + "%"), prop, from);
- }
- if (t2 < duration) {
- this.set((((t2 / duration * 100).toFixed(3)) + "%"), prop, to);
- }
- this.set('100.000%', prop, to);
-};
-
-KeyframeBlockList.prototype.toString = function toString () {
- var this$1 = this;
-
- return Object.keys(this.obj)
- .map(function (keyText) { return (
- (keyText + "{" + (Object.keys(this$1.obj[keyText])
- .map(function (prop) { return ("" + (vendor[prop] || '') + prop + ":" + (this$1.obj[keyText][prop]) + ";"); })
- .join('')) + "}")
- ); })
- .join('');
-};
-
-// TODO: multi \t can't be merged directly
-function mergeT(ts) {
- return ts.reduceRight(function (results, t) {
- var merged = false;
- return results
- .map(function (r) {
- merged = t.t1 === r.t1 && t.t2 === r.t2 && t.accel === r.accel;
- return assign({}, r, merged ? { tag: assign({}, r.tag, t.tag) } : {});
- })
- .concat(merged ? [] : t);
- }, []);
-}
-
-function getKeyframes() {
- var this$1 = this;
-
- var keyframes = '';
- this.dialogues.forEach(function (dialogue) {
- var start = dialogue.start;
- var end = dialogue.end;
- var effect = dialogue.effect;
- var move = dialogue.move;
- var fade = dialogue.fade;
- var slices = dialogue.slices;
- var duration = (end - start) * 1000;
- var diaKbl = new KeyframeBlockList();
- // TODO: when effect and move both exist, its behavior is weird, for now only move works.
- if (effect && !move) {
- var name = effect.name;
- var delay = effect.delay;
- var lefttoright = effect.lefttoright;
- var y1 = effect.y1;
- var y2 = effect.y2 || this$1._.resampledRes.height;
- if (name === 'banner') {
- var tx = this$1.scale * (duration / delay) * (lefttoright ? 1 : -1);
- diaKbl.set('0.000%', 'transform', 'translateX(0)');
- diaKbl.set('100.000%', 'transform', ("translateX(" + tx + "px)"));
- }
- if (/^scroll/.test(name)) {
- var updown = /up/.test(name) ? -1 : 1;
- var tFrom = "translateY(" + (this$1.scale * y1 * updown) + "px)";
- var tTo = "translateY(" + (this$1.scale * y2 * updown) + "px)";
- var dp = (y2 - y1) / (duration / delay) * 100;
- diaKbl.set('0.000%', 'transform', tFrom);
- if (dp < 100) {
- diaKbl.set(((dp.toFixed(3)) + "%"), 'transform', tTo);
- }
- diaKbl.set('100.000%', 'transform', tTo);
- }
- }
- if (move) {
- var x1 = move.x1;
- var y1$1 = move.y1;
- var x2 = move.x2;
- var y2$1 = move.y2;
- var t1 = move.t1;
- var t2 = move.t2 || duration;
- var pos = dialogue.pos || { x: 0, y: 0 };
- var values = [{ x: x1, y: y1$1 }, { x: x2, y: y2$1 }].map(function (ref) {
- var x = ref.x;
- var y = ref.y;
-
- return (
- ("translate(" + (this$1.scale * (x - pos.x)) + "px, " + (this$1.scale * (y - pos.y)) + "px)")
- );
- });
- diaKbl.setT({ t1: t1, t2: t2, duration: duration, prop: 'transform', from: values[0], to: values[1] });
- }
- if (fade) {
- if (fade.type === 'fad') {
- var t1$1 = fade.t1;
- var t2$1 = fade.t2;
- diaKbl.set('0.000%', 'opacity', 0);
- if (t1$1 < duration) {
- diaKbl.set((((t1$1 / duration * 100).toFixed(3)) + "%"), 'opacity', 1);
- if (t1$1 + t2$1 < duration) {
- diaKbl.set(((((duration - t2$1) / duration * 100).toFixed(3)) + "%"), 'opacity', 1);
- }
- diaKbl.set('100.000%', 'opacity', 0);
- } else {
- diaKbl.set('100.000%', 'opacity', duration / t1$1);
- }
- } else {
- var a1 = fade.a1;
- var a2 = fade.a2;
- var a3 = fade.a3;
- var t1$2 = fade.t1;
- var t2$2 = fade.t2;
- var t3 = fade.t3;
- var t4 = fade.t4;
- var keyTexts = [t1$2, t2$2, t3, t4].map(function (t) { return (((t / duration * 100).toFixed(3)) + "%"); });
- var values$1 = [a1, a2, a3].map(function (a) { return 1 - a / 255; });
- diaKbl.set('0.000%', 'opacity', values$1[0]);
- if (t1$2 < duration) { diaKbl.set(keyTexts[0], 'opacity', values$1[0]); }
- if (t2$2 < duration) { diaKbl.set(keyTexts[1], 'opacity', values$1[1]); }
- if (t3 < duration) { diaKbl.set(keyTexts[2], 'opacity', values$1[1]); }
- if (t4 < duration) { diaKbl.set(keyTexts[3], 'opacity', values$1[2]); }
- diaKbl.set('100.000%', 'opacity', values$1[2]);
- }
- }
- var diaList = diaKbl.toString();
- if (diaList) {
- assign(dialogue, { animationName: ("ASS-" + (uuid())) });
- keyframes += getKeyframeString(dialogue.animationName, diaList);
- }
- slices.forEach(function (slice) {
- slice.fragments.forEach(function (fragment) {
- if (!fragment.tag.t || !fragment.tag.t.length) {
- return;
- }
- var kbl = new KeyframeBlockList();
- var fromTag = assign({}, slice.tag, fragment.tag);
- // TODO: accel is not implemented yet
- mergeT(fragment.tag.t).forEach(function (ref) {
- var t1 = ref.t1;
- var t2 = ref.t2;
- var tag = ref.tag;
-
- if (tag.fs) {
- var from = (this$1.scale * getRealFontSize(fromTag.fn, fromTag.fs)) + "px";
- var to = (this$1.scale * getRealFontSize(tag.fn, fromTag.fs)) + "px";
- kbl.setT({ t1: t1, t2: t2, duration: duration, prop: 'font-size', from: from, to: to });
- }
- if (tag.fsp) {
- var from$1 = (this$1.scale * fromTag.fsp) + "px";
- var to$1 = (this$1.scale * tag.fsp) + "px";
- kbl.setT({ t1: t1, t2: t2, duration: duration, prop: 'letter-spacing', from: from$1, to: to$1 });
- }
- var hasAlpha = (
- tag.a1 !== undefined
- && tag.a1 === tag.a2
- && tag.a2 === tag.a3
- && tag.a3 === tag.a4
- );
- if (tag.c1 || (tag.a1 && !hasAlpha)) {
- var from$2 = color2rgba(fromTag.a1 + fromTag.c1);
- var to$2 = color2rgba((tag.a1 || fromTag.a1) + (tag.c1 || fromTag.c1));
- kbl.setT({ t1: t1, t2: t2, duration: duration, prop: 'color', from: from$2, to: to$2 });
- }
- if (hasAlpha) {
- var from$3 = 1 - parseInt(fromTag.a1, 16) / 255;
- var to$3 = 1 - parseInt(tag.a1, 16) / 255;
- kbl.setT({ t1: t1, t2: t2, duration: duration, prop: 'opacity', from: from$3, to: to$3 });
- }
- var hasStroke = strokeTags.some(function (x) { return (
- tag[x] !== undefined
- && tag[x] !== (fragment.tag[x] || slice.tag[x])
- ); });
- if (hasStroke) {
- var scale = /Yes/i.test(this$1.info.ScaledBorderAndShadow) ? this$1.scale : 1;
- var from$4 = createCSSStroke(fromTag, scale);
- var to$4 = createCSSStroke(assign({}, fromTag, tag), scale);
- kbl.setT({ t1: t1, t2: t2, duration: duration, prop: 'text-shadow', from: from$4, to: to$4 });
- }
- var hasTransfrom = transformTags.some(function (x) { return (
- tag[x] !== undefined
- && tag[x] !== (fragment.tag[x] || slice.tag[x])
- ); });
- if (hasTransfrom) {
- var toTag = assign({}, fromTag, tag);
- if (fragment.drawing) {
- // scales will be handled inside svg
- assign(toTag, {
- p: 0,
- fscx: ((tag.fscx || fromTag.fscx) / fromTag.fscx) * 100,
- fscy: ((tag.fscy || fromTag.fscy) / fromTag.fscy) * 100,
- });
- assign(fromTag, { fscx: 100, fscy: 100 });
- }
- var from$5 = createTransform(fromTag);
- var to$5 = createTransform(toTag);
- kbl.setT({ t1: t1, t2: t2, duration: duration, prop: 'transform', from: from$5, to: to$5 });
- }
- });
- var list = kbl.toString();
- assign(fragment, { animationName: ("ASS-" + (uuid())) });
- keyframes += getKeyframeString(fragment.animationName, list);
- });
- });
- });
- return keyframes;
-}
-
-function createAnimation(name, duration, delay) {
- var va = vendor.animation;
- return (
- va + "animation-name:" + name + ";"
- + va + "animation-duration:" + duration + "s;"
- + va + "animation-delay:" + delay + "s;"
- + va + "animation-timing-function:linear;"
- + va + "animation-iteration-count:1;"
- + va + "animation-fill-mode:forwards;"
- );
-}
-
-function createDrawing(fragment, styleTag) {
- var tag = assign({}, styleTag, fragment.tag);
- var ref = fragment.drawing;
- var minX = ref.minX;
- var minY = ref.minY;
- var width = ref.width;
- var height = ref.height;
- var baseScale = this.scale / (1 << (tag.p - 1));
- var scaleX = (tag.fscx ? tag.fscx / 100 : 1) * baseScale;
- var scaleY = (tag.fscy ? tag.fscy / 100 : 1) * baseScale;
- var blur = tag.blur || tag.be || 0;
- var vbx = tag.xbord + (tag.xshad < 0 ? -tag.xshad : 0) + blur;
- var vby = tag.ybord + (tag.yshad < 0 ? -tag.yshad : 0) + blur;
- var vbw = width * scaleX + 2 * tag.xbord + Math.abs(tag.xshad) + 2 * blur;
- var vbh = height * scaleY + 2 * tag.ybord + Math.abs(tag.yshad) + 2 * blur;
- var $svg = createSVGEl('svg', [
- ['width', vbw],
- ['height', vbh],
- ['viewBox', ((-vbx) + " " + (-vby) + " " + vbw + " " + vbh)] ]);
- var strokeScale = /Yes/i.test(this.info.ScaledBorderAndShadow) ? this.scale : 1;
- var filterId = "ASS-" + (uuid());
- var $defs = createSVGEl('defs');
- $defs.appendChild(createSVGStroke(tag, filterId, strokeScale));
- $svg.appendChild($defs);
- var symbolId = "ASS-" + (uuid());
- var $symbol = createSVGEl('symbol', [
- ['id', symbolId],
- ['viewBox', (minX + " " + minY + " " + width + " " + height)] ]);
- $symbol.appendChild(createSVGEl('path', [['d', fragment.drawing.d]]));
- $svg.appendChild($symbol);
- $svg.appendChild(createSVGEl('use', [
- ['width', width * scaleX],
- ['height', height * scaleY],
- ['xlink:href', ("#" + symbolId)],
- ['filter', ("url(#" + filterId + ")")] ]));
- $svg.style.cssText = (
- 'position:absolute;'
- + "left:" + (minX * scaleX - vbx) + "px;"
- + "top:" + (minY * scaleY - vby) + "px;"
- );
- return {
- $svg: $svg,
- cssText: ("position:relative;width:" + (width * scaleX) + "px;height:" + (height * scaleY) + "px;"),
- };
-}
-
-function encodeText(text, q) {
- return text
- .replace(//g, '>')
- .replace(/\s/g, ' ')
- .replace(/\\h/g, ' ')
- .replace(/\\N/g, '
')
- .replace(/\\n/g, q === 2 ? '
' : ' ');
-}
-
-function createDialogue(dialogue) {
- var this$1 = this;
-
- var $div = document.createElement('div');
- $div.className = 'ASS-dialogue';
- var df = document.createDocumentFragment();
- var slices = dialogue.slices;
- var start = dialogue.start;
- var end = dialogue.end;
- slices.forEach(function (slice) {
- var borderStyle = slice.borderStyle;
- slice.fragments.forEach(function (fragment) {
- var text = fragment.text;
- var drawing = fragment.drawing;
- var animationName = fragment.animationName;
- var tag = assign({}, slice.tag, fragment.tag);
- var cssText = 'display:inline-block;';
- var vct = this$1.video.currentTime;
- if (!drawing) {
- cssText += "font-family:\"" + (tag.fn) + "\",Arial;";
- cssText += "font-size:" + (this$1.scale * getRealFontSize(tag.fn, tag.fs)) + "px;";
- cssText += "color:" + (color2rgba(tag.a1 + tag.c1)) + ";";
- var scale = /Yes/i.test(this$1.info.ScaledBorderAndShadow) ? this$1.scale : 1;
- if (borderStyle === 1) {
- cssText += "text-shadow:" + (createCSSStroke(tag, scale)) + ";";
- }
- if (borderStyle === 3) {
- cssText += (
- "background-color:" + (color2rgba(tag.a3 + tag.c3)) + ";"
- + "box-shadow:" + (createCSSStroke(tag, scale)) + ";"
- );
- }
- cssText += tag.b ? ("font-weight:" + (tag.b === 1 ? 'bold' : tag.b) + ";") : '';
- cssText += tag.i ? 'font-style:italic;' : '';
- cssText += (tag.u || tag.s) ? ("text-decoration:" + (tag.u ? 'underline' : '') + " " + (tag.s ? 'line-through' : '') + ";") : '';
- cssText += tag.fsp ? ("letter-spacing:" + (tag.fsp) + "px;") : '';
- // TODO: (tag.q === 0) and (tag.q === 3) are not implemented yet,
- // for now just handle it as (tag.q === 1)
- if (tag.q === 1 || tag.q === 0 || tag.q === 3) {
- cssText += 'word-break:break-all;white-space:normal;';
- }
- if (tag.q === 2) {
- cssText += 'word-break:normal;white-space:nowrap;';
- }
- }
- var hasTransfrom = transformTags.some(function (x) { return (
- /^fsc[xy]$/.test(x) ? tag[x] !== 100 : !!tag[x]
- ); });
- if (hasTransfrom) {
- cssText += (vendor.transform) + "transform:" + (createTransform(tag)) + ";";
- if (!drawing) {
- cssText += 'transform-style:preserve-3d;word-break:normal;white-space:nowrap;';
- }
- }
- if (animationName) {
- cssText += createAnimation(animationName, end - start, Math.min(0, start - vct));
- }
- if (drawing && tag.pbo) {
- var pbo = this$1.scale * -tag.pbo * (tag.fscy || 100) / 100;
- cssText += "vertical-align:" + pbo + "px;";
- }
-
- var hasRotate = /"fr[xyz]":[^0]/.test(JSON.stringify(tag));
- encodeText(text, tag.q).split('
').forEach(function (html, idx) {
- var $span = document.createElement('span');
- $span.dataset.hasRotate = hasRotate;
- if (drawing) {
- var obj = createDrawing.call(this$1, fragment, slice.tag);
- $span.style.cssText = obj.cssText;
- $span.appendChild(obj.$svg);
- } else {
- if (idx) {
- df.appendChild(document.createElement('br'));
- }
- if (!html) {
- return;
- }
- $span.innerHTML = html;
- }
- // TODO: maybe it can be optimized
- $span.style.cssText += cssText;
- df.appendChild($span);
- });
- });
- });
- $div.appendChild(df);
- return $div;
-}
-
-function allocate(dialogue) {
- var layer = dialogue.layer;
- var margin = dialogue.margin;
- var width = dialogue.width;
- var height = dialogue.height;
- var alignment = dialogue.alignment;
- var end = dialogue.end;
- var stageWidth = this.width - (this.scale * (margin.left + margin.right) | 0);
- var stageHeight = this.height;
- var vertical = this.scale * margin.vertical | 0;
- var vct = this.video.currentTime * 100;
- this._.space[layer] = this._.space[layer] || {
- left: { width: new Uint16Array(stageHeight + 1), end: new Uint16Array(stageHeight + 1) },
- center: { width: new Uint16Array(stageHeight + 1), end: new Uint16Array(stageHeight + 1) },
- right: { width: new Uint16Array(stageHeight + 1), end: new Uint16Array(stageHeight + 1) },
- };
- var channel = this._.space[layer];
- var align = ['right', 'left', 'center'][alignment % 3];
- var willCollide = function (y) {
- var lw = channel.left.width[y];
- var cw = channel.center.width[y];
- var rw = channel.right.width[y];
- var le = channel.left.end[y];
- var ce = channel.center.end[y];
- var re = channel.right.end[y];
- return (
- (align === 'left' && (
- (le > vct && lw)
- || (ce > vct && cw && 2 * width + cw > stageWidth)
- || (re > vct && rw && width + rw > stageWidth)
- ))
- || (align === 'center' && (
- (le > vct && lw && 2 * lw + width > stageWidth)
- || (ce > vct && cw)
- || (re > vct && rw && 2 * rw + width > stageWidth)
- ))
- || (align === 'right' && (
- (le > vct && lw && lw + width > stageWidth)
- || (ce > vct && cw && 2 * width + cw > stageWidth)
- || (re > vct && rw)
- ))
- );
- };
- var count = 0;
- var result = 0;
- var find = function (y) {
- count = willCollide(y) ? 0 : count + 1;
- if (count >= height) {
- result = y;
- return true;
- }
- return false;
- };
- if (alignment <= 3) {
- for (var i = stageHeight - vertical - 1; i > vertical; i--) {
- if (find(i)) { break; }
- }
- } else if (alignment >= 7) {
- for (var i$1 = vertical + 1; i$1 < stageHeight - vertical; i$1++) {
- if (find(i$1)) { break; }
- }
- } else {
- for (var i$2 = (stageHeight - height) >> 1; i$2 < stageHeight - vertical; i$2++) {
- if (find(i$2)) { break; }
- }
- }
- if (alignment > 3) {
- result -= height - 1;
- }
- for (var i$3 = result; i$3 < result + height; i$3++) {
- channel[align].width[i$3] = width;
- channel[align].end[i$3] = end * 100;
- }
- return result;
-}
-
-function getPosition(dialogue) {
- var effect = dialogue.effect;
- var move = dialogue.move;
- var alignment = dialogue.alignment;
- var width = dialogue.width;
- var height = dialogue.height;
- var margin = dialogue.margin;
- var slices = dialogue.slices;
- var x = 0;
- var y = 0;
- if (effect) {
- if (effect.name === 'banner') {
- if (alignment <= 3) { y = this.height - height - margin.vertical; }
- if (alignment >= 4 && alignment <= 6) { y = (this.height - height) / 2; }
- if (alignment >= 7) { y = margin.vertical; }
- x = effect.lefttoright ? -width : this.width;
- }
- } else if (dialogue.pos || move) {
- var pos = dialogue.pos || { x: 0, y: 0 };
- if (alignment % 3 === 1) { x = this.scale * pos.x; }
- if (alignment % 3 === 2) { x = this.scale * pos.x - width / 2; }
- if (alignment % 3 === 0) { x = this.scale * pos.x - width; }
- if (alignment <= 3) { y = this.scale * pos.y - height; }
- if (alignment >= 4 && alignment <= 6) { y = this.scale * pos.y - height / 2; }
- if (alignment >= 7) { y = this.scale * pos.y; }
- } else {
- if (alignment % 3 === 1) { x = 0; }
- if (alignment % 3 === 2) { x = (this.width - width) / 2; }
- if (alignment % 3 === 0) { x = this.width - width - this.scale * margin.right; }
- var hasT = slices.some(function (slice) { return (
- slice.fragments.some(function (ref) {
- var animationName = ref.animationName;
-
- return animationName;
- })
- ); });
- if (hasT) {
- if (alignment <= 3) { y = this.height - height - margin.vertical; }
- if (alignment >= 4 && alignment <= 6) { y = (this.height - height) / 2; }
- if (alignment >= 7) { y = margin.vertical; }
- } else {
- y = allocate.call(this, dialogue);
- }
- }
- return { x: x, y: y };
-}
-
-function createStyle(dialogue) {
- var layer = dialogue.layer;
- var start = dialogue.start;
- var end = dialogue.end;
- var alignment = dialogue.alignment;
- var effect = dialogue.effect;
- var pos = dialogue.pos;
- var margin = dialogue.margin;
- var animationName = dialogue.animationName;
- var width = dialogue.width;
- var height = dialogue.height;
- var x = dialogue.x;
- var y = dialogue.y;
- var vct = this.video.currentTime;
- var cssText = '';
- if (layer) { cssText += "z-index:" + layer + ";"; }
- if (animationName) {
- cssText += createAnimation(animationName, end - start, Math.min(0, start - vct));
- }
- cssText += "text-align:" + (['right', 'left', 'center'][alignment % 3]) + ";";
- if (!effect) {
- var mw = this.width - this.scale * (margin.left + margin.right);
- cssText += "max-width:" + mw + "px;";
- if (!pos) {
- if (alignment % 3 === 1) {
- cssText += "margin-left:" + (this.scale * margin.left) + "px;";
- }
- if (alignment % 3 === 0) {
- cssText += "margin-right:" + (this.scale * margin.right) + "px;";
- }
- if (width > this.width - this.scale * (margin.left + margin.right)) {
- cssText += "margin-left:" + (this.scale * margin.left) + "px;";
- cssText += "margin-right:" + (this.scale * margin.right) + "px;";
- }
- }
- }
- cssText += "width:" + width + "px;height:" + height + "px;left:" + x + "px;top:" + y + "px;";
- return cssText;
-}
-
-function renderer(dialogue) {
- var $div = createDialogue.call(this, dialogue);
- assign(dialogue, { $div: $div });
- this._.$stage.appendChild($div);
- var ref = $div.getBoundingClientRect();
- var width = ref.width;
- var height = ref.height;
- assign(dialogue, { width: width, height: height });
- assign(dialogue, getPosition.call(this, dialogue));
- $div.style.cssText = createStyle.call(this, dialogue);
- setTransformOrigin(dialogue);
- setClipPath.call(this, dialogue);
- return dialogue;
-}
-
-function framing() {
- var vct = this.video.currentTime;
- for (var i = this._.stagings.length - 1; i >= 0; i--) {
- var dia = this._.stagings[i];
- var end = dia.end;
- if (dia.effect && /scroll/.test(dia.effect.name)) {
- var ref = dia.effect;
- var y1 = ref.y1;
- var y2 = ref.y2;
- var delay = ref.delay;
- var duration = ((y2 || this._.resampledRes.height) - y1) / (1000 / delay);
- end = Math.min(end, dia.start + duration);
- }
- if (end < vct) {
- this._.$stage.removeChild(dia.$div);
- if (dia.$clipPath) {
- this._.$defs.removeChild(dia.$clipPath);
- }
- this._.stagings.splice(i, 1);
- }
- }
- var dias = this.dialogues;
- while (
- this._.index < dias.length
- && vct >= dias[this._.index].start
- ) {
- if (vct < dias[this._.index].end) {
- var dia$1 = renderer.call(this, dias[this._.index]);
- this._.stagings.push(dia$1);
- }
- ++this._.index;
- }
-}
-
-function play() {
- var this$1 = this;
-
- var frame = function () {
- framing.call(this$1);
- this$1._.requestId = raf(frame);
- };
- caf(this._.requestId);
- this._.requestId = raf(frame);
- this._.$stage.classList.remove('ASS-animation-paused');
- return this;
-}
-
-function pause() {
- caf(this._.requestId);
- this._.requestId = 0;
- this._.$stage.classList.add('ASS-animation-paused');
- return this;
-}
-
-function clear() {
- while (this._.$stage.lastChild) {
- this._.$stage.removeChild(this._.$stage.lastChild);
- }
- while (this._.$defs.lastChild) {
- this._.$defs.removeChild(this._.$defs.lastChild);
- }
- this._.stagings = [];
- this._.space = [];
-}
-
-function seek() {
- var vct = this.video.currentTime;
- var dias = this.dialogues;
- clear.call(this);
- this._.index = (function () {
- var from = 0;
- var to = dias.length - 1;
- while (from + 1 < to && vct > dias[(to + from) >> 1].end) {
- from = (to + from) >> 1;
- }
- if (!from) { return 0; }
- for (var i = from; i < to; i++) {
- if (
- dias[i].end > vct && vct >= dias[i].start
- || (i && dias[i - 1].end < vct && vct < dias[i].start)
- ) {
- return i;
- }
- }
- return to;
- })();
- framing.call(this);
-}
-
-function bindEvents() {
- var l = this._.listener;
- l.play = play.bind(this);
- l.pause = pause.bind(this);
- l.seeking = seek.bind(this);
- this.video.addEventListener('play', l.play);
- this.video.addEventListener('pause', l.pause);
- this.video.addEventListener('seeking', l.seeking);
-}
-
-function unbindEvents() {
- var l = this._.listener;
- this.video.removeEventListener('play', l.play);
- this.video.removeEventListener('pause', l.pause);
- this.video.removeEventListener('seeking', l.seeking);
- l.play = null;
- l.pause = null;
- l.seeking = null;
-}
-
-function resize() {
- var cw = this.video.clientWidth;
- var ch = this.video.clientHeight;
- var vw = this.video.videoWidth || cw;
- var vh = this.video.videoHeight || ch;
- var sw = this._.scriptRes.width;
- var sh = this._.scriptRes.height;
- var rw = sw;
- var rh = sh;
- var videoScale = Math.min(cw / vw, ch / vh);
- if (this.resampling === 'video_width') {
- rh = sw / vw * vh;
- }
- if (this.resampling === 'video_height') {
- rw = sh / vh * vw;
- }
- this.scale = Math.min(cw / rw, ch / rh);
- if (this.resampling === 'script_width') {
- this.scale = videoScale * (vw / rw);
- }
- if (this.resampling === 'script_height') {
- this.scale = videoScale * (vh / rh);
- }
- this.width = this.scale * rw;
- this.height = this.scale * rh;
- this._.resampledRes = { width: rw, height: rh };
-
- this.container.style.cssText = "width:" + cw + "px;height:" + ch + "px;";
- var cssText = (
- "width:" + (this.width) + "px;"
- + "height:" + (this.height) + "px;"
- + "top:" + ((ch - this.height) / 2) + "px;"
- + "left:" + ((cw - this.width) / 2) + "px;"
- );
- this._.$stage.style.cssText = cssText;
- this._.$svg.style.cssText = cssText;
- this._.$svg.setAttributeNS(null, 'viewBox', ("0 0 " + sw + " " + sh));
-
- this._.$animation.innerHTML = getKeyframes.call(this);
- seek.call(this);
-
- return this;
-}
-
-var GLOBAL_CSS = '.ASS-container,.ASS-stage{position:relative;overflow:hidden}.ASS-container video{position:absolute;top:0;left:0}.ASS-stage{pointer-events:none;position:absolute}.ASS-dialogue{font-size:0;position:absolute}.ASS-fix-font-size{position:absolute;visibility:hidden}.ASS-fix-objectBoundingBox{width:100%;height:100%;position:absolute;top:0;left:0}.ASS-animation-paused *{-webkit-animation-play-state:paused!important;animation-play-state:paused!important}';
-
-function init(source, video, options) {
- if ( options === void 0 ) options = {};
-
- this.scale = 1;
-
- // private variables
- this._ = {
- index: 0,
- stagings: [],
- space: [],
- listener: {},
- $svg: createSVGEl('svg'),
- $defs: createSVGEl('defs'),
- $stage: document.createElement('div'),
- $animation: document.createElement('style'),
- };
- this._.$svg.appendChild(this._.$defs);
- this._.$stage.className = 'ASS-stage ASS-animation-paused';
-
- this._.resampling = options.resampling || 'video_height';
-
- this.container = options.container || document.createElement('div');
- this.container.classList.add('ASS-container');
- this.container.appendChild($fixFontSize);
- this.container.appendChild(this._.$svg);
- this._.hasInitContainer = !!options.container;
-
- this.video = video;
- bindEvents.call(this);
- if (!this._.hasInitContainer) {
- var isPlaying = !video.paused;
- video.parentNode.insertBefore(this.container, video);
- this.container.appendChild(video);
- if (isPlaying && video.paused) {
- video.play();
- }
- }
- this.container.appendChild(this._.$stage);
-
- var ref = compile(source);
- var info = ref.info;
- var width = ref.width;
- var height = ref.height;
- var dialogues = ref.dialogues;
- this.info = info;
- this._.scriptRes = {
- width: width || video.videoWidth,
- height: height || video.videoHeight,
- };
- this.dialogues = dialogues;
-
- var styleRoot = getStyleRoot(this.container);
- var $style = styleRoot.querySelector('#ASS-global-style');
- if (!$style) {
- $style = document.createElement('style');
- $style.type = 'text/css';
- $style.id = 'ASS-global-style';
- $style.appendChild(document.createTextNode(GLOBAL_CSS));
- styleRoot.appendChild($style);
- }
- this._.$animation.type = 'text/css';
- this._.$animation.className = 'ASS-animation';
- styleRoot.appendChild(this._.$animation);
-
- resize.call(this);
-
- if (!this.video.paused) {
- seek.call(this);
- play.call(this);
- }
-
- return this;
-}
-
-function show() {
- this._.$stage.style.visibility = 'visible';
- return this;
-}
-
-function hide() {
- this._.$stage.style.visibility = 'hidden';
- return this;
-}
-
-function destroy() {
- pause.call(this);
- clear.call(this);
- unbindEvents.call(this, this._.listener);
-
- var styleRoot = getStyleRoot(this.container);
- if (!this._.hasInitContainer) {
- var isPlay = !this.video.paused;
- this.container.parentNode.insertBefore(this.video, this.container);
- this.container.parentNode.removeChild(this.container);
- if (isPlay && this.video.paused) {
- this.video.play();
- }
- }
- styleRoot.removeChild(this._.$animation);
-
- // eslint-disable-next-line no-restricted-syntax
- for (var key in this) {
- if (Object.prototype.hasOwnProperty.call(this, key)) {
- this[key] = null;
- }
- }
-
- return this;
-}
-
-var regex = /^(video|script)_(width|height)$/;
-
-function getter() {
- return regex.test(this._.resampling) ? this._.resampling : 'video_height';
-}
-
-function setter(r) {
- if (r === this._.resampling) { return r; }
- if (regex.test(r)) {
- this._.resampling = r;
- this.resize();
- }
- return this._.resampling;
-}
-
-var ASS = function ASS(source, video, options) {
- if (typeof source !== 'string') {
- return this;
- }
- return init.call(this, source, video, options);
-};
-
-var prototypeAccessors = { resampling: { configurable: true } };
-
-ASS.prototype.resize = function resize$1 () {
- return resize.call(this);
-};
-
-ASS.prototype.show = function show$1 () {
- return show.call(this);
-};
-
-ASS.prototype.hide = function hide$1 () {
- return hide.call(this);
-};
-
-ASS.prototype.destroy = function destroy$1 () {
- return destroy.call(this);
-};
-
-prototypeAccessors.resampling.get = function () {
- return getter.call(this);
-};
-
-prototypeAccessors.resampling.set = function (r) {
- return setter.call(this, r);
-};
-
-Object.defineProperties( ASS.prototype, prototypeAccessors );
-
-export default ASS;
diff --git a/karma.conf.js b/karma.conf.js
deleted file mode 100644
index dcee41b..0000000
--- a/karma.conf.js
+++ /dev/null
@@ -1,39 +0,0 @@
-const fs = require('fs');
-const csso = require('csso');
-const istanbul = require('rollup-plugin-istanbul');
-const replace = require('@rollup/plugin-replace');
-const { nodeResolve } = require('@rollup/plugin-node-resolve');
-
-module.exports = (config) => {
- config.set({
- singleRun: true,
- frameworks: ['mocha', 'chai'],
- browsers: ['ChromeHeadless'],
- files: [
- { pattern: 'test/fixtures/**/*.*', included: false, served: true },
- { pattern: 'test/test.js', watched: false },
- ],
- preprocessors: {
- 'test/test.js': ['rollup'],
- },
- rollupPreprocessor: {
- output: {
- format: 'iife',
- },
- plugins: [
- replace({
- __GLOBAL_CSS__: csso.minify(fs.readFileSync('./src/global.css')).css,
- }),
- nodeResolve(),
- istanbul({
- include: ['src/**/*'],
- }),
- ],
- },
- reporters: ['dots', 'coverage'],
- coverageReporter: {
- type: 'lcov',
- subdir: '.',
- },
- });
-};
diff --git a/karma.sauce.js b/karma.sauce.js
deleted file mode 100644
index 9577518..0000000
--- a/karma.sauce.js
+++ /dev/null
@@ -1,72 +0,0 @@
-const path = require('path');
-const alias = require('@rollup/plugin-alias');
-
-// https://wiki.saucelabs.com/display/DOCS/Platform+Configurator#/
-const customLaunchers = {
- SL_iOS_Safari: {
- base: 'SauceLabs',
- browserName: 'Safari',
- deviceName: 'iPhone Simulator',
- platformVersion: '13.4',
- platformName: 'iOS',
- },
- // SL_Android: {
- // base: 'SauceLabs',
- // deviceName: 'Android Emulator',
- // browserName: 'Chrome',
- // platformVersion: '10.0',
- // platformName: 'Android',
- // },
- SL_Chrome: {
- base: 'SauceLabs',
- browserName: 'chrome',
- },
- SL_Firefox: {
- base: 'SauceLabs',
- browserName: 'firefox',
- },
- SL_Safari: {
- base: 'SauceLabs',
- browserName: 'safari',
- },
- SL_Edge: {
- base: 'SauceLabs',
- browserName: 'MicrosoftEdge',
- },
-};
-
-module.exports = (config) => {
- config.set({
- singleRun: true,
- concurrency: 5,
- captureTimeout: 300000,
- browserNoActivityTimeout: 120000,
- frameworks: ['mocha', 'chai'],
- browsers: Object.keys(customLaunchers),
- customLaunchers,
- files: [
- { pattern: 'test/fixtures/**/*.*', included: false, served: true },
- { pattern: 'test/test.js', watched: false },
- ],
- preprocessors: {
- 'test/test.js': ['rollup'],
- },
- rollupPreprocessor: {
- output: {
- format: 'iife',
- },
- plugins: [
- alias({
- entries: {
- '../../src/index.js': path.resolve(__dirname, './dist/ass.esm.js'),
- },
- }),
- ],
- },
- reporters: ['dots', 'saucelabs'],
- sauceLabs: {
- testName: 'ASS.js unit test',
- recordScreenshots: false,
- },
- });
-};
diff --git a/package.json b/package.json
index e5468e7..49b5a3d 100644
--- a/package.json
+++ b/package.json
@@ -1,9 +1,10 @@
{
"name": "assjs",
"version": "0.0.11",
+ "type": "module",
"description": "A JavaScript ASS subtitle format renderer",
"main": "dist/ass.js",
- "module": "dist/ass.esm.js",
+ "types": "dist/ass.d.ts",
"files": [
"dist",
"src"
@@ -11,10 +12,10 @@
"scripts": {
"dev": "rollup -c -w",
"lint": "eslint src test",
- "unit": "karma start",
- "sauce": "karma start karma.sauce.js",
+ "unit": "vitest",
"test": "npm run lint && npm run unit",
"build": "rollup -c",
+ "postbuild": "tsc && api-extractor run --local",
"preversion": "npm test",
"version": "npm run build && git add -A ./dist",
"postversion": "git push && git push --tags && npm publish"
@@ -35,25 +36,21 @@
},
"homepage": "https://ass.js.org/",
"devDependencies": {
- "@rollup/plugin-alias": "^3.1.1",
- "@rollup/plugin-node-resolve": "^9.0.0",
- "@rollup/plugin-replace": "^2.3.3",
- "ass-compiler": "^0.1.1",
- "chai": "^4.2.0",
- "csso": "^4.0.3",
- "eslint": "^7.9.0",
- "eslint-config-airbnb-base": "^14.2.0",
- "eslint-plugin-import": "^2.22.0",
- "karma": "^5.2.2",
- "karma-chai": "^0.1.0",
- "karma-chrome-launcher": "^3.1.0",
- "karma-coverage": "^2.0.3",
- "karma-mocha": "^2.0.1",
- "karma-rollup-preprocessor": "^7.0.5",
- "karma-sauce-launcher": "^1.2.0",
- "mocha": "^8.1.3",
- "rollup": "^2.27.1",
- "rollup-plugin-istanbul": "^2.0.1",
- "rollup-plugin-terser": "^7.0.2"
+ "@microsoft/api-extractor": "^7.47.0",
+ "@rollup/plugin-alias": "^5.1.0",
+ "@rollup/plugin-node-resolve": "^15.2.3",
+ "@rollup/plugin-replace": "^5.0.7",
+ "@rollup/plugin-terser": "^0.4.4",
+ "@vitest/browser": "^1.6.0",
+ "ass-compiler": "^0.1.11",
+ "csso": "^5.0.5",
+ "eslint": "^8.57.0",
+ "eslint-config-airbnb-base": "^15.0.0",
+ "eslint-plugin-import": "^2.29.1",
+ "eslint-plugin-unicorn": "^53.0.0",
+ "playwright": "^1.44.1",
+ "rollup": "^4.18.0",
+ "typescript": "^5.4.5",
+ "vitest": "^1.6.0"
}
}
diff --git a/rollup.config.js b/rollup.config.js
index 9789644..8609f35 100644
--- a/rollup.config.js
+++ b/rollup.config.js
@@ -1,31 +1,29 @@
-import fs from 'fs';
-import csso from 'csso';
+import { readFileSync } from 'node:fs';
+import { minify } from 'csso';
import replace from '@rollup/plugin-replace';
import { nodeResolve } from '@rollup/plugin-node-resolve';
-import { terser } from 'rollup-plugin-terser';
+import terser from '@rollup/plugin-terser';
+/** @type {import('rollup').RollupOptions} */
export default {
input: 'src/index.js',
output: [
{
file: 'dist/ass.js',
- format: 'umd',
- name: 'ASS',
+ format: 'esm',
},
{
file: 'dist/ass.min.js',
- format: 'umd',
- name: 'ASS',
- plugins: [terser()],
- },
- {
- file: 'dist/ass.esm.js',
format: 'esm',
+ plugins: [terser()],
},
],
plugins: [
replace({
- __GLOBAL_CSS__: csso.minify(fs.readFileSync('./src/global.css')).css,
+ preventAssignment: true,
+ values: {
+ __GLOBAL_CSS__: minify(readFileSync('./src/global.css', 'utf8')).css,
+ },
}),
nodeResolve(),
],
diff --git a/src/global.css b/src/global.css
index 88b6170..08ebf63 100644
--- a/src/global.css
+++ b/src/global.css
@@ -1,13 +1,4 @@
-.ASS-container {
- position: relative;
- overflow: hidden;
-}
-.ASS-container video {
- position: absolute;
- top: 0;
- left: 0;
-}
-.ASS-stage {
+.ASS-box {
overflow: hidden;
pointer-events: none;
position: absolute;
@@ -37,6 +28,7 @@
opacity: var(--ass-shadow-opacity);
}
.ASS-dialogue [data-stroke]::after {
+ color: transparent;
-webkit-text-stroke: var(--ass-border-width) var(--ass-border-color);
text-shadow: var(--ass-border-delta);
opacity: var(--ass-border-opacity);
diff --git a/src/index.js b/src/index.js
index c27ebe5..4ffea54 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,39 +1,216 @@
-import { init } from './internal/init.js';
-import { resize } from './internal/resize.js';
-import { show } from './internal/show.js';
-import { hide } from './internal/hide.js';
-import { destroy } from './internal/destroy.js';
-import { getter, setter } from './internal/resampling.js';
+/* eslint-disable max-len */
+import { compile } from 'ass-compiler';
+import { $fixFontSize } from './renderer/font-size.js';
+import { clear, createResize, createPlay, createPause, createSeek } from './internal.js';
+import { createSVGEl, addGlobalStyle } from './utils.js';
+
+/**
+ * @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.
+ */
export default class ASS {
- constructor(source, video, options) {
- if (typeof source !== 'string') {
- return this;
- }
- return init.call(this, source, video, options);
+ #store = {
+ /** @type {HTMLVideoElement} */
+ video: null,
+ /** the box to display subtitles */
+ box: document.createElement('div'),
+ // TODO: 是否可以动态添加
+ /** use for \clip */
+ svg: createSVGEl('svg'),
+ /** use for \clip */
+ defs: createSVGEl('defs'),
+ /**
+ * video resize observer
+ * @type {ResizeObserver}
+ */
+ observer: null,
+ scale: 1,
+ width: 0,
+ height: 0,
+ /** resolution from ASS file, it's PlayResX and PlayResY */
+ scriptRes: {},
+ /** resolution after resampling */
+ resampledRes: {},
+ /** current index of dialogues to match currentTime */
+ index: 0,
+ /** @type {import('ass-compiler').ScriptInfo} */
+ info: {},
+ /** @type {import('ass-compiler').CompiledASSStyle} */
+ styles: {},
+ /** @type {import('ass-compiler').Dialogue[]} */
+ dialogues: [],
+ /**
+ * active dialogues
+ * @type {import('ass-compiler').Dialogue[]}
+ */
+ actives: [],
+ /** record dialogues' position */
+ space: [],
+ requestId: 0,
+ };
+
+ #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
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * JavaScript:
+ * ```js
+ * import ASS from 'assjs';
+ *
+ * const content = await fetch('/path/to/example.ass').then((res) => res.text());
+ * const ass = new ASS(content, document.querySelector('#video'), {
+ * container: document.querySelector('#container'),
+ * });
+ * ```
+ */
+ constructor(content, video, { container = video.parentNode, resampling } = {}) {
+ this.#store.video = video;
+ if (!container) throw new Error('Missing container.');
+
+ const { info, width, height, styles, dialogues } = compile(content);
+ this.#store.info = info;
+ this.#store.scriptRes = {
+ width: width || video.videoWidth,
+ height: height || video.videoHeight,
+ };
+ this.#store.styles = styles;
+ this.#store.dialogues = dialogues.map((dia) => Object.assign(dia, {
+ align: {
+ // 0: left, 1: center, 2: right
+ h: (dia.alignment + 2) % 3,
+ // 0: top, 1: center, 2: bottom
+ v: Math.trunc((dia.alignment - 1) / 3),
+ },
+ }));
+
+ container.append($fixFontSize);
+
+ const { svg, defs, box } = this.#store;
+
+ svg.append(defs);
+ container.append(svg);
+
+ box.className = 'ASS-box';
+ container.append(box);
+
+ addGlobalStyle(container);
+
+ this.resampling = resampling;
+
+ 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);
+
+ const observer = new ResizeObserver(this.#resize);
+ observer.observe(video);
+ this.#store.observer = observer;
+
+ return this;
}
- resize() {
- return resize.call(this);
+ /**
+ * Desctroy the ASS instance
+ * @returns {ASS}
+ */
+ destroy() {
+ const { video, box, svg, observer } = this.#store;
+ this.#pause();
+ clear(this.#store);
+ video.removeEventListener('play', this.#play);
+ video.removeEventListener('pause', this.#pause);
+ video.removeEventListener('playing', this.#play);
+ video.removeEventListener('waiting', this.#pause);
+ video.removeEventListener('seeking', this.#seek);
+
+ $fixFontSize.remove();
+ svg.remove();
+ box.remove();
+ observer.unobserve(this.#store.video);
+
+ this.#store.styles = {};
+ this.#store.dialogues = [];
+
+ return this;
}
+ /**
+ * Show subtitles in the container
+ * @returns {ASS}
+ */
show() {
- return show.call(this);
+ this.#store.box.style.visibility = 'visible';
+ return this;
}
+ /**
+ * Hide subtitles in the container
+ * @returns {ASS}
+ */
hide() {
- return hide.call(this);
+ this.#store.box.style.visibility = 'hidden';
+ return this;
}
- destroy() {
- return destroy.call(this);
- }
+ #resampling = 'video_height';
+ /** @type {ASSOption['resampling']} */
get resampling() {
- return getter.call(this);
+ return this.#resampling;
}
set resampling(r) {
- return setter.call(this, r);
+ if (r === this.#resampling) return;
+ if (/^(video|script)_(width|height)$/.test(r)) {
+ this.#resampling = r;
+ this.#resize();
+ }
}
+
+ /** @type {number} Subtitle delay. TODO: not implement yet */
+ delay = 0;
+
+ // addDialogue(dialogue) {
+ // }
}
diff --git a/src/internal.js b/src/internal.js
new file mode 100644
index 0000000..21553bf
--- /dev/null
+++ b/src/internal.js
@@ -0,0 +1,147 @@
+/* eslint-disable no-param-reassign */
+import { renderer } from './renderer/renderer.js';
+import { setKeyframes } from './renderer/animation.js';
+import { batchAnimate } from './utils.js';
+
+export function clear(store) {
+ const { box, defs } = store;
+ while (box.lastChild) {
+ box.lastChild.remove();
+ }
+ while (defs.lastChild) {
+ defs.lastChild.remove();
+ }
+ store.actives = [];
+ store.space = [];
+}
+
+function framing(store) {
+ const { video, dialogues, actives, resampledRes } = store;
+ const vct = video.currentTime;
+ for (let i = actives.length - 1; i >= 0; i -= 1) {
+ const dia = actives[i];
+ let { end } = dia;
+ if (dia.effect && /scroll/.test(dia.effect.name)) {
+ const { y1, y2, delay } = dia.effect;
+ const duration = ((y2 || resampledRes.height) - y1) / (1000 / delay);
+ end = Math.min(end, dia.start + duration);
+ }
+ if (end < vct) {
+ dia.$div.remove();
+ dia.$clipPath?.remove();
+ actives.splice(i, 1);
+ }
+ }
+ while (
+ store.index < dialogues.length
+ && vct >= dialogues[store.index].start
+ ) {
+ if (vct < dialogues[store.index].end) {
+ const dia = renderer(dialogues[store.index], store);
+ if (!video.paused) {
+ batchAnimate(dia.$div, 'play');
+ }
+ actives.push(dia);
+ }
+ store.index += 1;
+ }
+}
+
+export function createSeek(store) {
+ return function seek() {
+ clear(store);
+ const { video, dialogues } = store;
+ const vct = video.currentTime;
+ store.index = (() => {
+ let from = 0;
+ const to = dialogues.length - 1;
+ while (from + 1 < to && vct > dialogues[(to + from) >> 1].end) {
+ from = (to + from) >> 1;
+ }
+ if (!from) return 0;
+ for (let i = from; i < to; i += 1) {
+ if (
+ dialogues[i].end > vct && vct >= dialogues[i].start
+ || (i && dialogues[i - 1].end < vct && vct < dialogues[i].start)
+ ) {
+ return i;
+ }
+ }
+ return to;
+ })();
+ framing(store);
+ };
+}
+
+export function createPlay(store) {
+ return function play() {
+ const frame = () => {
+ framing(store);
+ store.requestId = requestAnimationFrame(frame);
+ };
+ cancelAnimationFrame(store.requestId);
+ store.requestId = requestAnimationFrame(frame);
+ store.actives.forEach(({ $div }) => {
+ batchAnimate($div, 'play');
+ });
+ };
+}
+
+export function createPause(store) {
+ return function pause() {
+ cancelAnimationFrame(store.requestId);
+ store.requestId = 0;
+ store.actives.forEach(({ $div }) => {
+ batchAnimate($div, 'pause');
+ });
+ };
+}
+
+export function createResize(that, store) {
+ const { video, box, svg, dialogues } = store;
+ return function resize() {
+ const cw = video.clientWidth;
+ const ch = video.clientHeight;
+ const vw = video.videoWidth || cw;
+ const vh = video.videoHeight || ch;
+ const sw = store.scriptRes.width;
+ const sh = store.scriptRes.height;
+ let rw = sw;
+ let rh = sh;
+ const videoScale = Math.min(cw / vw, ch / vh);
+ if (that.resampling === 'video_width') {
+ rh = sw / vw * vh;
+ }
+ if (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 };
+
+ const cssText = (
+ `width:${bw}px;`
+ + `height:${bh}px;`
+ + `top:${(ch - bh) / 2}px;`
+ + `left:${(cw - bw) / 2}px;`
+ );
+ box.style.cssText = cssText;
+ svg.style.cssText = cssText;
+ svg.setAttributeNS(null, 'viewBox', `0 0 ${sw} ${sh}`);
+
+ dialogues.forEach((dialogue) => {
+ setKeyframes(dialogue, store);
+ });
+
+ createSeek(store)();
+ };
+}
diff --git a/src/internal/clear.js b/src/internal/clear.js
deleted file mode 100644
index 9770406..0000000
--- a/src/internal/clear.js
+++ /dev/null
@@ -1,10 +0,0 @@
-export function clear() {
- while (this._.$stage.lastChild) {
- this._.$stage.removeChild(this._.$stage.lastChild);
- }
- while (this._.$defs.lastChild) {
- this._.$defs.removeChild(this._.$defs.lastChild);
- }
- this._.stagings = [];
- this._.space = [];
-}
diff --git a/src/internal/destroy.js b/src/internal/destroy.js
deleted file mode 100644
index fd11c21..0000000
--- a/src/internal/destroy.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import { unbindEvents } from './events.js';
-import { clear } from './clear.js';
-import { pause } from './pause.js';
-
-export function destroy() {
- pause.call(this);
- clear.call(this);
- unbindEvents.call(this, this._.listener);
-
- if (!this._.hasInitContainer) {
- const isPlay = !this.video.paused;
- this.container.parentNode.insertBefore(this.video, this.container);
- this.container.parentNode.removeChild(this.container);
- if (isPlay && this.video.paused) {
- this.video.play();
- }
- }
-
- // eslint-disable-next-line no-restricted-syntax
- for (const key in this) {
- if (Object.prototype.hasOwnProperty.call(this, key)) {
- this[key] = null;
- }
- }
-
- return this;
-}
diff --git a/src/internal/events.js b/src/internal/events.js
deleted file mode 100644
index 0719a4e..0000000
--- a/src/internal/events.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import { play } from './play.js';
-import { pause } from './pause.js';
-import { seek } from './seek.js';
-
-export function bindEvents() {
- const l = this._.listener;
- l.play = play.bind(this);
- l.pause = pause.bind(this);
- l.seeking = seek.bind(this);
- this.video.addEventListener('play', l.play);
- this.video.addEventListener('pause', l.pause);
- this.video.addEventListener('playing', l.play);
- this.video.addEventListener('waiting', l.pause);
- this.video.addEventListener('seeking', l.seeking);
-}
-
-export function unbindEvents() {
- const l = this._.listener;
- this.video.removeEventListener('play', l.play);
- this.video.removeEventListener('pause', l.pause);
- this.video.removeEventListener('playing', l.play);
- this.video.removeEventListener('waiting', l.pause);
- this.video.removeEventListener('seeking', l.seeking);
- l.play = null;
- l.pause = null;
- l.seeking = null;
-}
diff --git a/src/internal/framing.js b/src/internal/framing.js
deleted file mode 100644
index dc2c84f..0000000
--- a/src/internal/framing.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import { renderer } from '../renderer/renderer.js';
-import { batchAnimate } from '../utils.js';
-
-export function framing() {
- const vct = this.video.currentTime;
- for (let i = this._.stagings.length - 1; i >= 0; i--) {
- const dia = this._.stagings[i];
- let { end } = dia;
- if (dia.effect && /scroll/.test(dia.effect.name)) {
- const { y1, y2, delay } = dia.effect;
- const duration = ((y2 || this._.resampledRes.height) - y1) / (1000 / delay);
- end = Math.min(end, dia.start + duration);
- }
- if (end < vct) {
- this._.$stage.removeChild(dia.$div);
- if (dia.$clipPath) {
- this._.$defs.removeChild(dia.$clipPath);
- }
- this._.stagings.splice(i, 1);
- }
- }
- const dias = this.dialogues;
- while (
- this._.index < dias.length
- && vct >= dias[this._.index].start
- ) {
- if (vct < dias[this._.index].end) {
- const dia = renderer.call(this, dias[this._.index]);
- if (!this.video.paused) {
- batchAnimate(dia.$div, 'play');
- }
- this._.stagings.push(dia);
- }
- ++this._.index;
- }
-}
diff --git a/src/internal/hide.js b/src/internal/hide.js
deleted file mode 100644
index 9050204..0000000
--- a/src/internal/hide.js
+++ /dev/null
@@ -1,4 +0,0 @@
-export function hide() {
- this._.$stage.style.visibility = 'hidden';
- return this;
-}
diff --git a/src/internal/init.js b/src/internal/init.js
deleted file mode 100644
index d977aaf..0000000
--- a/src/internal/init.js
+++ /dev/null
@@ -1,82 +0,0 @@
-import { compile } from 'ass-compiler';
-import { assign } from 'ass-compiler/src/utils';
-import { bindEvents } from './events.js';
-import { play } from './play.js';
-import { resize } from './resize.js';
-import { seek } from './seek.js';
-import { $fixFontSize } from '../renderer/font-size.js';
-import { createSVGEl, getStyleRoot } from '../utils.js';
-
-const GLOBAL_CSS = '__GLOBAL_CSS__';
-
-export function init(source, video, options = {}) {
- this.scale = 1;
-
- // private variables
- this._ = {
- index: 0,
- stagings: [],
- space: [],
- listener: {},
- $svg: createSVGEl('svg'),
- $defs: createSVGEl('defs'),
- $stage: document.createElement('div'),
- };
- this._.$svg.appendChild(this._.$defs);
- this._.$stage.className = 'ASS-stage';
-
- this._.resampling = options.resampling || 'video_height';
-
- this.container = options.container || document.createElement('div');
- this.container.classList.add('ASS-container');
- this.container.appendChild($fixFontSize);
- this.container.appendChild(this._.$svg);
- this._.hasInitContainer = !!options.container;
-
- this.video = video;
- bindEvents.call(this);
- if (!this._.hasInitContainer) {
- const isPlaying = !video.paused;
- video.parentNode.insertBefore(this.container, video);
- this.container.appendChild(video);
- if (isPlaying && video.paused) {
- video.play();
- }
- }
- this.container.appendChild(this._.$stage);
-
- const { info, width, height, styles, dialogues } = compile(source);
- this.info = info;
- this._.scriptRes = {
- width: width || video.videoWidth,
- height: height || video.videoHeight,
- };
- this.styles = styles;
- this.dialogues = dialogues.map((dia) => assign(dia, {
- align: {
- // 0: left, 1: center, 2: right
- h: (dia.alignment + 2) % 3,
- // 0: top, 1: center, 2: bottom
- v: (dia.alignment - 1) / 3 | 0,
- },
- }));
-
- const styleRoot = getStyleRoot(this.container);
- let $style = styleRoot.querySelector('#ASS-global-style');
- if (!$style) {
- $style = document.createElement('style');
- $style.type = 'text/css';
- $style.id = 'ASS-global-style';
- $style.appendChild(document.createTextNode(GLOBAL_CSS));
- styleRoot.appendChild($style);
- }
-
- resize.call(this);
-
- if (!this.video.paused) {
- seek.call(this);
- play.call(this);
- }
-
- return this;
-}
diff --git a/src/internal/pause.js b/src/internal/pause.js
deleted file mode 100644
index 1f9cceb..0000000
--- a/src/internal/pause.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import { batchAnimate } from '../utils.js';
-
-export function pause() {
- cancelAnimationFrame(this._.requestId);
- this._.requestId = 0;
- this._.stagings.forEach(({ $div }) => {
- batchAnimate($div, 'pause');
- });
- return this;
-}
diff --git a/src/internal/play.js b/src/internal/play.js
deleted file mode 100644
index fe9ec30..0000000
--- a/src/internal/play.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import { framing } from './framing.js';
-import { batchAnimate } from '../utils.js';
-
-export function play() {
- const frame = () => {
- framing.call(this);
- this._.requestId = requestAnimationFrame(frame);
- };
- cancelAnimationFrame(this._.requestId);
- this._.requestId = requestAnimationFrame(frame);
- this._.stagings.forEach(({ $div }) => {
- batchAnimate($div, 'play');
- });
- return this;
-}
diff --git a/src/internal/resampling.js b/src/internal/resampling.js
deleted file mode 100644
index 30417c2..0000000
--- a/src/internal/resampling.js
+++ /dev/null
@@ -1,14 +0,0 @@
-const regex = /^(video|script)_(width|height)$/;
-
-export function getter() {
- return regex.test(this._.resampling) ? this._.resampling : 'video_height';
-}
-
-export function setter(r) {
- if (r === this._.resampling) return r;
- if (regex.test(r)) {
- this._.resampling = r;
- this.resize();
- }
- return this._.resampling;
-}
diff --git a/src/internal/resize.js b/src/internal/resize.js
deleted file mode 100644
index fe28d1c..0000000
--- a/src/internal/resize.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import { seek } from './seek.js';
-import { setKeyframes } from '../renderer/animation.js';
-
-export function resize() {
- const cw = this.video.clientWidth;
- const ch = this.video.clientHeight;
- const vw = this.video.videoWidth || cw;
- const vh = this.video.videoHeight || ch;
- const sw = this._.scriptRes.width;
- const sh = this._.scriptRes.height;
- let rw = sw;
- let rh = sh;
- const videoScale = Math.min(cw / vw, ch / vh);
- if (this.resampling === 'video_width') {
- rh = sw / vw * vh;
- }
- if (this.resampling === 'video_height') {
- rw = sh / vh * vw;
- }
- this.scale = Math.min(cw / rw, ch / rh);
- if (this.resampling === 'script_width') {
- this.scale = videoScale * (vw / rw);
- }
- if (this.resampling === 'script_height') {
- this.scale = videoScale * (vh / rh);
- }
- this.width = this.scale * rw;
- this.height = this.scale * rh;
- this._.resampledRes = { width: rw, height: rh };
-
- this.container.style.cssText = `width:${cw}px;height:${ch}px;`;
- const cssText = (
- `width:${this.width}px;`
- + `height:${this.height}px;`
- + `top:${(ch - this.height) / 2}px;`
- + `left:${(cw - this.width) / 2}px;`
- );
- this._.$stage.style.cssText = cssText;
- this._.$svg.style.cssText = cssText;
- this._.$svg.setAttributeNS(null, 'viewBox', `0 0 ${sw} ${sh}`);
-
- this.dialogues.forEach((dialogue) => {
- setKeyframes.call(this, dialogue);
- });
- seek.call(this);
-
- return this;
-}
diff --git a/src/internal/seek.js b/src/internal/seek.js
deleted file mode 100644
index 393c00f..0000000
--- a/src/internal/seek.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import { clear } from './clear.js';
-import { framing } from './framing.js';
-
-export function seek() {
- const vct = this.video.currentTime;
- const dias = this.dialogues;
- clear.call(this);
- this._.index = (() => {
- let from = 0;
- const to = dias.length - 1;
- while (from + 1 < to && vct > dias[(to + from) >> 1].end) {
- from = (to + from) >> 1;
- }
- if (!from) return 0;
- for (let i = from; i < to; i++) {
- if (
- dias[i].end > vct && vct >= dias[i].start
- || (i && dias[i - 1].end < vct && vct < dias[i].start)
- ) {
- return i;
- }
- }
- return to;
- })();
- framing.call(this);
-}
diff --git a/src/internal/show.js b/src/internal/show.js
deleted file mode 100644
index 4b287fe..0000000
--- a/src/internal/show.js
+++ /dev/null
@@ -1,4 +0,0 @@
-export function show() {
- this._.$stage.style.visibility = 'visible';
- return this;
-}
diff --git a/src/renderer/animation.js b/src/renderer/animation.js
index f4e1602..91dc9e6 100644
--- a/src/renderer/animation.js
+++ b/src/renderer/animation.js
@@ -1,4 +1,3 @@
-import { assign } from 'ass-compiler/src/utils.js';
import { color2rgba } from '../utils.js';
import { getRealFontSize } from './font-size.js';
import { createTransform } from './transform.js';
@@ -10,28 +9,28 @@ function mergeT(ts) {
return results
.map((r) => {
merged = t.t1 === r.t1 && t.t2 === r.t2 && t.accel === r.accel;
- return assign({}, r, merged ? { tag: assign({}, r.tag, t.tag) } : {});
+ return { ...r, ...(merged ? { tag: { ...r.tag, ...t.tag } } : {}) };
})
.concat(merged ? [] : t);
}, []);
}
-function createEffectKeyframes({ effect, duration }) {
+function createEffectKeyframes({ effect, duration }, store) {
// TODO: when effect and move both exist, its behavior is weird, for now only move works.
const { name, delay, lefttoright, y1 } = effect;
- const y2 = effect.y2 || this._.resampledRes.height;
+ const y2 = effect.y2 || store.resampledRes.height;
if (name === 'banner') {
- const tx = this.scale * (duration / delay) * (lefttoright ? 1 : -1);
+ const tx = store.scale * (duration / delay) * (lefttoright ? 1 : -1);
return [0, `${tx}px`].map((x, i) => ({
offset: i,
transform: `translateX(${x})`,
}));
}
- if (/^scroll/.test(name)) {
+ if (name.startsWith('scroll')) {
const updown = /up/.test(name) ? -1 : 1;
const dp = (y2 - y1) / (duration / delay);
return [y1, y2]
- .map((y) => this.scale * y * updown)
+ .map((y) => store.scale * y * updown)
.map((y, i) => ({
offset: Math.min(i, dp),
transform: `translateY${y}`,
@@ -40,12 +39,12 @@ function createEffectKeyframes({ effect, duration }) {
return [];
}
-function createMoveKeyframes({ move, duration, dialogue }) {
+function createMoveKeyframes({ move, duration, dialogue }, store) {
const { x1, y1, x2, y2, t1, t2 } = move;
const t = [t1, t2 || duration];
const pos = dialogue.pos || { x: 0, y: 0 };
return [[x1, y1], [x2, y2]]
- .map(([x, y]) => [this.scale * (x - pos.x), this.scale * (y - pos.y)])
+ .map(([x, y]) => [store.scale * (x - pos.x), store.scale * (y - pos.y)])
.map(([x, y], index) => ({
offset: Math.min(t[index] / duration, 1),
transform: `translate(${x}px, ${y}px)`,
@@ -88,45 +87,45 @@ export function createFadeKeyframes(fade, duration) {
}
function createTransformKeyframes({ fromTag, tag, fragment }) {
- const toTag = assign({}, fromTag, tag);
+ const toTag = { ...fromTag, ...tag };
if (fragment.drawing) {
// scales will be handled inside svg
- assign(toTag, {
+ Object.assign(toTag, {
p: 0,
fscx: ((tag.fscx || fromTag.fscx) / fromTag.fscx) * 100,
fscy: ((tag.fscy || fromTag.fscy) / fromTag.fscy) * 100,
});
- assign(fromTag, { fscx: 100, fscy: 100 });
+ Object.assign(fromTag, { fscx: 100, fscy: 100 });
}
return { transform: createTransform(toTag) };
}
// TODO: accel is not implemented yet, maybe it can be simulated by cubic-bezier?
-export function setKeyframes(dialogue) {
+export function setKeyframes(dialogue, store) {
const { start, end, effect, move, fade, slices } = dialogue;
const duration = (end - start) * 1000;
const keyframes = [
- ...(effect && !move ? createEffectKeyframes.call(this, { effect, duration }) : []),
- ...(move ? createMoveKeyframes.call(this, { move, duration, dialogue }) : []),
+ ...(effect && !move ? createEffectKeyframes({ effect, duration }, store) : []),
+ ...(move ? createMoveKeyframes({ move, duration, dialogue }, store) : []),
...(fade ? createFadeKeyframes(fade, duration) : []),
].sort((a, b) => a.offset - b.offset);
- if (keyframes.length) {
- assign(dialogue, { keyframes });
+ if (keyframes.length > 0) {
+ Object.assign(dialogue, { keyframes });
}
slices.forEach((slice) => {
- const sliceTag = this.styles[slice.style].tag;
+ const sliceTag = store.styles[slice.style].tag;
slice.fragments.forEach((fragment) => {
- if (!fragment.tag.t || !fragment.tag.t.length) {
+ if (!fragment.tag.t || fragment.tag.t.length === 0) {
return;
}
- const fromTag = assign({}, sliceTag, fragment.tag);
+ const fromTag = { ...sliceTag, ...fragment.tag };
const tTags = mergeT(fragment.tag.t).sort((a, b) => a.t2 - b.t2 || a.t1 - b.t1);
if (tTags[0].t1 > 0) {
tTags.unshift({ t1: 0, t2: tTags[0].t1, tag: fromTag });
}
tTags.reduce((prevTag, curr) => {
- const tag = assign({}, prevTag, curr.tag);
- assign(curr.tag, tag);
+ const tag = { ...prevTag, ...curr.tag };
+ Object.assign(curr.tag, tag);
return tag;
}, {});
const fDuration = Math.max(duration, ...tTags.map(({ t2 }) => t2));
@@ -137,19 +136,20 @@ export function setKeyframes(dialogue) {
&& tag.a2 === tag.a3
&& tag.a3 === tag.a4
);
+ // TODO: border and shadow, should animate CSS vars
return {
offset: t2 / fDuration,
- ...(tag.fs && { 'font-size': `${this.scale * getRealFontSize(tag.fn, tag.fs)}px` }),
- ...(tag.fsp && { 'letter-spacing': `${this.scale * tag.fsp}px` }),
+ ...(tag.fs && { 'font-size': `${store.scale * getRealFontSize(tag.fn, tag.fs)}px` }),
+ ...(tag.fsp && { 'letter-spacing': `${store.scale * tag.fsp}px` }),
...((tag.c1 || (tag.a1 && !hasAlpha)) && {
color: color2rgba((tag.a1 || fromTag.a1) + (tag.c1 || fromTag.c1)),
}),
- ...(hasAlpha && { opacity: 1 - parseInt(tag.a1, 16) / 255 }),
+ ...(hasAlpha && { opacity: 1 - Number.parseInt(tag.a1, 16) / 255 }),
...createTransformKeyframes({ fromTag, tag, fragment }),
};
}).sort((a, b) => a.offset - b.offset);
- if (kfs.length) {
- assign(fragment, { keyframes: kfs, duration: fDuration });
+ if (kfs.length > 0) {
+ Object.assign(fragment, { keyframes: kfs, duration: fDuration });
}
});
});
diff --git a/src/renderer/clip.js b/src/renderer/clip.js
index 1d87893..a0dfef1 100644
--- a/src/renderer/clip.js
+++ b/src/renderer/clip.js
@@ -1,9 +1,8 @@
-import { assign } from 'ass-compiler/src/utils.js';
import { createSVGEl, uuid, vendor } from '../utils.js';
-export function createClipPath(clip) {
- const sw = this._.scriptRes.width;
- const sh = this._.scriptRes.height;
+export function createClipPath(clip, store) {
+ const sw = store.scriptRes.width;
+ const sh = store.scriptRes.height;
let d = '';
if (clip.dots !== null) {
let { x1, y1, x2, y2 } = clip.dots;
@@ -27,28 +26,26 @@ export function createClipPath(clip) {
['id', id],
['clipPathUnits', 'objectBoundingBox'],
]);
- $clipPath.appendChild(createSVGEl('path', [
+ $clipPath.append(createSVGEl('path', [
['d', d],
['transform', `scale(${scale})`],
['clip-rule', 'evenodd'],
]));
- this._.$defs.appendChild($clipPath);
+ store.defs.append($clipPath);
return {
$clipPath,
cssText: `${vendor.clipPath}clip-path:url(#${id});`,
};
}
-export function setClipPath(dialogue) {
- if (!dialogue.clip) {
- return;
- }
+export function getClipPath(dialogue, store) {
+ if (!dialogue.clip) return {};
const $fobb = document.createElement('div');
- this._.$stage.insertBefore($fobb, dialogue.$div);
- $fobb.appendChild(dialogue.$div);
+ store.box.insertBefore($fobb, dialogue.$div);
+ $fobb.append(dialogue.$div);
$fobb.className = 'ASS-fix-objectBoundingBox';
- const { cssText, $clipPath } = createClipPath.call(this, dialogue.clip);
- this._.$defs.appendChild($clipPath);
+ const { cssText, $clipPath } = createClipPath(dialogue.clip, store);
+ store.defs.append($clipPath);
$fobb.style.cssText = cssText;
- assign(dialogue, { $div: $fobb, $clipPath });
+ return { $div: $fobb, $clipPath };
}
diff --git a/src/renderer/dom.js b/src/renderer/dom.js
index 122136f..ddc267c 100644
--- a/src/renderer/dom.js
+++ b/src/renderer/dom.js
@@ -1,4 +1,3 @@
-import { assign } from 'ass-compiler/src/utils.js';
import { color2rgba, transformTags, initAnimation } from '../utils.js';
import { createDrawing } from './drawing.js';
import { getRealFontSize } from './font-size.js';
@@ -12,30 +11,31 @@ function encodeText(text, q) {
.replace(/\\n/g, q === 2 ? '\n' : ' ');
}
-export function createDialogue(dialogue) {
+export function createDialogue(dialogue, store) {
+ const { video, styles, info } = store;
const $div = document.createElement('div');
$div.className = 'ASS-dialogue';
const df = document.createDocumentFragment();
const { slices, start, end } = dialogue;
const animationOptions = {
duration: (end - start) * 1000,
- delay: Math.min(0, start - this.video.currentTime) * 1000,
+ delay: Math.min(0, start - video.currentTime) * 1000,
fill: 'forwards',
};
$div.animations = [];
slices.forEach((slice) => {
- const sliceTag = this.styles[slice.style].tag;
- const borderStyle = this.styles[slice.style].style.BorderStyle;
+ const sliceTag = styles[slice.style].tag;
+ const borderStyle = styles[slice.style].style.BorderStyle;
slice.fragments.forEach((fragment) => {
const { text, drawing } = fragment;
- const tag = assign({}, sliceTag, fragment.tag);
+ const tag = { ...sliceTag, ...fragment.tag };
let cssText = 'display:inline-block;';
const cssVars = [];
if (!drawing) {
cssText += `line-height:normal;font-family:"${tag.fn}",Arial;`;
- cssText += `font-size:${this.scale * getRealFontSize(tag.fn, tag.fs)}px;`;
+ cssText += `font-size:${store.scale * getRealFontSize(tag.fn, tag.fs)}px;`;
cssText += `color:${color2rgba(tag.a1 + tag.c1)};`;
- const scale = /Yes/i.test(this.info.ScaledBorderAndShadow) ? this.scale : 1;
+ const scale = /yes/i.test(info.ScaledBorderAndShadow) ? store.scale : 1;
if (borderStyle === 1) {
cssVars.push(...createCSSStroke(tag, scale));
}
@@ -78,22 +78,24 @@ export function createDialogue(dialogue) {
}
}
if (drawing && tag.pbo) {
- const pbo = this.scale * -tag.pbo * (tag.fscy || 100) / 100;
+ const pbo = store.scale * -tag.pbo * (tag.fscy || 100) / 100;
cssText += `vertical-align:${pbo}px;`;
}
- const hasRotate = /"fr[xyz]":[^0]/.test(JSON.stringify(tag));
+ const hasRotate = /"fr[x-z]":[^0]/.test(JSON.stringify(tag));
encodeText(text, tag.q).split('\n').forEach((content, idx) => {
const $span = document.createElement('span');
- $span._hasRotate = hasRotate;
+ if (hasRotate) {
+ $span.dataset.hasRotate = '';
+ }
if (drawing) {
- const obj = createDrawing.call(this, fragment, sliceTag);
+ const obj = createDrawing(fragment, sliceTag, store);
if (!obj) return;
$span.style.cssText = obj.cssText;
- $span.appendChild(obj.$svg);
+ $span.append(obj.$svg);
} else {
if (idx) {
- df.appendChild(document.createElement('br'));
+ df.append(document.createElement('br'));
}
if (!content) return;
$span.textContent = content;
@@ -110,17 +112,17 @@ export function createDialogue(dialogue) {
const animation = initAnimation(
$span,
fragment.keyframes,
- assign({}, animationOptions, { duration: fragment.duration }),
+ { ...animationOptions, duration: fragment.duration },
);
$div.animations.push(animation);
}
- df.appendChild($span);
+ df.append($span);
});
});
});
if (dialogue.keyframes) {
$div.animations.push(initAnimation($div, dialogue.keyframes, animationOptions));
}
- $div.appendChild(df);
+ $div.append(df);
return $div;
}
diff --git a/src/renderer/drawing.js b/src/renderer/drawing.js
index 9365c77..f51dc04 100644
--- a/src/renderer/drawing.js
+++ b/src/renderer/drawing.js
@@ -1,12 +1,12 @@
-import { assign } from 'ass-compiler/src/utils.js';
import { uuid, createSVGEl } from '../utils.js';
import { createSVGStroke } from './stroke.js';
-export function createDrawing(fragment, styleTag) {
+export function createDrawing(fragment, styleTag, store) {
if (!fragment.drawing.d) return null;
- const tag = assign({}, styleTag, fragment.tag);
+ const { scale, info } = store;
+ const tag = { ...styleTag, ...fragment.tag };
const { minX, minY, width, height } = fragment.drawing;
- const baseScale = this.scale / (1 << (tag.p - 1));
+ const baseScale = scale / (1 << (tag.p - 1));
const scaleX = (tag.fscx ? tag.fscx / 100 : 1) * baseScale;
const scaleY = (tag.fscy ? tag.fscy / 100 : 1) * baseScale;
const blur = tag.blur || tag.be || 0;
@@ -19,19 +19,19 @@ export function createDrawing(fragment, styleTag) {
['height', vbh],
['viewBox', `${-vbx} ${-vby} ${vbw} ${vbh}`],
]);
- const strokeScale = /Yes/i.test(this.info.ScaledBorderAndShadow) ? this.scale : 1;
+ const strokeScale = /yes/i.test(info.ScaledBorderAndShadow) ? scale : 1;
const filterId = `ASS-${uuid()}`;
const $defs = createSVGEl('defs');
- $defs.appendChild(createSVGStroke(tag, filterId, strokeScale));
- $svg.appendChild($defs);
+ $defs.append(createSVGStroke(tag, filterId, strokeScale));
+ $svg.append($defs);
const symbolId = `ASS-${uuid()}`;
const $symbol = createSVGEl('symbol', [
['id', symbolId],
['viewBox', `${minX} ${minY} ${width} ${height}`],
]);
- $symbol.appendChild(createSVGEl('path', [['d', fragment.drawing.d]]));
- $svg.appendChild($symbol);
- $svg.appendChild(createSVGEl('use', [
+ $symbol.append(createSVGEl('path', [['d', fragment.drawing.d]]));
+ $svg.append($symbol);
+ $svg.append(createSVGEl('use', [
['width', width * scaleX],
['height', height * scaleY],
['xlink:href', `#${symbolId}`],
diff --git a/src/renderer/position.js b/src/renderer/position.js
index 5b8da7e..fe7fdef 100644
--- a/src/renderer/position.js
+++ b/src/renderer/position.js
@@ -1,15 +1,16 @@
-function allocate(dialogue) {
+function allocate(dialogue, store) {
+ const { video, space, scale } = store;
const { layer, margin, width, height, alignment, end } = dialogue;
- const stageWidth = this.width - (this.scale * (margin.left + margin.right) | 0);
- const stageHeight = this.height;
- const vertical = this.scale * margin.vertical | 0;
- const vct = this.video.currentTime * 100;
- this._.space[layer] = this._.space[layer] || {
+ 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 = this._.space[layer];
+ const channel = space[layer];
const alignH = ['right', 'left', 'center'][alignment % 3];
const willCollide = (y) => {
const lw = channel.left.width[y];
@@ -47,65 +48,64 @@ function allocate(dialogue) {
return false;
};
if (alignment <= 3) {
- for (let i = stageHeight - vertical - 1; i > vertical; i--) {
+ for (let i = stageHeight - vertical - 1; i > vertical; i -= 1) {
if (find(i)) break;
}
} else if (alignment >= 7) {
- for (let i = vertical + 1; i < stageHeight - vertical; i++) {
+ for (let i = vertical + 1; i < stageHeight - vertical; i += 1) {
if (find(i)) break;
}
} else {
- for (let i = (stageHeight - height) >> 1; i < stageHeight - vertical; i++) {
+ for (let i = (stageHeight - height) >> 1; i < stageHeight - vertical; i += 1) {
if (find(i)) break;
}
}
if (alignment > 3) {
result -= height - 1;
}
- for (let i = result; i < result + height; i++) {
+ for (let i = result; i < result + height; i += 1) {
channel[alignH].width[i] = width;
channel[alignH].end[i] = end * 100;
}
return result;
}
-export function getPosition(dialogue) {
+export function getPosition(dialogue, store) {
+ const { scale } = store;
const { effect, move, align, width, height, margin, slices } = dialogue;
let x = 0;
let y = 0;
if (effect) {
if (effect.name === 'banner') {
- x = effect.lefttoright ? -width : this.width;
+ x = effect.lefttoright ? -width : store.width;
y = [
- this.height - height - margin.vertical,
- (this.height - height) / 2,
+ store.height - height - margin.vertical,
+ (store.height - height) / 2,
margin.vertical,
][align.v];
}
} else if (dialogue.pos || move) {
const pos = dialogue.pos || { x: 0, y: 0 };
- const sx = this.scale * pos.x;
- const sy = this.scale * pos.y;
+ 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,
- (this.width - width) / 2,
- this.width - width - this.scale * margin.right,
+ (store.width - width) / 2,
+ store.width - width - scale * margin.right,
][align.h];
const hasT = slices.some((slice) => (
slice.fragments.some(({ animationName }) => animationName)
));
- if (hasT) {
- y = [
- this.height - height - margin.vertical,
- (this.height - height) / 2,
+ y = hasT
+ ? [
+ store.height - height - margin.vertical,
+ (store.height - height) / 2,
margin.vertical,
- ][align.v];
- } else {
- y = allocate.call(this, dialogue);
- }
+ ][align.v]
+ : allocate(dialogue, store);
}
return { x, y };
}
diff --git a/src/renderer/renderer.js b/src/renderer/renderer.js
index b063402..57a0050 100644
--- a/src/renderer/renderer.js
+++ b/src/renderer/renderer.js
@@ -1,19 +1,18 @@
-import { assign } from 'ass-compiler/src/utils.js';
-import { setClipPath } from './clip.js';
+import { getClipPath } from './clip.js';
import { createDialogue } from './dom.js';
import { getPosition } from './position.js';
import { createStyle } from './style.js';
import { setTransformOrigin } from './transform.js';
-export function renderer(dialogue) {
- const $div = createDialogue.call(this, dialogue);
- assign(dialogue, { $div });
- this._.$stage.appendChild($div);
+export function renderer(dialogue, store) {
+ const $div = createDialogue(dialogue, store);
+ Object.assign(dialogue, { $div });
+ store.box.append($div);
const { width, height } = $div.getBoundingClientRect();
- assign(dialogue, { width, height });
- assign(dialogue, getPosition.call(this, dialogue));
- $div.style.cssText = createStyle.call(this, dialogue);
- setTransformOrigin(dialogue, this.scale);
- setClipPath.call(this, dialogue);
+ Object.assign(dialogue, { width, height });
+ Object.assign(dialogue, getPosition(dialogue, store));
+ $div.style.cssText = createStyle(dialogue, store);
+ setTransformOrigin(dialogue, store.scale);
+ Object.assign(dialogue, getClipPath(dialogue, store));
return dialogue;
}
diff --git a/src/renderer/stroke.js b/src/renderer/stroke.js
index f53c462..58a7b42 100644
--- a/src/renderer/stroke.js
+++ b/src/renderer/stroke.js
@@ -6,44 +6,44 @@ export function createSVGStroke(tag, id, scale) {
const isOpaque = tag.a1 !== 'FF';
const blur = tag.blur || tag.be || 0;
const $filter = createSVGEl('filter', [['id', id]]);
- $filter.appendChild(createSVGEl('feGaussianBlur', [
+ $filter.append(createSVGEl('feGaussianBlur', [
['stdDeviation', hasBorder ? 0 : blur],
['in', 'SourceGraphic'],
['result', 'sg_b'],
]));
- $filter.appendChild(createSVGEl('feFlood', [
+ $filter.append(createSVGEl('feFlood', [
['flood-color', color2rgba(tag.a1 + tag.c1)],
['result', 'c1'],
]));
- $filter.appendChild(createSVGEl('feComposite', [
+ $filter.append(createSVGEl('feComposite', [
['operator', 'in'],
['in', 'c1'],
['in2', 'sg_b'],
['result', 'main'],
]));
if (hasBorder) {
- $filter.appendChild(createSVGEl('feMorphology', [
+ $filter.append(createSVGEl('feMorphology', [
['radius', `${tag.xbord * scale} ${tag.ybord * scale}`],
['operator', 'dilate'],
['in', 'SourceGraphic'],
['result', 'dil'],
]));
- $filter.appendChild(createSVGEl('feGaussianBlur', [
+ $filter.append(createSVGEl('feGaussianBlur', [
['stdDeviation', blur],
['in', 'dil'],
['result', 'dil_b'],
]));
- $filter.appendChild(createSVGEl('feComposite', [
+ $filter.append(createSVGEl('feComposite', [
['operator', 'out'],
['in', 'dil_b'],
['in2', 'SourceGraphic'],
['result', 'dil_b_o'],
]));
- $filter.appendChild(createSVGEl('feFlood', [
+ $filter.append(createSVGEl('feFlood', [
['flood-color', color2rgba(tag.a3 + tag.c3)],
['result', 'c3'],
]));
- $filter.appendChild(createSVGEl('feComposite', [
+ $filter.append(createSVGEl('feComposite', [
['operator', 'in'],
['in', 'c3'],
['in2', 'dil_b_o'],
@@ -51,36 +51,36 @@ export function createSVGStroke(tag, id, scale) {
]));
}
if (hasShadow && (hasBorder || isOpaque)) {
- $filter.appendChild(createSVGEl('feOffset', [
+ $filter.append(createSVGEl('feOffset', [
['dx', tag.xshad * scale],
['dy', tag.yshad * scale],
['in', hasBorder ? 'dil' : 'SourceGraphic'],
['result', 'off'],
]));
- $filter.appendChild(createSVGEl('feGaussianBlur', [
+ $filter.append(createSVGEl('feGaussianBlur', [
['stdDeviation', blur],
['in', 'off'],
['result', 'off_b'],
]));
if (!isOpaque) {
- $filter.appendChild(createSVGEl('feOffset', [
+ $filter.append(createSVGEl('feOffset', [
['dx', tag.xshad * scale],
['dy', tag.yshad * scale],
['in', 'SourceGraphic'],
['result', 'sg_off'],
]));
- $filter.appendChild(createSVGEl('feComposite', [
+ $filter.append(createSVGEl('feComposite', [
['operator', 'out'],
['in', 'off_b'],
['in2', 'sg_off'],
['result', 'off_b_o'],
]));
}
- $filter.appendChild(createSVGEl('feFlood', [
+ $filter.append(createSVGEl('feFlood', [
['flood-color', color2rgba(tag.a4 + tag.c4)],
['result', 'c4'],
]));
- $filter.appendChild(createSVGEl('feComposite', [
+ $filter.append(createSVGEl('feComposite', [
['operator', 'in'],
['in', 'c4'],
['in2', isOpaque ? 'off_b' : 'off_b_o'],
@@ -89,13 +89,13 @@ export function createSVGStroke(tag, id, scale) {
}
const $merge = createSVGEl('feMerge', []);
if (hasShadow && (hasBorder || isOpaque)) {
- $merge.appendChild(createSVGEl('feMergeNode', [['in', 'shadow']]));
+ $merge.append(createSVGEl('feMergeNode', [['in', 'shadow']]));
}
if (hasBorder) {
- $merge.appendChild(createSVGEl('feMergeNode', [['in', 'border']]));
+ $merge.append(createSVGEl('feMergeNode', [['in', 'border']]));
}
- $merge.appendChild(createSVGEl('feMergeNode', [['in', 'main']]));
- $filter.appendChild($merge);
+ $merge.append(createSVGEl('feMergeNode', [['in', 'main']]));
+ $filter.append($merge);
return $filter;
}
@@ -119,8 +119,7 @@ function getOffsets(x, y) {
return Array.from({ length: Math.ceil(ny) - 1 }, (_, i) => i + 1).concat(ny)
.map((n) => [(ny - n) / ny * nx, n])
.map(([i, j]) => (x > y ? [j, i] : [i, j]))
- .map(get4QuadrantPoints)
- .flat();
+ .flatMap(get4QuadrantPoints);
}
// TODO: a1 === 'ff'
@@ -137,11 +136,11 @@ export function createCSSStroke(tag, scale) {
{ key: 'border-width', value: `${Math.min(bx, by) * 2}px` },
{ key: 'border-color', value: bc },
{ key: 'border-opacity', value: alpha2opacity(tag.a3) },
- { key: 'border-delta', value: deltaOffsets.map(([x, y]) => `${x}px ${y}px ${bc}`).join() },
+ { key: 'border-delta', value: deltaOffsets.map(([x, y]) => `${x}px ${y}px ${bc}`).join(',') },
{ key: 'shadow-offset', value: `${sx}px, ${sy}px` },
{ key: 'shadow-color', value: sc },
{ key: 'shadow-opacity', value: alpha2opacity(tag.a4) },
- { key: 'shadow-delta', value: deltaOffsets.map(([x, y]) => `${x}px ${y}px ${sc}`).join() },
+ { key: 'shadow-delta', value: deltaOffsets.map(([x, y]) => `${x}px ${y}px ${sc}`).join(',') },
{ key: 'blur', value: `blur(${blur}px)` },
].map((kv) => Object.assign(kv, { key: `--ass-${kv.key}` }));
}
diff --git a/src/renderer/style.js b/src/renderer/style.js
index e65c902..e0052d1 100644
--- a/src/renderer/style.js
+++ b/src/renderer/style.js
@@ -1,22 +1,22 @@
-export function createStyle(dialogue) {
+export function createStyle(dialogue, store) {
const { layer, align, effect, pos, margin } = dialogue;
const { width, height, x, y } = dialogue;
let cssText = '';
if (layer) cssText += `z-index:${layer};`;
cssText += `text-align:${['left', 'center', 'right'][align.h]};`;
if (!effect) {
- const mw = this.width - this.scale * (margin.left + margin.right);
+ const mw = store.width - store.scale * (margin.left + margin.right);
cssText += `max-width:${mw}px;`;
if (!pos) {
if (align.h === 0) {
- cssText += `margin-left:${this.scale * margin.left}px;`;
+ cssText += `margin-left:${store.scale * margin.left}px;`;
}
if (align.h === 2) {
- cssText += `margin-right:${this.scale * margin.right}px;`;
+ cssText += `margin-right:${store.scale * margin.right}px;`;
}
- if (width > this.width - this.scale * (margin.left + margin.right)) {
- cssText += `margin-left:${this.scale * margin.left}px;`;
- cssText += `margin-right:${this.scale * margin.right}px;`;
+ if (width > store.width - store.scale * (margin.left + margin.right)) {
+ cssText += `margin-left:${store.scale * margin.left}px;`;
+ cssText += `margin-right:${store.scale * margin.right}px;`;
}
}
}
diff --git a/src/renderer/transform.js b/src/renderer/transform.js
index 8b2f02d..7b4c8c0 100644
--- a/src/renderer/transform.js
+++ b/src/renderer/transform.js
@@ -20,9 +20,9 @@ export function setTransformOrigin(dialogue, scale) {
org.x = [x, x + width / 2, x + width][align.h];
org.y = [y + height, y + height / 2, y][align.v];
}
- for (let i = $div.childNodes.length - 1; i >= 0; i--) {
+ for (let i = $div.childNodes.length - 1; i >= 0; i -= 1) {
const node = $div.childNodes[i];
- if (node._hasRotate) {
+ if (node.dataset.hasRotate === '') {
// It's not extremely precise for offsets are round the value to an integer.
const tox = org.x - x - node.offsetLeft;
const toy = org.y - y - node.offsetTop;
diff --git a/src/utils.js b/src/utils.js
index 2cbc8ff..b35c02a 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -13,15 +13,20 @@ export function color2rgba(c) {
export function uuid() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
- const r = Math.random() * 16 | 0;
+ 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
+ */
export function createSVGEl(name, attrs = []) {
const $el = document.createElementNS('http://www.w3.org/2000/svg', name);
- for (let i = 0; i < attrs.length; i++) {
+ 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,
@@ -45,9 +50,21 @@ export const vendor = {
clipPath: getVendor('clipPath'),
};
-export function getStyleRoot(container) {
- const rootNode = container.getRootNode ? container.getRootNode() : document;
- return rootNode === document ? rootNode.head : rootNode;
+const GLOBAL_CSS = '__GLOBAL_CSS__';
+/**
+ * @param {HTMLElement} container
+ */
+export 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);
+ }
}
export const strokeTags = ['c3', 'a3', 'c4', 'a4', 'xbord', 'ybord', 'xshad', 'yshad', 'blur', 'be'];
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..95c1749
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "http://json.schemastore.org/tsconfig",
+ "compilerOptions": {
+ "target": "ESNext",
+ "declaration": true,
+ "emitDeclarationOnly": true,
+ "declarationDir": "./types",
+ "allowJs": true,
+ },
+ "include": [
+ "src/index.js"
+ ]
+}