Skip to content

Instantly share code, notes, and snippets.

@StuartJAtkinson
Created February 11, 2026 03:05
Show Gist options
  • Select an option

  • Save StuartJAtkinson/49fe5439c01f317e9b8fcef65c235d33 to your computer and use it in GitHub Desktop.

Select an option

Save StuartJAtkinson/49fe5439c01f317e9b8fcef65c235d33 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Bluesky Map Circle Hover Collector (Exhaustive + High Readability)
// @namespace http://tampermonkey.net/
// @version 2026-02-11
// @description Collect Bluesky handles from canvas using a circle radius with exhaustive hover, and enable high readability
// @match https://bluesky-map.theo.io/
// @grant none
// ==/UserScript==
(function () {
'use strict';
/* ---------------- Auto-enable High Readability ---------------- */
function enableHighReadability() {
const cb = document.getElementById('setting-high-readability');
if (cb && !cb.checked) {
cb.click();
console.log('βœ… High readability enabled');
}
}
window.addEventListener('load', enableHighReadability);
/* ---------------- Variables ---------------- */
let selecting = false;
let dragging = false;
let centerX = 0, centerY = 0;
let radius = 0;
let circle, overlay, canvas;
const handles = new Set();
/* ---------------- UI ---------------- */
const panel = document.createElement('div');
panel.id = 'hover-collector-panel';
panel.style.cssText = `
position: fixed; top: 20px; right: 20px; z-index: 999999;
background: #fff; color: #000; padding: 8px 10px;
font-family: system-ui, sans-serif; border: 1px solid #ccc;
border-radius: 6px; box-shadow: 0 2px 10px rgba(0,0,0,0.1);
display: flex; flex-direction: column; gap: 6px;
min-width: 220px;
`;
// Buttons
const btnStart = document.createElement('button');
const btnFinish = document.createElement('button');
const btnRedo = document.createElement('button');
btnStart.textContent = 'Scan Select';
btnFinish.textContent = 'Scan';
btnRedo.textContent = 'Reset';
btnFinish.disabled = true;
btnRedo.disabled = true;
// Row container for buttons
const buttonRow = document.createElement('div');
buttonRow.style.cssText = 'display:flex; gap:6px;';
[btnStart, btnFinish, btnRedo].forEach(b => {
b.style.cssText = `
background: #f5f5f5; color: #000; border: 1px solid #ccc;
padding: 4px 8px; cursor: pointer; font-size: 13px; flex:1;
`;
b.addEventListener('mouseenter', () => b.style.background = '#e0e0e0');
b.addEventListener('mouseleave', () => b.style.background = '#f5f5f5');
buttonRow.appendChild(b);
});
panel.appendChild(buttonRow);
// Handles list styled like #selections
const listEl = document.createElement('div');
listEl.style.cssText = `
margin-top:6px; max-height:200px; overflow:auto;
width:100%; font-size:13px; line-height:1.3;
`;
panel.appendChild(listEl);
document.body.appendChild(panel);
/* ---------------- Overlay & Circle ---------------- */
overlay = document.createElement('div');
overlay.style.cssText = `
position: fixed; inset:0; cursor: crosshair; z-index: 999998; display:none;
`;
document.body.appendChild(overlay);
circle = document.createElement('div');
circle.style.cssText = `
position:absolute; border:2px dashed #4af;
background: rgba(100,150,255,0.15); pointer-events:none; display:none; z-index:999999;
border-radius: 50%;
`;
overlay.appendChild(circle);
function findCanvas() {
canvas = document.querySelector('canvas');
if (!canvas) console.warn('⚠️ Canvas not found yet.');
}
findCanvas();
/* ---------------- Helpers ---------------- */
function renderList() {
listEl.innerHTML = [...handles]
.map(h => `<div style="padding:2px 0;"><a href="https://bsky.app/profile/${h.replace(/^@/, '')}" target="_blank" style="
text-decoration:none; color:#3578e5;
">${h}</a></div>`)
.join('');
}
function clearSelection() {
circle.style.display = 'none';
handles.clear();
renderList();
btnFinish.disabled = true;
btnRedo.disabled = true;
selecting = false;
dragging = false;
overlay.style.display = 'none';
btnStart.disabled = false;
}
/* ---------------- Drag Logic ---------------- */
overlay.addEventListener('mousedown', e => {
if (!selecting || e.button !== 0) return;
dragging = true;
centerX = e.clientX;
centerY = e.clientY;
radius = 0;
circle.style.width = '0px';
circle.style.height = '0px';
circle.style.left = centerX + 'px';
circle.style.top = centerY + 'px';
circle.style.display = 'block';
console.log('🟒 Drag start', centerX, centerY);
});
overlay.addEventListener('mousemove', e => {
if (!dragging) return;
const dx = e.clientX - centerX;
const dy = e.clientY - centerY;
radius = Math.sqrt(dx*dx + dy*dy);
circle.style.width = radius*2 + 'px';
circle.style.height = radius*2 + 'px';
circle.style.left = centerX - radius + 'px';
circle.style.top = centerY - radius + 'px';
});
overlay.addEventListener('mouseup', e => {
dragging = false;
btnFinish.disabled = false;
btnRedo.disabled = false;
console.log('🟒 Drag end, radius:', radius);
});
/* ---------------- Button Logic ---------------- */
btnStart.onclick = () => {
findCanvas();
if (!canvas) return alert('Canvas not found yet, try again after map loads.');
clearSelection();
selecting = true;
overlay.style.display = 'block';
btnStart.disabled = true;
};
btnRedo.onclick = btnStart.onclick;
/* ---------------- Exhaustive Hover Collection (Circle) ---------------- */
async function exhaustiveHoverCircle(step = 5) {
if (!canvas) findCanvas();
if (!canvas) return;
console.log(`🟒 Starting exhaustive circle hover with step ${step}px`);
const minX = centerX - radius;
const maxX = centerX + radius;
const minY = centerY - radius;
const maxY = centerY + radius;
for (let y = minY; y <= maxY; y += step) {
for (let x = minX; x <= maxX; x += step) {
const dx = x - centerX;
const dy = y - centerY;
if (dx*dx + dy*dy <= radius*radius) {
try {
canvas.dispatchEvent(new MouseEvent('mousemove', { clientX: x, clientY: y, bubbles: true }));
canvas.dispatchEvent(new PointerEvent('pointermove', { clientX: x, clientY: y, bubbles: true }));
await new Promise(r => setTimeout(r, 5));
document.querySelectorAll('.tt-handle').forEach(h => {
const txt = h.textContent.trim();
if (txt && !handles.has(txt)) {
console.log('βž• Collected:', txt, 'at', x, y);
handles.add(txt);
}
});
} catch (err) {
console.error('❌ Hover dispatch error at', x, y, err);
}
}
}
}
}
btnFinish.onclick = async () => {
console.log('🟒 Finish clicked, center:', centerX, centerY, 'radius:', radius);
handles.clear();
renderList();
try {
await exhaustiveHoverCircle(5); // 5px step for full coverage
} catch (err) {
console.error('❌ Error in exhaustiveHoverCircle', err);
}
renderList();
selecting = false;
overlay.style.display = 'none';
btnStart.disabled = false;
btnFinish.disabled = true;
btnRedo.disabled = true;
console.log(`🟒 Done. Collected ${handles.size} handles`);
};
console.log('🟒 Bluesky Map Circle Hover Collector (Exhaustive + High Readability) ready');
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment