@smoovy/tween
Version:
simple and easy-to-use tween lib
256 lines (255 loc) • 7.5 kB
JavaScript
"use strict";
import { TweenController } from "./controller";
import { fromTo, to } from "./helper/tween";
export class Timeline extends TweenController {
constructor(config) {
super(config);
this.config = config;
this.items = [];
this.sideEffects = [];
this.timelineReversed = false;
this._duration = 0;
if (config.reversed) {
this.reverse();
}
if (config.items) {
for (const item of config.items) {
if (item.controller) {
this.add(item.controller, item.config);
} else if (item.dynamic) {
this.add(item.dynamic, item.config);
}
}
}
if (config.autoStart !== false) {
requestAnimationFrame(() => this.start());
}
}
call(callback, config = {}) {
this.sideEffects.push({
callback,
config,
called: false,
item: this.items[this.items.length - 1]
});
return this;
}
clear() {
this.reset();
this.items.length = 0;
this.sideEffects.length = 0;
this._duration = 0;
return this;
}
add(item, config = {}) {
if (typeof item === "function") {
this.items.push({ dynamic: item, config });
return this;
}
const controllers = Array.isArray(item) ? item : [item];
for (let i = 0, len = controllers.length; i < len; i++) {
const controller = controllers[i].override().pause();
const itemConfig = { ...config };
if (this.timelineReversed && !controller.reversed) {
controller.reverse();
}
if (i > 0) {
itemConfig.offset = -1;
}
if (typeof itemConfig.offset !== "undefined") {
itemConfig.offset = Math.min(Math.max(itemConfig.offset, -1), 1);
}
this.items.push({ controller, config: itemConfig });
}
this.updateDuration();
return this;
}
to(target, props, config) {
return this.add(to(target, props, config), config);
}
fromTo(target, fromProps, toProps, config) {
return this.add(fromTo(target, fromProps, toProps, config), config);
}
remove(controller) {
const index = this.items.findIndex((item) => item.controller === controller);
if (index !== -1) {
this.items.splice(index, 1);
this.updateDuration();
}
return this;
}
updateDuration() {
const vectors = [];
for (let i = 0; i < this.items.length; i++) {
const item = this.items[i];
const controller = item.controller;
if (!controller) {
continue;
}
const config = item.config;
const duration = controller.duration;
const offset = config.offset || 0;
if (i === 0) {
vectors.push([duration, duration]);
continue;
}
if (vectors.length === 0) {
continue;
}
const [edge, length] = vectors[vectors.length - 1];
const gap = length * offset;
vectors.push([edge + gap + duration, duration]);
}
this._duration = Math.max(...vectors.map((vector) => vector[0]));
}
revealAllItems() {
for (const item of this.items) {
this.revealItem(item);
}
this.updateDuration();
return this;
}
revealItem(item) {
if (!item.controller && item.dynamic) {
item.controller = item.dynamic().override().reset();
if (item.controller instanceof Timeline) {
item.controller.revealAllItems();
}
this.updateDuration();
}
return item;
}
isDynamic() {
return this.items.some((item) => item.dynamic);
}
getStartMs(items, index, noDelay = false) {
let leftEdge = 0;
for (let i = 0; i <= index; i++) {
if (i - 1 < 0) {
continue;
}
const item = this.revealItem(items[i - 1]);
const controller = item.controller;
const offset = items[i].config.offset || 0;
const duration = controller.duration - (noDelay ? controller.delay : 0);
leftEdge += duration + duration * offset;
}
return leftEdge;
}
callEffects(passed, effects) {
for (const effect of effects) {
const effectOffset = effect.config.offset || 0;
const controller = effect.item?.controller;
const itemDuration = effect.item ? controller?.duration || 0 : 0;
const triggerMs = itemDuration + itemDuration * effectOffset;
if (!effect.called && passed >= triggerMs) {
effect.callback();
effect.called = true;
}
}
}
seek(ms, noDelay = false, noEvents = false, force = false) {
if (!this.preSeek(ms, noEvents)) {
return this;
}
this._passed = Math.min(ms, this.duration);
this._progress = this._passed / this.duration;
if (!noEvents) {
this.callback(this.config.onSeek, [ms, this._progress]);
}
if (!this.seekDelay(ms, noDelay, noEvents)) {
return this;
}
ms -= noDelay ? 0 : this.delay;
const items = this.items;
for (let i = 0, len = items.length; i < len; i++) {
const item = this.revealItem(items[i]);
const effects = this.sideEffects.filter((effect) => effect.item === item);
const startMs = this.getStartMs(items, i, noDelay);
const delayOffset = noDelay ? item.controller.delay : 0;
const length = item.controller.duration - delayOffset;
const endMs = startMs + length;
const passed = ms - startMs;
if (length === 0) {
continue;
}
if (ms > endMs) {
if (item.controller.progress < 1) {
if (item.controller instanceof Timeline) {
item.controller.seek(length, noDelay, noEvents, force);
} else {
item.controller.seek(length, noDelay, noEvents);
}
this.callEffects(length, effects);
}
continue;
}
if (item.controller instanceof Timeline) {
item.controller.seek(passed, noDelay, noEvents, force);
} else {
item.controller.seek(passed, noDelay, noEvents);
}
this.callEffects(passed, effects);
const nextItem = items[i + 1];
const nextOffset = nextItem ? nextItem.config.offset || 0 : 0;
if (force) {
continue;
}
if (!nextItem || endMs + nextOffset * length > ms) {
break;
}
}
this._progress = this._passed / this.duration;
if (this._progress >= 1 && !noEvents) {
this.stop();
this.resolve();
this.callback(this.config.onComplete);
this.callListeners("onComplete");
}
return this;
}
beforeStart() {
this.resetEffects();
for (const effect of this.sideEffects.filter((effect2) => !effect2.item)) {
effect.callback();
}
}
reverse() {
this.timelineReversed = !this.timelineReversed;
for (const { controller } of this.items) {
if (controller) {
controller.reverse();
}
}
return this;
}
stop(silent = false) {
super.stop();
for (const { controller } of this.items) {
if (controller) {
controller.stop(silent);
}
}
return this;
}
resetEffects() {
for (const effect of this.sideEffects) {
effect.called = false;
}
}
reset(seek = 0, noEvents = false) {
super.reset(seek, noEvents);
const lastActivity = this._reversed ? 1 - this._progress : this._progress;
const items = this.items.slice().reverse();
if (this._started || lastActivity > 0) {
this.seek(seek, true, noEvents, true);
}
for (const { controller } of items) {
if (controller) {
controller.reset(seek, noEvents);
}
}
this.resetEffects();
return this;
}
}