smippo 0.1.1 → 0.1.2
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/package.json +1 -1
- package/src/cli.js +18 -0
- package/src/crawler.js +1 -0
- package/src/delete.js +188 -0
- package/src/page-capture.js +250 -184
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smippo",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "S.M.I.P.P.O. — Structured Mirroring of Internet Pages and Public Objects. Modern website copier that captures sites exactly as they appear in your browser.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
package/src/cli.js
CHANGED
|
@@ -130,6 +130,11 @@ export function run() {
|
|
|
130
130
|
'--no-reveal-all',
|
|
131
131
|
'Disable force-reveal of scroll-triggered content',
|
|
132
132
|
)
|
|
133
|
+
.option(
|
|
134
|
+
'--reduced-motion',
|
|
135
|
+
'Use prefers-reduced-motion for accessibility (default: true)',
|
|
136
|
+
)
|
|
137
|
+
.option('--no-reduced-motion', 'Disable reduced motion preference')
|
|
133
138
|
.option('--user-agent <string>', 'Custom user agent')
|
|
134
139
|
.option('--viewport <WxH>', 'Viewport size', '1920x1080')
|
|
135
140
|
.option('--device <name>', 'Emulate device (e.g., "iPhone 13")')
|
|
@@ -256,6 +261,18 @@ export function run() {
|
|
|
256
261
|
});
|
|
257
262
|
});
|
|
258
263
|
|
|
264
|
+
// Delete command - remove captured sites
|
|
265
|
+
program
|
|
266
|
+
.command('delete')
|
|
267
|
+
.alias('rm')
|
|
268
|
+
.description('Delete captured sites from local storage')
|
|
269
|
+
.option('-a, --all', 'Delete all captured sites')
|
|
270
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
271
|
+
.action(async options => {
|
|
272
|
+
const {deleteSites} = await import('./delete.js');
|
|
273
|
+
await deleteSites(options);
|
|
274
|
+
});
|
|
275
|
+
|
|
259
276
|
// Screenshot capture command
|
|
260
277
|
program
|
|
261
278
|
.command('capture <url>')
|
|
@@ -357,6 +374,7 @@ async function capture(url, options) {
|
|
|
357
374
|
scrollDelay: parseInt(options.scrollDelay, 10),
|
|
358
375
|
scrollBehavior: options.scrollBehavior,
|
|
359
376
|
revealAll: options.revealAll,
|
|
377
|
+
reducedMotion: options.reducedMotion,
|
|
360
378
|
userAgent: options.userAgent,
|
|
361
379
|
viewport: parseViewport(options.viewport),
|
|
362
380
|
device: options.device,
|
package/src/crawler.js
CHANGED
|
@@ -276,6 +276,7 @@ export class Crawler extends EventEmitter {
|
|
|
276
276
|
scrollDelay: this.options.scrollDelay,
|
|
277
277
|
scrollBehavior: this.options.scrollBehavior,
|
|
278
278
|
revealAll: this.options.revealAll,
|
|
279
|
+
reducedMotion: this.options.reducedMotion,
|
|
279
280
|
});
|
|
280
281
|
|
|
281
282
|
const result = await capture.capture(url);
|
package/src/delete.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import * as p from '@clack/prompts';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import {readGlobalManifest, writeGlobalManifest} from './utils/home.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Format file size for display
|
|
10
|
+
*/
|
|
11
|
+
function formatSize(bytes) {
|
|
12
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
13
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
14
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Calculate directory size recursively
|
|
19
|
+
*/
|
|
20
|
+
async function getDirectorySize(dirPath) {
|
|
21
|
+
let totalSize = 0;
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const entries = await fs.readdir(dirPath, {withFileTypes: true});
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
27
|
+
if (entry.isDirectory()) {
|
|
28
|
+
totalSize += await getDirectorySize(fullPath);
|
|
29
|
+
} else {
|
|
30
|
+
const stats = await fs.stat(fullPath);
|
|
31
|
+
totalSize += stats.size;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
} catch (e) {
|
|
35
|
+
// Ignore errors
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return totalSize;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Interactive delete command for captured sites
|
|
43
|
+
*/
|
|
44
|
+
export async function deleteSites(options = {}) {
|
|
45
|
+
// Read global manifest
|
|
46
|
+
const manifest = await readGlobalManifest();
|
|
47
|
+
|
|
48
|
+
if (!manifest.sites || manifest.sites.length === 0) {
|
|
49
|
+
console.log(chalk.yellow('\nNo captured sites found.\n'));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Show header
|
|
54
|
+
console.log('');
|
|
55
|
+
p.intro(chalk.bgRed.white(' Delete Captured Sites '));
|
|
56
|
+
|
|
57
|
+
// Get site info with sizes
|
|
58
|
+
const sitesWithInfo = await Promise.all(
|
|
59
|
+
manifest.sites.map(async site => {
|
|
60
|
+
const exists = await fs.pathExists(site.path);
|
|
61
|
+
let size = 0;
|
|
62
|
+
if (exists) {
|
|
63
|
+
size = await getDirectorySize(site.path);
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
...site,
|
|
67
|
+
exists,
|
|
68
|
+
size,
|
|
69
|
+
displaySize: formatSize(size),
|
|
70
|
+
};
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// Filter to only existing sites
|
|
75
|
+
const existingSites = sitesWithInfo.filter(s => s.exists);
|
|
76
|
+
|
|
77
|
+
if (existingSites.length === 0) {
|
|
78
|
+
console.log(chalk.yellow('No captured sites found on disk.\n'));
|
|
79
|
+
|
|
80
|
+
// Clean up manifest
|
|
81
|
+
manifest.sites = [];
|
|
82
|
+
await writeGlobalManifest(manifest);
|
|
83
|
+
console.log(chalk.dim('Cleaned up manifest.\n'));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let sitesToDelete = [];
|
|
88
|
+
|
|
89
|
+
if (options.all) {
|
|
90
|
+
// Delete all sites
|
|
91
|
+
sitesToDelete = existingSites;
|
|
92
|
+
} else {
|
|
93
|
+
// Interactive selection
|
|
94
|
+
const selected = await p.multiselect({
|
|
95
|
+
message: 'Select sites to delete (space to select, enter to confirm):',
|
|
96
|
+
options: existingSites.map(site => ({
|
|
97
|
+
value: site,
|
|
98
|
+
label: `${site.domain}`,
|
|
99
|
+
hint: `${site.displaySize} • ${site.path}`,
|
|
100
|
+
})),
|
|
101
|
+
required: false,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (p.isCancel(selected) || !selected || selected.length === 0) {
|
|
105
|
+
p.cancel('No sites selected.');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
sitesToDelete = selected;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Show what will be deleted
|
|
113
|
+
console.log('');
|
|
114
|
+
console.log(chalk.bold('Sites to delete:'));
|
|
115
|
+
for (const site of sitesToDelete) {
|
|
116
|
+
console.log(chalk.red(` • ${site.domain} (${site.displaySize})`));
|
|
117
|
+
console.log(chalk.dim(` ${site.path}`));
|
|
118
|
+
}
|
|
119
|
+
console.log('');
|
|
120
|
+
|
|
121
|
+
// Calculate total size
|
|
122
|
+
const totalSize = sitesToDelete.reduce((sum, s) => sum + s.size, 0);
|
|
123
|
+
console.log(
|
|
124
|
+
chalk.bold(
|
|
125
|
+
`Total: ${sitesToDelete.length} site(s), ${formatSize(totalSize)}`,
|
|
126
|
+
),
|
|
127
|
+
);
|
|
128
|
+
console.log('');
|
|
129
|
+
|
|
130
|
+
// Confirmation
|
|
131
|
+
let confirmed = options.yes;
|
|
132
|
+
if (!confirmed) {
|
|
133
|
+
confirmed = await p.confirm({
|
|
134
|
+
message: 'Are you sure you want to delete these sites?',
|
|
135
|
+
initialValue: false,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
140
|
+
p.cancel('Deletion cancelled.');
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Delete the sites
|
|
145
|
+
const spinner = p.spinner();
|
|
146
|
+
spinner.start('Deleting sites...');
|
|
147
|
+
|
|
148
|
+
let deletedCount = 0;
|
|
149
|
+
let failedCount = 0;
|
|
150
|
+
const deletedDomains = new Set();
|
|
151
|
+
|
|
152
|
+
for (const site of sitesToDelete) {
|
|
153
|
+
try {
|
|
154
|
+
await fs.remove(site.path);
|
|
155
|
+
deletedCount++;
|
|
156
|
+
deletedDomains.add(site.domain);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
failedCount++;
|
|
159
|
+
console.error(
|
|
160
|
+
chalk.red(`\n Failed to delete ${site.domain}: ${error.message}`),
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Update manifest - remove deleted sites
|
|
166
|
+
manifest.sites = manifest.sites.filter(
|
|
167
|
+
s =>
|
|
168
|
+
!deletedDomains.has(s.domain) ||
|
|
169
|
+
!sitesToDelete.some(d => d.path === s.path),
|
|
170
|
+
);
|
|
171
|
+
await writeGlobalManifest(manifest);
|
|
172
|
+
|
|
173
|
+
spinner.stop('Deletion complete!');
|
|
174
|
+
|
|
175
|
+
// Summary
|
|
176
|
+
console.log('');
|
|
177
|
+
if (deletedCount > 0) {
|
|
178
|
+
console.log(
|
|
179
|
+
chalk.green(
|
|
180
|
+
`✓ Deleted ${deletedCount} site(s), freed ${formatSize(totalSize)}`,
|
|
181
|
+
),
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
if (failedCount > 0) {
|
|
185
|
+
console.log(chalk.red(`✗ Failed to delete ${failedCount} site(s)`));
|
|
186
|
+
}
|
|
187
|
+
console.log('');
|
|
188
|
+
}
|
package/src/page-capture.js
CHANGED
|
@@ -3,6 +3,8 @@ import {extractLinks} from './link-extractor.js';
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Capture a single page with all its rendered content
|
|
6
|
+
* Uses best-in-class techniques including accessibility features,
|
|
7
|
+
* reduced motion, and human-like scrolling behavior
|
|
6
8
|
*/
|
|
7
9
|
export class PageCapture {
|
|
8
10
|
constructor(page, options = {}) {
|
|
@@ -22,6 +24,12 @@ export class PageCapture {
|
|
|
22
24
|
await this._collectResource(response);
|
|
23
25
|
});
|
|
24
26
|
|
|
27
|
+
// Step 0: Enable reduced motion to disable CSS animations (accessibility feature)
|
|
28
|
+
// This causes many sites to show final animation states immediately
|
|
29
|
+
if (this.options.reducedMotion !== false) {
|
|
30
|
+
await this.page.emulateMedia({reducedMotion: 'reduce'});
|
|
31
|
+
}
|
|
32
|
+
|
|
25
33
|
// Navigate to the page
|
|
26
34
|
try {
|
|
27
35
|
await this.page.goto(url, {
|
|
@@ -41,22 +49,34 @@ export class PageCapture {
|
|
|
41
49
|
await this.page.waitForTimeout(waitTime);
|
|
42
50
|
}
|
|
43
51
|
|
|
44
|
-
// Step 1: Force
|
|
52
|
+
// Step 1: Force all scroll-triggered animations to their END state (100% progress)
|
|
53
|
+
// This is different from killing them - we want their final visual state
|
|
45
54
|
if (this.options.revealAll !== false) {
|
|
46
|
-
await this.
|
|
55
|
+
await this._completeAllAnimations();
|
|
47
56
|
}
|
|
48
57
|
|
|
49
|
-
// Step 2:
|
|
58
|
+
// Step 2: Human-like scrolling to trigger lazy content and intersection observers
|
|
50
59
|
if (this.options.scroll !== false) {
|
|
51
|
-
await this.
|
|
60
|
+
await this._humanLikeScroll();
|
|
52
61
|
}
|
|
53
62
|
|
|
54
|
-
// Step 3:
|
|
63
|
+
// Step 3: Complete animations again after scroll (new elements may have appeared)
|
|
64
|
+
if (this.options.revealAll !== false) {
|
|
65
|
+
await this._completeAllAnimations();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Step 4: Final wait for any remaining animations/network requests
|
|
55
69
|
const scrollWait = this.options.scrollWait ?? 1000;
|
|
56
|
-
if (scrollWait > 0
|
|
70
|
+
if (scrollWait > 0) {
|
|
57
71
|
await this.page.waitForTimeout(scrollWait);
|
|
58
72
|
}
|
|
59
73
|
|
|
74
|
+
// Step 5: Wait for network to be truly idle (no pending requests)
|
|
75
|
+
await this._waitForNetworkIdle();
|
|
76
|
+
|
|
77
|
+
// Step 6: Force reveal any remaining hidden elements
|
|
78
|
+
await this._forceRevealHiddenElements();
|
|
79
|
+
|
|
60
80
|
// Get the rendered HTML
|
|
61
81
|
const html = await this.page.content();
|
|
62
82
|
|
|
@@ -167,30 +187,41 @@ export class PageCapture {
|
|
|
167
187
|
}
|
|
168
188
|
|
|
169
189
|
/**
|
|
170
|
-
*
|
|
171
|
-
|
|
190
|
+
* Wait for network to be truly idle (no pending requests for a period)
|
|
191
|
+
*/
|
|
192
|
+
async _waitForNetworkIdle() {
|
|
193
|
+
try {
|
|
194
|
+
await this.page.waitForLoadState('networkidle', {timeout: 5000});
|
|
195
|
+
} catch {
|
|
196
|
+
// Timeout is ok, continue anyway
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Human-like scrolling behavior with pauses and mouse movements
|
|
202
|
+
* Triggers intersection observers and lazy loading more naturally
|
|
172
203
|
*/
|
|
173
|
-
async
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
const scrollDelay = this.options.scrollDelay || 50; // ms between steps
|
|
204
|
+
async _humanLikeScroll() {
|
|
205
|
+
const scrollStep = this.options.scrollStep || 300;
|
|
206
|
+
const scrollDelay = this.options.scrollDelay || 100;
|
|
177
207
|
|
|
178
208
|
/* eslint-disable no-undef */
|
|
179
209
|
await this.page.evaluate(
|
|
180
|
-
async ({step, delay
|
|
181
|
-
//
|
|
182
|
-
const
|
|
210
|
+
async ({step, delay}) => {
|
|
211
|
+
// Human-like smooth scroll with easing
|
|
212
|
+
const smoothScrollTo = (targetY, duration = 500) => {
|
|
183
213
|
return new Promise(resolve => {
|
|
184
214
|
const startY = window.scrollY;
|
|
185
215
|
const distance = targetY - startY;
|
|
186
216
|
const startTime = performance.now();
|
|
187
217
|
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
const progress = Math.min(elapsed / duration, 1);
|
|
218
|
+
const easeInOutCubic = t =>
|
|
219
|
+
t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
|
191
220
|
|
|
192
|
-
|
|
193
|
-
const
|
|
221
|
+
const animate = () => {
|
|
222
|
+
const elapsed = performance.now() - startTime;
|
|
223
|
+
const progress = Math.min(elapsed / duration, 1);
|
|
224
|
+
const eased = easeInOutCubic(progress);
|
|
194
225
|
|
|
195
226
|
window.scrollTo(0, startY + distance * eased);
|
|
196
227
|
|
|
@@ -200,80 +231,69 @@ export class PageCapture {
|
|
|
200
231
|
resolve();
|
|
201
232
|
}
|
|
202
233
|
};
|
|
203
|
-
|
|
204
234
|
requestAnimationFrame(animate);
|
|
205
235
|
});
|
|
206
236
|
};
|
|
207
237
|
|
|
208
|
-
//
|
|
209
|
-
|
|
238
|
+
// Wait helper
|
|
239
|
+
const wait = ms => new Promise(r => setTimeout(r, ms));
|
|
240
|
+
|
|
241
|
+
// Get viewport and page dimensions
|
|
242
|
+
const viewportHeight = window.innerHeight;
|
|
243
|
+
let pageHeight = document.body.scrollHeight;
|
|
210
244
|
let currentY = 0;
|
|
245
|
+
let lastHeight = pageHeight;
|
|
211
246
|
|
|
212
|
-
// Phase 1: Scroll down
|
|
213
|
-
while (currentY <
|
|
214
|
-
|
|
247
|
+
// Phase 1: Scroll down slowly, pausing at each "section"
|
|
248
|
+
while (currentY < pageHeight) {
|
|
249
|
+
// Scroll one viewport at a time (like a human reading)
|
|
250
|
+
const targetY = Math.min(currentY + step, pageHeight);
|
|
251
|
+
await smoothScrollTo(targetY, delay * 3);
|
|
215
252
|
|
|
216
|
-
|
|
217
|
-
|
|
253
|
+
// Pause longer at section boundaries (every viewport height)
|
|
254
|
+
if (
|
|
255
|
+
Math.floor(currentY / viewportHeight) !==
|
|
256
|
+
Math.floor(targetY / viewportHeight)
|
|
257
|
+
) {
|
|
258
|
+
await wait(delay * 2); // Pause to "read" the section
|
|
218
259
|
} else {
|
|
219
|
-
|
|
260
|
+
await wait(delay);
|
|
220
261
|
}
|
|
221
262
|
|
|
222
263
|
currentY = targetY;
|
|
223
|
-
await new Promise(r => setTimeout(r, delay));
|
|
224
264
|
|
|
225
|
-
// Check
|
|
265
|
+
// Check for lazy-loaded content increasing page height
|
|
226
266
|
const newHeight = document.body.scrollHeight;
|
|
227
267
|
if (newHeight > lastHeight) {
|
|
268
|
+
pageHeight = newHeight;
|
|
228
269
|
lastHeight = newHeight;
|
|
229
270
|
}
|
|
230
271
|
}
|
|
231
272
|
|
|
232
|
-
// Phase 2:
|
|
233
|
-
await
|
|
273
|
+
// Phase 2: Ensure we're at the very bottom
|
|
274
|
+
await smoothScrollTo(document.body.scrollHeight, 300);
|
|
275
|
+
await wait(500); // Wait at bottom
|
|
234
276
|
|
|
235
|
-
// Check
|
|
236
|
-
if (document.body.scrollHeight >
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
await smoothScroll(document.body.scrollHeight, 300);
|
|
240
|
-
} else {
|
|
241
|
-
window.scrollTo(0, document.body.scrollHeight);
|
|
242
|
-
}
|
|
243
|
-
await new Promise(r => setTimeout(r, 300));
|
|
277
|
+
// Check one more time for lazy content
|
|
278
|
+
if (document.body.scrollHeight > pageHeight) {
|
|
279
|
+
await smoothScrollTo(document.body.scrollHeight, 300);
|
|
280
|
+
await wait(300);
|
|
244
281
|
}
|
|
245
282
|
|
|
246
|
-
// Phase 3: Scroll back
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
while (currentY > 0) {
|
|
251
|
-
const targetY = Math.max(currentY - scrollUpStep, 0);
|
|
252
|
-
|
|
253
|
-
if (behavior === 'smooth') {
|
|
254
|
-
await smoothScroll(targetY, delay);
|
|
255
|
-
} else {
|
|
256
|
-
window.scrollTo(0, targetY);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
currentY = targetY;
|
|
260
|
-
await new Promise(r => setTimeout(r, delay / 2));
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Phase 4: Return to top and wait
|
|
264
|
-
window.scrollTo(0, 0);
|
|
265
|
-
await new Promise(r => setTimeout(r, 200));
|
|
283
|
+
// Phase 3: Scroll back to top (faster)
|
|
284
|
+
await smoothScrollTo(0, 800);
|
|
285
|
+
await wait(300);
|
|
266
286
|
},
|
|
267
|
-
{step: scrollStep, delay: scrollDelay
|
|
287
|
+
{step: scrollStep, delay: scrollDelay},
|
|
268
288
|
);
|
|
269
289
|
/* eslint-enable no-undef */
|
|
270
290
|
}
|
|
271
291
|
|
|
272
292
|
/**
|
|
273
|
-
*
|
|
274
|
-
*
|
|
293
|
+
* Complete all animations to their final state (100% progress)
|
|
294
|
+
* Instead of killing animations, we progress them to completion
|
|
275
295
|
*/
|
|
276
|
-
async
|
|
296
|
+
async _completeAllAnimations() {
|
|
277
297
|
/* eslint-disable no-undef */
|
|
278
298
|
await this.page.evaluate(() => {
|
|
279
299
|
// Helper to safely access nested properties
|
|
@@ -285,178 +305,224 @@ export class PageCapture {
|
|
|
285
305
|
}
|
|
286
306
|
};
|
|
287
307
|
|
|
288
|
-
// 1. GSAP
|
|
289
|
-
const
|
|
290
|
-
if (
|
|
308
|
+
// 1. GSAP - Progress ALL tweens and timelines to 100%
|
|
309
|
+
const gsap = safeGet(window, 'gsap');
|
|
310
|
+
if (gsap) {
|
|
291
311
|
try {
|
|
292
|
-
// Get all
|
|
293
|
-
const
|
|
294
|
-
|
|
312
|
+
// Get all global tweens and progress them to end
|
|
313
|
+
const tweens =
|
|
314
|
+
gsap.globalTimeline?.getChildren?.(true, true, true) || [];
|
|
315
|
+
tweens.forEach(tween => {
|
|
295
316
|
try {
|
|
296
|
-
//
|
|
297
|
-
|
|
317
|
+
// Progress to 100% (end state)
|
|
318
|
+
tween.progress?.(1, true);
|
|
319
|
+
// Also try totalProgress for timelines
|
|
320
|
+
tween.totalProgress?.(1, true);
|
|
298
321
|
} catch (e) {
|
|
299
322
|
/* ignore */
|
|
300
323
|
}
|
|
301
324
|
});
|
|
302
|
-
// Refresh to ensure proper state
|
|
303
|
-
ScrollTrigger.refresh?.();
|
|
304
325
|
} catch (e) {
|
|
305
326
|
/* ignore */
|
|
306
327
|
}
|
|
307
328
|
}
|
|
308
329
|
|
|
309
|
-
//
|
|
310
|
-
const
|
|
311
|
-
|
|
330
|
+
// 2. GSAP ScrollTrigger - Progress all triggers to their END state
|
|
331
|
+
const ScrollTrigger =
|
|
332
|
+
safeGet(window, 'ScrollTrigger') ||
|
|
333
|
+
safeGet(window, 'gsap.ScrollTrigger');
|
|
334
|
+
if (ScrollTrigger) {
|
|
312
335
|
try {
|
|
313
|
-
const triggers =
|
|
314
|
-
triggers.forEach(trigger =>
|
|
336
|
+
const triggers = ScrollTrigger.getAll?.() || [];
|
|
337
|
+
triggers.forEach(trigger => {
|
|
338
|
+
try {
|
|
339
|
+
// Progress the trigger to 100% (fully scrolled past)
|
|
340
|
+
if (trigger.animation) {
|
|
341
|
+
trigger.animation.progress?.(1, true);
|
|
342
|
+
trigger.animation.totalProgress?.(1, true);
|
|
343
|
+
}
|
|
344
|
+
// Also set the trigger's own progress
|
|
345
|
+
trigger.progress?.(1, true);
|
|
346
|
+
|
|
347
|
+
// Disable the trigger so it doesn't reset
|
|
348
|
+
trigger.disable?.();
|
|
349
|
+
} catch (e) {
|
|
350
|
+
/* ignore */
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Don't refresh - we want to keep the end states
|
|
315
355
|
} catch (e) {
|
|
316
356
|
/* ignore */
|
|
317
357
|
}
|
|
318
358
|
}
|
|
319
359
|
|
|
320
|
-
//
|
|
321
|
-
|
|
322
|
-
if (AOS) {
|
|
360
|
+
// 3. Web Animations API - Complete all animations
|
|
361
|
+
document.getAnimations?.().forEach(animation => {
|
|
323
362
|
try {
|
|
324
|
-
|
|
325
|
-
document.querySelectorAll('[data-aos]').forEach(el => {
|
|
326
|
-
el.classList.add('aos-animate');
|
|
327
|
-
el.style.opacity = '1';
|
|
328
|
-
el.style.transform = 'none';
|
|
329
|
-
el.style.visibility = 'visible';
|
|
330
|
-
});
|
|
363
|
+
animation.finish();
|
|
331
364
|
} catch (e) {
|
|
332
|
-
|
|
365
|
+
try {
|
|
366
|
+
animation.currentTime =
|
|
367
|
+
animation.effect?.getTiming?.()?.duration || 0;
|
|
368
|
+
} catch (e2) {
|
|
369
|
+
/* ignore */
|
|
370
|
+
}
|
|
333
371
|
}
|
|
334
|
-
}
|
|
372
|
+
});
|
|
335
373
|
|
|
336
|
-
//
|
|
374
|
+
// 4. AOS - Mark all as animated
|
|
375
|
+
document.querySelectorAll('[data-aos]').forEach(el => {
|
|
376
|
+
el.classList.add('aos-animate');
|
|
377
|
+
el.setAttribute('data-aos-animate', 'true');
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// 5. WOW.js elements
|
|
337
381
|
document.querySelectorAll('.wow').forEach(el => {
|
|
338
382
|
el.classList.add('animated');
|
|
339
383
|
el.style.visibility = 'visible';
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// 6. ScrollReveal elements
|
|
387
|
+
document.querySelectorAll('[data-sr-id]').forEach(el => {
|
|
388
|
+
el.style.visibility = 'visible';
|
|
340
389
|
el.style.opacity = '1';
|
|
341
|
-
el.style.animationName = 'none';
|
|
342
390
|
});
|
|
343
391
|
|
|
344
|
-
//
|
|
345
|
-
const
|
|
346
|
-
if (
|
|
392
|
+
// 7. Anime.js - Complete all animations
|
|
393
|
+
const anime = safeGet(window, 'anime');
|
|
394
|
+
if (anime?.running) {
|
|
395
|
+
anime.running.forEach(anim => {
|
|
396
|
+
try {
|
|
397
|
+
anim.seek?.(anim.duration);
|
|
398
|
+
} catch (e) {
|
|
399
|
+
/* ignore */
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// 8. Lottie animations - Go to last frame
|
|
405
|
+
document.querySelectorAll('lottie-player, [data-lottie]').forEach(el => {
|
|
347
406
|
try {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
el.style.opacity = '1';
|
|
351
|
-
el.style.transform = 'none';
|
|
352
|
-
});
|
|
407
|
+
el.goToAndStop?.(el.totalFrames - 1, true);
|
|
408
|
+
el.pause?.();
|
|
353
409
|
} catch (e) {
|
|
354
410
|
/* ignore */
|
|
355
411
|
}
|
|
356
|
-
}
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
/* eslint-enable no-undef */
|
|
415
|
+
}
|
|
357
416
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
417
|
+
/**
|
|
418
|
+
* Force reveal any remaining hidden elements
|
|
419
|
+
* This is the final cleanup pass
|
|
420
|
+
*/
|
|
421
|
+
async _forceRevealHiddenElements() {
|
|
422
|
+
/* eslint-disable no-undef */
|
|
423
|
+
await this.page.evaluate(() => {
|
|
424
|
+
// 1. Force load all lazy images
|
|
425
|
+
document.querySelectorAll('img').forEach(img => {
|
|
426
|
+
// Load from data attributes
|
|
427
|
+
const lazySrc =
|
|
428
|
+
img.dataset.src || img.dataset.lazy || img.dataset.lazySrc;
|
|
429
|
+
if (lazySrc && !img.src.includes(lazySrc)) {
|
|
430
|
+
img.src = lazySrc;
|
|
431
|
+
}
|
|
432
|
+
// Remove lazy loading attribute
|
|
433
|
+
img.removeAttribute('loading');
|
|
434
|
+
// Trigger load if not loaded
|
|
435
|
+
if (!img.complete) {
|
|
436
|
+
img.loading = 'eager';
|
|
437
|
+
}
|
|
438
|
+
});
|
|
361
439
|
|
|
362
|
-
//
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const computedStyle = window.getComputedStyle(el);
|
|
369
|
-
if (
|
|
370
|
-
computedStyle.opacity === '0' &&
|
|
371
|
-
!el.hasAttribute('aria-hidden')
|
|
372
|
-
) {
|
|
373
|
-
el.style.opacity = '1';
|
|
374
|
-
}
|
|
375
|
-
});
|
|
440
|
+
// 2. Load lazy srcset
|
|
441
|
+
document.querySelectorAll('source[data-srcset]').forEach(source => {
|
|
442
|
+
if (source.dataset.srcset) {
|
|
443
|
+
source.srcset = source.dataset.srcset;
|
|
444
|
+
}
|
|
445
|
+
});
|
|
376
446
|
|
|
377
|
-
//
|
|
447
|
+
// 3. Load lazy background images
|
|
378
448
|
document
|
|
379
449
|
.querySelectorAll(
|
|
380
|
-
'[
|
|
450
|
+
'[data-bg], [data-background], [data-background-image]',
|
|
381
451
|
)
|
|
382
452
|
.forEach(el => {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
// Only fix if it looks like a scroll animation starting position
|
|
390
|
-
if (
|
|
391
|
-
style.includes('translateY(') &&
|
|
392
|
-
(style.includes('opacity') || el.classList.length > 0)
|
|
393
|
-
) {
|
|
394
|
-
el.style.transform = 'none';
|
|
395
|
-
}
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
// 7. Lazy-loaded images - force load
|
|
399
|
-
document
|
|
400
|
-
.querySelectorAll('img[data-src], img[data-lazy], img[loading="lazy"]')
|
|
401
|
-
.forEach(img => {
|
|
402
|
-
const src =
|
|
403
|
-
img.getAttribute('data-src') || img.getAttribute('data-lazy');
|
|
404
|
-
if (src && !img.src) {
|
|
405
|
-
img.src = src;
|
|
453
|
+
const bg =
|
|
454
|
+
el.dataset.bg ||
|
|
455
|
+
el.dataset.background ||
|
|
456
|
+
el.dataset.backgroundImage;
|
|
457
|
+
if (bg) {
|
|
458
|
+
el.style.backgroundImage = `url("${bg}")`;
|
|
406
459
|
}
|
|
407
|
-
// Remove lazy loading to ensure images load
|
|
408
|
-
img.removeAttribute('loading');
|
|
409
460
|
});
|
|
410
461
|
|
|
411
|
-
//
|
|
462
|
+
// 4. Load lazy iframes
|
|
412
463
|
document.querySelectorAll('iframe[data-src]').forEach(iframe => {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
iframe.src = src;
|
|
464
|
+
if (iframe.dataset.src) {
|
|
465
|
+
iframe.src = iframe.dataset.src;
|
|
416
466
|
}
|
|
417
467
|
});
|
|
418
468
|
|
|
419
|
-
//
|
|
420
|
-
document
|
|
421
|
-
.
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
469
|
+
// 5. Fix elements with opacity: 0 (likely animation end states)
|
|
470
|
+
document.querySelectorAll('*').forEach(el => {
|
|
471
|
+
const computed = window.getComputedStyle(el);
|
|
472
|
+
|
|
473
|
+
// Only fix visible containers that have hidden content
|
|
474
|
+
if (computed.opacity === '0' && !el.getAttribute('aria-hidden')) {
|
|
475
|
+
// Check if this is likely an animated element
|
|
476
|
+
const hasAnimClass =
|
|
477
|
+
el.className && /anim|fade|slide|reveal|show/i.test(el.className);
|
|
478
|
+
const hasAnimAttr =
|
|
479
|
+
el.hasAttribute('data-aos') ||
|
|
480
|
+
el.hasAttribute('data-animate') ||
|
|
481
|
+
el.hasAttribute('data-scroll');
|
|
482
|
+
|
|
483
|
+
if (hasAnimClass || hasAnimAttr || el.style.opacity === '0') {
|
|
484
|
+
el.style.setProperty('opacity', '1', 'important');
|
|
426
485
|
}
|
|
427
|
-
}
|
|
486
|
+
}
|
|
428
487
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
el.getAttribute('
|
|
433
|
-
|
|
434
|
-
|
|
488
|
+
// Fix visibility
|
|
489
|
+
if (
|
|
490
|
+
computed.visibility === 'hidden' &&
|
|
491
|
+
!el.getAttribute('aria-hidden')
|
|
492
|
+
) {
|
|
493
|
+
const hasAnimClass =
|
|
494
|
+
el.className && /anim|fade|slide|reveal|show/i.test(el.className);
|
|
495
|
+
if (hasAnimClass || el.style.visibility === 'hidden') {
|
|
496
|
+
el.style.setProperty('visibility', 'visible', 'important');
|
|
497
|
+
}
|
|
435
498
|
}
|
|
436
|
-
});
|
|
437
499
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
500
|
+
// Fix transforms that look like animation starting positions
|
|
501
|
+
if (computed.transform && computed.transform !== 'none') {
|
|
502
|
+
const transform = computed.transform;
|
|
503
|
+
// Check for translateY/translateX that might be animation starting positions
|
|
504
|
+
if (/translate[XY]\s*\(\s*[+-]?\d+/.test(transform)) {
|
|
505
|
+
const hasAnimClass =
|
|
506
|
+
el.className && /anim|fade|slide|reveal|show/i.test(el.className);
|
|
507
|
+
if (hasAnimClass) {
|
|
508
|
+
el.style.setProperty('transform', 'none', 'important');
|
|
509
|
+
}
|
|
446
510
|
}
|
|
447
|
-
} catch (e) {
|
|
448
|
-
/* ignore */
|
|
449
511
|
}
|
|
450
512
|
});
|
|
451
513
|
|
|
452
|
-
//
|
|
514
|
+
// 6. Force CSS animations to end state
|
|
453
515
|
document.querySelectorAll('*').forEach(el => {
|
|
454
|
-
const
|
|
455
|
-
if (
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
516
|
+
const computed = window.getComputedStyle(el);
|
|
517
|
+
if (computed.animationName && computed.animationName !== 'none') {
|
|
518
|
+
el.style.setProperty('animation', 'none', 'important');
|
|
519
|
+
}
|
|
520
|
+
if (
|
|
521
|
+
computed.transition &&
|
|
522
|
+
computed.transition !== 'none' &&
|
|
523
|
+
computed.transition !== 'all 0s ease 0s'
|
|
524
|
+
) {
|
|
525
|
+
el.style.setProperty('transition', 'none', 'important');
|
|
460
526
|
}
|
|
461
527
|
});
|
|
462
528
|
});
|