mirror of https://github.com/sveltejs/svelte
parent
00912f2e01
commit
ab50e6042e
@ -1,416 +1,419 @@
|
||||
import { identity as linear, is_function, noop, run_all } from './utils';
|
||||
import { identity as linear, is_function, noop, run_all } from './utils.js';
|
||||
import { now } from './environment';
|
||||
import { loop } from './loop';
|
||||
import { create_rule, delete_rule } from './style_manager';
|
||||
import { custom_event } from './dom';
|
||||
import { add_render_callback } from './scheduler';
|
||||
import { TransitionConfig } from '../transition';
|
||||
import type { Fragment } from './public';
|
||||
|
||||
let promise: Promise<void> | null;
|
||||
type INTRO = 1;
|
||||
type OUTRO = 0;
|
||||
interface Outro {
|
||||
/**
|
||||
* remaining outros
|
||||
*/
|
||||
r: number;
|
||||
/**
|
||||
* callbacks
|
||||
*/
|
||||
c: Function[];
|
||||
/**
|
||||
* parent outro
|
||||
*/
|
||||
p: Outro;
|
||||
}
|
||||
/**
|
||||
* @type {Promise<void> | null} */
|
||||
let promise;
|
||||
|
||||
/**
|
||||
* @returns {Promise<void>} */
|
||||
function wait() {
|
||||
if (!promise) {
|
||||
promise = Promise.resolve();
|
||||
promise.then(() => {
|
||||
promise = null;
|
||||
});
|
||||
}
|
||||
|
||||
return promise;
|
||||
if (!promise) {
|
||||
promise = Promise.resolve();
|
||||
promise.then(() => {
|
||||
promise = null;
|
||||
});
|
||||
}
|
||||
return promise;
|
||||
}
|
||||
|
||||
function dispatch(node: Element, direction: INTRO | OUTRO | boolean, kind: 'start' | 'end') {
|
||||
node.dispatchEvent(custom_event(`${direction ? 'intro' : 'outro'}${kind}`));
|
||||
/**
|
||||
* @param {Element} node
|
||||
* @param {INTRO | OUTRO | boolean} direction
|
||||
* @param {'start' | 'end'} kind
|
||||
* @returns {void}
|
||||
*/
|
||||
function dispatch(node, direction, kind) {
|
||||
node.dispatchEvent(custom_event(`${direction ? 'intro' : 'outro'}${kind}`));
|
||||
}
|
||||
|
||||
const outroing = new Set();
|
||||
let outros: Outro;
|
||||
|
||||
/**
|
||||
* @type {Outro} */
|
||||
let outros;
|
||||
|
||||
/**
|
||||
* @returns {void} */
|
||||
export function group_outros() {
|
||||
outros = {
|
||||
r: 0, // remaining outros
|
||||
c: [], // callbacks
|
||||
p: outros // parent group
|
||||
};
|
||||
outros = {
|
||||
r: 0,
|
||||
c: [],
|
||||
p: outros // parent group
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {void} */
|
||||
export function check_outros() {
|
||||
if (!outros.r) {
|
||||
run_all(outros.c);
|
||||
}
|
||||
outros = outros.p;
|
||||
if (!outros.r) {
|
||||
run_all(outros.c);
|
||||
}
|
||||
outros = outros.p;
|
||||
}
|
||||
|
||||
export function transition_in(block: Fragment, local?: 0 | 1) {
|
||||
if (block && block.i) {
|
||||
outroing.delete(block);
|
||||
block.i(local);
|
||||
}
|
||||
/**
|
||||
* @param {Fragment} block
|
||||
* @param {0 | 1} local
|
||||
* @returns {void}
|
||||
*/
|
||||
export function transition_in(block, local) {
|
||||
if (block && block.i) {
|
||||
outroing.delete(block);
|
||||
block.i(local);
|
||||
}
|
||||
}
|
||||
|
||||
export function transition_out(block: Fragment, local: 0 | 1, detach?: 0 | 1, callback?) {
|
||||
if (block && block.o) {
|
||||
if (outroing.has(block)) return;
|
||||
outroing.add(block);
|
||||
|
||||
outros.c.push(() => {
|
||||
outroing.delete(block);
|
||||
if (callback) {
|
||||
if (detach) block.d(1);
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
block.o(local);
|
||||
} else if (callback) {
|
||||
callback();
|
||||
}
|
||||
/**
|
||||
* @param {Fragment} block
|
||||
* @param {0 | 1} local
|
||||
* @param {0 | 1} detach
|
||||
* @returns {void}
|
||||
*/
|
||||
export function transition_out(block, local, detach, callback) {
|
||||
if (block && block.o) {
|
||||
if (outroing.has(block))
|
||||
return;
|
||||
outroing.add(block);
|
||||
outros.c.push(() => {
|
||||
outroing.delete(block);
|
||||
if (callback) {
|
||||
if (detach)
|
||||
block.d(1);
|
||||
callback();
|
||||
}
|
||||
});
|
||||
block.o(local);
|
||||
}
|
||||
else if (callback) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
const null_transition: TransitionConfig = { duration: 0 };
|
||||
|
||||
type TransitionOptions = { direction: 'in' | 'out' | 'both' };
|
||||
type TransitionFn = (node: Element, params: any, options: TransitionOptions) => TransitionConfig;
|
||||
|
||||
export function create_in_transition(
|
||||
node: Element & ElementCSSInlineStyle,
|
||||
fn: TransitionFn,
|
||||
params: any
|
||||
) {
|
||||
const options: TransitionOptions = { direction: 'in' };
|
||||
let config = fn(node, params, options);
|
||||
let running = false;
|
||||
let animation_name;
|
||||
let task;
|
||||
let uid = 0;
|
||||
|
||||
function cleanup() {
|
||||
if (animation_name) delete_rule(node, animation_name);
|
||||
}
|
||||
|
||||
function go() {
|
||||
const {
|
||||
delay = 0,
|
||||
duration = 300,
|
||||
easing = linear,
|
||||
tick = noop,
|
||||
css
|
||||
} = config || null_transition;
|
||||
|
||||
if (css) animation_name = create_rule(node, 0, 1, duration, delay, easing, css, uid++);
|
||||
tick(0, 1);
|
||||
|
||||
const start_time = now() + delay;
|
||||
const end_time = start_time + duration;
|
||||
|
||||
if (task) task.abort();
|
||||
running = true;
|
||||
|
||||
add_render_callback(() => dispatch(node, true, 'start'));
|
||||
|
||||
task = loop((now) => {
|
||||
if (running) {
|
||||
if (now >= end_time) {
|
||||
tick(1, 0);
|
||||
|
||||
dispatch(node, true, 'end');
|
||||
|
||||
cleanup();
|
||||
return (running = false);
|
||||
}
|
||||
|
||||
if (now >= start_time) {
|
||||
const t = easing((now - start_time) / duration);
|
||||
tick(t, 1 - t);
|
||||
}
|
||||
}
|
||||
|
||||
return running;
|
||||
});
|
||||
}
|
||||
|
||||
let started = false;
|
||||
|
||||
return {
|
||||
start() {
|
||||
if (started) return;
|
||||
|
||||
started = true;
|
||||
delete_rule(node);
|
||||
|
||||
if (is_function(config)) {
|
||||
config = config(options);
|
||||
wait().then(go);
|
||||
} else {
|
||||
go();
|
||||
}
|
||||
},
|
||||
|
||||
invalidate() {
|
||||
started = false;
|
||||
},
|
||||
|
||||
end() {
|
||||
if (running) {
|
||||
cleanup();
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
/**
|
||||
* @type {TransitionConfig} */
|
||||
const null_transition = { duration: 0 };
|
||||
|
||||
/**
|
||||
* @param {Element & ElementCSSInlineStyle} node
|
||||
* @param {TransitionFn} fn
|
||||
* @param {any} params
|
||||
* @returns {{ start(): void; invalidate(): void; end(): void; }}
|
||||
*/
|
||||
export function create_in_transition(node, fn, params) {
|
||||
|
||||
/**
|
||||
* @type {TransitionOptions} */
|
||||
const options = { direction: 'in' };
|
||||
let config = fn(node, params, options);
|
||||
let running = false;
|
||||
let animation_name;
|
||||
let task;
|
||||
let uid = 0;
|
||||
|
||||
/**
|
||||
* @returns {void} */
|
||||
function cleanup() {
|
||||
if (animation_name)
|
||||
delete_rule(node, animation_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {void} */
|
||||
function go() {
|
||||
const { delay = 0, duration = 300, easing = linear, tick = noop, css } = config || null_transition;
|
||||
if (css)
|
||||
animation_name = create_rule(node, 0, 1, duration, delay, easing, css, uid++);
|
||||
tick(0, 1);
|
||||
const start_time = now() + delay;
|
||||
const end_time = start_time + duration;
|
||||
if (task)
|
||||
task.abort();
|
||||
running = true;
|
||||
add_render_callback(() => dispatch(node, true, 'start'));
|
||||
task = loop((now) => {
|
||||
if (running) {
|
||||
if (now >= end_time) {
|
||||
tick(1, 0);
|
||||
dispatch(node, true, 'end');
|
||||
cleanup();
|
||||
return (running = false);
|
||||
}
|
||||
if (now >= start_time) {
|
||||
const t = easing((now - start_time) / duration);
|
||||
tick(t, 1 - t);
|
||||
}
|
||||
}
|
||||
return running;
|
||||
});
|
||||
}
|
||||
let started = false;
|
||||
return {
|
||||
start() {
|
||||
if (started)
|
||||
return;
|
||||
started = true;
|
||||
delete_rule(node);
|
||||
if (is_function(config)) {
|
||||
config = config(options);
|
||||
wait().then(go);
|
||||
}
|
||||
else {
|
||||
go();
|
||||
}
|
||||
},
|
||||
invalidate() {
|
||||
started = false;
|
||||
},
|
||||
end() {
|
||||
if (running) {
|
||||
cleanup();
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function create_out_transition(
|
||||
node: Element & ElementCSSInlineStyle,
|
||||
fn: TransitionFn,
|
||||
params: any
|
||||
) {
|
||||
const options: TransitionOptions = { direction: 'out' };
|
||||
let config = fn(node, params, options);
|
||||
let running = true;
|
||||
let animation_name;
|
||||
|
||||
const group = outros;
|
||||
|
||||
group.r += 1;
|
||||
|
||||
function go() {
|
||||
const {
|
||||
delay = 0,
|
||||
duration = 300,
|
||||
easing = linear,
|
||||
tick = noop,
|
||||
css
|
||||
} = config || null_transition;
|
||||
|
||||
if (css) animation_name = create_rule(node, 1, 0, duration, delay, easing, css);
|
||||
|
||||
const start_time = now() + delay;
|
||||
const end_time = start_time + duration;
|
||||
|
||||
add_render_callback(() => dispatch(node, false, 'start'));
|
||||
|
||||
loop((now) => {
|
||||
if (running) {
|
||||
if (now >= end_time) {
|
||||
tick(0, 1);
|
||||
|
||||
dispatch(node, false, 'end');
|
||||
|
||||
if (!--group.r) {
|
||||
// this will result in `end()` being called,
|
||||
// so we don't need to clean up here
|
||||
run_all(group.c);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (now >= start_time) {
|
||||
const t = easing((now - start_time) / duration);
|
||||
tick(1 - t, t);
|
||||
}
|
||||
}
|
||||
|
||||
return running;
|
||||
});
|
||||
}
|
||||
|
||||
if (is_function(config)) {
|
||||
wait().then(() => {
|
||||
// @ts-ignore
|
||||
config = config(options);
|
||||
go();
|
||||
});
|
||||
} else {
|
||||
go();
|
||||
}
|
||||
|
||||
return {
|
||||
end(reset) {
|
||||
if (reset && config.tick) {
|
||||
config.tick(1, 0);
|
||||
}
|
||||
|
||||
if (running) {
|
||||
if (animation_name) delete_rule(node, animation_name);
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
/**
|
||||
* @param {Element & ElementCSSInlineStyle} node
|
||||
* @param {TransitionFn} fn
|
||||
* @param {any} params
|
||||
* @returns {{ end(reset: any): void; }}
|
||||
*/
|
||||
export function create_out_transition(node, fn, params) {
|
||||
|
||||
/**
|
||||
* @type {TransitionOptions} */
|
||||
const options = { direction: 'out' };
|
||||
let config = fn(node, params, options);
|
||||
let running = true;
|
||||
let animation_name;
|
||||
const group = outros;
|
||||
group.r += 1;
|
||||
|
||||
/**
|
||||
* @returns {void} */
|
||||
function go() {
|
||||
const { delay = 0, duration = 300, easing = linear, tick = noop, css } = config || null_transition;
|
||||
if (css)
|
||||
animation_name = create_rule(node, 1, 0, duration, delay, easing, css);
|
||||
const start_time = now() + delay;
|
||||
const end_time = start_time + duration;
|
||||
add_render_callback(() => dispatch(node, false, 'start'));
|
||||
loop((now) => {
|
||||
if (running) {
|
||||
if (now >= end_time) {
|
||||
tick(0, 1);
|
||||
dispatch(node, false, 'end');
|
||||
if (!--group.r) {
|
||||
// this will result in `end()` being called,
|
||||
// so we don't need to clean up here
|
||||
run_all(group.c);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (now >= start_time) {
|
||||
const t = easing((now - start_time) / duration);
|
||||
tick(1 - t, t);
|
||||
}
|
||||
}
|
||||
return running;
|
||||
});
|
||||
}
|
||||
if (is_function(config)) {
|
||||
wait().then(() => {
|
||||
// @ts-ignore
|
||||
config = config(options);
|
||||
go();
|
||||
});
|
||||
}
|
||||
else {
|
||||
go();
|
||||
}
|
||||
return {
|
||||
end(reset) {
|
||||
if (reset && config.tick) {
|
||||
config.tick(1, 0);
|
||||
}
|
||||
if (running) {
|
||||
if (animation_name)
|
||||
delete_rule(node, animation_name);
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
interface PendingProgram {
|
||||
start: number;
|
||||
b: INTRO | OUTRO;
|
||||
group?: Outro;
|
||||
}
|
||||
interface Program {
|
||||
a: number;
|
||||
b: INTRO | OUTRO;
|
||||
/**
|
||||
* direction
|
||||
*/
|
||||
d: 1 | -1;
|
||||
duration: number;
|
||||
start: number;
|
||||
end: number;
|
||||
group?: Outro;
|
||||
/**
|
||||
* @param {Element & ElementCSSInlineStyle} node
|
||||
* @param {TransitionFn} fn
|
||||
* @param {any} params
|
||||
* @param {boolean} intro
|
||||
* @returns {{ run(b: 0 | 1): void; end(): void; }}
|
||||
*/
|
||||
export function create_bidirectional_transition(node, fn, params, intro) {
|
||||
|
||||
/**
|
||||
* @type {TransitionOptions} */
|
||||
const options = { direction: 'both' };
|
||||
let config = fn(node, params, options);
|
||||
let t = intro ? 0 : 1;
|
||||
|
||||
/**
|
||||
* @type {Program | null} */
|
||||
let running_program = null;
|
||||
|
||||
/**
|
||||
* @type {PendingProgram | null} */
|
||||
let pending_program = null;
|
||||
let animation_name = null;
|
||||
|
||||
/**
|
||||
* @returns {void} */
|
||||
function clear_animation() {
|
||||
if (animation_name)
|
||||
delete_rule(node, animation_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PendingProgram} program
|
||||
* @param {number} duration
|
||||
* @returns {Program}
|
||||
*/
|
||||
function init(program, duration) {
|
||||
const d = (program.b - t);
|
||||
duration *= Math.abs(d);
|
||||
return {
|
||||
a: t,
|
||||
b: program.b,
|
||||
d,
|
||||
duration,
|
||||
start: program.start,
|
||||
end: program.start + duration,
|
||||
group: program.group
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {INTRO | OUTRO} b
|
||||
* @returns {void}
|
||||
*/
|
||||
function go(b) {
|
||||
const { delay = 0, duration = 300, easing = linear, tick = noop, css } = config || null_transition;
|
||||
|
||||
/**
|
||||
* @type {PendingProgram} */
|
||||
const program = {
|
||||
start: now() + delay,
|
||||
b
|
||||
};
|
||||
if (!b) {
|
||||
// @ts-ignore todo: improve typings
|
||||
program.group = outros;
|
||||
outros.r += 1;
|
||||
}
|
||||
if (running_program || pending_program) {
|
||||
pending_program = program;
|
||||
}
|
||||
else {
|
||||
// if this is an intro, and there's a delay, we need to do
|
||||
// an initial tick and/or apply CSS animation immediately
|
||||
if (css) {
|
||||
clear_animation();
|
||||
animation_name = create_rule(node, t, b, duration, delay, easing, css);
|
||||
}
|
||||
if (b)
|
||||
tick(0, 1);
|
||||
running_program = init(program, duration);
|
||||
add_render_callback(() => dispatch(node, b, 'start'));
|
||||
loop((now) => {
|
||||
if (pending_program && now > pending_program.start) {
|
||||
running_program = init(pending_program, duration);
|
||||
pending_program = null;
|
||||
dispatch(node, running_program.b, 'start');
|
||||
if (css) {
|
||||
clear_animation();
|
||||
animation_name = create_rule(node, t, running_program.b, running_program.duration, 0, easing, config.css);
|
||||
}
|
||||
}
|
||||
if (running_program) {
|
||||
if (now >= running_program.end) {
|
||||
tick((t = running_program.b), 1 - t);
|
||||
dispatch(node, running_program.b, 'end');
|
||||
if (!pending_program) {
|
||||
// we're done
|
||||
if (running_program.b) {
|
||||
// intro — we can tidy up immediately
|
||||
clear_animation();
|
||||
}
|
||||
else {
|
||||
// outro — needs to be coordinated
|
||||
if (!--running_program.group.r)
|
||||
run_all(running_program.group.c);
|
||||
}
|
||||
}
|
||||
running_program = null;
|
||||
}
|
||||
else if (now >= running_program.start) {
|
||||
const p = now - running_program.start;
|
||||
t = running_program.a + running_program.d * easing(p / running_program.duration);
|
||||
tick(t, 1 - t);
|
||||
}
|
||||
}
|
||||
return !!(running_program || pending_program);
|
||||
});
|
||||
}
|
||||
}
|
||||
return {
|
||||
run(b) {
|
||||
if (is_function(config)) {
|
||||
wait().then(() => {
|
||||
const opts = { direction: b ? 'in' : 'out' };
|
||||
// @ts-ignore
|
||||
config = config(opts);
|
||||
go(b);
|
||||
});
|
||||
}
|
||||
else {
|
||||
go(b);
|
||||
}
|
||||
},
|
||||
end() {
|
||||
clear_animation();
|
||||
running_program = pending_program = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function create_bidirectional_transition(
|
||||
node: Element & ElementCSSInlineStyle,
|
||||
fn: TransitionFn,
|
||||
params: any,
|
||||
intro: boolean
|
||||
) {
|
||||
const options: TransitionOptions = { direction: 'both' };
|
||||
let config = fn(node, params, options);
|
||||
|
||||
let t = intro ? 0 : 1;
|
||||
|
||||
let running_program: Program | null = null;
|
||||
let pending_program: PendingProgram | null = null;
|
||||
let animation_name = null;
|
||||
|
||||
function clear_animation() {
|
||||
if (animation_name) delete_rule(node, animation_name);
|
||||
}
|
||||
|
||||
function init(program: PendingProgram, duration: number): Program {
|
||||
const d = (program.b - t) as Program['d'];
|
||||
duration *= Math.abs(d);
|
||||
|
||||
return {
|
||||
a: t,
|
||||
b: program.b,
|
||||
d,
|
||||
duration,
|
||||
start: program.start,
|
||||
end: program.start + duration,
|
||||
group: program.group
|
||||
};
|
||||
}
|
||||
|
||||
function go(b: INTRO | OUTRO) {
|
||||
const {
|
||||
delay = 0,
|
||||
duration = 300,
|
||||
easing = linear,
|
||||
tick = noop,
|
||||
css
|
||||
} = config || null_transition;
|
||||
|
||||
const program: PendingProgram = {
|
||||
start: now() + delay,
|
||||
b
|
||||
};
|
||||
|
||||
if (!b) {
|
||||
// @ts-ignore todo: improve typings
|
||||
program.group = outros;
|
||||
outros.r += 1;
|
||||
}
|
||||
|
||||
if (running_program || pending_program) {
|
||||
pending_program = program;
|
||||
} else {
|
||||
// if this is an intro, and there's a delay, we need to do
|
||||
// an initial tick and/or apply CSS animation immediately
|
||||
if (css) {
|
||||
clear_animation();
|
||||
animation_name = create_rule(node, t, b, duration, delay, easing, css);
|
||||
}
|
||||
|
||||
if (b) tick(0, 1);
|
||||
|
||||
running_program = init(program, duration);
|
||||
add_render_callback(() => dispatch(node, b, 'start'));
|
||||
|
||||
loop((now) => {
|
||||
if (pending_program && now > pending_program.start) {
|
||||
running_program = init(pending_program, duration);
|
||||
pending_program = null;
|
||||
|
||||
dispatch(node, running_program.b, 'start');
|
||||
|
||||
if (css) {
|
||||
clear_animation();
|
||||
animation_name = create_rule(
|
||||
node,
|
||||
t,
|
||||
running_program.b,
|
||||
running_program.duration,
|
||||
0,
|
||||
easing,
|
||||
config.css
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (running_program) {
|
||||
if (now >= running_program.end) {
|
||||
tick((t = running_program.b), 1 - t);
|
||||
dispatch(node, running_program.b, 'end');
|
||||
|
||||
if (!pending_program) {
|
||||
// we're done
|
||||
if (running_program.b) {
|
||||
// intro — we can tidy up immediately
|
||||
clear_animation();
|
||||
} else {
|
||||
// outro — needs to be coordinated
|
||||
if (!--running_program.group.r) run_all(running_program.group.c);
|
||||
}
|
||||
}
|
||||
|
||||
running_program = null;
|
||||
} else if (now >= running_program.start) {
|
||||
const p = now - running_program.start;
|
||||
t = running_program.a + running_program.d * easing(p / running_program.duration);
|
||||
tick(t, 1 - t);
|
||||
}
|
||||
}
|
||||
|
||||
return !!(running_program || pending_program);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
run(b: INTRO | OUTRO) {
|
||||
if (is_function(config)) {
|
||||
wait().then(() => {
|
||||
const opts: TransitionOptions = { direction: b ? 'in' : 'out' };
|
||||
|
||||
// @ts-ignore
|
||||
config = config(opts);
|
||||
go(b);
|
||||
});
|
||||
} else {
|
||||
go(b);
|
||||
}
|
||||
},
|
||||
|
||||
end() {
|
||||
clear_animation();
|
||||
running_program = pending_program = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
/** @typedef {1} INTRO */
|
||||
/** @typedef {0} OUTRO */
|
||||
/** @typedef {{ direction: 'in' | 'out' | 'both' }} TransitionOptions */
|
||||
/** @typedef {(node: Element, params: any, options: TransitionOptions) => TransitionConfig} TransitionFn */
|
||||
|
||||
/** @typedef {Object} Outro
|
||||
* @property {number} r
|
||||
* remaining outros
|
||||
* @property {Function[]} c
|
||||
* callbacks
|
||||
* @property {Object} p
|
||||
* parent outro
|
||||
*/
|
||||
/** @typedef {Object} PendingProgram
|
||||
* @property {number} start
|
||||
* @property {INTRO|OUTRO} b
|
||||
* @property {Outro} [group]
|
||||
*/
|
||||
/** @typedef {Object} Program
|
||||
* @property {number} a
|
||||
* @property {INTRO|OUTRO} b
|
||||
* @property {1|-1} d
|
||||
* direction
|
||||
* @property {number} duration
|
||||
* @property {number} start
|
||||
* @property {number} end
|
||||
* @property {Outro} [group]
|
||||
*/
|
||||
Loading…
Reference in new issue