@google/model-viewer
Version:
Easily display interactive 3D models on the web and in AR!
380 lines • 18.9 kB
JavaScript
/* @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.
*/
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { property } from 'lit-element';
import { $announceModelVisibility, $ariaLabel, $getModelIsVisible, $hasTransitioned, $isElementInViewport, $progressTracker, $scene, $sceneIsReady, $shouldAttemptPreload, $updateSource, $userInputElement, toVector3D } from '../model-viewer-base.js';
import { $loader, CachingGLTFLoader } from '../three-components/CachingGLTFLoader.js';
import { Renderer } from '../three-components/Renderer.js';
import { throttle } from '../utilities.js';
import { LoadingStatusAnnouncer } from './loading/status-announcer.js';
export const POSTER_TRANSITION_TIME = 300;
export const PROGRESS_BAR_UPDATE_THRESHOLD = 100;
const PROGRESS_MASK_BASE_OPACITY = 0.2;
const DEFAULT_DRACO_DECODER_LOCATION = 'https://www.gstatic.com/draco/versioned/decoders/1.3.6/';
const DEFAULT_KTX2_TRANSCODER_LOCATION = 'https://www.gstatic.com/basis-universal/versioned/2020-12-21-ef70ddd/';
const SPACE_KEY = 32;
const ENTER_KEY = 13;
const RevealStrategy = {
AUTO: 'auto',
INTERACTION: 'interaction',
MANUAL: 'manual'
};
const LoadingStrategy = {
AUTO: 'auto',
LAZY: 'lazy',
EAGER: 'eager'
};
const PosterDismissalSource = {
INTERACTION: 'interaction'
};
const loadingStatusAnnouncer = new LoadingStatusAnnouncer();
export const $defaultProgressBarElement = Symbol('defaultProgressBarElement');
export const $defaultProgressMaskElement = Symbol('defaultProgressMaskElement');
export const $posterContainerElement = Symbol('posterContainerElement');
export const $defaultPosterElement = Symbol('defaultPosterElement');
const $posterDismissalSource = Symbol('posterDismissalSource');
const $hidePoster = Symbol('hidePoster');
const $modelIsRevealed = Symbol('modelIsRevealed');
const $updateProgressBar = Symbol('updateProgressBar');
const $lastReportedProgress = Symbol('lastReportedProgress');
const $transitioned = Symbol('transitioned');
const $ariaLabelCallToAction = Symbol('ariaLabelCallToAction');
const $onClick = Symbol('onClick');
const $onKeydown = Symbol('onKeydown');
const $onProgress = Symbol('onProgress');
/**
* LoadingMixin implements features related to lazy loading, as well as
* presentation details related to the pre-load / pre-render presentation of a
* <model-viewer>
*
* This mixin implements support for models with DRACO-compressed meshes.
* The DRACO decoder will be loaded on-demand if a glTF that uses the DRACO mesh
* compression extension is encountered.
*
* By default, the DRACO decoder will be loaded from a Google CDN. It is
* possible to customize where the decoder is loaded from by defining a global
* configuration option for `<model-viewer>` like so:
*
* ```html
* <script>
* ModelViewerElement = self.ModelViewerElement || {};
* ModelViewerElement.dracoDecoderLocation =
* 'http://example.com/location/of/draco/decoder/files/';
* </script>
* ```
*
* Note that the above configuration strategy must be performed *before* the
* first `<model-viewer>` element is created in the browser. The configuration
* can be done anywhere, but the easiest way to ensure it is done at the right
* time is to do it in the `<head>` of the HTML document. This is the
* recommended way to set the location because it is most compatible with
* scenarios where the `<model-viewer>` library is lazily loaded.
*
* If you absolutely have to set the DRACO decoder location *after* the first
* `<model-viewer>` element is created, you can do it this way:
*
* ```html
* <script>
* const ModelViewerElement = customElements.get('model-viewer');
* ModelViewerElement.dracoDecoderLocation =
* 'http://example.com/location/of/draco/decoder/files/';
* </script>
* ```
*
* Note that the above configuration approach will not work until *after*
* `<model-viewer>` is defined in the browser. Also note that this configuration
* *must* be set *before* the first DRACO model is fully loaded.
*
* It is recommended that users who intend to take advantage of DRACO mesh
* compression consider whether or not it is acceptable for their use case to
* have code side-loaded from a Google CDN. If it is not acceptable, then the
* location must be customized before loading any DRACO models in order to cause
* the decoder to be loaded from an alternative, acceptable location.
*/
export const LoadingMixin = (ModelViewerElement) => {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
class LoadingModelViewerElement extends ModelViewerElement {
constructor(...args) {
super(...args);
/**
* A URL pointing to the image to use as a poster in scenarios where the
* <model-viewer> is not ready to reveal a rendered model to the viewer.
*/
this.poster = null;
/**
* An enumerable attribute describing under what conditions the
* <model-viewer> should reveal a model to the viewer.
*
* The default value is "auto". The only supported alternative values are
* "interaction" and "manual".
*/
this.reveal = RevealStrategy.AUTO;
/**
* An enumerable attribute describing under what conditions the
* <model-viewer> should preload a model.
*
* The default value is "auto". The only supported alternative values are
* "lazy" and "eager". Auto is equivalent to lazy, which loads the model
* when it is near the viewport for reveal = "auto", and when interacted
* with for reveal = "interaction". Eager loads the model immediately.
*/
this.loading = LoadingStrategy.AUTO;
this[_a] = false;
this[_b] = false;
this[_c] = 0;
this[_d] = null;
// TODO: Add this to the shadow root as part of this mixin's
// implementation:
this[_e] = this.shadowRoot.querySelector('.slot.poster');
this[_f] = this.shadowRoot.querySelector('#default-poster');
this[_g] = this.shadowRoot.querySelector('#default-progress-bar > .bar');
this[_h] = this.shadowRoot.querySelector('#default-progress-bar > .mask');
this[_j] = this[$defaultPosterElement].getAttribute('aria-label');
this[_k] = throttle((progress) => {
const parentNode = this[$defaultProgressBarElement].parentNode;
requestAnimationFrame(() => {
this[$defaultProgressMaskElement].style.opacity =
`${(1.0 - progress) * PROGRESS_MASK_BASE_OPACITY}`;
this[$defaultProgressBarElement].style.transform =
`scaleX(${progress})`;
if (progress === 0) {
// NOTE(cdata): We remove and re-append the progress bar in this
// condition so that the progress bar does not appear to
// transition backwards from the right when we reset to 0 (or
// otherwise <1) progress after having already reached 1 progress
// previously.
parentNode.removeChild(this[$defaultProgressBarElement]);
parentNode.appendChild(this[$defaultProgressBarElement]);
}
// NOTE(cdata): IE11 does not properly respect the second parameter
// of classList.toggle, which this implementation originally used.
// @see https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/11865865/
if (progress === 1.0) {
this[$defaultProgressBarElement].classList.add('hide');
}
else {
this[$defaultProgressBarElement].classList.remove('hide');
}
});
}, PROGRESS_BAR_UPDATE_THRESHOLD);
this[_l] = () => {
if (this.reveal === RevealStrategy.MANUAL) {
return;
}
this.dismissPoster();
};
this[_m] = (event) => {
if (this.reveal === RevealStrategy.MANUAL) {
return;
}
switch (event.keyCode) {
// NOTE(cdata): Links and buttons can typically be activated with
// both spacebar and enter to produce a synthetic click action
case SPACE_KEY:
case ENTER_KEY:
this.dismissPoster();
break;
default:
break;
}
};
this[_o] = (event) => {
const progress = event.detail.totalProgress;
this[$lastReportedProgress] =
Math.max(progress, this[$lastReportedProgress]);
if (progress === 1.0) {
this[$updateProgressBar].flush();
if (this[$sceneIsReady]() &&
(this[$posterDismissalSource] != null ||
this.reveal === RevealStrategy.AUTO)) {
this[$hidePoster]();
}
}
this[$updateProgressBar](progress);
this.dispatchEvent(new CustomEvent('progress', { detail: { totalProgress: progress } }));
};
const ModelViewerElement = self.ModelViewerElement || {};
const dracoDecoderLocation = ModelViewerElement.dracoDecoderLocation ||
DEFAULT_DRACO_DECODER_LOCATION;
CachingGLTFLoader.setDRACODecoderLocation(dracoDecoderLocation);
const ktx2TranscoderLocation = ModelViewerElement.ktx2TranscoderLocation ||
DEFAULT_KTX2_TRANSCODER_LOCATION;
CachingGLTFLoader.setKTX2TranscoderLocation(ktx2TranscoderLocation);
}
static set dracoDecoderLocation(value) {
CachingGLTFLoader.setDRACODecoderLocation(value);
}
static get dracoDecoderLocation() {
return CachingGLTFLoader.getDRACODecoderLocation();
}
static set ktx2TranscoderLocation(value) {
CachingGLTFLoader.setKTX2TranscoderLocation(value);
}
static get ktx2TranscoderLocation() {
return CachingGLTFLoader.getKTX2TranscoderLocation();
}
/**
* If provided, the callback will be passed each resource URL before a
* request is sent. The callback may return the original URL, or a new URL
* to override loading behavior. This behavior can be used to load assets
* from .ZIP files, drag-and-drop APIs, and Data URIs.
*/
static mapURLs(callback) {
Renderer.singleton.loader[$loader].manager.setURLModifier(callback);
}
/**
* Dismisses the poster, causing the model to load and render if
* necessary. This is currently effectively the same as interacting with
* the poster via user input.
*/
dismissPoster() {
if (this[$sceneIsReady]()) {
this[$hidePoster]();
}
else {
this[$posterDismissalSource] = PosterDismissalSource.INTERACTION;
this[$updateSource]();
}
}
/**
* Displays the poster, hiding the 3D model. If this is called after the 3D
* model has been revealed, then it will behave as though
* reveal='interaction', being dismissed either by a user click or a call to
* dismissPoster().
*/
showPoster() {
const posterContainerElement = this[$posterContainerElement];
const defaultPosterElement = this[$defaultPosterElement];
defaultPosterElement.removeAttribute('tabindex');
defaultPosterElement.removeAttribute('aria-hidden');
posterContainerElement.classList.add('show');
const oldVisibility = this.modelIsVisible;
this[$modelIsRevealed] = false;
this[$announceModelVisibility](oldVisibility);
this[$transitioned] = false;
}
/**
* Returns the model's bounding box dimensions in meters, independent of
* turntable rotation.
*/
getDimensions() {
return toVector3D(this[$scene].size);
}
connectedCallback() {
super.connectedCallback();
// Fired when a user first clicks the model element. Used to
// change the visibility of a poster image, or start loading
// a model.
this[$posterContainerElement].addEventListener('click', this[$onClick]);
this[$posterContainerElement].addEventListener('keydown', this[$onKeydown]);
this[$progressTracker].addEventListener('progress', this[$onProgress]);
loadingStatusAnnouncer.registerInstance(this);
}
disconnectedCallback() {
super.disconnectedCallback();
this[$posterContainerElement].removeEventListener('click', this[$onClick]);
this[$posterContainerElement].removeEventListener('keydown', this[$onKeydown]);
this[$progressTracker].removeEventListener('progress', this[$onProgress]);
loadingStatusAnnouncer.unregisterInstance(this);
}
async updated(changedProperties) {
super.updated(changedProperties);
if (changedProperties.has('poster') && this.poster != null) {
this[$defaultPosterElement].style.backgroundImage =
`url(${this.poster})`;
}
if (changedProperties.has('alt')) {
this[$defaultPosterElement].setAttribute('aria-label', `${this[$ariaLabel]}. ${this[$ariaLabelCallToAction]}`);
}
if (changedProperties.has('reveal') || changedProperties.has('loaded')) {
if (!this[$sceneIsReady]()) {
this[$updateSource]();
}
}
}
[(_a = $modelIsRevealed, _b = $transitioned, _c = $lastReportedProgress, _d = $posterDismissalSource, _e = $posterContainerElement, _f = $defaultPosterElement, _g = $defaultProgressBarElement, _h = $defaultProgressMaskElement, _j = $ariaLabelCallToAction, _k = $updateProgressBar, _l = $onClick, _m = $onKeydown, _o = $onProgress, $shouldAttemptPreload)]() {
return !!this.src &&
(this[$posterDismissalSource] != null ||
this.loading === LoadingStrategy.EAGER ||
(this.reveal === RevealStrategy.AUTO && this[$isElementInViewport]));
}
[$sceneIsReady]() {
const { src } = this;
return !!src && super[$sceneIsReady]() &&
this[$lastReportedProgress] === 1.0;
}
[$hidePoster]() {
this[$posterDismissalSource] = null;
const posterContainerElement = this[$posterContainerElement];
const defaultPosterElement = this[$defaultPosterElement];
if (posterContainerElement.classList.contains('show')) {
posterContainerElement.classList.remove('show');
const oldVisibility = this.modelIsVisible;
this[$modelIsRevealed] = true;
this[$announceModelVisibility](oldVisibility);
// We might need to forward focus to our internal canvas, but that
// cannot happen until the poster has completely transitioned away
posterContainerElement.addEventListener('transitionend', () => {
requestAnimationFrame(() => {
this[$transitioned] = true;
const root = this.getRootNode();
// If the <model-viewer> is still focused, forward the focus to
// the canvas that has just been revealed
if (root &&
root.activeElement === this) {
this[$userInputElement].focus();
}
// Ensure that the poster is no longer focusable or visible to
// screen readers
defaultPosterElement.setAttribute('aria-hidden', 'true');
defaultPosterElement.tabIndex = -1;
this.dispatchEvent(new CustomEvent('poster-dismissed'));
});
}, { once: true });
}
}
[$getModelIsVisible]() {
return super[$getModelIsVisible]() && this[$modelIsRevealed];
}
[$hasTransitioned]() {
return super[$hasTransitioned]() && this[$transitioned];
}
async [$updateSource]() {
this[$lastReportedProgress] = 0;
if (this[$scene].currentGLTF == null || this.src == null ||
!this[$shouldAttemptPreload]()) {
// Don't show the poster when switching models.
this.showPoster();
}
await super[$updateSource]();
}
}
__decorate([
property({ type: String })
], LoadingModelViewerElement.prototype, "poster", void 0);
__decorate([
property({ type: String })
], LoadingModelViewerElement.prototype, "reveal", void 0);
__decorate([
property({ type: String })
], LoadingModelViewerElement.prototype, "loading", void 0);
return LoadingModelViewerElement;
};
//# sourceMappingURL=loading.js.map