From ada7407e414ae1f79b96981e07e185272a82affe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Mon, 31 Oct 2016 10:50:10 -0700 Subject: [PATCH] feat(animations): provide support for web-workers Closes #12656 --- .../src/animation/animation_group_player.ts | 12 +- .../animation/animation_sequence_player.ts | 10 +- .../core/testing/mock_animation_player.ts | 4 +- .../src/dom/dom_animate_player.ts | 1 + .../src/dom/web_animations_player.ts | 14 +- .../platform-browser/src/private_export.ts | 9 +- .../testing/mock_dom_animate_player.ts | 5 + .../src/private_import_platform-browser.ts | 1 + .../src/web_workers/shared/serializer.ts | 3 +- .../src/web_workers/ui/event_dispatcher.ts | 11 +- .../src/web_workers/ui/renderer.ts | 86 ++++- .../web_workers/worker/event_deserializer.ts | 1 - .../src/web_workers/worker/renderer.ts | 149 ++++++-- .../platform-webworker/src/worker_render.ts | 8 +- .../renderer_animation_integration_spec.ts | 333 ++++++++++++++++++ .../animations/background_index.ts | 21 ++ .../src/web_workers/animations/index.html | 13 + .../src/web_workers/animations/index.ts | 13 + .../web_workers/animations/index_common.ts | 43 +++ .../src/web_workers/animations/loader.js | 43 +++ 20 files changed, 728 insertions(+), 52 deletions(-) create mode 100644 modules/@angular/platform-webworker/test/web_workers/worker/renderer_animation_integration_spec.ts create mode 100644 modules/playground/src/web_workers/animations/background_index.ts create mode 100644 modules/playground/src/web_workers/animations/index.html create mode 100644 modules/playground/src/web_workers/animations/index.ts create mode 100644 modules/playground/src/web_workers/animations/index_common.ts create mode 100644 modules/playground/src/web_workers/animations/loader.js diff --git a/modules/@angular/core/src/animation/animation_group_player.ts b/modules/@angular/core/src/animation/animation_group_player.ts index daf0d0b8a1fd71..40e9c5b8b5e3ff 100644 --- a/modules/@angular/core/src/animation/animation_group_player.ts +++ b/modules/@angular/core/src/animation/animation_group_player.ts @@ -13,8 +13,9 @@ import {AnimationPlayer} from './animation_player'; export class AnimationGroupPlayer implements AnimationPlayer { private _onDoneFns: Function[] = []; private _onStartFns: Function[] = []; - private _finished = false; - private _started = false; + private _finished: boolean = false; + private _started: boolean = false; + private _destroyed: boolean = false; public parentPlayer: AnimationPlayer = null; @@ -73,8 +74,11 @@ export class AnimationGroupPlayer implements AnimationPlayer { } destroy(): void { - this._onFinish(); - this._players.forEach(player => player.destroy()); + if (!this._destroyed) { + this._onFinish(); + this._players.forEach(player => player.destroy()); + this._destroyed = true; + } } reset(): void { this._players.forEach(player => player.reset()); } diff --git a/modules/@angular/core/src/animation/animation_sequence_player.ts b/modules/@angular/core/src/animation/animation_sequence_player.ts index 20545bacb205f9..eeeca1ca671684 100644 --- a/modules/@angular/core/src/animation/animation_sequence_player.ts +++ b/modules/@angular/core/src/animation/animation_sequence_player.ts @@ -15,8 +15,9 @@ export class AnimationSequencePlayer implements AnimationPlayer { private _activePlayer: AnimationPlayer; private _onDoneFns: Function[] = []; private _onStartFns: Function[] = []; - private _finished = false; + private _finished: boolean = false; private _started: boolean = false; + private _destroyed: boolean = false; public parentPlayer: AnimationPlayer = null; @@ -90,8 +91,11 @@ export class AnimationSequencePlayer implements AnimationPlayer { } destroy(): void { - this._onFinish(); - this._players.forEach(player => player.destroy()); + if (!this._destroyed) { + this._onFinish(); + this._players.forEach(player => player.destroy()); + this._destroyed = true; + } } setPosition(p: any /** TODO #9100 */): void { this._players[0].setPosition(p); } diff --git a/modules/@angular/core/testing/mock_animation_player.ts b/modules/@angular/core/testing/mock_animation_player.ts index c887fcd19f23cd..a819885b3826e2 100644 --- a/modules/@angular/core/testing/mock_animation_player.ts +++ b/modules/@angular/core/testing/mock_animation_player.ts @@ -11,8 +11,8 @@ import {AnimationPlayer} from '@angular/core'; export class MockAnimationPlayer implements AnimationPlayer { private _onDoneFns: Function[] = []; private _onStartFns: Function[] = []; - private _finished = false; - private _destroyed = false; + private _finished: boolean = false; + private _destroyed: boolean = false; private _started: boolean = false; public parentPlayer: AnimationPlayer = null; diff --git a/modules/@angular/platform-browser/src/dom/dom_animate_player.ts b/modules/@angular/platform-browser/src/dom/dom_animate_player.ts index b82cbd8fa66480..4183a42529afd3 100644 --- a/modules/@angular/platform-browser/src/dom/dom_animate_player.ts +++ b/modules/@angular/platform-browser/src/dom/dom_animate_player.ts @@ -14,4 +14,5 @@ export interface DomAnimatePlayer { onfinish: Function; position: number; currentTime: number; + addEventListener(eventName: string, handler: (event: any) => any): any; } diff --git a/modules/@angular/platform-browser/src/dom/web_animations_player.ts b/modules/@angular/platform-browser/src/dom/web_animations_player.ts index 8c39e85c46c19a..924c9190aaf2cd 100644 --- a/modules/@angular/platform-browser/src/dom/web_animations_player.ts +++ b/modules/@angular/platform-browser/src/dom/web_animations_player.ts @@ -15,8 +15,9 @@ import {DomAnimatePlayer} from './dom_animate_player'; export class WebAnimationsPlayer implements AnimationPlayer { private _onDoneFns: Function[] = []; private _onStartFns: Function[] = []; - private _finished = false; - private _initialized = false; + private _finished: boolean = false; + private _initialized: boolean = false; + private _destroyed: boolean = false; private _player: DomAnimatePlayer; private _started: boolean = false; private _duration: number; @@ -54,7 +55,7 @@ export class WebAnimationsPlayer implements AnimationPlayer { // this is required so that the player doesn't start to animate right away this.reset(); - this._player.onfinish = () => this._onFinish(); + this._player.addEventListener('finish', () => this._onFinish()); } /** @internal */ @@ -97,8 +98,11 @@ export class WebAnimationsPlayer implements AnimationPlayer { hasStarted(): boolean { return this._started; } destroy(): void { - this.reset(); - this._onFinish(); + if (!this._destroyed) { + this.reset(); + this._onFinish(); + this._destroyed = true; + } } get totalTime(): number { return this._duration; } diff --git a/modules/@angular/platform-browser/src/private_export.ts b/modules/@angular/platform-browser/src/private_export.ts index 50cd2b41900b2d..ec4e0c41ffdf1a 100644 --- a/modules/@angular/platform-browser/src/private_export.ts +++ b/modules/@angular/platform-browser/src/private_export.ts @@ -18,8 +18,7 @@ import * as dom_events from './dom/events/dom_events'; import * as hammer_gesture from './dom/events/hammer_gestures'; import * as key_events from './dom/events/key_events'; import * as shared_styles_host from './dom/shared_styles_host'; - - +import {WebAnimationsDriver} from './dom/web_animations_driver'; export var __platform_browser_private__: { _BrowserPlatformLocation?: location.BrowserPlatformLocation, @@ -54,7 +53,8 @@ export var __platform_browser_private__: { HammerGesturesPlugin: typeof hammer_gesture.HammerGesturesPlugin, initDomAdapter: typeof browser.initDomAdapter, INTERNAL_BROWSER_PLATFORM_PROVIDERS: typeof browser.INTERNAL_BROWSER_PLATFORM_PROVIDERS, - BROWSER_SANITIZATION_PROVIDERS: typeof browser.BROWSER_SANITIZATION_PROVIDERS + BROWSER_SANITIZATION_PROVIDERS: typeof browser.BROWSER_SANITIZATION_PROVIDERS, + WebAnimationsDriver: typeof WebAnimationsDriver } = { BrowserPlatformLocation: location.BrowserPlatformLocation, DomAdapter: dom_adapter.DomAdapter, @@ -78,5 +78,6 @@ export var __platform_browser_private__: { HammerGesturesPlugin: hammer_gesture.HammerGesturesPlugin, initDomAdapter: browser.initDomAdapter, INTERNAL_BROWSER_PLATFORM_PROVIDERS: browser.INTERNAL_BROWSER_PLATFORM_PROVIDERS, - BROWSER_SANITIZATION_PROVIDERS: browser.BROWSER_SANITIZATION_PROVIDERS + BROWSER_SANITIZATION_PROVIDERS: browser.BROWSER_SANITIZATION_PROVIDERS, + WebAnimationsDriver: WebAnimationsDriver }; diff --git a/modules/@angular/platform-browser/testing/mock_dom_animate_player.ts b/modules/@angular/platform-browser/testing/mock_dom_animate_player.ts index 2f77ba1ae7ff73..20162743afbf1b 100644 --- a/modules/@angular/platform-browser/testing/mock_dom_animate_player.ts +++ b/modules/@angular/platform-browser/testing/mock_dom_animate_player.ts @@ -40,4 +40,9 @@ export class MockDomAnimatePlayer implements DomAnimatePlayer { this._position = val; } get position(): number { return this._position; } + addEventListener(eventName: string, handler: (event: any) => any): any { + if (eventName == 'finish') { + this.onfinish = handler; + } + } } diff --git a/modules/@angular/platform-webworker/src/private_import_platform-browser.ts b/modules/@angular/platform-webworker/src/private_import_platform-browser.ts index 91f95dfff6bc2a..c592b7f0cdfc2e 100644 --- a/modules/@angular/platform-webworker/src/private_import_platform-browser.ts +++ b/modules/@angular/platform-webworker/src/private_import_platform-browser.ts @@ -24,3 +24,4 @@ export const KeyEventsPlugin: typeof _.KeyEventsPlugin = _.KeyEventsPlugin; export const HammerGesturesPlugin: typeof _.HammerGesturesPlugin = _.HammerGesturesPlugin; export const DomAdapter: typeof _.DomAdapter = _.DomAdapter; export const setRootDomAdapter: typeof _.setRootDomAdapter = _.setRootDomAdapter; +export const WebAnimationsDriver: typeof _.WebAnimationsDriver = _.WebAnimationsDriver; diff --git a/modules/@angular/platform-webworker/src/web_workers/shared/serializer.ts b/modules/@angular/platform-webworker/src/web_workers/shared/serializer.ts index 52662b9b72b4ea..f16dd856ae5329 100644 --- a/modules/@angular/platform-webworker/src/web_workers/shared/serializer.ts +++ b/modules/@angular/platform-webworker/src/web_workers/shared/serializer.ts @@ -13,8 +13,6 @@ import {isPresent} from '../../facade/lang'; import {RenderStore} from './render_store'; import {LocationType} from './serialized_types'; - - // PRIMITIVE is any type that does not need to be serialized (string, number, boolean) // We set it to String so that it is considered a Type. /** @@ -121,5 +119,6 @@ export class Serializer { } } +export const ANIMATION_WORKER_PLAYER_PREFIX = 'AnimationPlayer.'; export class RenderStoreObject {} diff --git a/modules/@angular/platform-webworker/src/web_workers/ui/event_dispatcher.ts b/modules/@angular/platform-webworker/src/web_workers/ui/event_dispatcher.ts index 2f2d9ee106a37e..d46188029b5a06 100644 --- a/modules/@angular/platform-webworker/src/web_workers/ui/event_dispatcher.ts +++ b/modules/@angular/platform-webworker/src/web_workers/ui/event_dispatcher.ts @@ -5,8 +5,6 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - - import {EventEmitter} from '../../facade/async'; import {RenderStoreObject, Serializer} from '../shared/serializer'; @@ -15,6 +13,15 @@ import {serializeEventWithTarget, serializeGenericEvent, serializeKeyboardEvent, export class EventDispatcher { constructor(private _sink: EventEmitter, private _serializer: Serializer) {} + dispatchAnimationEvent(player: any, phaseName: string, element: any): boolean { + this._sink.emit({ + 'element': this._serializer.serialize(element, RenderStoreObject), + 'animationPlayer': this._serializer.serialize(player, RenderStoreObject), + 'phaseName': phaseName + }); + return true; + } + dispatchRenderEvent(element: any, eventTarget: string, eventName: string, event: any): boolean { var serializedEvent: any /** TODO #9100 */; // TODO (jteplitz602): support custom events #3350 diff --git a/modules/@angular/platform-webworker/src/web_workers/ui/renderer.ts b/modules/@angular/platform-webworker/src/web_workers/ui/renderer.ts index ea87241411af78..3a97dfa5033a76 100644 --- a/modules/@angular/platform-webworker/src/web_workers/ui/renderer.ts +++ b/modules/@angular/platform-webworker/src/web_workers/ui/renderer.ts @@ -6,13 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ -import {Injectable, RenderComponentType, Renderer, RootRenderer} from '@angular/core'; - +import {AnimationPlayer, Injectable, RenderComponentType, Renderer, RootRenderer} from '@angular/core'; import {MessageBus} from '../shared/message_bus'; import {EVENT_CHANNEL, RENDERER_CHANNEL} from '../shared/messaging_api'; import {RenderStore} from '../shared/render_store'; -import {PRIMITIVE, RenderStoreObject, Serializer} from '../shared/serializer'; -import {ServiceMessageBrokerFactory} from '../shared/service_message_broker'; +import {ANIMATION_WORKER_PLAYER_PREFIX, PRIMITIVE, RenderStoreObject, Serializer} from '../shared/serializer'; +import {ServiceMessageBroker, ServiceMessageBrokerFactory} from '../shared/service_message_broker'; import {EventDispatcher} from '../ui/event_dispatcher'; @Injectable() @@ -86,6 +85,65 @@ export class MessageBasedRenderer { this._listenGlobal.bind(this)); broker.registerMethod( 'listenDone', [RenderStoreObject, RenderStoreObject], this._listenDone.bind(this)); + broker.registerMethod( + 'animate', + [ + RenderStoreObject, RenderStoreObject, PRIMITIVE, PRIMITIVE, PRIMITIVE, PRIMITIVE, + PRIMITIVE, PRIMITIVE + ], + this._animate.bind(this)); + + this._bindAnimationPlayerMethods(broker); + } + + private _bindAnimationPlayerMethods(broker: ServiceMessageBroker) { + broker.registerMethod( + ANIMATION_WORKER_PLAYER_PREFIX + 'play', [RenderStoreObject, RenderStoreObject], + (player: AnimationPlayer, element: any) => player.play()); + + broker.registerMethod( + ANIMATION_WORKER_PLAYER_PREFIX + 'pause', [RenderStoreObject, RenderStoreObject], + (player: AnimationPlayer, element: any) => player.pause()); + + broker.registerMethod( + ANIMATION_WORKER_PLAYER_PREFIX + 'init', [RenderStoreObject, RenderStoreObject], + (player: AnimationPlayer, element: any) => player.init()); + + broker.registerMethod( + ANIMATION_WORKER_PLAYER_PREFIX + 'restart', [RenderStoreObject, RenderStoreObject], + (player: AnimationPlayer, element: any) => player.restart()); + + broker.registerMethod( + ANIMATION_WORKER_PLAYER_PREFIX + 'destroy', [RenderStoreObject, RenderStoreObject], + (player: AnimationPlayer, element: any) => { + player.destroy(); + this._renderStore.remove(player); + }); + + broker.registerMethod( + ANIMATION_WORKER_PLAYER_PREFIX + 'finish', [RenderStoreObject, RenderStoreObject], + (player: AnimationPlayer, element: any) => player.finish()); + + broker.registerMethod( + ANIMATION_WORKER_PLAYER_PREFIX + 'getPosition', [RenderStoreObject, RenderStoreObject], + (player: AnimationPlayer, element: any) => player.getPosition()); + + broker.registerMethod( + ANIMATION_WORKER_PLAYER_PREFIX + 'onStart', + [RenderStoreObject, RenderStoreObject, PRIMITIVE], + (player: AnimationPlayer, element: any) => + this._listenOnAnimationPlayer(player, element, 'onStart')); + + broker.registerMethod( + ANIMATION_WORKER_PLAYER_PREFIX + 'onDone', + [RenderStoreObject, RenderStoreObject, PRIMITIVE], + (player: AnimationPlayer, element: any) => + this._listenOnAnimationPlayer(player, element, 'onDone')); + + broker.registerMethod( + ANIMATION_WORKER_PLAYER_PREFIX + 'setPosition', + [RenderStoreObject, RenderStoreObject, PRIMITIVE], + (player: AnimationPlayer, element: any, position: number) => player.setPosition(position)); } private _renderComponent(renderComponentType: RenderComponentType, rendererId: number) { @@ -187,4 +245,24 @@ export class MessageBasedRenderer { } private _listenDone(renderer: Renderer, unlistenCallback: Function) { unlistenCallback(); } + + private _animate( + renderer: Renderer, element: any, startingStyles: any, keyframes: any[], duration: number, + delay: number, easing: string, playerId: any) { + var player = renderer.animate(element, startingStyles, keyframes, duration, delay, easing); + this._renderStore.store(player, playerId); + } + + private _listenOnAnimationPlayer(player: AnimationPlayer, element: any, phaseName: string) { + const onEventComplete = + () => { this._eventDispatcher.dispatchAnimationEvent(player, phaseName, element); }; + + // there is no need to register a unlistener value here since the + // internal player callbacks are removed when the player is destroyed + if (phaseName == 'onDone') { + player.onDone(() => onEventComplete()); + } else { + player.onStart(() => onEventComplete()); + } + } } diff --git a/modules/@angular/platform-webworker/src/web_workers/worker/event_deserializer.ts b/modules/@angular/platform-webworker/src/web_workers/worker/event_deserializer.ts index ca0b1b6f6aa3c6..93486c40186116 100644 --- a/modules/@angular/platform-webworker/src/web_workers/worker/event_deserializer.ts +++ b/modules/@angular/platform-webworker/src/web_workers/worker/event_deserializer.ts @@ -5,7 +5,6 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - // no deserialization is necessary in TS. // This is only here to match dart interface export function deserializeGenericEvent(serializedEvent: {[key: string]: any}): diff --git a/modules/@angular/platform-webworker/src/web_workers/worker/renderer.ts b/modules/@angular/platform-webworker/src/web_workers/worker/renderer.ts index d55ae476c08b7c..5c6a0567243100 100644 --- a/modules/@angular/platform-webworker/src/web_workers/worker/renderer.ts +++ b/modules/@angular/platform-webworker/src/web_workers/worker/renderer.ts @@ -15,8 +15,7 @@ import {ClientMessageBrokerFactory, FnArg, UiArguments} from '../shared/client_m import {MessageBus} from '../shared/message_bus'; import {EVENT_CHANNEL, RENDERER_CHANNEL} from '../shared/messaging_api'; import {RenderStore} from '../shared/render_store'; -import {RenderStoreObject, Serializer} from '../shared/serializer'; - +import {ANIMATION_WORKER_PLAYER_PREFIX, RenderStoreObject, Serializer} from '../shared/serializer'; import {deserializeGenericEvent} from './event_deserializer'; @Injectable() @@ -28,7 +27,7 @@ export class WebWorkerRootRenderer implements RootRenderer { constructor( messageBrokerFactory: ClientMessageBrokerFactory, bus: MessageBus, - private _serializer: Serializer, private _renderStore: RenderStore) { + private _serializer: Serializer, public renderStore: RenderStore) { this._messageBroker = messageBrokerFactory.createMessageBroker(RENDERER_CHANNEL); bus.initChannel(EVENT_CHANNEL); var source = bus.from(EVENT_CHANNEL); @@ -36,15 +35,22 @@ export class WebWorkerRootRenderer implements RootRenderer { } private _dispatchEvent(message: {[key: string]: any}): void { - var eventName = message['eventName']; - var target = message['eventTarget']; - var event = deserializeGenericEvent(message['event']); - if (isPresent(target)) { - this.globalEvents.dispatchEvent(eventNameWithTarget(target, eventName), event); + var element = + this._serializer.deserialize(message['element'], RenderStoreObject); + var playerData = message['animationPlayer']; + if (playerData) { + var phaseName = message['phaseName']; + var player = this._serializer.deserialize(playerData, RenderStoreObject); + element.animationPlayerEvents.dispatchEvent(player, phaseName); } else { - var element = - this._serializer.deserialize(message['element'], RenderStoreObject); - element.events.dispatchEvent(eventName, event); + var eventName = message['eventName']; + var target = message['eventTarget']; + var event = deserializeGenericEvent(message['event']); + if (isPresent(target)) { + this.globalEvents.dispatchEvent(eventNameWithTarget(target, eventName), event); + } else { + element.events.dispatchEvent(eventName, event); + } } } @@ -53,8 +59,8 @@ export class WebWorkerRootRenderer implements RootRenderer { if (!result) { result = new WebWorkerRenderer(this, componentType); this._componentRenderers.set(componentType.id, result); - var id = this._renderStore.allocateId(); - this._renderStore.store(result, id); + var id = this.renderStore.allocateId(); + this.renderStore.store(result, id); this.runOnService('renderComponent', [ new FnArg(componentType, RenderComponentType), new FnArg(result, RenderStoreObject), @@ -70,16 +76,16 @@ export class WebWorkerRootRenderer implements RootRenderer { allocateNode(): WebWorkerRenderNode { var result = new WebWorkerRenderNode(); - var id = this._renderStore.allocateId(); - this._renderStore.store(result, id); + var id = this.renderStore.allocateId(); + this.renderStore.store(result, id); return result; } - allocateId(): number { return this._renderStore.allocateId(); } + allocateId(): number { return this.renderStore.allocateId(); } destroyNodes(nodes: any[]) { for (var i = 0; i < nodes.length; i++) { - this._renderStore.remove(nodes[i]); + this.renderStore.remove(nodes[i]); } } } @@ -232,10 +238,20 @@ export class WebWorkerRenderer implements Renderer, RenderStoreObject { } animate( - element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], + renderElement: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], duration: number, delay: number, easing: string): AnimationPlayer { - // TODO - return null; + const playerId = this._rootRenderer.allocateId(); + + this._runOnService('animate', [ + new FnArg(renderElement, RenderStoreObject), new FnArg(startingStyles, null), + new FnArg(keyframes, null), new FnArg(duration, null), new FnArg(delay, null), + new FnArg(easing, null), new FnArg(playerId, null) + ]); + + const player = new _AnimationWorkerRendererPlayer(this._rootRenderer, renderElement); + this._rootRenderer.renderStore.store(player, playerId); + + return player; } } @@ -268,8 +284,99 @@ export class NamedEventEmitter { } } +export class AnimationPlayerEmitter { + private _listeners: Map; + + private _getListeners(player: AnimationPlayer, phaseName: string): Function[] { + if (!this._listeners) { + this._listeners = new Map(); + } + var phaseMap = this._listeners.get(player); + if (!phaseMap) { + this._listeners.set(player, phaseMap = {}); + } + var phaseFns = phaseMap[phaseName]; + if (!phaseFns) { + phaseFns = phaseMap[phaseName] = []; + } + return phaseFns; + } + + listen(player: AnimationPlayer, phaseName: string, callback: Function) { + this._getListeners(player, phaseName).push(callback); + } + + unlisten(player: AnimationPlayer) { this._listeners.delete(player); } + + dispatchEvent(player: AnimationPlayer, phaseName: string) { + var listeners = this._getListeners(player, phaseName); + for (var i = 0; i < listeners.length; i++) { + listeners[i](); + } + } +} + function eventNameWithTarget(target: string, eventName: string): string { return `${target}:${eventName}`; } -export class WebWorkerRenderNode { events: NamedEventEmitter = new NamedEventEmitter(); } +export class WebWorkerRenderNode { + events = new NamedEventEmitter(); + animationPlayerEvents = new AnimationPlayerEmitter(); +} + +class _AnimationWorkerRendererPlayer implements AnimationPlayer, RenderStoreObject { + public parentPlayer: AnimationPlayer = null; + + private _destroyed: boolean = false; + private _started: boolean = false; + + constructor(private _rootRenderer: WebWorkerRootRenderer, private _renderElement: any) {} + + private _runOnService(fnName: string, fnArgs: FnArg[]) { + var fnArgsWithRenderer = [ + new FnArg(this, RenderStoreObject), new FnArg(this._renderElement, RenderStoreObject) + ].concat(fnArgs); + this._rootRenderer.runOnService(ANIMATION_WORKER_PLAYER_PREFIX + fnName, fnArgsWithRenderer); + } + + onStart(fn: () => void): void { + this._renderElement.animationPlayerEvents.listen(this, 'onStart', fn); + this._runOnService('onStart', []); + } + + onDone(fn: () => void): void { + this._renderElement.animationPlayerEvents.listen(this, 'onDone', fn); + this._runOnService('onDone', []); + } + + hasStarted(): boolean { return this._started; } + + init(): void { this._runOnService('init', []); } + + play(): void { + this._started = true; + this._runOnService('play', []); + } + + pause(): void { this._runOnService('pause', []); } + + restart(): void { this._runOnService('restart', []); } + + finish(): void { this._runOnService('finish', []); } + + destroy(): void { + if (!this._destroyed) { + this._renderElement.animationPlayerEvents.unlisten(this); + this._runOnService('destroy', []); + this._rootRenderer.renderStore.remove(this); + this._destroyed = true; + } + } + + reset(): void { this._runOnService('reset', []); } + + setPosition(p: number): void { this._runOnService('setPosition', [new FnArg(p, null)]); } + + getPosition(): number { return 0; } +} diff --git a/modules/@angular/platform-webworker/src/worker_render.ts b/modules/@angular/platform-webworker/src/worker_render.ts index e0f3dc07eb1d04..84a1f043f30a25 100644 --- a/modules/@angular/platform-webworker/src/worker_render.ts +++ b/modules/@angular/platform-webworker/src/worker_render.ts @@ -10,7 +10,7 @@ import {ErrorHandler, Injectable, Injector, NgZone, OpaqueToken, PLATFORM_INITIA import {AnimationDriver, DOCUMENT, EVENT_MANAGER_PLUGINS, EventManager, HAMMER_GESTURE_CONFIG, HammerGestureConfig} from '@angular/platform-browser'; import {APP_ID_RANDOM_PROVIDER} from './private_import_core'; -import {BROWSER_SANITIZATION_PROVIDERS, BrowserDomAdapter, BrowserGetTestability, DomEventsPlugin, DomRootRenderer, DomRootRenderer_, DomSharedStylesHost, HammerGesturesPlugin, KeyEventsPlugin, SharedStylesHost, getDOM} from './private_import_platform-browser'; +import {BROWSER_SANITIZATION_PROVIDERS, BrowserDomAdapter, BrowserGetTestability, DomEventsPlugin, DomRootRenderer, DomRootRenderer_, DomSharedStylesHost, HammerGesturesPlugin, KeyEventsPlugin, SharedStylesHost, WebAnimationsDriver, getDOM} from './private_import_platform-browser'; import {ON_WEB_WORKER} from './web_workers/shared/api'; import {ClientMessageBrokerFactory, ClientMessageBrokerFactory_} from './web_workers/shared/client_message_broker'; import {MessageBus} from './web_workers/shared/message_bus'; @@ -21,7 +21,6 @@ import {ServiceMessageBrokerFactory, ServiceMessageBrokerFactory_} from './web_w import {MessageBasedRenderer} from './web_workers/ui/renderer'; - /** * Wrapper class that exposes the Worker * and underlying {@link MessageBus} for lower level message passing. @@ -155,7 +154,8 @@ function spawnWebWorker(uri: string, instance: WebWorkerInstance): void { } function _resolveDefaultAnimationDriver(): AnimationDriver { - // web workers have not been tested or configured to - // work with animations just yet... + if (getDOM().supportsWebAnimation()) { + return new WebAnimationsDriver(); + } return AnimationDriver.NOOP; } diff --git a/modules/@angular/platform-webworker/test/web_workers/worker/renderer_animation_integration_spec.ts b/modules/@angular/platform-webworker/test/web_workers/worker/renderer_animation_integration_spec.ts new file mode 100644 index 00000000000000..735276bfcd8659 --- /dev/null +++ b/modules/@angular/platform-webworker/test/web_workers/worker/renderer_animation_integration_spec.ts @@ -0,0 +1,333 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {AUTO_STYLE, AnimationTransitionEvent, Component, Injector, animate, state, style, transition, trigger} from '@angular/core'; +import {DebugDomRootRenderer} from '@angular/core/src/debug/debug_renderer'; +import {RootRenderer} from '@angular/core/src/render/api'; +import {TestBed} from '@angular/core/testing'; +import {MockAnimationPlayer} from '@angular/core/testing/testing_internal'; +import {AnimationDriver} from '@angular/platform-browser/src/dom/animation_driver'; +import {DomRootRenderer, DomRootRenderer_} from '@angular/platform-browser/src/dom/dom_renderer'; +import {BrowserTestingModule} from '@angular/platform-browser/testing'; +import {expect} from '@angular/platform-browser/testing/matchers'; +import {MockAnimationDriver} from '@angular/platform-browser/testing/mock_animation_driver'; +import {ClientMessageBrokerFactory, ClientMessageBrokerFactory_} from '@angular/platform-webworker/src/web_workers/shared/client_message_broker'; +import {RenderStore} from '@angular/platform-webworker/src/web_workers/shared/render_store'; +import {Serializer} from '@angular/platform-webworker/src/web_workers/shared/serializer'; +import {ServiceMessageBrokerFactory_} from '@angular/platform-webworker/src/web_workers/shared/service_message_broker'; +import {MessageBasedRenderer} from '@angular/platform-webworker/src/web_workers/ui/renderer'; +import {WebWorkerRootRenderer} from '@angular/platform-webworker/src/web_workers/worker/renderer'; + +import {platformBrowserDynamicTesting} from '../../../../platform-browser-dynamic/testing'; +import {PairedMessageBuses, createPairedMessageBuses} from '../shared/web_worker_test_util'; + +export function main() { + function createWebWorkerBrokerFactory( + messageBuses: PairedMessageBuses, workerSerializer: Serializer, uiSerializer: Serializer, + domRootRenderer: DomRootRenderer, uiRenderStore: RenderStore): ClientMessageBrokerFactory { + var uiMessageBus = messageBuses.ui; + var workerMessageBus = messageBuses.worker; + + // set up the worker side + var webWorkerBrokerFactory = + new ClientMessageBrokerFactory_(workerMessageBus, workerSerializer); + + // set up the ui side + var uiMessageBrokerFactory = new ServiceMessageBrokerFactory_(uiMessageBus, uiSerializer); + var renderer = new MessageBasedRenderer( + uiMessageBrokerFactory, uiMessageBus, uiSerializer, uiRenderStore, domRootRenderer); + renderer.start(); + + return webWorkerBrokerFactory; + } + + function createWorkerRenderer( + workerSerializer: Serializer, uiSerializer: Serializer, domRootRenderer: DomRootRenderer, + uiRenderStore: RenderStore, workerRenderStore: RenderStore): RootRenderer { + var messageBuses = createPairedMessageBuses(); + var brokerFactory = createWebWorkerBrokerFactory( + messageBuses, workerSerializer, uiSerializer, domRootRenderer, uiRenderStore); + var workerRootRenderer = new WebWorkerRootRenderer( + brokerFactory, messageBuses.worker, workerSerializer, workerRenderStore); + return new DebugDomRootRenderer(workerRootRenderer); + } + + describe('Web Worker Renderer Animations', () => { + var uiTestBed: TestBed; + var uiRenderStore: RenderStore; + var workerRenderStore: RenderStore; + + beforeEach(() => { + uiRenderStore = new RenderStore(); + uiTestBed = new TestBed(); + uiTestBed.platform = platformBrowserDynamicTesting(); + uiTestBed.ngModule = BrowserTestingModule; + uiTestBed.configureTestingModule({ + providers: [ + {provide: AnimationDriver, useClass: MockAnimationDriver}, Serializer, + {provide: RenderStore, useValue: uiRenderStore}, + {provide: DomRootRenderer, useClass: DomRootRenderer_}, + {provide: RootRenderer, useExisting: DomRootRenderer} + ] + }); + var uiSerializer = uiTestBed.get(Serializer); + var domRootRenderer = uiTestBed.get(DomRootRenderer); + workerRenderStore = new RenderStore(); + + TestBed.configureTestingModule({ + declarations: [AnimationCmp, MultiAnimationCmp], + providers: [ + Serializer, {provide: RenderStore, useValue: workerRenderStore}, { + provide: RootRenderer, + useFactory: (workerSerializer: Serializer) => { + return createWorkerRenderer( + workerSerializer, uiSerializer, domRootRenderer, uiRenderStore, + workerRenderStore); + }, + deps: [Serializer] + } + ] + }); + }); + + var uiDriver: MockAnimationDriver; + beforeEach(() => { uiDriver = uiTestBed.get(AnimationDriver) as MockAnimationDriver; }); + + function retrieveFinalAnimationStepStyles(keyframes: any[]) { return keyframes[1][1]; } + + it('should trigger an animation and animate styles', () => { + const fixture = TestBed.createComponent(AnimationCmp); + const cmp = fixture.componentInstance; + + cmp.state = 'on'; + fixture.detectChanges(); + + var step1 = uiDriver.log.shift(); + var step2 = uiDriver.log.shift(); + + var step1Styles = retrieveFinalAnimationStepStyles(step1['keyframeLookup']); + var step2Styles = retrieveFinalAnimationStepStyles(step2['keyframeLookup']); + + expect(step1Styles).toEqual({fontSize: '20px'}); + expect(step2Styles).toEqual({opacity: '1', fontSize: '50px'}); + + cmp.state = 'off'; + fixture.detectChanges(); + + var step3 = uiDriver.log.shift(); + var step3Styles = retrieveFinalAnimationStepStyles(step3['keyframeLookup']); + + expect(step3Styles).toEqual({opacity: '0', fontSize: AUTO_STYLE}); + }); + + it('should fire the onStart callback when the animation starts', () => { + const fixture = TestBed.createComponent(AnimationCmp); + const cmp = fixture.componentInstance; + + var capturedEvent: AnimationTransitionEvent = null; + cmp.stateStartFn = event => { capturedEvent = event; }; + + cmp.state = 'on'; + + expect(capturedEvent).toBe(null); + + fixture.detectChanges(); + + expect(capturedEvent instanceof AnimationTransitionEvent).toBe(true); + + expect(capturedEvent.toState).toBe('on'); + }); + + it('should fire the onDone callback when the animation ends', () => { + const fixture = TestBed.createComponent(AnimationCmp); + const cmp = fixture.componentInstance; + + var capturedEvent: AnimationTransitionEvent = null; + cmp.stateDoneFn = event => { capturedEvent = event; }; + + cmp.state = 'off'; + + expect(capturedEvent).toBe(null); + + fixture.detectChanges(); + + expect(capturedEvent).toBe(null); + + const step = uiDriver.log.shift(); + step['player'].finish(); + + expect(capturedEvent instanceof AnimationTransitionEvent).toBe(true); + + expect(capturedEvent.toState).toBe('off'); + }); + + it('should handle multiple animations on the same element that contain refs to .start and .done callbacks', + () => { + const fixture = TestBed.createComponent(MultiAnimationCmp); + const cmp = fixture.componentInstance; + + let log: {[triggerName: string]: AnimationTransitionEvent[]} = {}; + cmp.callback = (triggerName: string, event: AnimationTransitionEvent) => { + log[triggerName] = log[triggerName] || []; + log[triggerName].push(event); + }; + + cmp.oneTriggerState = 'a'; + cmp.twoTriggerState = 'c'; + fixture.detectChanges(); + + // clear any animation logs that were collected when + // the component was rendered (void => *) + log = {}; + + cmp.oneTriggerState = 'b'; + cmp.twoTriggerState = 'd'; + fixture.detectChanges(); + + uiDriver.log.shift()['player'].finish(); + const [triggerOneStart, triggerOneDone] = log['one']; + expect(triggerOneStart) + .toEqual(new AnimationTransitionEvent( + {fromState: 'a', toState: 'b', totalTime: 500, phaseName: 'start'})); + + expect(triggerOneDone) + .toEqual(new AnimationTransitionEvent( + {fromState: 'a', toState: 'b', totalTime: 500, phaseName: 'done'})); + + uiDriver.log.shift()['player'].finish(); + const [triggerTwoStart, triggerTwoDone] = log['two']; + expect(triggerTwoStart) + .toEqual(new AnimationTransitionEvent( + {fromState: 'c', toState: 'd', totalTime: 1000, phaseName: 'start'})); + + expect(triggerTwoDone) + .toEqual(new AnimationTransitionEvent( + {fromState: 'c', toState: 'd', totalTime: 1000, phaseName: 'done'})); + }); + + it('should handle .start and .done callbacks for mutliple elements that contain animations that are fired at the same time', + () => { + function logFactory( + log: {[phaseName: string]: AnimationTransitionEvent}, + phaseName: string): (event: AnimationTransitionEvent) => any { + return (event: AnimationTransitionEvent) => { log[phaseName] = event; }; + } + + const f1 = TestBed.createComponent(AnimationCmp); + const f2 = TestBed.createComponent(AnimationCmp); + const cmp1 = f1.componentInstance; + const cmp2 = f2.componentInstance; + + var cmp1Log: {[phaseName: string]: AnimationTransitionEvent} = {}; + var cmp2Log: {[phaseName: string]: AnimationTransitionEvent} = {}; + + cmp1.stateStartFn = logFactory(cmp1Log, 'start'); + cmp1.stateDoneFn = logFactory(cmp1Log, 'done'); + cmp2.stateStartFn = logFactory(cmp2Log, 'start'); + cmp2.stateDoneFn = logFactory(cmp2Log, 'done'); + + cmp1.state = 'off'; + cmp2.state = 'on'; + f1.detectChanges(); + f2.detectChanges(); + + uiDriver.log.shift()['player'].finish(); + + expect(cmp1Log['start']) + .toEqual(new AnimationTransitionEvent( + {fromState: 'void', toState: 'off', totalTime: 500, phaseName: 'start'})); + + expect(cmp1Log['done']) + .toEqual(new AnimationTransitionEvent( + {fromState: 'void', toState: 'off', totalTime: 500, phaseName: 'done'})); + + // the * => on transition has two steps + uiDriver.log.shift()['player'].finish(); + uiDriver.log.shift()['player'].finish(); + + expect(cmp2Log['start']) + .toEqual(new AnimationTransitionEvent( + {fromState: 'void', toState: 'on', totalTime: 1000, phaseName: 'start'})); + + expect(cmp2Log['done']) + .toEqual(new AnimationTransitionEvent( + {fromState: 'void', toState: 'on', totalTime: 1000, phaseName: 'done'})); + }); + + it('should destroy the player when the animation is complete', () => { + const fixture = TestBed.createComponent(AnimationCmp); + const cmp = fixture.componentInstance; + + cmp.state = 'off'; + fixture.detectChanges(); + + var player = uiDriver.log.shift()['player']; + expect(player.log.indexOf('destroy') >= 0).toBe(false); + + cmp.state = 'on'; + fixture.detectChanges(); + + expect(player.log.indexOf('destroy') >= 0).toBe(true); + }); + }); +} + + +@Component({ + selector: 'my-comp', + template: ` +
...
+ `, + animations: [trigger( + 'myTrigger', + [ + state('void, off', style({opacity: '0'})), + state('on', style({opacity: '1', fontSize: '50px'})), + transition('* => on', [animate(500, style({fontSize: '20px'})), animate(500)]), + transition('* => off', [animate(500)]) + ])] +}) +class AnimationCmp { + state = 'off'; + stateStartFn = (event: AnimationTransitionEvent): any => {}; + stateDoneFn = (event: AnimationTransitionEvent): any => {}; +} + +@Component({ + selector: 'my-multi-comp', + template: ` +
...
+
...
+ `, + animations: [ + trigger( + 'one', + [ + state('a', style({width: '0px'})), state('b', style({width: '100px'})), + transition('a => b', animate(500)) + ]), + trigger( + 'two', + [ + state('c', style({height: '0px'})), state('d', style({height: '100px'})), + transition('c => d', animate(1000)) + ]) + ] +}) +class MultiAnimationCmp { + oneTriggerState: string; + twoTriggerState: string; + callback = (triggerName: string, event: AnimationTransitionEvent): any => {}; +} diff --git a/modules/playground/src/web_workers/animations/background_index.ts b/modules/playground/src/web_workers/animations/background_index.ts new file mode 100644 index 00000000000000..7f541be763492f --- /dev/null +++ b/modules/playground/src/web_workers/animations/background_index.ts @@ -0,0 +1,21 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {NgModule} from '@angular/core'; +import {WorkerAppModule} from '@angular/platform-webworker'; +import {platformWorkerAppDynamic} from '@angular/platform-webworker-dynamic'; + +import {AnimationCmp} from './index_common'; + +@NgModule({imports: [WorkerAppModule], bootstrap: [AnimationCmp], declarations: [AnimationCmp]}) +class ExampleModule { +} + +export function main() { + platformWorkerAppDynamic().bootstrapModule(ExampleModule); +} diff --git a/modules/playground/src/web_workers/animations/index.html b/modules/playground/src/web_workers/animations/index.html new file mode 100644 index 00000000000000..4a9181e6a8891c --- /dev/null +++ b/modules/playground/src/web_workers/animations/index.html @@ -0,0 +1,13 @@ + + + WebWorker Input Tests + + + + Loading... + + + + + diff --git a/modules/playground/src/web_workers/animations/index.ts b/modules/playground/src/web_workers/animations/index.ts new file mode 100644 index 00000000000000..b45021b6fc1eb8 --- /dev/null +++ b/modules/playground/src/web_workers/animations/index.ts @@ -0,0 +1,13 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {bootstrapWorkerUi} from '@angular/platform-webworker'; + +export function main() { + bootstrapWorkerUi('loader.js'); +} diff --git a/modules/playground/src/web_workers/animations/index_common.ts b/modules/playground/src/web_workers/animations/index_common.ts new file mode 100644 index 00000000000000..0c4e0796c59058 --- /dev/null +++ b/modules/playground/src/web_workers/animations/index_common.ts @@ -0,0 +1,43 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Component, animate, state, style, transition, trigger} from '@angular/core'; + +@Component({ + selector: 'animation-app', + styles: [` + .box { + box-sizing:border-box; + border:10px solid black; + text-align:center; + overflow:hidden; + background:red; + color:white; + font-size:100px; + line-height:200px; + } + `], + animations: [trigger( + 'animate', + [ + state('off', style({width: '0px'})), state('on', style({width: '200px'})), + transition('off <=> on', animate(500)) + ])], + template: ` + + +
+ ... +
+ ` +}) +export class AnimationCmp { + animate = false; +} diff --git a/modules/playground/src/web_workers/animations/loader.js b/modules/playground/src/web_workers/animations/loader.js new file mode 100644 index 00000000000000..993c4119693702 --- /dev/null +++ b/modules/playground/src/web_workers/animations/loader.js @@ -0,0 +1,43 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +importScripts( + '../../../vendor/core.js', '../../../vendor/zone.js', + '../../../vendor/long-stack-trace-zone.js', '../../../vendor/system.src.js', + '../../../vendor/Reflect.js'); + + +System.config({ + baseURL: '/all', + + map: {'rxjs': '/all/playground/vendor/rxjs'}, + + packages: { + '@angular/core': {main: 'index.js', defaultExtension: 'js'}, + '@angular/compiler': {main: 'index.js', defaultExtension: 'js'}, + '@angular/common': {main: 'index.js', defaultExtension: 'js'}, + '@angular/platform-browser': {main: 'index.js', defaultExtension: 'js'}, + '@angular/platform-browser-dynamic': {main: 'index.js', defaultExtension: 'js'}, + '@angular/platform-webworker': {main: 'index.js', defaultExtension: 'js'}, + '@angular/platform-webworker-dynamic': {main: 'index.js', defaultExtension: 'js'}, + 'rxjs': {defaultExtension: 'js'}, + }, + + defaultJSExtensions: true +}); + +System.import('playground/src/web_workers/animations/background_index') + .then( + function(m) { + try { + m.main(); + } catch (e) { + console.error(e); + } + }, + function(error) { console.error('error loading background', error); });