app
Pup - the AI's own browser
Public Made by Adomby adom
pup gives your AI a real browser on your desktop that it drives completely, in the background, signed in as you: windows that never jump in front of your work, one shared profile that learns your logins, and every window labeled on the taskbar with its AI thread's name.
master
John Lauer
rust: Phase 3 complete (vault, cards, wiki auto-sign + view flip, annotate, tab + screen recording, highFps lease, down…
4869577
2mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
window.__pupStartAnnotate = function (initTool, initMode) {
if (window.__pupAnnotate) {
// v2.0.372: a re-arm with an explicit mode SWITCHES the live overlay's mode (Alt+Shift+M on
// an already-open toolbar). v2.0.81 rule preserved: a re-arm after emit-hide stays INVISIBLE.
if (initMode && window.__pupAnnotate.setMode) { try { window.__pupAnnotate.setMode(initMode); } catch (e) {} }
if (!window.__pupAnnotateHidden) window.__pupAnnotate.show();
return 'shown';
}
initTool = initTool || 'pen';
// v2.0.370 (John): two explicit capture modes. quick=true (Auto, the default) captures the
// moment the mouse lifts — the one-red-circle case. quick=false (Multi) accumulates shapes —
// a red circle here, a blue circle there — and captures ONCE on Done.
var S = { tool: 'pen', color: '#f85149', strokes: [], cur: null, quick: initMode !== 'multi', seq: 0 };
var wrap = document.createElement('div');
wrap.id = '__pup_annotate';
wrap.style.cssText = 'position:fixed;inset:0;z-index:2147483646;pointer-events:none;';
var cv = document.createElement('canvas');
cv.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;pointer-events:auto;touch-action:none;';
wrap.appendChild(cv);
var bar = document.createElement('div');
bar.style.cssText = 'position:fixed;top:14px;left:50%;transform:translateX(-50%);display:flex;gap:6px;align-items:center;background:#161b22f2;border:1px solid #30363d;border-radius:999px;padding:7px 12px;pointer-events:auto;font:13px Satoshi,system-ui,sans-serif;color:#c9d1d9;box-shadow:0 6px 24px #000a;';
wrap.appendChild(bar);
// drag grip (floating-windows: draggable by a visible grip, clamped inside the viewport)
var grip = document.createElement('div');
grip.title = '';
grip.style.cssText = 'width:14px;height:22px;cursor:grab;display:grid;grid-template-columns:1fr 1fr;gap:2px;align-content:center;justify-items:center;margin-right:2px;';
for (var gi = 0; gi < 6; gi++) { var gd = document.createElement('div'); gd.style.cssText = 'width:3px;height:3px;border-radius:50%;background:#8b949e;'; grip.appendChild(gd); }
bar.appendChild(grip);
(function () {
var drag = null;
grip.addEventListener('pointerdown', function (e) {
var r = bar.getBoundingClientRect();
if (bar.style.transform) { bar.style.transform = ''; bar.style.left = r.left + 'px'; bar.style.top = r.top + 'px'; }
drag = { dx: e.clientX - r.left, dy: e.clientY - r.top };
grip.setPointerCapture(e.pointerId); grip.style.cursor = 'grabbing'; e.preventDefault();
});
grip.addEventListener('pointermove', function (e) {
if (!drag) return;
var w = bar.offsetWidth, h = bar.offsetHeight, M = 8;
var x = Math.min(Math.max(M, e.clientX - drag.dx), innerWidth - w - M);
var y = Math.min(Math.max(M, e.clientY - drag.dy), innerHeight - h - M);
bar.style.left = x + 'px'; bar.style.top = y + 'px';
});
grip.addEventListener('pointerup', function () { drag = null; grip.style.cursor = 'grab'; });
})();
// tooltip singleton (tooltips skill: fixed, body-level z-max, 600ms reveal, 0.18s ease
// fade+translate, measured AFTER content set, clamped to the viewport on BOTH axes, flips
// above when it would clip the bottom)
var tipEl = document.createElement('div');
tipEl.style.cssText = 'position:fixed;z-index:2147483647;max-width:280px;background:#21262d;border:1px solid #30363d;border-radius:8px;padding:7px 10px;font:12px Satoshi,system-ui,sans-serif;color:#c9d1d9;box-shadow:0 6px 20px #000b;pointer-events:none;opacity:0;transform:translateY(4px);transition:opacity 0.18s ease,transform 0.18s ease;text-transform:none;';
wrap.appendChild(tipEl);
var tipTimer = null;
function tipShow(anchor, text) {
clearTimeout(tipTimer);
tipTimer = setTimeout(function () {
tipEl.textContent = text;
tipEl.style.opacity = '0';
// measure only after content is set and laid out
requestAnimationFrame(function () {
var a = anchor.getBoundingClientRect(), M = 8;
var w = tipEl.offsetWidth, h = tipEl.offsetHeight;
var x = a.left + a.width / 2 - w / 2;
var y = a.bottom + 10;
if (y + h > innerHeight - M) y = a.top - h - 10; // flip above instead of clipping
x = Math.min(Math.max(M, x), innerWidth - w - M);
y = Math.min(Math.max(M, y), innerHeight - h - M);
tipEl.style.left = x + 'px'; tipEl.style.top = y + 'px';
tipEl.style.opacity = '1'; tipEl.style.transform = 'translateY(0)';
});
}, 600);
}
function tipHide() { clearTimeout(tipTimer); tipEl.style.opacity = '0'; tipEl.style.transform = 'translateY(4px)'; }
function tipFor(el, text) { el.addEventListener('mouseenter', function () { tipShow(el, text); }); el.addEventListener('mouseleave', tipHide); el.addEventListener('pointerdown', tipHide); el.removeAttribute('title'); }
tipFor(grip, 'Drag to move this toolbar anywhere it is not in your way.');
var ctx = cv.getContext('2d');
// ── Photoshop-style tool cursors (v2.0.134, John's HN report: "pen mode shows the crosshair,
// must be old code"): it was NOT old code. The pen/arrow cursors were CSS
// url(data:image/svg+xml) images, and CSS cursor images load under the PAGE'S CSP img-src —
// sites that don't allow data: (Hacker News: img-src 'self' ...) silently block the image and
// CSS falls back to the declared crosshair, so one build looked new on the wiki and old on HN.
// Fix: never FETCH a cursor at all. Hide the native cursor over the canvas and follow the
// pointer with an inline-SVG sprite — DOM markup, which no CSP gates — hotspot preserved
// (pen tip at 5,19; arrow tip at 2,2). Pen still carries the live color swatch.
var spr = document.createElement('div');
spr.style.cssText = 'position:fixed;left:0;top:0;z-index:2147483647;pointer-events:none;display:none;will-change:transform;';
wrap.appendChild(spr);
var sprHot = [0, 0];
function penSvg() {
return '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24">'
+ '<path d="M5 19 L6.5 14.5 L16 5 a2 2 0 0 1 2.8 2.8 L9.5 17.5 Z" fill="#e5e7eb" stroke="#111" stroke-width="1.2"/>'
+ '<path d="M5 19 L6.5 14.5 L9.5 17.5 Z" fill="' + S.color + '" stroke="#111" stroke-width="0.8"/>'
+ '<circle cx="19.5" cy="19.5" r="3.5" fill="' + S.color + '" stroke="#fff" stroke-width="1"/></svg>';
}
function arrowSvg() {
return '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24">'
+ '<path d="M2 2 L11 20 L13.5 13.5 L20 11 Z" fill="' + S.color + '" stroke="#fff" stroke-width="1.4"/></svg>';
}
function setCursor() {
if (S.tool === 'pen' || S.tool === 'arrow') {
cv.style.cursor = 'none';
spr.innerHTML = S.tool === 'pen' ? penSvg() : arrowSvg();
sprHot = S.tool === 'pen' ? [5, 19] : [2, 2];
// shown on the next pointer move, so it never appears at a stale position
} else {
cv.style.cursor = S.tool === 'text' ? 'text' : 'crosshair';
spr.style.display = 'none';
}
}
function sprTrack(e) {
if (S.tool !== 'pen' && S.tool !== 'arrow') return;
spr.style.display = 'block';
spr.style.transform = 'translate(' + (e.clientX - sprHot[0]) + 'px,' + (e.clientY - sprHot[1]) + 'px)';
}
cv.addEventListener('pointermove', sprTrack);
cv.addEventListener('pointerenter', sprTrack);
cv.addEventListener('pointerleave', function () { spr.style.display = 'none'; });
function fit() { var d = window.devicePixelRatio || 1; cv.width = innerWidth * d; cv.height = innerHeight * d; ctx.setTransform(d, 0, 0, d, 0, 0); redraw(); }
function drawStroke(st) {
ctx.strokeStyle = st.color; ctx.fillStyle = st.color; ctx.lineWidth = 3.5; ctx.lineCap = 'round'; ctx.lineJoin = 'round';
if (st.tool === 'pen') { ctx.beginPath(); for (var i = 0; i < st.pts.length; i++) { var p = st.pts[i]; if (i === 0) ctx.moveTo(p[0], p[1]); else ctx.lineTo(p[0], p[1]); } ctx.stroke(); }
else if (st.tool === 'box') { var a = st.pts[0], b = st.pts[st.pts.length - 1]; ctx.strokeRect(Math.min(a[0],b[0]), Math.min(a[1],b[1]), Math.abs(b[0]-a[0]), Math.abs(b[1]-a[1])); }
else if (st.tool === 'arrow') { var a2 = st.pts[0], b2 = st.pts[st.pts.length - 1]; ctx.beginPath(); ctx.moveTo(a2[0], a2[1]); ctx.lineTo(b2[0], b2[1]); ctx.stroke();
var ang = Math.atan2(b2[1]-a2[1], b2[0]-a2[0]);
ctx.beginPath(); ctx.moveTo(b2[0], b2[1]);
ctx.lineTo(b2[0]-14*Math.cos(ang-0.42), b2[1]-14*Math.sin(ang-0.42));
ctx.lineTo(b2[0]-14*Math.cos(ang+0.42), b2[1]-14*Math.sin(ang+0.42));
ctx.closePath(); ctx.fill(); }
else if (st.tool === 'text') { ctx.font = '600 17px system-ui,sans-serif'; ctx.fillText(st.text || '', st.pts[0][0], st.pts[0][1]); }
else if (st.tool === 'snip') { var s1 = st.pts[0], s2 = st.pts[st.pts.length - 1]; ctx.save(); ctx.setLineDash([7, 5]); ctx.lineWidth = 2; ctx.strokeStyle = '#00d4cb'; ctx.strokeRect(Math.min(s1[0],s2[0]), Math.min(s1[1],s2[1]), Math.abs(s2[0]-s1[0]), Math.abs(s2[1]-s1[1])); ctx.restore(); }
}
function redraw() { ctx.clearRect(0, 0, innerWidth, innerHeight); S.strokes.forEach(drawStroke); if (S.cur) drawStroke(S.cur); }
// Tight-crop bbox over ALL strokes, padded and clamped to the viewport (1a2).
function bbox() {
if (!S.strokes.length) return null;
var x1 = 1e9, y1 = 1e9, x2 = -1e9, y2 = -1e9;
S.strokes.forEach(function (st) {
if (st.tool === 'text') {
var w = (st.text || '').length * 9.5, p0 = st.pts[0];
x1 = Math.min(x1, p0[0]); y1 = Math.min(y1, p0[1] - 18); x2 = Math.max(x2, p0[0] + w); y2 = Math.max(y2, p0[1] + 6);
} else st.pts.forEach(function (p) { x1 = Math.min(x1, p[0]); y1 = Math.min(y1, p[1]); x2 = Math.max(x2, p[0]); y2 = Math.max(y2, p[1]); });
});
var M = 48;
x1 = Math.max(0, x1 - M); y1 = Math.max(0, y1 - M);
x2 = Math.min(innerWidth, x2 + M); y2 = Math.min(innerHeight, y2 + M);
if (x2 - x1 < 24 || y2 - y1 < 24) return null;
return { x: Math.round(x1), y: Math.round(y1), w: Math.round(x2 - x1), h: Math.round(y2 - y1) };
}
function emit(extra, hide) {
S.seq++;
var r = { done: true, shapes: S.strokes.length, ts: Date.now(), seq: S.seq, bbox: bbox() };
if (extra) for (var k in extra) r[k] = extra[k];
if (hide) { bar.style.display = 'none'; cv.style.pointerEvents = 'none'; window.__pupAnnotateHidden = true; }
window.__pupAnnotateResult = r;
}
cv.addEventListener('pointerdown', function (e) {
if (S.tool === 'text') { var t = prompt('Annotation text:'); if (t) { S.strokes.push({ tool: 'text', color: S.color, pts: [[e.clientX, e.clientY]], text: t }); redraw(); if (S.quick) { emit(null, true); removeEventListener('keydown', onKey, true); } else advanceColor(); } return; }
S.cur = { tool: S.tool, color: S.color, pts: [[e.clientX, e.clientY]] }; cv.setPointerCapture(e.pointerId);
});
cv.addEventListener('pointermove', function (e) { if (S.cur) { S.cur.pts.push([e.clientX, e.clientY]); redraw(); } });
cv.addEventListener('pointerup', function () {
if (!S.cur) return;
if (S.cur.tool === 'snip') {
var p1 = S.cur.pts[0], p2 = S.cur.pts[S.cur.pts.length - 1];
var r = { x: Math.min(p1[0], p2[0]), y: Math.min(p1[1], p2[1]), w: Math.abs(p2[0]-p1[0]), h: Math.abs(p2[1]-p1[1]) };
S.cur = null; redraw();
if (r.w > 8 && r.h > 8) emit({ snip: { x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.w), h: Math.round(r.h) } }, true);
return;
}
S.strokes.push(S.cur); S.cur = null; redraw();
// QUICK MODE (1a3): the moment the mouse lifts, capture fires — keep drawing and each lift
// supersedes the last capture with the union bbox.
if (S.quick) { emit(null, true); removeEventListener('keydown', onKey, true); }
// v2.0.371 (John): in MULTI mode each finished shape auto-advances to the next color, so a
// red circle then a blue circle needs zero clicks. White is excluded (invisible on light
// pages); a manual swatch pick overrides and the cycle continues from there.
else advanceColor();
});
function icon(paths, dash) {
var n = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
n.setAttribute('viewBox', '0 0 24 24'); n.setAttribute('width', '14'); n.setAttribute('height', '14');
n.setAttribute('fill', 'none'); n.setAttribute('stroke', 'currentColor');
n.setAttribute('stroke-width', '1.9'); n.setAttribute('stroke-linecap', 'round'); n.setAttribute('stroke-linejoin', 'round');
paths.forEach(function (dd, i) { var p2 = document.createElementNS('http://www.w3.org/2000/svg', 'path'); p2.setAttribute('d', dd); if (dash && i === dash - 1) p2.setAttribute('stroke-dasharray', '2.5 2'); n.appendChild(p2); });
return n;
}
var ICONS = {
pen: [['M4 20l1.5-4.5L16 5a2.1 2.1 0 0 1 3 3L8.5 18.5 4 20z', 'M13.5 7.5l3 3']],
arrow: [['M5 19L17 7', 'M9 7h8v8']],
box: [['M4.5 5.5h15v13h-15z']],
text: [['M6 6h12', 'M12 6v12']],
snip: [['M4 8V5a1 1 0 0 1 1-1h3M16 4h3a1 1 0 0 1 1 1v3M20 16v3a1 1 0 0 1-1 1h-3M8 20H5a1 1 0 0 1-1-1v-3', 'M8.5 8.5h7v7h-7z'], 2],
auto: [['M13 3L5 13h5l-1 8 8-10h-5l1-8']],
multi: [['M4 4.5h10v10H4z', 'M10 9.5h10v10H10z']],
undo: [['M8 5L4 9l4 4', 'M4 9h9a6 6 0 0 1 0 12h-2']],
clear: [['M5 7h14', 'M9 7V5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2', 'M7 7l1 13h8l1-13']],
done: [['M4.5 12.5l5 5 10-11']],
cancel: [['M6 6l12 12', 'M18 6L6 18']],
};
function btn(label, title, fn, id, iconKey) {
var b = document.createElement('button');
if (iconKey && ICONS[iconKey]) b.appendChild(icon(ICONS[iconKey][0], ICONS[iconKey][1]));
b.appendChild(document.createTextNode(label)); b.title = title;
b.style.cssText = 'background:none;border:1px solid transparent;border-radius:8px;color:#c9d1d9;padding:3px 8px;cursor:pointer;font:inherit;display:flex;align-items:center;gap:5px;';
b.onmouseenter = function () { b.style.background = '#1c2128'; };
b.onmouseleave = function () { b.style.background = S.tool === b.dataset.tool ? 'rgba(0,184,177,.13)' : 'none'; };
b.onclick = fn; if (id) b.dataset.tool = id; bar.appendChild(b);
if (title) { tipFor(b, title); }
return b;
}
function selTool(id) { S.tool = id; setCursor(); [].forEach.call(bar.querySelectorAll('button[data-tool]'), function (x) { x.style.background = x.dataset.tool === id ? 'rgba(0,184,177,.13)' : 'none'; x.style.borderColor = x.dataset.tool === id ? 'rgba(0,184,177,.45)' : 'transparent'; }); }
btn('Pen', 'Draw freehand. With Auto on, the capture fires the moment you release the mouse: cropped tight to your drawing, copied to your clipboard.', function () { selTool('pen'); }, 'pen', 'pen');
btn('Arrow', 'Point at the thing you mean. Drag from tail to tip; the arrowhead lands where you release.', function () { selTool('arrow'); }, 'arrow', 'arrow');
btn('Box', 'Frame a region with a rectangle. Drag corner to corner.', function () { selTool('box'); }, 'box', 'box');
btn('Text', 'Drop a short text note onto the page. Click where the note should start.', function () { selTool('text'); }, 'text', 'text');
btn('Snip', 'Capture exactly a region, like Win+Shift+S: drag a marquee and it is copied to your clipboard the moment you release.', function () { selTool('snip'); }, 'snip', 'snip');
// v2.0.370 (John): the color control is now a one-click TRIO — red (default), blue, yellow —
// with the rarer colors one click deeper in a flyout. The old single cycling dot made
// red→blue three clicks; picking between about 3 colors must be instant.
var SW_MAIN = ['#f85149', '#64abff', '#d29922'];
var SW_MORE = ['#3fb950', '#8c6bf7', '#e6edf3'];
var flyout = document.createElement('div');
flyout.style.cssText = 'position:absolute;top:calc(100% + 8px);left:50%;transform:translateX(-50%);display:none;gap:7px;background:#161b22f2;border:1px solid #30363d;border-radius:999px;padding:7px 10px;box-shadow:0 6px 24px #000a;pointer-events:auto;';
var more = document.createElement('button');
function setColor(c) {
S.color = c; setCursor(); flyout.style.display = 'none';
[].forEach.call(wrap.querySelectorAll('button[data-sw]'), function (x) { x.style.borderColor = x.dataset.sw === c ? '#fff' : 'rgba(255,255,255,.25)'; });
more.style.outline = SW_MORE.indexOf(c) >= 0 ? '2px solid #fff' : 'none';
}
// v2.0.371: the multi-mode auto-advance cycle — the quick trio then green/purple, never white.
function advanceColor() {
var CYCLE = ['#f85149', '#64abff', '#d29922', '#3fb950', '#8c6bf7'];
setColor(CYCLE[(CYCLE.indexOf(S.color) + 1) % CYCLE.length]);
}
function swBtn(c, parent) {
var s = document.createElement('button');
s.dataset.sw = c;
s.style.cssText = 'width:18px;height:18px;border-radius:50%;cursor:pointer;background:' + c + ';border:2px solid ' + (S.color === c ? '#fff' : 'rgba(255,255,255,.25)') + ';padding:0;flex:none;';
s.onclick = function () { setColor(c); };
parent.appendChild(s);
return s;
}
SW_MAIN.forEach(function (c) { swBtn(c, bar); });
more.style.cssText = 'width:18px;height:18px;border-radius:50%;cursor:pointer;border:2px solid rgba(255,255,255,.25);padding:0;flex:none;background:conic-gradient(#f85149,#d29922,#3fb950,#64abff,#8c6bf7,#f85149);';
more.onclick = function () { flyout.style.display = flyout.style.display === 'none' ? 'flex' : 'none'; };
bar.appendChild(more); tipFor(more, 'More colors: green, purple, white.');
SW_MORE.forEach(function (c) { swBtn(c, flyout); });
bar.appendChild(flyout);
// v2.0.370 (John): Auto vs Multi as an explicit two-state control, not a lone toggle.
var modeWrap = document.createElement('div');
modeWrap.style.cssText = 'display:flex;border:1px solid #30363d;border-radius:8px;overflow:hidden;margin-left:2px;';
function modeBtn(label, iconKey, val, tip) {
var b = document.createElement('button');
if (ICONS[iconKey]) b.appendChild(icon(ICONS[iconKey][0], ICONS[iconKey][1]));
b.appendChild(document.createTextNode(label));
b.style.cssText = 'background:none;border:none;color:#c9d1d9;padding:3px 8px;cursor:pointer;font:inherit;display:flex;align-items:center;gap:5px;';
b.onclick = function () { S.quick = val; paintMode(); };
modeWrap.appendChild(b); tipFor(b, tip);
return b;
}
var mAuto = modeBtn('Auto', 'auto', true, 'One-shot (the default): draw one thing, lift the mouse, and it is captured and copied to your clipboard instantly.');
var mMulti = modeBtn('Multi', 'multi', false, 'Compose: draw several shapes and NOTHING is captured until you click Done. The color auto-advances after each shape (red, blue, yellow, green, purple) so every shape is distinct — click a swatch any time to override.');
function paintMode() {
mAuto.style.background = S.quick ? 'rgba(0,184,177,.13)' : 'none'; mAuto.style.color = S.quick ? '#00d4cb' : '#c9d1d9';
mMulti.style.background = !S.quick ? 'rgba(0,184,177,.13)' : 'none'; mMulti.style.color = !S.quick ? '#00d4cb' : '#c9d1d9';
}
paintMode(); bar.appendChild(modeWrap);
btn('Undo', 'Remove the last thing you drew. Click again to keep unwinding.', function () { S.strokes.pop(); redraw(); }, null, 'undo');
btn('Clear', 'Erase every drawing on this page. The page itself is untouched.', function () { S.strokes = []; redraw(); }, null, 'clear');
var done = btn('Done', 'Capture the annotated area (tight-cropped around your drawing), copy it to your clipboard, and leave the drawing visible so the AI thread that owns this window sees it too.', function () { emit(null, true); }, null, 'done');
done.style.cssText += 'background:rgba(0,184,177,.13);border:1px solid rgba(0,184,177,.55);color:#00d4cb;font-weight:600;';
btn('', 'Close the annotation toolbar and remove all drawings from the page (Esc does this too).', function () { exitAll(); }, null, 'cancel');
function exitAll() { removeEventListener('keydown', onKey, true); wrap.remove(); window.__pupAnnotate = null; window.__pupAnnotateResult = { done: false, ts: Date.now() }; }
function onKey(e) { if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); exitAll(); } }
addEventListener('keydown', onKey, true);
// v2.0.72: called by the bridge the moment the screenshot is TAKEN (never before, so the
// flash is not in the shot): camera-flash feedback over the captured region, instant toast
// with the preview, then the drawings fade out so the user is back to normal surfing.
window.__pupCaptureDone = function (region, previewUrl, line1) {
try {
var fl = document.createElement('div');
if (!region || !region.w) throw 0; // skip the flash only (caught below): John's rule, flash the captured region or nothing — toast and fade still run
var rg = region;
fl.style.cssText = 'position:fixed;left:' + rg.x + 'px;top:' + rg.y + 'px;width:' + rg.w + 'px;height:' + rg.h + 'px;z-index:2147483647;background:none;border:2px dashed #00d4cb;border-radius:6px;opacity:1;pointer-events:none;transition:opacity .45s ease;box-sizing:border-box;';
document.documentElement.appendChild(fl);
setTimeout(function () { fl.style.opacity = '0'; }, 550);
setTimeout(function () { try { fl.remove(); } catch (e) {} }, 1100);
} catch (e) {}
try {
var old = document.getElementById('__pup_ann_toast'); if (old) old.remove();
var t = document.createElement('div'); t.id = '__pup_ann_toast';
t.style.cssText = 'position:fixed;left:50%;transform:translateX(-50%);bottom:18px;z-index:2147483647;background:#21262df2;border:1px solid #30363d;border-radius:12px;padding:10px;box-shadow:0 8px 30px #000c;font:12px Satoshi,system-ui,sans-serif;color:#c9d1d9;max-width:340px;cursor:pointer;';
var cap = document.createElement('div'); cap.textContent = line1 || 'Captured'; cap.style.cssText = 'margin-bottom:7px;color:#00d4cb;font-weight:500;';
t.appendChild(cap);
// v2.0.135: the preview arrives as RAW RGBA pixels and is PAINTED (putImageData), never
// LOADED — an <img src="data:..."> is a resource load the page's CSP img-src can veto
// (Hacker News does), which showed the user a broken-image icon. Pixel writes to a canvas
// are not loads; no site's CSP can break this. (String form kept for mixed-version overlap.)
if (previewUrl && previewUrl.b64 && previewUrl.w) {
try {
var pc = document.createElement('canvas'); pc.width = previewUrl.w; pc.height = previewUrl.h;
pc.style.cssText = 'display:block;max-width:320px;max-height:200px;border-radius:8px;border:1px solid #30363d;';
var bin = atob(previewUrl.b64), n = bin.length, arr = new Uint8ClampedArray(n);
for (var i2 = 0; i2 < n; i2++) arr[i2] = bin.charCodeAt(i2);
pc.getContext('2d').putImageData(new ImageData(arr, previewUrl.w, previewUrl.h), 0, 0);
t.appendChild(pc);
} catch (e2) {}
} else if (previewUrl && typeof previewUrl === 'string') { var im = document.createElement('img'); im.src = previewUrl; im.style.cssText = 'display:block;max-width:320px;max-height:200px;border-radius:8px;border:1px solid #30363d;'; t.appendChild(im); }
t.onclick = function () { t.remove(); };
document.documentElement.appendChild(t);
setTimeout(function () { try { t.remove(); } catch (e) {} }, 4500);
} catch (e) {}
// fade the drawings out (John: after the snapshot, back to normal surfing)
setTimeout(function () {
try {
cv.style.transition = 'opacity .8s ease'; cv.style.opacity = '0';
setTimeout(function () { try { wrap.remove(); window.__pupAnnotate = null; window.__pupCaptureDone = null; removeEventListener('keydown', onKey, true); } catch (e) {} }, 900);
} catch (e) {}
}, 2200);
};
window.__pupAnnotate = {
show: function () { window.__pupAnnotateHidden = false; wrap.style.display = ''; bar.style.display = 'flex'; cv.style.pointerEvents = 'auto'; addEventListener('keydown', onKey, true); },
setMode: function (m) { S.quick = m !== 'multi'; paintMode(); },
};
addEventListener('resize', fit);
document.documentElement.appendChild(wrap);
fit(); selTool(initTool);
return 'started';
};