search for standard sites pub-search.waow.tech
search zig blog atproto
11
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: make atlas usable on mobile

- tap-to-select (shows tooltip), tap-again-to-open (navigates to doc)
- larger hit radius on touch devices (40px vs 20px)
- pinch zoom now zooms toward pinch midpoint instead of screen center
- scale labels down on narrow viewports (9/8/9px vs 12/11/11px)
- fewer labels and shorter titles on mobile to reduce clutter
- responsive CSS for header, legend, stats, search, tooltip
- tooltip anchored at top-center on mobile instead of following finger

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+98 -17
+14
site/atlas.css
··· 237 237 font-size: 10px; 238 238 color: var(--text-muted); 239 239 } 240 + 241 + /* mobile */ 242 + @media (max-width: 600px) { 243 + .header { padding: 8px 10px; } 244 + .header h1 { font-size: 11px; } 245 + .header .nav { gap: 8px; font-size: 10px; } 246 + #search-input { width: 90px; font-size: 10px; padding: 4px 6px; } 247 + #search-input:focus { width: 130px; } 248 + .legend { bottom: 8px; left: 10px; gap: 6px; font-size: 9px; } 249 + .stats { bottom: 8px; right: 10px; font-size: 9px; } 250 + .tooltip { max-width: 240px; font-size: 11px; } 251 + .tooltip .meta { font-size: 10px; } 252 + .tooltip .platform-badge { font-size: 9px; } 253 + }
+84 -17
site/atlas.js
··· 383 383 ctx.textAlign = 'center'; 384 384 ctx.textBaseline = 'middle'; 385 385 386 + var small = W < 600; 386 387 if (zoom < 2) { 387 - // coarse labels — fixed 12px screen size 388 - ctx.font = '12px monospace'; 388 + // coarse labels 389 + ctx.font = (small ? '9px' : '12px') + ' monospace'; 389 390 ctx.globalAlpha = 0.85; 390 391 for (var c = 0; c < data.clusters.coarse.length; c++) { 391 392 var cl = data.clusters.coarse[c]; ··· 394 395 drawLabel(cl.label, sx, sy - Math.sqrt(cl.count) * 1.5, dark); 395 396 } 396 397 } else if (zoom < 5) { 397 - // fine labels — fixed 11px screen size 398 - ctx.font = '11px monospace'; 398 + // fine labels 399 + ctx.font = (small ? '8px' : '11px') + ' monospace'; 399 400 ctx.globalAlpha = 0.75; 400 401 for (var c = 0; c < data.clusters.fine.length; c++) { 401 402 var cl = data.clusters.fine[c]; ··· 405 406 drawLabel(cl.label, sx, sy - 14, dark); 406 407 } 407 408 } else { 408 - // document titles — fixed 11px, readable at any zoom 409 - ctx.font = '11px monospace'; 409 + // document titles 410 + ctx.font = (small ? '9px' : '11px') + ' monospace'; 410 411 ctx.globalAlpha = 0.7; 411 - var shown = 0, maxLabels = 50; 412 + var shown = 0, maxLabels = small ? 25 : 50; 413 + var truncLen = small ? 30 : 45; 412 414 for (var i = 0; i < n && shown < maxLabels; i++) { 413 415 var px = pointsX[i], py = pointsY[i]; 414 416 if (px < xMin || px > xMax || py < yMin || py > yMax) continue; ··· 416 418 if (!title) continue; 417 419 var sx = cx + px * scale, sy = cy + py * scale; 418 420 if (sx < 0 || sx > W || sy < 0 || sy > H) continue; 419 - if (title.length > 45) title = title.substring(0, 43) + '\u2026'; 421 + if (title.length > truncLen) title = title.substring(0, truncLen - 2) + '\u2026'; 420 422 drawLabel(title, sx, sy - 10, dark); 421 423 shown++; 422 424 } ··· 456 458 var hoveredIndex = -1; 457 459 var mouseX = 0, mouseY = 0; 458 460 461 + // --- mobile detection --- 462 + var isMobile = 'ontouchstart' in window || navigator.maxTouchPoints > 0; 463 + var HIT_RADIUS = isMobile ? 40 : 20; 464 + 459 465 // --- interaction state --- 460 466 var dragging = false; 461 467 var dragStartX, dragStartY, dragStartPanX, dragStartPanY; 462 468 var pinchStartDist = 0, pinchStartZoom = 1; 469 + var pinchMidX = 0, pinchMidY = 0, pinchStartPanX = 0, pinchStartPanY = 0; 463 470 464 471 canvas.addEventListener('wheel', function(e) { 465 472 e.preventDefault(); ··· 493 500 return; 494 501 } 495 502 cacheTransform(); 496 - var idx = findNearest(mouseX, mouseY, 20); 503 + var idx = findNearest(mouseX, mouseY, HIT_RADIUS); 497 504 if (idx !== hoveredIndex) { 498 505 hoveredIndex = idx; 499 506 view.dirty = true; ··· 515 522 516 523 // --- touch --- 517 524 var touches = {}; 525 + var touchMoved = false; 526 + var selectedIndex = -1; // for tap-to-select, tap-again-to-open 518 527 519 528 canvas.addEventListener('touchstart', function(e) { 520 529 e.preventDefault(); ··· 522 531 var t = e.changedTouches[i]; 523 532 touches[t.identifier] = { x: t.clientX, y: t.clientY }; 524 533 } 534 + touchMoved = false; 525 535 var ids = Object.keys(touches); 526 536 if (ids.length === 1) { 527 537 dragging = true; ··· 532 542 var a = touches[ids[0]], b = touches[ids[1]]; 533 543 pinchStartDist = Math.hypot(a.x - b.x, a.y - b.y); 534 544 pinchStartZoom = view.zoom; 545 + pinchMidX = (a.x + b.x) / 2; 546 + pinchMidY = (a.y + b.y) / 2; 547 + pinchStartPanX = view.panX; 548 + pinchStartPanY = view.panY; 535 549 } 536 550 }, { passive: false }); 537 551 ··· 541 555 var t = e.changedTouches[i]; 542 556 touches[t.identifier] = { x: t.clientX, y: t.clientY }; 543 557 } 558 + touchMoved = true; 544 559 var ids = Object.keys(touches); 545 560 if (ids.length === 1 && dragging) { 546 561 cacheTransform(); 547 562 view.panX = dragStartPanX + (touches[ids[0]].x - dragStartX) / scale; 548 563 view.panY = dragStartPanY + (touches[ids[0]].y - dragStartY) / scale; 549 564 view.dirty = true; 565 + hideTooltip(); 566 + selectedIndex = -1; 550 567 } else if (ids.length === 2) { 551 568 var a = touches[ids[0]], b = touches[ids[1]]; 552 569 var dist = Math.hypot(a.x - b.x, a.y - b.y); 553 - view.zoom = Math.max(view.minZoom, Math.min(view.maxZoom, pinchStartZoom * (dist / pinchStartDist))); 570 + var newZoom = Math.max(view.minZoom, Math.min(view.maxZoom, pinchStartZoom * (dist / pinchStartDist))); 571 + // zoom toward pinch midpoint 572 + var midDataOld = screenToData(pinchMidX, pinchMidY); 573 + view.zoom = newZoom; 574 + cacheTransform(); 575 + var midDataNew = screenToData(pinchMidX, pinchMidY); 576 + view.panX += midDataNew[0] - midDataOld[0]; 577 + view.panY += midDataNew[1] - midDataOld[1]; 554 578 view.dirty = true; 555 579 } 556 580 }, { passive: false }); 557 581 558 582 canvas.addEventListener('touchend', function(e) { 559 - for (var i = 0; i < e.changedTouches.length; i++) delete touches[e.changedTouches[i].identifier]; 560 - if (Object.keys(touches).length === 0) dragging = false; 583 + var endedTouches = []; 584 + for (var i = 0; i < e.changedTouches.length; i++) { 585 + endedTouches.push(e.changedTouches[i]); 586 + delete touches[e.changedTouches[i].identifier]; 587 + } 588 + var remaining = Object.keys(touches).length; 589 + if (remaining === 0) { 590 + dragging = false; 591 + // tap detection — didn't drag significantly 592 + if (!touchMoved || (endedTouches.length === 1 && 593 + Math.abs(endedTouches[0].clientX - dragStartX) < 10 && 594 + Math.abs(endedTouches[0].clientY - dragStartY) < 10)) { 595 + var tx = endedTouches[0].clientX, ty = endedTouches[0].clientY; 596 + cacheTransform(); 597 + var idx = findNearest(tx, ty, HIT_RADIUS); 598 + if (idx >= 0) { 599 + if (idx === selectedIndex) { 600 + // second tap on same point — open URL 601 + var p = data.points[idx]; 602 + var url = atUriToUrl(p.uri, p.basePath, p.platform, p.path); 603 + if (url) window.open(url, '_blank'); 604 + selectedIndex = -1; 605 + hideTooltip(); 606 + } else { 607 + // first tap — show tooltip 608 + selectedIndex = idx; 609 + hoveredIndex = idx; 610 + showTooltip(idx, tx, ty); 611 + view.dirty = true; 612 + } 613 + } else { 614 + // tapped empty space — dismiss 615 + selectedIndex = -1; 616 + hideTooltip(); 617 + view.dirty = true; 618 + } 619 + } 620 + } 561 621 }); 562 622 563 623 // --- tooltip --- ··· 576 636 tooltipPlatform.style.color = c.core; 577 637 tooltip.style.display = 'block'; 578 638 var tw = tooltip.offsetWidth, th = tooltip.offsetHeight; 579 - var tx = sx + 16, ty = sy - th - 8; 580 - if (tx + tw > W - 10) tx = sx - tw - 16; 581 - if (ty < 10) ty = sy + 16; 582 - tooltip.style.left = tx + 'px'; 583 - tooltip.style.top = ty + 'px'; 639 + if (isMobile) { 640 + // on mobile, anchor tooltip at top center of screen 641 + var tx = Math.max(8, Math.min(W - tw - 8, (W - tw) / 2)); 642 + tooltip.style.left = tx + 'px'; 643 + tooltip.style.top = '48px'; 644 + } else { 645 + var tx = sx + 16, ty = sy - th - 8; 646 + if (tx + tw > W - 10) tx = sx - tw - 16; 647 + if (ty < 10) ty = sy + 16; 648 + tooltip.style.left = tx + 'px'; 649 + tooltip.style.top = ty + 'px'; 650 + } 584 651 canvas.style.cursor = 'pointer'; 585 652 } 586 653