UNPKG

@awayjs/scene

Version:
871 lines (676 loc) 22.8 kB
import { WaveAudio, IAudioChannel, EventBase, BaseAudioChannel } from '@awayjs/core'; import { IMovieClipAdapter } from '../adapters/IMovieClipAdapter'; import { Timeline } from '../base/Timeline'; import { MouseEvent } from '../events/MouseEvent'; import { FrameScriptManager } from '../managers/FrameScriptManager'; import { DisplayObject } from './DisplayObject'; import { Sprite } from './Sprite'; import { MouseButtons } from '../base/MouseButtons'; interface IScene { offset: number; name: string; labels: ILabel[]; numFrames: number; } interface ILabel { frame: number; name: string; } export class MovieClip extends Sprite { public static movieClipSoundsManagerClass = null; private static _movieClips: MovieClip[] = []; private static _activeSounds: Record<number, WaveAudio> = {}; public static stopSounds(sound?: WaveAudio) { if (sound) { if (MovieClip._activeSounds[sound.id]) MovieClip._activeSounds[sound.id].stop(); } else { for (const key in MovieClip._activeSounds) MovieClip._activeSounds[key].stop(); } } public static assetType: string = '[asset MovieClip]'; public static getNewMovieClip(timeline: Timeline): MovieClip { if (MovieClip._movieClips.length) { const movieClip: MovieClip = MovieClip._movieClips.pop(); movieClip.timeline = timeline; return movieClip; } return new MovieClip(timeline); } public static clearPool() { MovieClip._movieClips = []; } public symbolID: number = 0; public preventScript: boolean = false; private _timeline: Timeline; private _scenes: IScene[] = []; // buttonMode specifies if the mc has any mouse-listeners attached that should trigger showing the hand-cursor // if this is set once to true; it will never get set back to false again. private _buttonMode: boolean = false; // isButton specifies if the mc-timeline is actually considered a button-timeline private _isButton: boolean = false; private _onMouseOver: (event: MouseEvent) => void; private _onMouseOut: (event: MouseEvent) => void; private _onMouseDown: (event: MouseEvent) => void; private _onMouseUp: (event: MouseEvent) => void; private _onMouseUpOutside: (event: MouseEvent) => void; private _time: number = 0;// the current time inside the animation private _currentFrameIndex: number = -1;// the current frame private _isPlaying: boolean = true;// false if paused or stopped private _currentSceneIndex: number = 0; private _sceneDirty: boolean = false; private _skipAdvance: boolean; private _isInit: boolean = true; public _sessionID_childs: NumberMap<DisplayObject> = {}; private _sounds: Record<number, IAudioChannel[]> = {}; public _useHandCursor: boolean; private _soundVolume: number; private _skipFramesForStream: number = 0; private _soundStreams: any; /** * Mark that operation provided by timeline except a script, some operation not allowed from script * `removeChild` should not remove a child name * @private */ private _isTimelinePass = false; /** * Some symbols can be a sprite. This means that when we spawn it, we should track only first frame. * @private */ private _isSprite: boolean = false; public get isSprite() { return this._isSprite; } public buttonEnabled: boolean = true; public initSoundStream(streamInfo: any, maxFrameNum: number) { if (!this._soundStreams) { this._soundStreams = new MovieClip.movieClipSoundsManagerClass(this); } this._soundStreams.initSoundStream(streamInfo, maxFrameNum); } public addSoundStreamBlock(frameNum: number, streamBlock: any) { if (!this._soundStreams) { this._soundStreams = new MovieClip.movieClipSoundsManagerClass(this); } this._soundStreams.addSoundStreamBlock(frameNum, streamBlock); } private stopCurrentStream(frameNum: number) { if (this._soundStreams) { //console.log("sync sounds for mc: ", this.numFrames); return this._soundStreams.stopStream(frameNum); } } private resetStreamStopped() { if (this._soundStreams) { //console.log("sync sounds for mc: ", this.numFrames); this._soundStreams.resetStreamStopped(); } } private _syncSounds(frameNum: number): number { if (this._soundStreams) { //console.log("sync sounds for mc: ", this.numFrames); return this._soundStreams.syncSounds(frameNum, this._isPlaying, this.parent); } return 0; } constructor(timeline: Timeline, spriteMode = false) { super(); this._soundVolume = 1; this._isButton = false; this._buttonMode = false; this._useHandCursor = true; this.doingSwap = false; this.cursorType = 'pointer'; //this.debugVisible=true; this._onMouseOver = (_event: MouseEvent) => { if (this.buttonEnabled) this.currentFrameIndex = (_event.buttons & MouseButtons.PRIMARY_BUTTON) ? this.currentFrameIndex == 0 ? 0 : 2 : 1; else this.currentFrameIndex = 0; }; this._onMouseOut = (_event: MouseEvent) => { this.currentFrameIndex = (_event.buttons & MouseButtons.PRIMARY_BUTTON && this.currentFrameIndex != 0) ? 1 : 0; }; this._onMouseDown = (_event: MouseEvent) => { if (this.buttonEnabled) this.currentFrameIndex = 2; else this.currentFrameIndex = 0; }; this._onMouseUp = (_event: MouseEvent) => { this.currentFrameIndex = 1; }; this._onMouseUpOutside = (_event: MouseEvent) => { this.currentFrameIndex = 0; }; this._timeline = timeline; if (spriteMode) { this.transformToSprite(); } this._onChannelCompleteStopError = this._onChannelCompleteStopError.bind(this); } /** * Reduce frames of timeline to sprite mode. * Used for UIComponents, where timeline store more that 1 frame */ public transformToSprite() { if (this._isSprite) { return; } this._isSprite = true; const timeline = this._timeline; // regular MC or frames already is not more 1 if (timeline.numFrames <= 1) { return; } timeline.frame_command_indices = <any>[timeline.frame_command_indices[0]]; timeline.frame_recipe = <any>[timeline.frame_recipe[0]]; timeline.keyframe_constructframes = [timeline.keyframe_constructframes[0]]; timeline.keyframe_durations = <any>[timeline.keyframe_durations[0]]; timeline.keyframe_firstframes = [timeline.keyframe_firstframes[0]]; timeline.keyframe_indices = [timeline.keyframe_indices[0]]; } public startSound( sound: WaveAudio, loopsToPlay: number, onSoundComplete?: (event: EventBase) => void ) { const channel: IAudioChannel = sound.play(0, loopsToPlay); channel.volume = this._soundVolume; if (onSoundComplete) channel.addEventListener(BaseAudioChannel.COMPLETE, onSoundComplete); //internal listener to clear channel.addEventListener(BaseAudioChannel.COMPLETE, this._onChannelCompleteStopError); channel.addEventListener(BaseAudioChannel.STOP, this._onChannelCompleteStopError); channel.addEventListener(BaseAudioChannel.ERROR, this._onChannelCompleteStopError); const id = sound.id; if (!this._sounds[id]) this._sounds[id] = []; // store channels, stop it instead of sounds this._sounds[id].push(channel); //store active sound MovieClip._activeSounds[id] = sound; } public stopSounds(sound: WaveAudio = null) { if (sound) { if (this._sounds[sound.id]) this._stopChannels(sound); } else { for (const key in this._sounds) for (const c of this._sounds[key]) c.stop(); } const len: number = this._children.length; let child: DisplayObject; for (let i: number = 0; i < len; ++i) { child = this._children[i]; if (child.isAsset(MovieClip)) (<MovieClip>child).stopSounds(sound); } this.stopCurrentStream(this._currentFrameIndex); if (this._soundStreams) this._soundStreams.syncSounds(0, false, this.parent); } /** * Compute scene index by global frame index * @param frameIndex * @private */ private getSceneIndexByFrame(frameIndex: number): number { const scenes = this.scenes; if (scenes.length <= 1) { return 0; } for (let i = 0; i < scenes.length; i++) { if (scenes[i].offset > frameIndex) { return i - 1; } } return scenes.length - 1; } private getSceneIndex(scene: string) { const scenes = this.scenes; for (let i = 0; i < scenes.length && scene; i++) { if (scenes[i].name === scene) { return i; } } return 0; } public set currentSceneName(scene: string) { const index = this.getSceneIndex(scene); this._sceneDirty = this._currentSceneIndex !== index; this._currentSceneIndex = index; this._currentFrameIndex = this._scenes[index].offset; } public get currentSceneName(): string { return this.scenes[this._currentSceneIndex].name; } public get currentScene(): IScene { const currentScene = this.scenes[this._currentSceneIndex]; if (currentScene.numFrames === -1) { currentScene.numFrames = this.timeline.numFrames - currentScene.offset; } return currentScene; } public get scenes(): IScene[] { if (this._scenes.length === 0) { this._scenes[0] = { name: 'Scene1', offset: 0, labels: [], numFrames: this.timeline.numFrames, }; } return this._scenes; } public set scenes(value: IScene[]) { this._scenes = value; } public get isPlaying(): boolean { return this._isPlaying; } public get soundVolume(): number { return this._soundVolume; } public set soundVolume(value: number) { if (this._soundVolume == value) return; this._soundVolume = value; let channels: IAudioChannel[]; for (const key in this._sounds) if ((channels = this._sounds[key])) for (const c of channels) c.volume = value; } public stopSound(sound: WaveAudio) { MovieClip.stopSounds(sound); } public buttonReset() { if (this._isButton && !this.buttonEnabled) { this.currentFrameIndex = 0; } } public getMouseCursor(): string { if (this.name == 'scene') return 'initial'; if (this._useHandCursor && this.buttonMode) { return this.cursorType; } return 'initial'; /* var cursorName:string; var parent:DisplayObject=this.parent; while(parent){ if(parent.isAsset(MovieClip)){ cursorName=(<MovieClip>parent).getMouseCursor(); if(cursorName!="initial"){ return cursorName; } } parent=parent.parent; if(parent && parent.name=="scene"){ return "initial"; } } return "initial"; */ } public queueFrameScripts(timeline: Timeline, frame_idx: number, scriptPass1: boolean) { console.warn('[MovieClip] - queueFrameScripts should only be called on AVM-Adapters'); } public registerScriptObject(child: DisplayObject): void { this[child.name] = child; if (child.isAsset(MovieClip)) (<MovieClip>child).removeButtonListeners(); } public unregisterScriptObject(child: DisplayObject): void { delete this[child.name]; if (child.isAsset(MovieClip)) (<MovieClip>child).removeButtonListeners(); } public dispose(): void { this.disposeValues(); MovieClip._movieClips.push(this); } public disposeValues(): void { super.disposeValues(); this._sessionID_childs = {}; this._timeline = null; } public get useHandCursor(): boolean { return this._useHandCursor; } public set useHandCursor(value: boolean) { this._useHandCursor = value; } public get buttonMode(): boolean { return this._buttonMode; } public set buttonMode(value: boolean) { this._buttonMode = value; } public get isButton(): boolean { return this._isButton; } public set isButton(value: boolean) { this._isButton = value; } public get isInit(): boolean { return this._isInit; } public set isInit(value: boolean) { this._isInit = value; } public get timeline(): Timeline { return this._timeline; } public set timeline(value: Timeline) { if (this._timeline == value) return; this._timeline = value; this.reset(false, false); } /** * */ public loop: boolean = true; public get numFrames(): number { return this._timeline.numFrames; } public jumpToLabel(label: string, offset: number = 0): boolean { // the timeline.jumpTolabel will set currentFrameIndex const index = this._currentFrameIndex; this._timeline.jumpToLabel(this, label, offset); return index !== this._currentFrameIndex; } /** * the current index of the current active frame */ public constructedKeyFrameIndex: number = -1; public reset(fireScripts: boolean = true, resetSelf: boolean = true): void { this._isTimelinePass = true; if (resetSelf) super.reset(); this.resetStreamStopped(); // time only is relevant for the root mc, as it is the only one that executes the update function this._time = 0; //this.stopSounds(); if (resetSelf && this._adapter) (<IMovieClipAdapter> this.adapter).freeFromScript(); this.constructedKeyFrameIndex = -1; for (let i: number = this.numChildren - 1; i >= 0; i--) this.removeChildAt(i); if (this._graphics) this._graphics.clear(); if (fireScripts) { const numFrames: number = this._timeline.keyframe_indices.length; this._isPlaying = Boolean(numFrames > 1); this._currentSceneIndex = 0; if (numFrames) { this._currentFrameIndex = 0; // contruct the timeline and queue the script. //if(fireScripts){ this._timeline.constructNextFrame(this, fireScripts && !this.doingSwap && !this.preventScript, true); //} } else { this._currentFrameIndex = -1; } } // prevents the playhead to get moved in the advance frame again: this._skipAdvance = true; this._isTimelinePass = false; } public set currentFrameIndex(value: number) { this.jumpToIndex(value); } public get currentFrameIndex() { return this._currentFrameIndex; } /* * Setting the currentFrameIndex will move the playhead for this movieclip to the new position */ public get currentFrameIndexRelative(): number { return this._currentFrameIndex - this.currentScene.offset; } public set currentFrameIndexRelative(value: number) { this.jumpToIndex(value, this._currentSceneIndex); } public jumpToIndex(value: number, sceneIndex?: string | number): boolean { let queue_script: boolean = true; let offset: number = 0; let numFrames: number = this._timeline.keyframe_indices.length; this.resetStreamStopped(); if (!numFrames) return false; const scenes = this.scenes; // scene not presented - global navigation // we should compute scene index and shift frame if (typeof sceneIndex === 'undefined') { sceneIndex = this.getSceneIndexByFrame(value); } else { sceneIndex = typeof sceneIndex === 'string' ? this.getSceneIndex(sceneIndex) : sceneIndex; const scene = scenes[sceneIndex]; // fix negative frames size on latest scene. // this is because we don't know how many frames in instance time if (scene.numFrames === -1) { scene.numFrames = numFrames - scene.offset; } offset = scene.offset; numFrames = scene.numFrames; } this._currentSceneIndex = sceneIndex; if (value < 0) { value = 0; } else if (value >= numFrames) { // if value is greater than the available number of // frames, the playhead is moved to the last frame in the timeline. // In this case the frame specified is not considered a keyframe, // no scripts should be executed in this case value = numFrames - 1; queue_script = false; } value += offset; this._skipAdvance = false; if (this._currentFrameIndex === value && !this._sceneDirty) return false; this._sceneDirty = false; this._currentFrameIndex = value; //console.log("_currentFrameIndex ", this.name, this._currentFrameIndex); //changing current frame will ignore advance command for that //update's advanceFrame function, unless advanceFrame has //already been executed this._isTimelinePass = true; this._timeline.gotoFrame(this, value, queue_script, false, false); this._isTimelinePass = false; return true; } public addButtonListeners(): void { this._isButton = true; this.stop(); this.addEventListener(MouseEvent.MOUSE_OVER, this._onMouseOver); this.addEventListener(MouseEvent.MOUSE_OUT, this._onMouseOut); this.addEventListener(MouseEvent.MOUSE_DOWN, this._onMouseDown); this.addEventListener(MouseEvent.MOUSE_UP, this._onMouseUp); this.addEventListener(MouseEvent.MOUSE_UP_OUTSIDE, this._onMouseUpOutside); this.mouseChildren = false; } public removeButtonListeners(): void { this.removeEventListener(MouseEvent.MOUSE_OVER, this._onMouseOver); this.removeEventListener(MouseEvent.MOUSE_OUT, this._onMouseOut); this.removeEventListener(MouseEvent.MOUSE_DOWN, this._onMouseDown); this.removeEventListener(MouseEvent.MOUSE_UP, this._onMouseUp); this.removeEventListener(MouseEvent.MOUSE_UP_OUTSIDE, this._onMouseUpOutside); } public getTimelineChildAtSessionID(sessionID: number): DisplayObject { return this._sessionID_childs[sessionID]; } // should only be called from timeline when navigating frames public constructFrame(timeline: Timeline, start_construct_idx: number, target_keyframe_idx: number, jump_forward: boolean, frame_idx: number, queue_pass2: boolean, queue_script: boolean) { console.warn('[scene/MovieClip] - constructFrame not implemented'); } public addTimelineChildAtDepth(child: DisplayObject, depth: number, sessionID: number): DisplayObject { console.warn('[scene/MovieClip] - addTimelineChildAtDepth not implemented'); return null; } public removeTimelineChildAtDepth(depth: number): void { console.warn('[scene/MovieClip] - removeTimelineChildAtDepth not implemented'); } public removeChildAtInternal(index: number): DisplayObject { const child: DisplayObject = this._children[index]; if (child._adapter) (<IMovieClipAdapter>child.adapter).freeFromScript(); // only timeline can do this if (this._isTimelinePass) { (<IMovieClipAdapter> this.adapter).unregisterScriptObject(child); } delete this._sessionID_childs[child._sessionID]; child._sessionID = -1; return super.removeChildAtInternal(index); } public get assetType(): string { return MovieClip.assetType; } /** * Starts playback of animation from current position */ public play(): void { if (this._timeline.keyframe_indices.length > 1) this._isPlaying = true; } /** * Stop playback of animation and hold current position */ public stop(): void { //this.stopSounds(); this.resetStreamStopped(); this._isPlaying = false; } public clone(): MovieClip { const newInstance: MovieClip = MovieClip.getNewMovieClip(this._timeline); this.copyTo(newInstance); return newInstance; } public copyTo(movieClip: MovieClip): void { super.copyTo(movieClip); movieClip.buttonMode = this.buttonMode; movieClip.symbolID = this.symbolID; movieClip.loop = this.loop; movieClip._soundStreams = this._soundStreams; movieClip._scenes = this._scenes; // move to sprite mode too if (this._isSprite) { movieClip.transformToSprite(); } } public advanceFrameInternal(): void { // if this._skipadvance is true, the mc has already been moving on its timeline this frame // this happens for objects that have been newly added to parent // they still need to queue their scripts //if(this._timeline && this._timeline.numFrames>0) if (this._timeline && this._timeline.numFrames > 0 && this._isPlaying && !this._skipAdvance) { if (this._currentFrameIndex === this._timeline.numFrames - 1) { if (this.loop) { if (this._currentFrameIndex !== 0) { this._currentFrameIndex = 0; this._currentSceneIndex = 0; this.resetStreamStopped(); this._timeline.gotoFrame(this, 0, true, true, true); } // end of loop - jump to first frame. } else //end of timeline, stop playing this._isPlaying = false; } else { // not end - construct next frame this._currentFrameIndex++; this._currentSceneIndex = this.getSceneIndexByFrame(this._currentFrameIndex); this._timeline.constructNextFrame(this); } //console.log("advancedFrame ", this.name, this._currentFrameIndex); } super.advanceFrame(); this._skipAdvance = false; } public advanceFrame(): void { this._isTimelinePass = true; if (this._skipFramesForStream == 0) { this.advanceFrameInternal(); } /*if(this._skipFramesForStream<0){ console.log("wait for audio to catch up"); }*/ this._skipFramesForStream = this._syncSounds(this._currentFrameIndex); while (this._skipFramesForStream > 0) { //console.log("skip frame for keeping audio stream synced"); FrameScriptManager.execute_queue(); this.advanceFrameInternal(); this._skipFramesForStream = this._syncSounds(this._currentFrameIndex); } this._isTimelinePass = false; } // DEBUG CODE: logHierarchy(depth: number = 0): void { this.printHierarchyName(depth, this); const len = this._children.length; let child: DisplayObject; for (let i: number = 0; i < len; i++) { child = this._children[i]; if (child.isAsset(MovieClip)) (<MovieClip>child).logHierarchy(depth + 1); else this.printHierarchyName(depth + 1, child); } } printHierarchyName(depth: number, target: DisplayObject): void { let str = ''; for (let i = 0; i < depth; ++i) str += '--'; str += ' ' + target.name + ' = ' + target.id; console.log(str); } public clear(): void { //clear out potential instances this.resetStreamStopped(); /* check memory disposal with new approach of child-instancing for (var key in this._potentialInstances) { var instance: IAsset = this._potentialInstances[key]; //only dispose instances that are not used in script ie. do not have an instance name if (instance && !instance.name) { if (!instance.isAsset(Sprite)) { FrameScriptManager.add_child_to_dispose(<DisplayObject>instance); } delete this._potentialInstances[key]; } } */ super.clear(); } private _onChannelCompleteStopError(event: EventBase): void { const channel: IAudioChannel = event.target; const sound = channel.owner; const channels = this._sounds[sound.id]; const index = channels ? channels.indexOf(channel) : -1; channel.removeEventListener(BaseAudioChannel.COMPLETE, this._onChannelCompleteStopError); channel.removeEventListener(BaseAudioChannel.STOP, this._onChannelCompleteStopError); channel.removeEventListener(BaseAudioChannel.ERROR, this._onChannelCompleteStopError); if (index != -1) { channels.splice(index, 1); if (channels.length === 0) this._removeSound(sound); } } private _stopChannels(sound: WaveAudio) { const id: number = sound.id; const channels = this._sounds[id]; for (const c of channels) c.stop(); } private _removeSound(sound: WaveAudio): void { //remove sound from local sounds delete this._sounds[sound.id]; //remove sound from active sounds if (!sound.isPlaying) delete MovieClip._activeSounds[sound.id]; } }