2023-08-17 09:36:59 -07:00
|
|
|
/**
|
|
|
|
|
* Copyright (c) Microsoft Corporation.
|
|
|
|
|
*
|
|
|
|
|
* 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.
|
|
|
|
|
*/
|
2021-01-07 16:15:34 -08:00
|
|
|
|
2024-07-31 12:12:06 +02:00
|
|
|
import { clsx, msToString, useMeasure } from '@web/uiUtils';
|
2023-12-22 10:17:35 -08:00
|
|
|
import { GlassPane } from '@web/shared/glassPane';
|
2021-01-07 16:15:34 -08:00
|
|
|
import * as React from 'react';
|
2022-04-06 13:57:14 -08:00
|
|
|
import type { Boundaries } from '../geometry';
|
2021-04-08 05:32:12 +08:00
|
|
|
import { FilmStrip } from './filmStrip';
|
2023-08-16 16:30:17 -07:00
|
|
|
import type { FilmStripPreviewPoint } from './filmStrip';
|
2023-05-19 15:18:18 -07:00
|
|
|
import type { ActionTraceEventInContext, MultiTraceModel } from './modelUtil';
|
2022-02-07 17:05:42 -08:00
|
|
|
import './timeline.css';
|
2023-06-16 17:56:11 +02:00
|
|
|
import type { Language } from '@isomorphic/locatorGenerators';
|
2023-08-16 16:30:17 -07:00
|
|
|
import type { Entry } from '@trace/har';
|
2024-07-18 16:39:40 +02:00
|
|
|
import type { ConsoleEntry } from './consoleTab';
|
2021-01-15 18:30:55 -08:00
|
|
|
|
|
|
|
|
type TimelineBar = {
|
2023-05-19 15:18:18 -07:00
|
|
|
action?: ActionTraceEventInContext;
|
2023-08-16 16:30:17 -07:00
|
|
|
resource?: Entry;
|
2024-07-18 16:39:40 +02:00
|
|
|
consoleMessage?: ConsoleEntry;
|
2021-01-15 18:30:55 -08:00
|
|
|
leftPosition: number;
|
|
|
|
|
rightPosition: number;
|
|
|
|
|
leftTime: number;
|
|
|
|
|
rightTime: number;
|
2023-08-29 22:23:08 -07:00
|
|
|
active: boolean;
|
2023-08-30 15:48:51 -07:00
|
|
|
error: boolean;
|
2021-01-15 18:30:55 -08:00
|
|
|
};
|
2021-01-07 16:15:34 -08:00
|
|
|
|
|
|
|
|
export const Timeline: React.FunctionComponent<{
|
2023-03-06 21:37:39 -08:00
|
|
|
model: MultiTraceModel | undefined,
|
2024-07-18 16:39:40 +02:00
|
|
|
consoleEntries: ConsoleEntry[] | undefined,
|
2023-08-16 16:30:17 -07:00
|
|
|
boundaries: Boundaries,
|
2023-08-29 22:23:08 -07:00
|
|
|
highlightedAction: ActionTraceEventInContext | undefined,
|
|
|
|
|
highlightedEntry: Entry | undefined,
|
2024-07-18 16:39:40 +02:00
|
|
|
highlightedConsoleEntry: ConsoleEntry | undefined,
|
2023-05-19 15:18:18 -07:00
|
|
|
onSelected: (action: ActionTraceEventInContext) => void,
|
2023-08-16 16:30:17 -07:00
|
|
|
selectedTime: Boundaries | undefined,
|
|
|
|
|
setSelectedTime: (time: Boundaries | undefined) => void,
|
2023-06-16 17:56:11 +02:00
|
|
|
sdkLanguage: Language,
|
2024-07-18 16:39:40 +02:00
|
|
|
}> = ({ model, boundaries, consoleEntries, onSelected, highlightedAction, highlightedEntry, highlightedConsoleEntry, selectedTime, setSelectedTime, sdkLanguage }) => {
|
2021-01-07 16:15:34 -08:00
|
|
|
const [measure, ref] = useMeasure<HTMLDivElement>();
|
2023-08-16 16:30:17 -07:00
|
|
|
const [dragWindow, setDragWindow] = React.useState<{ startX: number, endX: number, pivot?: number, type: 'resize' | 'move' } | undefined>();
|
|
|
|
|
const [previewPoint, setPreviewPoint] = React.useState<FilmStripPreviewPoint | undefined>();
|
2021-06-03 21:52:29 -07:00
|
|
|
|
2023-08-16 16:30:17 -07:00
|
|
|
const { offsets, curtainLeft, curtainRight } = React.useMemo(() => {
|
|
|
|
|
let activeWindow = selectedTime || boundaries;
|
|
|
|
|
if (dragWindow && dragWindow.startX !== dragWindow.endX) {
|
|
|
|
|
const time1 = positionToTime(measure.width, boundaries, dragWindow.startX);
|
|
|
|
|
const time2 = positionToTime(measure.width, boundaries, dragWindow.endX);
|
|
|
|
|
activeWindow = { minimum: Math.min(time1, time2), maximum: Math.max(time1, time2) };
|
2023-03-10 22:52:31 -08:00
|
|
|
}
|
2023-08-16 16:30:17 -07:00
|
|
|
const curtainLeft = timeToPosition(measure.width, boundaries, activeWindow.minimum);
|
|
|
|
|
const maxRight = timeToPosition(measure.width, boundaries, boundaries.maximum);
|
|
|
|
|
const curtainRight = maxRight - timeToPosition(measure.width, boundaries, activeWindow.maximum);
|
|
|
|
|
return { offsets: calculateDividerOffsets(measure.width, boundaries), curtainLeft, curtainRight };
|
|
|
|
|
}, [selectedTime, boundaries, dragWindow, measure]);
|
2021-01-15 18:30:55 -08:00
|
|
|
|
|
|
|
|
const bars = React.useMemo(() => {
|
|
|
|
|
const bars: TimelineBar[] = [];
|
2023-03-06 21:37:39 -08:00
|
|
|
for (const entry of model?.actions || []) {
|
2023-08-16 16:30:17 -07:00
|
|
|
if (entry.class === 'Test')
|
|
|
|
|
continue;
|
2021-10-15 14:22:49 -08:00
|
|
|
bars.push({
|
|
|
|
|
action: entry,
|
2023-02-27 15:29:20 -08:00
|
|
|
leftTime: entry.startTime,
|
2023-08-16 16:30:17 -07:00
|
|
|
rightTime: entry.endTime || boundaries.maximum,
|
2023-02-27 15:29:20 -08:00
|
|
|
leftPosition: timeToPosition(measure.width, boundaries, entry.startTime),
|
2023-08-16 16:30:17 -07:00
|
|
|
rightPosition: timeToPosition(measure.width, boundaries, entry.endTime || boundaries.maximum),
|
2023-08-30 12:40:46 -07:00
|
|
|
active: false,
|
2023-08-30 15:48:51 -07:00
|
|
|
error: !!entry.error,
|
2021-10-15 14:22:49 -08:00
|
|
|
});
|
|
|
|
|
}
|
2021-05-13 20:41:32 -07:00
|
|
|
|
2023-08-16 16:30:17 -07:00
|
|
|
for (const resource of model?.resources || []) {
|
|
|
|
|
const startTime = resource._monotonicTime!;
|
|
|
|
|
const endTime = resource._monotonicTime! + resource.time;
|
2021-10-15 14:22:49 -08:00
|
|
|
bars.push({
|
2023-08-16 16:30:17 -07:00
|
|
|
resource,
|
2021-10-15 14:22:49 -08:00
|
|
|
leftTime: startTime,
|
2023-08-16 16:30:17 -07:00
|
|
|
rightTime: endTime,
|
2021-10-15 14:22:49 -08:00
|
|
|
leftPosition: timeToPosition(measure.width, boundaries, startTime),
|
2023-08-16 16:30:17 -07:00
|
|
|
rightPosition: timeToPosition(measure.width, boundaries, endTime),
|
2023-08-30 12:40:46 -07:00
|
|
|
active: false,
|
2023-08-30 15:48:51 -07:00
|
|
|
error: false,
|
2021-10-15 14:22:49 -08:00
|
|
|
});
|
2021-01-15 18:30:55 -08:00
|
|
|
}
|
2024-07-18 16:39:40 +02:00
|
|
|
|
|
|
|
|
for (const consoleMessage of consoleEntries || []) {
|
|
|
|
|
bars.push({
|
|
|
|
|
consoleMessage,
|
|
|
|
|
leftTime: consoleMessage.timestamp,
|
|
|
|
|
rightTime: consoleMessage.timestamp,
|
|
|
|
|
leftPosition: timeToPosition(measure.width, boundaries, consoleMessage.timestamp),
|
|
|
|
|
rightPosition: timeToPosition(measure.width, boundaries, consoleMessage.timestamp),
|
|
|
|
|
active: false,
|
|
|
|
|
error: consoleMessage.isError,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-15 18:30:55 -08:00
|
|
|
return bars;
|
2024-07-18 16:39:40 +02:00
|
|
|
}, [model, consoleEntries, boundaries, measure]);
|
2023-08-30 12:40:46 -07:00
|
|
|
|
|
|
|
|
React.useMemo(() => {
|
2024-07-18 16:39:40 +02:00
|
|
|
for (const bar of bars) {
|
|
|
|
|
if (highlightedAction)
|
|
|
|
|
bar.active = bar.action === highlightedAction;
|
|
|
|
|
else if (highlightedEntry)
|
|
|
|
|
bar.active = bar.resource === highlightedEntry;
|
|
|
|
|
else if (highlightedConsoleEntry)
|
|
|
|
|
bar.active = bar.consoleMessage === highlightedConsoleEntry;
|
|
|
|
|
else
|
|
|
|
|
bar.active = false;
|
|
|
|
|
}
|
|
|
|
|
}, [bars, highlightedAction, highlightedEntry, highlightedConsoleEntry]);
|
2021-02-17 17:51:57 -08:00
|
|
|
|
2023-08-16 16:30:17 -07:00
|
|
|
const onMouseDown = React.useCallback((event: React.MouseEvent) => {
|
|
|
|
|
setPreviewPoint(undefined);
|
|
|
|
|
if (!ref.current)
|
|
|
|
|
return;
|
|
|
|
|
const x = event.clientX - ref.current.getBoundingClientRect().left;
|
2021-01-14 20:16:02 -08:00
|
|
|
const time = positionToTime(measure.width, boundaries, x);
|
2023-08-16 16:30:17 -07:00
|
|
|
const leftX = selectedTime ? timeToPosition(measure.width, boundaries, selectedTime.minimum) : 0;
|
|
|
|
|
const rightX = selectedTime ? timeToPosition(measure.width, boundaries, selectedTime.maximum) : 0;
|
|
|
|
|
|
|
|
|
|
if (selectedTime && Math.abs(x - leftX) < 10) {
|
|
|
|
|
// Resize left.
|
|
|
|
|
setDragWindow({ startX: rightX, endX: x, type: 'resize' });
|
|
|
|
|
} else if (selectedTime && Math.abs(x - rightX) < 10) {
|
|
|
|
|
// Resize right.
|
|
|
|
|
setDragWindow({ startX: leftX, endX: x, type: 'resize' });
|
|
|
|
|
} else if (selectedTime && time > selectedTime.minimum && time < selectedTime.maximum && event.clientY - ref.current.getBoundingClientRect().top < 20) {
|
|
|
|
|
// Move window.
|
|
|
|
|
setDragWindow({ startX: leftX, endX: rightX, pivot: x, type: 'move' });
|
|
|
|
|
} else {
|
|
|
|
|
// Create new.
|
|
|
|
|
setDragWindow({ startX: x, endX: x, type: 'resize' });
|
2021-01-14 20:16:02 -08:00
|
|
|
}
|
2023-08-16 16:30:17 -07:00
|
|
|
}, [boundaries, measure, ref, selectedTime]);
|
2021-01-14 20:16:02 -08:00
|
|
|
|
2023-08-17 09:36:59 -07:00
|
|
|
const onGlassPaneMouseMove = React.useCallback((event: MouseEvent) => {
|
2023-03-10 22:52:31 -08:00
|
|
|
if (!ref.current)
|
2021-02-17 17:51:57 -08:00
|
|
|
return;
|
2021-06-03 21:52:29 -07:00
|
|
|
const x = event.clientX - ref.current.getBoundingClientRect().left;
|
2023-08-16 16:30:17 -07:00
|
|
|
const time = positionToTime(measure.width, boundaries, x);
|
|
|
|
|
const action = model?.actions.findLast(action => action.startTime <= time);
|
2023-08-17 09:36:59 -07:00
|
|
|
|
2023-08-16 16:30:17 -07:00
|
|
|
if (!event.buttons) {
|
|
|
|
|
setDragWindow(undefined);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2021-04-19 19:50:11 -07:00
|
|
|
|
2023-08-16 16:30:17 -07:00
|
|
|
// When moving window reveal action under cursor.
|
|
|
|
|
if (action)
|
|
|
|
|
onSelected(action);
|
|
|
|
|
|
2023-08-17 09:36:59 -07:00
|
|
|
// Should not happen, but for type safety.
|
|
|
|
|
if (!dragWindow)
|
|
|
|
|
return;
|
|
|
|
|
|
2023-08-16 16:30:17 -07:00
|
|
|
let newDragWindow = dragWindow;
|
|
|
|
|
if (dragWindow.type === 'resize') {
|
|
|
|
|
newDragWindow = { ...dragWindow, endX: x };
|
|
|
|
|
} else {
|
|
|
|
|
const delta = x - dragWindow.pivot!;
|
|
|
|
|
let startX = dragWindow.startX + delta;
|
|
|
|
|
let endX = dragWindow.endX + delta;
|
|
|
|
|
if (startX < 0) {
|
|
|
|
|
startX = 0;
|
|
|
|
|
endX = startX + (dragWindow.endX - dragWindow.startX);
|
|
|
|
|
}
|
|
|
|
|
if (endX > measure.width) {
|
|
|
|
|
endX = measure.width;
|
|
|
|
|
startX = endX - (dragWindow.endX - dragWindow.startX);
|
|
|
|
|
}
|
|
|
|
|
newDragWindow = { ...dragWindow, startX, endX, pivot: x };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setDragWindow(newDragWindow);
|
|
|
|
|
const time1 = positionToTime(measure.width, boundaries, newDragWindow.startX);
|
|
|
|
|
const time2 = positionToTime(measure.width, boundaries, newDragWindow.endX);
|
|
|
|
|
if (time1 !== time2)
|
|
|
|
|
setSelectedTime({ minimum: Math.min(time1, time2), maximum: Math.max(time1, time2) });
|
2023-08-17 09:36:59 -07:00
|
|
|
}, [boundaries, dragWindow, measure, model, onSelected, ref, setSelectedTime]);
|
2021-04-19 19:50:11 -07:00
|
|
|
|
2023-08-17 09:36:59 -07:00
|
|
|
const onGlassPaneMouseUp = React.useCallback(() => {
|
2021-05-12 20:54:17 -07:00
|
|
|
setPreviewPoint(undefined);
|
2023-08-16 16:30:17 -07:00
|
|
|
if (!dragWindow)
|
2021-02-17 17:51:57 -08:00
|
|
|
return;
|
2023-08-16 16:30:17 -07:00
|
|
|
if (dragWindow.startX !== dragWindow.endX) {
|
|
|
|
|
const time1 = positionToTime(measure.width, boundaries, dragWindow.startX);
|
|
|
|
|
const time2 = positionToTime(measure.width, boundaries, dragWindow.endX);
|
|
|
|
|
setSelectedTime({ minimum: Math.min(time1, time2), maximum: Math.max(time1, time2) });
|
|
|
|
|
} else {
|
|
|
|
|
const time = positionToTime(measure.width, boundaries, dragWindow.startX);
|
|
|
|
|
const action = model?.actions.findLast(action => action.startTime <= time);
|
|
|
|
|
if (action)
|
|
|
|
|
onSelected(action);
|
2023-09-11 18:16:02 -07:00
|
|
|
setSelectedTime(undefined);
|
2023-08-16 16:30:17 -07:00
|
|
|
}
|
|
|
|
|
setDragWindow(undefined);
|
2023-09-11 18:16:02 -07:00
|
|
|
}, [boundaries, dragWindow, measure, model, setSelectedTime, onSelected]);
|
2023-08-16 16:30:17 -07:00
|
|
|
|
2023-08-17 09:36:59 -07:00
|
|
|
const onMouseMove = React.useCallback((event: React.MouseEvent) => {
|
|
|
|
|
if (!ref.current)
|
|
|
|
|
return;
|
|
|
|
|
const x = event.clientX - ref.current.getBoundingClientRect().left;
|
|
|
|
|
const time = positionToTime(measure.width, boundaries, x);
|
|
|
|
|
const action = model?.actions.findLast(action => action.startTime <= time);
|
|
|
|
|
setPreviewPoint({ x, clientY: event.clientY, action, sdkLanguage });
|
|
|
|
|
}, [boundaries, measure, model, ref, sdkLanguage]);
|
|
|
|
|
|
2023-08-16 16:30:17 -07:00
|
|
|
const onMouseLeave = React.useCallback(() => {
|
|
|
|
|
setPreviewPoint(undefined);
|
|
|
|
|
}, []);
|
|
|
|
|
|
2023-08-17 09:36:59 -07:00
|
|
|
const onPaneDoubleClick = React.useCallback(() => {
|
2023-08-16 16:30:17 -07:00
|
|
|
setSelectedTime(undefined);
|
|
|
|
|
}, [setSelectedTime]);
|
2021-01-07 16:15:34 -08:00
|
|
|
|
2023-03-07 12:43:16 -08:00
|
|
|
return <div style={{ flex: 'none', borderBottom: '1px solid var(--vscode-panel-border)' }}>
|
2024-01-10 15:28:33 -08:00
|
|
|
{!!dragWindow && <GlassPane
|
2023-08-17 09:36:59 -07:00
|
|
|
cursor={dragWindow?.type === 'resize' ? 'ew-resize' : 'grab'}
|
|
|
|
|
onPaneMouseUp={onGlassPaneMouseUp}
|
|
|
|
|
onPaneMouseMove={onGlassPaneMouseMove}
|
2024-01-10 15:28:33 -08:00
|
|
|
onPaneDoubleClick={onPaneDoubleClick} />}
|
2023-08-17 09:36:59 -07:00
|
|
|
<div ref={ref}
|
|
|
|
|
className='timeline-view'
|
2023-08-16 16:30:17 -07:00
|
|
|
onMouseDown={onMouseDown}
|
|
|
|
|
onMouseMove={onMouseMove}
|
2023-08-17 09:36:59 -07:00
|
|
|
onMouseLeave={onMouseLeave}>
|
2023-03-07 12:43:16 -08:00
|
|
|
<div className='timeline-grid'>{
|
|
|
|
|
offsets.map((offset, index) => {
|
|
|
|
|
return <div key={index} className='timeline-divider' style={{ left: offset.position + 'px' }}>
|
|
|
|
|
<div className='timeline-time'>{msToString(offset.time - boundaries.minimum)}</div>
|
|
|
|
|
</div>;
|
|
|
|
|
})
|
|
|
|
|
}</div>
|
2023-08-29 22:23:08 -07:00
|
|
|
<div style={{ height: 8 }}></div>
|
|
|
|
|
<FilmStrip model={model} boundaries={boundaries} previewPoint={previewPoint} />
|
|
|
|
|
<div className='timeline-bars'>{
|
2023-03-07 12:43:16 -08:00
|
|
|
bars.map((bar, index) => {
|
|
|
|
|
return <div key={index}
|
2024-07-31 12:12:06 +02:00
|
|
|
className={clsx('timeline-bar',
|
|
|
|
|
bar.action && 'action',
|
|
|
|
|
bar.resource && 'network',
|
|
|
|
|
bar.consoleMessage && 'console-message',
|
|
|
|
|
bar.active && 'active',
|
|
|
|
|
bar.error && 'error')}
|
2023-03-07 12:43:16 -08:00
|
|
|
style={{
|
2023-08-29 22:23:08 -07:00
|
|
|
left: bar.leftPosition,
|
2024-07-18 16:39:40 +02:00
|
|
|
width: Math.max(5, bar.rightPosition - bar.leftPosition),
|
2023-08-29 22:23:08 -07:00
|
|
|
top: barTop(bar),
|
|
|
|
|
bottom: 0,
|
2023-03-07 12:43:16 -08:00
|
|
|
}}
|
|
|
|
|
></div>;
|
|
|
|
|
})
|
2023-08-29 22:23:08 -07:00
|
|
|
}</div>
|
2023-08-16 16:30:17 -07:00
|
|
|
<div className='timeline-marker' style={{
|
2023-03-07 12:43:16 -08:00
|
|
|
display: (previewPoint !== undefined) ? 'block' : 'none',
|
|
|
|
|
left: (previewPoint?.x || 0) + 'px',
|
2023-08-16 16:30:17 -07:00
|
|
|
}} />
|
2023-08-17 09:36:59 -07:00
|
|
|
{selectedTime && <div className='timeline-window'>
|
2023-08-16 16:30:17 -07:00
|
|
|
<div className='timeline-window-curtain left' style={{ width: curtainLeft }}></div>
|
2023-08-30 15:48:51 -07:00
|
|
|
<div className='timeline-window-resizer' style={{ left: -5 }}></div>
|
2023-08-16 16:30:17 -07:00
|
|
|
<div className='timeline-window-center'>
|
|
|
|
|
<div className='timeline-window-drag'></div>
|
|
|
|
|
</div>
|
2023-08-30 15:48:51 -07:00
|
|
|
<div className='timeline-window-resizer' style={{ left: 5 }}></div>
|
2023-08-16 16:30:17 -07:00
|
|
|
<div className='timeline-window-curtain right' style={{ width: curtainRight }}></div>
|
2023-08-17 09:36:59 -07:00
|
|
|
</div>}
|
2023-03-07 12:43:16 -08:00
|
|
|
</div>
|
2021-01-07 16:15:34 -08:00
|
|
|
</div>;
|
|
|
|
|
};
|
|
|
|
|
|
2021-01-15 18:30:55 -08:00
|
|
|
function calculateDividerOffsets(clientWidth: number, boundaries: Boundaries): { position: number, time: number }[] {
|
2021-01-07 16:15:34 -08:00
|
|
|
const minimumGap = 64;
|
|
|
|
|
let dividerCount = clientWidth / minimumGap;
|
|
|
|
|
const boundarySpan = boundaries.maximum - boundaries.minimum;
|
|
|
|
|
const pixelsPerMillisecond = clientWidth / boundarySpan;
|
|
|
|
|
let sectionTime = boundarySpan / dividerCount;
|
|
|
|
|
|
|
|
|
|
const logSectionTime = Math.ceil(Math.log(sectionTime) / Math.LN10);
|
|
|
|
|
sectionTime = Math.pow(10, logSectionTime);
|
|
|
|
|
if (sectionTime * pixelsPerMillisecond >= 5 * minimumGap)
|
|
|
|
|
sectionTime = sectionTime / 5;
|
|
|
|
|
if (sectionTime * pixelsPerMillisecond >= 2 * minimumGap)
|
|
|
|
|
sectionTime = sectionTime / 2;
|
|
|
|
|
|
|
|
|
|
const firstDividerTime = boundaries.minimum;
|
|
|
|
|
let lastDividerTime = boundaries.maximum;
|
|
|
|
|
lastDividerTime += minimumGap / pixelsPerMillisecond;
|
|
|
|
|
dividerCount = Math.ceil((lastDividerTime - firstDividerTime) / sectionTime);
|
|
|
|
|
|
|
|
|
|
if (!sectionTime)
|
|
|
|
|
dividerCount = 0;
|
|
|
|
|
|
|
|
|
|
const offsets = [];
|
|
|
|
|
for (let i = 0; i < dividerCount; ++i) {
|
|
|
|
|
const time = firstDividerTime + sectionTime * i;
|
2021-01-15 18:30:55 -08:00
|
|
|
offsets.push({ position: timeToPosition(clientWidth, boundaries, time), time });
|
2021-01-07 16:15:34 -08:00
|
|
|
}
|
|
|
|
|
return offsets;
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-15 18:30:55 -08:00
|
|
|
function timeToPosition(clientWidth: number, boundaries: Boundaries, time: number): number {
|
|
|
|
|
return (time - boundaries.minimum) / (boundaries.maximum - boundaries.minimum) * clientWidth;
|
2021-01-07 16:15:34 -08:00
|
|
|
}
|
|
|
|
|
|
2021-01-14 20:16:02 -08:00
|
|
|
function positionToTime(clientWidth: number, boundaries: Boundaries, x: number): number {
|
2021-01-15 18:30:55 -08:00
|
|
|
return x / clientWidth * (boundaries.maximum - boundaries.minimum) + boundaries.minimum;
|
2021-01-14 20:16:02 -08:00
|
|
|
}
|
2021-06-03 21:52:29 -07:00
|
|
|
|
|
|
|
|
function barTop(bar: TimelineBar): number {
|
2023-08-29 22:23:08 -07:00
|
|
|
return bar.resource ? 25 : 20;
|
2021-06-03 21:52:29 -07:00
|
|
|
}
|