@ciphore/radiocli 0.1.7 → 0.1.9

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.
@@ -3,8 +3,8 @@ export function buildVisualizer(style, pulse, width, height, _station, playback,
3
3
  if (!playbackHasSignal(playback)) {
4
4
  return buildZeroSignalVisualizer(width, height, theme);
5
5
  }
6
- if (style === 'equalizer') {
7
- return buildEqualizer(pulse, width, height, theme);
6
+ if (style === 'ultracode') {
7
+ return buildUltracode(pulse, width, height, theme);
8
8
  }
9
9
  if (style === 'motion-blob') {
10
10
  return buildMotionBlob(pulse, width, height, theme);
@@ -153,7 +153,7 @@ export function buildVisualizer(style, pulse, width, height, _station, playback,
153
153
  if (style === 'newton') {
154
154
  return buildNewton(pulse, width, height, theme);
155
155
  }
156
- return buildEqualizer(pulse, width, height, theme);
156
+ return buildUltracode(pulse, width, height, theme);
157
157
  }
158
158
  function playbackHasSignal(playback) {
159
159
  return playback.state === 'playing' && playback.ready;
@@ -172,71 +172,80 @@ function buildFlatZeroSignal(width, requestedHeight, theme) {
172
172
  color: rowIndex === height - 1 ? accent : '#767676'
173
173
  }));
174
174
  }
175
- function buildEqualizer(pulse, width, height, theme) {
175
+ const ultracodeWavelength = 20;
176
+ const ultracodeTravelPerPulse = 80 * 0.03;
177
+ const ultracodeVioletRamp = ['#3e1676', '#491e87', '#542799', '#5f2faa', '#6b37bc', '#763fcd', '#8148df', '#8c50f0'];
178
+ function buildUltracode(pulse, width, height, theme) {
179
+ const renderWidth = Math.max(1, width);
180
+ const renderHeight = Math.max(1, height);
176
181
  const accent = themeAccent(theme);
177
- const bandWidth = 3;
178
- const bandCount = Math.floor((width - 2) / bandWidth);
179
- const levels = Array.from({ length: bandCount }, (_, i) => {
180
- const low = Math.sin(i * 0.18 + pulse * 0.46);
181
- const mid = Math.cos(i * 0.32 - pulse * 0.28);
182
- const high = Math.sin(i * 0.45 + pulse * 0.68);
183
- const normalized = (low * 0.4 + mid * 0.35 + high * 0.25 + 1) / 2;
184
- const eased = Math.pow(Math.max(0, Math.min(1, normalized)), 1.4);
185
- return Math.round(eased * height);
186
- });
187
- const peaks = Array.from({ length: bandCount }, (_, i) => {
188
- let maxLvl = 0;
189
- for (let k = 0; k < 8; k++) {
190
- const p = (pulse - k + 240) % 240;
191
- const low = Math.sin(i * 0.18 + p * 0.46);
192
- const mid = Math.cos(i * 0.32 - p * 0.28);
193
- const high = Math.sin(i * 0.45 + p * 0.68);
194
- const normalized = (low * 0.4 + mid * 0.35 + high * 0.25 + 1) / 2;
195
- const eased = Math.pow(Math.max(0, Math.min(1, normalized)), 1.4);
196
- const lvl = Math.round(eased * height);
197
- if (lvl > maxLvl) {
198
- maxLvl = lvl;
199
- }
200
- }
201
- return maxLvl;
202
- });
203
- const rows = [];
204
- const colors = themeContributionColors(theme);
205
- for (let rowIndex = 0; rowIndex < height; rowIndex++) {
206
- const threshold = height - rowIndex;
207
- let rowText = ' ';
208
- for (let i = 0; i < bandCount; i++) {
209
- const lvl = levels[i];
210
- const peak = peaks[i];
211
- let bandChar = ' ';
212
- if (lvl >= threshold) {
213
- bandChar = '██';
214
- }
215
- else if (peak === threshold) {
216
- bandChar = '◆◆';
217
- }
218
- else {
219
- bandChar = '··';
220
- }
221
- rowText += bandChar + ' ';
222
- }
223
- let color = colors[2] ?? accent;
224
- if (rowIndex < Math.max(1, height * 0.3)) {
225
- color = '#ff5f87';
226
- }
227
- else if (rowIndex < Math.max(2, height * 0.6)) {
228
- color = colors[4] ?? accent;
229
- }
230
- else {
231
- color = colors[3] ?? accent;
232
- }
233
- rows.push({
234
- text: rowText.padEnd(width).slice(0, width),
235
- color
182
+ const ramp = ultracodeRippleRamp(theme);
183
+ const selectedColor = ramp[ramp.length - 1] ?? accent;
184
+ const travel = pulse * ultracodeTravelPerPulse;
185
+ const originColumn = Math.floor(renderWidth / 2);
186
+ const originRow = Math.floor(renderHeight / 2);
187
+ const rows = Array.from({ length: renderHeight }, (_, rowIndex) => {
188
+ const cells = Array.from({ length: renderWidth }, (_, columnIndex) => {
189
+ const level = ultracodeRippleLevel(ultracodeDistance(columnIndex, rowIndex, originColumn, originRow), travel, ramp.length);
190
+ return {
191
+ text: ' ',
192
+ color: level === null ? accent : selectedColor,
193
+ backgroundColor: level === null ? undefined : ramp[level] ?? selectedColor
194
+ };
236
195
  });
237
- }
196
+ return lineFromCells(cells, accent);
197
+ });
238
198
  return rows;
239
199
  }
200
+ function ultracodeDistance(column, row, originColumn, originRow) {
201
+ const dx = column - originColumn;
202
+ const dy = (row - originRow) * 2;
203
+ return Math.sqrt(dx * dx + dy * dy);
204
+ }
205
+ function ultracodeRippleLevel(distance, travel, rampLength) {
206
+ if (distance > travel) {
207
+ return null;
208
+ }
209
+ const phase = (((distance - travel) % ultracodeWavelength) + ultracodeWavelength) % ultracodeWavelength;
210
+ const brightness = (1 + Math.cos((2 * Math.PI * phase) / ultracodeWavelength)) / 2;
211
+ return Math.min(rampLength - 1, Math.round(brightness * (rampLength - 1)));
212
+ }
213
+ function ultracodeRippleRamp(theme) {
214
+ if (theme === 'violet') {
215
+ return ultracodeVioletRamp;
216
+ }
217
+ const colors = themeContributionColors(theme);
218
+ const start = colors[1] ?? '#1c1c1c';
219
+ const end = themeAccent(theme);
220
+ return Array.from({ length: 8 }, (_, index) => interpolateHex(start, end, index / 7));
221
+ }
222
+ function interpolateHex(startHex, endHex, amount) {
223
+ const start = hexToRgb(startHex);
224
+ const end = hexToRgb(endHex);
225
+ return rgbToHex([
226
+ Math.round(start[0] + (end[0] - start[0]) * amount),
227
+ Math.round(start[1] + (end[1] - start[1]) * amount),
228
+ Math.round(start[2] + (end[2] - start[2]) * amount)
229
+ ]);
230
+ }
231
+ function hexToRgb(hex) {
232
+ const normalized = hex.replace('#', '');
233
+ const expanded = normalized.length === 3
234
+ ? [...normalized].map(value => value + value).join('')
235
+ : normalized.padEnd(6, '0').slice(0, 6);
236
+ const value = Number.parseInt(expanded, 16);
237
+ return [
238
+ (value >> 16) & 255,
239
+ (value >> 8) & 255,
240
+ value & 255
241
+ ];
242
+ }
243
+ function rgbToHex([red, green, blue]) {
244
+ return `#${[red, green, blue].map(value => clampColor(value).toString(16).padStart(2, '0')).join('')}`;
245
+ }
246
+ function clampColor(value) {
247
+ return Math.max(0, Math.min(255, Math.round(value)));
248
+ }
240
249
  function buildMotionBlob(pulse, width, height, theme) {
241
250
  const h = Math.max(7, height);
242
251
  const mid = (h - 1) / 2;
@@ -1876,11 +1885,19 @@ function lineFromCells(cells, fallbackColor) {
1876
1885
  const segments = [];
1877
1886
  for (const cell of cells) {
1878
1887
  const previous = segments[segments.length - 1];
1879
- if (previous && previous.color === cell.color) {
1888
+ if (previous &&
1889
+ previous.color === cell.color &&
1890
+ previous.backgroundColor === cell.backgroundColor &&
1891
+ previous.bold === cell.bold) {
1880
1892
  previous.text += cell.text;
1881
1893
  }
1882
1894
  else {
1883
- segments.push({ text: cell.text, color: cell.color });
1895
+ segments.push({
1896
+ text: cell.text,
1897
+ color: cell.color,
1898
+ backgroundColor: cell.backgroundColor,
1899
+ bold: cell.bold
1900
+ });
1884
1901
  }
1885
1902
  }
1886
1903
  return { text, color: fallbackColor, segments };
@@ -1938,7 +1955,7 @@ function dimMotionColorAt(position, theme) {
1938
1955
  }
1939
1956
  export function visualizerHeight(style, availableRows, width = 80) {
1940
1957
  const maxRowsByStyle = {
1941
- equalizer: 12,
1958
+ ultracode: 14,
1942
1959
  'motion-blob': 12,
1943
1960
  'motion-area': 11,
1944
1961
  'motion-contour': 14,
@@ -1990,7 +2007,7 @@ export function visualizerHeight(style, availableRows, width = 80) {
1990
2007
  newton: 14
1991
2008
  };
1992
2009
  const spaciousStyles = new Set([
1993
- 'equalizer',
2010
+ 'ultracode',
1994
2011
  'motion-blob',
1995
2012
  'motion-area',
1996
2013
  'motion-contour',
@@ -2041,13 +2058,15 @@ export function visualizerHeight(style, availableRows, width = 80) {
2041
2058
  'lava-lamp',
2042
2059
  'newton'
2043
2060
  ]);
2044
- const minRows = style === 'cube' || style === 'motion-contour' || style === 'spinning-donut'
2045
- ? 8
2046
- : spaciousStyles.has(style)
2047
- ? 7
2048
- : style.startsWith('motion-')
2049
- ? 6
2050
- : 3;
2061
+ const minRows = style === 'ultracode'
2062
+ ? 7
2063
+ : style === 'cube' || style === 'motion-contour' || style === 'spinning-donut'
2064
+ ? 8
2065
+ : spaciousStyles.has(style)
2066
+ ? 7
2067
+ : style.startsWith('motion-')
2068
+ ? 6
2069
+ : 3;
2051
2070
  const baseMaxRows = maxRowsByStyle[style] ?? 8;
2052
2071
  const maxRows = responsiveVisualizerMaxRows(style, availableRows, width, baseMaxRows, spaciousStyles);
2053
2072
  return Math.max(minRows, Math.min(maxRows, availableRows));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ciphore/radiocli",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "A terminal-first world radio receiver built with Ink, mpv, and resilient public-radio providers.",
5
5
  "type": "module",
6
6
  "license": "MIT",