UNPKG

@smoovy/tween

Version:
256 lines (255 loc) 7.5 kB
"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; } }