bouncy/render.js

202 lines
5.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const readline = require('readline');
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;
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 = [
{
value: ' ' + displaytitle + ' ',
x: Math.round(w / 2 - displaytitle.length / 2 + Math.sin(time) * 5),
y: Math.floor(h / 2) - 1,
color: '\033[7;1m'
},
{
value: ' ' + displayalbum + ' ',
x: Math.round(w / 2 - displayalbum.length / 2 - Math.sin(time) * 5),
y: Math.floor(h / 2) + 1,
color: '\033[7m'
}
];
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';
texts.push({
value: ` ${symbol} ${errorText} `,
x: 4,
y: Math.round(3 * outCirc(Math.min(errorTimer, 1))),
color: color
});
}
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;