@littlepartytime/dev-kit 1.18.0 → 1.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,22 +10,68 @@ const CAPTURE_H = 751; // 844 - 59 - 34
10
10
  *
11
11
  * Also exposed on window.__devkit__.captureScreen() for LLM/Playwright callers:
12
12
  * await page.evaluate(() => window.__devkit__.captureScreen())
13
+ *
14
+ * ## Why the style normalization?
15
+ * The PhoneFrame has two CSS properties that break html2canvas:
16
+ *
17
+ * 1. `contain: paint` on Screen div and safe-area div — html2canvas uses
18
+ * getBoundingClientRect() to determine clip rects. With a scaled ancestor,
19
+ * getBoundingClientRect() returns the *visual* (post-transform) size. Combined
20
+ * with contain:paint, html2canvas clips the rendered output to the visual height
21
+ * (e.g. 375px at 0.5× scale) even though the canvas is 751px tall, leaving the
22
+ * bottom half black.
23
+ *
24
+ * 2. `transform: scale(x)` on the phone body — causes the same getBoundingClientRect
25
+ * mismatch; the element's intrinsic size is 390×751 but its visual rect is smaller.
26
+ *
27
+ * Fix: temporarily set both to neutral values, wait two rAF ticks for the browser to
28
+ * apply the changes, run html2canvas, then restore. An invisible overlay prevents the
29
+ * user from seeing the brief layout shift.
13
30
  */
14
31
  export async function captureScreen(): Promise<string> {
15
32
  const el = document.getElementById('devkit-game-screen');
16
33
  if (!el) throw new Error('[devkit] #devkit-game-screen not found — is the Preview page active?');
17
34
 
18
- const canvas = await html2canvas(el, {
19
- width: CAPTURE_W,
20
- height: CAPTURE_H,
21
- scale: 2,
22
- logging: false,
23
- backgroundColor: null,
24
- useCORS: true,
25
- allowTaint: true,
26
- });
27
-
28
- return canvas.toDataURL('image/png');
35
+ // Invisible overlay so the user never sees the brief style normalization flash.
36
+ const overlay = document.createElement('div');
37
+ overlay.style.cssText = 'position:fixed;inset:0;z-index:99999;pointer-events:none;';
38
+ document.body.appendChild(overlay);
39
+
40
+ // Wait for overlay to paint before touching any styles.
41
+ await new Promise<void>(r => requestAnimationFrame(() => requestAnimationFrame(r)));
42
+
43
+ // Walk up the DOM and neutralise contain:paint and transform:scale on all ancestors.
44
+ const saved: Array<[HTMLElement, 'contain' | 'transform', string]> = [];
45
+ let node: HTMLElement | null = el;
46
+ while (node && node !== document.body) {
47
+ for (const prop of ['contain', 'transform'] as const) {
48
+ if (node.style[prop]) {
49
+ saved.push([node, prop, node.style[prop]]);
50
+ node.style[prop] = prop === 'transform' ? 'none' : '';
51
+ }
52
+ }
53
+ node = node.parentElement;
54
+ }
55
+
56
+ // Wait for the browser to reflow/repaint with the normalised styles.
57
+ await new Promise<void>(r => requestAnimationFrame(() => requestAnimationFrame(r)));
58
+
59
+ try {
60
+ const canvas = await html2canvas(el, {
61
+ width: CAPTURE_W,
62
+ height: CAPTURE_H,
63
+ scale: 2,
64
+ logging: false,
65
+ backgroundColor: null,
66
+ useCORS: true,
67
+ allowTaint: true,
68
+ });
69
+ return canvas.toDataURL('image/png');
70
+ } finally {
71
+ // Restore styles and remove overlay regardless of success/failure.
72
+ for (const [node, prop, val] of saved) node.style[prop] = val;
73
+ document.body.removeChild(overlay);
74
+ }
29
75
  }
30
76
 
31
77
  export function downloadScreenshot(dataUrl: string): void {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@littlepartytime/dev-kit",
3
- "version": "1.18.0",
3
+ "version": "1.18.1",
4
4
  "description": "Development toolkit CLI for Little Party Time game developers",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",