Skip to content

Commit 0256425

Browse files
committed
refactor: add wrapper for all effects and support fadeaway
1 parent 9966471 commit 0256425

File tree

10 files changed

+88
-78
lines changed

10 files changed

+88
-78
lines changed

Diff for: README.md

+1-6
Original file line numberDiff line numberDiff line change
@@ -139,17 +139,12 @@ ASS.js uses many Web APIs to render subtitles, some features will be disabled if
139139

140140
* [Script Info]
141141
* __WrapStyle__: 3
142-
* __Collisions__: Reverse
143142
* [Events]
144143
* __Dialogue__
145-
+ __Effect__
146-
- __Scroll up__: fadeawayheight
147-
- __Scroll down__: fadeawayheight
148-
- __Banner__: fadeawaywidth
149144
+ __Text__ (override codes)
150145
- __\k, \kf, \ko, \kt, \K__: Karaoke
151146
- __\q__: 3
152-
- __\t([<t1>, <t2>, ][<accel>, ]<style modifiers>)__: <accel>, \2c, \2a
147+
- __\t([<t1>, <t2>, ][<accel>, ]<style modifiers>)__: <accel>
153148

154149
## Known issues
155150

Diff for: src/global.css

+12-1
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,19 @@
142142
top: 0;
143143
left: 0;
144144
}
145-
.ASS-scroll-area {
145+
.ASS-effect-area {
146146
position: absolute;
147+
display: flex;
147148
width: 100%;
149+
height: fit-content;
148150
overflow: hidden;
151+
mask-composite: intersect;
152+
}
153+
.ASS-effect-area[data-effect="banner"] {
154+
flex-direction: column;
155+
height: 100%;
156+
}
157+
.ASS-effect-area .ASS-dialogue {
158+
position: static;
159+
transform: none;
149160
}

Diff for: src/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ export default class ASS {
113113
};
114114
this.#store.styles = styles;
115115
this.#store.dialogues = dialogues.map((dia) => Object.assign(dia, {
116+
effect: ['banner', 'scroll up', 'scroll down'].includes(dia.effect?.name) ? dia.effect : null,
116117
align: {
117118
// 0: left, 1: center, 2: right
118119
h: (dia.alignment + 2) % 3,

Diff for: src/renderer/animation.js

+13-18
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { color2rgba } from '../utils.js';
1+
import { color2rgba, alpha2opacity, initAnimation } from '../utils.js';
22
import { getRealFontSize } from './font-size.js';
33
import { createCSSStroke } from './stroke.js';
44
import { createTransform } from './transform.js';
@@ -20,23 +20,18 @@ function mergeT(ts) {
2020
export function createEffectKeyframes({ effect, duration }) {
2121
// TODO: when effect and move both exist, its behavior is weird, for now only move works.
2222
const { name, delay, leftToRight } = effect;
23-
if (name === 'banner') {
24-
const tx = (duration / (delay || 1)) * (leftToRight ? 1 : -1);
25-
return [0, `calc(var(--ass-scale) * ${tx}px)`].map((x, i) => ({
26-
offset: i,
27-
transform: `translateX(${x})`,
28-
}));
29-
}
30-
if (name.startsWith('scroll')) {
31-
// speed is 1000px/s when delay=1
32-
const updown = /up/.test(name) ? -1 : 1;
33-
const y = duration / (delay || 1) * updown;
34-
return [
35-
{ offset: 0, transform: 'translateY(-100%)' },
36-
{ offset: 1, transform: `translateY(calc(var(--ass-scale) * ${y}px))` },
37-
];
38-
}
39-
return [];
23+
const translate = name === 'banner' ? 'X' : 'Y';
24+
const dir = ({
25+
X: leftToRight ? 1 : -1,
26+
Y: /up/.test(name) ? -1 : 1,
27+
})[translate];
28+
const start = -100 * dir;
29+
// speed is 1000px/s when delay=1
30+
const distance = (duration / (delay || 1)) * dir;
31+
return [
32+
{ offset: 0, transform: `translate${translate}(${start}%)` },
33+
{ offset: 1, transform: `translate${translate}(calc(${start}% + var(--ass-scale) * ${distance}px))` },
34+
];
4035
}
4136

4237
function createMoveKeyframes({ move, duration, dialogue }) {

Diff for: src/renderer/effect.js

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
export function setEffect(dialogue, store) {
2+
const $area = document.createElement('div');
3+
$area.className = 'ASS-effect-area';
4+
store.box.insertBefore($area, dialogue.$div);
5+
$area.append(dialogue.$div);
6+
const { width, height } = store.scriptRes;
7+
const { name, y1, y2, leftToRight, fadeAwayWidth, fadeAwayHeight } = dialogue.effect;
8+
const min = Math.min(y1, y2);
9+
const max = Math.max(y1, y2);
10+
$area.dataset.effect = name;
11+
if (name === 'banner') {
12+
$area.style.alignItems = leftToRight ? 'flex-start' : 'flex-end';
13+
$area.style.justifyContent = ['flex-end', 'center', 'flex-start'][dialogue.align.v];
14+
}
15+
if (name.startsWith('scroll')) {
16+
const top = min / height * 100;
17+
const bottom = (height - max) / height * 100;
18+
$area.style.cssText = `top:${top}%;bottom:${bottom}%;`;
19+
$area.style.justifyContent = ['flex-start', 'center', 'flex-end'][dialogue.align.h];
20+
}
21+
if (fadeAwayHeight) {
22+
const p = fadeAwayHeight / (max - min) * 100;
23+
$area.style.maskImage = [
24+
`linear-gradient(#000 ${100 - p}%, transparent)`,
25+
`linear-gradient(transparent, #000 ${p}%)`,
26+
].join(',');
27+
}
28+
if (fadeAwayWidth) {
29+
const p = fadeAwayWidth / width * 100;
30+
// only left side has fade away effect in VSFilter
31+
$area.style.maskImage = `linear-gradient(90deg, transparent, #000 ${p}%)`;
32+
}
33+
return $area;
34+
}

Diff for: src/renderer/position.js

+2-9
Original file line numberDiff line numberDiff line change
@@ -75,17 +75,10 @@ function allocate(dialogue, store) {
7575

7676
export function getPosition(dialogue, store) {
7777
const { scale } = store;
78-
const { effect, move, align, width, height, margin, slices } = dialogue;
78+
const { move, align, width, height, margin, slices } = dialogue;
7979
let x = 0;
8080
let y = 0;
81-
if (effect && effect.name === 'banner') {
82-
x = effect.lefttoright ? -width : store.width;
83-
y = [
84-
store.height - height - margin.vertical,
85-
(store.height - height) / 2,
86-
margin.vertical,
87-
][align.v];
88-
} else if (dialogue.pos || move) {
81+
if (dialogue.pos || move) {
8982
const pos = dialogue.pos || { x: 0, y: 0 };
9083
const sx = scale * pos.x;
9184
const sy = scale * pos.y;

Diff for: src/renderer/renderer.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { createDialogue } from './dom.js';
33
import { getPosition } from './position.js';
44
import { createStyle } from './style.js';
55
import { setTransformOrigin } from './transform.js';
6-
import { getScrollEffect } from './scroll.js';
6+
import { setEffect } from './effect.js';
77

88
export function renderer(dialogue, store) {
99
const { $div, animations } = createDialogue(dialogue, store);
@@ -20,8 +20,8 @@ export function renderer(dialogue, store) {
2020
$div.style.cssText += `left:${x}px;top:${y}px;`;
2121
setTransformOrigin(dialogue, store.scale);
2222
Object.assign(dialogue, getClipPath(dialogue, store));
23-
if (dialogue.effect?.name?.startsWith('scroll')) {
24-
Object.assign(dialogue, getScrollEffect(dialogue, store));
23+
if (dialogue.effect) {
24+
Object.assign(dialogue, { $div: setEffect(dialogue, store) });
2525
}
2626
return dialogue;
2727
}

Diff for: src/renderer/scroll.js

-19
This file was deleted.

Diff for: src/renderer/style.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ export function createStyle(dialogue) {
33
let cssText = '';
44
if (layer) cssText += `z-index:${layer};`;
55
cssText += `text-align:${['left', 'center', 'right'][align.h]};`;
6-
if (!['banner', 'scroll up', 'scroll downn'].includes(effect?.name)) {
6+
if (!effect) {
77
if (q !== 2) {
88
cssText += `max-width:calc(100% - var(--ass-scale) * ${margin.left + margin.right}px);`;
99
}

Diff for: test/renderer/animation.js

+21-21
Original file line numberDiff line numberDiff line change
@@ -7,36 +7,36 @@ describe('render animation', () => {
77
effect: { name: 'banner', delay: 0, leftToRight: 0, fadeAwayWidth: 0 },
88
duration: 1000,
99
})).to.deep.equal([
10-
{ offset: 0, transform: 'translateX(0)' },
11-
{ offset: 1, transform: 'translateX(calc(var(--ass-scale) * -1000px))' },
10+
{ offset: 0, transform: 'translateX(100%)' },
11+
{ offset: 1, transform: 'translateX(calc(100% + var(--ass-scale) * -1000px))' },
1212
]);
1313
expect(createEffectKeyframes({
1414
effect: { name: 'banner', delay: 1, leftToRight: 0, fadeAwayWidth: 0 },
1515
duration: 1000,
1616
})).to.deep.equal([
17-
{ offset: 0, transform: 'translateX(0)' },
18-
{ offset: 1, transform: 'translateX(calc(var(--ass-scale) * -1000px))' },
17+
{ offset: 0, transform: 'translateX(100%)' },
18+
{ offset: 1, transform: 'translateX(calc(100% + var(--ass-scale) * -1000px))' },
1919
]);
2020
expect(createEffectKeyframes({
2121
effect: { name: 'banner', delay: 2, leftToRight: 0, fadeAwayWidth: 0 },
2222
duration: 1000,
2323
})).to.deep.equal([
24-
{ offset: 0, transform: 'translateX(0)' },
25-
{ offset: 1, transform: 'translateX(calc(var(--ass-scale) * -500px))' },
24+
{ offset: 0, transform: 'translateX(100%)' },
25+
{ offset: 1, transform: 'translateX(calc(100% + var(--ass-scale) * -500px))' },
2626
]);
2727
expect(createEffectKeyframes({
2828
effect: { name: 'banner', delay: 1, leftToRight: 1, fadeAwayWidth: 0 },
2929
duration: 1000,
3030
})).to.deep.equal([
31-
{ offset: 0, transform: 'translateX(0)' },
32-
{ offset: 1, transform: 'translateX(calc(var(--ass-scale) * 1000px))' },
31+
{ offset: 0, transform: 'translateX(-100%)' },
32+
{ offset: 1, transform: 'translateX(calc(-100% + var(--ass-scale) * 1000px))' },
3333
]);
3434
expect(createEffectKeyframes({
3535
effect: { name: 'banner', delay: 1, leftToRight: 0, fadeAwayWidth: 0 },
3636
duration: 5000,
3737
})).to.deep.equal([
38-
{ offset: 0, transform: 'translateX(0)' },
39-
{ offset: 1, transform: 'translateX(calc(var(--ass-scale) * -5000px))' },
38+
{ offset: 0, transform: 'translateX(100%)' },
39+
{ offset: 1, transform: 'translateX(calc(100% + var(--ass-scale) * -5000px))' },
4040
]);
4141
});
4242

@@ -45,50 +45,50 @@ describe('render animation', () => {
4545
effect: { name: 'scroll up', y1: 0, y2: 360, delay: 1, fadeAwayHeight: 0 },
4646
duration: 1000,
4747
})).to.deep.equal([
48-
{ offset: 0, transform: 'translateY(-100%)' },
49-
{ offset: 1, transform: 'translateY(calc(var(--ass-scale) * -1000px))' },
48+
{ offset: 0, transform: 'translateY(100%)' },
49+
{ offset: 1, transform: 'translateY(calc(100% + var(--ass-scale) * -1000px))' },
5050
]);
5151
expect(createEffectKeyframes({
5252
effect: { name: 'scroll up', y1: 0, y2: 360, delay: 1, fadeAwayHeight: 0 },
5353
duration: 2000,
5454
})).to.deep.equal([
55-
{ offset: 0, transform: 'translateY(-100%)' },
56-
{ offset: 1, transform: 'translateY(calc(var(--ass-scale) * -2000px))' },
55+
{ offset: 0, transform: 'translateY(100%)' },
56+
{ offset: 1, transform: 'translateY(calc(100% + var(--ass-scale) * -2000px))' },
5757
]);
5858
expect(createEffectKeyframes({
5959
effect: { name: 'scroll up', y1: 0, y2: 360, delay: 2, fadeAwayHeight: 0 },
6060
duration: 1000,
6161
})).to.deep.equal([
62-
{ offset: 0, transform: 'translateY(-100%)' },
63-
{ offset: 1, transform: 'translateY(calc(var(--ass-scale) * -500px))' },
62+
{ offset: 0, transform: 'translateY(100%)' },
63+
{ offset: 1, transform: 'translateY(calc(100% + var(--ass-scale) * -500px))' },
6464
]);
6565
expect(createEffectKeyframes({
6666
effect: { name: 'scroll up', y1: 0, y2: 360, delay: 0, fadeAwayHeight: 0 },
6767
duration: 1000,
6868
})).to.deep.equal([
69-
{ offset: 0, transform: 'translateY(-100%)' },
70-
{ offset: 1, transform: 'translateY(calc(var(--ass-scale) * -1000px))' },
69+
{ offset: 0, transform: 'translateY(100%)' },
70+
{ offset: 1, transform: 'translateY(calc(100% + var(--ass-scale) * -1000px))' },
7171
]);
7272
expect(createEffectKeyframes({
7373
effect: { name: 'scroll down', y1: 0, y2: 360, delay: 1, fadeAwayHeight: 0 },
7474
duration: 1000,
7575
})).to.deep.equal([
7676
{ offset: 0, transform: 'translateY(-100%)' },
77-
{ offset: 1, transform: 'translateY(calc(var(--ass-scale) * 1000px))' },
77+
{ offset: 1, transform: 'translateY(calc(-100% + var(--ass-scale) * 1000px))' },
7878
]);
7979
expect(createEffectKeyframes({
8080
effect: { name: 'scroll down', y1: 0, y2: 360, delay: 1, fadeAwayHeight: 0 },
8181
duration: 2000,
8282
})).to.deep.equal([
8383
{ offset: 0, transform: 'translateY(-100%)' },
84-
{ offset: 1, transform: 'translateY(calc(var(--ass-scale) * 2000px))' },
84+
{ offset: 1, transform: 'translateY(calc(-100% + var(--ass-scale) * 2000px))' },
8585
]);
8686
expect(createEffectKeyframes({
8787
effect: { name: 'scroll down', y1: 0, y2: 360, delay: 2, fadeAwayHeight: 0 },
8888
duration: 1000,
8989
})).to.deep.equal([
9090
{ offset: 0, transform: 'translateY(-100%)' },
91-
{ offset: 1, transform: 'translateY(calc(var(--ass-scale) * 500px))' },
91+
{ offset: 1, transform: 'translateY(calc(-100% + var(--ass-scale) * 500px))' },
9292
]);
9393
});
9494

0 commit comments

Comments
 (0)