@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.
- package/CANVAS.md +858 -0
- package/INSTALL.md +603 -0
- package/LICENSE +27 -0
- package/README.md +150 -0
- package/TRADEMARKS.md +72 -0
- package/agents/README.md +65 -0
- package/agents/canon-extract.md +117 -0
- package/agents/canon.md +151 -0
- package/agents/canvas.md +88 -0
- package/agents/currency.md +74 -0
- package/agents/drafts.md +92 -0
- package/agents/identity.md +148 -0
- package/agents/prototype-contract.md +69 -0
- package/agents/publishing.md +324 -0
- package/agents/review-feedback.md +76 -0
- package/agents/space-json.md +132 -0
- package/agents/ui-skill.md +101 -0
- package/agents/working-marks.md +86 -0
- package/brand/augur-eye.svg +3 -0
- package/brand/augur-icon.svg +41 -0
- package/brand/augur-mark.svg +5 -0
- package/brand/lentianova-bulky.otf +0 -0
- package/brand/lentianova-median.otf +0 -0
- package/brand/lentianova.woff2 +0 -0
- package/build.js +8409 -0
- package/changelog.md +133 -0
- package/fonts/inter-latin-wght-normal.woff2 +0 -0
- package/package.json +57 -0
- package/pitis/PETS.md +121 -0
- package/pitis/README.md +117 -0
- package/pitis/index.html +266 -0
- package/pitis/piti.build.js +81 -0
- package/pitis/piti.js +929 -0
- package/pitis/roast-agent.md +185 -0
- package/realtime/src/index.js +119 -0
- package/realtime/wrangler.example.toml +44 -0
- package/scripts/adopt.mjs +106 -0
- package/scripts/board-snapshot.mjs +197 -0
- package/scripts/build-seed-pack.mjs +33 -0
- package/scripts/bundle-rekey.mjs +112 -0
- package/scripts/bundle-tenancy-rehearsal.mjs +781 -0
- package/scripts/canon-extract.mjs +578 -0
- package/scripts/canon.mjs +599 -0
- package/scripts/canvas-screen.mjs +333 -0
- package/scripts/check-local.mjs +136 -0
- package/scripts/clawd-art.mjs +75 -0
- package/scripts/clawd-canvas.mjs +741 -0
- package/scripts/cli.mjs +100 -0
- package/scripts/clone.mjs +422 -0
- package/scripts/close.mjs +30 -0
- package/scripts/connect.mjs +103 -0
- package/scripts/deploy.mjs +157 -0
- package/scripts/dev.mjs +54 -0
- package/scripts/doc-lint.mjs +143 -0
- package/scripts/export.mjs +300 -0
- package/scripts/fork.mjs +64 -0
- package/scripts/freeze.mjs +74 -0
- package/scripts/frontdoor-parity.mjs +180 -0
- package/scripts/hook.mjs +67 -0
- package/scripts/identity-rekey.mjs +112 -0
- package/scripts/init.mjs +122 -0
- package/scripts/land.mjs +40 -0
- package/scripts/lib/adapters.mjs +156 -0
- package/scripts/lib/board-snapshot.mjs +511 -0
- package/scripts/lib/draft.mjs +385 -0
- package/scripts/lib/graduate.mjs +276 -0
- package/scripts/lib/instance.mjs +74 -0
- package/scripts/lib/marks.mjs +107 -0
- package/scripts/lib/materialize.mjs +138 -0
- package/scripts/lib/merge3.mjs +124 -0
- package/scripts/lib/offline-posture.mjs +120 -0
- package/scripts/lib/offline-respawn.mjs +18 -0
- package/scripts/lib/offline-wrangler.mjs +78 -0
- package/scripts/lib/publish-compose.mjs +12 -0
- package/scripts/lib/publish-conflict.mjs +78 -0
- package/scripts/lib/publish-evidence.mjs +144 -0
- package/scripts/lib/refine-compare.mjs +140 -0
- package/scripts/lib/refine-ledger.mjs +150 -0
- package/scripts/lib/refine-png.mjs +149 -0
- package/scripts/lib/seed-pack-build.mjs +141 -0
- package/scripts/lib/state-compare.mjs +68 -0
- package/scripts/lib/store.mjs +122 -0
- package/scripts/login.mjs +84 -0
- package/scripts/mark.mjs +112 -0
- package/scripts/migrate-board-keys.mjs +164 -0
- package/scripts/migrate.mjs +265 -0
- package/scripts/no-foreign-vocabulary.mjs +216 -0
- package/scripts/no-product-names.mjs +157 -0
- package/scripts/no-tenant-globals.mjs +1134 -0
- package/scripts/offline.mjs +247 -0
- package/scripts/og.mjs +170 -0
- package/scripts/one-tenant-resolver.mjs +146 -0
- package/scripts/open.mjs +63 -0
- package/scripts/publish.mjs +1041 -0
- package/scripts/read.mjs +31 -0
- package/scripts/refine.mjs +455 -0
- package/scripts/release-drift.mjs +138 -0
- package/scripts/restore.mjs +322 -0
- package/scripts/review.mjs +125 -0
- package/scripts/save.mjs +21 -0
- package/scripts/shell-lint.mjs +189 -0
- package/scripts/ship.mjs +460 -0
- package/scripts/shoot.mjs +264 -0
- package/scripts/smoke-invite.mjs +113 -0
- package/scripts/state-inventory.mjs +223 -0
- package/scripts/status.mjs +177 -0
- package/scripts/sync.mjs +27 -0
- package/scripts/tenant-do-rehearsal.mjs +1314 -0
- package/scripts/ui-copy-lint.mjs +202 -0
- package/scripts/watch.mjs +32 -0
- package/scripts/wrangler-preflight.mjs +246 -0
- package/seed/CANON.md +43 -0
- package/seed/README.md +119 -0
- package/seed/prototype-status.json +9 -0
- package/seed/registry.json +88 -0
- package/seed/skills/starter-ui/SKILL.md +52 -0
- package/seed/skills/starter-ui/skill.json +11 -0
- package/seed/skills/starter-ui/starter-tokens.css +73 -0
- package/seed/skills/starter-ui/starter-ui.css +318 -0
- package/seed/skills/starter-ui/starter-ui.js +50 -0
- package/seed/space.json +5 -0
- package/seed/start-here/prototypes/connect-your-terminal/index.html +428 -0
- package/seed/start-here/prototypes/connect-your-terminal/preview.webp +0 -0
- package/seed/start-here/prototypes/sample-with-comments/index.html +172 -0
- package/seed/start-here/prototypes/sample-with-comments/preview.webp +0 -0
- package/seed/start-here/prototypes/set-up-your-design-system/index.html +353 -0
- package/seed/start-here/prototypes/set-up-your-design-system/preview.webp +0 -0
- package/seed/threads.json +52 -0
- package/seed/worked-examples/prototypes/field-readings/index.html +273 -0
- package/seed/worked-examples/prototypes/field-readings/preview.webp +0 -0
- package/seed/worked-examples/prototypes/slide-deck/index.html +214 -0
- package/seed/worked-examples/prototypes/slide-deck/preview.webp +0 -0
- package/seed/worked-examples/prototypes/specimen-viewer/index.html +203 -0
- package/seed/worked-examples/prototypes/specimen-viewer/preview.webp +0 -0
- package/src/_worker.js +12668 -0
- package/src/board-key.mjs +57 -0
- package/src/board-room.mjs +881 -0
- package/src/bundle-keys.mjs +138 -0
- package/src/canon/collect-in-browser.js +215 -0
- package/src/canon/emit.mjs +232 -0
- package/src/canon/observe.mjs +260 -0
- package/src/canon/schema.mjs +451 -0
- package/src/canvas/DSEG-LICENSE.txt +95 -0
- package/src/canvas/DSEG7Classic-Bold.woff2 +0 -0
- package/src/canvas/canvas.css +885 -0
- package/src/canvas/canvas.js +5780 -0
- package/src/canvas/capture.js +485 -0
- package/src/chrome/appchrome.mjs +568 -0
- package/src/currency.mjs +203 -0
- package/src/delete-confirmation.mjs +254 -0
- package/src/drafts/drafts.js +326 -0
- package/src/entry.js +58 -0
- package/src/galleries.mjs +400 -0
- package/src/health-cron.mjs +173 -0
- package/src/identity.json +1 -0
- package/src/kv-codec.mjs +135 -0
- package/src/kv-identity.mjs +259 -0
- package/src/mail.mjs +599 -0
- package/src/provenance.mjs +74 -0
- package/src/publish-compose.mjs +363 -0
- package/src/publish-fork.mjs +221 -0
- package/src/publish-units.mjs +44 -0
- package/src/purge.mjs +81 -0
- package/src/review/COMMENTING-UX.md +306 -0
- package/src/review/cat.png +0 -0
- package/src/review/comment-cursor.svg +7 -0
- package/src/review/comments.js +1738 -0
- package/src/room-ticket.mjs +96 -0
- package/src/roster-chip.mjs +32 -0
- package/src/seed-pack.mjs +261 -0
- package/src/state-inventory.mjs +249 -0
- package/src/sw-logic.mjs +48 -0
- package/src/tenant-cache.mjs +141 -0
- package/src/tenant-context.mjs +386 -0
- package/src/tenant-do.js +2940 -0
- package/src/tenant-host.mjs +144 -0
- package/src/tenant-quotas.mjs +140 -0
- package/src/unit-core.mjs +103 -0
- package/src/unit-object.mjs +338 -0
- package/templates/README.md +67 -0
- package/templates/shell/deploy.yml +145 -0
- package/templates/shell/engine-bump.yml +142 -0
- package/templates/shell/health.yml +233 -0
- package/templates/shell/kv-backup.yml +201 -0
- package/templates/shell/roster-update.yml +110 -0
- package/templates/shell/space-preflight.yml +35 -0
- package/templates/shell/space-rebake.yml +79 -0
- package/templates/shell/store-backup.yml +152 -0
- package/templates/shell/wrangler.example.toml +266 -0
- 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
|
+
})();
|