@ciphore/radiocli 0.2.2 → 0.2.4
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.
- package/CHANGELOG.md +124 -1
- package/CONTRIBUTING.md +36 -6
- package/README.md +84 -10
- package/dist/agent/alarm-service.js +210 -0
- package/dist/agent/cli.js +193 -0
- package/dist/agent/headless-host.js +165 -0
- package/dist/agent/launcher.js +10 -0
- package/dist/agent/mcp-install.js +467 -0
- package/dist/agent/mcp-server.js +139 -0
- package/dist/agent/service.js +347 -0
- package/dist/agent/session.js +232 -0
- package/dist/alarms/active-session.js +9 -12
- package/dist/alarms/cli.js +4 -1
- package/dist/alarms/guard.js +79 -36
- package/dist/alarms/inhibitor.js +27 -22
- package/dist/alarms/power-guard-store.js +2 -10
- package/dist/alarms/runner.js +96 -48
- package/dist/alarms/schedule.js +9 -2
- package/dist/alarms/scheduler.js +194 -52
- package/dist/alarms/setup-verification.js +1 -2
- package/dist/alarms/system-volume-ownership.js +267 -0
- package/dist/alarms/system-volume.js +155 -14
- package/dist/alarms/terminal-launcher.js +76 -136
- package/dist/alarms/tui-presence.js +2 -4
- package/dist/cli.js +157 -35
- package/dist/platform/capabilities.js +53 -0
- package/dist/platform/desktop.js +36 -0
- package/dist/{player/command.js → platform/executables.js} +43 -9
- package/dist/platform/ipc.js +14 -0
- package/dist/platform/launch-command.js +135 -0
- package/dist/platform/loopback.js +44 -0
- package/dist/platform/network.js +312 -0
- package/dist/platform/packages.js +216 -0
- package/dist/platform/paths.js +40 -0
- package/dist/platform/runtime.js +67 -0
- package/dist/platform/shell.js +24 -0
- package/dist/platform/storage.js +48 -0
- package/dist/platform/support.js +214 -0
- package/dist/platform/terminal.js +63 -0
- package/dist/platform/terminals.js +203 -0
- package/dist/player/airplay-discovery.js +4 -2
- package/dist/player/backend-install.js +13 -94
- package/dist/player/command-diagnostics.js +2 -2
- package/dist/player/mpv-ipc-client.js +2 -1
- package/dist/player/player-controller.js +220 -31
- package/dist/providers/cache.js +4 -26
- package/dist/providers/provider-manager.js +5 -0
- package/dist/providers/radio-browser.js +156 -54
- package/dist/providers/radio-garden.js +15 -22
- package/dist/setup.js +149 -152
- package/dist/storage/store.js +138 -53
- package/dist/streams/import-stream.js +163 -0
- package/dist/types.js +6 -0
- package/dist/ui/AdaptiveContent.js +54 -30
- package/dist/ui/App.js +466 -98
- package/dist/ui/AppContent.js +4 -4
- package/dist/ui/app-state.js +8 -1
- package/dist/ui/ascii.js +11 -2
- package/dist/ui/components/AdaptiveMarquee.js +6 -3
- package/dist/ui/components/Logo.js +5 -2
- package/dist/ui/components/Menu.js +2 -2
- package/dist/ui/components/ScreenHeader.js +1 -1
- package/dist/ui/components/StationList.js +8 -8
- package/dist/ui/components/TopTabs.js +1 -1
- package/dist/ui/components/VersionIndicator.js +19 -0
- package/dist/ui/display-context.js +8 -11
- package/dist/ui/help-content.js +5 -5
- package/dist/ui/layout.js +5 -2
- package/dist/ui/page-footer.js +5 -3
- package/dist/ui/screen-items.js +42 -11
- package/dist/ui/screen-meta.js +1 -1
- package/dist/ui/screens/AirPlayCodeScreen.js +6 -2
- package/dist/ui/screens/AirPlaySettingsScreen.js +7 -3
- package/dist/ui/screens/AlarmsScreen.js +2 -1
- package/dist/ui/screens/CountriesScreen.js +8 -5
- package/dist/ui/screens/ExploreScreen.js +2 -1
- package/dist/ui/screens/HelpScreen.js +5 -1
- package/dist/ui/screens/HomeScreen.js +7 -1
- package/dist/ui/screens/MapScreen.js +1 -1
- package/dist/ui/screens/NowPlayingScreen.js +4 -4
- package/dist/ui/screens/SearchScreen.js +3 -1
- package/dist/ui/screens/SettingsScreen.js +80 -56
- package/dist/ui/screens/StatsScreen.js +3 -1
- package/dist/ui/system-actions.js +80 -52
- package/dist/ui/terminal-renderer.js +16 -0
- package/dist/ui/use-alarm-tui.js +40 -21
- package/dist/ui/use-app-input.js +69 -23
- package/dist/ui/use-command-executor.js +31 -7
- package/dist/ui/visualizers/gallop.js +118 -0
- package/dist/ui/visualizers/horse-stride.js +20 -0
- package/dist/ui/visualizers/receiver-style-registry.js +12 -2
- package/dist/ui/visualizers/receiver-visualizers.js +3 -0
- package/dist/ui/visualizers/retro-receivers.js +4 -0
- package/dist/ui/visualizers/terminal-receivers.js +57 -0
- package/dist/update-check.js +33 -23
- package/package.json +4 -1
package/dist/ui/use-app-input.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import { useEffect } from 'react';
|
|
1
|
+
import { useCallback, useEffect } from 'react';
|
|
2
2
|
import { useInput } from 'ink';
|
|
3
|
-
import { homeItems,
|
|
4
|
-
import { applyTextInput, favoriteTarget, isEditableInput, isPlainPrintableInput, mediaTransportActionForInput, searchEditingArrowAction, shouldHandleKeyboardEvent, shouldToggleNearbyLocationShortcut } from './app-state.js';
|
|
3
|
+
import { homeItems, settingsItemsForPage, settingsPageForRootItem } from './screen-items.js';
|
|
4
|
+
import { applyTextInput, favoriteTarget, isEditableInput, isPlainPrintableInput, isSearchFavoriteShortcut, mediaTransportActionForInput, searchEditingArrowAction, shouldHandleKeyboardEvent, shouldTuneSearchResult, shouldToggleNearbyLocationShortcut } from './app-state.js';
|
|
5
5
|
import { parseTerminalMouseEvents, primaryMousePress, wheelScrollDelta } from './terminal-mouse.js';
|
|
6
6
|
import { completeCommand } from './help-content.js';
|
|
7
7
|
import { commitImmediateSelection } from './selection-state.js';
|
|
8
|
-
|
|
8
|
+
import { sanitizeTerminalText } from '../safety.js';
|
|
9
|
+
export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, cancelPendingAutoSkip, beginLearningTransportKey, capturingTransportAction, commandMode, commandText, confirmCtrlCExit, copyStationUrl, openStationHomepage, currentItemCount, cycleDisplayColor, cycleAudioOutput, cycleReceiverStyle, cycleSleepTimer, editingCountryFilter, editingSearch, executeCommand, filteredCountries, go, lastRawTransportAtRef, lastSubmittedSearchRef, loadCountry, openAdjacentTab, openAirPlayCode, openAirPlaySettings, openSettingsPage, closeSettingsPage, openScreen, handleAlarmInput, openAlarmForStation, openActiveAlarms, playAdjacent, playStation, player, playingStation, recallSearchHistory, moveExploreCursor, moveExploreCursorToCell, refreshAirPlayTargets, refreshProviderHealth, resetLearnedTransportKeys, repairMcpIntegrations, setAgentIntegrationEnabled, runSearch, saveLearnedTransportKey, screen, settingsPage, searchQuery, selectedRef, selectedStationForInput, selectAirPlayDeviceAt, setCapturingTransportAction, setAirPlayCode, setCommandMode, setCommandText, setCountryFilter, setEditingCountryFilter, setEditingSearch, setMessage, setSearchQuery, setSelected, setShowDiagnostics, submitAirPlayCode, settingsRef, shutdown, stdin, toggleFavorite, toggleMute, togglePause, toggleSetting, toggleAgentSetting, toggleNearbyLocation, toggleDirectoryVoting, toggleRadioGarden, toggleSkipBrokenStreams, updateFromSettings }) {
|
|
10
|
+
const reportInputError = useCallback((error) => {
|
|
11
|
+
setMessage(sanitizeTerminalText(error instanceof Error ? error.message : String(error)) ?? 'Could not complete this action.');
|
|
12
|
+
}, [setMessage]);
|
|
9
13
|
const commitSelection = (next) => {
|
|
10
14
|
commitImmediateSelection(selectedRef, setSelected, next, currentItemCount(screen));
|
|
11
15
|
};
|
|
@@ -15,7 +19,7 @@ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, ca
|
|
|
15
19
|
commitSelection(selectedRef.current + delta);
|
|
16
20
|
};
|
|
17
21
|
useEffect(() => {
|
|
18
|
-
const
|
|
22
|
+
const handleData = (data) => {
|
|
19
23
|
const rawInput = String(data);
|
|
20
24
|
if (capturingTransportAction) {
|
|
21
25
|
if (rawInput === '\u001B') {
|
|
@@ -63,6 +67,14 @@ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, ca
|
|
|
63
67
|
togglePause();
|
|
64
68
|
}
|
|
65
69
|
};
|
|
70
|
+
const onData = (data) => {
|
|
71
|
+
try {
|
|
72
|
+
handleData(data);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
reportInputError(error);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
66
78
|
stdin.on('data', onData);
|
|
67
79
|
return () => {
|
|
68
80
|
stdin.off('data', onData);
|
|
@@ -78,6 +90,7 @@ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, ca
|
|
|
78
90
|
lastRawTransportAtRef,
|
|
79
91
|
playAdjacent,
|
|
80
92
|
player,
|
|
93
|
+
reportInputError,
|
|
81
94
|
moveExploreCursorToCell,
|
|
82
95
|
saveLearnedTransportKey,
|
|
83
96
|
screen,
|
|
@@ -89,7 +102,7 @@ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, ca
|
|
|
89
102
|
stdin,
|
|
90
103
|
togglePause
|
|
91
104
|
]);
|
|
92
|
-
|
|
105
|
+
const handleInput = async (input, key) => {
|
|
93
106
|
if (key.ctrl && input === 'c') {
|
|
94
107
|
confirmCtrlCExit();
|
|
95
108
|
return;
|
|
@@ -105,9 +118,9 @@ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, ca
|
|
|
105
118
|
}
|
|
106
119
|
if (commandMode) {
|
|
107
120
|
if (key.return) {
|
|
108
|
-
void executeCommand(commandText);
|
|
109
121
|
setCommandText('');
|
|
110
122
|
setCommandMode(false);
|
|
123
|
+
await executeCommand(commandText);
|
|
111
124
|
return;
|
|
112
125
|
}
|
|
113
126
|
if (key.escape) {
|
|
@@ -128,17 +141,21 @@ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, ca
|
|
|
128
141
|
if (handleAlarmInput(input, key)) {
|
|
129
142
|
return;
|
|
130
143
|
}
|
|
131
|
-
// An active text field owns
|
|
132
|
-
//
|
|
133
|
-
//
|
|
144
|
+
// An active text field owns ordinary keystrokes before app-wide shortcuts.
|
|
145
|
+
// Dedicated modified shortcuts are handled here so plain query characters
|
|
146
|
+
// never trigger actions or leave Search.
|
|
134
147
|
if (screen === 'search' && editingSearch) {
|
|
148
|
+
if (isSearchFavoriteShortcut(input, key)) {
|
|
149
|
+
toggleFavorite(favoriteTarget(screen, selectedStationForInput(), playingStation));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
135
152
|
if (key.return) {
|
|
136
153
|
const inputStation = selectedStationForInput();
|
|
137
|
-
if (
|
|
138
|
-
|
|
154
|
+
if (inputStation && shouldTuneSearchResult(searchQuery, lastSubmittedSearchRef.current)) {
|
|
155
|
+
await playStation(inputStation);
|
|
139
156
|
}
|
|
140
157
|
else {
|
|
141
|
-
|
|
158
|
+
await runSearch();
|
|
142
159
|
}
|
|
143
160
|
return;
|
|
144
161
|
}
|
|
@@ -240,7 +257,7 @@ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, ca
|
|
|
240
257
|
if (input.startsWith(':')) {
|
|
241
258
|
const seed = input.slice(1).replace(/[\r\n]+$/g, '');
|
|
242
259
|
if (/[\r\n]/.test(input)) {
|
|
243
|
-
|
|
260
|
+
await executeCommand(seed);
|
|
244
261
|
}
|
|
245
262
|
else {
|
|
246
263
|
setCommandMode(true);
|
|
@@ -338,6 +355,10 @@ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, ca
|
|
|
338
355
|
go('settings');
|
|
339
356
|
return;
|
|
340
357
|
}
|
|
358
|
+
if ((input === 'b' || key.escape) && screen === 'settings') {
|
|
359
|
+
closeSettingsPage();
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
341
362
|
if (input === 'b' || key.escape) {
|
|
342
363
|
if (screen === 'stations') {
|
|
343
364
|
go('countries');
|
|
@@ -368,11 +389,11 @@ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, ca
|
|
|
368
389
|
return;
|
|
369
390
|
}
|
|
370
391
|
if (input === 'O') {
|
|
371
|
-
openStationHomepage(favoriteTarget(screen, selectedStationForInput(), playingStation));
|
|
392
|
+
await openStationHomepage(favoriteTarget(screen, selectedStationForInput(), playingStation));
|
|
372
393
|
return;
|
|
373
394
|
}
|
|
374
395
|
if (input === 'y') {
|
|
375
|
-
|
|
396
|
+
await copyStationUrl(favoriteTarget(screen, selectedStationForInput(), playingStation));
|
|
376
397
|
return;
|
|
377
398
|
}
|
|
378
399
|
if (input === ' ') {
|
|
@@ -414,12 +435,19 @@ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, ca
|
|
|
414
435
|
if (screen === 'countries') {
|
|
415
436
|
const country = filteredCountries[selectedRef.current];
|
|
416
437
|
if (country) {
|
|
417
|
-
|
|
438
|
+
await loadCountry(country);
|
|
418
439
|
}
|
|
419
440
|
return;
|
|
420
441
|
}
|
|
421
442
|
if (screen === 'settings') {
|
|
422
|
-
const item =
|
|
443
|
+
const item = settingsItemsForPage(settingsPage)[selectedRef.current];
|
|
444
|
+
if (settingsPage === 'root') {
|
|
445
|
+
const targetPage = settingsPageForRootItem(item);
|
|
446
|
+
if (targetPage) {
|
|
447
|
+
openSettingsPage(targetPage);
|
|
448
|
+
}
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
423
451
|
if (item === 'Cycle display color') {
|
|
424
452
|
cycleDisplayColor();
|
|
425
453
|
}
|
|
@@ -468,16 +496,31 @@ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, ca
|
|
|
468
496
|
else if (item === 'Mouse and trackpad scrolling') {
|
|
469
497
|
toggleSetting('mouseSupport');
|
|
470
498
|
}
|
|
499
|
+
else if (item === 'Automatically check for updates') {
|
|
500
|
+
toggleSetting('automaticUpdateChecks');
|
|
501
|
+
}
|
|
502
|
+
else if (item === 'Allow local agent control') {
|
|
503
|
+
await setAgentIntegrationEnabled();
|
|
504
|
+
}
|
|
505
|
+
else if (item === 'Install or repair MCP integrations') {
|
|
506
|
+
await repairMcpIntegrations();
|
|
507
|
+
}
|
|
508
|
+
else if (item === 'Open TUI for agent playback') {
|
|
509
|
+
toggleAgentSetting('openUiOnPlay');
|
|
510
|
+
}
|
|
511
|
+
else if (item === 'Show Now Playing for agent playback') {
|
|
512
|
+
toggleAgentSetting('focusNowPlaying');
|
|
513
|
+
}
|
|
471
514
|
else if (item === 'Export preferences and library') {
|
|
472
515
|
setCommandText('export ');
|
|
473
516
|
setCommandMode(true);
|
|
474
517
|
}
|
|
475
|
-
else if (item === '
|
|
476
|
-
setCommandText('
|
|
518
|
+
else if (item === 'Restore preferences and library') {
|
|
519
|
+
setCommandText('restore ');
|
|
477
520
|
setCommandMode(true);
|
|
478
521
|
}
|
|
479
522
|
else if (item === 'Check for updates') {
|
|
480
|
-
|
|
523
|
+
await updateFromSettings();
|
|
481
524
|
}
|
|
482
525
|
else if (item === 'Refresh provider health') {
|
|
483
526
|
refreshProviderHealth();
|
|
@@ -504,15 +547,18 @@ export function useAppInput({ adjustVolume, airPlayCode, canEnterAirPlayCode, ca
|
|
|
504
547
|
if (screen === 'map') {
|
|
505
548
|
const country = filteredCountries[selectedRef.current];
|
|
506
549
|
if (country) {
|
|
507
|
-
|
|
550
|
+
await loadCountry(country);
|
|
508
551
|
}
|
|
509
552
|
return;
|
|
510
553
|
}
|
|
511
554
|
const inputStation = selectedStationForInput();
|
|
512
555
|
if (inputStation) {
|
|
513
|
-
|
|
556
|
+
await playStation(inputStation);
|
|
514
557
|
}
|
|
515
558
|
}
|
|
559
|
+
};
|
|
560
|
+
useInput((input, key) => {
|
|
561
|
+
void handleInput(input, key).catch(reportInputError);
|
|
516
562
|
});
|
|
517
563
|
}
|
|
518
564
|
function shouldScrollSelectionWithWheel(screen, commandMode, editingCountryFilter) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback } from 'react';
|
|
2
2
|
import { favoriteTarget, normalizeMediaKeyBindings, parseMediaActionName } from './app-state.js';
|
|
3
|
-
|
|
3
|
+
import { importStreamUrl } from '../streams/import-stream.js';
|
|
4
|
+
export function useCommandExecutor({ beginLearningTransportKey, countries, go, loadCountry, openAirPlaySettings, openLibrary, persistLibrary, player, playingStation, providers, resetLearnedTransportKeys, runSearch, screen, selectedStation, setCountries, setFilters, setLibrary, setMessage, setSearchQuery, setSleepUntil, setVolume, settingsRef, store, toggleFavorite, toggleMute, updateCommand, updateSettings }) {
|
|
4
5
|
return useCallback(async (rawCommand) => {
|
|
5
6
|
const trimmed = rawCommand.trim();
|
|
6
7
|
if (!trimmed) {
|
|
@@ -146,9 +147,31 @@ export function useCommandExecutor({ beginLearningTransportKey, countries, go, l
|
|
|
146
147
|
}
|
|
147
148
|
return;
|
|
148
149
|
}
|
|
149
|
-
if (name === 'import'
|
|
150
|
+
if (name === 'import') {
|
|
150
151
|
if (!value.trim()) {
|
|
151
|
-
setMessage('Usage: :import <
|
|
152
|
+
setMessage('Usage: :import <stream-url> [station name]');
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const [url = '', ...nameParts] = rest;
|
|
157
|
+
setMessage('Reading stream metadata...');
|
|
158
|
+
const result = await importStreamUrl(url, nameParts.join(' ') || undefined);
|
|
159
|
+
const nextLibrary = persistLibrary(() => store.addImported([result.station]));
|
|
160
|
+
if (!nextLibrary)
|
|
161
|
+
return;
|
|
162
|
+
openLibrary();
|
|
163
|
+
setMessage(result.warning
|
|
164
|
+
? `Imported ${result.station.name}. ${result.warning}`
|
|
165
|
+
: `Imported ${result.station.name}${result.station.codec ? ` · ${result.station.codec}` : ''}${result.station.bitrate ? ` · ${result.station.bitrate} kbps` : ''}.`);
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
setMessage(error instanceof Error ? error.message : 'Could not import the radio stream.');
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (name === 'restore') {
|
|
173
|
+
if (!value.trim()) {
|
|
174
|
+
setMessage('Usage: :restore <backup.json>');
|
|
152
175
|
return;
|
|
153
176
|
}
|
|
154
177
|
try {
|
|
@@ -156,11 +179,11 @@ export function useCommandExecutor({ beginLearningTransportKey, countries, go, l
|
|
|
156
179
|
settingsRef.current = result.state.settings;
|
|
157
180
|
setLibrary(result.state);
|
|
158
181
|
setMessage(result.safetyBackupPath
|
|
159
|
-
? `Backup
|
|
160
|
-
: `Backup
|
|
182
|
+
? `Backup restored. Previous library saved to ${result.safetyBackupPath}`
|
|
183
|
+
: `Backup restored: ${result.sourcePath}`);
|
|
161
184
|
}
|
|
162
185
|
catch (error) {
|
|
163
|
-
setMessage(error instanceof Error ? error.message : 'Could not
|
|
186
|
+
setMessage(error instanceof Error ? error.message : 'Could not restore the RadioCLI backup.');
|
|
164
187
|
}
|
|
165
188
|
return;
|
|
166
189
|
}
|
|
@@ -169,7 +192,7 @@ export function useCommandExecutor({ beginLearningTransportKey, countries, go, l
|
|
|
169
192
|
return;
|
|
170
193
|
}
|
|
171
194
|
if (name === 'stop') {
|
|
172
|
-
|
|
195
|
+
persistLibrary(() => store.finishActiveListeningSession());
|
|
173
196
|
await player.stop();
|
|
174
197
|
return;
|
|
175
198
|
}
|
|
@@ -203,6 +226,7 @@ export function useCommandExecutor({ beginLearningTransportKey, countries, go, l
|
|
|
203
226
|
loadCountry,
|
|
204
227
|
openAirPlaySettings,
|
|
205
228
|
openLibrary,
|
|
229
|
+
persistLibrary,
|
|
206
230
|
player,
|
|
207
231
|
playingStation,
|
|
208
232
|
providers,
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { themeAccent } from '../theme.js';
|
|
2
|
+
import { horseStrideFrames, horseStrideHeight, horseStrideWidth } from './horse-stride.js';
|
|
3
|
+
const frames = horseStrideFrames.map(frame => Buffer.from(frame, 'base64'));
|
|
4
|
+
const brailleBits = [[1, 8], [2, 16], [4, 32], [64, 128]];
|
|
5
|
+
/** One pose per receiver tick. The same scale and torso anchor are used for every pose. */
|
|
6
|
+
export function buildGallop(pulse, width, height, theme) {
|
|
7
|
+
const w = Math.max(1, Math.floor(width));
|
|
8
|
+
const h = Math.max(1, Math.floor(height));
|
|
9
|
+
const color = themeAccent(theme);
|
|
10
|
+
const frameIndex = ((Math.floor(pulse) % frames.length) + frames.length) % frames.length;
|
|
11
|
+
const frame = frames[frameIndex];
|
|
12
|
+
const usableW = w > 8 ? w - 4 : w;
|
|
13
|
+
const usableH = h > 4 ? h - 2 : h;
|
|
14
|
+
const scale = Math.min(usableW * 2 / horseStrideWidth, usableH * 4 / horseStrideHeight);
|
|
15
|
+
const dotsW = Math.max(1, Math.floor(horseStrideWidth * scale));
|
|
16
|
+
const dotsH = Math.max(1, Math.floor(horseStrideHeight * scale));
|
|
17
|
+
const left = Math.floor((w - Math.ceil(dotsW / 2)) / 2) * 2;
|
|
18
|
+
const top = Math.floor((h - Math.ceil(dotsH / 4)) / 2) * 4;
|
|
19
|
+
const masks = new Uint8Array(w * h);
|
|
20
|
+
for (let y = 0; y < dotsH; y++) {
|
|
21
|
+
const sy0 = Math.floor(y * horseStrideHeight / dotsH);
|
|
22
|
+
const sy1 = Math.ceil((y + 1) * horseStrideHeight / dotsH);
|
|
23
|
+
for (let x = 0; x < dotsW; x++) {
|
|
24
|
+
const sx0 = Math.floor(x * horseStrideWidth / dotsW);
|
|
25
|
+
const sx1 = Math.ceil((x + 1) * horseStrideWidth / dotsW);
|
|
26
|
+
let ink = 0;
|
|
27
|
+
for (let sy = sy0; sy < sy1; sy++)
|
|
28
|
+
for (let sx = sx0; sx < sx1; sx++) {
|
|
29
|
+
const bit = sy * horseStrideWidth + sx;
|
|
30
|
+
ink += (frame[bit >> 3] >> (bit & 7)) & 1;
|
|
31
|
+
}
|
|
32
|
+
// Area coverage keeps thin legs continuous as the terminal becomes smaller.
|
|
33
|
+
if (ink / ((sx1 - sx0) * (sy1 - sy0)) < 0.22)
|
|
34
|
+
continue;
|
|
35
|
+
const dx = left + x;
|
|
36
|
+
const dy = top + y;
|
|
37
|
+
const index = Math.floor(dy / 4) * w + Math.floor(dx / 2);
|
|
38
|
+
masks[index] |= brailleBits[dy % 4][dx % 2];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return Array.from({ length: h }, (_, y) => {
|
|
42
|
+
let text = '';
|
|
43
|
+
for (let x = 0; x < w; x++) {
|
|
44
|
+
const mask = masks[y * w + x];
|
|
45
|
+
text += mask === 0 ? ' ' : String.fromCharCode(0x2800 + mask);
|
|
46
|
+
}
|
|
47
|
+
// No background color: only the horse's ink is drawn over the receiver panel.
|
|
48
|
+
return { text, color, segments: [{ text, color }] };
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/** Lightly pare back thick stroke edges, keeping endpoints and junctions intact. */
|
|
52
|
+
export function buildGallopFine(pulse, width, height, theme) {
|
|
53
|
+
return thinGallopStrokes(buildGallop(pulse, width, height, theme), 0.14);
|
|
54
|
+
}
|
|
55
|
+
function thinGallopStrokes(original, reduction) {
|
|
56
|
+
const w = original[0].text.length;
|
|
57
|
+
const h = original.length;
|
|
58
|
+
const dw = w * 2;
|
|
59
|
+
const dh = h * 4;
|
|
60
|
+
const dots = new Uint8Array(dw * dh);
|
|
61
|
+
let inkCount = 0;
|
|
62
|
+
original.forEach((row, y) => {
|
|
63
|
+
[...row.text].forEach((glyph, x) => {
|
|
64
|
+
const mask = glyph === ' ' ? 0 : glyph.charCodeAt(0) - 0x2800;
|
|
65
|
+
for (let yy = 0; yy < 4; yy++)
|
|
66
|
+
for (let xx = 0; xx < 2; xx++) {
|
|
67
|
+
if (mask & brailleBits[yy][xx]) {
|
|
68
|
+
dots[(y * 4 + yy) * dw + x * 2 + xx] = 1;
|
|
69
|
+
inkCount++;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
const neighbors = [-dw, -dw + 1, 1, dw + 1, dw, dw - 1, -1, -dw - 1];
|
|
75
|
+
const canThin = (index) => {
|
|
76
|
+
if (!dots[index])
|
|
77
|
+
return false;
|
|
78
|
+
const ring = neighbors.map(offset => dots[index + offset]);
|
|
79
|
+
const count = ring.reduce((total, dot) => total + dot, 0);
|
|
80
|
+
// Already-fine lines and endpoints stay untouched. A single connected arc of
|
|
81
|
+
// neighbors ensures removing this edge dot cannot split a leg or contour.
|
|
82
|
+
if (count < 3 || count > 6)
|
|
83
|
+
return false;
|
|
84
|
+
let transitions = 0;
|
|
85
|
+
for (let i = 0; i < 8; i++)
|
|
86
|
+
if (!ring[i] && ring[(i + 1) % 8])
|
|
87
|
+
transitions++;
|
|
88
|
+
return transitions === 1;
|
|
89
|
+
};
|
|
90
|
+
const candidates = [];
|
|
91
|
+
for (let y = 1; y < dh - 1; y++)
|
|
92
|
+
for (let x = 1; x < dw - 1; x++) {
|
|
93
|
+
const index = y * dw + x;
|
|
94
|
+
if (canThin(index))
|
|
95
|
+
candidates.push(index);
|
|
96
|
+
}
|
|
97
|
+
const budget = Math.min(candidates.length, Math.floor(inkCount * reduction));
|
|
98
|
+
for (let i = 0; i < budget; i++) {
|
|
99
|
+
// Spread the small reduction over the entire horse instead of thinning its
|
|
100
|
+
// head first. Recheck after each removal to preserve connected strokes.
|
|
101
|
+
const index = candidates[Math.floor(i * candidates.length / budget)];
|
|
102
|
+
if (canThin(index))
|
|
103
|
+
dots[index] = 0;
|
|
104
|
+
}
|
|
105
|
+
return original.map((row, y) => {
|
|
106
|
+
let text = '';
|
|
107
|
+
for (let x = 0; x < w; x++) {
|
|
108
|
+
let mask = 0;
|
|
109
|
+
for (let yy = 0; yy < 4; yy++)
|
|
110
|
+
for (let xx = 0; xx < 2; xx++) {
|
|
111
|
+
if (dots[(y * 4 + yy) * dw + x * 2 + xx])
|
|
112
|
+
mask |= brailleBits[yy][xx];
|
|
113
|
+
}
|
|
114
|
+
text += mask ? String.fromCharCode(0x2800 + mask) : ' ';
|
|
115
|
+
}
|
|
116
|
+
return { text, color: row.color, segments: [{ text, color: row.color }] };
|
|
117
|
+
});
|
|
118
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Fixed-canvas gallop poses derived from the user-provided horse line-art reference.
|
|
2
|
+
* Each base64 frame is a 128 × 80 one-bit ink mask, packed least-significant bit first.
|
|
3
|
+
* Background pixels are transparent; no video decoder or source file is needed at runtime.
|
|
4
|
+
*/
|
|
5
|
+
export const horseStrideWidth = 128;
|
|
6
|
+
export const horseStrideHeight = 80;
|
|
7
|
+
export const horseStrideFrames = [
|
|
8
|
+
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAOB9AAAAAAAAAAAAAAAAAADwMQAAAAAAAAAAAAAAAAAADzADAAAAAAAAAAAAAAAAwA8mBwAAAAAAAAAAAAAAAPCAAxwAAAAAAAAAAAAAAAA4wAE4AAAAAAAAAAAAAAAAmGcAcAAAAAAAAAAAAAAAAIg5APgAAAAAAAAAAAAAAAAMOGCAAQAAAAAAAAAAAAAABgxgIAMAAAAAAAAAAAAAgAMGYAAIAAAAAAAAAAAAAMABA2AAGAAAAAAAAAAAAADgxwPAABAAAAAAAAAAAAAA8OMA4A8QAAAAAAAAAAAAAPB8AGB+EAAAAAAAAAAAAADgHgAw4BAAAAAAAAAAAAAAwAcAEIARAAAAAAAAAID/AcABABgAAQAAAAAAAADw//9hgAAYAAAAAAAAAAAAeAD8AQABDAAAAAAAAAAAAB4AAAAAAAwAAAAAAAAAAAAHEAAAAAAMAAAAAAAAAACAAxAAAAAGDAAAAAAAAAAAgAEAAAAADAYAAAAAAAAAADgAAAAAABgGAAAAAAAAAMAfAAAAAAA4BgAAAAAAAAD/AwAAAAAAMAIAAAAAAADg/wMAAAAAAHAHAAAAAAAAMIBgAAAAAADwBwAAAAAAAAjwYADAAAAA8AcAAAAAAAAAOOAAwAAAAPAGAAAAAAAAAAjAEMAAAAD4DgAAAAAAAAAIwHGAAAAA/DwAAAAAAAAAAIBDgAAAAPwgAAAAAAAAAAAAB4AAAAB9AA8AAAAABgAAAB+AAACCfQAeAAAA5gwAAAB+gAAAxngAGAAAAIwBgAAA+MADAObw/zEAAAAYAIAAALCB/wB+YK8HAAAAcADAAADggf//f+AADgAAAMABYAAA4IP5f/DEAQwAAAAAPnAAAECDAQDAhwMYAAAAAD84AABAhgEAAB8HGAAAAIAfHwAAwIYAAAB8DtAAAAAA/QcAAMCGAAAA8BzQAAAAAPABAACAhAEAAMABsAEAAAAAAAAAgIYBAEyACbABAAAAAAAAAIAGAYDPwAlwAQAAAAAAAADAHgOA8/kAcAMAAAAAAAAAwD4cANt/APADAAAAAAAAAGD4+AC4PgDgAwAAAAAAAAAAwOcDcD4AwAMAAAAAAAAAwACfD+APAOAGAAAAAAAAAIACOAzgBwDgBwAAAAAAAAAAAuAYwAAAQAcAAAAAAAAAADbAOQAAAMAAAAAAAAAAAABmAHMAAADABQAAAAAAAAAAbADBAAAAAAcAAAAAAAAAANgAhwAAAAAGAAAAAAAAAACYAA4AAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAACACAAAAAAAAAAAAAAAAAABgAgAAAAAAAAAAAAAAAAAAYAYAAAAAAAAAAAAAAAAAAEAdAAAAAAAAAAAAAAAAAACANwAAAAAAAAAAAAAAAAAAgGMAAAAAAAAAAAAAAAAA4P//AwAAAAAAAAAAAAAAAOD//wEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
|
|
9
|
+
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAACA/wAAAAAAAAAAAAAAAAAAAEQAAAAAAAAAAAAAAAAAAABgBgAAAAAAAAAAAAAAAAAA7BwAAAAAAAAAAAAAAAAAAAA4AAAAAAAAAAAAAAAAAAYA4AAAAAAAAAAAAAAAAABAAOABAAAAAAAAAAAAAABgMABwAwAAAAAAAAAAAABAABjAAAYAAAAAAAAAAAAAAAAMwMAMAAAAAAAAAAAAAAAAAMAAGAAAAAAAAAAAAAAAAADAADAAAAAAAAAAAAAAAMAAwAMgAAAAAAAAAAAAABx4AIB/YAAAAAAAAAAAAAAEHACA4GEAAAAAAAAAAAAAAAcAAIBjAAAAAAAAAAAAAIABAAAAQgAAAAAAAACA/wHAAAAAAAAAAAAAAAAAAMD/XwAAEAAAAAAAAAAAAAYA/AMAAhAAAAAAAAAAAIADAAAAAAYYAAAAAAAAAACAAQAAAAAMCAAAAAAAAAAA4AAAAAAADAwAAAAAAAAAAHwAAAAAABgMAAAAAAAAAAAeACAAAAAQBAAAAAAAAAAAGABgAAAAAAYAAAAAAAAAADgAYAAAAAACAAAAAAAAAAAAAEAAAAAAAwAAAAAAAAwAYADAAAAAgAcAAAAAAAAAAGAAwAAAAKAEAAAAAAAAAADAAIAAAAAgAAAAAAAAAAAA4AGAAAAAwAAAAAAAAAAAAKADgAEAA0AAAAAAAAAAAABgDwAAAAKAQAAAAAAAAAAAYB8AAAAG4EAAAAAAOAAAAMB4AAEABOEADwAAAPABAAHA8QMDAIzAAR4AAADAi4ABgIEDHwDMADwwAAAAwMDAAAADA///D3z8YAAAAIB5wAAAAwbi//vxuQMAAAAAH2AAAAMMAgDAf2AOAAAAAAAwAAADGwYAAPhjOAAAAMACHAAAgxkGAAAAR7ADAADA/w8AgIExBgAAAExgAwAAAP8HAIDBMAQAAAAMwAYAAAA8AADAcSAMAAAABIAMAAAAAAAA8DmwDAAAABaAGQAAAAAAAPAYsBkAAAAbgBkAAAAAAADwDKA3AACAG4ARAAAAAAAA4AzgcAAAxA2AOwAAAAAAAGAMgOMAgPUGAH8AAAAAAABgBgDPAeAfBgDsAAAAAAAAwAQAnAfwDQMAiAAAAAAAAMAEAHAOsI0BAPwBAAAAAADADADgHPC/AQCcAQAAAAAAgAwAwDkAMAAAOAMAAAAAAIANAIAzAAAAAPADAAAAAACAGQAAdwAAAACAAgAAAAAAgBkAAOwAAAAAAAAAAAAAAIA5AADuAQAAAAAAAAAAAACAOwAAvgMAAAAAAAAAAAAAwD8AAPwHAAAAAAAAAAAAAAA/AAD4BwAAAAAAAAAAAAAAfQAAAAAAAAAAAAAAAAAAAO4AAAAAAAAAAAAAAAAAAACEAQAAAAAAAAAAAAAAAAAA/AMAAAAAAAAAAAAAAAAAAPgDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
|
|
10
|
+
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAADAAAAAAAAAAAAAAAAAAAwMwAAAAAAAAAAAAAAAAAAPjNAQAAAAAAAAAAAAAAAAAcAAMAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAAAAAAAYAAADAAAAAAAAAAAAABAhjgACB4AAAAAAAAAAAAAcBAcAAwwAAAAAAAAAAAAAAAABwAMbAAAAAAAAAAAAAAA4AMADIAAAAAAAAAAAAAAAHgAAAyAAQAAAAAAAAAAAAAOAAAcAAMAAAAAAAAAAACAAwAA/gECAAAAAAAA8B8A4AEBAOQHAgAAAAAAAP7/D34AAwAAHgIAAAAAAIAPAA8PAAYAADgCAAAAAADAAQAAAAAEAAAQAgAAAAAAcAAAAAAAECAAAAAAAAAAAD4AAAAAABAwAAAAAAAAAIAfAAAAAAAAGAAAAAAAAACADwAAAAAAAAwAAAAAAAAAgA8AAAAAAAAMAAAAAAAAAAAEAAAAAAAABgAAAAAAAAAADgAAAAAAgAMAAAAAAAAAABoAAAAAAIACAAAAAAAAAAAbABAAAACAAgAAAAAAAAAAGwAQAAAAwAIAAAAAAAAAADEAEAAAAMACAAAAAAAAAIAxABAAAAAAAgAAAAAAAACAYQAYAAAM4AIAAAAAAAAQgOEAGAAAAMAHAAAAAAAACIDBAQgAAAAGBwAAAAAAAAyAAAd4AAAcAgwAAAAA4AAEwAAE/AMAPAL8AAAAAMADBuAABow/gH/44AAAAAAAA0NwwAUE8P/nTwABAAAAAABhPOAEBgAAgP8fBgAAAAAMYB5wBgMAAADAPwYAAAAACLgfOAYDAAAAgHMAAAAAADjPBx6GAQAAAADnAAAAAADnR0EHwwAAAAAAjAQAAAAA/mDxgeMAAAAAAJwEAAAAAACgeMBhAAAAAACYDQAAAAAAMB7gMQAAAAAA8A0AAAAAABgHYBEAAAAAAOANAAAAAACYA+ARAAAAAACACQAAAAAA4AHAEQAAAAAAADkAAAAAAHcAgAEAAAAAAAD4AAAAAPA7AIAzAAAAAACAiQEAAAD0HAAANgAAAAAAgAkDAAAAHw4AAGYAAAAAAMAPHgAAgAkHAADsAAAAAADA/XwAAMCgAwAA3AAAAAAA4P/5AQBg/AEAAJgBAAAAAGCHjwMAcHwAAACYAwAAAABABjcAAPAHAAAAMAcAAAAAwAIwAADAAAAAAHAGAAAAAIADAAAAAAAAAABwDgAAAACAAQAAAAAAAAAAwB8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAYwAAAAAAAAAAAAAAAAAAgP8AAAAAAAAAAAAAAAAAAAD+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
|
|
11
|
+
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwH8AAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAcAAAAAAAAAAAAAAAAAAA8H8DAAAAAAAAAAAAAAAAAH4ABwAAAAAAAAAAAAAAAIAPAAwAAAAAAAAAAAAAAATgAQAYAAAAAAAAAAAAAAAAPAAAMAAAAAAAAAAAAAAAAA8AIHgAAAAAAAAAAAAAAMADACDYAAAAAAAAAAAAAAD+AAAwkAEAAAAAAAAAAACADwAAOBADAAAAAAAAAAAAwAMAADgAAwAAAAAAAPwfAPABAABwAAYAAAAAAID/////AAAA/AMMAAAAAADAB4A/AAAAAAEPAAAAAAAA4AEAAAAADMABOAwAAAAAAH8AAAAAAADgAHAMAAAAAOAfAAAAAABgMABADAAAAIC7DwAAAAAAQBgAAAAAAADgzQ8AAAAAAMAcAAAAAAAA8AAOAAAAAACADwAAAAAAAHgADgAAAAAAgAcAAAAAAAA4AA4AAAAAAAADAAAAAAAACAAOAAAAAACAAQAAAAAAAAAADgAIAAAAgAEAAAAAAAAAAB4ADAAACIABAAAAAAAAAAAeAAwAAAiAAQAAAAAAAAAAvgAMAAAIgAEAAAAAAAAAAPYBBgAACIABAAAAAAAAAABmA38AADiIAQAAAAAAAAAAYoIPAAA/DAMAAAAAAAAAAGKA/wAAeAQGAAAAAAAgAABgwPADAGAEDAAAAAAAIwBgYMCA//PPBzgAAAAAQAYAOHBgAPD/w5/hAAAAAMAEAB4wcAAAAAD7gAEAAADAAAADODAAAAAA5wcDAAAAgAMABx0cAAAAAIY/BwAAAAB/gO8fDgAAAAAscQwAAAAAfsD3DwcAAAAAOOMAAAAAAADA+IMBAAAAADDCAwAAAAAAYN7AAAAAAABgBgMAAAAAAADHYAAAAAAAwAw2AAAAAAAww3AAAAAAAMANbgAAAAAAmMExAAAAAACADewAAAAA/syAEQAAAAAAgA/YAAAAAI9kgBkAAAAAAAAXmAEAAACHMwAbAAAAAAAAPrgDAAAAjjEAEwAAAAAAAHz4AwAAAJgYABIAAAAAAAD88AcAAADgHwA2AAAAAAAA+MEMAAAAwAcANgAAAAAAAPBDDAAAAAACAGwAAAAAAAAwwggAAAAAAADMAAAAAAAAYIAfAAAAAAAAiAEAAAAAAOAcFAAAAAAAAIwBAAAAAADAewAAAAAAAAC+AwAAAAAAgPcAAAAAAAAAPA8AAAAAAADGAQAAAAAAAEAdAAAAAAAA/gMAAAAAAADgPwAAAAAAAPwAAAAAAAAA4D8AAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
|
|
12
|
+
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwH8AAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAcAAAAAAAAAAAAAAAAAAA8H8DAAAAAAAAAAAAAAAAAH4ABwAAAAAAAAAAAAAAAIAPAAwAAAAAAAAAAAAAAATgAQAYAAAAAAAAAAAAAAAAPAAAMAAAAAAAAAAAAAAAAA8AIHgAAAAAAAAAAAAAAMADACDYAAAAAAAAAAAAAAD+AAAwkAEAAAAAAAAAAACADwAAOBADAAAAAAAAAAAAwAMAADgAAwAAAAAAAPwfAPABAABwAAYAAAAAAID/////AAAA/AMMAAAAAADAB4A/AAAAAAEPAAAAAAAA4AEAAAAADMABOAwAAAAAAH8AAAAAAADgAHAMAAAAAOAfAAAAAABgMABADAAAAIC7DwAAAAAAQBgAAAAAAADgzQ8AAAAAAMAcAAAAAAAA8AAOAAAAAACADwAAAAAAAHgADgAAAAAAgAcAAAAAAAA4AA4AAAAAAAADAAAAAAAACAAOAAAAAACAAQAAAAAAAAAADgAIAAAAgAEAAAAAAAAAAB4ADAAACIABAAAAAAAAAAAeAAwAAAiAAQAAAAAAAAAAvgAMAAAIgAEAAAAAAAAAAPYBBgAACIABAAAAAAAAAABmA38AADiIAQAAAAAAAAAAYoIPAAA/DAMAAAAAAAAAAGKA/wAAeAQGAAAAAAAgAABgwPADAGAEDAAAAAAAIwBgYMCA//PPBzgAAAAAQAYAOHBgAPD/w5/hAAAAAMAEAB4wcAAAAAD7gAEAAADAAAADODAAAAAA5wcDAAAAgAMABx0cAAAAAIY/BwAAAAB/gO8fDgAAAAAscQwAAAAAfsD3DwcAAAAAOOMAAAAAAADA+IMBAAAAADDCAwAAAAAAYN7AAAAAAABgBgMAAAAAAADHYAAAAAAAwAw2AAAAAAAww3AAAAAAAMAMbgAAAAAAmMExAAAAAACADewAAAAA/syAEQAAAAAAgA/YAAAAAI9kgBkAAAAAAAAXmAEAAACHMwAbAAAAAAAAPrgDAAAAjjEAEwAAAAAAAHz4AwAAAJgYABIAAAAAAAD88AcAAADgHwA2AAAAAAAA+MEMAAAAwAcANgAAAAAAAPBDDAAAAAACAGwAAAAAAAAwwggAAAAAAADMAAAAAAAAYIAfAAAAAAAAiAEAAAAAAOAcFAAAAAAAAIwBAAAAAADAewAAAAAAAAC+AwAAAAAAgPcAAAAAAAAAPA8AAAAAAADGAQAAAAAAAEAdAAAAAAAA/gMAAAAAAADgPwAAAAAAAPwAAAAAAAAA4D8AAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
|
|
13
|
+
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+PgAAAAAAAAAAAAAAAAAA4DwAAAAAAAAAAAAAAAAAAMAtAAAAAAAAAAAAAAAAABj43wAAAAAAAAAAAAAAAAAMEMABAAAAAAAAAAAAAAAAwAGAAwAAAAAAAAAAAAAAwGAAAAcAAAAAAAAAAAAAAHA4AAAMAAAAAAAAAAAAAMAgDgAMDwAAAAAAAAAAAADwBwYADB8AAAAAAAAAAAAA8IcAAA42AAAAAAAAAAAAAAB8AAAOZAAAAAAAAAAAAAAADwAADmAAAAAAAAAA/AcAgAMAAA7AAAAAAAAAgP/+APABAAB/gAEAAAAAAPAB4P8/AACA4YEBAAAAAAA4AAjgAQAAAAAHAQAAAAAAHgAAAAAAAAAAjgEAAAAAgAsAAAAAAAAAAIwBAAAAAOAEAAAAAAAAGACIAQAAAAAABAAAAAAAQAwAAAAAAAAACAQAAAAAAMAMAAAAAAAAAA4GAAQAAACABgAAAAAAAAADBAAEAAAAgAMAAAAAAACAAQQABAAAAIABAAAAAAAAAAAAAAQAAAAAAQAAAAAAAAAAAAAGAAAAAAMAAAAAAACAAAAABgAAAAADAAAAAAAAEAAAAAMAAAAAAwAAAAAAABCAF4ADAAAAAAMAAAAAAAAA8A+AHwAAAIADAAAAAAAACBAM4B8AAADAAwAAAAAAAAwGDvA5AAAA4AIAAAAAAAAGGgc4cAAAOGAGAAAAAAAABpwDHMAHAPAADAAAAAAAAAf9AA4A/wzcARgAAAAAAICDv4AHAPD/HwcwAAAAAADDgz/gAQAADAyUYAAAAACg4cEHeAAAAACE8cMBAAAA8IDBDw4AAAAAhMCPAwAAAODP4AsBAAAAAMQABwcAAAAARnPDAQAAAABEABwOAAAAAIB/wwAAAAAAZABwDAAAAADAvAEAAAAAAGYA4AgAAAAA4I0ZAAAAAABmAIBjAAAAAHDQDAAAAAAAZgAAxwMAAAD4/wYAAAAAADYAANgPAAAA/HcDAAAAAAA2AABwHAAAAAC8AwAAAAAAHgAAwPEAAADAnAEAAAAAAB4AAIDvAwAA8McAAAAAAAAPAAAAjw8AALjjAAAAAAAADwAAAAgAAAAcfwAAAAAAgA8AAAD4AQAALD8AAAAAAIALAAAA8AEAAP4/AAAAAACADwAAAAAAAAB4AAAAAAAAwAwAAAAAAAAAAAAAAAAAAMAMAAAAAAAAAAAAAAAAAADADAAAAAAAAAAAAAAAAAAAgA8AAAAAAAAAAAAAAAAAAAA7AAAAAAAAAAAAAAAAAAAAcwAAAAAAAAAAAAAAAAAAAOMAAAAAAAAAAAAAAAAAAAD/AQAAAAAAAAAAAAAAAAAA/wEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
|
|
14
|
+
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcAAAAAAAAAAAAAAAAAAAAfAAAAAAAAAAAAAAAAAADAHAAAAAAAAAAAAAAAAAAA4A8AAAAAAAAAAAAAAAAAIPhvAAAAAAAAAAAAAAAAAPAcwAAAAAAAAAAAAAAAAACAAIABAAAAAAAAAAAAAAAAAgAAAwAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAAMAIYPAAAAAAAAAAAAAABwBgCHCwAAAAAAAAAAAAAAAAMAAxgAAAAAAAAAAAAAQIADAAcTAAAAAAAAAAAAAAD+AAAHIAAAAAAAAADwAQAAHgAAD2AAAAAAAAAA/x8AgAcAgHvAAAAAAAAAwAf4B8ABAIDhgQAAAAAAAHAAAOdvAAQAgIMAAAAAAIAfAAAAAAAMAADGAAAAAADwAwAAAAAAGAAAxAAAAAAAeAEAAAAAABgQAMQAAAAAAOAFAAAAAAAwGAAAAAAAACwgDAAAAAAAIAwAAAAAAAABAAQAAAAAAEAMAAAAAAAAAAAEAAAAAAAABgAAAAAAAAAADAAAAAAAAAYAAAAAAAAAAAwABAAAAIADAAAAAAAAQAAMAAAAAACAAQAAAAAAAAAADAAAAAAAgAEAAAAAAAAwAAwAAQAAAIABAAAAAAAAGOAPgAEAAACAAQAAAAAAYAzwDYABAAAAgAEAAAAAACAE8AbAHwAAAMAAAAAAAAAwBJADwAcAAADAAAAAAAAAMwH+ATAfAAAAQAAAAAAAABMBfgAA+AAAAAAAAAAAAACbAR8AAOAHAACIAAAAAAAA24EDwAEA/wNgjAAAAAAAAOuAAf4AAPD//4ABAAAAAIDvgMAfAAAA4PkDAQAAAADA98DgAAAAAGCYFwMAAAAA4HpgeAAAAABgDDwDAAAAAMA/YE4AAAAAcAd4BgAAAACADzBHAAAAAHAD+AYAAAAAAACYZwAAAAA4A+AMAAAAAAAAzmcAAAAAuAEAHAAAAAAAMMM7AAAAAIwAABgAAAAAAPjjPgAAAADMAIAxAAAAAAD8sQ4AAAAAbAAAMQAAAAAAPvwHAAAAADYAACYAAAAAAPf/AAAAAAAbAAAcAAAAAID/NwAAAAAACwAAOAAAAACADwAAAAAAgA0AAHAAAAAAAAAAAAAAAMAGAADgBgAAAAAAAAAAAABgBgAAwBwAAAAAAAAAAAAAcAMAAAA4AAAAAAAAAAAAADADAAAA8AAAAAAAAAAAAAAcAQAAAOMHAAAAAAAAAAAA+AEAAAA+PgAAAAAAAAAAALABAAAA+BAAAAAAAAAAAAAgAwAAAOAEAAAAAAAAAAAAYA8AAADABwAAAAAAAAAAAOAPAAAAAAAAAAAAAAAAAADgHQAAAAAAAAAAAAAAAAAA4D8AAAAAAAAAAAAAAAAAAOA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
|
|
15
|
+
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAMAAAAAAAAAAAAAAAAAAAAHAAAAAAAAAAAAAAAAAAC8HwAAAAAAAAAAAAAAAAAAxDcAAAAAAAAAAAAAAAAAAAxwAAAAAAAAAAAAAAAAAOAPwAAAAAAAAAAAAAAAAADfA4ABAAAAAAAAAAAAAAAA/wHBAwAAAAAAAAAAAAAAAP4A4QcAAAAAAAAAAAAAAAB8gMEGAAAAAAAAAAAAAAAAHoCBDQAAAAAAAAAAAAAAFA+AARgAAAAAAAAAAAAAAJwPgAMQAAAAAAAAAAAAAAD4B4APcAAAAAAAAAD/HwAA/AGAfWAAAAAAAADAv/8DAB8AwOBAAAAAAAAA4ADgf8APAOCAYQAAAAAAgD8AAPD/AQBgAGMAAAAAAPgfAAAASAAIMABgAAAAAAC/AQAAAAAAGDAAAAAAAADA9wkAAAAAABgYAAAAAAAAeDwIAAAAAAAwDAAAAAAAAA4GDAAAAAAAYAwAAAAAAAAAAAwAAAAAAGAGAAAAAAAAAAAMAAQAAABgBgAAAAAAAwAADAAGAAAAQAIAAAAAAAEAAAwAAgAAAMADAAAAAIABAAA8AAIAAADAAwAAAACAAAAALAADAAAAwAEAAAAAAAAAAG4AAwAAAMAAAAAAAAAAAMAHgAEAAADAAAAAAAAAAAD8A4A/AAAAwAAAAAAAAAAA/gHAPwAAAMAAAAAAAAAAgGAA8A8AAAJgAAAAAAAAAMAAAPgfAAAHcAAAAAAAAABgAIA/fAAABzgAAAAAACAAYOD/D/AHAI4fAAAAAAAgADD8/wCA/8fsCwAAAAAAYACIDwAAAPj//wgAAAAAAGAIjAEAAAAA/zkYAAAAAABgBocABgAAAOMwGAAAAAAAPOIxAAYAAABhYBgAAAAAACDsGAAGAACAM+AZAAAAAAAAfA7ABgAAwBjAGwAAAAAAADMHgAYAAOAcwBgAAAAAAIDrA0AGAABgDoAZAAAAAACA/QF4BgAAYAaAMwAAAAAAwF8AfAYAADADACMAAAAAAMAPAEwDAAAAAQAjAAAAAAAAAADeAwAADgAAZgAAAAAAAAAA4gAAAD8AAGQAAAAAAAAAAAIAAMAfAAAMAAAAAAAAAAACAADwDgAAaAAAAAAAAAAAAAAAMAYAABgAAAAAAAAAAAAAAD4DAAAQAAAAAAAAAAAAAAD/AQAAsAAAAAAAAAAAAACA9QAAALABAAAAAAAAAAAAgD0AAABgAwAAAAAAAAAAAMAPAAAAYAMAAAAAAAAAAADAAQAAAGAGAAAAAAAAAAAAAAAAAADgDAAAAAAAAAAAAAAAAAAA4A8AAAAAAAAAAAAAAAAAAIA7AAAAAAAAAAAAAAAAAAAA+wAAAAAAAAAAAAAAAAAAAN8BAAAAAAAAAAAAAAAAAAD+AwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
|
|
16
|
+
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAADAAQAAAAAAAAAAAAAAAAAAfAcAAAAAAAAAAAAAAAAAAPwPAAAAAAAAAAAAAAAAAAAeGQAAAAAAAAAAAAAAAADgADAAAAAAAAAAAAAAAAAA8ABgAAAAAAAAAAAAAAAAAPAAwAAAAAAAAAAAAAAAAADwgOABAAAAAAAAAAAAAAAAfcDwAwAAAAAAAAAAAAAAAD/AYAcAAAAAAAAAAAAAAAAewMAGAAAAAAAAAAAAAADAAcAACAAAAAAAAAAAAAAA+AXAARgAAAAAAAAAAAAAAH4AwA8wAAAAAAAAAPgfAAAeAMA+IAAAAAAAAAA/OACABwBg4DAAAAAAAACAAwD/5wAAAMAxAAAAAAAAAAAY/G8AAACAMQAAAAAAAAwAEAAAAAAAABAAAAAAAMAPAAAAAAAAAAAAAAAAAADgAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAACAAAAAAAAAQAAAAAAAAAPggAAAAAACAGAAAAAIAHAAwIAAAAAAAAAgAAAADAAYADGAAAAAAAQAMAAAAA4ADwARgADAAAAAADAAAAAGAAeAAYAAwAAACAAQAAAAAwCAgAEAAMAAAAwAAAAAAAEAwAADAABgAAgAAAAAAAABAAAAA4AAYAAAAAAAAAAADQAgAAHAAPAAAAAAAAAAAAUAIAAA8APwAAADAAAAAAADACAP4AwD8AgAEwAAAAAAAQAAACAPB4AIABHAAAAAAAGAEAAAA+4AeAAwwAAAAAABgGAAPwH4D/AGMCAAAAAAB4AwAD/wEA8D9/AwAAAAAAvAMA4zEAAACABwMAAAAAAP4BAHMkAAAAgIcBAAAAAAB+AAAZbAAAAICGAAAAAAAAAAAAHWwAAADAxwAAAAAAAAAAgA1sAAAAwMcAAAAAAAAAAIAOzAAAAOBGAAAAAAAAAAAgBswAAABwZgAAAAAAAAAAOAfIAAAAGG8AAAAAAAAAAB8DiAEA+IdnAAAAAAAAAACPA5gBgPzDYwAAAAAAAABAhgGwAcAP/DMAAAAAAAAAQP8AsAHwxg8xAAAAAAAAAGB/ALgB+P8AGAAAAAAAAABgBwC4AVwbABkAAAAAAAAA8AMA+AF8AAABAAAAAAAAAPABAJABHACAAQAAAAAAAABAAACwAAAAgAUAAAAAAAAAAAAA8AAAAIAEAAAAAAAAAAAAAOAAAADABgAAAAAAAAAAAADAAAAAwAYAAAAAAAAAAAAAAAAAAGAGAAAAAAAAAAAAAAAAAAAwBgAAAAAAAAAAAAAAAAAAMAMAAAAAAAAAAAAAAAAAAOAHAAAAAAAAAAAAAAAAAADADQAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIBzAAAAAAAAAAAAAAAAAACA/gAAAAAAAAAAAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
|
|
17
|
+
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAPwBAAAAAAAAAAAAAAAAAAD8AwAAAAAAAAAAAAAAAAAA+A8AAAAAAAAAAAAAAAAAIIAcAAAAAAAAAAAAAAAAAKgBOAAAAAAAAAAAAAAAAACsAHAAAAAAAAAAAAAAAAAApgBgAAAAAAAAAAAAAAAAAP7A8AAAAAAAAAAAAAAAAMD8wPABAAAAAAAAAAAAAABAGGAgAwAAAAAAAAAAAAAAAABgYAMAAAAAAAAAAAAAAAAC4AAGAAAAAAAAAAAAAAAAAuAADAAAAAAAAAAAAAAATwPgBxgAAAAAAAAA4A8AwH8DYB4YAAAAAAAAAPx/AMD/AQBwCAAAAAAAAAAP4P//cQAA4BgAAAAAAACAAwD/PwAAEMAYAAAAAAAA4AAAwAAAAhiACAAAAAAAAHAAAAAAAAYIAAAAAAAAAAAeAAAAAAAECAAAAAAAAADgBwAAAAAADAwAAAAAAAD8+QcAAAAAAAgEAAAAAADg/8EHAAAAAAAIBAAAAAAAfACAGAAAAAAAAAYAAAAAgH8AcBgAAAAAAAACAAAAAMABAD8YABAAAAAAAwAAAAAAAAADEAAQAAAAAAEAAAAAAAAAATAAGAAAAIABAAAAAAAAAAEwABgAAADAAAAAAAAACHAAMAAYAAAA4AAAAAAAAA8/AGAAGAAAAGAAAAAAAMAAAwDgAAwAQAAwAAAAAABgAAAA8AB8AEAAMAAAAAAAMAAAADAAfABAADgAAAAAABgAAAAcAP4AQAAcAAAAAAAMDgDwA8DcBwAABgAAAAAADAcA+AHwD3/gCAMAAAAAAAwDABgAvwP4f44BAAAAAADGAQAA4OMBAOABAAAAAAAAYwAACGZ+AADg4AAAAAAAADkAAAifDwAAYPAAAAAAAAAIAAAIsAMAAOD4AAAAAAAAAAAAiDAGAADw3AAAAAAAAAAAAMhgBgAAcM4AAAAAAAAAAADIwAwAAHjPAAAAAAAAAAAAzIAJAACYzwAAAAAAAAAAAEyAGQAAzuwAAAAAAAAAAABMABMAeMbMAAAAAAAAAAAATAAjAB7nZwAAAAAAAAAAAGQAZwAH42MAAAAAAAAAAIBnAGYAjzk+AAAAAAAAAACAYwBsAJj9HwAAAAAAAAAAgDAAbACACwAAAAAAAAAAAIAdAG4AkAEAAAAAAAAAAAAgDgBOANgBAAAAAAAAAAAAIAcAXgDsAAAAAAAAAAAAALABAEwAYgAAAAAAAAAAAAAwAABMADMAAAAAAAAAAAAAAAAAQIAxAAAAAAAAAAAAAAAAAECAGQAAAAAAAAAAAAAAAAAAgA0AAAAAAAAAAAAAAAAAAIANAAAAAAAAAAAAAAAAAAAAGwAAAAAAAAAAAAAAAAAAACMAAAAAAAAAAAAAAAAAAIBjAAAAAAAAAAAAAAAAAAAAwQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
|
|
18
|
+
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAA4AAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAA+N8BAAAAAAAAAAAAAAAAAMwBBwAAAAAAAAAAAAAAAABmAA4AAAAAAAAAAAAAAAAAPgAYAAAAAAAAAAAAAAAAAB4QPgAAAAAAAAAAAAAAAMAOGG4AAAAAAAAAAAAAAABgBxjIAAAAAAAAAAAAAAAA4AMYCAAAAAAAAAAAAAAAAPABGAADAAAAAAAAAAAAAADwATAABgAAAAAAAAAAAAAA8AHwAQQAAAAAAAAAAAAAAOAB8A8EAAAAAAAAAAAAAAD2Axg4BAAAAAAAAAAADAAA/wAIcAQAAAAAAAAAwP8AwH8ADOAEAAAAAAAAAPD/f/9/AAzAAQAAAAAAAAA8AP7/fwAMAAAAAAAAAAAADwAAwHcABAAAAAAAAAAAgAMAAAAwAAQAAAAAAAAAAMABAAAAAAAEAAAAAAAAAADgAAAAAAAABgAAAAAAAAAAcAAAAAAAAAYAAAAAAAAAADgAIAAAAAAGAAAAAAAAAAA+ACAAAAAAAgAAAAAAgAPANwBgAAAAGAMAAAAAAP///zEAYAAAADADAAAAAMAP/P83AGAAAACwAQAAAADwAADgIwFAAAAAoAEAAAAAGAAAAGABQAAAAOAAAAAAAAAAAABgBsAAAADgAAAAAAAAAAAAwAzAAIAA4AAAAAAAAAAAAMAZQADAAPAAAAAAAAAEAAAAB2AAwAD4AAAAAADABQAAAB7wAMAAeAAAAABAYCAAAAAe8AHAAHgAAAAAQBwwAAAABvgfwPg/AAAAAEAWAAAAAAMe/v9/fwAAAABABgAAAIADB+P/l2MAAAAAwAYAAADwgQ8DgIFhAAAAAMAGAAAAfOAMA4DBwAAAAACAAQAAAAY44A+AwsMBAAAAAAEAAAAGHvAfgGPHAQAAAAAAAAAADAfwe4BzPgMAAAAAAAAAABgDzPuAM3gGAAAAAAAAAAAwAc7PgRvgBAAAAAAAAAAAcALPnIMIwAQAAAAAAAAAAGAG/zjPDOAAAAAAAAAAAADADh/x/QxwAAAAAAAAAAAAgB0ewycGvAMAAAAAAAAAAIA7PAcPB84BAAAAAAAAAAAAc/AO/ON3AAAAAAAAAAAAAGfAc/D8PwAAAAAAAAAAAABugMcAegwAAAAAAAAAAAAAbADPAD8HAAAAAAAAAAAAAG4AnoHpAwAAAAAAAAAAAABmAHzH/wEAAAAAAAAAAAAAcADw5h8AAAAAAAAAAAAAAHYA4I0AAAAAAAAAAAAAAABsAGAMAAAAAAAAAAAAAAAAfADAHwAAAAAAAAAAAAAAAHgAgB8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
|
|
19
|
+
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAA4AAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAA+N8BAAAAAAAAAAAAAAAAAMwBBwAAAAAAAAAAAAAAAABmAA4AAAAAAAAAAAAAAAAAPgAYAAAAAAAAAAAAAAAAAB4QPgAAAAAAAAAAAAAAAMAOGG4AAAAAAAAAAAAAAABgBxhIAAAAAAAAAAAAAAAA4AMYCAAAAAAAAAAAAAAAAPABGAADAAAAAAAAAAAAAADwATAABgAAAAAAAAAAAAAA8AHwAQQAAAAAAAAAAAAAAOAB8A8EAAAAAAAAAAAAAAD2Axg4BAAAAAAAAAAADAAA/wAIcAQAAAAAAAAAwP8AwH8ADOAEAAAAAAAAAPD/f/9/AAzAAwAAAAAAAAA8AP7/fwAMAAAAAAAAAAAADwAAwHcABAAAAAAAAAAAgAMAAAAwAAQAAAAAAAAAAMABAAAAAAAEAAAAAAAAAADgAAAAAAAABgAAAAAAAAAAcAAAAAAAAAYAAAAAAAAAADgAMAAAAAAGAAAAAAAAAAA+ACAAAAAAAgAAAAAAgAPANwBgAAAAGAMAAAAAAP///zEAYAAAADADAAAAAMAP/P83AGAAAACwAQAAAADwAADgIwBAAAAAoAEAAAAAGAAAAGAAQAAAAOAAAAAAAAAAAABgBsAAAADgAAAAAAAAAAAAwAzAAIAA4AAAAAAAAAAAAMAZQADAAPAAAAAAAAAEAAAAB2AAwAD4AAAAAADABwAAAB7wAMAAeAAAAABAYAAAAAAe8AHAAHgAAAAAQBwQAAAABvgfwPg/AAAAAEAUAAAAAAMe/v9/fwAAAABABgAAAIADB+P/l2EAAAAAwAYAAADwgQ8DgIFhAAAAAMAGAAAAfOAMA4DBwAAAAACAAQAAAAY44A+AwsMBAAAAAAEAAAAGHvAfgGPHAQAAAAAAAAAADAf0e4BzPgMAAAAAAAAAABgDzPuAM3gGAAAAAAAAAAAwAc7PgRvgBAAAAAAAAAAAcALPnIMIwAQAAAAAAAAAAGAGzzjPDOAAAAAAAAAAAADADp/x/QxwAAAAAAAAAAAAgB2ewycGnAMAAAAAAAAAAIA7PAcOB84BAAAAAAAAAAAAc/Ae/ON3AAAAAAAAAAAAAGfAc/D8PwAAAAAAAAAAAABugMcAegwAAAAAAAAAAAAAbADPAD8HAAAAAAAAAAAAAG4AnoHpAwAAAAAAAAAAAABmAHzH/wEAAAAAAAAAAAAAcADw5h8AAAAAAAAAAAAAAHYA4I0AAAAAAAAAAAAAAABsAGAMAAAAAAAAAAAAAAAAfADAHwAAAAAAAAAAAAAAAHgAgB8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
|
|
20
|
+
];
|
|
@@ -8,6 +8,7 @@ export const receiverStyleRegistry = [
|
|
|
8
8
|
{ id: 'cyclone', maxRows: 14, minRows: 7 },
|
|
9
9
|
{ id: 'nebula', maxRows: 14, minRows: 7 },
|
|
10
10
|
{ id: 'lava-lamp', maxRows: 14, minRows: 7 },
|
|
11
|
+
{ id: 'gallop-fine', maxRows: 18, minRows: 7 },
|
|
11
12
|
{ id: 'motion-contour', maxRows: 14, minRows: 8 },
|
|
12
13
|
{ id: 'ribbon', maxRows: 12, minRows: 7 },
|
|
13
14
|
{ id: 'braille-wave', maxRows: 12, minRows: 7 },
|
|
@@ -60,12 +61,21 @@ export const receiverStyleRegistry = [
|
|
|
60
61
|
{ id: 'skyline', maxRows: 14, minRows: 7 },
|
|
61
62
|
{ id: 'golden-gate', maxRows: 14, minRows: 7 },
|
|
62
63
|
{ id: 'manhattan', maxRows: 14, minRows: 7 },
|
|
63
|
-
{ id: 'alpine', maxRows: 14, minRows: 7 }
|
|
64
|
+
{ id: 'alpine', maxRows: 14, minRows: 7 },
|
|
65
|
+
{ id: 'crossfade', maxRows: 14, minRows: 7 },
|
|
64
66
|
];
|
|
65
67
|
export const receiverStyleNames = receiverStyleRegistry.map(style => style.id);
|
|
66
|
-
export const defaultReceiverStyle = '
|
|
68
|
+
export const defaultReceiverStyle = 'gallop-fine';
|
|
67
69
|
export const receiverStyleMetadata = Object.fromEntries(receiverStyleRegistry.map(style => [style.id, style]));
|
|
68
70
|
const retiredReceiverStyleNames = [
|
|
71
|
+
'duplex-wave', 'color-drift', 'undertow', 'cube-tide', 'parallax-slabs', 'voxel-swell',
|
|
72
|
+
'dot-current', 'flip-matrix', 'raster-harp',
|
|
73
|
+
'spin-lattice', 'packet-storm', 'spark-chamber',
|
|
74
|
+
'magnetic-bloom', 'liquid-chrome', 'vector-braid', 'halftone-orbit',
|
|
75
|
+
'sonic-fan', 'raster-disc', 'diamond-current', 'velvet-vortex',
|
|
76
|
+
'din-stack', 'bass-shutters', 'velvet-dither', 'chrome-sweep', 'prism-steps', 'stereo-iris',
|
|
77
|
+
'phosphor-marquee', 'ascii-drive', 'prism-bank', 'gallop', 'gallop-ultrafine',
|
|
78
|
+
'gallop-solid', 'gallop-shaded', 'gallop-dither', 'gallop-hatch', 'gallop-scanline',
|
|
69
79
|
'scope', 'spectrum', 'oscilloscope', 'sdr', 'signal', 'retro', 'neon', 'waterfall',
|
|
70
80
|
'cassette', 'casette', 'stars', 'radio-waves', 'raindrops', 'vinyl', 'soundwave',
|
|
71
81
|
'spectrum-3d', 'tuning-dial', 'rf-constellation', 'rf-constelation', 'sphere', 'mobius',
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { themeAccent, themeContributionColors } from '../theme.js';
|
|
2
|
+
import { retroReceiverBuilders } from './retro-receivers.js';
|
|
2
3
|
import { receiverStyleMetadata } from './receiver-style-registry.js';
|
|
3
4
|
const receiverStyleBuilders = {
|
|
5
|
+
...retroReceiverBuilders,
|
|
4
6
|
ultracode: buildUltracode,
|
|
5
7
|
'motion-contour': buildMotionContour,
|
|
6
8
|
leds: buildLeds,
|
|
@@ -89,6 +91,7 @@ function buildMicroVisualizer(style, pulse, width, height, theme) {
|
|
|
89
91
|
// the circular braille rings) while fixed-size and minimum-size scenes use the
|
|
90
92
|
// supersampled fit path below.
|
|
91
93
|
const microNativeScaleStyles = new Set([
|
|
94
|
+
...Object.keys(retroReceiverBuilders),
|
|
92
95
|
'pulse-grid',
|
|
93
96
|
'hex-pulse',
|
|
94
97
|
'galaxy',
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { buildGallopFine } from './gallop.js';
|
|
2
|
+
import { panelBackground, themeAccent, themeContributionColors } from '../theme.js';
|
|
3
|
+
const clamp = (n) => Math.max(0, Math.min(1, n));
|
|
4
|
+
function blend(a, b, t) {
|
|
5
|
+
return '#' + [1, 3, 5].map(offset => Math.round(parseInt(a.slice(offset, offset + 2), 16) * (1 - t) + parseInt(b.slice(offset, offset + 2), 16) * t).toString(16).padStart(2, '0')).join('');
|
|
6
|
+
}
|
|
7
|
+
/** A cell canvas, not a bitmap: glyphs, whole-cell color and intentional empty space. */
|
|
8
|
+
function canvas(width, height, theme) {
|
|
9
|
+
const w = Math.max(1, Math.floor(width));
|
|
10
|
+
const h = Math.max(1, Math.floor(height));
|
|
11
|
+
const accent = themeAccent(theme);
|
|
12
|
+
const contributions = themeContributionColors(theme);
|
|
13
|
+
const ramp = [panelBackground, blend(panelBackground, contributions[1], 0.55),
|
|
14
|
+
...contributions.slice(1), blend(accent, '#ffffff', 0.36), '#ffffff'];
|
|
15
|
+
const cells = Array.from({ length: h }, () => Array.from({ length: w }, () => ({ text: ' ', color: accent })));
|
|
16
|
+
const paint = (x, y, text, level, background = false, color) => {
|
|
17
|
+
const row = Math.round(y);
|
|
18
|
+
const col = Math.round(x);
|
|
19
|
+
if (row < 0 || row >= h || col < 0 || col >= w)
|
|
20
|
+
return;
|
|
21
|
+
const ink = color ?? ramp[Math.max(0, Math.min(ramp.length - 1, Math.round(level)))];
|
|
22
|
+
cells[row][col] = background ? { text: ' ', color: ink, backgroundColor: ink } : { text, color: ink };
|
|
23
|
+
};
|
|
24
|
+
const finish = () => cells.map(row => {
|
|
25
|
+
const segments = [];
|
|
26
|
+
for (const cell of row) {
|
|
27
|
+
const last = segments.at(-1);
|
|
28
|
+
if (last && last.color === cell.color && last.backgroundColor === cell.backgroundColor)
|
|
29
|
+
last.text += cell.text;
|
|
30
|
+
else
|
|
31
|
+
segments.push({ ...cell });
|
|
32
|
+
}
|
|
33
|
+
return { text: row.map(c => c.text).join(''), color: accent, segments };
|
|
34
|
+
});
|
|
35
|
+
return { w, h, ramp, paint, finish };
|
|
36
|
+
}
|
|
37
|
+
// Interleaved wavefronts, folded through each other. Entire cells are the light source.
|
|
38
|
+
const crossfade = (pulse, width, height, theme) => {
|
|
39
|
+
const c = canvas(width, height, theme);
|
|
40
|
+
const t = pulse * 0.08;
|
|
41
|
+
const shades = Array.from({ length: 18 }, (_, i) => blend(c.ramp[2], c.ramp[5], i / 17));
|
|
42
|
+
for (let y = 0; y < c.h; y++) {
|
|
43
|
+
for (let x = 0; x < c.w; x++) {
|
|
44
|
+
const u = x / c.w * 2 - 1;
|
|
45
|
+
const v = y / c.h * 2 - 1;
|
|
46
|
+
const a = u * 8.5 + v * 3 + Math.sin(v * 3.4 - t * 0.7) * 2 - t * 1.1;
|
|
47
|
+
const b = v * 5 - u * 2 + Math.sin(u * 3 + t * 0.5) * 1.5 + t * 0.75;
|
|
48
|
+
const value = clamp(0.5 + 0.28 * Math.cos(a) + 0.24 * Math.cos(b));
|
|
49
|
+
c.paint(x, y, ' ', 0, true, shades[Math.round(value * 17)]);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return c.finish();
|
|
53
|
+
};
|
|
54
|
+
export const terminalReceiverBuilders = {
|
|
55
|
+
crossfade,
|
|
56
|
+
'gallop-fine': buildGallopFine
|
|
57
|
+
};
|