A terminal relay chat client where people join and leave a channel, chat with timestamps and the nick list updates beside them.
#asciiart [7 users] +nt relay.lan:6697 (tls) ──────────────────────────────────────────────────────────────────────────────────────────────── -- Now talking on #asciiart │ @moth -- Topic: text mode forever · faq: see /docs · meetup sat 7pm │ +byteweaver 21:32 <moth> new art pack is up in the files area │ dmz 21:33 <kestrel> the harbour scene is lovely │ kestrel 21:34 <-- lumen has quit (connection reset) │ pixel_ann 21:35 <byteweaver> anyone know a good braille dither ramp? │ tarn 21:36 <moth> byteweaver: 2x4 dots per cell, threshold per dot │ you 21:36 <byteweaver> oh nice, that doubles the resolution │ 21:38 * kestrel stretches │ 21:39 --> dmz (~dmz@10.4.2.9) has joined #asciiart │ 21:41 <dmz> anyone tried the new ascii shaders build? │ 21:41 <kestrel> yeah, the plasma one runs at 60 on my old laptop │ 21:41 --> pixel_ann (~pixel_ann@10.4.2.17) has joined #asciiart │ 21:41 <pixel_ann> hey all o/ │ 21:41 <moth> welcome pixel_ann, topic has the faq link │ 21:41 <byteweaver> dmz: the tunnel one needs the depth buffer patch │ 21:41 * dmz pulls latest and rebuilds │ 21:41 <dmz> much better, thanks │ 21:42 --> tarn (~tarn@10.4.2.17) has joined #asciiart │ │ │ │ │ ──────────────────────────────────────────────────────────────────────────────────────────────── [21:41] [you(+i)] [3:relay/#asciiart(+nt)] [Act: 2] [#asciiart] █
Paste two lines into any page, play it in a terminal, or put a GIF in a README. The code follows the look you pick above.
<script type="module" src="https://www.ascii-magic.com/ascii/ascii.js"></script>
<ascii-art piece="relay-chat-channel"></ascii-art>// copy relay-chat-channel.js and kit.js into your project, then:
import { useEffect, useRef } from "react";
import create from "./relay-chat-channel.js";
export function Piece() {
const ref = useRef(null);
useEffect(() => {
const draw = create(), t0 = performance.now();
let raf;
const tick = () => {
const f = draw((performance.now() - t0) / 1000);
ref.current.textContent = f.text ?? f;
raf = requestAnimationFrame(tick);
};
tick();
return () => cancelAnimationFrame(raf);
}, []);
return <pre ref={ref} style={{ lineHeight: 1, fontFamily: "Menlo, Consolas, monospace" }} />;
}npx --yes https://www.ascii-magic.com/ascii/cli.tgz relay-chat-channel<!-- Download GIF below, commit it, then: -->
<p align="center">
<img src="assets/relay-chat-channel.gif" alt="Relay Chat Channel, animated ASCII art" width="720">
</p>Pause on the frame you like, pick a look, then download it at an exact size.
MIT licensed. It imports one small helper file, kit.js.
/* relay-chat-channel: a terminal relay chat channel with joins, parts, messages, a nick list and an input line. */
import { Grid, palette } from "../lib/kit.js";
export const meta = {
slug: "relay-chat-channel",
name: "Relay Chat Channel",
category: "screens",
note: "A terminal relay chat client where people join and leave a channel, chat with timestamps and the nick list updates beside them.",
tags: ["irc", "chat", "terminal", "retro", "community", "messages"],
cols: 96, rows: 28, fps: 20,
palette: palette(["#e6edf3", "#8b949e", "#7ee787", "#79c0ff", "#ffa657", "#ff7b72", "#d2a8ff", "#484f58", "#30363d", "#f2cc60", "#56d4dd", "#ff9bce"]),
};
const NICKC = { moth: "6", byteweaver: "3", kestrel: "4", "pixel_ann": "b", dmz: "a", tarn: "9", you: "2" };
const BASE = ["@moth", "+byteweaver", "dmz", "kestrel"];
// [dt, kind, nick, text]
const EV = [
[0.0, "msg", "dmz", "anyone tried the new ascii shaders build?"],
[1.0, "msg", "kestrel", "yeah, the plasma one runs at 60 on my old laptop"],
[1.3, "join", "pixel_ann"],
[1.0, "msg", "pixel_ann", "hey all o/"],
[0.9, "msg", "moth", "welcome pixel_ann, topic has the faq link"],
[1.4, "msg", "byteweaver", "dmz: the tunnel one needs the depth buffer patch"],
[1.2, "action", "dmz", "pulls latest and rebuilds"],
[1.3, "msg", "dmz", "much better, thanks"],
[1.0, "join", "tarn"],
[1.0, "msg", "tarn", "is the meetup still on for saturday?"],
[1.2, "msg", "moth", "yes, 7pm at the usual place"],
[1.1, "part", "kestrel", "off to sleep"],
[0.9, "type", "you", "count me in for saturday"],
[2.2, "msg", "you", "count me in for saturday"],
[1.0, "msg", "pixel_ann", "same here :)"],
[3.0, "end"],
];
const HIST = [
[["21:32 ", "7"], ["<", "7"], ["moth", "6"], ["> ", "7"], ["new art pack is up in the files area", "0"]],
[["21:33 ", "7"], ["<", "7"], ["kestrel", "4"], ["> ", "7"], ["the harbour scene is lovely", "0"]],
[["21:34 ", "7"], ["<--", "5"], [" lumen", "1"], [" has quit (connection reset)", "5"]],
[["21:35 ", "7"], ["<", "7"], ["byteweaver", "3"], ["> ", "7"], ["anyone know a good braille dither ramp?", "0"]],
[["21:36 ", "7"], ["<", "7"], ["moth", "6"], ["> ", "7"], ["byteweaver: 2x4 dots per cell, threshold per dot", "0"]],
[["21:36 ", "7"], ["<", "7"], ["byteweaver", "3"], ["> ", "7"], ["oh nice, that doubles the resolution", "0"]],
[["21:38 ", "7"], [" * ", "6"], ["kestrel stretches", "6"]],
[["21:39 ", "7"], ["-->", "2"], [" dmz", "a"], [" (~dmz@10.4.2.9) has joined #asciiart", "2"]],
];
export default function create() {
const g = new Grid(meta.cols, meta.rows);
const W = meta.cols, H = meta.rows;
let acc = 0;
const ev = EV.map((e) => { acc += e[0]; return [acc, ...e.slice(1)]; });
const PERIOD = acc;
const NW = 16, CW = W - NW - 1;
return (t) => {
g.clear();
const lt = t % PERIOD;
const nicks = BASE.slice();
const lines = [
[[" -- ", "7"], ["Now talking on ", "1"], ["#asciiart", "a"]],
[[" -- ", "7"], ["Topic: ", "1"], ["text mode forever · faq: see /docs · meetup sat 7pm", "0"]],
...HIST,
];
let typing = null;
let minute = 41 * 60;
for (const [at, kind, nick, text] of ev) {
if (at > lt || kind === "end") break;
const ts = `21:${String(Math.floor((minute + at * 7) / 60) % 60).padStart(2, "0")}`;
const T = [ts + " ", "7"];
if (kind === "msg") lines.push([T, ["<", "7"], [nick, NICKC[nick]], ["> ", "7"], [text, "0"]]);
else if (kind === "action") lines.push([T, [" * ", "6"], [nick + " " + text, "6"]]);
else if (kind === "join") { lines.push([T, ["-->", "2"], [" " + nick, NICKC[nick]], [" (~" + nick + "@10.4.2.17) has joined #asciiart", "2"]]); nicks.push(nick); }
else if (kind === "part") { lines.push([T, ["<--", "5"], [" " + nick, NICKC[nick]], [` has left #asciiart (${text})`, "5"]]); nicks.splice(nicks.indexOf(nick), 1); }
else if (kind === "type") typing = [at, text];
if (kind === "msg" && nick === "you") typing = null;
}
// title bar
g.text(0, 0, " #asciiart ", "a"); g.text(11, 0, `[${nicks.length + 1} users] +nt`, "1");
g.text(W - 24, 0, "relay.lan:6697 (tls)", "7");
g.hline(0, W - 1, 1, "─", "8");
// messages
const VIS = H - 5;
// wrap long lines like a real client: break at spaces, hang the continuation under the text
const rows = [];
for (const segs of lines) {
const cells = [];
for (const [s, c] of segs) for (const ch of s) cells.push([ch, c]);
const max = CW - 2, ind = 6;
let start = 0, first = true;
while (start < cells.length) {
const w = first ? max : max - ind;
let end = Math.min(cells.length, start + w);
if (end < cells.length) { let b = end; while (b > start + 8 && cells[b][0] !== " ") b--; if (b > start + 8) end = b; }
rows.push([first ? 0 : ind, cells.slice(start, end)]);
start = end; while (start < cells.length && cells[start][0] === " ") start++;
first = false;
}
}
rows.slice(-VIS).forEach(([x0, cells], i) => cells.forEach(([ch, c], k) => g.set(x0 + k, 2 + i, ch, c)));
// nick list
for (let y = 2; y < H - 3; y++) g.set(CW, y, "│", "8");
const all = [...nicks, "you"].sort((a, b) => {
const r = (n) => (n.startsWith("@") ? 0 : n.startsWith("+") ? 1 : 2);
return r(a) - r(b) || a.replace(/[@+]/, "").localeCompare(b.replace(/[@+]/, ""));
});
all.forEach((n, i) => {
const bare = n.replace(/[@+]/, "");
const pre = n[0] === "@" ? "@" : n[0] === "+" ? "+" : " ";
g.set(CW + 2, 2 + i, pre, pre === "@" ? "2" : "9");
g.text(CW + 3, 2 + i, bare, NICKC[bare] || "0");
});
// status + input
g.hline(0, W - 1, H - 3, "─", "8");
g.text(0, H - 2, "[21:4" + (Math.floor(lt / 6) % 10) + "] [you(+i)] [3:relay/#asciiart(+nt)] [Act: 2]", "3");
g.text(0, H - 1, "[#asciiart] ", "a");
if (typing) {
let s = typing[1].slice(0, Math.floor((lt - typing[0]) / 0.07));
if (s.length > W - 14) s = s.slice(s.length - (W - 14)); // input scrolls left
g.text(12, H - 1, s, "0");
if (Math.floor(lt * 3) % 2 === 0) g.set(12 + s.length, H - 1, "█", "0");
} else if (Math.floor(lt * 3) % 2 === 0) g.set(12, H - 1, "█", "0");
return g.frame();
};
}
/YOUR OWN
The pieces here are hand-built animations. To turn your own photo, logo or video into animated ASCII, use the ASCII Magic editor.