@google/model-viewer
Version:
Easily display interactive 3D models on the web and in AR!
188 lines (161 loc) • 6.22 kB
text/typescript
/* @license
* Copyright 2019 Google LLC. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {ASTWalker, ExpressionNode, FunctionNode} from './parsers';
interface AnyObserver {
observe(): void;
disconnect(): void;
}
const $instances = Symbol('instances');
const $activateListener = Symbol('activateListener');
const $deactivateListener = Symbol('deactivateListener');
const $notifyInstances = Symbol('notifyInstances');
const $notify = Symbol('notify');
const $scrollCallback = Symbol('callback');
type ScrollObserverCallback = () => void;
/**
* This internal helper is intended to work as a reference-counting manager of
* scroll event listeners. Only one scroll listener is ever registered for all
* instances of the class, and when the last ScrollObserver "disconnects", that
* event listener is removed. This spares us from thrashing
* the {add,remove}EventListener API (the binding cost of these methods has been
* known to show up in performance anlyses) as well as potential memory leaks.
*/
class ScrollObserver {
private static[$notifyInstances]() {
for (const instance of ScrollObserver[$instances]) {
instance[$notify]();
}
}
private static[$instances]: Set<ScrollObserver> = new Set();
private static[$activateListener]() {
window.addEventListener('scroll', this[$notifyInstances], {passive: true});
}
private static[$deactivateListener]() {
window.removeEventListener('scroll', this[$notifyInstances]);
}
private[$scrollCallback]: ScrollObserverCallback;
constructor(callback: ScrollObserverCallback) {
this[$scrollCallback] = callback;
}
/**
* Listen for scroll events. The configured callback (passed to the
* constructor) will be invoked for subsequent global scroll events.
*/
observe() {
if (ScrollObserver[$instances].size === 0) {
ScrollObserver[$activateListener]();
}
ScrollObserver[$instances].add(this);
}
/**
* Stop listening for scroll events.
*/
disconnect() {
ScrollObserver[$instances].delete(this);
if (ScrollObserver[$instances].size === 0) {
ScrollObserver[$deactivateListener]();
}
}
private[$notify]() {
this[$scrollCallback]();
};
}
export type EnvironmentState = 'window-scroll';
export type StyleEffectorCallback = (record: EnvironmentChangeRecord) => void;
export interface EnvironmentChangeRecord {
relatedState: EnvironmentState;
}
type EnvironmentDependencies = {
[key in EnvironmentState]?: AnyObserver
};
const $computeStyleCallback = Symbol('computeStyleCallback');
const $astWalker = Symbol('astWalker');
const $dependencies = Symbol('dependencies');
const $onScroll = Symbol('onScroll');
/**
* The StyleEffector is configured with a callback that will be invoked at the
* optimal time that some array of CSS expression ASTs ought to be evaluated.
*
* For example, our CSS-like expression syntax supports usage of the env()
* function to incorporate the current top-level scroll position into a CSS
* expression: env(window-scroll-y).
*
* This "environment variable" will change dynamically as the user scrolls the
* page. If an AST contains such a usage of env(), we would have to evaluate the
* AST on every frame in order to be sure that the computed style stays up to
* date.
*
* The StyleEffector spares us from evaluating the expressions on every frame by
* correlating specific parts of an AST with observers of the external effects
* that they refer to (if any). So, if the AST contains env(window-scroll-y),
* the StyleEffector manages the lifetime of a global scroll event listener and
* notifies the user at the optimal time to evaluate the computed style.
*/
export class StyleEffector {
protected[$dependencies]: EnvironmentDependencies = {};
protected[$computeStyleCallback]: StyleEffectorCallback;
protected[$astWalker] = new ASTWalker<FunctionNode>(['function']);
constructor(callback: StyleEffectorCallback) {
this[$computeStyleCallback] = callback;
}
/**
* Sets the expressions that govern when the StyleEffector callback will be
* invoked.
*/
observeEffectsFor(ast: Array<ExpressionNode>) {
const newDependencies: EnvironmentDependencies = {};
const oldDependencies = this[$dependencies];
this[$astWalker].walk(ast, functionNode => {
const {name} = functionNode;
const firstArgument = functionNode.arguments[0];
const firstTerm = firstArgument.terms[0];
if (name.value !== 'env' || firstTerm == null ||
firstTerm.type !== 'ident') {
return;
}
switch (firstTerm.value) {
case 'window-scroll-y':
if (newDependencies['window-scroll'] == null) {
const observer = 'window-scroll' in oldDependencies ?
oldDependencies['window-scroll'] :
new ScrollObserver(this[$onScroll]);
observer!.observe();
delete oldDependencies['window-scroll'];
newDependencies['window-scroll'] = observer;
}
break;
}
});
for (const environmentState in oldDependencies) {
const observer = oldDependencies[environmentState as EnvironmentState]!;
observer.disconnect();
}
this[$dependencies] = newDependencies;
}
/**
* Disposes of the StyleEffector by disconnecting all observers of external
* effects.
*/
dispose() {
for (const environmentState in this[$dependencies]) {
const observer =
this[$dependencies][environmentState as EnvironmentState]!;
observer.disconnect();
}
}
protected[$onScroll] = () => {
this[$computeStyleCallback]({relatedState: 'window-scroll'});
};
}