1/* 2 * Copyright 2024 Google LLC 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17// golden recordings from motion tests produces frames at exactly 16ms each 18const frameDuration = 0.016; 19 20/** Maps frame time to frame number and vice versa. */ 21export class Timeline { 22 /** Duration , represented as a double-precision floating-point value in seconds. */ 23 readonly duration: number; 24 /** 25 * The labels of all 26 */ 27 readonly frameLabels: ReadonlyArray<string>; 28 readonly frameTimes: ReadonlyArray<number>; 29 30 constructor(frameIds: ReadonlyArray<string | number>) { 31 this.frameLabels = frameIds.map((it) => 32 typeof it === 'string' ? it : `${it}ms`, 33 ); 34 35 let lastFrameTime = 0; 36 37 const frameTimes = [lastFrameTime]; 38 39 for (let i = 0; i < frameIds.length - 1; i++) { 40 const thisFrame = frameIds[i]; 41 const nextFrame = frameIds[i + 1]; 42 43 if (typeof thisFrame === 'number' && typeof nextFrame === 'number') { 44 lastFrameTime += (nextFrame - thisFrame) / 1000; 45 } else { 46 lastFrameTime += frameDuration; 47 } 48 frameTimes.push(lastFrameTime); 49 } 50 this.frameTimes = frameTimes; 51 52 this.duration = frameTimes[frameTimes.length - 1] + frameDuration; 53 } 54 55 /** Number of frames in this timeline. */ 56 get frameCount(): number { 57 return this.frameLabels.length; 58 } 59 60 /** 61 * Compute the frame number associated the time in seconds. 62 * 63 * Each frame starts at the frame time, and just before the next frame's start time, or duration 64 * if it's the last frame. 65 */ 66 timeToFrame(timeSeconds: number): number { 67 if (timeSeconds < 0) return Number.NEGATIVE_INFINITY; 68 if (timeSeconds >= this.duration) return Number.POSITIVE_INFINITY; 69 70 const frameTimes = this.frameTimes; 71 72 let start = 0; 73 let end = frameTimes.length - 1; 74 75 while (start <= end) { 76 let mid = (start + end) >> 1; 77 78 if (frameTimes[mid] === timeSeconds) { 79 return mid; 80 } 81 82 if (timeSeconds < frameTimes[mid]) { 83 end = mid - 1; 84 } else { 85 start = mid + 1; 86 } 87 } 88 89 return end; 90 } 91 92 /** 93 * Start time of the frame with `frameNumber`. 94 */ 95 frameToTime(frameNumber: number): number { 96 frameNumber = Math.floor(frameNumber); 97 if (frameNumber < 0) return Number.NEGATIVE_INFINITY; 98 const frameTimes = this.frameTimes; 99 if (frameNumber >= frameTimes.length) return Number.POSITIVE_INFINITY; 100 101 return frameTimes[frameNumber]; 102 } 103 104 clampTimeToFrame(timeSeconds: number): number { 105 return this.frameToTime(this.timeToFrame(timeSeconds)); 106 } 107} 108