Skip to content

Commit

Permalink
refactor: add wrapper for all effects and support fadeaway
Browse files Browse the repository at this point in the history
  • Loading branch information
weizhenye committed Aug 25, 2024
1 parent 9966471 commit 0256425
Show file tree
Hide file tree
Showing 10 changed files with 88 additions and 78 deletions.
7 changes: 1 addition & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,17 +139,12 @@ ASS.js uses many Web APIs to render subtitles, some features will be disabled if

* [Script Info]
* __WrapStyle__: 3
* __Collisions__: Reverse
* [Events]
* __Dialogue__
+ __Effect__
- __Scroll up__: fadeawayheight
- __Scroll down__: fadeawayheight
- __Banner__: fadeawaywidth
+ __Text__ (override codes)
- __\k, \kf, \ko, \kt, \K__: Karaoke
- __\q__: 3
- __\t([<t1>, <t2>, ][<accel>, ]<style modifiers>)__: <accel>, \2c, \2a
- __\t([<t1>, <t2>, ][<accel>, ]<style modifiers>)__: <accel>

## Known issues

Expand Down
13 changes: 12 additions & 1 deletion src/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,19 @@
top: 0;
left: 0;
}
.ASS-scroll-area {
.ASS-effect-area {
position: absolute;
display: flex;
width: 100%;
height: fit-content;
overflow: hidden;
mask-composite: intersect;
}
.ASS-effect-area[data-effect="banner"] {
flex-direction: column;
height: 100%;
}
.ASS-effect-area .ASS-dialogue {
position: static;
transform: none;
}
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export default class ASS {
};
this.#store.styles = styles;
this.#store.dialogues = dialogues.map((dia) => Object.assign(dia, {
effect: ['banner', 'scroll up', 'scroll down'].includes(dia.effect?.name) ? dia.effect : null,
align: {
// 0: left, 1: center, 2: right
h: (dia.alignment + 2) % 3,
Expand Down
31 changes: 13 additions & 18 deletions src/renderer/animation.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { color2rgba } from '../utils.js';
import { color2rgba, alpha2opacity, initAnimation } from '../utils.js';
import { getRealFontSize } from './font-size.js';
import { createCSSStroke } from './stroke.js';
import { createTransform } from './transform.js';
Expand All @@ -20,23 +20,18 @@ function mergeT(ts) {
export function createEffectKeyframes({ effect, duration }) {
// TODO: when effect and move both exist, its behavior is weird, for now only move works.
const { name, delay, leftToRight } = effect;
if (name === 'banner') {
const tx = (duration / (delay || 1)) * (leftToRight ? 1 : -1);
return [0, `calc(var(--ass-scale) * ${tx}px)`].map((x, i) => ({
offset: i,
transform: `translateX(${x})`,
}));
}
if (name.startsWith('scroll')) {
// speed is 1000px/s when delay=1
const updown = /up/.test(name) ? -1 : 1;
const y = duration / (delay || 1) * updown;
return [
{ offset: 0, transform: 'translateY(-100%)' },
{ offset: 1, transform: `translateY(calc(var(--ass-scale) * ${y}px))` },
];
}
return [];
const translate = name === 'banner' ? 'X' : 'Y';
const dir = ({
X: leftToRight ? 1 : -1,
Y: /up/.test(name) ? -1 : 1,
})[translate];
const start = -100 * dir;
// speed is 1000px/s when delay=1
const distance = (duration / (delay || 1)) * dir;
return [
{ offset: 0, transform: `translate${translate}(${start}%)` },
{ offset: 1, transform: `translate${translate}(calc(${start}% + var(--ass-scale) * ${distance}px))` },
];
}

function createMoveKeyframes({ move, duration, dialogue }) {
Expand Down
34 changes: 34 additions & 0 deletions src/renderer/effect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export function setEffect(dialogue, store) {
const $area = document.createElement('div');
$area.className = 'ASS-effect-area';
store.box.insertBefore($area, dialogue.$div);
$area.append(dialogue.$div);
const { width, height } = store.scriptRes;
const { name, y1, y2, leftToRight, fadeAwayWidth, fadeAwayHeight } = dialogue.effect;
const min = Math.min(y1, y2);
const max = Math.max(y1, y2);
$area.dataset.effect = name;
if (name === 'banner') {
$area.style.alignItems = leftToRight ? 'flex-start' : 'flex-end';
$area.style.justifyContent = ['flex-end', 'center', 'flex-start'][dialogue.align.v];
}
if (name.startsWith('scroll')) {
const top = min / height * 100;
const bottom = (height - max) / height * 100;
$area.style.cssText = `top:${top}%;bottom:${bottom}%;`;
$area.style.justifyContent = ['flex-start', 'center', 'flex-end'][dialogue.align.h];
}
if (fadeAwayHeight) {
const p = fadeAwayHeight / (max - min) * 100;
$area.style.maskImage = [
`linear-gradient(#000 ${100 - p}%, transparent)`,
`linear-gradient(transparent, #000 ${p}%)`,
].join(',');
}
if (fadeAwayWidth) {
const p = fadeAwayWidth / width * 100;
// only left side has fade away effect in VSFilter
$area.style.maskImage = `linear-gradient(90deg, transparent, #000 ${p}%)`;
}
return $area;
}
11 changes: 2 additions & 9 deletions src/renderer/position.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,17 +75,10 @@ function allocate(dialogue, store) {

export function getPosition(dialogue, store) {
const { scale } = store;
const { effect, move, align, width, height, margin, slices } = dialogue;
const { move, align, width, height, margin, slices } = dialogue;
let x = 0;
let y = 0;
if (effect && effect.name === 'banner') {
x = effect.lefttoright ? -width : store.width;
y = [
store.height - height - margin.vertical,
(store.height - height) / 2,
margin.vertical,
][align.v];
} else if (dialogue.pos || move) {
if (dialogue.pos || move) {
const pos = dialogue.pos || { x: 0, y: 0 };
const sx = scale * pos.x;
const sy = scale * pos.y;
Expand Down
6 changes: 3 additions & 3 deletions src/renderer/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { createDialogue } from './dom.js';
import { getPosition } from './position.js';
import { createStyle } from './style.js';
import { setTransformOrigin } from './transform.js';
import { getScrollEffect } from './scroll.js';
import { setEffect } from './effect.js';

export function renderer(dialogue, store) {
const { $div, animations } = createDialogue(dialogue, store);
Expand All @@ -20,8 +20,8 @@ export function renderer(dialogue, store) {
$div.style.cssText += `left:${x}px;top:${y}px;`;
setTransformOrigin(dialogue, store.scale);
Object.assign(dialogue, getClipPath(dialogue, store));
if (dialogue.effect?.name?.startsWith('scroll')) {
Object.assign(dialogue, getScrollEffect(dialogue, store));
if (dialogue.effect) {
Object.assign(dialogue, { $div: setEffect(dialogue, store) });
}
return dialogue;
}
19 changes: 0 additions & 19 deletions src/renderer/scroll.js

This file was deleted.

2 changes: 1 addition & 1 deletion src/renderer/style.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export function createStyle(dialogue) {
let cssText = '';
if (layer) cssText += `z-index:${layer};`;
cssText += `text-align:${['left', 'center', 'right'][align.h]};`;
if (!['banner', 'scroll up', 'scroll downn'].includes(effect?.name)) {
if (!effect) {
if (q !== 2) {
cssText += `max-width:calc(100% - var(--ass-scale) * ${margin.left + margin.right}px);`;
}
Expand Down
42 changes: 21 additions & 21 deletions test/renderer/animation.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,36 @@ describe('render animation', () => {
effect: { name: 'banner', delay: 0, leftToRight: 0, fadeAwayWidth: 0 },
duration: 1000,
})).to.deep.equal([
{ offset: 0, transform: 'translateX(0)' },
{ offset: 1, transform: 'translateX(calc(var(--ass-scale) * -1000px))' },
{ offset: 0, transform: 'translateX(100%)' },
{ offset: 1, transform: 'translateX(calc(100% + var(--ass-scale) * -1000px))' },
]);
expect(createEffectKeyframes({
effect: { name: 'banner', delay: 1, leftToRight: 0, fadeAwayWidth: 0 },
duration: 1000,
})).to.deep.equal([
{ offset: 0, transform: 'translateX(0)' },
{ offset: 1, transform: 'translateX(calc(var(--ass-scale) * -1000px))' },
{ offset: 0, transform: 'translateX(100%)' },
{ offset: 1, transform: 'translateX(calc(100% + var(--ass-scale) * -1000px))' },
]);
expect(createEffectKeyframes({
effect: { name: 'banner', delay: 2, leftToRight: 0, fadeAwayWidth: 0 },
duration: 1000,
})).to.deep.equal([
{ offset: 0, transform: 'translateX(0)' },
{ offset: 1, transform: 'translateX(calc(var(--ass-scale) * -500px))' },
{ offset: 0, transform: 'translateX(100%)' },
{ offset: 1, transform: 'translateX(calc(100% + var(--ass-scale) * -500px))' },
]);
expect(createEffectKeyframes({
effect: { name: 'banner', delay: 1, leftToRight: 1, fadeAwayWidth: 0 },
duration: 1000,
})).to.deep.equal([
{ offset: 0, transform: 'translateX(0)' },
{ offset: 1, transform: 'translateX(calc(var(--ass-scale) * 1000px))' },
{ offset: 0, transform: 'translateX(-100%)' },
{ offset: 1, transform: 'translateX(calc(-100% + var(--ass-scale) * 1000px))' },
]);
expect(createEffectKeyframes({
effect: { name: 'banner', delay: 1, leftToRight: 0, fadeAwayWidth: 0 },
duration: 5000,
})).to.deep.equal([
{ offset: 0, transform: 'translateX(0)' },
{ offset: 1, transform: 'translateX(calc(var(--ass-scale) * -5000px))' },
{ offset: 0, transform: 'translateX(100%)' },
{ offset: 1, transform: 'translateX(calc(100% + var(--ass-scale) * -5000px))' },
]);
});

Expand All @@ -45,50 +45,50 @@ describe('render animation', () => {
effect: { name: 'scroll up', y1: 0, y2: 360, delay: 1, fadeAwayHeight: 0 },
duration: 1000,
})).to.deep.equal([
{ offset: 0, transform: 'translateY(-100%)' },
{ offset: 1, transform: 'translateY(calc(var(--ass-scale) * -1000px))' },
{ offset: 0, transform: 'translateY(100%)' },
{ offset: 1, transform: 'translateY(calc(100% + var(--ass-scale) * -1000px))' },
]);
expect(createEffectKeyframes({
effect: { name: 'scroll up', y1: 0, y2: 360, delay: 1, fadeAwayHeight: 0 },
duration: 2000,
})).to.deep.equal([
{ offset: 0, transform: 'translateY(-100%)' },
{ offset: 1, transform: 'translateY(calc(var(--ass-scale) * -2000px))' },
{ offset: 0, transform: 'translateY(100%)' },
{ offset: 1, transform: 'translateY(calc(100% + var(--ass-scale) * -2000px))' },
]);
expect(createEffectKeyframes({
effect: { name: 'scroll up', y1: 0, y2: 360, delay: 2, fadeAwayHeight: 0 },
duration: 1000,
})).to.deep.equal([
{ offset: 0, transform: 'translateY(-100%)' },
{ offset: 1, transform: 'translateY(calc(var(--ass-scale) * -500px))' },
{ offset: 0, transform: 'translateY(100%)' },
{ offset: 1, transform: 'translateY(calc(100% + var(--ass-scale) * -500px))' },
]);
expect(createEffectKeyframes({
effect: { name: 'scroll up', y1: 0, y2: 360, delay: 0, fadeAwayHeight: 0 },
duration: 1000,
})).to.deep.equal([
{ offset: 0, transform: 'translateY(-100%)' },
{ offset: 1, transform: 'translateY(calc(var(--ass-scale) * -1000px))' },
{ offset: 0, transform: 'translateY(100%)' },
{ offset: 1, transform: 'translateY(calc(100% + var(--ass-scale) * -1000px))' },
]);
expect(createEffectKeyframes({
effect: { name: 'scroll down', y1: 0, y2: 360, delay: 1, fadeAwayHeight: 0 },
duration: 1000,
})).to.deep.equal([
{ offset: 0, transform: 'translateY(-100%)' },
{ offset: 1, transform: 'translateY(calc(var(--ass-scale) * 1000px))' },
{ offset: 1, transform: 'translateY(calc(-100% + var(--ass-scale) * 1000px))' },
]);
expect(createEffectKeyframes({
effect: { name: 'scroll down', y1: 0, y2: 360, delay: 1, fadeAwayHeight: 0 },
duration: 2000,
})).to.deep.equal([
{ offset: 0, transform: 'translateY(-100%)' },
{ offset: 1, transform: 'translateY(calc(var(--ass-scale) * 2000px))' },
{ offset: 1, transform: 'translateY(calc(-100% + var(--ass-scale) * 2000px))' },
]);
expect(createEffectKeyframes({
effect: { name: 'scroll down', y1: 0, y2: 360, delay: 2, fadeAwayHeight: 0 },
duration: 1000,
})).to.deep.equal([
{ offset: 0, transform: 'translateY(-100%)' },
{ offset: 1, transform: 'translateY(calc(var(--ass-scale) * 500px))' },
{ offset: 1, transform: 'translateY(calc(-100% + var(--ass-scale) * 500px))' },
]);
});

Expand Down

0 comments on commit 0256425

Please sign in to comment.