@yancyyu/openhermit 1.6.38 → 1.6.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/dist-renderer/assets/ProjectEditorOverlay-krO5vQxX.js +58 -0
  2. package/dist-renderer/assets/{TeamGraphOverlay-ZEDfZyHb.js → TeamGraphOverlay-DqhQzcTr.js} +1 -1
  3. package/dist-renderer/assets/{_basePickBy-CIhniz70.js → _basePickBy-B7kSYPxr.js} +1 -1
  4. package/dist-renderer/assets/{_baseUniq-cKAW4Q8I.js → _baseUniq-CnjxqwAk.js} +1 -1
  5. package/dist-renderer/assets/{arc-YmNsoDXW.js → arc-CLeZuINP.js} +1 -1
  6. package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-DHEls2sX.js → architectureDiagram-VXUJARFQ-QKtqaqdY.js} +1 -1
  7. package/dist-renderer/assets/{blockDiagram-VD42YOAC-Bpwf1Sbg.js → blockDiagram-VD42YOAC-BqdrzO_f.js} +1 -1
  8. package/dist-renderer/assets/{c4Diagram-YG6GDRKO-B0IaQ4w5.js → c4Diagram-YG6GDRKO-gwPlCxDC.js} +1 -1
  9. package/dist-renderer/assets/channel-DpMHF50r.js +1 -0
  10. package/dist-renderer/assets/{chunk-4BX2VUAB-DLk-hcFc.js → chunk-4BX2VUAB-C6XLurL4.js} +1 -1
  11. package/dist-renderer/assets/{chunk-55IACEB6-1XRmX_Zm.js → chunk-55IACEB6-Ds6quhEP.js} +1 -1
  12. package/dist-renderer/assets/{chunk-B4BG7PRW-1waH1DAD.js → chunk-B4BG7PRW-5UlA1_e9.js} +1 -1
  13. package/dist-renderer/assets/{chunk-DI55MBZ5-BqpZBtrN.js → chunk-DI55MBZ5-ywFrqIsY.js} +1 -1
  14. package/dist-renderer/assets/{chunk-FMBD7UC4-Bly7vVym.js → chunk-FMBD7UC4-C7ifUA17.js} +1 -1
  15. package/dist-renderer/assets/{chunk-QN33PNHL-Ci2QWBAs.js → chunk-QN33PNHL-BxGCo80U.js} +1 -1
  16. package/dist-renderer/assets/{chunk-QZHKN3VN-YCqFW7d-.js → chunk-QZHKN3VN-B2CuaZs6.js} +1 -1
  17. package/dist-renderer/assets/{chunk-TZMSLE5B-B0xGXInl.js → chunk-TZMSLE5B-Ds1hInvp.js} +1 -1
  18. package/dist-renderer/assets/classDiagram-2ON5EDUG-CBYCBVRl.js +1 -0
  19. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-CBYCBVRl.js +1 -0
  20. package/dist-renderer/assets/clone-DcMF6Psb.js +1 -0
  21. package/dist-renderer/assets/{cose-bilkent-S5V4N54A-DxcFNQKT.js → cose-bilkent-S5V4N54A-Cz1GVtLp.js} +1 -1
  22. package/dist-renderer/assets/{dagre-6UL2VRFP-DPo_RfZY.js → dagre-6UL2VRFP-BrmR-P4h.js} +1 -1
  23. package/dist-renderer/assets/{diagram-PSM6KHXK-U3hQsFe4.js → diagram-PSM6KHXK-DbNjC5Rg.js} +1 -1
  24. package/dist-renderer/assets/{diagram-QEK2KX5R-OrwrAy0V.js → diagram-QEK2KX5R-qkRX5_Mq.js} +1 -1
  25. package/dist-renderer/assets/{diagram-S2PKOQOG-CXATPWVw.js → diagram-S2PKOQOG-CyL5rCv2.js} +1 -1
  26. package/dist-renderer/assets/{erDiagram-Q2GNP2WA-B0e8AfMF.js → erDiagram-Q2GNP2WA-Dox3-bA5.js} +1 -1
  27. package/dist-renderer/assets/{flowDiagram-NV44I4VS-CXfzA4jJ.js → flowDiagram-NV44I4VS-BtkaxlDL.js} +1 -1
  28. package/dist-renderer/assets/{ganttDiagram-JELNMOA3-CMr08qVl.js → ganttDiagram-JELNMOA3-Dhy_d9GK.js} +1 -1
  29. package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-vYFHpPmy.js → gitGraphDiagram-V2S2FVAM-B5XRhIQA.js} +1 -1
  30. package/dist-renderer/assets/{graph-DOe5j8dH.js → graph-CsoEwUhS.js} +1 -1
  31. package/dist-renderer/assets/{index-BySQS7AB.js → index-BWPWmJNo.js} +1 -1
  32. package/dist-renderer/assets/{index-V7dAKPqd.js → index-Bu2R-Se7.js} +587 -705
  33. package/dist-renderer/assets/index-CnWV3BhG.css +32 -0
  34. package/dist-renderer/assets/{index-CzWxVCRL.js → index-D-3KgskL.js} +1 -1
  35. package/dist-renderer/assets/{index-VJ-MM9xa.js → index-DGEBzLNT.js} +1 -1
  36. package/dist-renderer/assets/{index-B2Dy7M2G.js → index-NhHNs2Oo.js} +1 -1
  37. package/dist-renderer/assets/{index-C_okzZXP.js → index-h17WuEyf.js} +1 -1
  38. package/dist-renderer/assets/{infoDiagram-HS3SLOUP-D_WubR0B.js → infoDiagram-HS3SLOUP-hMGmNojH.js} +1 -1
  39. package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-w9ca-1TI.js → journeyDiagram-XKPGCS4Q-DXV2rBDl.js} +1 -1
  40. package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-Jg9p6_pN.js → kanban-definition-3W4ZIXB7-Bf99WLRy.js} +1 -1
  41. package/dist-renderer/assets/{layout-B-z3y17c.js → layout-C3XWrpwo.js} +1 -1
  42. package/dist-renderer/assets/{linear-D-RTX5UW.js → linear-OEEcn8KN.js} +1 -1
  43. package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-CDQmHOYP.js → mindmap-definition-VGOIOE7T-Dpi3S2x4.js} +1 -1
  44. package/dist-renderer/assets/{pieDiagram-ADFJNKIX-D_odsQL7.js → pieDiagram-ADFJNKIX-xTPPhtNx.js} +1 -1
  45. package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-BRsmYWSA.js → quadrantDiagram-AYHSOK5B-euniyDlz.js} +1 -1
  46. package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-ChNE_BOV.js → requirementDiagram-UZGBJVZJ-D9Uiw4kF.js} +1 -1
  47. package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-C8FtpwKc.js → sankeyDiagram-TZEHDZUN-CySU4nED.js} +1 -1
  48. package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-DmLCzNcc.js → sequenceDiagram-WL72ISMW-JVGpET6V.js} +1 -1
  49. package/dist-renderer/assets/splashScene-D0YB9uxm.js +17 -0
  50. package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-WJBm4bhu.js → stateDiagram-FKZM4ZOC-B2FY5qqi.js} +1 -1
  51. package/dist-renderer/assets/stateDiagram-v2-4FDKWEC3-DcoMiR8H.js +1 -0
  52. package/dist-renderer/assets/{timeline-definition-IT6M3QCI-BXs_hOJs.js → timeline-definition-IT6M3QCI-DmycNUUe.js} +1 -1
  53. package/dist-renderer/assets/{treemap-GDKQZRPO-o04MA0G9.js → treemap-GDKQZRPO-DPq4gZuB.js} +1 -1
  54. package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-Czj69XRd.js → xychartDiagram-PRI3JC2R-J6VVJzRq.js} +1 -1
  55. package/dist-renderer/index.html +20 -53
  56. package/package.json +25 -18
  57. package/src/main/ipc/extensions.ts +2 -1
  58. package/src/main/server.ts +873 -221
  59. package/src/main/services/extensions/ExtensionFacadeService.ts +2 -5
  60. package/src/main/services/extensions/catalog/PluginCatalogService.ts +4 -2
  61. package/src/main/services/session-intelligence/ConversationTelemetryService.ts +1101 -0
  62. package/src/main/services/session-intelligence/LocalSessionScanner.ts +512 -0
  63. package/src/main/services/session-intelligence/SessionUsageParser.ts +4 -4
  64. package/src/main/services/system-manager/SystemManagerConfigService.ts +122 -0
  65. package/src/main/services/system-manager/SystemManagerPtyService.ts +233 -0
  66. package/src/main/services/system-manager/WorkflowPromptService.ts +75 -0
  67. package/src/main/services/teams-mvp/TaskDispatchService.ts +5 -6
  68. package/src/main/services/teams-mvp/TeamProvisioningService.ts +39 -2
  69. package/src/main/services/teams-mvp/TeamWorkspaceService.ts +22 -4
  70. package/src/main/utils/teamProjectResolution.ts +15 -0
  71. package/src/renderer/App.tsx +8 -4
  72. package/src/renderer/api/httpClient.ts +68 -18
  73. package/src/renderer/api/providers.ts +23 -2
  74. package/src/renderer/assets/participant-avatars/01.svg +3 -0
  75. package/src/renderer/assets/participant-avatars/02.svg +3 -0
  76. package/src/renderer/assets/participant-avatars/03.svg +3 -0
  77. package/src/renderer/assets/participant-avatars/04.svg +3 -0
  78. package/src/renderer/assets/participant-avatars/05.svg +3 -0
  79. package/src/renderer/assets/participant-avatars/06.svg +3 -0
  80. package/src/renderer/assets/participant-avatars/07.svg +3 -0
  81. package/src/renderer/assets/participant-avatars/08.svg +3 -0
  82. package/src/renderer/assets/participant-avatars/09.svg +3 -0
  83. package/src/renderer/assets/participant-avatars/10.svg +3 -0
  84. package/src/renderer/assets/participant-avatars/11.svg +3 -0
  85. package/src/renderer/assets/participant-avatars/12.svg +3 -0
  86. package/src/renderer/assets/participant-avatars/13.svg +3 -0
  87. package/src/renderer/components/common/TerminalPane.tsx +213 -0
  88. package/src/renderer/components/dashboard/DashboardView.tsx +9 -36
  89. package/src/renderer/components/extensions/ExtensionStoreView.tsx +6 -125
  90. package/src/renderer/components/extensions/ExtensionsSubTabTrigger.tsx +1 -1
  91. package/src/renderer/components/extensions/mcp/McpLibraryEnableDialog.tsx +305 -0
  92. package/src/renderer/components/extensions/mcp/McpLibraryEntryDialog.tsx +418 -0
  93. package/src/renderer/components/extensions/mcp/McpLibraryPanel.tsx +404 -0
  94. package/src/renderer/components/extensions/plugins/PluginCard.tsx +6 -6
  95. package/src/renderer/components/extensions/plugins/PluginsPanel.tsx +34 -21
  96. package/src/renderer/components/extensions/skills/SkillsLibraryPanel.tsx +335 -0
  97. package/src/renderer/components/layout/PaneContent.tsx +8 -1
  98. package/src/renderer/components/layout/Sidebar.tsx +11 -54
  99. package/src/renderer/components/layout/SortableTab.tsx +20 -31
  100. package/src/renderer/components/layout/TabBar.tsx +1 -1
  101. package/src/renderer/components/layout/TabContextMenu.tsx +1 -1
  102. package/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx +768 -157
  103. package/src/renderer/components/schedules/SchedulesView.tsx +51 -462
  104. package/src/renderer/components/schedules/calendar/CalendarDayView.tsx +173 -0
  105. package/src/renderer/components/schedules/calendar/CalendarEventBlock.tsx +113 -0
  106. package/src/renderer/components/schedules/calendar/CalendarHeader.tsx +148 -0
  107. package/src/renderer/components/schedules/calendar/CalendarMonthView.tsx +142 -0
  108. package/src/renderer/components/schedules/calendar/CalendarWeekView.tsx +219 -0
  109. package/src/renderer/components/schedules/calendar/ScheduleCalendarBoard.tsx +41 -0
  110. package/src/renderer/components/schedules/calendar/TeamGanttView.tsx +405 -0
  111. package/src/renderer/components/schedules/calendar/computeOccurrences.ts +234 -0
  112. package/src/renderer/components/schedules/calendar/index.ts +2 -0
  113. package/src/renderer/components/schedules/calendar/types.ts +44 -0
  114. package/src/renderer/components/settings/SettingsTabs.tsx +50 -55
  115. package/src/renderer/components/settings/SettingsView.tsx +30 -35
  116. package/src/renderer/components/settings/components/SettingsSectionHeader.tsx +5 -1
  117. package/src/renderer/components/settings/components/SettingsSelect.tsx +5 -3
  118. package/src/renderer/components/settings/components/SettingsToggle.tsx +2 -2
  119. package/src/renderer/components/settings/sections/AdvancedSection.tsx +11 -42
  120. package/src/renderer/components/settings/sections/CliStatusSection.tsx +71 -112
  121. package/src/renderer/components/settings/sections/ConfigEditorDialog.tsx +1 -1
  122. package/src/renderer/components/settings/sections/GeneralSection.tsx +11 -3
  123. package/src/renderer/components/settings/sections/HarnessSection.tsx +18 -14
  124. package/src/renderer/components/settings/sections/PlatformsSection.tsx +3 -3
  125. package/src/renderer/components/settings/sections/TaskBusSection.tsx +33 -40
  126. package/src/renderer/components/settings/sections/index.ts +0 -1
  127. package/src/renderer/components/sidebar/SidebarSessions.tsx +182 -4
  128. package/src/renderer/components/sidebar/SidebarTaskItem.tsx +4 -4
  129. package/src/renderer/components/sidebar/WorkspaceBrowser.tsx +39 -4
  130. package/src/renderer/components/splash/splashScene.ts +121 -929
  131. package/src/renderer/components/system-manager/FolderBrowser.tsx +163 -0
  132. package/src/renderer/components/system-manager/SystemManagerView.tsx +351 -0
  133. package/src/renderer/components/tasks/TasksView.tsx +112 -134
  134. package/src/renderer/components/team/CcSessionsSection.tsx +431 -89
  135. package/src/renderer/components/team/CollapsibleTeamSection.tsx +17 -32
  136. package/src/renderer/components/team/TeamDetailView.tsx +319 -123
  137. package/src/renderer/components/team/TeamListView.tsx +108 -123
  138. package/src/renderer/components/team/dialogs/CreateTaskDialog.tsx +2 -2
  139. package/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +84 -306
  140. package/src/renderer/components/team/dialogs/EditTeamDialog.tsx +259 -342
  141. package/src/renderer/components/team/dialogs/GlobalTaskDetailDialog.tsx +1 -1
  142. package/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx +17 -15
  143. package/src/renderer/components/team/dialogs/PlatformBindingDialog.tsx +221 -0
  144. package/src/renderer/components/team/dialogs/PlatformManualForm.tsx +7 -0
  145. package/src/renderer/components/team/dialogs/PlatformSetupQR.tsx +1 -1
  146. package/src/renderer/components/team/dialogs/RuntimeConfigDialog.tsx +361 -0
  147. package/src/renderer/components/team/dialogs/platformMeta.ts +122 -11
  148. package/src/renderer/components/team/dialogs/useTeamEditForm.ts +17 -5
  149. package/src/renderer/components/team/kanban/KanbanBoard.tsx +9 -9
  150. package/src/renderer/components/team/members/MemberCard.tsx +14 -47
  151. package/src/renderer/components/team/members/MemberDetailDialog.tsx +3 -95
  152. package/src/renderer/components/team/members/MemberDetailStats.tsx +50 -65
  153. package/src/renderer/components/team/messages/MessageComposer.tsx +8 -110
  154. package/src/renderer/components/team/messages/MessagesPanel.tsx +131 -114
  155. package/src/renderer/components/team/schedule/ScheduleStatusBadge.tsx +2 -2
  156. package/src/renderer/components/team/tools/AddMcpInline.tsx +27 -17
  157. package/src/renderer/components/team/tools/McpChip.tsx +6 -3
  158. package/src/renderer/components/team/tools/SkillChip.tsx +2 -2
  159. package/src/renderer/components/team/tools/ToolsSection.tsx +418 -70
  160. package/src/renderer/hooks/useExtensionsTabState.ts +3 -114
  161. package/src/renderer/index.css +39 -22
  162. package/src/renderer/index.html +17 -50
  163. package/src/renderer/store/index.ts +2 -1
  164. package/src/renderer/store/slices/scheduleSlice.ts +1 -1
  165. package/src/renderer/store/slices/teamSlice.ts +45 -168
  166. package/src/renderer/utils/claudeCodeOnlyProviders.ts +3 -10
  167. package/src/renderer/utils/memberHelpers.ts +5 -17
  168. package/src/renderer/utils/openCodeRuntimeDeliveryDiagnostics.ts +4 -2
  169. package/src/renderer/utils/providerSlashCommands.ts +0 -5
  170. package/src/renderer/utils/scheduleFormatters.ts +3 -1
  171. package/src/renderer/utils/teamMessageFiltering.ts +14 -1
  172. package/src/renderer/utils/teamModelAvailability.ts +18 -2
  173. package/src/shared/types/api.ts +121 -2
  174. package/src/shared/types/ccConnect.ts +2 -0
  175. package/src/shared/types/index.ts +3 -0
  176. package/src/shared/types/systemManager.ts +49 -0
  177. package/src/shared/types/team.ts +29 -0
  178. package/src/shared/types/terminal.ts +4 -2
  179. package/src/shared/utils/extensionNormalizers.ts +15 -8
  180. package/src/shared/utils/providerExtensionCapabilities.ts +2 -2
  181. package/dist-renderer/assets/ProjectEditorOverlay-lJZi-9Hp.js +0 -52
  182. package/dist-renderer/assets/channel-yIlSKy0e.js +0 -1
  183. package/dist-renderer/assets/classDiagram-2ON5EDUG-24fHez0s.js +0 -1
  184. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-24fHez0s.js +0 -1
  185. package/dist-renderer/assets/clone-BTNuUva-.js +0 -1
  186. package/dist-renderer/assets/index-Bi6nrZ4z.css +0 -1
  187. package/dist-renderer/assets/splashScene-C8lWNnm4.js +0 -1
  188. package/dist-renderer/assets/stateDiagram-v2-4FDKWEC3-_m6iPPUR.js +0 -1
@@ -1,4 +1,9 @@
1
- import { PARTICIPANT_AVATAR_URLS } from '@renderer/utils/memberAvatarCatalog';
1
+ /**
2
+ * splashScene — Terminal-style loading splash.
3
+ *
4
+ * Replaces the heavy canvas robot animation with a clean,
5
+ * Yume-inspired terminal boot sequence.
6
+ */
2
7
 
3
8
  export interface SplashSceneHandle {
4
9
  stop: () => void;
@@ -16,964 +21,151 @@ declare global {
16
21
  }
17
22
  }
18
23
 
19
- interface Point {
20
- x: number;
21
- y: number;
22
- }
23
-
24
- interface RobotNode extends Point {
25
- teamIndex: number;
26
- robotIndex: number;
27
- color: string;
28
- size: number;
29
- bob: number;
30
- receivePulse: number;
31
- avatarUrl: string;
32
- }
33
-
34
- interface TeamNode {
35
- index: number;
36
- center: Point;
37
- color: string;
38
- radius: number;
39
- robots: RobotNode[];
40
- }
41
-
42
- interface MessageFlightState {
43
- progress: number;
44
- motionSpeed: number;
45
- bubbleScale: number;
46
- bubbleAlpha: number;
47
- }
48
-
49
- interface DepthParticle {
50
- x: number;
51
- y: number;
52
- size: number;
53
- speed: number;
54
- phase: number;
55
- alpha: number;
56
- }
57
-
58
- interface Palette {
59
- isLight: boolean;
60
- centerGlow: string;
61
- teamColors: string[];
62
- robotBody: string;
63
- robotShade: string;
64
- robotEye: string;
65
- messageAccent: string;
66
- particle: string;
67
- }
68
-
69
- const TAU = Math.PI * 2;
70
- const TEAM_MEMBER_COUNTS = [4, 3, 5] as const;
71
- const TEAM_MEMBER_OFFSETS = [0, 4, 7] as const;
72
- const TEAM_LABELS = ['Marketing', 'Researchers', 'Coding'] as const;
73
- const MAX_DPR = 2;
74
- const avatarCache = new Map<string, HTMLImageElement>();
75
- const avatarLoading = new Map<string, Promise<HTMLImageElement | null>>();
24
+ const BOOT_LINES = [
25
+ '🦀 hermit v1.6.38',
26
+ 'connecting harness…',
27
+ 'loading team configs…',
28
+ 'scanning session history…',
29
+ 'indexing project files…',
30
+ 'ready.',
31
+ ];
76
32
 
77
33
  export function startSplashScene(
78
34
  splash: HTMLElement,
79
35
  options: SplashSceneOptions = {}
80
36
  ): SplashSceneHandle {
81
37
  const existingScene = window.__claudeTeamsSplashScene;
82
- if (existingScene && splash.querySelector('#splash-enhanced-canvas')) {
38
+ if (existingScene && splash.querySelector('#splash-terminal')) {
83
39
  return existingScene;
84
40
  }
85
41
 
86
- const ready = preloadAvatarImages();
87
- const previousCanvas = splash.querySelector<HTMLCanvasElement>('#splash-enhanced-canvas');
88
- previousCanvas?.remove();
89
-
90
- const canvas = document.createElement('canvas');
91
- canvas.id = 'splash-enhanced-canvas';
92
- canvas.setAttribute('aria-hidden', 'true');
93
- splash.appendChild(canvas);
94
-
95
- const ctx = canvas.getContext('2d', { alpha: true });
96
- if (!ctx) {
97
- const emptyHandle = {
98
- stop: () => {
99
- canvas.remove();
100
- },
101
- ready,
102
- };
103
- return emptyHandle;
104
- }
105
-
106
42
  const reducedMotion =
107
43
  options.reducedMotion ?? window.matchMedia('(prefers-reduced-motion: reduce)').matches;
108
- const state = {
109
- width: 1,
110
- height: 1,
111
- dpr: 1,
112
- particles: [] as DepthParticle[],
113
- running: true,
114
- frameId: 0,
115
- startedAt: performance.now(),
116
- };
117
44
 
118
- const resize = (): void => {
119
- const rect = splash.getBoundingClientRect();
120
- const width = Math.max(1, Math.round(rect.width));
121
- const height = Math.max(1, Math.round(rect.height));
122
- const dpr = Math.min(MAX_DPR, window.devicePixelRatio || 1);
123
-
124
- if (state.width === width && state.height === height && state.dpr === dpr) {
125
- return;
45
+ // Remove old canvas if present
46
+ splash.querySelector('#splash-enhanced-canvas')?.remove();
47
+
48
+ // Hide the original HTML splash content (logo, text, tagline)
49
+ const splashCopy = splash.querySelector('#splash-copy');
50
+ if (splashCopy instanceof HTMLElement) {
51
+ splashCopy.style.display = 'none';
52
+ }
53
+
54
+ const container = document.createElement('div');
55
+ container.id = 'splash-terminal';
56
+ container.style.cssText = `
57
+ position: absolute;
58
+ inset: 0;
59
+ display: flex;
60
+ flex-direction: column;
61
+ align-items: center;
62
+ justify-content: center;
63
+ gap: 8px;
64
+ font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
65
+ font-size: 13px;
66
+ line-height: 1.8;
67
+ color: rgba(148, 163, 184, 0.8);
68
+ padding: 24px;
69
+ opacity: 0;
70
+ z-index: 5;
71
+ animation: splash-term-in 0.4s ease-out 0.1s forwards;
72
+ `;
73
+
74
+ // Boot lines appear one by one
75
+ const linesContainer = document.createElement('div');
76
+ linesContainer.style.cssText = 'text-align: left; width: 100%; max-width: 320px;';
77
+ container.appendChild(linesContainer);
78
+
79
+ // Cursor
80
+ const cursor = document.createElement('span');
81
+ cursor.textContent = '█';
82
+ cursor.style.cssText = reducedMotion
83
+ ? 'opacity: 0.6;'
84
+ : 'animation: splash-cursor-blink 1s step-end infinite;';
85
+ cursor.setAttribute('aria-hidden', 'true');
86
+
87
+ splash.appendChild(container);
88
+
89
+ let running = true;
90
+ let lineIndex = 0;
91
+
92
+ function addNextLine(): void {
93
+ if (!running || lineIndex >= BOOT_LINES.length) return;
94
+
95
+ const line = BOOT_LINES[lineIndex];
96
+ if (!line) return;
97
+
98
+ const lineEl = document.createElement('div');
99
+ lineEl.style.cssText = 'opacity: 0; transform: translateY(4px); transition: opacity 0.3s, transform 0.3s;';
100
+
101
+ if (lineIndex === 0) {
102
+ // First line — brand with crab
103
+ const brand = document.createElement('span');
104
+ brand.textContent = '> ';
105
+ brand.style.color = 'rgba(148, 163, 184, 0.4)';
106
+ lineEl.appendChild(brand);
107
+
108
+ const cmd = document.createElement('span');
109
+ cmd.textContent = line;
110
+ cmd.style.color = 'rgba(226, 232, 240, 0.9)';
111
+ cmd.style.fontWeight = '500';
112
+ lineEl.appendChild(cmd);
113
+ } else if (line === 'ready.') {
114
+ const ready = document.createElement('span');
115
+ ready.textContent = '✓ ';
116
+ ready.style.color = 'rgba(52, 211, 153, 0.7)';
117
+ lineEl.appendChild(ready);
118
+
119
+ const text = document.createElement('span');
120
+ text.textContent = line;
121
+ text.style.color = 'rgba(52, 211, 153, 0.6)';
122
+ lineEl.appendChild(text);
123
+ } else {
124
+ const arrow = document.createElement('span');
125
+ arrow.textContent = ' ';
126
+ lineEl.appendChild(arrow);
127
+
128
+ const text = document.createElement('span');
129
+ text.textContent = line;
130
+ lineEl.appendChild(text);
126
131
  }
127
132
 
128
- state.width = width;
129
- state.height = height;
130
- state.dpr = dpr;
131
- canvas.width = Math.ceil(width * dpr);
132
- canvas.height = Math.ceil(height * dpr);
133
- canvas.style.width = `${width}px`;
134
- canvas.style.height = `${height}px`;
135
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
136
- state.particles = createDepthParticles(width, height);
137
- };
133
+ linesContainer.appendChild(lineEl);
138
134
 
139
- const render = (now: number): void => {
140
- if (!state.running) return;
135
+ // Animate in
136
+ requestAnimationFrame(() => {
137
+ lineEl.style.opacity = '1';
138
+ lineEl.style.transform = 'translateY(0)';
139
+ });
141
140
 
142
- resize();
143
- const time = (now - state.startedAt) / 1000;
144
- drawScene(ctx, state.width, state.height, time, state.particles, reducedMotion);
141
+ lineIndex++;
145
142
 
146
- if (!reducedMotion) {
147
- state.frameId = window.requestAnimationFrame(render);
143
+ if (lineIndex < BOOT_LINES.length) {
144
+ setTimeout(addNextLine, reducedMotion ? 80 : 280);
145
+ } else {
146
+ // Append cursor after last line
147
+ const lastLine = linesContainer.lastElementChild;
148
+ if (lastLine) {
149
+ lastLine.appendChild(cursor);
150
+ }
148
151
  }
149
- };
152
+ }
150
153
 
151
- const onResize = (): void => resize();
152
- window.addEventListener('resize', onResize);
153
- resize();
154
- render(performance.now());
154
+ // Start boot sequence after a brief delay
155
+ setTimeout(addNextLine, reducedMotion ? 50 : 120);
155
156
 
156
157
  const handle: SplashSceneHandle = {
157
158
  stop: () => {
158
- state.running = false;
159
- window.cancelAnimationFrame(state.frameId);
160
- window.removeEventListener('resize', onResize);
161
- canvas.remove();
159
+ running = false;
160
+ container.remove();
162
161
  if (window.__claudeTeamsSplashScene === handle) {
163
162
  window.__claudeTeamsSplashScene = undefined;
164
163
  window.__claudeTeamsSplashEnhancedStartedAt = undefined;
165
164
  }
166
165
  },
167
- ready,
168
166
  };
169
167
  window.__claudeTeamsSplashScene = handle;
170
168
  window.__claudeTeamsSplashEnhancedStartedAt = performance.now();
171
169
 
172
170
  return handle;
173
171
  }
174
-
175
- function drawScene(
176
- ctx: CanvasRenderingContext2D,
177
- width: number,
178
- height: number,
179
- time: number,
180
- particles: DepthParticle[],
181
- reducedMotion: boolean
182
- ): void {
183
- ctx.clearRect(0, 0, width, height);
184
- const palette = resolvePalette();
185
- const mobile = width < 560 || height < 620;
186
- const sceneTime = reducedMotion ? 1.2 : time;
187
- const teams = buildTeams(width, height, sceneTime, mobile, palette);
188
- const center = getCenter(width, height, mobile);
189
-
190
- drawAmbientField(ctx, width, height, sceneTime, particles, palette, mobile);
191
- drawCenterAura(ctx, center, sceneTime, palette, mobile);
192
- drawCrossTeamGuides(ctx, teams, sceneTime, palette);
193
-
194
- for (const team of teams) {
195
- drawTeamHalo(ctx, team, sceneTime, palette);
196
- }
197
-
198
- drawMessages(ctx, teams, sceneTime, palette, mobile);
199
-
200
- for (const team of teams) {
201
- for (const robot of team.robots) {
202
- drawRobot(ctx, robot, sceneTime, palette);
203
- }
204
- }
205
-
206
- for (const team of teams) {
207
- drawTeamLabel(ctx, team, palette, mobile);
208
- }
209
- }
210
-
211
- function resolvePalette(): Palette {
212
- const isLight = document.documentElement.classList.contains('light');
213
- return isLight
214
- ? {
215
- isLight,
216
- centerGlow: '#4f46e5',
217
- teamColors: ['#0369a1', '#047857', '#b45309'],
218
- robotBody: '#eef2ff',
219
- robotShade: '#dbe4ff',
220
- robotEye: '#ffffff',
221
- messageAccent: '#7c3aed',
222
- particle: '#312e81',
223
- }
224
- : {
225
- isLight,
226
- centerGlow: '#7c83f7',
227
- teamColors: ['#24a8d8', '#23b488', '#d58a19'],
228
- robotBody: '#0f1724',
229
- robotShade: '#1a2438',
230
- robotEye: '#d8f3ff',
231
- messageAccent: '#8b5cf6',
232
- particle: '#a6a4d6',
233
- };
234
- }
235
-
236
- function getCenter(width: number, height: number, mobile: boolean): Point {
237
- return {
238
- x: width / 2,
239
- y: height * (mobile ? 0.47 : 0.49),
240
- };
241
- }
242
-
243
- function buildTeams(
244
- width: number,
245
- height: number,
246
- time: number,
247
- mobile: boolean,
248
- palette: Palette
249
- ): TeamNode[] {
250
- const center = getCenter(width, height, mobile);
251
- const spreadX = mobile ? Math.min(width * 0.36, 148) : Math.min(width * 0.34, 380);
252
- const spreadY = mobile ? Math.min(height * 0.22, 154) : Math.min(height * 0.22, 220);
253
- const teamRadius = mobile
254
- ? clamp(Math.min(width, height) * 0.092, 31, 42)
255
- : clamp(Math.min(width, height) * 0.072, 42, 62);
256
- const robotSize = mobile ? 9.8 : 11.8;
257
- const centers: Point[] = [
258
- {
259
- x: center.x - spreadX,
260
- y: center.y - spreadY * (mobile ? 0.66 : 0.58),
261
- },
262
- {
263
- x: center.x + spreadX,
264
- y: center.y - spreadY * (mobile ? 0.66 : 0.58),
265
- },
266
- {
267
- x: center.x,
268
- y: center.y + spreadY * (mobile ? 1.34 : 1.18),
269
- },
270
- ];
271
-
272
- return centers.map((teamCenter, teamIndex) => {
273
- const drift = Math.sin(time * 0.75 + teamIndex * 1.7) * (mobile ? 2.2 : 4.2);
274
- const centerWithDrift = {
275
- x: teamCenter.x + Math.cos(teamIndex * 2.1 + time * 0.35) * (mobile ? 1.4 : 2.8),
276
- y: teamCenter.y + drift,
277
- };
278
- const color = palette.teamColors[teamIndex % palette.teamColors.length] ?? palette.centerGlow;
279
- const memberCount = TEAM_MEMBER_COUNTS[teamIndex] ?? 3;
280
- const robots = Array.from({ length: memberCount }, (_, robotIndex) => {
281
- const baseAngle =
282
- -Math.PI / 2 + robotIndex * (TAU / memberCount) + (teamIndex === 2 ? TAU / 20 : 0);
283
- const orbit = baseAngle + Math.sin(time * 0.55 + teamIndex + robotIndex) * 0.07;
284
- const orbitRadius =
285
- teamRadius * (0.94 + (memberCount > 4 ? 0.07 : 0) + 0.03 * Math.sin(time + robotIndex));
286
- return {
287
- teamIndex,
288
- robotIndex,
289
- color,
290
- size: memberCount > 4 ? robotSize * 0.88 : robotSize,
291
- bob: Math.sin(time * 2.2 + teamIndex * 0.8 + robotIndex * 1.1),
292
- receivePulse: 0,
293
- avatarUrl:
294
- PARTICIPANT_AVATAR_URLS[(TEAM_MEMBER_OFFSETS[teamIndex] ?? 0) + robotIndex] ??
295
- PARTICIPANT_AVATAR_URLS[0],
296
- x: centerWithDrift.x + Math.cos(orbit) * orbitRadius,
297
- y: centerWithDrift.y + Math.sin(orbit) * orbitRadius,
298
- };
299
- });
300
-
301
- return {
302
- index: teamIndex,
303
- center: centerWithDrift,
304
- color,
305
- radius: teamRadius,
306
- robots,
307
- };
308
- });
309
- }
310
-
311
- function drawAmbientField(
312
- ctx: CanvasRenderingContext2D,
313
- width: number,
314
- height: number,
315
- time: number,
316
- particles: DepthParticle[],
317
- palette: Palette,
318
- mobile: boolean
319
- ): void {
320
- const visibleParticles = mobile ? Math.floor(particles.length * 0.6) : particles.length;
321
- for (let i = 0; i < visibleParticles; i++) {
322
- const particle = particles[i];
323
- if (!particle) continue;
324
- const y = (particle.y + time * particle.speed) % (height + 24);
325
- const x = particle.x + Math.sin(time * 0.45 + particle.phase) * 8;
326
- const pulse = 0.78 + Math.sin(time * 1.8 + particle.phase) * 0.22;
327
- ctx.beginPath();
328
- ctx.fillStyle = withAlpha(palette.particle, particle.alpha * pulse);
329
- ctx.arc(x, y - 12, particle.size, 0, TAU);
330
- ctx.fill();
331
- }
332
- }
333
-
334
- function drawCenterAura(
335
- ctx: CanvasRenderingContext2D,
336
- center: Point,
337
- time: number,
338
- palette: Palette,
339
- mobile: boolean
340
- ): void {
341
- const radius = mobile ? 86 : 128;
342
- const glow = ctx.createRadialGradient(center.x, center.y, 20, center.x, center.y, radius);
343
- glow.addColorStop(0, withAlpha(palette.centerGlow, palette.isLight ? 0.1 : 0.14));
344
- glow.addColorStop(0.48, withAlpha(palette.messageAccent, palette.isLight ? 0.04 : 0.07));
345
- glow.addColorStop(1, withAlpha(palette.centerGlow, 0));
346
- ctx.fillStyle = glow;
347
- ctx.beginPath();
348
- ctx.arc(center.x, center.y, radius, 0, TAU);
349
- ctx.fill();
350
-
351
- for (let i = 0; i < 3; i++) {
352
- const ringRadius = radius * (0.42 + i * 0.18) + Math.sin(time * 1.1 + i) * 3;
353
- ctx.beginPath();
354
- ctx.strokeStyle = withAlpha(palette.centerGlow, 0.07 - i * 0.014);
355
- ctx.lineWidth = 1;
356
- ctx.setLineDash([8 + i * 2, 12 + i * 3]);
357
- ctx.lineDashOffset = -time * (18 + i * 8);
358
- ctx.arc(center.x, center.y, ringRadius, 0, TAU);
359
- ctx.stroke();
360
- }
361
- ctx.setLineDash([]);
362
- }
363
-
364
- function drawCrossTeamGuides(
365
- ctx: CanvasRenderingContext2D,
366
- teams: TeamNode[],
367
- time: number,
368
- palette: Palette
369
- ): void {
370
- for (let i = 0; i < teams.length; i++) {
371
- const from = teams[i];
372
- const to = teams[(i + 1) % teams.length];
373
- if (!from || !to) continue;
374
- ctx.beginPath();
375
- ctx.moveTo(from.center.x, from.center.y);
376
- ctx.lineTo(to.center.x, to.center.y);
377
- ctx.strokeStyle = withAlpha(palette.messageAccent, palette.isLight ? 0.14 : 0.18);
378
- ctx.lineWidth = 1.05;
379
- ctx.setLineDash([7, 12]);
380
- ctx.lineDashOffset = -time * 34;
381
- ctx.stroke();
382
- }
383
- ctx.setLineDash([]);
384
- }
385
-
386
- function drawTeamHalo(
387
- ctx: CanvasRenderingContext2D,
388
- team: TeamNode,
389
- time: number,
390
- palette: Palette
391
- ): void {
392
- const pulse = 1 + Math.sin(time * 1.8 + team.index) * 0.035;
393
- const radiusX = team.radius * 1.56 * pulse;
394
- const radiusY = team.radius * 1.14 * pulse;
395
- const glow = ctx.createRadialGradient(
396
- team.center.x,
397
- team.center.y,
398
- team.radius * 0.35,
399
- team.center.x,
400
- team.center.y,
401
- team.radius * 2
402
- );
403
- glow.addColorStop(0, withAlpha(team.color, palette.isLight ? 0.045 : 0.065));
404
- glow.addColorStop(1, withAlpha(team.color, 0));
405
- ctx.fillStyle = glow;
406
- ctx.beginPath();
407
- ctx.ellipse(team.center.x, team.center.y, team.radius * 1.82, team.radius * 1.36, 0, 0, TAU);
408
- ctx.fill();
409
-
410
- ctx.beginPath();
411
- ctx.ellipse(team.center.x, team.center.y, radiusX, radiusY, time * 0.08, 0, TAU);
412
- ctx.strokeStyle = withAlpha(team.color, palette.isLight ? 0.2 : 0.24);
413
- ctx.lineWidth = 1;
414
- ctx.setLineDash([12, 10]);
415
- ctx.lineDashOffset = -time * (22 + team.index * 4);
416
- ctx.stroke();
417
- ctx.setLineDash([]);
418
- }
419
-
420
- function drawMessages(
421
- ctx: CanvasRenderingContext2D,
422
- teams: TeamNode[],
423
- time: number,
424
- palette: Palette,
425
- mobile: boolean
426
- ): void {
427
- for (const team of teams) {
428
- drawLocalMessages(ctx, team, time, palette, mobile);
429
- }
430
- drawCrossTeamMessages(ctx, teams, time, palette, mobile);
431
- }
432
-
433
- function drawLocalMessages(
434
- ctx: CanvasRenderingContext2D,
435
- team: TeamNode,
436
- time: number,
437
- palette: Palette,
438
- mobile: boolean
439
- ): void {
440
- const pairs = getLocalMessagePairs(team.index, team.robots.length);
441
- const activeWindow = 0.76;
442
- const period = 2.15 + team.index * 0.12;
443
-
444
- for (let pairIndex = 0; pairIndex < pairs.length; pairIndex++) {
445
- const [fromIndex, toIndex] = pairs[pairIndex] ?? [0, 1];
446
- const from = team.robots[fromIndex];
447
- const to = team.robots[toIndex];
448
- if (!from || !to) continue;
449
- const raw = positiveModulo(time + team.index * 0.7 + pairIndex * 0.36, period) / period;
450
- applyReceivePulse(to, getReceivePulse(raw, activeWindow));
451
- const flightState = getMessageFlightState(raw, activeWindow, 0.12);
452
- if (!flightState) continue;
453
- const curve = makeLocalCurve(from, to, team.center, team.radius * 0.42);
454
- drawMessageFlight(ctx, curve, flightState, team.color, mobile ? 4.6 : 5.8, palette);
455
- }
456
- }
457
-
458
- function drawCrossTeamMessages(
459
- ctx: CanvasRenderingContext2D,
460
- teams: TeamNode[],
461
- time: number,
462
- palette: Palette,
463
- mobile: boolean
464
- ): void {
465
- const activeWindow = 0.64;
466
- const period = 4.25;
467
- const routes = [
468
- { fromTeam: 0, fromRobot: 3, toTeam: 1, toRobot: 1, delay: 0 },
469
- { fromTeam: 1, fromRobot: 2, toTeam: 2, toRobot: 0, delay: 1.34, accent: true },
470
- { fromTeam: 2, fromRobot: 4, toTeam: 0, toRobot: 1, delay: 2.68 },
471
- ];
472
-
473
- for (const route of routes) {
474
- const fromTeam = teams[route.fromTeam];
475
- const toTeam = teams[route.toTeam];
476
- if (!fromTeam || !toTeam) continue;
477
- const raw = positiveModulo(time + route.delay, period) / period;
478
-
479
- const from = fromTeam.robots[route.fromRobot % fromTeam.robots.length];
480
- const to = toTeam.robots[route.toRobot % toTeam.robots.length];
481
- if (!from || !to) continue;
482
- applyReceivePulse(to, getReceivePulse(raw, activeWindow) * 0.88);
483
- const flightState = getMessageFlightState(raw, activeWindow, 0.1);
484
- if (!flightState) continue;
485
- const curve = makeStraightCurve(from, to);
486
- drawMessageFlight(
487
- ctx,
488
- curve,
489
- flightState,
490
- route.accent ? palette.messageAccent : fromTeam.color,
491
- mobile ? 5.2 : 6.8,
492
- palette,
493
- true
494
- );
495
- }
496
- }
497
-
498
- function drawMessageFlight(
499
- ctx: CanvasRenderingContext2D,
500
- curve: [Point, Point, Point, Point],
501
- state: MessageFlightState,
502
- color: string,
503
- size: number,
504
- palette: Palette,
505
- crossTeam = false
506
- ): void {
507
- const [p0, p1, p2, p3] = curve;
508
- ctx.save();
509
-
510
- const progress = state.progress;
511
- const speed = clamp(state.motionSpeed, 0, 1);
512
- if (speed > 0.045) {
513
- drawSpeedTrail(ctx, curve, progress, speed, color, size, palette, crossTeam);
514
- }
515
-
516
- const position = cubicPoint(p0, p1, p2, p3, progress);
517
- const tangent = cubicTangent(p0, p1, p2, p3, progress);
518
- const angle = Math.atan2(tangent.y, tangent.x);
519
- drawMessageBubble(
520
- ctx,
521
- position,
522
- angle,
523
- size,
524
- color,
525
- palette,
526
- crossTeam,
527
- state.bubbleScale,
528
- state.bubbleAlpha
529
- );
530
- ctx.restore();
531
- }
532
-
533
- function drawSpeedTrail(
534
- ctx: CanvasRenderingContext2D,
535
- curve: [Point, Point, Point, Point],
536
- progress: number,
537
- speed: number,
538
- color: string,
539
- size: number,
540
- palette: Palette,
541
- crossTeam: boolean
542
- ): void {
543
- const [p0, p1, p2, p3] = curve;
544
- const trailLength = (crossTeam ? 0.26 : 0.21) * (0.24 + speed * 1.08);
545
- const segmentCount = Math.round(9 + speed * 10);
546
- const alphaBase = (palette.isLight ? 0.22 : 0.32) * speed;
547
-
548
- ctx.save();
549
- ctx.lineCap = 'round';
550
- ctx.lineJoin = 'round';
551
- ctx.shadowColor = withAlpha(color, alphaBase * 0.58);
552
- ctx.shadowBlur = size * (0.78 + speed * 1.28);
553
-
554
- for (let segment = 0; segment < segmentCount; segment++) {
555
- const startRatio = segment / segmentCount;
556
- const endRatio = (segment + 1) / segmentCount;
557
- const t0 = progress - trailLength * (1 - startRatio);
558
- const t1 = progress - trailLength * (1 - endRatio);
559
- if (t1 <= 0) continue;
560
-
561
- const from = cubicPoint(p0, p1, p2, p3, Math.max(0, t0));
562
- const to = cubicPoint(p0, p1, p2, p3, Math.max(0, t1));
563
- const headWeight = endRatio * endRatio;
564
- const width = size * (0.12 + headWeight * 0.48) * (0.9 + speed * 0.45);
565
- const alpha = alphaBase * headWeight;
566
-
567
- ctx.beginPath();
568
- ctx.moveTo(from.x, from.y);
569
- ctx.lineTo(to.x, to.y);
570
- ctx.strokeStyle = withAlpha(color, alpha * 0.34);
571
- ctx.lineWidth = width * 2.35;
572
- ctx.stroke();
573
-
574
- ctx.beginPath();
575
- ctx.moveTo(from.x, from.y);
576
- ctx.lineTo(to.x, to.y);
577
- ctx.strokeStyle = withAlpha(color, alpha);
578
- ctx.lineWidth = width;
579
- ctx.stroke();
580
- }
581
-
582
- ctx.restore();
583
- }
584
-
585
- function getMessageFlightState(
586
- raw: number,
587
- activeWindow: number,
588
- settleWindow: number
589
- ): MessageFlightState | null {
590
- if (raw > activeWindow + settleWindow) return null;
591
-
592
- if (raw <= activeWindow) {
593
- const phase = raw / activeWindow;
594
- return {
595
- progress: easeInOutCubic(phase),
596
- motionSpeed: getEasedMotionSpeed(phase),
597
- bubbleScale: 1,
598
- bubbleAlpha: 1,
599
- };
600
- }
601
-
602
- const settlePhase = (raw - activeWindow) / settleWindow;
603
- const eased = easeOutCubic(settlePhase);
604
- return {
605
- progress: 1,
606
- motionSpeed: 0,
607
- bubbleScale: Math.max(0.12, 1 - eased * 0.88),
608
- bubbleAlpha: Math.max(0, 1 - eased),
609
- };
610
- }
611
-
612
- function applyReceivePulse(robot: RobotNode, pulse: number): void {
613
- robot.receivePulse = Math.max(robot.receivePulse, pulse);
614
- }
615
-
616
- function getReceivePulse(raw: number, activeWindow: number): number {
617
- const previousStart = activeWindow * 0.78;
618
- const previousEnd = Math.min(0.96, activeWindow + 0.11);
619
- const duration = (previousEnd - previousStart) / 3;
620
- const start = activeWindow - duration * 0.62;
621
- const end = activeWindow + duration * 0.38;
622
- if (raw < start || raw > end) return 0;
623
-
624
- const phase = (raw - start) / (end - start);
625
- return Math.sin(phase * Math.PI) * (1 - phase * 0.28);
626
- }
627
-
628
- function getEasedMotionSpeed(value: number): number {
629
- const t = clamp(value, 0, 1);
630
- const derivative = t < 0.5 ? 12 * t * t : 12 * (1 - t) * (1 - t);
631
- return clamp(derivative / 3, 0, 1);
632
- }
633
-
634
- function easeOutCubic(value: number): number {
635
- const t = clamp(value, 0, 1);
636
- return 1 - Math.pow(1 - t, 3);
637
- }
638
-
639
- function drawMessageBubble(
640
- ctx: CanvasRenderingContext2D,
641
- position: Point,
642
- angle: number,
643
- size: number,
644
- color: string,
645
- palette: Palette,
646
- crossTeam: boolean,
647
- scale = 1,
648
- alpha = 1
649
- ): void {
650
- if (scale <= 0.02 || alpha <= 0.01) return;
651
-
652
- ctx.save();
653
- ctx.translate(position.x, position.y);
654
- ctx.rotate(angle * 0.08);
655
- ctx.scale(scale, scale);
656
- ctx.globalAlpha = alpha;
657
- ctx.shadowColor = withAlpha(color, (palette.isLight ? 0.16 : 0.3) * alpha);
658
- ctx.shadowBlur = (crossTeam ? 12 : 8) * (0.5 + scale * 0.5);
659
-
660
- const width = size * (crossTeam ? 2.28 : 2.06);
661
- const height = size * 1.42;
662
- roundRectPath(ctx, -width / 2, -height / 2, width, height, size * 0.28);
663
- ctx.fillStyle = withAlpha(color, palette.isLight ? 0.82 : 0.9);
664
- ctx.fill();
665
-
666
- ctx.beginPath();
667
- ctx.moveTo(-width * 0.24, height * 0.42);
668
- ctx.lineTo(-width * 0.32, height * 0.68);
669
- ctx.lineTo(-width * 0.03, height * 0.42);
670
- ctx.closePath();
671
- ctx.fill();
672
-
673
- ctx.shadowBlur = 0;
674
- ctx.fillStyle = palette.robotEye;
675
- for (let i = -1; i <= 1; i++) {
676
- ctx.beginPath();
677
- ctx.arc(i * size * 0.4, -size * 0.02, size * 0.095, 0, TAU);
678
- ctx.fill();
679
- }
680
- ctx.restore();
681
- }
682
-
683
- function drawTeamLabel(
684
- ctx: CanvasRenderingContext2D,
685
- team: TeamNode,
686
- palette: Palette,
687
- mobile: boolean
688
- ): void {
689
- const label = TEAM_LABELS[team.index] ?? '';
690
- if (!label) return;
691
-
692
- const fontSize = mobile ? 7.5 : 8.5;
693
- const y = team.center.y + team.radius * (mobile ? 1.65 : 1.58);
694
- ctx.save();
695
- ctx.font = `600 ${fontSize}px ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif`;
696
- ctx.textAlign = 'center';
697
- ctx.textBaseline = 'middle';
698
-
699
- const metrics = ctx.measureText(label);
700
- const paddingX = mobile ? 4 : 5;
701
- const paddingY = mobile ? 2 : 2.5;
702
- const width = metrics.width + paddingX * 2;
703
- const height = fontSize + paddingY * 2;
704
- const x = team.center.x - width / 2;
705
- const rectY = y - height / 2;
706
-
707
- roundRectPath(ctx, x, rectY, width, height, height / 2);
708
- ctx.fillStyle = withAlpha(palette.isLight ? '#ffffff' : '#090a14', palette.isLight ? 0.36 : 0.24);
709
- ctx.fill();
710
- ctx.strokeStyle = withAlpha(team.color, palette.isLight ? 0.18 : 0.24);
711
- ctx.lineWidth = 0.75;
712
- ctx.stroke();
713
-
714
- ctx.shadowColor = withAlpha(team.color, palette.isLight ? 0.12 : 0.22);
715
- ctx.shadowBlur = mobile ? 4 : 6;
716
- ctx.fillStyle = withAlpha(palette.isLight ? '#3f3f46' : '#e4e4e7', palette.isLight ? 0.58 : 0.66);
717
- ctx.fillText(label, team.center.x, y + 0.2);
718
- ctx.restore();
719
- }
720
-
721
- function drawRobot(
722
- ctx: CanvasRenderingContext2D,
723
- robot: RobotNode,
724
- time: number,
725
- palette: Palette
726
- ): void {
727
- const size = robot.size;
728
- const x = robot.x;
729
- const y = robot.y + robot.bob * 0.9 - robot.receivePulse * size * 0.24;
730
- const tilt = Math.sin(time * 1.5 + robot.teamIndex + robot.robotIndex * 0.8) * 0.045;
731
- const img = getAvatarImage(robot.avatarUrl);
732
- const avatarSize = size * 2.65;
733
-
734
- ctx.save();
735
- ctx.translate(x, y);
736
- ctx.rotate(tilt);
737
- ctx.scale(1 + robot.receivePulse * 0.065, 1 + robot.receivePulse * 0.065);
738
- ctx.shadowColor = withAlpha(robot.color, palette.isLight ? 0.2 : 0.34);
739
- ctx.shadowBlur = size * (1.25 + robot.receivePulse * 0.72);
740
-
741
- if (img) {
742
- ctx.globalAlpha = palette.isLight ? 0.92 : 0.86;
743
- ctx.drawImage(img, -avatarSize / 2, -avatarSize / 2, avatarSize, avatarSize);
744
- ctx.globalAlpha = 1;
745
- } else {
746
- drawAvatarFallback(ctx, size, robot.color, palette);
747
- }
748
- ctx.restore();
749
- }
750
-
751
- function getAvatarImage(url: string): HTMLImageElement | null {
752
- const cached = avatarCache.get(url);
753
- if (cached) {
754
- avatarCache.delete(url);
755
- avatarCache.set(url, cached);
756
- return cached;
757
- }
758
-
759
- void loadAvatarImage(url);
760
- return null;
761
- }
762
-
763
- function preloadAvatarImages(): Promise<void> {
764
- return Promise.allSettled(PARTICIPANT_AVATAR_URLS.map((url) => loadAvatarImage(url))).then(
765
- () => undefined
766
- );
767
- }
768
-
769
- function loadAvatarImage(url: string): Promise<HTMLImageElement | null> {
770
- const cached = avatarCache.get(url);
771
- if (cached) return Promise.resolve(cached);
772
-
773
- const loading = avatarLoading.get(url);
774
- if (loading) return loading;
775
-
776
- const promise = new Promise<HTMLImageElement | null>((resolve) => {
777
- const img = new Image();
778
- img.decoding = 'async';
779
- img.onload = () => {
780
- const finish = (): void => {
781
- avatarCache.set(url, img);
782
- avatarLoading.delete(url);
783
- resolve(img);
784
- };
785
-
786
- if (typeof img.decode === 'function') {
787
- void img.decode().then(finish, finish);
788
- } else {
789
- finish();
790
- }
791
- };
792
- img.onerror = () => {
793
- avatarLoading.delete(url);
794
- resolve(null);
795
- };
796
- img.src = url;
797
- });
798
-
799
- avatarLoading.set(url, promise);
800
- return promise;
801
- }
802
-
803
- function drawAvatarFallback(
804
- ctx: CanvasRenderingContext2D,
805
- size: number,
806
- color: string,
807
- palette: Palette
808
- ): void {
809
- ctx.strokeStyle = withAlpha(color, palette.isLight ? 0.44 : 0.56);
810
- ctx.lineWidth = Math.max(1, size * 0.08);
811
- ctx.beginPath();
812
- ctx.moveTo(0, -size * 0.72);
813
- ctx.lineTo(0, -size * 1.0);
814
- ctx.stroke();
815
- ctx.fillStyle = withAlpha(color, palette.isLight ? 0.64 : 0.78);
816
- ctx.beginPath();
817
- ctx.arc(0, -size * 1.08, size * 0.13, 0, TAU);
818
- ctx.fill();
819
- ctx.fillStyle = palette.robotEye;
820
- ctx.beginPath();
821
- ctx.arc(-size * 0.24, -size * 0.13, size * 0.095, 0, TAU);
822
- ctx.arc(size * 0.24, -size * 0.13, size * 0.095, 0, TAU);
823
- ctx.fill();
824
- }
825
-
826
- function getLocalMessagePairs(teamIndex: number, memberCount: number): [number, number][] {
827
- const routeMap: [number, number][][] = [
828
- [
829
- [0, 2],
830
- [3, 1],
831
- [1, 0],
832
- ],
833
- [
834
- [2, 0],
835
- [0, 1],
836
- [1, 2],
837
- ],
838
- [
839
- [4, 1],
840
- [0, 3],
841
- [2, 4],
842
- [3, 0],
843
- ],
844
- ];
845
- return (routeMap[teamIndex] ?? routeMap[0]).filter(
846
- ([fromIndex, toIndex]) => fromIndex < memberCount && toIndex < memberCount
847
- );
848
- }
849
-
850
- function makeLocalCurve(
851
- from: Point,
852
- to: Point,
853
- center: Point,
854
- lift: number
855
- ): [Point, Point, Point, Point] {
856
- const mid = mix(from, to, 0.5);
857
- const away = normalize({ x: mid.x - center.x, y: mid.y - center.y });
858
- const control = {
859
- x: mid.x + away.x * lift,
860
- y: mid.y + away.y * lift,
861
- };
862
- return [from, mix(from, control, 0.72), mix(to, control, 0.72), to];
863
- }
864
-
865
- function makeStraightCurve(from: Point, to: Point): [Point, Point, Point, Point] {
866
- return [from, mix(from, to, 0.33), mix(from, to, 0.66), to];
867
- }
868
-
869
- function createDepthParticles(width: number, height: number): DepthParticle[] {
870
- const count = width < 560 ? 46 : 78;
871
- return Array.from({ length: count }, (_, index) => {
872
- const seed = index * 97.13;
873
- return {
874
- x: pseudoRandom(seed) * width,
875
- y: pseudoRandom(seed + 12.4) * (height + 24),
876
- size: 0.45 + pseudoRandom(seed + 22.8) * 1.15,
877
- speed: 8 + pseudoRandom(seed + 31.2) * 18,
878
- phase: pseudoRandom(seed + 48.7) * TAU,
879
- alpha: 0.06 + pseudoRandom(seed + 72.1) * 0.16,
880
- };
881
- });
882
- }
883
-
884
- function pseudoRandom(seed: number): number {
885
- const value = Math.sin(seed * 12.9898) * 43758.5453;
886
- return value - Math.floor(value);
887
- }
888
-
889
- function cubicPoint(p0: Point, p1: Point, p2: Point, p3: Point, t: number): Point {
890
- const clamped = clamp(t, 0, 1);
891
- const mt = 1 - clamped;
892
- const mt2 = mt * mt;
893
- const t2 = clamped * clamped;
894
- return {
895
- x: mt2 * mt * p0.x + 3 * mt2 * clamped * p1.x + 3 * mt * t2 * p2.x + t2 * clamped * p3.x,
896
- y: mt2 * mt * p0.y + 3 * mt2 * clamped * p1.y + 3 * mt * t2 * p2.y + t2 * clamped * p3.y,
897
- };
898
- }
899
-
900
- function cubicTangent(p0: Point, p1: Point, p2: Point, p3: Point, t: number): Point {
901
- const clamped = clamp(t, 0, 1);
902
- const mt = 1 - clamped;
903
- return {
904
- x:
905
- 3 * mt * mt * (p1.x - p0.x) +
906
- 6 * mt * clamped * (p2.x - p1.x) +
907
- 3 * clamped * clamped * (p3.x - p2.x),
908
- y:
909
- 3 * mt * mt * (p1.y - p0.y) +
910
- 6 * mt * clamped * (p2.y - p1.y) +
911
- 3 * clamped * clamped * (p3.y - p2.y),
912
- };
913
- }
914
-
915
- function mix(from: Point, to: Point, amount: number): Point {
916
- return {
917
- x: from.x + (to.x - from.x) * amount,
918
- y: from.y + (to.y - from.y) * amount,
919
- };
920
- }
921
-
922
- function normalize(point: Point): Point {
923
- const length = Math.hypot(point.x, point.y) || 1;
924
- return {
925
- x: point.x / length,
926
- y: point.y / length,
927
- };
928
- }
929
-
930
- function easeInOutCubic(value: number): number {
931
- const t = clamp(value, 0, 1);
932
- return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
933
- }
934
-
935
- function positiveModulo(value: number, divisor: number): number {
936
- return ((value % divisor) + divisor) % divisor;
937
- }
938
-
939
- function clamp(value: number, min: number, max: number): number {
940
- return Math.min(max, Math.max(min, value));
941
- }
942
-
943
- function roundRectPath(
944
- ctx: CanvasRenderingContext2D,
945
- x: number,
946
- y: number,
947
- width: number,
948
- height: number,
949
- radius: number
950
- ): void {
951
- const r = Math.min(radius, width / 2, height / 2);
952
- ctx.beginPath();
953
- ctx.moveTo(x + r, y);
954
- ctx.lineTo(x + width - r, y);
955
- ctx.quadraticCurveTo(x + width, y, x + width, y + r);
956
- ctx.lineTo(x + width, y + height - r);
957
- ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
958
- ctx.lineTo(x + r, y + height);
959
- ctx.quadraticCurveTo(x, y + height, x, y + height - r);
960
- ctx.lineTo(x, y + r);
961
- ctx.quadraticCurveTo(x, y, x + r, y);
962
- ctx.closePath();
963
- }
964
-
965
- function withAlpha(hex: string, alpha: number): string {
966
- const normalized = normalizeHex(hex);
967
- const r = Number.parseInt(normalized.slice(1, 3), 16);
968
- const g = Number.parseInt(normalized.slice(3, 5), 16);
969
- const b = Number.parseInt(normalized.slice(5, 7), 16);
970
- return `rgba(${r}, ${g}, ${b}, ${clamp(alpha, 0, 1)})`;
971
- }
972
-
973
- function normalizeHex(hex: string): string {
974
- if (/^#[0-9a-fA-F]{6}$/.test(hex)) return hex;
975
- if (/^#[0-9a-fA-F]{3}$/.test(hex)) {
976
- return `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}`;
977
- }
978
- return '#ffffff';
979
- }