import { Timeline } from '../timeline/timeline'; import { Sequenced } from './sequenced-timeline'; import { AbstractCompositeTimeline } from '../timeline/abstract-composite-timeline'; import { EventEmitter } from '@akolos/event-emitter'; import { TimelineEvents } from '../timeline'; export interface SequenceEvents extends TimelineEvents> { completed: [event: {}, source: Sequence]; sought: [event: { from: number }, source: Sequence]; timelineActivated: [timeline: T, source: Sequence]; timelineDeactivated: [timeline: T, source: Sequence]; updated: [event: { dt: number }, source: Sequence]; } export class Sequence extends AbstractCompositeTimeline implements Timeline { private readonly eventEmitter = new EventEmitter>(); public readonly on = this.eventEmitter.makeDelegate('on', this); public readonly off = this.eventEmitter.makeDelegate('off', this); protected readonly emit = this.eventEmitter.makeDelegate('emit', this); private readonly _items: ReadonlySet>; private readonly _activeTimelines = new Set(); /** * The set of all timelines that were updated during this sequence's most recent update. */ public getActiveTimelines(): ReadonlySet { return new Set(this._activeTimelines); } /** * All timelines included in this sequence along with their start times. */ public getItems(): ReadonlyArray> { return [...this._items.values()]; } /** * Creates a new sequence. * @param sequenceItems The timelines to include in this sequence, with each having a start time. */ public constructor(sequenceItems: Sequenced[]) { super(sequenceItems.map(si => si.timeline), latestEndingTime()); this._items = new Set(sequenceItems.sort((a, b) => a.startTime - b.startTime)); function latestEndingTime() { return sequenceItems.reduce((latestSoFar, currentItem) => { return Math.max(latestSoFar, currentItem.startTime + currentItem.timeline.length); }, 0); } } public _update(dt: number): void { this.updateTimelines(); this.emit('updated', { dt }, this); } protected _completed() { this.emit('completed', {}, this); } protected _start(): void { this.emit('started', {}, this); } protected _stop(): void { this.emit('stopped', {}, this); } protected _seek(from: number): void { this.emit('sought', { from }, this); } private updateTimelines() { this._items.forEach(si => { const { startTime, timeline } = si; if (itemIsInFutureButIsAlsoInProgress(si, this)) { removeFromActive(timeline, this); timeline.__update(0); } if (itemIsInPastButIsNotCompleted(si, this)) { removeFromActive(timeline, this); timeline.__update(timeline.length); } else if (startTime <= this.localTime && this.localTime <= startTime + timeline.length) { addToActive(timeline, this); timeline.__update(this.localTime - startTime); if (timeline.localTime >= timeline.length) { removeFromActive(timeline, this); } } }); function itemIsInFutureButIsAlsoInProgress(item: Sequenced, self: Sequence) { return self.localTime < item.startTime && item.timeline.localTime > 0; } function itemIsInPastButIsNotCompleted(item: Sequenced, self: Sequence) { const { startTime, timeline } = item; return self.localTime > startTime + timeline.length && timeline.localTime < timeline.length; } function addToActive(timeline: T, self: Sequence) { if (isActive(timeline, self)) return; self._activeTimelines.add(timeline); self.emit('timelineActivated', timeline, self); } function removeFromActive(timeline: T, self: Sequence) { if (!isActive(timeline, self)) return; self._activeTimelines.delete(timeline); self.emit('timelineDeactivated', timeline, self); } function isActive(timeline: T, self: Sequence) { return self._activeTimelines.has(timeline); } } }