@augurworks/augur 0.15.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.
Files changed (190) hide show
  1. package/CANVAS.md +858 -0
  2. package/INSTALL.md +603 -0
  3. package/LICENSE +27 -0
  4. package/README.md +150 -0
  5. package/TRADEMARKS.md +72 -0
  6. package/agents/README.md +65 -0
  7. package/agents/canon-extract.md +117 -0
  8. package/agents/canon.md +151 -0
  9. package/agents/canvas.md +88 -0
  10. package/agents/currency.md +74 -0
  11. package/agents/drafts.md +92 -0
  12. package/agents/identity.md +148 -0
  13. package/agents/prototype-contract.md +69 -0
  14. package/agents/publishing.md +324 -0
  15. package/agents/review-feedback.md +76 -0
  16. package/agents/space-json.md +132 -0
  17. package/agents/ui-skill.md +101 -0
  18. package/agents/working-marks.md +86 -0
  19. package/brand/augur-eye.svg +3 -0
  20. package/brand/augur-icon.svg +41 -0
  21. package/brand/augur-mark.svg +5 -0
  22. package/brand/lentianova-bulky.otf +0 -0
  23. package/brand/lentianova-median.otf +0 -0
  24. package/brand/lentianova.woff2 +0 -0
  25. package/build.js +8409 -0
  26. package/changelog.md +133 -0
  27. package/fonts/inter-latin-wght-normal.woff2 +0 -0
  28. package/package.json +57 -0
  29. package/pitis/PETS.md +121 -0
  30. package/pitis/README.md +117 -0
  31. package/pitis/index.html +266 -0
  32. package/pitis/piti.build.js +81 -0
  33. package/pitis/piti.js +929 -0
  34. package/pitis/roast-agent.md +185 -0
  35. package/realtime/src/index.js +119 -0
  36. package/realtime/wrangler.example.toml +44 -0
  37. package/scripts/adopt.mjs +106 -0
  38. package/scripts/board-snapshot.mjs +197 -0
  39. package/scripts/build-seed-pack.mjs +33 -0
  40. package/scripts/bundle-rekey.mjs +112 -0
  41. package/scripts/bundle-tenancy-rehearsal.mjs +781 -0
  42. package/scripts/canon-extract.mjs +578 -0
  43. package/scripts/canon.mjs +599 -0
  44. package/scripts/canvas-screen.mjs +333 -0
  45. package/scripts/check-local.mjs +136 -0
  46. package/scripts/clawd-art.mjs +75 -0
  47. package/scripts/clawd-canvas.mjs +741 -0
  48. package/scripts/cli.mjs +100 -0
  49. package/scripts/clone.mjs +422 -0
  50. package/scripts/close.mjs +30 -0
  51. package/scripts/connect.mjs +103 -0
  52. package/scripts/deploy.mjs +157 -0
  53. package/scripts/dev.mjs +54 -0
  54. package/scripts/doc-lint.mjs +143 -0
  55. package/scripts/export.mjs +300 -0
  56. package/scripts/fork.mjs +64 -0
  57. package/scripts/freeze.mjs +74 -0
  58. package/scripts/frontdoor-parity.mjs +180 -0
  59. package/scripts/hook.mjs +67 -0
  60. package/scripts/identity-rekey.mjs +112 -0
  61. package/scripts/init.mjs +122 -0
  62. package/scripts/land.mjs +40 -0
  63. package/scripts/lib/adapters.mjs +156 -0
  64. package/scripts/lib/board-snapshot.mjs +511 -0
  65. package/scripts/lib/draft.mjs +385 -0
  66. package/scripts/lib/graduate.mjs +276 -0
  67. package/scripts/lib/instance.mjs +74 -0
  68. package/scripts/lib/marks.mjs +107 -0
  69. package/scripts/lib/materialize.mjs +138 -0
  70. package/scripts/lib/merge3.mjs +124 -0
  71. package/scripts/lib/offline-posture.mjs +120 -0
  72. package/scripts/lib/offline-respawn.mjs +18 -0
  73. package/scripts/lib/offline-wrangler.mjs +78 -0
  74. package/scripts/lib/publish-compose.mjs +12 -0
  75. package/scripts/lib/publish-conflict.mjs +78 -0
  76. package/scripts/lib/publish-evidence.mjs +144 -0
  77. package/scripts/lib/refine-compare.mjs +140 -0
  78. package/scripts/lib/refine-ledger.mjs +150 -0
  79. package/scripts/lib/refine-png.mjs +149 -0
  80. package/scripts/lib/seed-pack-build.mjs +141 -0
  81. package/scripts/lib/state-compare.mjs +68 -0
  82. package/scripts/lib/store.mjs +122 -0
  83. package/scripts/login.mjs +84 -0
  84. package/scripts/mark.mjs +112 -0
  85. package/scripts/migrate-board-keys.mjs +164 -0
  86. package/scripts/migrate.mjs +265 -0
  87. package/scripts/no-foreign-vocabulary.mjs +216 -0
  88. package/scripts/no-product-names.mjs +157 -0
  89. package/scripts/no-tenant-globals.mjs +1134 -0
  90. package/scripts/offline.mjs +247 -0
  91. package/scripts/og.mjs +170 -0
  92. package/scripts/one-tenant-resolver.mjs +146 -0
  93. package/scripts/open.mjs +63 -0
  94. package/scripts/publish.mjs +1041 -0
  95. package/scripts/read.mjs +31 -0
  96. package/scripts/refine.mjs +455 -0
  97. package/scripts/release-drift.mjs +138 -0
  98. package/scripts/restore.mjs +322 -0
  99. package/scripts/review.mjs +125 -0
  100. package/scripts/save.mjs +21 -0
  101. package/scripts/shell-lint.mjs +189 -0
  102. package/scripts/ship.mjs +460 -0
  103. package/scripts/shoot.mjs +264 -0
  104. package/scripts/smoke-invite.mjs +113 -0
  105. package/scripts/state-inventory.mjs +223 -0
  106. package/scripts/status.mjs +177 -0
  107. package/scripts/sync.mjs +27 -0
  108. package/scripts/tenant-do-rehearsal.mjs +1314 -0
  109. package/scripts/ui-copy-lint.mjs +202 -0
  110. package/scripts/watch.mjs +32 -0
  111. package/scripts/wrangler-preflight.mjs +246 -0
  112. package/seed/CANON.md +43 -0
  113. package/seed/README.md +119 -0
  114. package/seed/prototype-status.json +9 -0
  115. package/seed/registry.json +88 -0
  116. package/seed/skills/starter-ui/SKILL.md +52 -0
  117. package/seed/skills/starter-ui/skill.json +11 -0
  118. package/seed/skills/starter-ui/starter-tokens.css +73 -0
  119. package/seed/skills/starter-ui/starter-ui.css +318 -0
  120. package/seed/skills/starter-ui/starter-ui.js +50 -0
  121. package/seed/space.json +5 -0
  122. package/seed/start-here/prototypes/connect-your-terminal/index.html +428 -0
  123. package/seed/start-here/prototypes/connect-your-terminal/preview.webp +0 -0
  124. package/seed/start-here/prototypes/sample-with-comments/index.html +172 -0
  125. package/seed/start-here/prototypes/sample-with-comments/preview.webp +0 -0
  126. package/seed/start-here/prototypes/set-up-your-design-system/index.html +353 -0
  127. package/seed/start-here/prototypes/set-up-your-design-system/preview.webp +0 -0
  128. package/seed/threads.json +52 -0
  129. package/seed/worked-examples/prototypes/field-readings/index.html +273 -0
  130. package/seed/worked-examples/prototypes/field-readings/preview.webp +0 -0
  131. package/seed/worked-examples/prototypes/slide-deck/index.html +214 -0
  132. package/seed/worked-examples/prototypes/slide-deck/preview.webp +0 -0
  133. package/seed/worked-examples/prototypes/specimen-viewer/index.html +203 -0
  134. package/seed/worked-examples/prototypes/specimen-viewer/preview.webp +0 -0
  135. package/src/_worker.js +12668 -0
  136. package/src/board-key.mjs +57 -0
  137. package/src/board-room.mjs +881 -0
  138. package/src/bundle-keys.mjs +138 -0
  139. package/src/canon/collect-in-browser.js +215 -0
  140. package/src/canon/emit.mjs +232 -0
  141. package/src/canon/observe.mjs +260 -0
  142. package/src/canon/schema.mjs +451 -0
  143. package/src/canvas/DSEG-LICENSE.txt +95 -0
  144. package/src/canvas/DSEG7Classic-Bold.woff2 +0 -0
  145. package/src/canvas/canvas.css +885 -0
  146. package/src/canvas/canvas.js +5780 -0
  147. package/src/canvas/capture.js +485 -0
  148. package/src/chrome/appchrome.mjs +568 -0
  149. package/src/currency.mjs +203 -0
  150. package/src/delete-confirmation.mjs +254 -0
  151. package/src/drafts/drafts.js +326 -0
  152. package/src/entry.js +58 -0
  153. package/src/galleries.mjs +400 -0
  154. package/src/health-cron.mjs +173 -0
  155. package/src/identity.json +1 -0
  156. package/src/kv-codec.mjs +135 -0
  157. package/src/kv-identity.mjs +259 -0
  158. package/src/mail.mjs +599 -0
  159. package/src/provenance.mjs +74 -0
  160. package/src/publish-compose.mjs +363 -0
  161. package/src/publish-fork.mjs +221 -0
  162. package/src/publish-units.mjs +44 -0
  163. package/src/purge.mjs +81 -0
  164. package/src/review/COMMENTING-UX.md +306 -0
  165. package/src/review/cat.png +0 -0
  166. package/src/review/comment-cursor.svg +7 -0
  167. package/src/review/comments.js +1738 -0
  168. package/src/room-ticket.mjs +96 -0
  169. package/src/roster-chip.mjs +32 -0
  170. package/src/seed-pack.mjs +261 -0
  171. package/src/state-inventory.mjs +249 -0
  172. package/src/sw-logic.mjs +48 -0
  173. package/src/tenant-cache.mjs +141 -0
  174. package/src/tenant-context.mjs +386 -0
  175. package/src/tenant-do.js +2940 -0
  176. package/src/tenant-host.mjs +144 -0
  177. package/src/tenant-quotas.mjs +140 -0
  178. package/src/unit-core.mjs +103 -0
  179. package/src/unit-object.mjs +338 -0
  180. package/templates/README.md +67 -0
  181. package/templates/shell/deploy.yml +145 -0
  182. package/templates/shell/engine-bump.yml +142 -0
  183. package/templates/shell/health.yml +233 -0
  184. package/templates/shell/kv-backup.yml +201 -0
  185. package/templates/shell/roster-update.yml +110 -0
  186. package/templates/shell/space-preflight.yml +35 -0
  187. package/templates/shell/space-rebake.yml +79 -0
  188. package/templates/shell/store-backup.yml +152 -0
  189. package/templates/shell/wrangler.example.toml +266 -0
  190. package/templates/space/publish.yml +91 -0
@@ -0,0 +1,485 @@
1
+ /* Augur Canvas — capture: rasterize a set of canvas nodes to a PNG. Served from
2
+ * /__canvas/capture.js and LAZY-LOADED by canvas.js on the first ⌘⇧C, so a board pays
3
+ * nothing for it until someone asks for a picture.
4
+ *
5
+ * The one export is board-agnostic on purpose: it takes DOM elements and a world rect and
6
+ * knows nothing about board state, node types or the multiplayer room.
7
+ *
8
+ * GVCanvasCapture.nodesToPng({ els, rect, scale, background, poster, onInfo }) -> Promise<Blob>
9
+ *
10
+ * els node host elements to draw, in Z-ORDER (i.e. DOM order in the world layer)
11
+ * rect {x,y,w,h} world box to capture — the output frame
12
+ * scale output pixels per world px (2 = the "retina screenshot" default)
13
+ * background optional {fill, dot, step, r}: paper + dot grid, drawn natively
14
+ * poster(el) optional: a still image URL to stand in for a live frame that can't be read
15
+ * onInfo(i) optional: {scale, downgraded, frames:{ok,failed}} once the raster is done
16
+ *
17
+ * HOW IT WORKS — re-render, not screen capture. The nodes are cloned into a mini "world"
18
+ * at scale 1, offset by -rect.x/-rect.y, and rasterized through <foreignObject> into a 2D
19
+ * canvas. That is why the output is crisp at 2x NATURAL size no matter what zoom the board
20
+ * is at, why editor chrome can be stripped, and why nothing asks for a screen-share
21
+ * permission. Three layers composite into one canvas so one bad node degrades instead of
22
+ * killing the capture:
23
+ *
24
+ * 1. paper fill + dot grid, drawn straight into the 2D context
25
+ * 2. the nodes themselves, cloned + inlined + rasterized in <foreignObject> passes
26
+ * 3. live embedded frames, each rasterized in its OWN isolated pass and composited
27
+ *
28
+ * Layer 3 is separate because a framed page carries its own stylesheet, and one shared
29
+ * document would leak that stylesheet across every other node in the shot. The passes in
30
+ * layer 2 are split at each frame so z-order still holds (a note dropped on top of a frame
31
+ * stays on top of it in the shot).
32
+ *
33
+ * ⚠️ An <img> holding an SVG never fetches external references while it rasterizes. So
34
+ * EVERYTHING has to travel inline: stylesheets are read out of document.styleSheets, every
35
+ * url() in them is absolutized and then swallowed as a data URI, and every <img> src is
36
+ * swapped for one too. Anything that can't be inlined is dropped rather than left as a
37
+ * broken reference.
38
+ */
39
+ (function () {
40
+ "use strict";
41
+ if (window.GVCanvasCapture) return;
42
+
43
+ var XHTML = "http://www.w3.org/1999/xhtml";
44
+ var MAX_PIXELS = 40e6; // ~40MP: past this a browser starts handing back empty blobs
45
+ var MAX_RESOURCE = 6e6; // one inlined asset
46
+ var MAX_INLINED = 24e6; // all inlined assets in one capture
47
+ var FETCH_MS = 8000;
48
+
49
+ // editor chrome — everything here is UI for working the board, not board content
50
+ var CHROME = ".gvc-resize,.gvc-rot,.gvc-handle,.gvc-addrow,.gvc-addcol,.gvc-hit,.gvc-draghandle," +
51
+ ".gvc-peer-sel,.gvc-remote-focus,.gvc-crop,.gvc-cropwin";
52
+ var CHROME_CLASSES = ["sel", "editing", "interacting", "gvc-linked", "gvc-remote-move", "dropping"];
53
+ // chrome that is counter-scaled to the live zoom on the board; the capture is at natural
54
+ // scale, so these go back to 1:1 (they're content — a frame's name, a section's label)
55
+ var COUNTER_SCALED = ".gvc-tilename,.gvc-seclabel";
56
+
57
+ // ⚠️ A rasterizing SVG is frozen at time ZERO, so any entrance animation is caught at its
58
+ // FIRST keyframe. With the common `animation: fade-in .4s both` that means opacity 0 — a page
59
+ // that reads perfectly on screen comes out as an empty panel (this cost an afternoon: the
60
+ // frame's nav and footer drew, the whole slide did not). Zero duration + zero delay lands
61
+ // every animation on its END state instead, which is the settled page you were looking at.
62
+ var FROZEN = "\n*,*::before,*::after{animation-duration:0s!important;animation-delay:0s!important;" +
63
+ "transition:none!important;caret-color:transparent!important;}\n";
64
+
65
+ var inlined = 0;
66
+ var resCache = {}; // absolute url -> Promise<dataURI|null> (+ .__value once settled)
67
+ var hostCss = null; // the board page's own stylesheet, inlined once per session
68
+
69
+ /* ---------- inlining ---------- */
70
+
71
+ function each(list, fn) { Array.prototype.forEach.call(list, fn); }
72
+ function abs(url, base) {
73
+ if (!url) return null;
74
+ try { return new URL(url, base).href; } catch (e) { return null; }
75
+ }
76
+ function readAsDataUri(blob) {
77
+ return new Promise(function (resolve, reject) {
78
+ var fr = new FileReader();
79
+ fr.onload = function () { resolve(fr.result); };
80
+ fr.onerror = function () { reject(new Error("read")); };
81
+ fr.readAsDataURL(blob);
82
+ });
83
+ }
84
+ // Never rejects: a resource that can't be inlined resolves to null and is dropped, because
85
+ // a shot missing one image beats no shot at all.
86
+ function asDataUri(url) {
87
+ if (!url) return Promise.resolve(null);
88
+ if (/^data:/i.test(url)) return Promise.resolve(url);
89
+ if (resCache[url]) return resCache[url];
90
+ var ctl = typeof AbortController === "function" ? new AbortController() : null;
91
+ var timer = ctl ? setTimeout(function () { try { ctl.abort(); } catch (e) {} }, FETCH_MS) : null;
92
+ var p = fetch(url, { credentials: "same-origin", signal: ctl ? ctl.signal : undefined })
93
+ .then(function (r) { if (!r.ok) throw new Error("http"); return r.blob(); })
94
+ .then(function (b) {
95
+ if (b.size > MAX_RESOURCE || inlined + b.size > MAX_INLINED) throw new Error("too big");
96
+ inlined += b.size;
97
+ return readAsDataUri(b);
98
+ })
99
+ .catch(function () { return null; })
100
+ .then(function (v) { if (timer) clearTimeout(timer); return v; });
101
+ resCache[url] = p;
102
+ return p;
103
+ }
104
+ // resolve a batch and park each value ON its cached promise, so the clone pass that needs
105
+ // them can stay synchronous
106
+ function settle(urls) {
107
+ return Promise.all(urls.map(function (u) {
108
+ var p = asDataUri(u);
109
+ return p.then(function (v) { p.__value = v; return v; });
110
+ }));
111
+ }
112
+
113
+ /* ---------- stylesheets ---------- */
114
+
115
+ function rulesText(sheet, base) {
116
+ var rules;
117
+ try { rules = sheet.cssRules; } catch (e) { return ""; } // cross-origin sheet: unreadable
118
+ if (!rules) return "";
119
+ var out = "";
120
+ for (var i = 0; i < rules.length; i++) {
121
+ var r = rules[i];
122
+ // @import: take the imported sheet's rules, drop the statement (it would never load)
123
+ if (r.styleSheet) { out += rulesText(r.styleSheet, r.styleSheet.href || base); continue; }
124
+ out += r.cssText + "\n";
125
+ }
126
+ return absolutizeCss(out, base);
127
+ }
128
+ function absolutizeCss(text, base) {
129
+ return text.replace(/url\((\s*['"]?)([^'")]+)\1\s*\)/g, function (m, q, u) {
130
+ if (/^(data:|blob:|#)/i.test(u)) return m;
131
+ var a = abs(u, base);
132
+ return a ? 'url("' + a + '")' : m;
133
+ });
134
+ }
135
+ function docCss(doc) {
136
+ var out = "";
137
+ var sheets = doc.styleSheets;
138
+ for (var i = 0; i < sheets.length; i++) out += rulesText(sheets[i], sheets[i].href || doc.baseURI);
139
+ return out;
140
+ }
141
+ // swallow every url() the stylesheet still points at — fonts above all, since a page's
142
+ // typeface is the difference between "that's the prototype" and "that's some text"
143
+ function inlineCssUrls(css) {
144
+ var urls = [], re = /url\("([^"]+)"\)/g, m;
145
+ while ((m = re.exec(css))) if (!/^data:/i.test(m[1]) && urls.indexOf(m[1]) < 0) urls.push(m[1]);
146
+ if (!urls.length) return Promise.resolve(css);
147
+ return Promise.all(urls.map(function (u) {
148
+ return asDataUri(u).then(function (d) { return { u: u, d: d }; });
149
+ })).then(function (list) {
150
+ list.forEach(function (it) {
151
+ if (it.d) css = css.split('url("' + it.u + '")').join('url("' + it.d + '")');
152
+ });
153
+ return css;
154
+ });
155
+ }
156
+
157
+ /* ---------- cloning ---------- */
158
+
159
+ // A clone is inert markup, so anything the DOM was holding live has to be written down:
160
+ // canvas pixels, form values. Source and clone are walked in parallel — querySelectorAll
161
+ // returns document order in both, so the pairs line up. Call this BEFORE pruning the clone.
162
+ function carryLiveState(src, clone) {
163
+ var sc = src.querySelectorAll("canvas"), cc = clone.querySelectorAll("canvas");
164
+ for (var i = 0; i < sc.length && i < cc.length; i++) {
165
+ var data = null;
166
+ try { data = sc[i].width && sc[i].height ? sc[i].toDataURL() : null; } catch (e) { data = null; }
167
+ var rep = document.createElement("img");
168
+ rep.setAttribute("style", (cc[i].getAttribute("style") || "") +
169
+ ";width:" + sc[i].offsetWidth + "px;height:" + sc[i].offsetHeight + "px");
170
+ if (data) rep.setAttribute("src", data);
171
+ if (cc[i].parentNode) cc[i].parentNode.replaceChild(rep, cc[i]);
172
+ }
173
+ var si = src.querySelectorAll("input,textarea,select"), ci = clone.querySelectorAll("input,textarea,select");
174
+ for (var j = 0; j < si.length && j < ci.length; j++) {
175
+ var s = si[j], c = ci[j];
176
+ if (s.tagName === "TEXTAREA") c.textContent = s.value;
177
+ else if (s.tagName === "SELECT") {
178
+ each(c.options, function (o, k) {
179
+ if (s.options[k] && s.options[k].selected) o.setAttribute("selected", "selected");
180
+ else o.removeAttribute("selected");
181
+ });
182
+ } else if (s.type === "checkbox" || s.type === "radio") {
183
+ if (s.checked) c.setAttribute("checked", "checked"); else c.removeAttribute("checked");
184
+ } else c.setAttribute("value", s.value == null ? "" : s.value);
185
+ }
186
+ }
187
+ function dropScripts(clone) {
188
+ each(clone.querySelectorAll("script,noscript,iframe,object,embed,link[rel~=stylesheet]"), function (e) { e.remove(); });
189
+ // the review overlay hangs its whole UI in a shadow root on this element — chrome, and a
190
+ // clone can't see into it anyway
191
+ each(clone.querySelectorAll("#gv-review-host,#gvc-cursors,#gvc-ui,#gvc-guides"), function (e) { e.remove(); });
192
+ }
193
+ // ⚠️ Only ever run this on the ENGINE's own nodes. A framed page is somebody else's markup,
194
+ // where a class called "sel" or "editing" is theirs and means something.
195
+ function stripChrome(clone) {
196
+ CHROME_CLASSES.forEach(function (c) { clone.classList.remove(c); });
197
+ each(clone.querySelectorAll(CHROME), function (e) { e.remove(); });
198
+ each(clone.querySelectorAll("." + CHROME_CLASSES.join(",.")), function (e) {
199
+ CHROME_CLASSES.forEach(function (c) { e.classList.remove(c); });
200
+ });
201
+ each(clone.querySelectorAll(COUNTER_SCALED), function (e) {
202
+ e.style.transform = "none";
203
+ e.style.maxWidth = "100%";
204
+ });
205
+ }
206
+ function freezeEditables(clone) {
207
+ each(clone.querySelectorAll("[contenteditable]"), function (e) { e.setAttribute("contenteditable", "false"); });
208
+ }
209
+
210
+ // Every <img> in the shot, so it can be swapped for a data URI before the clone is
211
+ // serialized (the rasterizer will not fetch anything itself).
212
+ function imageUrls(el, base) {
213
+ var urls = [];
214
+ each(el.querySelectorAll("img[src]"), function (i) {
215
+ var a = abs(i.getAttribute("src"), base); if (a) urls.push(a);
216
+ });
217
+ return urls;
218
+ }
219
+ function swapImages(clone, base) {
220
+ each(clone.querySelectorAll("img"), function (i) {
221
+ var raw = i.getAttribute("src") || "";
222
+ if (/^data:/i.test(raw)) return;
223
+ i.removeAttribute("srcset");
224
+ var a = abs(raw, base);
225
+ var v = a && resCache[a] ? resCache[a].__value : null;
226
+ if (v) i.setAttribute("src", v); else i.remove();
227
+ });
228
+ }
229
+
230
+ /* ---------- rasterizing through <foreignObject> ---------- */
231
+
232
+ // ⚠️ DATA URL, NEVER A BLOB URL. An <img> carrying the same SVG loads happily from either,
233
+ // but a blob: one TAINTS the canvas it is drawn into (opaque origin) — and the tainting only
234
+ // shows up at the very end, as toBlob throwing SecurityError. Verified in Chromium: data =
235
+ // clean, blob = tainted. Blob URLs would be the tidier choice for a payload this big; they
236
+ // are simply not an option here.
237
+ function svgUrlImage(svg) {
238
+ return new Promise(function (resolve, reject) {
239
+ var img = new Image();
240
+ img.onload = function () { resolve(img); };
241
+ img.onerror = function () { reject(new Error("svg")); };
242
+ img.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg);
243
+ });
244
+ }
245
+ function serialize(el) {
246
+ el.setAttribute("xmlns", XHTML);
247
+ return new XMLSerializer().serializeToString(el);
248
+ }
249
+ // w/h = the CSS box the content lays out in; k = pixels per CSS px in the image we want back.
250
+ // ⚠️ The upscale is a CSS transform on the content, NOT an SVG viewBox. WebKit lays a
251
+ // foreignObject out at 1:1 and ignores the viewBox scale, which put the whole capture in the
252
+ // top-left quadrant of its own bitmap at half size (Chromium honours the viewBox, so this
253
+ // only showed up when the same test was run under WebKit). Keeping viewBox == width/height
254
+ // means no engine has to scale anything.
255
+ function rasterize(el, w, h, k) {
256
+ var outW = Math.max(1, Math.round(w * k)), outH = Math.max(1, Math.round(h * k));
257
+ var box = document.createElement("div");
258
+ box.setAttribute("style", "width:" + w + "px;height:" + h + "px;transform:scale(" + k + ");transform-origin:top left;");
259
+ box.appendChild(el);
260
+ var svg = '<svg xmlns="http://www.w3.org/2000/svg" width="' + outW + '" height="' + outH +
261
+ '" viewBox="0 0 ' + outW + " " + outH + '"><foreignObject x="0" y="0" width="' + outW + '" height="' + outH + '">' +
262
+ serialize(box) + "</foreignObject></svg>";
263
+ return svgUrlImage(svg).then(function (img) {
264
+ if (!img.decode) return img;
265
+ return img.decode().then(function () { return img; }, function () { return img; });
266
+ });
267
+ }
268
+
269
+ /* ---------- layer 1: paper + dot grid ---------- */
270
+
271
+ function drawBackground(ctx, rect, bg) {
272
+ if (!bg) return;
273
+ if (bg.fill) { ctx.fillStyle = bg.fill; ctx.fillRect(0, 0, rect.w, rect.h); }
274
+ var step = bg.step || 0, r = bg.r || 0;
275
+ if (!bg.dot || !step || !r) return;
276
+ // the live grid's dots sit at the CENTRE of each step-sized tile anchored on the world
277
+ // origin — world (step/2 + k*step) — which is exactly what makes them camera-independent
278
+ ctx.fillStyle = bg.dot;
279
+ var half = step / 2;
280
+ var x0 = Math.floor((rect.x - half) / step) * step + half;
281
+ var y0 = Math.floor((rect.y - half) / step) * step + half;
282
+ for (var y = y0; y < rect.y + rect.h + step; y += step) {
283
+ for (var x = x0; x < rect.x + rect.w + step; x += step) {
284
+ ctx.beginPath();
285
+ ctx.arc(x - rect.x, y - rect.y, r, 0, Math.PI * 2);
286
+ ctx.fill();
287
+ }
288
+ }
289
+ }
290
+
291
+ /* ---------- layer 3: one live frame, in its own document ---------- */
292
+
293
+ function frameOf(el) {
294
+ var body = el.querySelector ? el.querySelector(".gvc-tilebody") : null;
295
+ if (!body) return null;
296
+ var frame = body.querySelector("iframe");
297
+ if (!frame) return null;
298
+ var doc = null;
299
+ try { doc = frame.contentDocument; } catch (e) { doc = null; } // cross-origin: unreadable
300
+ if (!doc || !doc.documentElement || !doc.body || !doc.body.firstChild) return null;
301
+ return { body: body, frame: frame, doc: doc };
302
+ }
303
+ // The framed page renders at a fixed device viewport and is CSS-scaled into the frame box —
304
+ // the same trick the live board uses — so the shot matches what is on screen at any zoom.
305
+ function rasterizeFrame(el, outScale) {
306
+ var f = frameOf(el);
307
+ if (!f) return Promise.resolve(null);
308
+ var bw = f.body.clientWidth, bh = f.body.clientHeight;
309
+ var dw = f.frame.offsetWidth, dh = f.frame.offsetHeight;
310
+ if (!bw || !bh || !dw || !dh) return Promise.resolve(null);
311
+ var s = bw / dw, doc = f.doc, base = doc.baseURI;
312
+
313
+ var html = doc.documentElement.cloneNode(true);
314
+ carryLiveState(doc.documentElement, html);
315
+ dropScripts(html);
316
+ freezeEditables(html);
317
+
318
+ return Promise.all([
319
+ inlineCssUrls(docCss(doc)),
320
+ settle(imageUrls(doc.documentElement, base))
321
+ ]).then(function (res) {
322
+ swapImages(html, base);
323
+ var style = document.createElement("style");
324
+ style.textContent = res[0] + FROZEN;
325
+ var head = html.querySelector("head");
326
+ if (head) head.appendChild(style); else html.insertBefore(style, html.firstChild);
327
+ html.setAttribute("style", (html.getAttribute("style") || "") +
328
+ ";width:" + dw + "px;height:" + dh + "px;overflow:hidden");
329
+
330
+ var inner = document.createElement("div");
331
+ inner.setAttribute("style", "width:" + dw + "px;height:" + dh + "px;overflow:hidden;" +
332
+ "transform:scale(" + s + ");transform-origin:top left;");
333
+ inner.appendChild(html);
334
+ var wrap = document.createElement("div");
335
+ wrap.setAttribute("style", "width:" + bw + "px;height:" + bh + "px;overflow:hidden;background:#fff;");
336
+ wrap.appendChild(inner);
337
+
338
+ return rasterize(wrap, bw, bh, outScale);
339
+ }).then(function (img) {
340
+ return { img: img, x: el.offsetLeft, y: el.offsetTop, w: bw, h: bh, radius: radiusOf(f.body) };
341
+ });
342
+ }
343
+ function radiusOf(elm) {
344
+ try { return parseFloat(getComputedStyle(elm).borderTopLeftRadius) || 0; } catch (e) { return 0; }
345
+ }
346
+ function roundRect(ctx, x, y, w, h, r) {
347
+ r = Math.min(r, w / 2, h / 2);
348
+ ctx.beginPath();
349
+ if (r <= 0) { ctx.rect(x, y, w, h); return; }
350
+ ctx.moveTo(x + r, y);
351
+ ctx.arcTo(x + w, y, x + w, y + h, r);
352
+ ctx.arcTo(x + w, y + h, x, y + h, r);
353
+ ctx.arcTo(x, y + h, x, y, r);
354
+ ctx.arcTo(x, y, x + w, y, r);
355
+ ctx.closePath();
356
+ }
357
+
358
+ /* ---------- layer 2: a run of ordinary nodes, one document ---------- */
359
+
360
+ // One pass can hold any number of ordinary nodes: they are all styled by the engine's own
361
+ // stylesheet, so there is nothing to leak. Runs are cut at live frames only.
362
+ function buildRun(els, rect, css, poster) {
363
+ var wrap = document.createElement("div");
364
+ wrap.id = "gvc-root"; // inherits the engine's font/colour rules...
365
+ // ...but not its fixed full-screen box, and not its paper fill (layer 1 painted that)
366
+ wrap.setAttribute("style", "position:relative;inset:auto;left:auto;top:auto;width:" + rect.w +
367
+ "px;height:" + rect.h + "px;overflow:hidden;background:none;");
368
+ var style = document.createElement("style");
369
+ style.textContent = css + FROZEN;
370
+ wrap.appendChild(style);
371
+ var world = document.createElement("div");
372
+ world.id = "gvc-world";
373
+ world.setAttribute("style", "position:absolute;left:0;top:0;width:0;height:0;transform-origin:0 0;" +
374
+ "transform:translate(" + (-rect.x) + "px," + (-rect.y) + "px);");
375
+ wrap.appendChild(world);
376
+ els.forEach(function (src) {
377
+ var c = src.cloneNode(true);
378
+ carryLiveState(src, c);
379
+ stripChrome(c);
380
+ freezeEditables(c);
381
+ // A live frame draws nothing in a clone, so the frame box falls back to the node's own
382
+ // still image — and to a neutral placeholder if there isn't one. When the frame's own
383
+ // pass succeeds this is simply painted over.
384
+ var body = c.querySelector(".gvc-tilebody");
385
+ var iframe = body ? body.querySelector("iframe") : null;
386
+ if (iframe) {
387
+ iframe.remove();
388
+ var v = poster ? poster(src) : null;
389
+ if (v) {
390
+ var im = document.createElement("img");
391
+ im.setAttribute("src", v);
392
+ body.appendChild(im);
393
+ } else {
394
+ var ph = document.createElement("div");
395
+ ph.className = "ph";
396
+ ph.textContent = "…";
397
+ body.appendChild(ph);
398
+ }
399
+ }
400
+ swapImages(c, document.baseURI);
401
+ world.appendChild(c);
402
+ });
403
+ return wrap;
404
+ }
405
+
406
+ /* ---------- the export ---------- */
407
+
408
+ function nodesToPng(opts) {
409
+ var els = (opts.els || []).slice();
410
+ var rect = opts.rect;
411
+ var want = opts.scale || 2;
412
+ var posterOf = opts.poster || null;
413
+ var info = { scale: want, downgraded: false, frames: { ok: 0, failed: 0 } };
414
+ if (!rect || !(rect.w > 0) || !(rect.h > 0)) return Promise.reject(new Error("empty rect"));
415
+
416
+ // a broken blob is worse than a smaller picture: halve until it fits, then say so
417
+ var scale = want;
418
+ while (scale > 1 && rect.w * rect.h * scale * scale > MAX_PIXELS) scale = scale / 2;
419
+ if (rect.w * rect.h * scale * scale > MAX_PIXELS) scale = Math.max(0.05, Math.sqrt(MAX_PIXELS / (rect.w * rect.h)));
420
+ info.scale = scale;
421
+ info.downgraded = scale < want;
422
+
423
+ var cv = document.createElement("canvas");
424
+ cv.width = Math.max(1, Math.round(rect.w * scale));
425
+ cv.height = Math.max(1, Math.round(rect.h * scale));
426
+ var ctx = cv.getContext("2d");
427
+ ctx.setTransform(scale, 0, 0, scale, 0, 0);
428
+ drawBackground(ctx, rect, opts.background);
429
+
430
+ // cut the element list into runs of ordinary nodes separated by live frames, so a note
431
+ // dropped ON a framed prototype still lands on top of it in the shot
432
+ var segments = [], run = [], frames = [];
433
+ els.forEach(function (e) {
434
+ run.push(e);
435
+ if (frameOf(e)) { frames.push(e); segments.push({ run: run, frame: e }); run = []; }
436
+ });
437
+ if (run.length) segments.push({ run: run, frame: null });
438
+
439
+ inlined = 0;
440
+ // every still image the shot might need, resolved up front — the clone pass is synchronous
441
+ var urls = [];
442
+ els.forEach(function (e) { urls = urls.concat(imageUrls(e, document.baseURI)); });
443
+ if (posterOf) frames.forEach(function (e) {
444
+ var u = abs(posterOf(e), document.baseURI); if (u) urls.push(u);
445
+ });
446
+ function posterData(src) {
447
+ var u = posterOf ? abs(posterOf(src), document.baseURI) : null;
448
+ return u && resCache[u] ? resCache[u].__value : null;
449
+ }
450
+
451
+ if (!hostCss) hostCss = inlineCssUrls(docCss(document)).catch(function () { return ""; });
452
+
453
+ var chain = Promise.all([hostCss, settle(urls)]);
454
+ segments.forEach(function (seg) {
455
+ chain = chain.then(function (pre) {
456
+ var wrap = buildRun(seg.run, rect, pre[0], posterData);
457
+ return rasterize(wrap, rect.w, rect.h, scale)
458
+ .then(function (img) { ctx.drawImage(img, 0, 0, rect.w, rect.h); })
459
+ .catch(function () {}) // one bad run must not cost the whole capture
460
+ .then(function () {
461
+ if (!seg.frame) return;
462
+ return rasterizeFrame(seg.frame, scale).then(function (r) {
463
+ if (!r || !r.img) { info.frames.failed++; return; }
464
+ info.frames.ok++;
465
+ ctx.save();
466
+ roundRect(ctx, r.x - rect.x, r.y - rect.y, r.w, r.h, r.radius);
467
+ ctx.clip();
468
+ ctx.drawImage(r.img, r.x - rect.x, r.y - rect.y, r.w, r.h);
469
+ ctx.restore();
470
+ }, function () { info.frames.failed++; });
471
+ })
472
+ .then(function () { return pre; });
473
+ });
474
+ });
475
+
476
+ return chain.then(function () {
477
+ if (opts.onInfo) { try { opts.onInfo(info); } catch (e) {} }
478
+ return new Promise(function (resolve, reject) {
479
+ cv.toBlob(function (b) { if (b) resolve(b); else reject(new Error("blob")); }, "image/png");
480
+ });
481
+ });
482
+ }
483
+
484
+ window.GVCanvasCapture = { nodesToPng: nodesToPng };
485
+ })();