UNPKG

@fluritheme/spider

Version:

A tiny, expressive spider for your website. Delicate webs that follow DOM layers, blinking eyes, and playful pointer reactions.

98 lines • 4.42 kB
import { clamp, intersect, cornerPoint } from './geometry.js'; import { silkWhite } from './silk-paint.js'; const corners = ['top-right', 'top-left', 'bottom-right', 'bottom-left']; const boxOf = (element) => { const r = element.getBoundingClientRect(); return { x: r.left, y: r.top, width: r.width, height: r.height }; }; export function isVisible(element, point, doc) { const top = doc.elementFromPoint(point.x, point.y); return top !== null && (element === top || element.contains(top)); } export function measureSurface(element, index, span, color, born, doc, win) { if (!element.isConnected || element.closest('[data-spider-ignore]')) return null; const style = win.getComputedStyle(element); if (style.visibility !== 'visible' || style.display === 'none' || Number(style.opacity) === 0) return null; const box = boxOf(element); let visible = intersect(box, { x: 0, y: 0, width: win.innerWidth, height: win.innerHeight }); if (box.width < 64 || box.height < 64 || visible.width < 2 || visible.height < 2) return null; let parent = element.parentElement; const ancestorClips = []; while (parent) { const s = win.getComputedStyle(parent); if (s.visibility !== 'visible' || Number(s.opacity) === 0) return null; const b = boxOf(parent); const clipsX = /hidden|clip|auto|scroll/.test(s.overflowX), clipsY = /hidden|clip|auto|scroll/.test(s.overflowY); if (clipsX || clipsY) { visible = intersect(visible, { x: clipsX ? b.x : visible.x, y: clipsY ? b.y : visible.y, width: clipsX ? b.width : visible.width, height: clipsY ? b.height : visible.height, }); if (clipsX && clipsY) ancestorClips.push({ box: b, radius: parseFloat(s.borderRadius) || 0 }); } parent = parent.parentElement; } if (visible.width <= 0 || visible.height <= 0) return null; const attr = element.getAttribute('data-spider-web'); const corner = corners.includes(attr) ? attr : corners[index % 2]; const radius = clamp(parseFloat(style.borderRadius) || 0, 0, Math.min(box.width, box.height) / 2); const size = Math.min(span, box.width * 0.48, box.height * 0.48, win.innerWidth < 600 ? 88 : span); const inset = Math.max(3, radius * 0.32); const webBox = { x: box.x + inset, y: box.y + inset, width: box.width - inset * 2, height: box.height - inset * 2, }; const anchor = cornerPoint(webBox, corner, size * 0.39, size * 0.39); const home = { x: anchor.x + (corner.endsWith('right') ? -10 : 10), y: anchor.y + (corner.startsWith('bottom') ? -35 : 40), }; // Cache a coarse visibility mask from actual browser paint order. No z-index arithmetic: // nested stacking contexts, transforms, sticky layers, and foreground siblings all participate. // The exact rounded target/overflow boundaries are applied separately during painting. const clip = new Path2D(); const region = intersect(visible, { x: corner.endsWith('right') ? box.x + box.width - size - 115 : box.x, y: corner.startsWith('bottom') ? box.y + box.height - size - 115 : box.y, width: size + 115, height: size + 115, }); const step = 12; for (let y = region.y; y < region.y + region.height; y += step) { for (let x = region.x; x < region.x + region.width; x += step) { const w = Math.min(step, region.x + region.width - x), h = Math.min(step, region.y + region.height - y); const p = { x: x + w / 2, y: y + h / 2 }; if (isVisible(element, p, doc) && ancestorClips.every((c) => roundedContains(c.box, c.radius, p))) clip.rect(x, y, w, h); } } return { element, box: webBox, corner, span: size, ink: color === 'auto' ? silkWhite : color, clip, anchor, home, born, radius: Math.max(0, radius - inset), }; } function roundedContains(b, radius, p) { const r = Math.min(radius, b.width / 2, b.height / 2); const cx = clamp(p.x, b.x + r, b.x + b.width - r), cy = clamp(p.y, b.y + r, b.y + b.height - r); return Math.hypot(p.x - cx, p.y - cy) <= r; } //# sourceMappingURL=layers.js.map