fuzzyworld

north-star / commit

testing arrow separation

feaee58345bb68ff3132ca09ddca98ecb6e296b7 · Pavel Safronov · 2026-03-16 00:25:25 -0700 · parents cf7471ca2d5c

testing arrow separation
 src/renderer.ts | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 87 insertions(+)

diff --git a/src/renderer.ts b/src/renderer.ts
index eaefef8..7e9d7aa 100644
--- a/src/renderer.ts
+++ b/src/renderer.ts
@@ -3,6 +3,81 @@ import type { PresenceThresholds } from './utils';
 
 type ArrowElement = HTMLElement & { currentAngle: number | null };
 
+const OVERLAP_CLUSTER_DEG = 8;
+const OVERLAP_SPACING_PX = 16;
+
+const getAngleDistance = (a: number, b: number) => Math.abs(getShortestDelta(a, b));
+
+const buildOverlapOffsets = ({
+    friendsToRender,
+    selfFriend,
+    selfHeading,
+    isFriendVisible,
+}: {
+    friendsToRender: any[];
+    selfFriend: any;
+    selfHeading: number;
+    isFriendVisible: (friendId: string) => boolean;
+}) => {
+    const angleEntries = friendsToRender
+        .filter((target) => target.id !== selfFriend.id && isFriendVisible(target.id))
+        .map((target) => {
+            const dx = target.position.lon - selfFriend.position.lon;
+            const dy = target.position.lat - selfFriend.position.lat;
+            const rawAngle = Math.atan2(dx, dy) * (180 / Math.PI);
+            return {
+                id: target.id,
+                angle: normalizeAngle(rawAngle - selfHeading),
+            };
+        })
+        .sort((a, b) => a.angle - b.angle);
+
+    if (angleEntries.length === 0) return new Map<string, number>();
+
+    const clusters: Array<Array<{ id: string; angle: number }>> = [];
+    for (const entry of angleEntries) {
+        const currentCluster = clusters[clusters.length - 1];
+        if (!currentCluster) {
+            clusters.push([entry]);
+            continue;
+        }
+
+        const lastInCluster = currentCluster[currentCluster.length - 1];
+        if (getAngleDistance(lastInCluster.angle, entry.angle) <= OVERLAP_CLUSTER_DEG) {
+            currentCluster.push(entry);
+        } else {
+            clusters.push([entry]);
+        }
+    }
+
+    if (clusters.length > 1) {
+        const firstCluster = clusters[0];
+        const lastCluster = clusters[clusters.length - 1];
+        const firstAngle = firstCluster[0].angle;
+        const lastAngle = lastCluster[lastCluster.length - 1].angle;
+        if (getAngleDistance(firstAngle, lastAngle) <= OVERLAP_CLUSTER_DEG) {
+            clusters[0] = [...lastCluster, ...firstCluster];
+            clusters.pop();
+        }
+    }
+
+    const offsetById = new Map<string, number>();
+    for (const cluster of clusters) {
+        if (cluster.length === 1) {
+            offsetById.set(cluster[0].id, 0);
+            continue;
+        }
+
+        const stableCluster = [...cluster].sort((a, b) => a.id.localeCompare(b.id));
+        const midpoint = (stableCluster.length - 1) / 2;
+        stableCluster.forEach((entry, index) => {
+            offsetById.set(entry.id, (index - midpoint) * OVERLAP_SPACING_PX);
+        });
+    }
+
+    return offsetById;
+};
+
 export const renderArrows = ({
     content,
     arrow,
@@ -14,6 +89,12 @@ export const renderArrows = ({
     presenceThresholds,
 }) => {
     const now = Date.now();
+    const overlapOffsetById = buildOverlapOffsets({
+        friendsToRender,
+        selfFriend,
+        selfHeading,
+        isFriendVisible,
+    });
     const activeArrowIds = new Set(
         friendsToRender
             .filter((target) => target.id !== selfFriend.id)
@@ -68,6 +149,12 @@ export const renderArrows = ({
         existingArrow.style.transform = `rotate(${currentArrowAngle}deg)`;
         existingArrow.style.color = getArrowDisplayColor(target.color);
 
+        const overlapOffset = overlapOffsetById.get(target.id) ?? 0;
+        const angleRad = currentArrowAngle * (Math.PI / 180);
+        const offsetX = overlapOffset * Math.cos(angleRad);
+        const offsetY = overlapOffset * Math.sin(angleRad);
+        existingArrow.style.translate = `calc(-50% + ${offsetX.toFixed(2)}px) calc(-50% + ${offsetY.toFixed(2)}px)`;
+
         const presenceState = getFriendPresenceState(target, now, presenceThresholds);
         existingArrow.classList.toggle('is-online', presenceState === 'online');
         existingArrow.classList.toggle('is-recent-offline', presenceState === 'recent-offline');