pursr 0.6.0 → 0.7.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/src/overlays.js CHANGED
@@ -1,170 +1,170 @@
1
- // Shared capture-side helpers: page-side CSS overlays, camera control,
2
- // frame stability wait.
3
-
4
- export const NAV_TIMEOUT_MS = 90_000;
5
- export const SETTLE_MS = 1200;
6
- export const CLICK_TIMEOUT_MS = 15_000;
7
- export const WAIT_DEFAULT_TIMEOUT_MS = 30_000;
8
-
9
- export async function gotoOrThrow(page, url, opts = {}) {
10
- const timeout = opts.timeoutMs || NAV_TIMEOUT_MS;
11
- const resp = await page.goto(url, { waitUntil: "domcontentloaded", timeout });
12
- if (!resp) throw new Error(`No response for ${url}`);
13
- const status = resp.status();
14
- if (status >= 400) throw new Error(`HTTP ${status} for ${url}`);
15
- return { status, title: await page.title() };
16
- }
17
-
18
- export async function settle(page) {
19
- await page.waitForTimeout(SETTLE_MS);
20
- }
21
-
22
- // --- overlays ---
23
-
24
- export async function overlayCursor(page, kind) {
25
- if (!kind || kind === "default" || kind === "none") return async () => {};
26
- const map = { pointer: "pointer", grab: "grab", grabbing: "grabbing", crosshair: "crosshair" };
27
- const css = map[kind] || "pointer";
28
- const marker = `/*purr_visual_cursor_${css}*/`;
29
- await page.addStyleTag({ content: `${marker}\n*, *::before, *::after { cursor: ${css} !important; }` });
30
- return async () => {
31
- await page.evaluate((m) => document.querySelectorAll("style").forEach(s => { if (s.textContent && s.textContent.includes(m)) s.remove(); }), marker).catch(() => {});
32
- };
33
- }
34
-
35
- export async function overlayGrid(page, opts = {}) {
36
- const tile = Math.max(8, Math.min(512, Number(opts.tileSize) || 64));
37
- // Sanitize color: allow only CSS-safe color tokens (hex, rgba, named)
38
- const raw = (opts.color || "rgba(255, 0, 255, 0.35)").trim();
39
- // Reject anything with braces, semicolons, quotes, or HTML — CSS injection guard
40
- const color = /^[a-zA-Z#()\d\s,%.]+$/.test(raw) ? raw : "rgba(255, 0, 255, 0.35)";
41
- await page.evaluate(({ tile, color }) => {
42
- let s = document.getElementById("__purr_visual_grid__");
43
- if (s) s.remove();
44
- s = document.createElement("style");
45
- s.id = "__purr_visual_grid__";
46
- s.textContent = `body::before { content: ""; position: fixed; inset: 0; pointer-events: none; z-index: 99999; background-image: linear-gradient(to right, ${color} 1px, transparent 1px), linear-gradient(to bottom, ${color} 1px, transparent 1px); background-size: ${tile}px ${tile}px, ${tile}px ${tile}px; }`;
47
- document.documentElement.appendChild(s);
48
- }, { tile, color });
49
- return async () => {
50
- await page.evaluate(() => document.getElementById("__purr_visual_grid__")?.remove()).catch(() => {});
51
- };
52
- }
53
-
54
- export async function hideHud(page) {
55
- const marker = "/*purr_visual_hide_hud*/";
56
- const css = marker + "\n" + [
57
- "header, footer, nav { display: none !important; }",
58
- ".hud-topbar, .bottom-nav { display: none !important; }",
59
- "[data-pursor-hud=\"hide\"] { display: none !important; }",
60
- ].join("\n");
61
- await page.addStyleTag({ content: css });
62
- return async () => {
63
- await page.evaluate((m) => document.querySelectorAll("style").forEach(s => { if (s.textContent && s.textContent.includes(m)) s.remove(); }), marker).catch(() => {});
64
- };
65
- }
66
-
67
- export async function isolateLayer(page, layer) {
68
- if (!layer || layer === "all" || layer === "none") return async () => {};
69
- let css = "";
70
- // entity = canvas only (game worlds typically render into <canvas>)
71
- if (layer === "entity") css = "[class*=\"bottom\"], [class*=\"hud\"], [class*=\"nav\"], [class*=\"bar\"], [class*=\"companion\"] { display: none !important; }";
72
- else if (layer === "terrain") css = "canvas { display: none !important; }";
73
- else if (layer === "hud") css = "[class*=\"hud\"], [class*=\"nav\"], [class*=\"bar\"] { display: none !important; }";
74
- else if (layer === "ui") css = "canvas, header, footer, main > nav { display: none !important; }";
75
- else throw new Error("unknown layer: " + layer);
76
- const marker = `/*purr_visual_layer_${layer}*/`;
77
- await page.addStyleTag({ content: `${marker}\n${css}` });
78
- return async () => {
79
- await page.evaluate((m) => document.querySelectorAll("style").forEach(s => { if (s.textContent && s.textContent.includes(m)) s.remove(); }), marker).catch(() => {});
80
- };
81
- }
82
-
83
- export async function freezeAnimation(page, freeze) {
84
- if (!freeze) return async () => {};
85
- const marker = "/*purr_visual_freeze*/";
86
- await page.addStyleTag({ content: `${marker}\n*, *::before, *::after { animation-play-state: paused !important; animation-delay: 0s !important; transition: none !important; }` });
87
- return async () => {
88
- await page.evaluate((m) => document.querySelectorAll("style").forEach(s => { if (s.textContent && s.textContent.includes(m)) s.remove(); }), marker).catch(() => {});
89
- };
90
- }
91
-
92
- export async function waitForStableFrame(page, ms) {
93
- if (!ms || ms <= 0) return;
94
- const hard = 8000;
95
- const t0 = Date.now();
96
- let lastHash = null, lastChange = Date.now();
97
- while (Date.now() - t0 < hard) {
98
- let h;
99
- try {
100
- h = await page.evaluate(() => {
101
- const c = document.querySelector("canvas");
102
- if (!c) return null;
103
- // Prefer WebGL context (common in game canvases), fall back to 2D
104
- let ctx, type;
105
- try {
106
- ctx = c.getContext("webgl2", { willReadFrequently: false }) || c.getContext("webgl");
107
- if (ctx) type = "webgl";
108
- } catch {}
109
- if (!ctx) {
110
- try { ctx = c.getContext("2d", { willReadFrequently: true }); type = "2d"; } catch {}
111
- }
112
- if (!ctx) return null;
113
- if (type === "webgl") {
114
- // Create a small readback for WebGL with complete framebuffer
115
- const fb = ctx.createFramebuffer();
116
- ctx.bindFramebuffer(ctx.FRAMEBUFFER, fb);
117
- const rb = ctx.createRenderbuffer();
118
- ctx.bindRenderbuffer(ctx.RENDERBUFFER, rb);
119
- ctx.renderbufferStorage(ctx.RENDERBUFFER, ctx.RGBA4, 4, 4);
120
- ctx.framebufferRenderbuffer(ctx.FRAMEBUFFER, ctx.COLOR_ATTACHMENT0, ctx.RENDERBUFFER, rb);
121
- const d = new Uint8Array(64);
122
- ctx.readPixels(0, 0, 4, 4, ctx.RGBA, ctx.UNSIGNED_BYTE, d);
123
- let acc = 0; for (let i = 0; i < d.length; i += 4) acc = (acc * 31 + d[i]) | 0;
124
- return acc.toString(36);
125
- } else {
126
- const d = ctx.getImageData(Math.floor(c.width / 2), Math.floor(c.height / 2), 32, 32).data;
127
- let acc = 0; for (let i = 0; i < d.length; i += 64) acc = (acc * 31 + d[i]) | 0;
128
- return acc.toString(36);
129
- }
130
- });
131
- } catch {
132
- // page detached mid-poll — give up
133
- return;
134
- }
135
- if (h && h === lastHash) { if (Date.now() - lastChange >= ms) return; }
136
- else { lastHash = h; lastChange = Date.now(); }
137
- await page.waitForTimeout(120);
138
- }
139
- }
140
-
141
- // --- camera ---
142
-
143
- export async function applyCamera(page, opts = {}) {
144
- if (!opts) return;
145
- const zoom = Number(opts.zoom) || 1;
146
- const panX = Number(opts.panX) || 0;
147
- const panY = Number(opts.panY) || 0;
148
- const center = await page.evaluate(() => {
149
- const c = document.querySelector("canvas");
150
- if (!c) return null;
151
- const r = c.getBoundingClientRect();
152
- return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
153
- });
154
- if (!center) return;
155
- if (zoom !== 1) {
156
- const factor = Math.log2(zoom) * 8;
157
- const delta = -120 * (factor > 0 ? 1 : -1);
158
- for (let i = 0; i < Math.max(1, Math.abs(Math.round(factor))); i++) {
159
- await page.mouse.move(center.x, center.y);
160
- await page.mouse.wheel(0, delta);
161
- await page.waitForTimeout(50);
162
- }
163
- }
164
- if (panX || panY) {
165
- await page.mouse.move(center.x, center.y);
166
- await page.mouse.down();
167
- await page.mouse.move(center.x + panX, center.y + panY, { steps: 10 });
168
- await page.mouse.up();
169
- }
1
+ // Shared capture-side helpers: page-side CSS overlays, camera control,
2
+ // frame stability wait.
3
+
4
+ export const NAV_TIMEOUT_MS = 90_000;
5
+ export const SETTLE_MS = 1200;
6
+ export const CLICK_TIMEOUT_MS = 15_000;
7
+ export const WAIT_DEFAULT_TIMEOUT_MS = 30_000;
8
+
9
+ export async function gotoOrThrow(page, url, opts = {}) {
10
+ const timeout = opts.timeoutMs || NAV_TIMEOUT_MS;
11
+ const resp = await page.goto(url, { waitUntil: "domcontentloaded", timeout });
12
+ if (!resp) throw new Error(`No response for ${url}`);
13
+ const status = resp.status();
14
+ if (status >= 400) throw new Error(`HTTP ${status} for ${url}`);
15
+ return { status, title: await page.title() };
16
+ }
17
+
18
+ export async function settle(page) {
19
+ await page.waitForTimeout(SETTLE_MS);
20
+ }
21
+
22
+ // --- overlays ---
23
+
24
+ export async function overlayCursor(page, kind) {
25
+ if (!kind || kind === "default" || kind === "none") return async () => {};
26
+ const map = { pointer: "pointer", grab: "grab", grabbing: "grabbing", crosshair: "crosshair" };
27
+ const css = map[kind] || "pointer";
28
+ const marker = `/*purr_visual_cursor_${css}*/`;
29
+ await page.addStyleTag({ content: `${marker}\n*, *::before, *::after { cursor: ${css} !important; }` });
30
+ return async () => {
31
+ await page.evaluate((m) => document.querySelectorAll("style").forEach(s => { if (s.textContent && s.textContent.includes(m)) s.remove(); }), marker).catch(() => {});
32
+ };
33
+ }
34
+
35
+ export async function overlayGrid(page, opts = {}) {
36
+ const tile = Math.max(8, Math.min(512, Number(opts.tileSize) || 64));
37
+ // Sanitize color: allow only CSS-safe color tokens (hex, rgba, named)
38
+ const raw = (opts.color || "rgba(255, 0, 255, 0.35)").trim();
39
+ // Reject anything with braces, semicolons, quotes, or HTML — CSS injection guard
40
+ const color = /^[a-zA-Z#()\d\s,%.]+$/.test(raw) ? raw : "rgba(255, 0, 255, 0.35)";
41
+ await page.evaluate(({ tile, color }) => {
42
+ let s = document.getElementById("__purr_visual_grid__");
43
+ if (s) s.remove();
44
+ s = document.createElement("style");
45
+ s.id = "__purr_visual_grid__";
46
+ s.textContent = `body::before { content: ""; position: fixed; inset: 0; pointer-events: none; z-index: 99999; background-image: linear-gradient(to right, ${color} 1px, transparent 1px), linear-gradient(to bottom, ${color} 1px, transparent 1px); background-size: ${tile}px ${tile}px, ${tile}px ${tile}px; }`;
47
+ document.documentElement.appendChild(s);
48
+ }, { tile, color });
49
+ return async () => {
50
+ await page.evaluate(() => document.getElementById("__purr_visual_grid__")?.remove()).catch(() => {});
51
+ };
52
+ }
53
+
54
+ export async function hideHud(page) {
55
+ const marker = "/*purr_visual_hide_hud*/";
56
+ const css = marker + "\n" + [
57
+ "header, footer, nav { display: none !important; }",
58
+ ".hud-topbar, .bottom-nav { display: none !important; }",
59
+ "[data-pursr-hud=\"hide\"] { display: none !important; }",
60
+ ].join("\n");
61
+ await page.addStyleTag({ content: css });
62
+ return async () => {
63
+ await page.evaluate((m) => document.querySelectorAll("style").forEach(s => { if (s.textContent && s.textContent.includes(m)) s.remove(); }), marker).catch(() => {});
64
+ };
65
+ }
66
+
67
+ export async function isolateLayer(page, layer) {
68
+ if (!layer || layer === "all" || layer === "none") return async () => {};
69
+ let css = "";
70
+ // entity = canvas only (game worlds typically render into <canvas>)
71
+ if (layer === "entity") css = "[class*=\"bottom\"], [class*=\"hud\"], [class*=\"nav\"], [class*=\"bar\"], [class*=\"companion\"] { display: none !important; }";
72
+ else if (layer === "terrain") css = "canvas { display: none !important; }";
73
+ else if (layer === "hud") css = "[class*=\"hud\"], [class*=\"nav\"], [class*=\"bar\"] { display: none !important; }";
74
+ else if (layer === "ui") css = "canvas, header, footer, main > nav { display: none !important; }";
75
+ else throw new Error("unknown layer: " + layer);
76
+ const marker = `/*purr_visual_layer_${layer}*/`;
77
+ await page.addStyleTag({ content: `${marker}\n${css}` });
78
+ return async () => {
79
+ await page.evaluate((m) => document.querySelectorAll("style").forEach(s => { if (s.textContent && s.textContent.includes(m)) s.remove(); }), marker).catch(() => {});
80
+ };
81
+ }
82
+
83
+ export async function freezeAnimation(page, freeze) {
84
+ if (!freeze) return async () => {};
85
+ const marker = "/*purr_visual_freeze*/";
86
+ await page.addStyleTag({ content: `${marker}\n*, *::before, *::after { animation-play-state: paused !important; animation-delay: 0s !important; transition: none !important; }` });
87
+ return async () => {
88
+ await page.evaluate((m) => document.querySelectorAll("style").forEach(s => { if (s.textContent && s.textContent.includes(m)) s.remove(); }), marker).catch(() => {});
89
+ };
90
+ }
91
+
92
+ export async function waitForStableFrame(page, ms) {
93
+ if (!ms || ms <= 0) return;
94
+ const hard = 8000;
95
+ const t0 = Date.now();
96
+ let lastHash = null, lastChange = Date.now();
97
+ while (Date.now() - t0 < hard) {
98
+ let h;
99
+ try {
100
+ h = await page.evaluate(() => {
101
+ const c = document.querySelector("canvas");
102
+ if (!c) return null;
103
+ // Prefer WebGL context (common in game canvases), fall back to 2D
104
+ let ctx, type;
105
+ try {
106
+ ctx = c.getContext("webgl2", { willReadFrequently: false }) || c.getContext("webgl");
107
+ if (ctx) type = "webgl";
108
+ } catch {}
109
+ if (!ctx) {
110
+ try { ctx = c.getContext("2d", { willReadFrequently: true }); type = "2d"; } catch {}
111
+ }
112
+ if (!ctx) return null;
113
+ if (type === "webgl") {
114
+ // Create a small readback for WebGL with complete framebuffer
115
+ const fb = ctx.createFramebuffer();
116
+ ctx.bindFramebuffer(ctx.FRAMEBUFFER, fb);
117
+ const rb = ctx.createRenderbuffer();
118
+ ctx.bindRenderbuffer(ctx.RENDERBUFFER, rb);
119
+ ctx.renderbufferStorage(ctx.RENDERBUFFER, ctx.RGBA4, 4, 4);
120
+ ctx.framebufferRenderbuffer(ctx.FRAMEBUFFER, ctx.COLOR_ATTACHMENT0, ctx.RENDERBUFFER, rb);
121
+ const d = new Uint8Array(64);
122
+ ctx.readPixels(0, 0, 4, 4, ctx.RGBA, ctx.UNSIGNED_BYTE, d);
123
+ let acc = 0; for (let i = 0; i < d.length; i += 4) acc = (acc * 31 + d[i]) | 0;
124
+ return acc.toString(36);
125
+ } else {
126
+ const d = ctx.getImageData(Math.floor(c.width / 2), Math.floor(c.height / 2), 32, 32).data;
127
+ let acc = 0; for (let i = 0; i < d.length; i += 64) acc = (acc * 31 + d[i]) | 0;
128
+ return acc.toString(36);
129
+ }
130
+ });
131
+ } catch {
132
+ // page detached mid-poll — give up
133
+ return;
134
+ }
135
+ if (h && h === lastHash) { if (Date.now() - lastChange >= ms) return; }
136
+ else { lastHash = h; lastChange = Date.now(); }
137
+ await page.waitForTimeout(120);
138
+ }
139
+ }
140
+
141
+ // --- camera ---
142
+
143
+ export async function applyCamera(page, opts = {}) {
144
+ if (!opts) return;
145
+ const zoom = Number(opts.zoom) || 1;
146
+ const panX = Number(opts.panX) || 0;
147
+ const panY = Number(opts.panY) || 0;
148
+ const center = await page.evaluate(() => {
149
+ const c = document.querySelector("canvas");
150
+ if (!c) return null;
151
+ const r = c.getBoundingClientRect();
152
+ return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
153
+ });
154
+ if (!center) return;
155
+ if (zoom !== 1) {
156
+ const factor = Math.log2(zoom) * 8;
157
+ const delta = -120 * (factor > 0 ? 1 : -1);
158
+ for (let i = 0; i < Math.max(1, Math.abs(Math.round(factor))); i++) {
159
+ await page.mouse.move(center.x, center.y);
160
+ await page.mouse.wheel(0, delta);
161
+ await page.waitForTimeout(50);
162
+ }
163
+ }
164
+ if (panX || panY) {
165
+ await page.mouse.move(center.x, center.y);
166
+ await page.mouse.down();
167
+ await page.mouse.move(center.x + panX, center.y + panY, { steps: 10 });
168
+ await page.mouse.up();
169
+ }
170
170
  }