指定した桁数を表示できるHTML・JSコードを公開します。また桁数が多いためバックグラウンド仕様のworker.jsで作成しました。

仕様は桁数の入力・計算開始・表示クリア・印刷となっており、1万桁ならほぼ一瞬で表示、10万桁なら約1分超で表示できるOffscreenCanvas 版です。※CPUスペック・メモリ必須

また、高速スピゴット法・C版(C → WASM → JavaScript)もオプションとしてコードを載せておきますので参考まで。

PI.HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>円周率 =</title>
</head>
<body>

<div style="margin-bottom:10px;">
    桁数:<input id="digitsInput" type="number" value="100">
    <button onclick="start()">描画開始</button>
    <button onclick="clearCanvas()">クリア</button>
    <button onclick="printCanvas()">印刷</button>
</div>

<canvas id="piCanvas" width="800"></canvas>

<script>
// ===============================
// 変数宣言
// ===============================
let digits = 100;
let fontSize = 16;
let x = 10;
let y = 30;
let blockCount = 0;

const canvas = document.getElementById('piCanvas');
const ctx = canvas.getContext('2d');

const worker = new Worker("worker.js");
/*const worker = new Worker("worker.js?v=2");*/
worker.onmessage = function(e) {
    const data = e.data;

    if (data.error) {
        alert(data.error);
        return;
    }

    if (data.done) {
        console.log("計算完了");
        return;
    }

    if (data.batch) {
        // バッチで受け取って一気に描画
        for (const block of data.batch) {
            drawBlock(block);
        }
        return;
    }

    if (data.block) {
        drawBlock(data.block);
    }
};

// ===============================
// 行幅計測
// ===============================
function measureLineWidth() {
    ctx.font = `${fontSize}px monospace`;

    let line = "3.14159 ";

    for (let i = 1; i <= 20; i++) {
        if (i % 2 === 1 && i > 1) line += " ";
        line += "00000";
    }

    return ctx.measureText(line).width;
}

// ===============================
// 描画開始
// ===============================
function start() {
    digits = parseInt(document.getElementById('digitsInput').value, 10);

    const width = measureLineWidth();
    canvas.width = width + 20;

    const lines = Math.ceil(digits / 100);
    canvas.height = (lines + 1) * (fontSize + 10);

    ctx.fillStyle = "#f0f8ff";
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.strokeStyle = "#333";
    ctx.strokeRect(0, 0, canvas.width, canvas.height);
    ctx.fillStyle = "#000";
    ctx.font = `${fontSize}px monospace`;

    x = 10;
    y = fontSize + 10;
    blockCount = 0;

    drawText("3.");

    worker.postMessage({ cmd: "start", value: digits });
}

// ===============================
// ブロック描画
// ===============================
function drawBlock(block) {
    blockCount++;

    if (blockCount % 2 === 1 && blockCount > 1) {
        drawText(" ");
    }

    drawText(block);

    if (blockCount % 20 === 0) {
        x = 10;
        y += fontSize + 10;
    }
}

// ===============================
// テキスト描画
// ===============================
function drawText(text) {
    ctx.fillText(text, x, y);
    x += ctx.measureText(text).width;
}

// ===============================
// クリア
// ===============================
function clearCanvas() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
}

// ===============================
// 印刷(全ブラウザ対応版)
// ===============================
function printCanvas() {
    const dataURL = canvas.toDataURL("image/png");
    const w = window.open("", "_blank");

    w.document.write(`
        <html>
        <head><title>Canvas Print</title></head>
        <body style="margin:0; text-align:center; font-size:18px;">
            <div style="margin:10px 0; font-weight:bold;">
                円周率(表示桁数:${digits} 桁)
            </div>

            <img id="printImg" src="${dataURL}" style="max-width:100%;"/>

            <div style="margin-top:20px; font-size:16px; color:#555;">
                ※ 入力桁数:${digits}
            </div>
        </body>
        </html>
    `);

    w.document.close();

    const waitDomReady = setInterval(() => {
        if (w.document.readyState === "complete") {
            clearInterval(waitDomReady);

            const img = w.document.getElementById("printImg");

            img.onload = () => {
                setTimeout(() => {
                    w.focus();
                    w.print();
                }, 100);
            };

            if (img.complete) {
                setTimeout(() => {
                    w.focus();
                    w.print();
                }, 100);
            }
        }
    }, 50);
}
</script>

</body>
</html>
worker.js
// π 計算 Worker(Spigot法 + OffscreenCanvas描画)

onmessage = async function(e) {
    const digits = e.data.digits;

    // -----------------------------
    // OffscreenCanvas 準備
    // -----------------------------
    const fontSize = 16;

    const blocks = Math.ceil(digits / 5);
    const lines = Math.ceil(blocks / 20);

    const width = 2000;
    const height = (lines + 1) * (fontSize + 10);

    const off = new OffscreenCanvas(width, height);
    const ctx = off.getContext("2d");

    ctx.fillStyle = "#f0f8ff";
    ctx.fillRect(0, 0, width, height);
    ctx.strokeStyle = "#333";
    ctx.strokeRect(0, 0, width, height);
    ctx.fillStyle = "#000";
    ctx.font = `${fontSize}px monospace`;

    let x = 10;
    let y = fontSize + 10;
    let blockCount = 0;

    // -----------------------------
    // π 計算準備
    // -----------------------------
    let base = 100000;
    let a = new Array(5000).fill(0);
    let carry = 0;
    let index = 0;
    let drawnDigits = 0;
    let lastBlock = "";

    // 最初の "3."
    ctx.fillText("3.", x, y);
    x += ctx.measureText("3.").width;

    // -----------------------------
    // π 計算ループ
    // -----------------------------
    while (drawnDigits < digits) {

        let b = 0;

        // --- 前半 ---
        for (let j = index + 17; j > index; j--) {
            b = (20 * base + b) * j;
            a[j] = b % (2 * j + 1);
            b = Math.floor(b / (2 * j + 1));
        }

        // --- 後半 ---
        for (let j = index; j > 0; j -= 17) {
            a[j] += b * j;
            b = 0;

            const jEnd = j - 17;
            for (let k = j; k > jEnd; k--) {
                const l = 2 * k + 1;

                const c = Math.floor(a[k] / l) * base + Math.floor(b / l) * k;
                b = (b % l) * k + (a[k] % l) * base;

                a[k] = b % l;
                b = c + Math.floor(b / l);
            }
        }

        carry = carry * base + b;

        if (index > 0) {
            const block = ('00000000000' + carry).slice(-11, -6);

            if (block === lastBlock) {
                postMessage({ error: "循環小数" });
                return;
            }
            lastBlock = block;

            drawnDigits += 5;

            // -----------------------------
            // Worker 内で描画
            // -----------------------------
            blockCount++;

            if (blockCount % 2 === 1 && blockCount > 1) {
                ctx.fillText(" ", x, y);
                x += ctx.measureText(" ").width;
            }

            ctx.fillText(block, x, y);
            x += ctx.measureText(block).width;

            if (blockCount % 20 === 0) {
                x = 10;
                y += fontSize + 10;
            }

            carry %= (10 * base);
        }

        index += 17;
    }

    // -----------------------------
    // 完成した OffscreenCanvas をメインへ送る
    // -----------------------------
    const blob = await off.convertToBlob();
    postMessage({ blob, width, height });
};

高速スピゴット法・C版

gcc pi_spigot.c -O3 -o pi_spigot (ターミナル)
./pi_spigot 100 # 100桁
./pi_spigot 1000 # 1000桁
./pi_spigot 10000 # 10000桁

pi_spigot.c
#include <stdio.h>
#include <stdlib.h>

#define BASE 100000      // 5桁ブロック用
#define ARR_SIZE 5000    // 配列サイズ(JS版と同程度)

int main(int argc, char *argv[]) {
    int digits = 100;    // デフォルト桁数

    if (argc >= 2) {
        digits = atoi(argv[1]);
        if (digits <= 0) {
            fprintf(stderr, "桁数は正の整数で指定してください。\n");
            return 1;
        }
    }

    int *a = (int *)calloc(ARR_SIZE, sizeof(int));
    if (!a) {
        fprintf(stderr, "メモリ確保に失敗しました。\n");
        return 1;
    }

    long long carry = 0;
    int index = 0;
    int drawnDigits = 0;

    // 先頭の "3."
    printf("3.");

    while (drawnDigits < digits) {
        long long b = 0;

        // --- 前半 ---
        for (int j = index + 17; j > index; j--) {
            b = (20LL * BASE + b) * j;
            a[j] = (int)(b % (2 * j + 1));
            b = b / (2 * j + 1);
        }

        // --- 後半 ---
        for (int j = index; j > 0; j -= 17) {
            a[j] += (int)(b * j);
            b = 0;

            int jEnd = j - 17;
            for (int k = j; k > jEnd; k--) {
                int l = 2 * k + 1;

                long long c = (long long)(a[k] / l) * BASE
                            + (long long)(b / l) * k;
                b = (long long)(b % l) * k
                  + (long long)(a[k] % l) * BASE;

                a[k] = (int)(b % l);
                b = c + b / l;
            }
        }

        carry = carry * BASE + b;

        if (index > 0) {
            // 5桁ブロック抽出
            int blockVal = (int)((carry / BASE) % BASE);

            // 5桁ゼロ埋めで出力
            printf("%05d", blockVal);

            drawnDigits += 5;

            carry = carry % (10LL * BASE);
        }

        index += 17;
    }

    printf("\n");

    free(a);
    return 0;
}

この C コードを WASM にコンパイルしてブラウザから呼び、出力を 5桁ブロック配列として JS に渡す。それを上記の Canvas 描画に流す

C → WASM(Emscripten)ターミナルでEmscripten をインストール

git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh

C のスピゴット法を WASM 用に書く(C 関数)

#include <stdint.h>
#include <stdlib.h>

#define BASE 100000
#define ARR_SIZE 5000

// WASM から呼び出す関数
// digits: 欲しい桁数
// out: 5桁ブロックを格納する配列(JS側で確保)
void compute_pi(int digits, uint32_t *out) {
    int *a = (int *)calloc(ARR_SIZE, sizeof(int));
    long long carry = 0;
    int index = 0;
    int drawn = 0;

    while (drawn < digits) {
        long long b = 0;

        for (int j = index + 17; j > index; j--) {
            b = (20LL * BASE + b) * j;
            a[j] = b % (2 * j + 1);
            b /= (2 * j + 1);
        }

        for (int j = index; j > 0; j -= 17) {
            a[j] += b * j;
            b = 0;

            int jEnd = j - 17;
            for (int k = j; k > jEnd; k--) {
                int l = 2 * k + 1;

                long long c = (a[k] / l) * BASE + (b / l) * k;
                b = (b % l) * k + (a[k] % l) * BASE;

                a[k] = b % l;
                b = c + b / l;
            }
        }

        carry = carry * BASE + b;

        if (index > 0) {
            uint32_t block = (carry / BASE) % BASE;
            out[drawn / 5] = block;
            drawn += 5;
            carry %= (10LL * BASE);
        }

        index += 17;
    }

    free(a);
}

C → WASM にコンパイル

emcc pi.c -O3 \
-s WASM=1 \
-s EXPORTED_FUNCTIONS='["_compute_pi"]' \
-s EXPORTED_RUNTIME_METHODS='["cwrap","getValue"]' \
-o pi.js

pi.js(JavaScript ラッパー)・pi.wasm(高速ネイティブコード)が生成

JavaScript から WASM を呼び出し

import init from "./pi.js";

let Module;

async function loadWasm() {
    Module = await init();

    // C関数 compute_pi を JS から呼び出せるようにする
    const computePi = Module.cwrap("compute_pi", null, ["number", "number"]);

    return computePi;
}

WASM に配列を渡して π を計算

async function runPi(digits) {
    const computePi = await loadWasm();

    const blocks = Math.ceil(digits / 5);

    // WASM メモリに配列を確保
    const ptr = Module._malloc(blocks * 4);

    // π 計算(C側で ptr に書き込まれる)
    computePi(digits, ptr);

    // JS 側に読み出す
    const result = [];
    for (let i = 0; i < blocks; i++) {
        result.push(Module.getValue(ptr + i * 4, "i32"));
    }

    Module._free(ptr);

    return result;
}

OffscreenCanvas に描画(既存コード使用)

const blocks = await runPi(digits);

const off = new OffscreenCanvas(width, height);
const ctx = off.getContext("2d");

ctx.fillText("3.", x, y);

let blockCount = 0;

for (const block of blocks) {
    blockCount++;

    if (blockCount % 2 === 1 && blockCount > 1) {
        ctx.fillText(" ", x, y);
        x += ctx.measureText(" ").width;
    }

    const text = block.toString().padStart(5, "0");
    ctx.fillText(text, x, y);
    x += ctx.measureText(text).width;

    if (blockCount % 20 === 0) {
        x = 10;
        y += fontSize + 10;
    }
}

const blob = await off.convertToBlob();
postMessage({ blob });

このコードで10万桁でも数十秒ぐらいで処理可能です。

ローカル環境でWebサーバを動かしたい場合は、ターミナル等でpython -m http.server 8000を入力 ブラウザでhttp://localhost:8000を実行すると動作確認ができます。

※全てのコードは参考コードなので、コピペしたコードが仮に不具合が生じても管理者は責任を負いません。

デモで動作表示を見てみる※サーバー・CPU過負荷のため1万桁までです