@google/model-viewer
Version:
Easily display interactive 3D models on the web and in AR!
152 lines (129 loc) • 5.48 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 {clamp} from '../utilities.js';
interface OngoingActivity {
progress: number;
completed: boolean;
}
/**
* An Activity is represented by a callback that accepts values from 0 to 1,
* where 1 represents the completion of the activity. The callback returns the
* actual progress as it is stored by the ProgressTracker (which may be clamped,
* and can never be lower than its previous value).
*/
export type Activity = (progress: number) => number;
/**
* A progress event contains the total progress of all ongoing activities in the
* ProgressTracker. The total progress is a heuristic, but has some useful
* properties: for a single activity, it equals the input progress; for multiple
* activities that progress in lockstep, it will also equal each input progress.
* When more activities overlap as time goes on, total progress will tend to
* decelerate.
*/
export interface ProgressDetails {
totalProgress: number;
reason: string;
}
/**
* ProgressTracker is an event emitter that helps to track the ongoing progress
* of many simultaneous actions.
*
* ProgressTracker reports progress activity in the form of a progress event.
* The event.detail.totalProgress value indicates the elapsed progress of all
* activities being tracked by the ProgressTracker.
*
* The value of totalProgress is a number that progresses from 0 to 1. The
* ProgressTracker allows for the lazy accumulation of tracked actions, so the
* total progress represents an abstract, non-absolute progress towards the
* completion of all currently tracked events.
*
* When all currently tracked activities are finished, the ProgressTracker
* emits one final progress event and then resets the list of its currently
* tracked activities. This means that from an observer's perspective,
* ongoing activities will accumulate and collectively contribute to the notion
* of total progress until all currently tracked ongoing activities have
* completed.
*/
export class ProgressTracker extends EventTarget {
private ongoingActivities: Set<OngoingActivity> = new Set();
private totalProgress = 0;
/**
* The total number of activities currently being tracked.
*/
get ongoingActivityCount(): number {
return this.ongoingActivities.size;
}
/**
* Registers a new activity to be tracked by the progress tracker. The method
* returns a special callback that should be invoked whenever new progress is
* ready to be reported. The progress should be reported as a value between 0
* and 1, where 0 would represent the beginning of the action and 1 would
* represent its completion.
*
* There is no built-in notion of a time-out for ongoing activities, so once
* an ongoing activity is begun, it is up to the consumer of this API to
* update the progress until that activity is no longer ongoing.
*
* Progress is only allowed to move forward for any given activity. If a lower
* progress is reported than the previously reported progress, it will be
* ignored.
*/
beginActivity(reason: string): Activity {
const activity: OngoingActivity = {progress: 0, completed: false};
this.ongoingActivities.add(activity);
if (this.ongoingActivityCount === 1) {
// Announce the first progress event (which should always be 0 / 1
// total progress):
this.announceTotalProgress(activity, 0, reason);
}
return (progress: number): number => {
let nextProgress: number;
nextProgress = Math.max(clamp(progress, 0, 1), activity.progress);
if (nextProgress !== activity.progress) {
this.announceTotalProgress(activity, nextProgress, reason);
}
return activity.progress;
};
}
private announceTotalProgress(
updatedActivity: OngoingActivity, nextProgress: number, reason: string) {
let progressLeft = 0;
let completedActivities = 0;
if (nextProgress == 1.0)
updatedActivity.completed = true;
for (const activity of this.ongoingActivities) {
const {progress} = activity;
progressLeft += 1.0 - progress;
if (activity.completed) {
completedActivities++;
}
}
const lastProgress = updatedActivity.progress;
updatedActivity.progress = nextProgress;
// Advance the total progress by the fraction of total remaining progress
// due to this activity.
this.totalProgress += (nextProgress - lastProgress) *
(1.0 - this.totalProgress) / progressLeft;
const totalProgress = completedActivities === this.ongoingActivityCount ?
1.0 :
this.totalProgress;
this.dispatchEvent(new CustomEvent<ProgressDetails>(
'progress', {detail: {totalProgress, reason }}));
if (completedActivities === this.ongoingActivityCount) {
this.totalProgress = 0.0;
this.ongoingActivities.clear();
}
}
}