@midscene/visualizer 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +9 -0
- package/README.md +24 -0
- package/dist/es/assets/logo-plain.16842bbc.svg +70 -0
- package/dist/es/assets/logo-plain2.16842bbc.svg +70 -0
- package/dist/es/component/blackboard.css +25 -0
- package/dist/es/component/blackboard.js +256 -0
- package/dist/es/component/color.js +34 -0
- package/dist/es/component/common.css +0 -0
- package/dist/es/component/detail-panel.css +34 -0
- package/dist/es/component/detail-panel.js +106 -0
- package/dist/es/component/detail-side.css +99 -0
- package/dist/es/component/detail-side.js +285 -0
- package/dist/es/component/global-hover-preview.css +19 -0
- package/dist/es/component/global-hover-preview.js +44 -0
- package/dist/es/component/misc.js +24 -0
- package/dist/es/component/panel-title.css +8 -0
- package/dist/es/component/panel-title.js +9 -0
- package/dist/es/component/side-item.js +0 -0
- package/dist/es/component/sidebar.css +87 -0
- package/dist/es/component/sidebar.js +175 -0
- package/dist/es/component/store.js +128 -0
- package/dist/es/component/timeline.css +18 -0
- package/dist/es/component/timeline.js +438 -0
- package/dist/es/index.css +89 -0
- package/dist/es/index.js +174 -0
- package/dist/es/utils.js +76 -0
- package/dist/lib/assets/logo-plain.16842bbc.svg +70 -0
- package/dist/lib/assets/logo-plain2.16842bbc.svg +70 -0
- package/dist/lib/component/blackboard.css +25 -0
- package/dist/lib/component/blackboard.js +286 -0
- package/dist/lib/component/color.js +59 -0
- package/dist/lib/component/common.css +0 -0
- package/dist/lib/component/detail-panel.css +34 -0
- package/dist/lib/component/detail-panel.js +136 -0
- package/dist/lib/component/detail-side.css +99 -0
- package/dist/lib/component/detail-side.js +313 -0
- package/dist/lib/component/global-hover-preview.css +19 -0
- package/dist/lib/component/global-hover-preview.js +64 -0
- package/dist/lib/component/misc.js +48 -0
- package/dist/lib/component/panel-title.css +8 -0
- package/dist/lib/component/panel-title.js +29 -0
- package/dist/lib/component/side-item.js +1 -0
- package/dist/lib/component/sidebar.css +87 -0
- package/dist/lib/component/sidebar.js +198 -0
- package/dist/lib/component/store.js +153 -0
- package/dist/lib/component/timeline.css +18 -0
- package/dist/lib/component/timeline.js +466 -0
- package/dist/lib/index.css +89 -0
- package/dist/lib/index.js +202 -0
- package/dist/lib/utils.js +111 -0
- package/dist/types/component/blackboard.d.ts +4 -0
- package/dist/types/component/color.d.ts +2 -0
- package/dist/types/component/detail-panel.d.ts +4 -0
- package/dist/types/component/detail-side.d.ts +4 -0
- package/dist/types/component/global-hover-preview.d.ts +4 -0
- package/dist/types/component/misc.d.ts +2 -0
- package/dist/types/component/panel-title.d.ts +6 -0
- package/dist/types/component/side-item.d.ts +0 -0
- package/dist/types/component/sidebar.d.ts +4 -0
- package/dist/types/component/store.d.ts +35 -0
- package/dist/types/component/timeline.d.ts +4 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/utils.d.ts +5 -0
- package/docs/index.tsx +6 -0
- package/modern.config.ts +15 -0
- package/package.json +46 -0
- package/src/component/assets/logo-plain.svg +70 -0
- package/src/component/assets/logo-plain2.svg +70 -0
- package/src/component/blackboard.less +37 -0
- package/src/component/blackboard.tsx +293 -0
- package/src/component/color.tsx +34 -0
- package/src/component/common.less +21 -0
- package/src/component/detail-panel.less +47 -0
- package/src/component/detail-panel.tsx +124 -0
- package/src/component/detail-side.less +131 -0
- package/src/component/detail-side.tsx +361 -0
- package/src/component/global-hover-preview.less +23 -0
- package/src/component/global-hover-preview.tsx +50 -0
- package/src/component/misc.tsx +20 -0
- package/src/component/panel-title.less +11 -0
- package/src/component/panel-title.tsx +11 -0
- package/src/component/side-item.tsx +0 -0
- package/src/component/sidebar.less +122 -0
- package/src/component/sidebar.tsx +205 -0
- package/src/component/store.tsx +151 -0
- package/src/component/timeline.less +25 -0
- package/src/component/timeline.tsx +486 -0
- package/src/global.d.ts +11 -0
- package/src/index.less +113 -0
- package/src/index.tsx +210 -0
- package/src/utils.ts +58 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import * as PIXI from 'pixi.js';
|
|
4
|
+
|
|
5
|
+
import './timeline.less';
|
|
6
|
+
import { ExecutionRecorderItem, ExecutionTask } from '@midscene/core';
|
|
7
|
+
import { useAllCurrentTasks, useExecutionDump } from './store';
|
|
8
|
+
|
|
9
|
+
interface TimelineItem {
|
|
10
|
+
id: string;
|
|
11
|
+
img: string;
|
|
12
|
+
timeOffset: number;
|
|
13
|
+
x?: number;
|
|
14
|
+
y?: number;
|
|
15
|
+
width?: number;
|
|
16
|
+
height?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface HighlightParam {
|
|
20
|
+
mouseX: number;
|
|
21
|
+
mouseY: number;
|
|
22
|
+
item: TimelineItem;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface HighlightMask {
|
|
26
|
+
startMs: number;
|
|
27
|
+
endMs: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Function to clone a sprite
|
|
31
|
+
function cloneSprite(sprite: PIXI.Sprite) {
|
|
32
|
+
const clonedSprite = new PIXI.Sprite(sprite.texture);
|
|
33
|
+
|
|
34
|
+
// Copy properties
|
|
35
|
+
clonedSprite.position.copyFrom(sprite.position);
|
|
36
|
+
clonedSprite.scale.copyFrom(sprite.scale);
|
|
37
|
+
clonedSprite.rotation = sprite.rotation;
|
|
38
|
+
clonedSprite.alpha = sprite.alpha;
|
|
39
|
+
clonedSprite.visible = sprite.visible;
|
|
40
|
+
|
|
41
|
+
return clonedSprite;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const TimelineWidget = (props: {
|
|
45
|
+
screenshots: TimelineItem[];
|
|
46
|
+
onHighlight?: (param: HighlightParam) => any;
|
|
47
|
+
onUnhighlight?: () => any;
|
|
48
|
+
onTap?: (param: TimelineItem) => any;
|
|
49
|
+
highlightMask?: HighlightMask;
|
|
50
|
+
hoverMask?: HighlightMask;
|
|
51
|
+
}): JSX.Element => {
|
|
52
|
+
const domRef = useRef<HTMLDivElement>(null); // Should be HTMLDivElement not HTMLInputElement
|
|
53
|
+
const app = useMemo<PIXI.Application>(() => new PIXI.Application(), []);
|
|
54
|
+
|
|
55
|
+
const gridsContainer = useMemo(() => new PIXI.Container(), []);
|
|
56
|
+
const screenshotsContainer = useMemo(() => new PIXI.Container(), []);
|
|
57
|
+
const highlightMaskContainer = useMemo(() => new PIXI.Container(), []);
|
|
58
|
+
const containerUpdaterRef = useRef(
|
|
59
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
60
|
+
(_s: number | undefined, _e: number | undefined, _hs: number | undefined, _he: number | undefined) => {},
|
|
61
|
+
);
|
|
62
|
+
const indicatorContainer = useMemo(() => new PIXI.Container(), []);
|
|
63
|
+
|
|
64
|
+
const allScreenshots = props.screenshots || [];
|
|
65
|
+
const maxTime = allScreenshots[allScreenshots.length - 1].timeOffset;
|
|
66
|
+
|
|
67
|
+
const sizeRatio = 2;
|
|
68
|
+
|
|
69
|
+
const titleBg = 0xdddddd; // @title-bg
|
|
70
|
+
const sideBg = 0xececec;
|
|
71
|
+
const gridTextColor = 0;
|
|
72
|
+
const shotBorderColor = 0x777777;
|
|
73
|
+
const gridLineColor = 0xcccccc; // @border-color
|
|
74
|
+
const gridHighlightColor = 0x06b1ab; // @main-blue
|
|
75
|
+
const timeContentFontSize = 20;
|
|
76
|
+
const commonPadding = 12;
|
|
77
|
+
const timeTextTop = commonPadding;
|
|
78
|
+
const timeTitleBottom = timeTextTop * 2 + timeContentFontSize;
|
|
79
|
+
const highlightMaskAlpha = 0.6;
|
|
80
|
+
const hoverMaskAlpha = 0.3;
|
|
81
|
+
|
|
82
|
+
const closestScreenshotItemOnXY = (x: number, _y: number) => {
|
|
83
|
+
// find out the screenshot that is closest to the mouse on the left
|
|
84
|
+
let closestScreenshot: TimelineItem | undefined; // already sorted
|
|
85
|
+
let closestIndex = -1;
|
|
86
|
+
for (let i = 0; i < allScreenshots.length; i++) {
|
|
87
|
+
const shot = allScreenshots[i];
|
|
88
|
+
if (shot.x! <= x) {
|
|
89
|
+
closestScreenshot = allScreenshots[i];
|
|
90
|
+
closestIndex = i;
|
|
91
|
+
} else {
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
closestScreenshot,
|
|
97
|
+
closestIndex,
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
useMemo(() => {
|
|
102
|
+
const { startMs, endMs } = props.highlightMask || {};
|
|
103
|
+
const { startMs: hoverStartMs, endMs: hoverEndMs } = props.hoverMask || {};
|
|
104
|
+
const fn = containerUpdaterRef.current;
|
|
105
|
+
fn(startMs, endMs, hoverStartMs, hoverEndMs);
|
|
106
|
+
}, [
|
|
107
|
+
props.highlightMask?.startMs,
|
|
108
|
+
props.highlightMask?.endMs,
|
|
109
|
+
props.hoverMask?.startMs,
|
|
110
|
+
props.hoverMask?.endMs,
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
Promise.resolve(
|
|
115
|
+
(async () => {
|
|
116
|
+
if (!domRef.current) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// width of domRef
|
|
121
|
+
const { clientWidth, clientHeight } = domRef.current;
|
|
122
|
+
const canvasWidth = clientWidth * sizeRatio;
|
|
123
|
+
const canvasHeight = clientHeight * sizeRatio;
|
|
124
|
+
|
|
125
|
+
let singleGridWidth = 100 * sizeRatio;
|
|
126
|
+
let gridCount = Math.floor(canvasWidth / singleGridWidth);
|
|
127
|
+
const stepCandidate = [
|
|
128
|
+
50, 100, 200, 300, 500, 1000, 2000, 3000, 5000, 6000, 8000, 9000, 10000, 20000, 30000, 40000, 60000,
|
|
129
|
+
90000, 12000, 300000,
|
|
130
|
+
];
|
|
131
|
+
let timeStep = stepCandidate[0];
|
|
132
|
+
for (let i = stepCandidate.length - 1; i >= 0; i--) {
|
|
133
|
+
if (gridCount * stepCandidate[i] >= maxTime) {
|
|
134
|
+
timeStep = stepCandidate[i];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const gridRatio = maxTime / (gridCount * timeStep);
|
|
138
|
+
if (gridRatio <= 0.8) {
|
|
139
|
+
singleGridWidth = Math.floor(singleGridWidth * (1 / gridRatio) * 0.9);
|
|
140
|
+
gridCount = Math.floor(canvasWidth / singleGridWidth);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const leftForTimeOffset = (timeOffset: number) => {
|
|
144
|
+
return Math.floor((singleGridWidth * timeOffset) / timeStep);
|
|
145
|
+
};
|
|
146
|
+
const timeOffsetForLeft = (left: number) => {
|
|
147
|
+
return Math.floor((left * timeStep) / singleGridWidth);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
await app.init({
|
|
151
|
+
width: canvasWidth,
|
|
152
|
+
height: canvasHeight,
|
|
153
|
+
backgroundColor: sideBg,
|
|
154
|
+
});
|
|
155
|
+
if (!domRef.current) {
|
|
156
|
+
app.destroy();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
domRef.current.replaceChildren(app.canvas);
|
|
160
|
+
|
|
161
|
+
const pixiTextForNumber = (num: number) => {
|
|
162
|
+
const textContent = `${num}ms`;
|
|
163
|
+
const text = new PIXI.Text(`${textContent}`, {
|
|
164
|
+
fontSize: timeContentFontSize,
|
|
165
|
+
fill: gridTextColor,
|
|
166
|
+
});
|
|
167
|
+
return text;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// drawing vertical grids, texts, title bg
|
|
171
|
+
gridsContainer.removeChildren();
|
|
172
|
+
const titleBgSection = new PIXI.Graphics();
|
|
173
|
+
titleBgSection.beginFill(titleBg);
|
|
174
|
+
titleBgSection.drawRect(0, 0, canvasWidth, timeTitleBottom);
|
|
175
|
+
titleBgSection.endFill();
|
|
176
|
+
gridsContainer.addChild(titleBgSection);
|
|
177
|
+
|
|
178
|
+
const gridHeight = canvasHeight;
|
|
179
|
+
for (let i = 1; i <= gridCount; i++) {
|
|
180
|
+
const gridLine = new PIXI.Graphics();
|
|
181
|
+
const gridLineLeft = leftForTimeOffset(i * timeStep);
|
|
182
|
+
gridLine.beginFill(gridLineColor);
|
|
183
|
+
gridLine.drawRect(gridLineLeft, 0, sizeRatio, gridHeight);
|
|
184
|
+
gridLine.endFill();
|
|
185
|
+
gridsContainer.addChild(gridLine);
|
|
186
|
+
|
|
187
|
+
// mark text at the left of each line
|
|
188
|
+
const text = pixiTextForNumber(i * timeStep); // `${i * timeStep}ms`;
|
|
189
|
+
// measure text width
|
|
190
|
+
const textLeft = gridLineLeft - text.width - commonPadding;
|
|
191
|
+
|
|
192
|
+
text.x = textLeft;
|
|
193
|
+
text.y = timeTextTop;
|
|
194
|
+
|
|
195
|
+
gridsContainer.addChild(text);
|
|
196
|
+
}
|
|
197
|
+
app.stage.addChild(gridsContainer);
|
|
198
|
+
|
|
199
|
+
if (!allScreenshots.length) {
|
|
200
|
+
console.warn('No screenshots found');
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const shotContainers: PIXI.Container[] = [];
|
|
205
|
+
|
|
206
|
+
// draw all screenshots
|
|
207
|
+
screenshotsContainer.removeChildren();
|
|
208
|
+
const screenshotTop = timeTitleBottom + commonPadding * 1.5;
|
|
209
|
+
const screenshotMaxHeight = canvasHeight - screenshotTop - commonPadding * 1.5;
|
|
210
|
+
allScreenshots.forEach((screenshot, index) => {
|
|
211
|
+
const container = new PIXI.Container();
|
|
212
|
+
shotContainers.push(container);
|
|
213
|
+
app.stage.addChild(container);
|
|
214
|
+
const img = new Image();
|
|
215
|
+
img.src = screenshot.img;
|
|
216
|
+
img.onload = () => {
|
|
217
|
+
const screenshotTexture = PIXI.Texture.from(img);
|
|
218
|
+
const screenshotSprite = new PIXI.Sprite(screenshotTexture);
|
|
219
|
+
|
|
220
|
+
// get width / height of img
|
|
221
|
+
const originalWidth = img.width;
|
|
222
|
+
const originalHeight = img.height;
|
|
223
|
+
|
|
224
|
+
const screenshotHeight = screenshotMaxHeight;
|
|
225
|
+
const screenshotWidth = Math.floor((screenshotHeight / originalHeight) * originalWidth);
|
|
226
|
+
|
|
227
|
+
const screenshotX = leftForTimeOffset(screenshot.timeOffset);
|
|
228
|
+
allScreenshots[index].x = screenshotX;
|
|
229
|
+
allScreenshots[index].y = screenshotTop;
|
|
230
|
+
allScreenshots[index].width = screenshotWidth;
|
|
231
|
+
allScreenshots[index].height = screenshotMaxHeight;
|
|
232
|
+
|
|
233
|
+
const border = new PIXI.Graphics();
|
|
234
|
+
border.lineStyle(sizeRatio, shotBorderColor, 1);
|
|
235
|
+
border.drawRect(screenshotX, screenshotTop, screenshotWidth, screenshotMaxHeight);
|
|
236
|
+
border.endFill();
|
|
237
|
+
container.addChild(border);
|
|
238
|
+
|
|
239
|
+
screenshotSprite.x = screenshotX;
|
|
240
|
+
screenshotSprite.y = screenshotTop;
|
|
241
|
+
screenshotSprite.width = screenshotWidth;
|
|
242
|
+
screenshotSprite.height = screenshotMaxHeight;
|
|
243
|
+
container.addChild(screenshotSprite);
|
|
244
|
+
};
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const highlightMaskUpdater = (
|
|
248
|
+
start: number | undefined,
|
|
249
|
+
end: number | undefined,
|
|
250
|
+
hoverStart: number | undefined,
|
|
251
|
+
hoverEnd: number | undefined,
|
|
252
|
+
) => {
|
|
253
|
+
highlightMaskContainer.removeChildren();
|
|
254
|
+
|
|
255
|
+
const mask = (start: number | undefined, end: number | undefined, alpha: number) => {
|
|
256
|
+
if (typeof start === 'undefined' || typeof end === 'undefined' || end === 0) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const leftBorder = new PIXI.Graphics();
|
|
260
|
+
leftBorder.beginFill(gridHighlightColor, 1);
|
|
261
|
+
leftBorder.drawRect(leftForTimeOffset(start), 0, sizeRatio, canvasHeight);
|
|
262
|
+
leftBorder.endFill();
|
|
263
|
+
highlightMaskContainer.addChild(leftBorder);
|
|
264
|
+
|
|
265
|
+
const rightBorder = new PIXI.Graphics();
|
|
266
|
+
rightBorder.beginFill(gridHighlightColor, 1);
|
|
267
|
+
rightBorder.drawRect(leftForTimeOffset(end), 0, sizeRatio, canvasHeight);
|
|
268
|
+
rightBorder.endFill();
|
|
269
|
+
highlightMaskContainer.addChild(rightBorder);
|
|
270
|
+
|
|
271
|
+
const mask = new PIXI.Graphics();
|
|
272
|
+
mask.beginFill(gridHighlightColor, alpha);
|
|
273
|
+
mask.drawRect(
|
|
274
|
+
leftForTimeOffset(start),
|
|
275
|
+
0,
|
|
276
|
+
leftForTimeOffset(end) - leftForTimeOffset(start),
|
|
277
|
+
canvasHeight,
|
|
278
|
+
);
|
|
279
|
+
mask.endFill();
|
|
280
|
+
highlightMaskContainer.addChild(mask);
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
mask(start, end, highlightMaskAlpha);
|
|
284
|
+
mask(hoverStart, hoverEnd, hoverMaskAlpha);
|
|
285
|
+
};
|
|
286
|
+
highlightMaskUpdater(props.highlightMask?.startMs, props.highlightMask?.endMs, 0, 0);
|
|
287
|
+
containerUpdaterRef.current = highlightMaskUpdater;
|
|
288
|
+
|
|
289
|
+
// keep tracking the position of the mouse moving above the canvas
|
|
290
|
+
app.stage.interactive = true;
|
|
291
|
+
const onPointerMove = (event: PointerEvent) => {
|
|
292
|
+
const x = event.offsetX * sizeRatio;
|
|
293
|
+
const y = event.offsetY * sizeRatio;
|
|
294
|
+
indicatorContainer.removeChildren();
|
|
295
|
+
|
|
296
|
+
// find out the screenshot that is closest to the mouse on the left
|
|
297
|
+
const { closestScreenshot, closestIndex } = closestScreenshotItemOnXY(x, y);
|
|
298
|
+
if (closestIndex < 0) {
|
|
299
|
+
props.onUnhighlight?.();
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const closestContainer = shotContainers[closestIndex];
|
|
303
|
+
|
|
304
|
+
// highlight the items in closestContainer
|
|
305
|
+
closestContainer.children.forEach((child) => {
|
|
306
|
+
if (child instanceof PIXI.Sprite) {
|
|
307
|
+
// border
|
|
308
|
+
const newSpirit = new PIXI.Graphics();
|
|
309
|
+
newSpirit.lineStyle(2, gridHighlightColor, 1);
|
|
310
|
+
newSpirit.drawRect(
|
|
311
|
+
x, // follow mouse
|
|
312
|
+
closestScreenshot!.y!,
|
|
313
|
+
closestScreenshot!.width!,
|
|
314
|
+
closestScreenshot!.height!,
|
|
315
|
+
);
|
|
316
|
+
newSpirit.endFill();
|
|
317
|
+
indicatorContainer.addChild(newSpirit);
|
|
318
|
+
|
|
319
|
+
const screenshotSpirit = cloneSprite(child);
|
|
320
|
+
screenshotSpirit.x = x;
|
|
321
|
+
indicatorContainer.addChild(screenshotSpirit);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// cursor line
|
|
326
|
+
const indicator = new PIXI.Graphics();
|
|
327
|
+
indicator.beginFill(gridHighlightColor, 1);
|
|
328
|
+
indicator.drawRect(x - 1, 0, 3, canvasHeight);
|
|
329
|
+
indicator.endFill();
|
|
330
|
+
indicatorContainer.addChild(indicator);
|
|
331
|
+
|
|
332
|
+
// time string
|
|
333
|
+
const text = pixiTextForNumber(timeOffsetForLeft(x));
|
|
334
|
+
text.x = x + 5;
|
|
335
|
+
text.y = timeTextTop;
|
|
336
|
+
const textBg = new PIXI.Graphics();
|
|
337
|
+
textBg.beginFill(titleBg, 1);
|
|
338
|
+
textBg.drawRect(text.x, text.y, text.width + 10, text.height);
|
|
339
|
+
textBg.endFill();
|
|
340
|
+
|
|
341
|
+
indicatorContainer.addChild(textBg);
|
|
342
|
+
indicatorContainer.addChild(text);
|
|
343
|
+
|
|
344
|
+
props.onHighlight?.({
|
|
345
|
+
mouseX: x / sizeRatio,
|
|
346
|
+
mouseY: y / sizeRatio,
|
|
347
|
+
item: closestScreenshot!,
|
|
348
|
+
});
|
|
349
|
+
};
|
|
350
|
+
// app.stage.on('pointermove', onPointerMove);
|
|
351
|
+
// on pointer move out
|
|
352
|
+
const onPointerOut = () => {
|
|
353
|
+
indicatorContainer.removeChildren();
|
|
354
|
+
props.onUnhighlight?.();
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const onPointerTap = (event: PointerEvent) => {
|
|
358
|
+
const x = event.offsetX * sizeRatio;
|
|
359
|
+
const y = event.offsetY * sizeRatio;
|
|
360
|
+
const { closestScreenshot } = closestScreenshotItemOnXY(x, y);
|
|
361
|
+
if (closestScreenshot) {
|
|
362
|
+
props.onTap?.(closestScreenshot);
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
app.stage.addChild(screenshotsContainer);
|
|
367
|
+
app.stage.addChild(highlightMaskContainer);
|
|
368
|
+
app.stage.addChild(indicatorContainer);
|
|
369
|
+
|
|
370
|
+
const canvas = app.view;
|
|
371
|
+
canvas.addEventListener('pointermove', onPointerMove);
|
|
372
|
+
canvas.addEventListener('pointerout', onPointerOut);
|
|
373
|
+
canvas.addEventListener('pointerdown', onPointerTap);
|
|
374
|
+
})(),
|
|
375
|
+
);
|
|
376
|
+
}, []);
|
|
377
|
+
|
|
378
|
+
return <div className="timeline-canvas-wrapper" ref={domRef}></div>;
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const Timeline = () => {
|
|
382
|
+
const allTasks = useAllCurrentTasks();
|
|
383
|
+
const wrapper = useRef<HTMLDivElement>(null);
|
|
384
|
+
const setActiveTask = useExecutionDump((store) => store.setActiveTask);
|
|
385
|
+
const activeTask = useExecutionDump((store) => store.activeTask);
|
|
386
|
+
const hoverTask = useExecutionDump((store) => store.hoverTask);
|
|
387
|
+
const setHoverTask = useExecutionDump((store) => store.setHoverTask);
|
|
388
|
+
const setHoverPreviewConfig = useExecutionDump((store) => store.setHoverPreviewConfig);
|
|
389
|
+
|
|
390
|
+
// should be first task time ?
|
|
391
|
+
let startingTime = -1;
|
|
392
|
+
let idCount = 1;
|
|
393
|
+
const idTaskMap: Record<string, ExecutionTask> = {};
|
|
394
|
+
const allScreenshots: TimelineItem[] = allTasks
|
|
395
|
+
.reduce<(ExecutionRecorderItem & { id: string })[]>((acc, current) => {
|
|
396
|
+
const recorders = current.recorder || [];
|
|
397
|
+
recorders.forEach((item) => {
|
|
398
|
+
if (startingTime === -1 || startingTime > item.ts) {
|
|
399
|
+
startingTime = item.ts;
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
if (current.timing?.start && (startingTime === -1 || startingTime > current.timing.start)) {
|
|
403
|
+
startingTime = current.timing.start;
|
|
404
|
+
}
|
|
405
|
+
const recorderItemWithId = recorders.map((item) => {
|
|
406
|
+
const idStr = `id_${idCount++}`;
|
|
407
|
+
idTaskMap[idStr] = current;
|
|
408
|
+
return {
|
|
409
|
+
...item,
|
|
410
|
+
id: idStr,
|
|
411
|
+
};
|
|
412
|
+
});
|
|
413
|
+
return acc.concat(recorderItemWithId || []);
|
|
414
|
+
}, [])
|
|
415
|
+
.filter((item) => {
|
|
416
|
+
return item.screenshot;
|
|
417
|
+
})
|
|
418
|
+
.map((recorderItem) => {
|
|
419
|
+
return {
|
|
420
|
+
id: recorderItem.id,
|
|
421
|
+
img: recorderItem.screenshot!,
|
|
422
|
+
timeOffset: recorderItem.ts - startingTime,
|
|
423
|
+
};
|
|
424
|
+
})
|
|
425
|
+
.sort((a, b) => a.timeOffset - b.timeOffset);
|
|
426
|
+
|
|
427
|
+
const itemOnTap = (item: TimelineItem) => {
|
|
428
|
+
const task = idTaskMap[item.id];
|
|
429
|
+
if (task) {
|
|
430
|
+
setActiveTask(task);
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
const onHighlightItem = (param: HighlightParam) => {
|
|
435
|
+
const { mouseX, item } = param;
|
|
436
|
+
const refBounding = wrapper.current?.getBoundingClientRect();
|
|
437
|
+
const task = idTaskMap[item.id];
|
|
438
|
+
if (task) {
|
|
439
|
+
setHoverTask(task);
|
|
440
|
+
setHoverPreviewConfig({
|
|
441
|
+
x: mouseX + (refBounding?.left || 0),
|
|
442
|
+
y: (refBounding?.bottom || 1) - 1,
|
|
443
|
+
});
|
|
444
|
+
} else {
|
|
445
|
+
setHoverTask(null);
|
|
446
|
+
setHoverPreviewConfig(null);
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
const unhighlight = () => {
|
|
451
|
+
setHoverTask(null);
|
|
452
|
+
setHoverPreviewConfig(null);
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
// overall left of wrapper
|
|
456
|
+
|
|
457
|
+
const maskConfigForTask = (task?: ExecutionTask | null): HighlightMask | undefined => {
|
|
458
|
+
if (!task) {
|
|
459
|
+
return undefined;
|
|
460
|
+
}
|
|
461
|
+
return task.timing?.start && task.timing?.end
|
|
462
|
+
? {
|
|
463
|
+
startMs: task.timing.start - startingTime || 0,
|
|
464
|
+
endMs: task.timing.end - startingTime || 0,
|
|
465
|
+
}
|
|
466
|
+
: undefined;
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const highlightMaskConfig = maskConfigForTask(activeTask);
|
|
470
|
+
const hoverMaskConfig = maskConfigForTask(hoverTask);
|
|
471
|
+
|
|
472
|
+
return (
|
|
473
|
+
<div className="timeline-wrapper" ref={wrapper}>
|
|
474
|
+
<TimelineWidget
|
|
475
|
+
// key={dimensions.width}
|
|
476
|
+
screenshots={allScreenshots}
|
|
477
|
+
onTap={itemOnTap}
|
|
478
|
+
onHighlight={onHighlightItem}
|
|
479
|
+
onUnhighlight={unhighlight}
|
|
480
|
+
highlightMask={highlightMaskConfig}
|
|
481
|
+
hoverMask={hoverMaskConfig}
|
|
482
|
+
/>
|
|
483
|
+
</div>
|
|
484
|
+
);
|
|
485
|
+
};
|
|
486
|
+
export default Timeline;
|
package/src/global.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
declare module '*.svg' {
|
|
2
|
+
export const ReactComponent: React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
|
|
3
|
+
|
|
4
|
+
const content: string;
|
|
5
|
+
export default content;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
declare module '*.svg?react' {
|
|
9
|
+
const ReactComponent: React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
|
|
10
|
+
export default ReactComponent;
|
|
11
|
+
}
|
package/src/index.less
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
@import './component/common.less';
|
|
2
|
+
|
|
3
|
+
@layout-space: 22px;
|
|
4
|
+
|
|
5
|
+
html,
|
|
6
|
+
body {
|
|
7
|
+
padding: 0;
|
|
8
|
+
margin: 0;
|
|
9
|
+
font-family: PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
|
|
10
|
+
font-size: 14px;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// local debug
|
|
14
|
+
|
|
15
|
+
:root {
|
|
16
|
+
--modern-sidebar-width: 0 !important;
|
|
17
|
+
--modern-aside-width: 0 !important;
|
|
18
|
+
--modern-preview-padding:0 !important;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.modern-doc-layout,
|
|
22
|
+
.modern-doc {
|
|
23
|
+
width: 100% !important;
|
|
24
|
+
margin: 0 !important;
|
|
25
|
+
padding: 0 !important;
|
|
26
|
+
height: 100vh;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.modern-sidebar,
|
|
30
|
+
header.w-full {
|
|
31
|
+
display: none !important;
|
|
32
|
+
}
|
|
33
|
+
.modern-doc-container{
|
|
34
|
+
padding: 0 !important;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
footer.mt-8{
|
|
38
|
+
display: none;
|
|
39
|
+
}
|
|
40
|
+
// ----------
|
|
41
|
+
|
|
42
|
+
.page-container {
|
|
43
|
+
display: flex;
|
|
44
|
+
flex-direction: column;
|
|
45
|
+
height: 100%;
|
|
46
|
+
color: #000;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.ant-layout {
|
|
50
|
+
flex-grow: 1;
|
|
51
|
+
height: 100%;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.main-right {
|
|
55
|
+
display: flex;
|
|
56
|
+
flex-direction: column;
|
|
57
|
+
width: 100%;
|
|
58
|
+
height: 100%;
|
|
59
|
+
box-sizing: border-box;
|
|
60
|
+
// padding: @layout-space;
|
|
61
|
+
|
|
62
|
+
.main-content{
|
|
63
|
+
display: flex;
|
|
64
|
+
flex-direction: row;
|
|
65
|
+
flex-grow: 1;
|
|
66
|
+
overflow: hidden;
|
|
67
|
+
background: @side-bg;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
&.uploader-wrapper {
|
|
71
|
+
box-sizing: border-box;
|
|
72
|
+
margin: auto;
|
|
73
|
+
max-width: 800px;
|
|
74
|
+
flex-direction: column;
|
|
75
|
+
justify-content: center;
|
|
76
|
+
|
|
77
|
+
.uploader {
|
|
78
|
+
width: 100%;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.demo-loader {
|
|
82
|
+
width: 100%;
|
|
83
|
+
text-align: center;
|
|
84
|
+
margin-top: 10px;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.main-canvas-container {
|
|
89
|
+
flex-grow: 1;
|
|
90
|
+
height: 100%;
|
|
91
|
+
background: #F5F5F5;
|
|
92
|
+
overflow-x: hidden;
|
|
93
|
+
overflow-y: scroll;
|
|
94
|
+
border-left: 1px solid @border-color;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.main-side {
|
|
98
|
+
box-sizing: border-box;
|
|
99
|
+
// margin-left: @layout-space;
|
|
100
|
+
// margin: calc(@layout-space/2);
|
|
101
|
+
// padding-left: @layout-padding;
|
|
102
|
+
overflow-y: scroll;
|
|
103
|
+
|
|
104
|
+
// flex-basis: 380px; /* Set the fixed width */
|
|
105
|
+
// flex-grow: 0; /* Prevent it from growing */
|
|
106
|
+
// flex-shrink: 0; /* Prevent it from shrinking */
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.json-content {
|
|
110
|
+
word-wrap: break-word;
|
|
111
|
+
white-space: pre-wrap;
|
|
112
|
+
}
|
|
113
|
+
}
|