266 lines
7.1 KiB
JavaScript
266 lines
7.1 KiB
JavaScript
const readline = require('readline');
|
||
|
||
readline.emitKeypressEvents(process.stdin);
|
||
|
||
if (process.stdin.isTTY) {
|
||
process.stdin.setRawMode(true);
|
||
}
|
||
|
||
const modes = ['classic', 'bottom', 'top'];
|
||
let mode = 0;
|
||
|
||
let modePopup = 0;
|
||
const modePopupDuration = 2;
|
||
|
||
process.stdin.on('keypress', (str, key) => {
|
||
if (key.name === 'space') {
|
||
mode++;
|
||
mode = mode % modes.length;
|
||
modePopup = modePopupDuration;
|
||
}
|
||
if (key.sequence === '\x03' || key.sequence === '\x04') process.exit(0); // ctrl+c, ctrl+d
|
||
});
|
||
|
||
const plas = [' ', '.', '*', '/', '0'];
|
||
const progressChars = ['▏', '▎', '▍', '▌', '▋', '▊', '▉'];
|
||
//const progressChars = ['0', '1', '2', '3', '4', '5', '6', '7'];
|
||
const refreshrate = 50;
|
||
const errorDuration = 4;
|
||
|
||
function outCirc(t) {return Math.sqrt(-t * t + 2 * t)}
|
||
|
||
function plasma(x, y, time, k) {
|
||
let v = 0;
|
||
|
||
let cx = x * k - k / 2;
|
||
let cy = y * k - k / 2;
|
||
|
||
v += Math.sin(cx + time);
|
||
v += Math.sin((cy + time) / 2);
|
||
v += Math.sin((cx + cy + time) / 2);
|
||
|
||
cx = cx + k / 2 * Math.sin(time / 3);
|
||
cy = cy + k / 2 * Math.cos(time / 2);
|
||
|
||
v += Math.sin(Math.sqrt(cx * cx + cy * cy + 1) + time);
|
||
v /= 2;
|
||
|
||
return Math.sin(Math.PI * v) * 0.5 + 0.5;
|
||
}
|
||
|
||
let displaytitle = '';
|
||
let displayalbum = '';
|
||
|
||
let errorText = '';
|
||
let errorTimer = 0;
|
||
let errorType = 0; // 0 = error, 1 = warning, 2 = info
|
||
|
||
function transform(old, n) {
|
||
if (old.length < n.length) {
|
||
// old += ' '.repeat(n.length - old.length);
|
||
old += ' ';
|
||
} else if (old.length > n.length) {
|
||
old = old.slice(0, old.length - 1);
|
||
}
|
||
|
||
let changeable = [];
|
||
for (let i = 0; i < old.length; i++) {
|
||
if (old[i] !== n[i] && n[i] && old[i]) changeable.push(i);
|
||
}
|
||
|
||
let rand = Math.floor(Math.random() * changeable.length);
|
||
let i = changeable[rand];
|
||
if (n[i]) {
|
||
return old.slice(0, i) + n[i] + old.slice(i + 1);
|
||
}
|
||
return old;
|
||
}
|
||
|
||
let time = 0;
|
||
function render(artist, album, title, songStart, songEnd, pauseSpot, paused) {
|
||
let dt = refreshrate / 1000
|
||
time += dt;
|
||
errorTimer -= dt;
|
||
modePopup -= dt;
|
||
let w = process.stdout.columns;
|
||
let h = process.stdout.rows;
|
||
|
||
let newtitle = `${artist.toLowerCase()} - ${title.toLowerCase()}`;
|
||
if (newtitle !== displaytitle) {
|
||
displaytitle = transform(displaytitle, newtitle);
|
||
}
|
||
|
||
let newalbum = `album / ${album.toLowerCase()}`;
|
||
if (newalbum !== displayalbum) {
|
||
displayalbum = transform(displayalbum, newalbum);
|
||
}
|
||
|
||
// leaving the coordinates as non-integer values is very undefined behavior
|
||
let texts = [];
|
||
|
||
switch (mode) {
|
||
case 0:
|
||
texts.push({
|
||
value: ' ' + displaytitle + ' ',
|
||
x: Math.round(w / 2 - displaytitle.length / 2 + Math.sin(time) * 5),
|
||
y: Math.floor(h / 2) - 1,
|
||
color: '\033[7;1m'
|
||
});
|
||
texts.push({
|
||
value: ' ' + displayalbum + ' ',
|
||
x: Math.round(w / 2 - displayalbum.length / 2 - Math.sin(time) * 5),
|
||
y: Math.floor(h / 2) + 1,
|
||
color: '\033[7m'
|
||
});
|
||
break;
|
||
case 1:
|
||
texts.push({
|
||
value: ' ' + displaytitle + ' '.repeat(w),
|
||
x: 0,
|
||
y: h - 3,
|
||
color: '\033[7;1m'
|
||
});
|
||
texts.push({
|
||
value: ' ' + displayalbum + ' '.repeat(w),
|
||
x: 0,
|
||
y: h - 2,
|
||
color: '\033[7m'
|
||
});
|
||
break;
|
||
case 2:
|
||
texts.push({
|
||
value: ' ' + displaytitle + ' '.repeat(w),
|
||
x: 0,
|
||
y: 0,
|
||
color: '\033[7;1m'
|
||
});
|
||
texts.push({
|
||
value: ' ' + displayalbum + ' '.repeat(w),
|
||
x: 0,
|
||
y: 1,
|
||
color: '\033[7m'
|
||
});
|
||
break;
|
||
}
|
||
|
||
if (errorText && errorTimer > 0) {
|
||
let symbol = '⚠️';
|
||
if (errorType === 2) symbol = 'ℹ';
|
||
|
||
let color = '\033[41;37m';
|
||
if (errorType === 1) color = '\033[43;37m';
|
||
if (errorType === 2) color = '\033[44;37m';
|
||
|
||
let text = ` ${symbol} ${errorText} `;
|
||
texts.push({
|
||
value: ' '.repeat(text.length) + text,
|
||
x: -text.length * 2 + Math.round((text.length + 3) * outCirc(Math.min(errorTimer, 1))),
|
||
y: 3,
|
||
color: color
|
||
});
|
||
}
|
||
|
||
if (modePopup > 0) {
|
||
texts.push({
|
||
value: modes[mode],
|
||
x: Math.floor(w - ((modes[mode].length + 2) * outCirc(Math.min(modePopup, 1)))),
|
||
y: 0,
|
||
color: '\033[40;37m'
|
||
});
|
||
}
|
||
|
||
let reset = '\033[0m';
|
||
|
||
let now = paused ? pauseSpot : Date.now();
|
||
|
||
let timeCurrent = now - songStart;
|
||
let timeAll = songEnd - songStart;
|
||
|
||
let timerCurrent = String(Math.floor(timeCurrent / 60000)).padStart(2, '0') + ':' + String(Math.floor((timeCurrent % 60000) / 1000)).padStart(2, '0');
|
||
let timerAll = String(Math.floor(timeAll / 60000)).padStart(2, '0') + ':' + String(Math.floor((timeAll % 60000) / 1000)).padStart(2, '0');
|
||
|
||
timerString = timerCurrent + ' / ' + timerAll;
|
||
|
||
let progress = (now - songStart) / (songEnd - songStart);
|
||
|
||
for (let y = 0; y < h; y++) {
|
||
let renderingtext = false;
|
||
let renderingtextLast = false;
|
||
readline.cursorTo(process.stdout, 0, y);
|
||
process.stdout.write('\x1B[?25l\033[0m'); // hide cursor, reset color
|
||
for (let x = 0; x < w; x++) {
|
||
let sum = '';
|
||
|
||
let isText = false;
|
||
let text;
|
||
for (let t of texts) {
|
||
if (y === t.y && x >= t.x && x < t.x + t.value.length) {
|
||
isText = true;
|
||
text = t;
|
||
}
|
||
}
|
||
|
||
renderingtext = isText;
|
||
|
||
if (renderingtext && !renderingtextLast) {
|
||
sum += text.color;
|
||
} else if (!renderingtext && renderingtextLast) {
|
||
sum += reset;
|
||
}
|
||
|
||
if (text) {
|
||
sum += text.value[x - text.x] || ' ';
|
||
} else {
|
||
if (y === h - 1) {
|
||
let blockProgress = (progress % (1/w)) / (1/w);
|
||
let blocks = progress * w;
|
||
let inProgressArea = (x > Math.floor(blocks) - 0.5) && (x < Math.ceil(blocks) - 0.5);
|
||
let pChar = progressChars[Math.floor(blockProgress * progressChars.length)];
|
||
|
||
let color = (x / (w - 1) > (Math.floor(progress * w) / w)) ? '\033[40;37m' : '\033[47;30m';
|
||
|
||
let pause = (paused && x === (w - 2)) ? '⏸' : null;
|
||
|
||
sum += color + (pause || (timerString[x - 1] === ' ' ? null : timerString[x - 1]) || (inProgressArea ? pChar : ' ')) + '\033[0m';
|
||
} else if (x === 0 || x === w - 1 || y === 0) {
|
||
sum += '\033[40m \033[0m';
|
||
} else {
|
||
let gate = 2.5
|
||
|
||
let l1 = plasma(x, y * 2, time, 4 / Math.min(w, h));
|
||
let l2 = Math.max(plasma(x, y * 2, time * 0.7, 3 / Math.min(w, h)) * gate - (gate - 1), 0);
|
||
|
||
sum += ((l2 > (1 / plas.length)) ? '\x1B[90m' + plas[Math.floor(l2 * plas.length)] : '\033[30m' + plas[Math.floor(l1 * plas.length)]) + '\033[0m';
|
||
|
||
//sum += '\033[30m' + plas[Math.floor(plasma(x, y * 2, time, 4 / Math.min(w, h)) * plas.length)] + '\033[0m';
|
||
}
|
||
}
|
||
|
||
renderingtextLast = renderingtext;
|
||
|
||
process.stdout.write(sum);
|
||
}
|
||
}
|
||
}
|
||
|
||
function error(content) {
|
||
errorText = content;
|
||
errorTimer = errorDuration;
|
||
errorType = 0;
|
||
}
|
||
function warning(content) {
|
||
errorText = content;
|
||
errorTimer = errorDuration;
|
||
errorType = 1;
|
||
}
|
||
function info(content) {
|
||
errorText = content;
|
||
errorTimer = errorDuration;
|
||
errorType = 2;
|
||
}
|
||
|
||
module.exports.render = render;
|
||
module.exports.refreshrate = refreshrate;
|
||
module.exports.error = error;
|
||
module.exports.warning = warning;
|
||
module.exports.info = info; |