fuzzyworld

north-star / main_old / files / src / features / friends / renderer.ts

Branch main_old · default branch · all branches

renderer.ts

Blame: the commit that last changed each line · file · history

f32f5e95 2026-03-16 Pavel Safronov1import { getShortestDelta, normalizeAngle, getFriendPresenceState } from '../../core/utils';
2import type { PresenceThresholds } from '../../core/utils';
92f24c4e 2026-03-16 Pavel Safronov3
4type ArrowElement = HTMLElement & { currentAngle: number | null };
36d64b37 2026-03-15 Pavel Safronov5
b4e7f682 2026-03-16 Pavel Safronov6type RenderableFriend = {
7 id: string;
8 name: string;
9 color: string;
10 position: { lat: number; lon: number };
11 timestamp?: number;
12 lastSeen?: number;
13};
14
36d64b37 2026-03-15 Pavel Safronov15export const renderArrows = ({
16 content,
17 arrow,
e790982a 2026-03-16 Pavel Safronov18 friendsToRender,
19 selfFriend,
36d64b37 2026-03-15 Pavel Safronov20 selfHeading,
e790982a 2026-03-16 Pavel Safronov21 isFriendVisible,
e4870993 2026-03-16 Pavel Safronov22 getArrowDisplayColor,
9fda5271 2026-03-16 Pavel Safronov23 presenceThresholds,
b4e7f682 2026-03-16 Pavel Safronov24}: {
25 content: HTMLElement;
26 arrow: HTMLElement;
27 friendsToRender: RenderableFriend[];
28 selfFriend: RenderableFriend;
29 selfHeading: number;
30 isFriendVisible: (friendId: string) => boolean;
31 getArrowDisplayColor: (hexColor: string) => string;
32 presenceThresholds: PresenceThresholds;
36d64b37 2026-03-15 Pavel Safronov33}) => {
e4870993 2026-03-16 Pavel Safronov34 const now = Date.now();
36d64b37 2026-03-15 Pavel Safronov35 const activeArrowIds = new Set(
e790982a 2026-03-16 Pavel Safronov36 friendsToRender
b4e7f682 2026-03-16 Pavel Safronov37 .filter((target: RenderableFriend) => target.id !== selfFriend.id)
38 .map((target: RenderableFriend) => `arrow-${target.id}`)
36d64b37 2026-03-15 Pavel Safronov39 );
40
e790982a 2026-03-16 Pavel Safronov41 for (const target of friendsToRender) {
42 if (target.id === selfFriend.id) continue;
36d64b37 2026-03-15 Pavel Safronov43
92f24c4e 2026-03-16 Pavel Safronov44 let existingArrow = document.getElementById(`arrow-${target.id}`) as ArrowElement | null;
36d64b37 2026-03-15 Pavel Safronov45 if (!existingArrow) {
92f24c4e 2026-03-16 Pavel Safronov46 const newArrow = arrow.cloneNode(true) as ArrowElement;
36d64b37 2026-03-15 Pavel Safronov47 newArrow.id = `arrow-${target.id}`;
48 newArrow.style.display = 'block';
49 newArrow.currentAngle = null;
50
51 const arrowLabelAnchor = document.createElement('div');
52 arrowLabelAnchor.className = 'arrow-label-anchor';
53
54 const arrowLabel = document.createElement('span');
55 arrowLabel.className = 'arrow-label';
56 arrowLabel.textContent = target.name;
57 arrowLabelAnchor.appendChild(arrowLabel);
58 newArrow.appendChild(arrowLabelAnchor);
59
60 content.appendChild(newArrow);
61 existingArrow = newArrow;
62 }
63
64 content.appendChild(existingArrow);
65
e790982a 2026-03-16 Pavel Safronov66 if (!isFriendVisible(target.id)) {
36d64b37 2026-03-15 Pavel Safronov67 existingArrow.style.display = 'none';
68 continue;
69 }
70
71 existingArrow.style.display = 'block';
72
e790982a 2026-03-16 Pavel Safronov73 const dx = target.position.lon - selfFriend.position.lon;
74 const dy = target.position.lat - selfFriend.position.lat;
36d64b37 2026-03-15 Pavel Safronov75 const rawAngle = Math.atan2(dx, dy) * (180 / Math.PI);
76 const targetAngle = normalizeAngle(rawAngle - selfHeading);
77
78 let currentArrowAngle = existingArrow.currentAngle;
79 if (currentArrowAngle === null) {
80 currentArrowAngle = targetAngle;
81 } else {
82 currentArrowAngle += getShortestDelta(currentArrowAngle, targetAngle) * 0.1;
83 }
84
85 existingArrow.currentAngle = currentArrowAngle;
86 existingArrow.style.transform = `rotate(${currentArrowAngle}deg)`;
87 existingArrow.style.color = getArrowDisplayColor(target.color);
efc31a14 2026-03-16 Pavel Safronov88 existingArrow.style.translate = '';
feaee583 2026-03-16 Pavel Safronov89
e4870993 2026-03-16 Pavel Safronov90 const presenceState = getFriendPresenceState(target, now, presenceThresholds);
91 existingArrow.classList.toggle('is-online', presenceState === 'online');
92 existingArrow.classList.toggle('is-recent-offline', presenceState === 'recent-offline');
93 existingArrow.classList.toggle('is-stale-offline', presenceState === 'stale-offline');
94
92f24c4e 2026-03-16 Pavel Safronov95 const arrowLabel = existingArrow.querySelector('.arrow-label') as HTMLElement | null;
36d64b37 2026-03-15 Pavel Safronov96 if (arrowLabel) {
9fda5271 2026-03-16 Pavel Safronov97 arrowLabel.textContent = target.name;
36d64b37 2026-03-15 Pavel Safronov98 arrowLabel.style.transform = `rotate(${-currentArrowAngle}deg)`;
99 }
100 }
101
b4e7f682 2026-03-16 Pavel Safronov102 content.querySelectorAll<HTMLElement>('.arrow[id^="arrow-"]').forEach((arrowElement: HTMLElement) => {
36d64b37 2026-03-15 Pavel Safronov103 if (!activeArrowIds.has(arrowElement.id)) {
104 arrowElement.remove();
105 }
106 });
107};