deio-scroll 1.0.0
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/LICENSE +21 -0
- package/README.md +21 -0
- package/dist/api/server.d.ts +3 -0
- package/dist/api/server.d.ts.map +1 -0
- package/dist/api/server.js +393 -0
- package/dist/api/server.js.map +1 -0
- package/dist/artifacts/store.d.ts +24 -0
- package/dist/artifacts/store.d.ts.map +1 -0
- package/dist/artifacts/store.js +43 -0
- package/dist/artifacts/store.js.map +1 -0
- package/dist/browser/componentInteraction.d.ts +24 -0
- package/dist/browser/componentInteraction.d.ts.map +1 -0
- package/dist/browser/componentInteraction.js +328 -0
- package/dist/browser/componentInteraction.js.map +1 -0
- package/dist/browser/composition.d.ts +42 -0
- package/dist/browser/composition.d.ts.map +1 -0
- package/dist/browser/composition.js +132 -0
- package/dist/browser/composition.js.map +1 -0
- package/dist/browser/cookies.d.ts +3 -0
- package/dist/browser/cookies.d.ts.map +1 -0
- package/dist/browser/cookies.js +20 -0
- package/dist/browser/cookies.js.map +1 -0
- package/dist/browser/curves.d.ts +4 -0
- package/dist/browser/curves.d.ts.map +1 -0
- package/dist/browser/curves.js +35 -0
- package/dist/browser/curves.js.map +1 -0
- package/dist/browser/detectScrollMode.d.ts +5 -0
- package/dist/browser/detectScrollMode.d.ts.map +1 -0
- package/dist/browser/detectScrollMode.js +61 -0
- package/dist/browser/detectScrollMode.js.map +1 -0
- package/dist/browser/directionGuardrails.d.ts +22 -0
- package/dist/browser/directionGuardrails.d.ts.map +1 -0
- package/dist/browser/directionGuardrails.js +99 -0
- package/dist/browser/directionGuardrails.js.map +1 -0
- package/dist/browser/goto.d.ts +9 -0
- package/dist/browser/goto.d.ts.map +1 -0
- package/dist/browser/goto.js +62 -0
- package/dist/browser/goto.js.map +1 -0
- package/dist/browser/hydrate.d.ts +8 -0
- package/dist/browser/hydrate.d.ts.map +1 -0
- package/dist/browser/hydrate.js +73 -0
- package/dist/browser/hydrate.js.map +1 -0
- package/dist/browser/mediaClock.d.ts +9 -0
- package/dist/browser/mediaClock.d.ts.map +1 -0
- package/dist/browser/mediaClock.js +86 -0
- package/dist/browser/mediaClock.js.map +1 -0
- package/dist/browser/motion.d.ts +21 -0
- package/dist/browser/motion.d.ts.map +1 -0
- package/dist/browser/motion.js +42 -0
- package/dist/browser/motion.js.map +1 -0
- package/dist/browser/networkPolicy.d.ts +11 -0
- package/dist/browser/networkPolicy.d.ts.map +1 -0
- package/dist/browser/networkPolicy.js +119 -0
- package/dist/browser/networkPolicy.js.map +1 -0
- package/dist/browser/prime.d.ts +3 -0
- package/dist/browser/prime.d.ts.map +1 -0
- package/dist/browser/prime.js +13 -0
- package/dist/browser/prime.js.map +1 -0
- package/dist/browser/sanitize.d.ts +3 -0
- package/dist/browser/sanitize.d.ts.map +1 -0
- package/dist/browser/sanitize.js +37 -0
- package/dist/browser/sanitize.js.map +1 -0
- package/dist/browser/scroll.d.ts +29 -0
- package/dist/browser/scroll.d.ts.map +1 -0
- package/dist/browser/scroll.js +420 -0
- package/dist/browser/scroll.js.map +1 -0
- package/dist/browser/scrollEasing.d.ts +5 -0
- package/dist/browser/scrollEasing.d.ts.map +1 -0
- package/dist/browser/scrollEasing.js +40 -0
- package/dist/browser/scrollEasing.js.map +1 -0
- package/dist/browser/scrollReadiness.d.ts +25 -0
- package/dist/browser/scrollReadiness.d.ts.map +1 -0
- package/dist/browser/scrollReadiness.js +105 -0
- package/dist/browser/scrollReadiness.js.map +1 -0
- package/dist/browser/urlGuard.d.ts +4 -0
- package/dist/browser/urlGuard.d.ts.map +1 -0
- package/dist/browser/urlGuard.js +22 -0
- package/dist/browser/urlGuard.js.map +1 -0
- package/dist/browser/virtualScroll.d.ts +19 -0
- package/dist/browser/virtualScroll.d.ts.map +1 -0
- package/dist/browser/virtualScroll.js +50 -0
- package/dist/browser/virtualScroll.js.map +1 -0
- package/dist/capture/detectMargins.d.ts +6 -0
- package/dist/capture/detectMargins.d.ts.map +1 -0
- package/dist/capture/detectMargins.js +145 -0
- package/dist/capture/detectMargins.js.map +1 -0
- package/dist/capture/frameRecorder.d.ts +29 -0
- package/dist/capture/frameRecorder.d.ts.map +1 -0
- package/dist/capture/frameRecorder.js +74 -0
- package/dist/capture/frameRecorder.js.map +1 -0
- package/dist/capture/stitchFrames.d.ts +9 -0
- package/dist/capture/stitchFrames.d.ts.map +1 -0
- package/dist/capture/stitchFrames.js +62 -0
- package/dist/capture/stitchFrames.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +329 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/browserLaunch.d.ts +13 -0
- package/dist/config/browserLaunch.d.ts.map +1 -0
- package/dist/config/browserLaunch.js +63 -0
- package/dist/config/browserLaunch.js.map +1 -0
- package/dist/config/recordingProfile.d.ts +10 -0
- package/dist/config/recordingProfile.d.ts.map +1 -0
- package/dist/config/recordingProfile.js +25 -0
- package/dist/config/recordingProfile.js.map +1 -0
- package/dist/config/runtimeMode.d.ts +2 -0
- package/dist/config/runtimeMode.d.ts.map +1 -0
- package/dist/config/runtimeMode.js +4 -0
- package/dist/config/runtimeMode.js.map +1 -0
- package/dist/config/virtualScroll.d.ts +8 -0
- package/dist/config/virtualScroll.d.ts.map +1 -0
- package/dist/config/virtualScroll.js +19 -0
- package/dist/config/virtualScroll.js.map +1 -0
- package/dist/contracts.d.ts +157 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +136 -0
- package/dist/contracts.js.map +1 -0
- package/dist/editor/compileVideo.d.ts +21 -0
- package/dist/editor/compileVideo.d.ts.map +1 -0
- package/dist/editor/compileVideo.js +104 -0
- package/dist/editor/compileVideo.js.map +1 -0
- package/dist/editor/composeComparison.d.ts +30 -0
- package/dist/editor/composeComparison.d.ts.map +1 -0
- package/dist/editor/composeComparison.js +315 -0
- package/dist/editor/composeComparison.js.map +1 -0
- package/dist/editor/composeResponsiveness.d.ts +19 -0
- package/dist/editor/composeResponsiveness.d.ts.map +1 -0
- package/dist/editor/composeResponsiveness.js +198 -0
- package/dist/editor/composeResponsiveness.js.map +1 -0
- package/dist/editor/frameVideo.d.ts +11 -0
- package/dist/editor/frameVideo.d.ts.map +1 -0
- package/dist/editor/frameVideo.js +181 -0
- package/dist/editor/frameVideo.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/inspection/inspectWebsite.d.ts +21 -0
- package/dist/inspection/inspectWebsite.d.ts.map +1 -0
- package/dist/inspection/inspectWebsite.js +412 -0
- package/dist/inspection/inspectWebsite.js.map +1 -0
- package/dist/jobs/manager.d.ts +123 -0
- package/dist/jobs/manager.d.ts.map +1 -0
- package/dist/jobs/manager.js +378 -0
- package/dist/jobs/manager.js.map +1 -0
- package/dist/jobs/store.d.ts +150 -0
- package/dist/jobs/store.d.ts.map +1 -0
- package/dist/jobs/store.js +446 -0
- package/dist/jobs/store.js.map +1 -0
- package/dist/jobs/thumbnail.d.ts +2 -0
- package/dist/jobs/thumbnail.d.ts.map +1 -0
- package/dist/jobs/thumbnail.js +36 -0
- package/dist/jobs/thumbnail.js.map +1 -0
- package/dist/pipeline/processRecordingRequest.d.ts +4 -0
- package/dist/pipeline/processRecordingRequest.d.ts.map +1 -0
- package/dist/pipeline/processRecordingRequest.js +17 -0
- package/dist/pipeline/processRecordingRequest.js.map +1 -0
- package/dist/pipeline/recordComparison.d.ts +4 -0
- package/dist/pipeline/recordComparison.d.ts.map +1 -0
- package/dist/pipeline/recordComparison.js +99 -0
- package/dist/pipeline/recordComparison.js.map +1 -0
- package/dist/pipeline/recordResponsiveness.d.ts +4 -0
- package/dist/pipeline/recordResponsiveness.d.ts.map +1 -0
- package/dist/pipeline/recordResponsiveness.js +122 -0
- package/dist/pipeline/recordResponsiveness.js.map +1 -0
- package/dist/pipeline/recordWebsite.d.ts +11 -0
- package/dist/pipeline/recordWebsite.d.ts.map +1 -0
- package/dist/pipeline/recordWebsite.js +446 -0
- package/dist/pipeline/recordWebsite.js.map +1 -0
- package/dist/pipeline/styleRecording.d.ts +12 -0
- package/dist/pipeline/styleRecording.d.ts.map +1 -0
- package/dist/pipeline/styleRecording.js +59 -0
- package/dist/pipeline/styleRecording.js.map +1 -0
- package/dist/transcode/ffmpeg.d.ts +4 -0
- package/dist/transcode/ffmpeg.d.ts.map +1 -0
- package/dist/transcode/ffmpeg.js +62 -0
- package/dist/transcode/ffmpeg.js.map +1 -0
- package/dist/transcode/probe.d.ts +7 -0
- package/dist/transcode/probe.d.ts.map +1 -0
- package/dist/transcode/probe.js +91 -0
- package/dist/transcode/probe.js.map +1 -0
- package/dist/transcode/quality.d.ts +8 -0
- package/dist/transcode/quality.d.ts.map +1 -0
- package/dist/transcode/quality.js +13 -0
- package/dist/transcode/quality.js.map +1 -0
- package/dist/types.d.ts +302 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/worker.d.ts +3 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +22 -0
- package/dist/worker.js.map +1 -0
- package/package.json +63 -0
- package/public/assets/index-C63t4P1L.css +1 -0
- package/public/assets/index-Sfgf7dSH.js +9 -0
- package/public/background_presets/gray_noise_gradient.png +0 -0
- package/public/background_presets/paper_blue.png +0 -0
- package/public/background_presets/red_blocks_gradient.png +0 -0
- package/public/deio-scroll-mark.svg +12 -0
- package/public/favicon.svg +1 -0
- package/public/icons.svg +24 -0
- package/public/index.html +16 -0
- package/public/scrollizard-mark.png +0 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { probeVideoDurationMs, probeVideoFps, probeVideoSize } from "../transcode/probe.js";
|
|
7
|
+
const PRESET_FILES = {
|
|
8
|
+
gray_noise_gradient: "gray_noise_gradient.png",
|
|
9
|
+
paper_blue: "paper_blue.png",
|
|
10
|
+
red_blocks_gradient: "red_blocks_gradient.png",
|
|
11
|
+
};
|
|
12
|
+
const CONTENT_SCALE = 0.84;
|
|
13
|
+
export async function frameVideoOnBackground(options) {
|
|
14
|
+
const { inputPath, outputPath, preset, addShadow, roundedCorners = false, encode } = options;
|
|
15
|
+
const size = await probeVideoSize(inputPath);
|
|
16
|
+
if (!size)
|
|
17
|
+
throw new Error("Could not read video dimensions for background export");
|
|
18
|
+
const fps = (await probeVideoFps(inputPath)) ?? 30;
|
|
19
|
+
const durationMs = await probeVideoDurationMs(inputPath);
|
|
20
|
+
const width = even(size.width);
|
|
21
|
+
const height = even(size.height);
|
|
22
|
+
const contentWidth = even(Math.round(width * CONTENT_SCALE));
|
|
23
|
+
const contentHeight = even(Math.round(height * CONTENT_SCALE));
|
|
24
|
+
const x = Math.floor((width - contentWidth) / 2);
|
|
25
|
+
const y = Math.floor((height - contentHeight) / 2);
|
|
26
|
+
const cornerRadius = roundedCorners
|
|
27
|
+
? Math.max(6, Math.round(Math.min(contentWidth, contentHeight) * 0.014))
|
|
28
|
+
: 0;
|
|
29
|
+
const backgroundPath = await resolvePresetPath(PRESET_FILES[preset]);
|
|
30
|
+
// Corners and shadows do not change from frame to frame. Building them once
|
|
31
|
+
// avoids running geq/gblur thousands of times on long recordings.
|
|
32
|
+
const staticLayers = await createStaticLayers({
|
|
33
|
+
outputPath,
|
|
34
|
+
width,
|
|
35
|
+
height,
|
|
36
|
+
contentWidth,
|
|
37
|
+
contentHeight,
|
|
38
|
+
x,
|
|
39
|
+
y,
|
|
40
|
+
cornerRadius,
|
|
41
|
+
addShadow,
|
|
42
|
+
});
|
|
43
|
+
try {
|
|
44
|
+
const filters = [
|
|
45
|
+
`[1:v]scale=${width}:${height}:force_original_aspect_ratio=increase,crop=${width}:${height},setsar=1[bg]`,
|
|
46
|
+
`[0:v]scale=${contentWidth}:${contentHeight}:flags=lanczos,setsar=1,format=rgba[card-base]`,
|
|
47
|
+
];
|
|
48
|
+
const inputs = ["-y", "-i", inputPath, "-loop", "1", "-framerate", String(fps), "-i", backgroundPath];
|
|
49
|
+
let inputIndex = 2;
|
|
50
|
+
if (staticLayers.maskPath) {
|
|
51
|
+
inputs.push("-loop", "1", "-framerate", String(fps), "-i", staticLayers.maskPath);
|
|
52
|
+
filters.push(`[${inputIndex}:v]format=gray[mask]`, `[card-base][mask]alphamerge[card]`);
|
|
53
|
+
inputIndex += 1;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
filters.push("[card-base]copy[card]");
|
|
57
|
+
}
|
|
58
|
+
if (staticLayers.shadowPath) {
|
|
59
|
+
inputs.push("-loop", "1", "-framerate", String(fps), "-i", staticLayers.shadowPath);
|
|
60
|
+
filters.push(`[${inputIndex}:v]format=rgba[shadow]`, "[bg][shadow]overlay=0:0[canvas]", `[canvas][card]overlay=${x}:${y}:shortest=1,fps=${fps},format=yuv420p[output]`);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
filters.push(`[bg][card]overlay=${x}:${y}:shortest=1,fps=${fps},format=yuv420p[output]`);
|
|
64
|
+
}
|
|
65
|
+
await runFfmpeg([
|
|
66
|
+
...inputs,
|
|
67
|
+
"-filter_complex",
|
|
68
|
+
filters.join(";"),
|
|
69
|
+
"-map",
|
|
70
|
+
"[output]",
|
|
71
|
+
"-an",
|
|
72
|
+
"-c:v",
|
|
73
|
+
"libx264",
|
|
74
|
+
"-preset",
|
|
75
|
+
encode.preset,
|
|
76
|
+
"-crf",
|
|
77
|
+
String(encode.crf),
|
|
78
|
+
"-pix_fmt",
|
|
79
|
+
"yuv420p",
|
|
80
|
+
...(durationMs ? ["-t", (durationMs / 1000).toFixed(3)] : []),
|
|
81
|
+
"-shortest",
|
|
82
|
+
"-movflags",
|
|
83
|
+
"+faststart",
|
|
84
|
+
outputPath,
|
|
85
|
+
]);
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
await Promise.all(staticLayers.files.map((file) => fs.unlink(file).catch(() => undefined)));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function createStaticLayers(options) {
|
|
92
|
+
const { outputPath, width, height, contentWidth, contentHeight, x, y, cornerRadius, addShadow } = options;
|
|
93
|
+
if (!cornerRadius && !addShadow)
|
|
94
|
+
return { files: [], maskPath: undefined, shadowPath: undefined };
|
|
95
|
+
const id = randomUUID();
|
|
96
|
+
const dir = path.dirname(outputPath);
|
|
97
|
+
const maskPath = path.join(dir, `.style-mask-${id}.png`);
|
|
98
|
+
const shadowPath = addShadow ? path.join(dir, `.style-shadow-${id}.png`) : undefined;
|
|
99
|
+
const maskFilter = cornerRadius ? roundedMaskFilter(cornerRadius) : "format=gray";
|
|
100
|
+
await runFfmpeg([
|
|
101
|
+
"-y", "-f", "lavfi", "-i", `color=c=white:s=${contentWidth}x${contentHeight}:r=1`,
|
|
102
|
+
"-vf", maskFilter,
|
|
103
|
+
"-frames:v", "1", "-c:v", "png", maskPath,
|
|
104
|
+
]);
|
|
105
|
+
if (shadowPath) {
|
|
106
|
+
// Soft bottom drop-shadow: mainly below the card so it reads on light and busy
|
|
107
|
+
// backgrounds. Slight X inset + wider blur keeps it natural without a hard halo.
|
|
108
|
+
const shadowOffsetY = Math.max(22, Math.round(height * 0.032));
|
|
109
|
+
const shadowY = y + shadowOffsetY;
|
|
110
|
+
const shadowBlur = Math.max(28, Math.round(Math.min(width, height) * 0.034));
|
|
111
|
+
const shadowAlpha = 0.38;
|
|
112
|
+
// Grow the shadow plate a touch so blur doesn't thin out at the sides.
|
|
113
|
+
const shadowPadX = Math.max(8, Math.round(contentWidth * 0.012));
|
|
114
|
+
const shadowW = even(contentWidth + shadowPadX * 2);
|
|
115
|
+
const shadowH = even(contentHeight + Math.round(shadowOffsetY * 0.35));
|
|
116
|
+
const shadowX = Math.max(0, x - shadowPadX);
|
|
117
|
+
await runFfmpeg([
|
|
118
|
+
"-y", "-i", maskPath,
|
|
119
|
+
"-f", "lavfi", "-i", `color=c=black:s=${contentWidth}x${contentHeight}:r=1`,
|
|
120
|
+
"-filter_complex",
|
|
121
|
+
// Soft plate → pad into full frame (offset down) → opacity → blur
|
|
122
|
+
`[0:v]format=gray,scale=${shadowW}:${shadowH}:flags=bilinear[mask];` +
|
|
123
|
+
`[1:v]format=rgba,scale=${shadowW}:${shadowH}:flags=bilinear[black];` +
|
|
124
|
+
`[black][mask]alphamerge,` +
|
|
125
|
+
`pad=${width}:${height}:${shadowX}:${shadowY}:color=black@0,` +
|
|
126
|
+
`format=rgba,colorchannelmixer=aa=${shadowAlpha},` +
|
|
127
|
+
`gblur=sigma=${shadowBlur}:steps=3[output]`,
|
|
128
|
+
"-map", "[output]", "-frames:v", "1", "-c:v", "png", shadowPath,
|
|
129
|
+
]);
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
files: shadowPath ? [maskPath, shadowPath] : [maskPath],
|
|
133
|
+
maskPath: cornerRadius ? maskPath : undefined,
|
|
134
|
+
shadowPath,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function roundedMaskFilter(radius) {
|
|
138
|
+
const right = `W-${radius}`;
|
|
139
|
+
const bottom = `H-${radius}`;
|
|
140
|
+
const outsideCorner = [
|
|
141
|
+
`lt(X,${radius})*lt(Y,${radius})*gt(hypot(X-${radius},Y-${radius}),${radius})`,
|
|
142
|
+
`gt(X,${right})*lt(Y,${radius})*gt(hypot(X-(${right}),Y-${radius}),${radius})`,
|
|
143
|
+
`lt(X,${radius})*gt(Y,${bottom})*gt(hypot(X-${radius},Y-(${bottom})),${radius})`,
|
|
144
|
+
`gt(X,${right})*gt(Y,${bottom})*gt(hypot(X-(${right}),Y-(${bottom})),${radius})`,
|
|
145
|
+
].join("+");
|
|
146
|
+
return `format=gray,geq=lum='if(gt(${outsideCorner},0),0,255)'`;
|
|
147
|
+
}
|
|
148
|
+
async function resolvePresetPath(filename) {
|
|
149
|
+
const apiPublicDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../public/background_presets");
|
|
150
|
+
const builtPath = path.join(apiPublicDir, filename);
|
|
151
|
+
if (await exists(builtPath))
|
|
152
|
+
return builtPath;
|
|
153
|
+
// During Vite development, its public directory is the authoritative source.
|
|
154
|
+
const webPublicPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../web/public/background_presets", filename);
|
|
155
|
+
if (await exists(webPublicPath))
|
|
156
|
+
return webPublicPath;
|
|
157
|
+
throw new Error(`Background preset is unavailable: ${filename}`);
|
|
158
|
+
}
|
|
159
|
+
function even(value) {
|
|
160
|
+
return value % 2 === 0 ? value : value - 1;
|
|
161
|
+
}
|
|
162
|
+
async function exists(filePath) {
|
|
163
|
+
return fs.access(filePath).then(() => true).catch(() => false);
|
|
164
|
+
}
|
|
165
|
+
function runFfmpeg(args) {
|
|
166
|
+
return new Promise((resolve, reject) => {
|
|
167
|
+
const child = spawn("ffmpeg", args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
168
|
+
let stderr = "";
|
|
169
|
+
child.stderr.on("data", (chunk) => {
|
|
170
|
+
stderr += chunk.toString();
|
|
171
|
+
});
|
|
172
|
+
child.on("error", reject);
|
|
173
|
+
child.on("close", (code) => {
|
|
174
|
+
if (code === 0)
|
|
175
|
+
resolve();
|
|
176
|
+
else
|
|
177
|
+
reject(new Error(`Background render failed with code ${code}: ${stderr.slice(-2000)}`));
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
//# sourceMappingURL=frameVideo.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frameVideo.js","sourceRoot":"","sources":["../../src/editor/frameVideo.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA;AAC3D,mBAAmB,YAAY,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { WebsiteInspection, WebsiteSection } from "../types.js";
|
|
2
|
+
export interface RawWebsiteSection {
|
|
3
|
+
label: string;
|
|
4
|
+
selector: string;
|
|
5
|
+
kind: "heading" | "landmark";
|
|
6
|
+
y: number;
|
|
7
|
+
height: number;
|
|
8
|
+
stable: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function inspectWebsite(options: {
|
|
11
|
+
targetUrl: string;
|
|
12
|
+
viewport?: {
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
};
|
|
16
|
+
}): Promise<WebsiteInspection>;
|
|
17
|
+
export declare function normalizeInspectionSections(raw: RawWebsiteSection[], safeViewport: {
|
|
18
|
+
topInsetPx: number;
|
|
19
|
+
bottomInsetPx: number;
|
|
20
|
+
}, viewportHeight: number, maxScroll: number): WebsiteSection[];
|
|
21
|
+
//# sourceMappingURL=inspectWebsite.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inspectWebsite.d.ts","sourceRoot":"","sources":["../../src/inspection/inspectWebsite.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,iBAAiB,EAEjB,cAAc,EACf,MAAM,aAAa,CAAC;AAGrB,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,SAAS,GAAG,UAAU,CAAC;IAC7B,CAAC,EAAE,MAAM,CAAC;IACV,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAsB,cAAc,CAAC,OAAO,EAAE;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9C,GAAG,OAAO,CAAC,iBAAiB,CAAC,CA2E7B;AAwED,wBAAgB,2BAA2B,CACzC,GAAG,EAAE,iBAAiB,EAAE,EACxB,YAAY,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,EAC3D,cAAc,EAAE,MAAM,EACtB,SAAS,EAAE,MAAM,GAChB,cAAc,EAAE,CAoFlB"}
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { chromium } from "playwright";
|
|
2
|
+
import { gotoReachablePage } from "../browser/goto.js";
|
|
3
|
+
export async function inspectWebsite(options) {
|
|
4
|
+
const target = assertHttpUrl(options.targetUrl);
|
|
5
|
+
const viewport = options.viewport ?? { width: 1280, height: 720 };
|
|
6
|
+
const browser = await chromium.launch({ headless: true });
|
|
7
|
+
try {
|
|
8
|
+
const page = await browser.newPage({ viewport });
|
|
9
|
+
await gotoReachablePage(page, target.href);
|
|
10
|
+
await page
|
|
11
|
+
.waitForLoadState("networkidle", { timeout: 10_000 })
|
|
12
|
+
.catch(() => undefined);
|
|
13
|
+
const pageHeight = await page.evaluate(() => Math.max(document.body.scrollHeight, document.documentElement.scrollHeight));
|
|
14
|
+
const scrollMode = await detectInspectionScrollMode(page, pageHeight, viewport.height);
|
|
15
|
+
const safeViewport = await detectInspectionSafeViewport(page, viewport);
|
|
16
|
+
const sections = normalizeInspectionSections(await collectSections(page), safeViewport, viewport.height, Math.max(0, pageHeight - viewport.height));
|
|
17
|
+
const interactions = scrollMode === "document"
|
|
18
|
+
? await collectInteractionCandidates(page)
|
|
19
|
+
: [];
|
|
20
|
+
const { screenshots, storyboard } = scrollMode === "virtual"
|
|
21
|
+
? await takeVirtualStoryboardScreenshots(page, viewport)
|
|
22
|
+
: await takeDocumentStoryboardScreenshots(page, pageHeight, viewport.height);
|
|
23
|
+
if (scrollMode === "document") {
|
|
24
|
+
await attachSectionPreviewScreenshots(page, sections, screenshots, viewport.height);
|
|
25
|
+
}
|
|
26
|
+
const warnings = [];
|
|
27
|
+
if (page.url() !== target.href)
|
|
28
|
+
warnings.push(`Redirected to ${page.url()}`);
|
|
29
|
+
if (pageHeight <= viewport.height + 48 && scrollMode === "document") {
|
|
30
|
+
warnings.push("This page has little or no scrollable content.");
|
|
31
|
+
}
|
|
32
|
+
if (scrollMode === "virtual") {
|
|
33
|
+
warnings.push("Virtual scrolling detected; direction uses storyboard progress points.");
|
|
34
|
+
}
|
|
35
|
+
if (safeViewport.topInsetPx > 0) {
|
|
36
|
+
warnings.push(`A ${safeViewport.topInsetPx}px fixed header will be kept clear.`);
|
|
37
|
+
}
|
|
38
|
+
if (scrollMode === "document" && sections.length === 0) {
|
|
39
|
+
warnings.push("No semantic sections were detected; storyboard waypoints will be used.");
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
url: page.url(),
|
|
43
|
+
title: await page.title(),
|
|
44
|
+
pageHeight,
|
|
45
|
+
viewport,
|
|
46
|
+
scrollMode,
|
|
47
|
+
safeViewport,
|
|
48
|
+
sections,
|
|
49
|
+
interactions,
|
|
50
|
+
storyboard,
|
|
51
|
+
screenshots,
|
|
52
|
+
warnings,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
await browser.close();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function collectInteractionCandidates(page) {
|
|
60
|
+
return page.evaluate(`(() => {
|
|
61
|
+
const blocked = /\\b(delete|remove|unsubscribe|sign out|log out|purchase|buy|pay|checkout|submit|send|publish|confirm|destroy|upload|download)\\b/i;
|
|
62
|
+
const pathFor = (element) => {
|
|
63
|
+
if (element.id) return "#" + CSS.escape(element.id);
|
|
64
|
+
for (const name of ["data-testid", "data-test", "data-qa"]) {
|
|
65
|
+
const value = element.getAttribute(name);
|
|
66
|
+
if (value) return "[" + name + "=\\"" + CSS.escape(value) + "\\"]";
|
|
67
|
+
}
|
|
68
|
+
const segments = [];
|
|
69
|
+
let current = element;
|
|
70
|
+
while (current && current !== document.body && segments.length < 5) {
|
|
71
|
+
const parent = current.parentElement;
|
|
72
|
+
const tag = current.tagName.toLowerCase();
|
|
73
|
+
const siblings = parent ? Array.from(parent.children).filter((child) => child.tagName === current.tagName) : [];
|
|
74
|
+
const suffix = siblings.length > 1 ? ":nth-of-type(" + (siblings.indexOf(current) + 1) + ")" : "";
|
|
75
|
+
segments.unshift(tag + suffix);
|
|
76
|
+
current = parent;
|
|
77
|
+
}
|
|
78
|
+
return "body > " + segments.join(" > ");
|
|
79
|
+
};
|
|
80
|
+
return Array.from(document.querySelectorAll("button,a[href],[role='button'],[role='tab'],[role='switch'],[role='menuitem'],[aria-expanded],summary,input:not([type='hidden'])"))
|
|
81
|
+
.flatMap((element, index) => {
|
|
82
|
+
const rect = element.getBoundingClientRect();
|
|
83
|
+
const style = getComputedStyle(element);
|
|
84
|
+
const label = (element.getAttribute("aria-label") || element.getAttribute("title") || element.textContent || element.value || "").replace(/\\s+/g, " ").trim().slice(0, 120);
|
|
85
|
+
if (!label || blocked.test(label) || rect.width < 28 || rect.height < 20 || style.display === "none" || style.visibility === "hidden" || Number(style.opacity) < 0.05 || element.hasAttribute("disabled") || element.getAttribute("aria-disabled") === "true") return [];
|
|
86
|
+
const tag = element.tagName.toLowerCase();
|
|
87
|
+
const role = element.getAttribute("role") || (tag === "a" ? "link" : tag);
|
|
88
|
+
const type = (element.getAttribute("type") || "").toLowerCase();
|
|
89
|
+
const semanticClick = ["tab", "switch", "menuitem", "option"].includes(role) || element.hasAttribute("aria-expanded") || tag === "summary" || (tag === "button" && type === "button") || (tag === "button" && !element.closest("form") && !type);
|
|
90
|
+
const focusable = element.tabIndex >= 0 || ["button", "a", "input", "summary"].includes(tag);
|
|
91
|
+
const actions = ["hover", ...(focusable ? ["focus"] : []), ...(semanticClick && tag !== "a" ? ["click"] : [])];
|
|
92
|
+
const selector = pathFor(element);
|
|
93
|
+
const id = "interaction_" + String(index + 1).padStart(2, "0");
|
|
94
|
+
const maxScroll = Math.max(0, document.documentElement.scrollHeight - innerHeight);
|
|
95
|
+
const progress = maxScroll === 0 ? 0 : Math.max(0, Math.min(1, (rect.top + scrollY - innerHeight / 2) / maxScroll));
|
|
96
|
+
const recommendedZoomScale = rect.width > innerWidth * 0.4 ? 1.12 : 1.28;
|
|
97
|
+
return [{
|
|
98
|
+
id,
|
|
99
|
+
selector,
|
|
100
|
+
label,
|
|
101
|
+
tag,
|
|
102
|
+
role,
|
|
103
|
+
actions,
|
|
104
|
+
rect: {
|
|
105
|
+
x: Math.round(rect.left + window.scrollX),
|
|
106
|
+
y: Math.round(rect.top + window.scrollY),
|
|
107
|
+
width: Math.round(rect.width),
|
|
108
|
+
height: Math.round(rect.height),
|
|
109
|
+
},
|
|
110
|
+
recommendedTarget: { type: "selector", selector, align: "center", fallbackProgress: progress },
|
|
111
|
+
recommendedHoldMs: semanticClick ? 1600 : 1300,
|
|
112
|
+
recommendedZoomScale,
|
|
113
|
+
recommendedInteraction: {
|
|
114
|
+
action: "hover",
|
|
115
|
+
candidateId: id,
|
|
116
|
+
label,
|
|
117
|
+
role,
|
|
118
|
+
zoomScale: recommendedZoomScale,
|
|
119
|
+
showCursor: true,
|
|
120
|
+
},
|
|
121
|
+
}];
|
|
122
|
+
})
|
|
123
|
+
.slice(0, 30);
|
|
124
|
+
})()`);
|
|
125
|
+
}
|
|
126
|
+
export function normalizeInspectionSections(raw, safeViewport, viewportHeight, maxScroll) {
|
|
127
|
+
const safeTop = safeViewport.topInsetPx + 24;
|
|
128
|
+
const safeBottom = viewportHeight - safeViewport.bottomInsetPx;
|
|
129
|
+
const headingLeadPx = Math.round((safeBottom - safeTop) * 0.14);
|
|
130
|
+
const scored = raw
|
|
131
|
+
.map((section) => ({
|
|
132
|
+
...section,
|
|
133
|
+
score: (section.kind === "heading" ? 4 : 0) + section.stable * 2,
|
|
134
|
+
recommendedAlign: "top",
|
|
135
|
+
recommendedOffsetPx: section.kind === "heading" ? -headingLeadPx : 0,
|
|
136
|
+
targetY: Math.max(0, Math.min(maxScroll, section.kind === "heading"
|
|
137
|
+
? section.y - safeTop - headingLeadPx
|
|
138
|
+
: section.y - safeTop)),
|
|
139
|
+
}))
|
|
140
|
+
.sort((a, b) => a.y - b.y || b.score - a.score);
|
|
141
|
+
const selected = [];
|
|
142
|
+
for (const section of scored) {
|
|
143
|
+
const nearby = selected.findIndex((candidate) => Math.abs(candidate.y - section.y) <= 160);
|
|
144
|
+
if (nearby < 0)
|
|
145
|
+
selected.push(section);
|
|
146
|
+
else {
|
|
147
|
+
const candidate = selected[nearby];
|
|
148
|
+
const sameVisualRow = candidate.kind === "heading"
|
|
149
|
+
&& section.kind === "heading"
|
|
150
|
+
&& Math.abs(candidate.y - section.y) <= 48;
|
|
151
|
+
const candidateLabel = candidate.label.toLowerCase();
|
|
152
|
+
const sectionLabel = section.label.toLowerCase();
|
|
153
|
+
const distinctLabels = candidateLabel !== sectionLabel
|
|
154
|
+
&& !candidateLabel.includes(sectionLabel)
|
|
155
|
+
&& !sectionLabel.includes(candidateLabel);
|
|
156
|
+
if (sameVisualRow && distinctLabels) {
|
|
157
|
+
candidate.label = `${candidate.label} · ${section.label}`.slice(0, 120);
|
|
158
|
+
}
|
|
159
|
+
else if (section.score > candidate.score) {
|
|
160
|
+
selected[nearby] = section;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
let previousTarget = 0;
|
|
165
|
+
return selected
|
|
166
|
+
.sort((a, b) => a.y - b.y)
|
|
167
|
+
.slice(0, 30)
|
|
168
|
+
.map((section) => {
|
|
169
|
+
const distanceFromPrevious = Math.max(0, section.targetY - previousTarget);
|
|
170
|
+
previousTarget = section.targetY;
|
|
171
|
+
const recommendedTransitionMs = Math.max(600, Math.ceil((((distanceFromPrevious * 1.9) /
|
|
172
|
+
Math.max(1, viewportHeight * 1.5)) *
|
|
173
|
+
1000) /
|
|
174
|
+
50) * 50);
|
|
175
|
+
return {
|
|
176
|
+
label: section.label,
|
|
177
|
+
selector: section.selector,
|
|
178
|
+
kind: section.kind,
|
|
179
|
+
y: section.y,
|
|
180
|
+
height: section.height,
|
|
181
|
+
targetY: section.targetY,
|
|
182
|
+
progress: maxScroll === 0 ? 0 : section.targetY / maxScroll,
|
|
183
|
+
distanceFromPrevious,
|
|
184
|
+
recommendedTransitionMs,
|
|
185
|
+
recommendedTarget: {
|
|
186
|
+
type: "selector",
|
|
187
|
+
selector: section.selector,
|
|
188
|
+
align: section.recommendedAlign,
|
|
189
|
+
...(section.recommendedOffsetPx
|
|
190
|
+
? { offsetPx: section.recommendedOffsetPx }
|
|
191
|
+
: {}),
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
async function collectSections(page) {
|
|
197
|
+
return page.evaluate(`(() => {
|
|
198
|
+
const pathFor = (element) => {
|
|
199
|
+
if (element.id) return "#" + CSS.escape(element.id);
|
|
200
|
+
const framerName = element.getAttribute("data-framer-name");
|
|
201
|
+
if (framerName) {
|
|
202
|
+
const namedSelector = "[data-framer-name=\\"" + CSS.escape(framerName) + "\\"]";
|
|
203
|
+
if (document.querySelectorAll(namedSelector).length === 1) return namedSelector;
|
|
204
|
+
}
|
|
205
|
+
const segments = [];
|
|
206
|
+
let current = element;
|
|
207
|
+
while (current && current !== document.body && segments.length < 12) {
|
|
208
|
+
const parent = current.parentElement;
|
|
209
|
+
const tag = current.tagName.toLowerCase();
|
|
210
|
+
const index = parent ? Array.from(parent.children).filter((child) => child.tagName === current.tagName).indexOf(current) + 1 : 1;
|
|
211
|
+
segments.unshift(tag + ":nth-of-type(" + index + ")");
|
|
212
|
+
current = parent;
|
|
213
|
+
}
|
|
214
|
+
return current === document.body
|
|
215
|
+
? "body > " + segments.join(" > ")
|
|
216
|
+
: "body " + segments.join(" > ");
|
|
217
|
+
};
|
|
218
|
+
const genericFramerNames = new Set([
|
|
219
|
+
"app", "background", "bg", "border", "bottom", "container", "content",
|
|
220
|
+
"default", "desktop", "footer", "header", "interface", "layout", "left",
|
|
221
|
+
"main", "mobile", "new layout", "right", "section", "shade", "tablet",
|
|
222
|
+
"top", "ui", "visual", "wrapper",
|
|
223
|
+
]);
|
|
224
|
+
const semantic = Array.from(document.querySelectorAll("h1,h2,h3,main,section,article,footer,[role='main'],[role='region']"))
|
|
225
|
+
// A footer inside main/section/article is commonly part of a UI mockup
|
|
226
|
+
// or card. Treat only page-level footers as navigation landmarks.
|
|
227
|
+
.filter((element) => element.tagName.toLowerCase() !== "footer" || !element.closest("main,section,article"))
|
|
228
|
+
.map((element) => {
|
|
229
|
+
const tag = element.tagName.toLowerCase();
|
|
230
|
+
const heading = /^h[1-3]$/.test(tag) ? element : element.querySelector("h1,h2,h3");
|
|
231
|
+
const focus = heading || element;
|
|
232
|
+
const rect = focus.getBoundingClientRect();
|
|
233
|
+
const framerName = element.getAttribute("data-framer-name") || "";
|
|
234
|
+
const namedLabel = genericFramerNames.has(framerName.toLowerCase()) ? "" : framerName;
|
|
235
|
+
const text = (
|
|
236
|
+
element.getAttribute("aria-label")
|
|
237
|
+
|| heading?.innerText
|
|
238
|
+
|| heading?.textContent
|
|
239
|
+
|| namedLabel
|
|
240
|
+
|| (tag === "footer" ? "Footer" : element.innerText || element.textContent)
|
|
241
|
+
|| ""
|
|
242
|
+
).replace(/\\s+/g, " ").trim();
|
|
243
|
+
return { label: text.slice(0, 120) || tag, selector: pathFor(focus), kind: heading ? "heading" : "landmark", y: Math.max(0, Math.round(rect.top + window.scrollY)), height: Math.max(1, Math.round(rect.height)), stable: focus.id ? 1 : 0, visible: rect.width > 0 && rect.height > 0 };
|
|
244
|
+
})
|
|
245
|
+
.filter((section) => section.visible && section.label.length > 0 && Number.isFinite(section.y));
|
|
246
|
+
const namedBands = Array.from(document.querySelectorAll("main [data-framer-name]"))
|
|
247
|
+
.flatMap((element) => {
|
|
248
|
+
const name = (element.getAttribute("data-framer-name") || "").replace(/\\s+/g, " ").trim();
|
|
249
|
+
const normalizedName = name.toLowerCase();
|
|
250
|
+
if (
|
|
251
|
+
name.length < 3
|
|
252
|
+
|| name.length > 60
|
|
253
|
+
|| genericFramerNames.has(normalizedName)
|
|
254
|
+
|| element.querySelector("h1,h2,h3")
|
|
255
|
+
) return [];
|
|
256
|
+
const main = element.closest("main");
|
|
257
|
+
let depth = 0;
|
|
258
|
+
let current = element;
|
|
259
|
+
while (current && current !== main) {
|
|
260
|
+
depth += 1;
|
|
261
|
+
current = current.parentElement;
|
|
262
|
+
}
|
|
263
|
+
const rect = element.getBoundingClientRect();
|
|
264
|
+
const style = getComputedStyle(element);
|
|
265
|
+
const visible =
|
|
266
|
+
rect.width >= innerWidth * 0.65
|
|
267
|
+
&& rect.height >= Math.max(280, innerHeight * 0.34)
|
|
268
|
+
&& style.display !== "none"
|
|
269
|
+
&& style.visibility !== "hidden"
|
|
270
|
+
&& Number(style.opacity || 1) > 0
|
|
271
|
+
&& style.position !== "fixed"
|
|
272
|
+
&& style.position !== "sticky";
|
|
273
|
+
if (!main || depth > 2 || !visible) return [];
|
|
274
|
+
return [{
|
|
275
|
+
label: name,
|
|
276
|
+
selector: pathFor(element),
|
|
277
|
+
kind: "landmark",
|
|
278
|
+
y: Math.max(0, Math.round(rect.top + window.scrollY)),
|
|
279
|
+
height: Math.max(1, Math.round(rect.height)),
|
|
280
|
+
stable: element.id || document.querySelectorAll("[data-framer-name=\\"" + CSS.escape(name) + "\\"]").length === 1 ? 1 : 0,
|
|
281
|
+
visible: true,
|
|
282
|
+
}];
|
|
283
|
+
});
|
|
284
|
+
return [...semantic, ...namedBands];
|
|
285
|
+
})()`);
|
|
286
|
+
}
|
|
287
|
+
async function detectInspectionSafeViewport(page, viewport) {
|
|
288
|
+
const topInsetPx = await page.evaluate(`(() => Array.from(document.querySelectorAll("body *")).reduce((inset, element) => {
|
|
289
|
+
const style = getComputedStyle(element); const rect = element.getBoundingClientRect();
|
|
290
|
+
const pinned = style.position === "fixed" || style.position === "sticky";
|
|
291
|
+
const visible = style.display !== "none" && style.visibility !== "hidden" && Number(style.opacity || 1) > 0;
|
|
292
|
+
const topBar = rect.top <= 8 && rect.bottom > 0 && rect.width >= ${viewport.width} * 0.5 && rect.height >= 24 && rect.height <= ${viewport.height} * 0.3;
|
|
293
|
+
return pinned && visible && topBar ? Math.max(inset, Math.round(rect.bottom)) : inset;
|
|
294
|
+
}, 0))()`);
|
|
295
|
+
return { topInsetPx, bottomInsetPx: 24 };
|
|
296
|
+
}
|
|
297
|
+
async function takeDocumentStoryboardScreenshots(page, pageHeight, viewportHeight) {
|
|
298
|
+
const maxScroll = Math.max(0, pageHeight - viewportHeight);
|
|
299
|
+
const positions = [
|
|
300
|
+
...new Set([
|
|
301
|
+
0,
|
|
302
|
+
Math.round(maxScroll / 3),
|
|
303
|
+
Math.round((maxScroll * 2) / 3),
|
|
304
|
+
maxScroll,
|
|
305
|
+
]),
|
|
306
|
+
];
|
|
307
|
+
const screenshots = [];
|
|
308
|
+
const storyboard = [];
|
|
309
|
+
for (const position of positions) {
|
|
310
|
+
await page.evaluate(`window.scrollTo(0, ${position})`);
|
|
311
|
+
await page.waitForTimeout(250);
|
|
312
|
+
screenshots.push((await page.screenshot({ type: "jpeg", quality: 60 })).toString("base64"));
|
|
313
|
+
storyboard.push({
|
|
314
|
+
imageIndex: screenshots.length - 1,
|
|
315
|
+
target: { type: "progress", value: maxScroll === 0 ? 0 : position / maxScroll },
|
|
316
|
+
y: position,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
return { screenshots, storyboard };
|
|
320
|
+
}
|
|
321
|
+
async function attachSectionPreviewScreenshots(page, sections, screenshots, viewportHeight) {
|
|
322
|
+
if (sections.length === 0)
|
|
323
|
+
return;
|
|
324
|
+
const allPreviewPositions = [];
|
|
325
|
+
const minimumSpacing = Math.max(180, viewportHeight * 0.28);
|
|
326
|
+
for (const section of sections) {
|
|
327
|
+
const nearby = allPreviewPositions.findIndex((position) => Math.abs(position - section.targetY) < minimumSpacing);
|
|
328
|
+
if (nearby < 0) {
|
|
329
|
+
allPreviewPositions.push(section.targetY);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
allPreviewPositions.sort((a, b) => a - b);
|
|
333
|
+
const maximumPreviewCount = 16;
|
|
334
|
+
const previewPositions = allPreviewPositions.length <= maximumPreviewCount
|
|
335
|
+
? allPreviewPositions
|
|
336
|
+
: [...new Set(Array.from({ length: maximumPreviewCount }, (_, index) => allPreviewPositions[Math.round((index * (allPreviewPositions.length - 1)) / (maximumPreviewCount - 1))]))];
|
|
337
|
+
const indexes = new Map();
|
|
338
|
+
for (const position of previewPositions) {
|
|
339
|
+
await page.evaluate(`window.scrollTo({ top: ${Math.round(position)}, left: 0, behavior: "instant" })`);
|
|
340
|
+
await page.waitForTimeout(120);
|
|
341
|
+
screenshots.push((await page.screenshot({ type: "jpeg", quality: 68 })).toString("base64"));
|
|
342
|
+
indexes.set(position, screenshots.length - 1);
|
|
343
|
+
}
|
|
344
|
+
for (const section of sections) {
|
|
345
|
+
const closest = previewPositions.reduce((best, position) => {
|
|
346
|
+
if (best === undefined)
|
|
347
|
+
return position;
|
|
348
|
+
return Math.abs(position - section.targetY) < Math.abs(best - section.targetY)
|
|
349
|
+
? position
|
|
350
|
+
: best;
|
|
351
|
+
}, undefined);
|
|
352
|
+
if (closest !== undefined)
|
|
353
|
+
section.imageIndex = indexes.get(closest);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
async function takeVirtualStoryboardScreenshots(page, viewport) {
|
|
357
|
+
const progressPoints = [0, 0.25, 0.5, 0.75, 1];
|
|
358
|
+
const wheelBudget = viewport.height * 8;
|
|
359
|
+
const centerX = Math.floor(viewport.width / 2);
|
|
360
|
+
const centerY = Math.floor(viewport.height / 2);
|
|
361
|
+
const cdp = await page.context().newCDPSession(page);
|
|
362
|
+
const screenshots = [];
|
|
363
|
+
const storyboard = [];
|
|
364
|
+
let dispatched = 0;
|
|
365
|
+
try {
|
|
366
|
+
await page.mouse.move(centerX, centerY);
|
|
367
|
+
await page.mouse.click(centerX, centerY);
|
|
368
|
+
for (const value of progressPoints) {
|
|
369
|
+
const target = value * wheelBudget;
|
|
370
|
+
if (target > dispatched) {
|
|
371
|
+
const delta = (target - dispatched) / 20;
|
|
372
|
+
for (let tick = 0; tick < 20; tick += 1) {
|
|
373
|
+
await cdp.send("Input.dispatchMouseEvent", {
|
|
374
|
+
type: "mouseWheel",
|
|
375
|
+
x: centerX,
|
|
376
|
+
y: centerY,
|
|
377
|
+
deltaX: 0,
|
|
378
|
+
deltaY: delta,
|
|
379
|
+
});
|
|
380
|
+
await page.waitForTimeout(16);
|
|
381
|
+
}
|
|
382
|
+
dispatched = target;
|
|
383
|
+
await page.waitForTimeout(250);
|
|
384
|
+
}
|
|
385
|
+
screenshots.push((await page.screenshot({ type: "jpeg", quality: 60 })).toString("base64"));
|
|
386
|
+
storyboard.push({ imageIndex: screenshots.length - 1, target: { type: "progress", value } });
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
finally {
|
|
390
|
+
await cdp.detach().catch(() => undefined);
|
|
391
|
+
}
|
|
392
|
+
return { screenshots, storyboard };
|
|
393
|
+
}
|
|
394
|
+
async function detectInspectionScrollMode(page, pageHeight, viewportHeight) {
|
|
395
|
+
if (pageHeight - viewportHeight > 48)
|
|
396
|
+
return "document";
|
|
397
|
+
const virtual = await page.evaluate(`(() => {
|
|
398
|
+
const body = getComputedStyle(document.body); const html = getComputedStyle(document.documentElement);
|
|
399
|
+
const locked = body.overflow === "hidden" || body.overflowY === "hidden" || html.overflow === "hidden" || html.overflowY === "hidden";
|
|
400
|
+
const fixedShell = Array.from(document.querySelectorAll("body *")).some((element) => { const style = getComputedStyle(element); const rect = element.getBoundingClientRect(); return (style.position === "fixed" || style.position === "sticky") && rect.height >= innerHeight * 0.85 && rect.width >= innerWidth * 0.85; });
|
|
401
|
+
return locked || fixedShell || document.querySelectorAll("canvas").length >= 3;
|
|
402
|
+
})()`);
|
|
403
|
+
return virtual ? "virtual" : "document";
|
|
404
|
+
}
|
|
405
|
+
function assertHttpUrl(value) {
|
|
406
|
+
const url = new URL(value);
|
|
407
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
408
|
+
throw new Error("Only HTTP(S) URLs are supported");
|
|
409
|
+
}
|
|
410
|
+
return url;
|
|
411
|
+
}
|
|
412
|
+
//# sourceMappingURL=inspectWebsite.js.map
|