1998年に Java アプレットで公開していたデモ7本を、
ブラウザでそのまま動く JavaScript に書き直しました。
頂点の座標も、回転角も、セマフォの初期値も、当時の .java と同じ値です。
下の絵はすべていま動いています。
.java のソースはそのまま残してあります。
この作り直しでいちばん大事なのは、見た目が同じでも中身が違うことです。
オブジェクトごとに Thread を立て、OS が本当に同時に走らせていました。
いつ切り替わるかは分からないので、共有するものにはセマフォが要ります。
実行の線は1本だけです。タイマーを6本仕掛けても、順番に呼ばれているだけ。 それでも「代わりばんこ」が十分速ければ、同時に動いて見えます。
1本しかないので、途中で割り込まれて壊れるという競合状態は起きません。 デモ⑤⑥のセマフォは、当時の考え方をなぞって見せるためのものです。
言語ごとの違いは Java 版・ JavaScript 版・ Python 版の講義資料で扱っています。
簡単なクラスを用いて 3D オブジェクトを生成する。断面の10点を Y 軸まわりに 36°ずつ回して回転体を作り、X・Y・Z の各軸まわりに 12°ずつ回し続ける。
元のアプレットは Thread を1本立て、80 ミリ秒ごとに Rotate() と repaint() を呼んでいた。ここでも同じ 80 ミリ秒で回している。
function FreeSoR(w, h, n) {
this.w = w; this.h = h; this.n = n;
this.x = []; this.y = []; this.z = [];
for (var i = 0; i < 10; i++) { this.x.push(new Array(10)); this.y.push(new Array(10)); this.z.push(new Array(10)); }
for (var j = 0; j < 10; j++) { this.x[0][j] = XX[j]; this.y[0][j] = YY[j]; this.z[0][j] = 0; }
// 断面を Y 軸まわりに delta ずつ回して回転体にする
for (var i2 = 0; i2 < 9; i2++) {
for (var j2 = 0; j2 < 10; j2++) {
var x1 = this.x[i2][j2], z1 = this.z[i2][j2];
this.x[i2 + 1][j2] = x1 * Math.cos(DELTA) + z1 * Math.sin(DELTA);
this.y[i2 + 1][j2] = this.y[i2][j2];
this.z[i2 + 1][j2] = -x1 * Math.sin(DELTA) + z1 * Math.cos(DELTA);
}
}
this.turns = 0;
}
FreeSoR.prototype.rotate = function () {
for (var i = 0; i < 10; i++) {
for (var j = 0; j < 10; j++) {
var x1 = this.x[i][j], y1 = this.y[i][j], z1 = this.z[i][j];
var x2 = x1;
var y2 = y1 * Math.cos(ALPHA) - z1 * Math.sin(ALPHA);
var z2 = y1 * Math.sin(ALPHA) + z1 * Math.cos(ALPHA);
var x3 = x2 * Math.cos(BETA) + z2 * Math.sin(BETA);
var y3 = y2;
this.x[i][j] = x3 * Math.cos(GAMMA) - y3 * Math.sin(GAMMA);
this.y[i][j] = x3 * Math.sin(GAMMA) + y3 * Math.cos(GAMMA);
this.z[i][j] = -x2 * Math.sin(BETA) + z2 * Math.cos(BETA);
}
}
this.turns++;
};
FreeSoR.prototype.draw = function (g) {
var hcell = Math.floor(this.n / 3), vcell = this.n % 3;
var wu = this.w / 3, hu = this.h / 4;
var ox = (hcell + 1) * wu, oy = (vcell + 1) * hu;
g.beginPath();
for (var i = 0; i < 10; i++) {
for (var j = 0; j < 10; j++) {
g.moveTo(this.x[i][j] / 3 + ox, this.y[i][j] / 3 + oy);
g.lineTo(this.x[(i + 1) % 10][j] / 3 + ox, this.y[(i + 1) % 10][j] / 3 + oy);
}
}
for (var i2 = 0; i2 < 10; i2++) {
for (var j2 = 0; j2 < 9; j2++) {
g.moveTo(this.x[i2][j2] / 3 + ox, this.y[i2][j2] / 3 + oy);
g.lineTo(this.x[i2][(j2 + 1) % 10] / 3 + ox, this.y[i2][(j2 + 1) % 10] / 3 + oy);
}
}
g.stroke();
};
// ── 共通の下ごしらえ ────────────────────────────────
/* 元のアプレットは 400x400(有限バッファだけ 500x250)。
図形の大きさはこの寸法を前提に決め打ちされているので、
**論理座標をその寸法に固定し、画面に合わせて拡大縮小する**。
canvas を CSS で伸ばすだけにすると端末ごとに比が狂う(実測して px を入れる)。 */
function setup(canvas, LW, LH) {
var dpr = Math.min(window.devicePixelRatio || 1, 2);
var avail = canvas.parentNode.clientWidth || LW;
var dispW = Math.min(LW, Math.max(240, avail));
var dispH = Math.round(dispW * LH / LW);
canvas.style.width = dispW + 'px';
canvas.style.height = dispH + 'px';
canvas.width = Math.round(dispW * dpr);
canvas.height = Math.round(dispH * dpr);
var g = canvas.getContext('2d');
var k = dispW * dpr / LW; // 論理座標 → 実ピクセル
g.setTransform(k, 0, 0, k, 0, 0);
return { g: g, w: LW, h: LH, k: k, dispW: dispW, dispH: dispH };
}
// ── デモ1・2: 一つのオブジェクト(ダブルバッファの有無)──────
function single(box, doubleBuffered) {
var canvas = box.querySelector('canvas');
var s = setup(canvas, 400, 400);
var obj = new FreeSoR(s.w, s.h, 0);
var off = document.createElement('canvas');
off.width = canvas.width; off.height = canvas.height;
var og = off.getContext('2d');
og.setTransform(s.k, 0, 0, s.k, 0, 0);
var turns = box.querySelector('[data-turns]');
var paused = false;
setInterval(function () { if (!paused && running(canvas)) obj.rotate(); }, 80); // Sample.java と同じ 80ms
function render(force) {
if (!force && !running(canvas)) return;
if (doubleBuffered) {
clear(og, s.w, s.h); obj.draw(og);
s.g.clearRect(0, 0, s.w, s.h);
s.g.drawImage(off, 0, 0, s.w, s.h);
} else {
// わざと「消してから描く」を分ける。間に画面が出ると、ちらついて見える。
clear(s.g, s.w, s.h);
obj.draw(s.g);
}
if (turns) turns.textContent = obj.turns;
}
render(true); // 到着した瞬間に空にならないよう、まず1枚描く
setInterval(render, 80);
var b = box.querySelector('[data-pause]');
function pauseLabel() {
if (b) b.textContent = paused ? T('▶ 再開', '▶ Resume') : T('⏸ 止める', '⏸ Pause');
}
if (b) b.addEventListener('click', function () { paused = !paused; pauseLabel(); });
pauseLabel();
redraws.push(function () { pauseLabel(); render(true); });
}
1998年の版: Sample.html (アプレットなので今のブラウザでは動きません) / 当時の Java ソース: Sample.java FreeSoR.java
次は、ダブルバッファを用いて処理をする。
画面に直接描くと、消してから描き終わるまでの一瞬が見えてしまい、ちらつく。見えない紙に描いてから一気に貼り替えるのがダブルバッファ。いまのブラウザの canvas はもともと二重に持っているので、上のデモとの差は分かりにくい ── そこが1998年との違いでもある。
// ── デモ1・2: 一つのオブジェクト(ダブルバッファの有無)──────
function single(box, doubleBuffered) {
var canvas = box.querySelector('canvas');
var s = setup(canvas, 400, 400);
var obj = new FreeSoR(s.w, s.h, 0);
var off = document.createElement('canvas');
off.width = canvas.width; off.height = canvas.height;
var og = off.getContext('2d');
og.setTransform(s.k, 0, 0, s.k, 0, 0);
var turns = box.querySelector('[data-turns]');
var paused = false;
setInterval(function () { if (!paused && running(canvas)) obj.rotate(); }, 80); // Sample.java と同じ 80ms
function render(force) {
if (!force && !running(canvas)) return;
if (doubleBuffered) {
clear(og, s.w, s.h); obj.draw(og);
s.g.clearRect(0, 0, s.w, s.h);
s.g.drawImage(off, 0, 0, s.w, s.h);
} else {
// わざと「消してから描く」を分ける。間に画面が出ると、ちらついて見える。
clear(s.g, s.w, s.h);
obj.draw(s.g);
}
if (turns) turns.textContent = obj.turns;
}
render(true); // 到着した瞬間に空にならないよう、まず1枚描く
setInterval(render, 80);
var b = box.querySelector('[data-pause]');
function pauseLabel() {
if (b) b.textContent = paused ? T('▶ 再開', '▶ Resume') : T('⏸ 止める', '⏸ Pause');
}
if (b) b.addEventListener('click', function () { paused = !paused; pauseLabel(); });
pauseLabel();
redraws.push(function () { pauseLabel(); render(true); });
}
1998年の版: Sample05.html (アプレットなので今のブラウザでは動きません) / 当時の Java ソース: Sample05.java
これら 3D オブジェクトは、並行に処理される。ここでは、六個のオブジェクトを生成する。
元は TR のスレッドを6本立て、それぞれが 40 ミリ秒ごとに勝手に回っていた。描く係は別に1本。Java は本当に同時に走っていたが、JavaScript は1本しかないので、6個ぶんのタイマーを順番に処理している。見え方は似ていても、中身は「代わりばんこ」である。
// ── デモ3・4: 六つのオブジェクト ────────────────────────
function many(box, doubleBuffered) {
var canvas = box.querySelector('canvas');
var s = setup(canvas, 400, 400);
var objs = [];
for (var i = 0; i < 6; i++) objs.push(new FreeSoR(s.w, s.h, i));
var off = document.createElement('canvas');
off.width = canvas.width; off.height = canvas.height;
var og = off.getContext('2d');
og.setTransform(s.k, 0, 0, s.k, 0, 0);
var paused = false;
// TR.java は 40ms ごとに Rotate。6本ぶん別々のタイマーにする=別スレッドのつもり
objs.forEach(function (o, k) {
setInterval(function () { if (!paused && running(canvas)) o.rotate(); }, 40);
});
function render(force) {
if (!force && !running(canvas)) return;
var g = doubleBuffered ? og : s.g;
clear(g, s.w, s.h);
objs.forEach(function (o) { o.draw(g); });
if (doubleBuffered) {
s.g.clearRect(0, 0, s.w, s.h);
s.g.drawImage(off, 0, 0, s.w, s.h);
}
}
render(true);
setInterval(render, 80);
var b = box.querySelector('[data-pause]');
function pauseLabel() {
if (b) b.textContent = paused ? T('▶ 再開', '▶ Resume') : T('⏸ 止める', '⏸ Pause');
}
if (b) b.addEventListener('click', function () { paused = !paused; pauseLabel(); });
pauseLabel();
redraws.push(function () { pauseLabel(); render(true); });
}
1998年の版: Sample1.html (アプレットなので今のブラウザでは動きません) / 当時の Java ソース: Sample1.java TR.java
次は、ダブルバッファを用いて処理をする。
6個ぶんを見えない紙にまとめて描いてから貼り替える。描く量が増えるほど、ダブルバッファの効き目は大きくなる。
// ── デモ3・4: 六つのオブジェクト ────────────────────────
function many(box, doubleBuffered) {
var canvas = box.querySelector('canvas');
var s = setup(canvas, 400, 400);
var objs = [];
for (var i = 0; i < 6; i++) objs.push(new FreeSoR(s.w, s.h, i));
var off = document.createElement('canvas');
off.width = canvas.width; off.height = canvas.height;
var og = off.getContext('2d');
og.setTransform(s.k, 0, 0, s.k, 0, 0);
var paused = false;
// TR.java は 40ms ごとに Rotate。6本ぶん別々のタイマーにする=別スレッドのつもり
objs.forEach(function (o, k) {
setInterval(function () { if (!paused && running(canvas)) o.rotate(); }, 40);
});
function render(force) {
if (!force && !running(canvas)) return;
var g = doubleBuffered ? og : s.g;
clear(g, s.w, s.h);
objs.forEach(function (o) { o.draw(g); });
if (doubleBuffered) {
s.g.clearRect(0, 0, s.w, s.h);
s.g.drawImage(off, 0, 0, s.w, s.h);
}
}
render(true);
setInterval(render, 80);
var b = box.querySelector('[data-pause]');
function pauseLabel() {
if (b) b.textContent = paused ? T('▶ 再開', '▶ Resume') : T('⏸ 止める', '⏸ Pause');
}
if (b) b.addEventListener('click', function () { paused = !paused; pauseLabel(); });
pauseLabel();
redraws.push(function () { pauseLabel(); render(true); });
}
1998年の版: Sample15.html (アプレットなので今のブラウザでは動きません) / 当時の Java ソース: Sample15.java
セマフォを使って、同期・非同期を切替える。さらに、ダブルバッファの機能は追加してある。
6個のオブジェクトを輪にして、セマフォの札を1枚だけ回している(0番だけが初期値 1 を持つ)。札を受け取ったものだけが1回まわり、次へ渡す。ボタンで Async にすると札を無視して全員が回りはじめる。
// ── デモ5: 同期 vs 非同期(リング状のセマフォ)───────────────
function syncDemo(box) {
var canvas = box.querySelector('canvas');
var s = setup(canvas, 400, 400);
var objs = [], sem = [];
for (var i = 0; i < 6; i++) {
objs.push(new FreeSoR(s.w, s.h, i));
sem.push(i === 0 ? 1 : 0); // Sample2.java と同じ: 0番だけ 1 を持つ
}
var sync = true;
var off = document.createElement('canvas');
off.width = canvas.width; off.height = canvas.height;
var og = off.getContext('2d');
og.setTransform(s.k, 0, 0, s.k, 0, 0);
objs.forEach(function (o, k) {
setInterval(function () {
if (!running(canvas)) return;
if (!sync) { o.rotate(); return; }
if (sem[k] > 0) { // Wait(自分) → 回す → Signal(次)
sem[k]--;
o.rotate();
sem[(k + 1) % 6]++;
}
}, 40);
});
function render(force) {
if (!force && !running(canvas)) return;
clear(og, s.w, s.h);
objs.forEach(function (o) { o.draw(og); });
// セマフォの値を各オブジェクトの脇に出す
og.fillStyle = '#003f8c';
og.font = '600 13px ui-monospace, monospace';
objs.forEach(function (o, k) {
var hcell = Math.floor(k / 3), vcell = k % 3;
og.fillText('S=' + sem[k], (hcell + 1) * (s.w / 3) - 34, (vcell + 1) * (s.h / 4) - 26);
});
s.g.clearRect(0, 0, s.w, s.h);
s.g.drawImage(off, 0, 0, s.w, s.h);
}
render(true);
setInterval(render, 80);
var b = box.querySelector('[data-sync]');
var note = box.querySelector('[data-note]');
function label() {
b.textContent = sync ? T('Sync(順番に)', 'Sync (one at a time)')
: T('Async(勝手に)', 'Async (all at once)');
b.className = 'demo-btn' + (sync ? '' : ' alt');
if (note) note.textContent = sync
? T('順番待ちの札(セマフォ)が1枚だけ輪を回っています。札を持つ1個しか回れません。',
'A single token (a semaphore) travels around the ring. Only the one holding it may turn.')
: T('札を無視して全員が回っています。誰がどこまで回ったかは揃いません。',
'The token is ignored and everyone turns freely, so they drift out of step.');
}
b.addEventListener('click', function () { sync = !sync; label(); });
label();
redraws.push(function () { label(); render(true); });
}
1998年の版: Sample2.html (アプレットなので今のブラウザでは動きません) / 当時の Java ソース: Sample2.java
有限バッファ問題を表現する。作る側と使う側が、10個ぶんの置き場を共有する。
置き場の空き数を数えるセマフォ(S= 緑)と、中身の数を数えるセマフォ(S= 橙)の2つで待ち合わせる。作る方を速くすると置き場が埋まって作る側が待ち、使う方を速くすると空になって使う側が待つ。元のアプレットのスクロールバーと同じように、下の2本で速さを変えられる。
// ── デモ6・7: 有限バッファ(生産者・消費者)────────────────
function bounded(box, useDot) {
var canvas = box.querySelector('canvas');
var s = setup(canvas, 500, 250);
var N = 10;
var values = new Array(N).fill(null);
var inputp = 0, outputp = 0;
var isem = N, osem = 0; // Sample3.java と同じ初期値
var made = 0, taken = 0, seq = 0;
var pWait = false, cWait = false;
var pIv = box.querySelector('[data-p]'), cIv = box.querySelector('[data-c]');
var pOut = box.querySelector('[data-pv]'), cOut = box.querySelector('[data-cv]');
function produce() {
if (isem > 0) { // Wait(isem)
isem--;
values[inputp] = useDot ? '●' : (seq % 100);
seq++; inputp = (inputp + 1) % N; made++;
osem++; pWait = false; // Signal(osem)
} else pWait = true;
}
function consume() {
if (osem > 0) { // Wait(osem)
osem--;
values[outputp] = null;
outputp = (outputp + 1) % N; taken++;
isem++; cWait = false; // Signal(isem)
} else cWait = true;
}
var pt = null, ct = null;
function retime() {
if (pt) clearInterval(pt);
if (ct) clearInterval(ct);
var pv = +pIv.value, cv = +cIv.value;
pOut.textContent = pv + ' ms';
cOut.textContent = cv + ' ms';
pt = setInterval(function () { if (running(canvas)) produce(); }, pv);
ct = setInterval(function () { if (running(canvas)) consume(); }, cv);
}
pIv.addEventListener('input', retime);
cIv.addEventListener('input', retime);
retime();
function render(force) {
if (!force && !running(canvas)) return;
var g = s.g;
clear(g, s.w, s.h);
var bw = Math.min(44, (s.w - 40) / N), left = (s.w - bw * N) / 2, top = s.h / 2 - bw / 2;
for (var i = 0; i < N; i++) {
var x = left + i * bw;
g.strokeStyle = '#c9d6ea';
g.strokeRect(x, top, bw, bw);
if (values[i] !== null) {
g.fillStyle = '#e6eef9';
g.fillRect(x + 1, top + 1, bw - 2, bw - 2);
g.fillStyle = '#003f8c';
g.font = '600 14px ui-monospace, monospace';
g.textAlign = 'center'; g.textBaseline = 'middle';
g.fillText(String(values[i]), x + bw / 2, top + bw / 2);
}
}
// 入れる側・取り出す側の位置とセマフォの値(元の Pointer と同じ考え方)
function arrow(idx, y, color, label, up) {
var x = left + idx * bw + bw / 2;
g.strokeStyle = color; g.fillStyle = color; g.lineWidth = 2;
g.beginPath();
if (up) { g.moveTo(x, y); g.lineTo(x - 7, y - 11); g.lineTo(x + 7, y - 11); }
else { g.moveTo(x, y); g.lineTo(x - 7, y + 11); g.lineTo(x + 7, y + 11); }
g.closePath(); g.fill();
g.font = '600 12px ui-monospace, monospace';
g.textAlign = 'center'; g.textBaseline = up ? 'bottom' : 'top';
g.fillText(label, x, up ? y - 14 : y + 14);
g.lineWidth = 1;
}
arrow(inputp, top - 6, '#0d7a4f',
T('入れる', 'put') + ' S=' + isem + (pWait ? T('(待ち)', ' (waiting)') : ''), true);
arrow(outputp, top + bw + 6, '#d97200',
T('取り出す', 'take') + ' S=' + osem + (cWait ? T('(待ち)', ' (waiting)') : ''), false);
g.textAlign = 'left'; g.textBaseline = 'alphabetic';
g.fillStyle = '#7c89a3';
g.font = '12px ui-monospace, monospace';
g.fillText(T('入れた ' + made + ' 個 / 取り出した ' + taken + ' 個',
'put ' + made + ' / taken ' + taken), 10, s.h - 10);
}
render(true);
setInterval(render, 60);
redraws.push(function () { render(true); });
}
1998年の版: Sample3.html (アプレットなので今のブラウザでは動きません) / 当時の Java ソース: Sample3.java
次に入力として●を使う。
入れるものが数字でも●でも、待ち合わせの仕組みは変わらない。何を運ぶかと、どう待ち合わせるかは別の話だと分かる。
// ── デモ6・7: 有限バッファ(生産者・消費者)────────────────
function bounded(box, useDot) {
var canvas = box.querySelector('canvas');
var s = setup(canvas, 500, 250);
var N = 10;
var values = new Array(N).fill(null);
var inputp = 0, outputp = 0;
var isem = N, osem = 0; // Sample3.java と同じ初期値
var made = 0, taken = 0, seq = 0;
var pWait = false, cWait = false;
var pIv = box.querySelector('[data-p]'), cIv = box.querySelector('[data-c]');
var pOut = box.querySelector('[data-pv]'), cOut = box.querySelector('[data-cv]');
function produce() {
if (isem > 0) { // Wait(isem)
isem--;
values[inputp] = useDot ? '●' : (seq % 100);
seq++; inputp = (inputp + 1) % N; made++;
osem++; pWait = false; // Signal(osem)
} else pWait = true;
}
function consume() {
if (osem > 0) { // Wait(osem)
osem--;
values[outputp] = null;
outputp = (outputp + 1) % N; taken++;
isem++; cWait = false; // Signal(isem)
} else cWait = true;
}
var pt = null, ct = null;
function retime() {
if (pt) clearInterval(pt);
if (ct) clearInterval(ct);
var pv = +pIv.value, cv = +cIv.value;
pOut.textContent = pv + ' ms';
cOut.textContent = cv + ' ms';
pt = setInterval(function () { if (running(canvas)) produce(); }, pv);
ct = setInterval(function () { if (running(canvas)) consume(); }, cv);
}
pIv.addEventListener('input', retime);
cIv.addEventListener('input', retime);
retime();
function render(force) {
if (!force && !running(canvas)) return;
var g = s.g;
clear(g, s.w, s.h);
var bw = Math.min(44, (s.w - 40) / N), left = (s.w - bw * N) / 2, top = s.h / 2 - bw / 2;
for (var i = 0; i < N; i++) {
var x = left + i * bw;
g.strokeStyle = '#c9d6ea';
g.strokeRect(x, top, bw, bw);
if (values[i] !== null) {
g.fillStyle = '#e6eef9';
g.fillRect(x + 1, top + 1, bw - 2, bw - 2);
g.fillStyle = '#003f8c';
g.font = '600 14px ui-monospace, monospace';
g.textAlign = 'center'; g.textBaseline = 'middle';
g.fillText(String(values[i]), x + bw / 2, top + bw / 2);
}
}
// 入れる側・取り出す側の位置とセマフォの値(元の Pointer と同じ考え方)
function arrow(idx, y, color, label, up) {
var x = left + idx * bw + bw / 2;
g.strokeStyle = color; g.fillStyle = color; g.lineWidth = 2;
g.beginPath();
if (up) { g.moveTo(x, y); g.lineTo(x - 7, y - 11); g.lineTo(x + 7, y - 11); }
else { g.moveTo(x, y); g.lineTo(x - 7, y + 11); g.lineTo(x + 7, y + 11); }
g.closePath(); g.fill();
g.font = '600 12px ui-monospace, monospace';
g.textAlign = 'center'; g.textBaseline = up ? 'bottom' : 'top';
g.fillText(label, x, up ? y - 14 : y + 14);
g.lineWidth = 1;
}
arrow(inputp, top - 6, '#0d7a4f',
T('入れる', 'put') + ' S=' + isem + (pWait ? T('(待ち)', ' (waiting)') : ''), true);
arrow(outputp, top + bw + 6, '#d97200',
T('取り出す', 'take') + ' S=' + osem + (cWait ? T('(待ち)', ' (waiting)') : ''), false);
g.textAlign = 'left'; g.textBaseline = 'alphabetic';
g.fillStyle = '#7c89a3';
g.font = '12px ui-monospace, monospace';
g.fillText(T('入れた ' + made + ' 個 / 取り出した ' + taken + ' 個',
'put ' + made + ' / taken ' + taken), 10, s.h - 10);
}
render(true);
setInterval(render, 60);
redraws.push(function () { render(true); });
}
1998年の版: Sample4.html (アプレットなので今のブラウザでは動きません) / 当時の Java ソース: Sample4.java