@hyperframes/engine 0.4.5 → 0.4.7
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/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +9 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/services/browserManager.d.ts.map +1 -1
- package/dist/services/browserManager.js +6 -3
- package/dist/services/browserManager.js.map +1 -1
- package/dist/services/chunkEncoder.d.ts +14 -7
- package/dist/services/chunkEncoder.d.ts.map +1 -1
- package/dist/services/chunkEncoder.js +25 -9
- package/dist/services/chunkEncoder.js.map +1 -1
- package/dist/services/chunkEncoder.types.d.ts +4 -0
- package/dist/services/chunkEncoder.types.d.ts.map +1 -1
- package/dist/services/frameCapture.d.ts.map +1 -1
- package/dist/services/frameCapture.js +18 -2
- package/dist/services/frameCapture.js.map +1 -1
- package/dist/services/hdrCapture.d.ts +62 -0
- package/dist/services/hdrCapture.d.ts.map +1 -0
- package/dist/services/hdrCapture.js +259 -0
- package/dist/services/hdrCapture.js.map +1 -0
- package/dist/services/screenshotService.d.ts +34 -0
- package/dist/services/screenshotService.d.ts.map +1 -1
- package/dist/services/screenshotService.js +97 -19
- package/dist/services/screenshotService.js.map +1 -1
- package/dist/services/streamingEncoder.d.ts +18 -3
- package/dist/services/streamingEncoder.d.ts.map +1 -1
- package/dist/services/streamingEncoder.js +104 -50
- package/dist/services/streamingEncoder.js.map +1 -1
- package/dist/services/videoFrameExtractor.d.ts.map +1 -1
- package/dist/services/videoFrameExtractor.js +109 -18
- package/dist/services/videoFrameExtractor.js.map +1 -1
- package/dist/services/videoFrameInjector.d.ts +54 -0
- package/dist/services/videoFrameInjector.d.ts.map +1 -1
- package/dist/services/videoFrameInjector.js +159 -0
- package/dist/services/videoFrameInjector.js.map +1 -1
- package/dist/utils/alphaBlit.d.ts +105 -0
- package/dist/utils/alphaBlit.d.ts.map +1 -0
- package/dist/utils/alphaBlit.js +550 -0
- package/dist/utils/alphaBlit.js.map +1 -0
- package/dist/utils/ffprobe.d.ts +10 -0
- package/dist/utils/ffprobe.d.ts.map +1 -1
- package/dist/utils/ffprobe.js +7 -0
- package/dist/utils/ffprobe.js.map +1 -1
- package/dist/utils/hdr.d.ts +82 -0
- package/dist/utils/hdr.d.ts.map +1 -0
- package/dist/utils/hdr.js +87 -0
- package/dist/utils/hdr.js.map +1 -0
- package/dist/utils/layerCompositor.d.ts +36 -0
- package/dist/utils/layerCompositor.d.ts.map +1 -0
- package/dist/utils/layerCompositor.js +49 -0
- package/dist/utils/layerCompositor.js.map +1 -0
- package/package.json +3 -2
- package/src/config.ts +16 -0
- package/src/index.ts +42 -0
- package/src/services/browserManager.ts +6 -3
- package/src/services/chunkEncoder.test.ts +88 -0
- package/src/services/chunkEncoder.ts +35 -9
- package/src/services/chunkEncoder.types.ts +3 -0
- package/src/services/frameCapture.ts +31 -4
- package/src/services/hdrCapture.test.ts +159 -0
- package/src/services/hdrCapture.ts +354 -0
- package/src/services/screenshotService.ts +102 -17
- package/src/services/streamingEncoder.test.ts +228 -0
- package/src/services/streamingEncoder.ts +153 -63
- package/src/services/videoFrameExtractor.ts +135 -31
- package/src/services/videoFrameInjector.ts +200 -0
- package/src/utils/alphaBlit.test.ts +993 -0
- package/src/utils/alphaBlit.ts +643 -0
- package/src/utils/ffprobe.ts +22 -0
- package/src/utils/hdr.test.ts +191 -0
- package/src/utils/hdr.ts +137 -0
- package/src/utils/layerCompositor.test.ts +141 -0
- package/src/utils/layerCompositor.ts +58 -0
- package/tsconfig.json +2 -1
|
@@ -10,7 +10,9 @@ import { existsSync, mkdirSync, readdirSync, rmSync } from "fs";
|
|
|
10
10
|
import { join } from "path";
|
|
11
11
|
import { parseHTML } from "linkedom";
|
|
12
12
|
import { extractVideoMetadata, type VideoMetadata } from "../utils/ffprobe.js";
|
|
13
|
+
import { isHdrColorSpace as isHdrColorSpaceUtil } from "../utils/hdr.js";
|
|
13
14
|
import { downloadToTemp, isHttpUrl } from "../utils/urlDownloader.js";
|
|
15
|
+
import { runFfmpeg } from "../utils/runFfmpeg.js";
|
|
14
16
|
import { DEFAULT_CONFIG, type EngineConfig } from "../config.js";
|
|
15
17
|
|
|
16
18
|
export interface VideoElement {
|
|
@@ -114,18 +116,28 @@ export async function extractVideoFramesRange(
|
|
|
114
116
|
const framePattern = `frame_%05d.${format}`;
|
|
115
117
|
const outputPattern = join(videoOutputDir, framePattern);
|
|
116
118
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
"-
|
|
127
|
-
|
|
128
|
-
|
|
119
|
+
// When extracting from HDR source, tone-map to SDR in FFmpeg rather than
|
|
120
|
+
// letting Chrome's uncontrollable tone-mapper handle it (which washes out).
|
|
121
|
+
// macOS: VideoToolbox hardware decoder does HDR→SDR natively on Apple Silicon.
|
|
122
|
+
// Linux: zscale filter (when available) or colorspace filter as fallback.
|
|
123
|
+
const isHdr = isHdrColorSpaceUtil(metadata.colorSpace);
|
|
124
|
+
const isMacOS = process.platform === "darwin";
|
|
125
|
+
|
|
126
|
+
const args: string[] = [];
|
|
127
|
+
if (isHdr && isMacOS) {
|
|
128
|
+
args.push("-hwaccel", "videotoolbox");
|
|
129
|
+
}
|
|
130
|
+
args.push("-ss", String(startTime), "-i", videoPath, "-t", String(duration));
|
|
131
|
+
|
|
132
|
+
const vfFilters: string[] = [];
|
|
133
|
+
if (isHdr && isMacOS) {
|
|
134
|
+
// VideoToolbox tone-maps during decode; force output to bt709 SDR format
|
|
135
|
+
vfFilters.push("format=nv12");
|
|
136
|
+
}
|
|
137
|
+
vfFilters.push(`fps=${fps}`);
|
|
138
|
+
args.push("-vf", vfFilters.join(","));
|
|
139
|
+
|
|
140
|
+
args.push("-q:v", format === "jpg" ? String(Math.ceil((100 - quality) / 3)) : "0");
|
|
129
141
|
if (format === "png") args.push("-compression_level", "6");
|
|
130
142
|
args.push("-y", outputPattern);
|
|
131
143
|
|
|
@@ -195,6 +207,53 @@ export async function extractVideoFramesRange(
|
|
|
195
207
|
});
|
|
196
208
|
}
|
|
197
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Convert an SDR video to HDR color space (HLG / BT.2020) so it can be
|
|
212
|
+
* composited alongside HDR content without looking washed out.
|
|
213
|
+
*
|
|
214
|
+
* Uses zscale for color space conversion with a nominal peak luminance of
|
|
215
|
+
* 600 nits — high enough that SDR content doesn't appear too dark next to
|
|
216
|
+
* HDR, matching the approach used by HeyGen's Rio pipeline.
|
|
217
|
+
*/
|
|
218
|
+
async function convertSdrToHdr(
|
|
219
|
+
inputPath: string,
|
|
220
|
+
outputPath: string,
|
|
221
|
+
signal?: AbortSignal,
|
|
222
|
+
config?: Partial<Pick<EngineConfig, "ffmpegProcessTimeout">>,
|
|
223
|
+
): Promise<void> {
|
|
224
|
+
const timeout = config?.ffmpegProcessTimeout ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
|
|
225
|
+
|
|
226
|
+
const args = [
|
|
227
|
+
"-i",
|
|
228
|
+
inputPath,
|
|
229
|
+
"-vf",
|
|
230
|
+
"colorspace=all=bt2020:iall=bt709:range=tv",
|
|
231
|
+
"-color_primaries",
|
|
232
|
+
"bt2020",
|
|
233
|
+
"-color_trc",
|
|
234
|
+
"arib-std-b67",
|
|
235
|
+
"-colorspace",
|
|
236
|
+
"bt2020nc",
|
|
237
|
+
"-c:v",
|
|
238
|
+
"libx264",
|
|
239
|
+
"-preset",
|
|
240
|
+
"fast",
|
|
241
|
+
"-crf",
|
|
242
|
+
"16",
|
|
243
|
+
"-c:a",
|
|
244
|
+
"copy",
|
|
245
|
+
"-y",
|
|
246
|
+
outputPath,
|
|
247
|
+
];
|
|
248
|
+
|
|
249
|
+
const result = await runFfmpeg(args, { signal, timeout });
|
|
250
|
+
if (!result.success) {
|
|
251
|
+
throw new Error(
|
|
252
|
+
`SDR→HDR conversion failed (exit ${result.exitCode}): ${result.stderr.slice(-300)}`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
198
257
|
export async function extractAllVideoFrames(
|
|
199
258
|
videos: VideoElement[],
|
|
200
259
|
baseDir: string,
|
|
@@ -208,30 +267,75 @@ export async function extractAllVideoFrames(
|
|
|
208
267
|
const errors: Array<{ videoId: string; error: string }> = [];
|
|
209
268
|
let totalFramesExtracted = 0;
|
|
210
269
|
|
|
211
|
-
//
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
270
|
+
// Phase 1: Resolve paths and download remote videos
|
|
271
|
+
const resolvedVideos: Array<{ video: VideoElement; videoPath: string }> = [];
|
|
272
|
+
for (const video of videos) {
|
|
273
|
+
if (signal?.aborted) break;
|
|
274
|
+
try {
|
|
275
|
+
let videoPath = video.src;
|
|
276
|
+
if (!videoPath.startsWith("/") && !isHttpUrl(videoPath)) {
|
|
277
|
+
const fromCompiled = compiledDir ? join(compiledDir, videoPath) : null;
|
|
278
|
+
videoPath =
|
|
279
|
+
fromCompiled && existsSync(fromCompiled) ? fromCompiled : join(baseDir, videoPath);
|
|
216
280
|
}
|
|
217
|
-
try {
|
|
218
|
-
let videoPath = video.src;
|
|
219
|
-
if (!videoPath.startsWith("/") && !isHttpUrl(videoPath)) {
|
|
220
|
-
const fromCompiled = compiledDir ? join(compiledDir, videoPath) : null;
|
|
221
|
-
videoPath =
|
|
222
|
-
fromCompiled && existsSync(fromCompiled) ? fromCompiled : join(baseDir, videoPath);
|
|
223
|
-
}
|
|
224
281
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
282
|
+
if (isHttpUrl(videoPath)) {
|
|
283
|
+
const downloadDir = join(options.outputDir, "_downloads");
|
|
284
|
+
mkdirSync(downloadDir, { recursive: true });
|
|
285
|
+
videoPath = await downloadToTemp(videoPath, downloadDir);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (!existsSync(videoPath)) {
|
|
289
|
+
errors.push({ videoId: video.id, error: `Video file not found: ${videoPath}` });
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
resolvedVideos.push({ video, videoPath });
|
|
293
|
+
} catch (err) {
|
|
294
|
+
errors.push({ videoId: video.id, error: err instanceof Error ? err.message : String(err) });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
230
297
|
|
|
231
|
-
|
|
232
|
-
|
|
298
|
+
// Phase 2: Probe color spaces and normalize if mixed HDR/SDR
|
|
299
|
+
const videoColorSpaces = await Promise.all(
|
|
300
|
+
resolvedVideos.map(async ({ videoPath }) => {
|
|
301
|
+
const metadata = await extractVideoMetadata(videoPath);
|
|
302
|
+
return metadata.colorSpace;
|
|
303
|
+
}),
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
const hasAnyHdr = videoColorSpaces.some(isHdrColorSpaceUtil);
|
|
307
|
+
if (hasAnyHdr) {
|
|
308
|
+
const convertDir = join(options.outputDir, "_hdr_normalized");
|
|
309
|
+
mkdirSync(convertDir, { recursive: true });
|
|
310
|
+
|
|
311
|
+
for (let i = 0; i < resolvedVideos.length; i++) {
|
|
312
|
+
if (signal?.aborted) break;
|
|
313
|
+
const cs = videoColorSpaces[i] ?? null;
|
|
314
|
+
if (!isHdrColorSpaceUtil(cs)) {
|
|
315
|
+
// SDR video in a mixed timeline — convert to HDR color space
|
|
316
|
+
const entry = resolvedVideos[i];
|
|
317
|
+
if (!entry) continue;
|
|
318
|
+
const convertedPath = join(convertDir, `${entry.video.id}_hdr.mp4`);
|
|
319
|
+
try {
|
|
320
|
+
await convertSdrToHdr(entry.videoPath, convertedPath, signal, config);
|
|
321
|
+
entry.videoPath = convertedPath;
|
|
322
|
+
} catch (err) {
|
|
323
|
+
errors.push({
|
|
324
|
+
videoId: entry.video.id,
|
|
325
|
+
error: `SDR→HDR conversion failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
326
|
+
});
|
|
233
327
|
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
234
331
|
|
|
332
|
+
// Phase 3: Extract frames (parallel)
|
|
333
|
+
const results = await Promise.all(
|
|
334
|
+
resolvedVideos.map(async ({ video, videoPath }) => {
|
|
335
|
+
if (signal?.aborted) {
|
|
336
|
+
throw new Error("Video frame extraction cancelled");
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
235
339
|
let videoDuration = video.end - video.start;
|
|
236
340
|
|
|
237
341
|
// Fallback: if no data-duration/data-end was specified (end is Infinity or 0),
|
|
@@ -117,3 +117,203 @@ export function createVideoFrameInjector(
|
|
|
117
117
|
}
|
|
118
118
|
};
|
|
119
119
|
}
|
|
120
|
+
|
|
121
|
+
// ── HDR compositing utilities ─────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Bounds and transform of a video element, queried from Chrome each frame.
|
|
125
|
+
* Used by the two-pass HDR compositing pipeline to position native HDR frames.
|
|
126
|
+
*/
|
|
127
|
+
export interface VideoElementBounds {
|
|
128
|
+
videoId: string;
|
|
129
|
+
x: number;
|
|
130
|
+
y: number;
|
|
131
|
+
width: number;
|
|
132
|
+
height: number;
|
|
133
|
+
opacity: number;
|
|
134
|
+
/** CSS transform matrix as a DOMMatrix-compatible string, e.g. "matrix(1,0,0,1,0,0)" */
|
|
135
|
+
transform: string;
|
|
136
|
+
zIndex: number;
|
|
137
|
+
visible: boolean;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Hide specific video elements by ID. Used in Pass 1 of the HDR pipeline so
|
|
142
|
+
* Chrome screenshots only contain DOM content (text, overlays) with transparent
|
|
143
|
+
* holes where the HDR videos go.
|
|
144
|
+
*/
|
|
145
|
+
export async function hideVideoElements(page: Page, videoIds: string[]): Promise<void> {
|
|
146
|
+
if (videoIds.length === 0) return;
|
|
147
|
+
await page.evaluate((ids: string[]) => {
|
|
148
|
+
for (const id of ids) {
|
|
149
|
+
const el = document.getElementById(id) as HTMLVideoElement | null;
|
|
150
|
+
if (el) {
|
|
151
|
+
el.style.setProperty("visibility", "hidden", "important");
|
|
152
|
+
el.style.setProperty("opacity", "0", "important");
|
|
153
|
+
// Also hide the injected render frame image if present
|
|
154
|
+
const img = document.getElementById(`__render_frame_${id}__`);
|
|
155
|
+
if (img) img.style.setProperty("visibility", "hidden", "important");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}, videoIds);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Restore visibility of video elements after a DOM screenshot.
|
|
163
|
+
*/
|
|
164
|
+
export async function showVideoElements(page: Page, videoIds: string[]): Promise<void> {
|
|
165
|
+
if (videoIds.length === 0) return;
|
|
166
|
+
await page.evaluate((ids: string[]) => {
|
|
167
|
+
for (const id of ids) {
|
|
168
|
+
const el = document.getElementById(id) as HTMLVideoElement | null;
|
|
169
|
+
if (el) {
|
|
170
|
+
el.style.removeProperty("visibility");
|
|
171
|
+
el.style.removeProperty("opacity");
|
|
172
|
+
const img = document.getElementById(`__render_frame_${id}__`);
|
|
173
|
+
if (img) img.style.removeProperty("visibility");
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}, videoIds);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Query the current bounds, transform, and visibility of video elements.
|
|
181
|
+
* Called after seeking (so GSAP has moved things) but before the screenshot.
|
|
182
|
+
*/
|
|
183
|
+
export async function queryVideoElementBounds(
|
|
184
|
+
page: Page,
|
|
185
|
+
videoIds: string[],
|
|
186
|
+
): Promise<VideoElementBounds[]> {
|
|
187
|
+
if (videoIds.length === 0) return [];
|
|
188
|
+
return page.evaluate((ids: string[]): VideoElementBounds[] => {
|
|
189
|
+
return ids.map((id) => {
|
|
190
|
+
const el = document.getElementById(id) as HTMLVideoElement | null;
|
|
191
|
+
if (!el) {
|
|
192
|
+
return {
|
|
193
|
+
videoId: id,
|
|
194
|
+
x: 0,
|
|
195
|
+
y: 0,
|
|
196
|
+
width: 0,
|
|
197
|
+
height: 0,
|
|
198
|
+
opacity: 0,
|
|
199
|
+
transform: "none",
|
|
200
|
+
zIndex: 0,
|
|
201
|
+
visible: false,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
const rect = el.getBoundingClientRect();
|
|
205
|
+
const style = window.getComputedStyle(el);
|
|
206
|
+
const zIndex = parseInt(style.zIndex) || 0;
|
|
207
|
+
const opacity = parseFloat(style.opacity) || 1;
|
|
208
|
+
const transform = style.transform || "none";
|
|
209
|
+
const visible =
|
|
210
|
+
style.visibility !== "hidden" &&
|
|
211
|
+
style.display !== "none" &&
|
|
212
|
+
rect.width > 0 &&
|
|
213
|
+
rect.height > 0;
|
|
214
|
+
return {
|
|
215
|
+
videoId: id,
|
|
216
|
+
x: Math.round(rect.x),
|
|
217
|
+
y: Math.round(rect.y),
|
|
218
|
+
width: Math.round(rect.width),
|
|
219
|
+
height: Math.round(rect.height),
|
|
220
|
+
opacity,
|
|
221
|
+
transform,
|
|
222
|
+
zIndex,
|
|
223
|
+
visible,
|
|
224
|
+
};
|
|
225
|
+
});
|
|
226
|
+
}, videoIds);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Stacking info for a single timed element, used by the z-ordered layer compositor.
|
|
231
|
+
*/
|
|
232
|
+
export interface ElementStackingInfo {
|
|
233
|
+
id: string;
|
|
234
|
+
zIndex: number;
|
|
235
|
+
x: number;
|
|
236
|
+
y: number;
|
|
237
|
+
width: number;
|
|
238
|
+
height: number;
|
|
239
|
+
opacity: number;
|
|
240
|
+
visible: boolean;
|
|
241
|
+
isHdr: boolean;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Query Chrome for ALL timed elements' stacking context.
|
|
246
|
+
* Returns z-index, bounds, opacity, and whether each element is a native HDR video.
|
|
247
|
+
*
|
|
248
|
+
* Queries every element with `data-start` (not just videos) so the layer compositor
|
|
249
|
+
* can determine z-ordering between DOM content and HDR video elements.
|
|
250
|
+
*/
|
|
251
|
+
export async function queryElementStacking(
|
|
252
|
+
page: Page,
|
|
253
|
+
nativeHdrVideoIds: Set<string>,
|
|
254
|
+
): Promise<ElementStackingInfo[]> {
|
|
255
|
+
const hdrIds = Array.from(nativeHdrVideoIds);
|
|
256
|
+
return page.evaluate((hdrIdList: string[]): ElementStackingInfo[] => {
|
|
257
|
+
const hdrSet = new Set(hdrIdList);
|
|
258
|
+
const elements = document.querySelectorAll("[data-start]");
|
|
259
|
+
const results: ElementStackingInfo[] = [];
|
|
260
|
+
|
|
261
|
+
// Walk up the DOM to find the effective z-index from the nearest
|
|
262
|
+
// positioned ancestor with a z-index. CSS z-index only applies to
|
|
263
|
+
// positioned elements; video elements inside positioned wrappers
|
|
264
|
+
// inherit the wrapper's stacking context.
|
|
265
|
+
//
|
|
266
|
+
// ## Supported subset
|
|
267
|
+
//
|
|
268
|
+
// This implementation looks for explicit `z-index` on positioned
|
|
269
|
+
// (non-static) ancestors. It does NOT detect the CSS stacking contexts
|
|
270
|
+
// created implicitly by other properties — including `opacity < 1`,
|
|
271
|
+
// `transform`, `filter`, `will-change`, `isolation: isolate`, and
|
|
272
|
+
// `mix-blend-mode`. GSAP routinely sets `transform` on wrappers, which
|
|
273
|
+
// creates an implicit stacking context with auto z-index; an HDR video
|
|
274
|
+
// inside such a wrapper with no explicit z-index will return the
|
|
275
|
+
// wrapper-of-the-wrapper's z-index here, potentially reordering layers
|
|
276
|
+
// incorrectly relative to sibling stacking contexts.
|
|
277
|
+
//
|
|
278
|
+
// The workaround is to set explicit `z-index` on the positioned wrapper
|
|
279
|
+
// when you want it treated as a compositing layer root. This matches
|
|
280
|
+
// what compositions need to do anyway for deterministic z-ordering.
|
|
281
|
+
function getEffectiveZIndex(node: Element): number {
|
|
282
|
+
let current: Element | null = node;
|
|
283
|
+
while (current) {
|
|
284
|
+
const cs = window.getComputedStyle(current);
|
|
285
|
+
const pos = cs.position;
|
|
286
|
+
const z = parseInt(cs.zIndex);
|
|
287
|
+
if (!Number.isNaN(z) && pos !== "static") return z;
|
|
288
|
+
current = current.parentElement;
|
|
289
|
+
}
|
|
290
|
+
return 0;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
for (const el of elements) {
|
|
294
|
+
const id = el.id;
|
|
295
|
+
if (!id) continue;
|
|
296
|
+
const rect = el.getBoundingClientRect();
|
|
297
|
+
const style = window.getComputedStyle(el);
|
|
298
|
+
const zIndex = getEffectiveZIndex(el);
|
|
299
|
+
const opacity = parseFloat(style.opacity) || 1;
|
|
300
|
+
const visible =
|
|
301
|
+
style.visibility !== "hidden" &&
|
|
302
|
+
style.display !== "none" &&
|
|
303
|
+
rect.width > 0 &&
|
|
304
|
+
rect.height > 0;
|
|
305
|
+
results.push({
|
|
306
|
+
id,
|
|
307
|
+
zIndex,
|
|
308
|
+
x: Math.round(rect.x),
|
|
309
|
+
y: Math.round(rect.y),
|
|
310
|
+
width: Math.round(rect.width),
|
|
311
|
+
height: Math.round(rect.height),
|
|
312
|
+
opacity,
|
|
313
|
+
visible,
|
|
314
|
+
isHdr: hdrSet.has(id),
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
return results;
|
|
318
|
+
}, hdrIds);
|
|
319
|
+
}
|