@google/model-viewer
Version:
Easily display interactive 3D models on the web and in AR!
327 lines • 16.3 kB
JavaScript
/*
* Copyright 2019 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.
*/
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 { $ariaLabel, $canvas, $progressTracker, $updateSource } from '../model-viewer-base.js';
import { CachingGLTFLoader } from '../three-components/CachingGLTFLoader.js';
import { debounce, deserializeUrl, 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 ANNOUNCE_MODEL_VISIBILITY_DEBOUNCE_THRESHOLD = 0;
const SPACE_KEY = 32;
const ENTER_KEY = 13;
const RevealStrategy = {
AUTO: 'auto',
INTERACTION: 'interaction'
};
const PosterDismissalSource = {
INTERACTION: 'interaction'
};
const loader = new CachingGLTFLoader();
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 $announceModelVisibility = Symbol('announceModelVisibility');
const $modelIsReadyForReveal = Symbol('modelIsReadyForReveal');
const $shouldAttemptPreload = Symbol('shouldAttemptPreload');
const $shouldRevealModel = Symbol('shouldRevealModel');
const $showPoster = Symbol('showPoster');
const $hidePoster = Symbol('hidePoster');
const $modelIsVisible = Symbol('modelIsVisible');
const $preloadAttempted = Symbol('preloadAttempted');
const $sourceUpdated = Symbol('sourceUpdated');
const $updateLoadingAndVisibility = Symbol('updateLoadingAndVisibility');
const $updateProgressBar = Symbol('updateProgressBar');
const $lastReportedProgress = Symbol('lastReportedProgress');
const $ariaLabelCallToAction = Symbol('ariaLabelCallToAction');
const $clickHandler = Symbol('clickHandler');
const $keydownHandler = Symbol('keydownHandler');
const $progressHandler = Symbol('processHandler');
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>
*/
export const LoadingMixin = (ModelViewerElement) => {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
class LoadingModelViewerElement extends ModelViewerElement {
constructor() {
super(...arguments);
/**
* 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 value as
* of now is "interaction".
*/
this.reveal = RevealStrategy.AUTO;
/**
* If true, a configured model file will be aggressively loaded, even if
* the <model-viewer> is only configured to reveal upon interaction.
*/
this.preload = false;
this[_a] = false;
this[_b] = false;
this[_c] = false;
this[_d] = 0;
this[_e] = null;
// TODO: Add this to the shadow root as part of this mixin's
// implementation:
this[_f] = this.shadowRoot.querySelector('.slot.poster');
this[_g] = this.shadowRoot.querySelector('#default-poster');
this[_h] = this.shadowRoot.querySelector('#default-progress-bar > .bar');
this[_j] = this.shadowRoot.querySelector('#default-progress-bar > .mask');
this[_k] = this[$defaultPosterElement].getAttribute('aria-label');
this[_l] = () => this[$onClick]();
this[_m] = (event) => this[$onKeydown](event);
this[_o] = (event) => this[$onProgress](event);
this[_p] = debounce((visible) => {
this.dispatchEvent(new CustomEvent('model-visibility', { detail: { visible } }));
}, ANNOUNCE_MODEL_VISIBILITY_DEBOUNCE_THRESHOLD);
this[_q] = 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);
}
/**
* True if the model has finished loading. Note that a model can be
* loaded, but not yet be rendered.
*/
get loaded() {
const src = this.src;
return super.loaded ||
!!(src && CachingGLTFLoader.hasFinishedLoading(src));
}
/**
* True if the model is visible. Visibility implies that a model has
* finished loading, that it has rendered at least once and that the
* poster is no longer obscuring it.
*/
get modelIsVisible() {
return super.modelIsVisible && this[$modelIsVisible];
}
/**
* 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() {
this[$posterDismissalSource] = PosterDismissalSource.INTERACTION;
this.requestUpdate();
}
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[$clickHandler]);
this[$posterContainerElement].addEventListener('keydown', this[$keydownHandler]);
this[$progressTracker].addEventListener('progress', this[$progressHandler]);
loadingStatusAnnouncer.registerInstance(this);
}
disconnectedCallback() {
super.disconnectedCallback();
this[$posterContainerElement].removeEventListener('click', this[$clickHandler]);
this[$posterContainerElement].removeEventListener('keydown', this[$keydownHandler]);
this[$progressTracker].removeEventListener('progress', this[$progressHandler]);
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('src')) {
this[$posterDismissalSource] = null;
this[$preloadAttempted] = false;
this[$lastReportedProgress] = 0;
this[$sourceUpdated] = false;
}
if (changedProperties.has('alt')) {
this[$defaultPosterElement].setAttribute('aria-label', `${this[$ariaLabel]}. ${this[$ariaLabelCallToAction]}`);
}
this[$updateLoadingAndVisibility]();
}
[(_a = $modelIsVisible, _b = $preloadAttempted, _c = $sourceUpdated, _d = $lastReportedProgress, _e = $posterDismissalSource, _f = $posterContainerElement, _g = $defaultPosterElement, _h = $defaultProgressBarElement, _j = $defaultProgressMaskElement, _k = $ariaLabelCallToAction, _l = $clickHandler, _m = $keydownHandler, _o = $progressHandler, _p = $announceModelVisibility, _q = $updateProgressBar, $onClick)]() {
this[$posterDismissalSource] = PosterDismissalSource.INTERACTION;
this.requestUpdate();
}
[$onKeydown](event) {
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[$posterDismissalSource] = PosterDismissalSource.INTERACTION;
break;
default:
break;
}
this.requestUpdate();
}
[$onProgress](event) {
const progress = event.detail.totalProgress;
this.requestUpdate();
if (progress === 1.0) {
this[$updateProgressBar].flush();
}
this[$updateProgressBar](progress);
this.dispatchEvent(new CustomEvent('progress', { detail: { totalProgress: progress } }));
this[$lastReportedProgress] =
Math.max(progress, this[$lastReportedProgress]);
}
get [$modelIsReadyForReveal]() {
const { src } = this;
return !!src && CachingGLTFLoader.hasFinishedLoading(src) &&
this[$lastReportedProgress] === 1.0 && this[$shouldRevealModel];
}
get [$shouldRevealModel]() {
return this.reveal === RevealStrategy.AUTO ||
!!this[$posterDismissalSource];
}
get [$shouldAttemptPreload]() {
const { src } = this;
return !!src && !CachingGLTFLoader.hasFinishedLoading(src) &&
(this.preload || this[$shouldRevealModel]);
}
async [$updateLoadingAndVisibility]() {
if (this[$shouldAttemptPreload] && !this[$preloadAttempted]) {
this[$preloadAttempted] = true;
const updatePreloadProgress = this[$progressTracker].beginActivity();
try {
const src = this.src;
const detail = { url: src };
await loader.preload(src, updatePreloadProgress);
this.dispatchEvent(new CustomEvent('preload', { detail }));
}
catch (error) {
this.dispatchEvent(new CustomEvent('error', { detail: { type: 'preload', sourceError: error } }));
}
finally {
updatePreloadProgress(1.0);
this.requestUpdate();
}
}
if (this[$modelIsReadyForReveal]) {
if (!this[$sourceUpdated]) {
this[$updateSource]();
this[$hidePoster]();
}
}
else {
this[$showPoster]();
}
}
[$showPoster]() {
const posterContainerElement = this[$posterContainerElement];
const defaultPosterElement = this[$defaultPosterElement];
const posterContainerOpacity = parseFloat(self.getComputedStyle(posterContainerElement).opacity);
defaultPosterElement.tabIndex = 1;
defaultPosterElement.removeAttribute('aria-hidden');
posterContainerElement.classList.add('show');
if (posterContainerOpacity < 1.0) {
posterContainerElement.addEventListener('transitionend', () => {
this[$modelIsVisible] = false;
this[$announceModelVisibility](false);
}, { once: true });
}
}
[$hidePoster]() {
const posterContainerElement = this[$posterContainerElement];
const defaultPosterElement = this[$defaultPosterElement];
if (posterContainerElement.classList.contains('show')) {
posterContainerElement.classList.remove('show');
// We might need to forward focus to our internal canvas, but that
// cannot happen until the poster has completely transitioned away
posterContainerElement.addEventListener('transitionend', () => {
this[$announceModelVisibility](true);
requestAnimationFrame(() => {
this[$modelIsVisible] = 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[$canvas].focus();
}
// Ensure that the poster is no longer focusable or visible to
// screen readers
defaultPosterElement.setAttribute('aria-hidden', 'true');
defaultPosterElement.removeAttribute('tabindex');
});
}, { once: true });
}
}
async [$updateSource]() {
if (this[$modelIsReadyForReveal]) {
this[$sourceUpdated] = true;
await super[$updateSource]();
}
}
}
__decorate([
property({ converter: { fromAttribute: deserializeUrl } })
], LoadingModelViewerElement.prototype, "poster", void 0);
__decorate([
property({ type: String })
], LoadingModelViewerElement.prototype, "reveal", void 0);
__decorate([
property({ type: Boolean })
], LoadingModelViewerElement.prototype, "preload", void 0);
return LoadingModelViewerElement;
};
//# sourceMappingURL=loading.js.map