A container image pull where each layer downloads with its own progress bar, extracts and completes, ending with a digest.
╭─ ops@runner-3 ─────────────────────────────────────────────────────────────────────────╮ │ $ ctr pull registry.internal/team/ledger-api:2.4.1 │ │ 2.4.1: Pulling from team/ledger-api │ │ a3ed95caeb02: Downloading [========================> ] 24.6MB/31.4MB │ │ 5f70bf18a086: Pull complete │ │ c1d8a4b7e9f3: Downloading [===========> ] 19.1MB/48.2MB │ │ 9e0f3a17bd52: Downloading [==> ] 12.6MB/118.6MB │ │ 2b6c4d8f1a90: Pull complete │ │ d47e2a6b3c18: Downloading [============================> ] 11.6MB/12.7MB │ │ f02a9c3e7b61: Downloading [=======================> ] 4.7MB/6.3MB │ │ 7a1b5e9d0c24: Pulling fs layer │ │ │ │ │ │ pulling 8 layers · 220.5 MB total · 3 concurrent │ │ │ │ │ │ │ │ │ │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯
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="image-layer-pull"></ascii-art>// copy image-layer-pull.js and kit.js into your project, then:
import { useEffect, useRef } from "react";
import create from "./image-layer-pull.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 image-layer-pull<!-- Download GIF below, commit it, then: -->
<p align="center">
<img src="assets/image-layer-pull.gif" alt="Container Layer Pull, 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.
/* image-layer-pull: a container image pull with layers downloading, extracting and completing in parallel. */
import { Grid, palette, rng } from "../lib/kit.js";
export const meta = {
slug: "image-layer-pull",
name: "Container Layer Pull",
category: "screens",
note: "A container image pull where each layer downloads with its own progress bar, extracts and completes, ending with a digest.",
tags: ["container", "terminal", "devops", "download", "loader", "cli"],
cols: 90, rows: 20, fps: 24,
palette: palette(["#e6edf3", "#8b949e", "#7ee787", "#79c0ff", "#ffa657", "#ff7b72", "#d2a8ff", "#484f58", "#30363d", "#f2cc60"]),
};
const LAYERS = [
["a3ed95caeb02", 31.4, 0.2], ["5f70bf18a086", 2.1, 0.3], ["c1d8a4b7e9f3", 48.2, 0.4], ["9e0f3a17bd52", 118.6, 0.5],
["2b6c4d8f1a90", 0.9, 0.6], ["d47e2a6b3c18", 12.7, 0.7], ["f02a9c3e7b61", 6.3, 1.2], ["7a1b5e9d0c24", 0.3, 1.6],
];
const PERIOD = 15;
export default function create() {
const g = new Grid(meta.cols, meta.rows);
const W = meta.cols, H = meta.rows;
const r = rng(3);
const speed = LAYERS.map(() => 9 + r() * 14);
return (t) => {
g.clear();
const lt = t % PERIOD;
g.box(0, 0, W, H, "round", "7");
g.text(2, 0, " ops@runner-3 ", "1");
g.text(2, 1, "$ ", "2"); g.text(4, 1, "ctr pull registry.internal/team/ledger-api:2.4.1".slice(0, Math.floor(lt / 0.02)), "0");
if (lt < 1.0) return g.frame();
g.text(2, 2, "2.4.1: Pulling from team/ledger-api", "0");
let allDone = true, maxEnd = 0;
LAYERS.forEach(([id, mb, start], i) => {
const y = 3 + i;
const dlT = mb / speed[i] + 0.4, exT = 0.3 + mb / 60;
const t0 = 1.2 + start;
const k = lt - t0;
const end = t0 + dlT + exT;
maxEnd = Math.max(maxEnd, end);
g.text(2, y, id + ": ", "4");
let state, c, p = 0;
if (k < 0) { state = "Waiting"; c = "7"; allDone = false; }
else if (k < dlT) { state = "Downloading"; c = "3"; p = k / dlT; allDone = false; }
else if (k < dlT + exT) { state = "Extracting"; c = "9"; p = (k - dlT) / exT; allDone = false; }
else { state = "Pull complete"; c = "2"; }
if (k >= 0 && k < 0.25) { state = "Pulling fs layer"; c = "1"; }
g.text(16, y, state.padEnd(17), c);
if (state === "Downloading" || state === "Extracting") {
const bw = 32, n = Math.floor(p * bw);
g.text(34, y, "[" + "=".repeat(Math.max(0, n - 1)) + ">" + " ".repeat(bw - n) + "]", c);
const fmt = (v) => (v >= 1 ? v.toFixed(1) + "MB" : (v * 1000).toFixed(0) + "kB");
g.text(69, y, `${fmt(p * mb)}/${fmt(mb)}`.padStart(16), "1");
}
});
const y = 4 + LAYERS.length;
if (allDone) {
g.text(2, y, "Digest: ", "0"); g.text(10, y, "sha256:4f9a2c71d0b8e35a6c1f7e90b2d4a8c3e5f60718293a4b5c6d7e8f9012a3b4c5", "1");
g.text(2, y + 1, "Status: ", "0"); g.text(10, y + 1, "Downloaded newer image for registry.internal/team/ledger-api:2.4.1", "2");
if (lt > maxEnd + 0.8) {
g.text(2, y + 3, "$ ", "2"); g.text(4, y + 3, "ctr images ls | head -3", "0");
g.text(2, y + 4, "REPOSITORY".padEnd(36) + "TAG".padEnd(9) + "IMAGE ID".padEnd(15) + "SIZE", "1");
g.text(2, y + 5, "registry.internal/team/ledger-api".padEnd(36) + "2.4.1".padEnd(9) + "4f9a2c71d0b8".padEnd(15) + "221MB", "0");
g.text(2, y + 6, "registry.internal/team/ledger-api".padEnd(36) + "2.4.0".padEnd(9) + "e1b7d0a93c42".padEnd(15) + "219MB", "7");
}
} else {
const tot = LAYERS.reduce((a, l) => a + l[1], 0);
g.text(2, y + 1, `pulling ${LAYERS.length} layers · ${tot.toFixed(1)} MB total · 3 concurrent`, "7");
}
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.