@google/model-viewer
Version:
Easily display interactive 3D models on the web and in AR!
219 lines (177 loc) • 6.16 kB
text/typescript
/*
* Copyright 2018 Google Inc. 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 {HAS_FULLSCREEN_API, HAS_WEBXR_DEVICE_API, HAS_WEBXR_HIT_TEST_API, IS_WEBXR_AR_CANDIDATE} from './constants.js';
import Timer from './utilities/timer';
export {
Timer,
};
export type Constructor<T = object> = {
new (...args: any[]): T,
prototype: T
};
export const deserializeUrl = (url: string|null): string|null =>
(url != null && url !== 'null') ? toFullUrl(url) : null;
export const assertIsArCandidate = () => {
if (IS_WEBXR_AR_CANDIDATE) {
return;
}
const missingApis = [];
if (!HAS_FULLSCREEN_API) {
missingApis.push('Fullscreen API');
}
if (!HAS_WEBXR_DEVICE_API) {
missingApis.push('WebXR Device API');
}
if (!HAS_WEBXR_HIT_TEST_API) {
missingApis.push('WebXR Hit Test API');
}
throw new Error(
`The following APIs are required for AR, but are missing in this browser: ${
missingApis.join(', ')}`);
};
/**
* Converts a partial URL string to a fully qualified URL string.
*
* @param {String} url
* @return {String}
*/
export const toFullUrl = (partialUrl: string): string => {
const url = new URL(partialUrl, window.location.toString());
return url.toString();
};
/**
* Returns a throttled version of a given function that is only invoked at most
* once within a given threshold of time in milliseconds.
*
* The throttled version of the function has a "flush" property that resets the
* threshold for cases when immediate invokation is desired.
*/
export const throttle = (fn: (...args: Array<any>) => any, ms: number) => {
let timer: number|null = null;
const throttled = (...args: Array<any>) => {
if (timer != null) {
return;
}
fn(...args);
timer = self.setTimeout(() => timer = null, ms);
};
throttled.flush = () => {
if (timer != null) {
self.clearTimeout(timer);
timer = null;
}
};
return throttled;
};
export const debounce = (fn: (...args: Array<any>) => any, ms: number) => {
let timer: number|null = null;
return (...args: Array<any>) => {
if (timer != null) {
self.clearTimeout(timer);
}
timer = self.setTimeout(() => {
timer = null;
fn(...args);
}, ms);
};
};
/**
* @param {Number} edge
* @param {Number} value
* @return {Number} 0 if value is less than edge, otherwise 1
*/
export const step = (edge: number, value: number): number => {
return value < edge ? 0 : 1;
};
/**
* @param {Number} value
* @param {Number} lowerLimit
* @param {Number} upperLimit
* @return {Number} value clamped within lowerLimit..upperLimit
*/
export const clamp =
(value: number, lowerLimit: number, upperLimit: number): number => Math.max(
lowerLimit === -Infinity ? value : lowerLimit,
Math.min(upperLimit === Infinity ? value : upperLimit, value));
// The DPR we use for a "capped" scenario (see resolveDpr below):
export const CAPPED_DEVICE_PIXEL_RATIO = 1;
/**
* This helper analyzes the layout of the current page to decide if we should
* use the natural device pixel ratio, or a capped value.
*
* We cap DPR if there is no meta viewport (suggesting that user is not
* consciously specifying how to scale the viewport relative to the device
* screen size).
*
* The rationale is that this condition typically leads to a pathological
* outcome on mobile devices. When the window dimensions are scaled up on a
* device with a high DPR, we create a canvas that is much larger than
* appropriate to accomodate for the pixel density if we naively use the
* reported DPR.
*
* This value needs to be measured in real time, as device pixel ratio can
* change over time (e.g., when a user zooms the page). Also, in some cases
* (such as Firefox on Android), the window's innerWidth is initially reported
* as the same as the screen's availWidth but changes later.
*
* A user who specifies a meta viewport, thereby consciously creating scaling
* conditions where <model-viewer> is slow, will be encouraged to live their
* best life.
*/
export const resolveDpr: () => number = (() => {
// If true, implies that the user is conscious of the viewport scaling
// relative to the device screen size.
const HAS_META_VIEWPORT_TAG = (() => {
const metas = document.head != null ?
Array.from(document.head.querySelectorAll('meta')) :
[];
for (const meta of metas) {
if (meta.name === 'viewport') {
return true;
}
}
return false;
})();
if (!HAS_META_VIEWPORT_TAG) {
console.warn(
'No <meta name="viewport"> detected; <model-viewer> will cap pixel density at 1.');
}
return () => HAS_META_VIEWPORT_TAG ? window.devicePixelRatio :
CAPPED_DEVICE_PIXEL_RATIO;
})();
/**
* Returns the first key in a Map in iteration order.
*
* NOTE(cdata): This is necessary because IE11 does not implement iterator
* methods of Map, and polymer-build does not polyfill these methods for
* compatibility and performance reasons. This helper proposes that it is
* a reasonable compromise to sacrifice a very small amount of runtime
* performance in IE11 for the sake of code clarity.
*/
export const getFirstMapKey = <T = any, U = any>(map: Map<T, U>): T|null => {
if (map.keys != null) {
return map.keys().next().value || null;
}
let firstKey: T|null = null;
try {
map.forEach((_value: U, key: T, _map: Map<T, U>) => {
firstKey = key;
// Stop iterating the Map with forEach:
throw new Error();
});
} catch (_error) {
}
return firstKey;
};