<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>John Senneker's blog</title><link>https://john.senneker.ca/</link><description>Recent content on John Senneker's blog</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Fri, 21 Jan 2022 00:00:00 +0000</lastBuildDate><atom:link href="https://john.senneker.ca/index.xml" rel="self" type="application/rss+xml"/><item><title>Squeaking labyrinths</title><link>https://john.senneker.ca/2022/01/21/labysqueak.html</link><pubDate>Fri, 21 Jan 2022 00:00:00 +0000</pubDate><guid>https://john.senneker.ca/2022/01/21/labysqueak.html</guid><description>&lt;script type="text/javascript"&gt;
window.DEFAULT_LABY = {
	d0: 1,
	h: 1.75,
	b: 0.09,
	kmax: 100
};
window.SPEED_OF_SOUND = 343;

window.canvasResizer = function canvasResizer(canvas, aspect, maxHeight) {
	function resize() {
		canvas.height = Math.min(maxHeight, window.innerHeight);
		canvas.width = Math.min(window.innerWidth*0.5, canvas.height*aspect);
	}

	return resize;
};

window.amplitudeDecay = function amplitudeDecay(initial, dist, quarterDist) {
	const denom = (dist/quarterDist + 1.0) * (dist/quarterDist + 1.0);
	return initial / denom;
}

window.drawRoundRect = function drawRoundRect(ctx, x, y, width, height, cornerRadius) {
	ctx.moveTo(x + cornerRadius, y);
	ctx.arcTo(x + width, y, x+width, y+height, cornerRadius);
	ctx.arcTo(x + width, y + height, x, y+height, cornerRadius);
	ctx.arcTo(x, y + height, x, y, cornerRadius);
	ctx.arcTo(x, y, x + width, y, cornerRadius);
};

window.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
window.togglePlay = function togglePlay(button, playfunc) {
	if (button.osc) {
		button.textContent = "Play";
		button.osc.stop();
		button.osc.disconnect();
		button.osc = null;
	} else {
		button.textContent = "Stop";
		button.osc = audioCtx.createOscillator();
		button.osc.connect(audioCtx.destination);
		playfunc(button.osc);
	}
};

window.playSin = function playSin(osc, freq) {
	osc.type = "sine";
	osc.frequency.setValueAtTime(freq, window.audioCtx.currentTime);
	osc.start();
};

window.playSqueak = function playSqueak(osc, laby) {
	laby = laby || window.DEFAULT_LABY;
	osc.type = "sine";
	const freqs = window.getSqueakFreqs(laby, 100);
	const duration = window.getSqueakDuration(laby);
	const ramp = 0.1;
	osc.frequency.setValueCurveAtTime(freqs, window.audioCtx.currentTime + ramp, duration);
	osc.start(window.audioCtx.currentTime + ramp);
	osc.stop(window.audioCtx.currentTime + ramp + duration);
};

window.getSqueakFreqs = function getSqueakFreqs(laby, N) {
	const d0 = laby.d0;
	const h = laby.h;
	const b = laby.b;
	const kmax = laby.kmax;

	D_p = (k) =&gt; d0 + b*k;
	F = (t) =&gt; Math.hypot(window.SPEED_OF_SOUND*t, h) / (2*b*t);
	const t0 = D_p(0) / window.SPEED_OF_SOUND;
	const tmax = D_p(kmax) / window.SPEED_OF_SOUND;
	freqs = new Float32Array(N);
	for (let i = 0; i &lt; N; ++i) {
		const t = t0 + i/(N-1) * (tmax - t0);
		freqs[i] = F(t);
	}

	return freqs;
};

window.getSqueakDuration = function getSqueakDuration(laby) {
	const b = laby.b;
	const kmax = laby.kmax;

	return kmax * 2 * b / window.SPEED_OF_SOUND;
};

window.drawLabyrinth2d = function drawLabyrinth2d(ctx, width, height, laby) {
	const centerX = width/2;
	const centerY = height/2;
	ctx.strokeStyle = 'black';
	for (let k = 0; k &lt;= laby.kmax; ++k) {
		ctx.beginPath();
		ctx.arc(centerX, centerY, laby.d0 + laby.b * k, 0, 2*Math.PI);
		ctx.stroke();
	}
};

window.drawSoundWave2d = function drawSoundWave2d(ctx, wave, angle_start, angle_end) {
	ctx.strokeStyle = `rgba(0, 0, 0, ${wave.amplitude})`;
	ctx.beginPath();
	ctx.arc(wave.originX, wave.originY, wave.dist, angle_start, angle_end);
	ctx.stroke();
};

window.drawClap = function drawClap(ctx, originX, originY, size, alpha) {
	ctx.font = `${size}px`;
	ctx.textAlign = 'center';
	ctx.fillStyle = `rgba(0, 0, 0, ${alpha})`;
	ctx.fillText("\u{1f44f}", originX, originY);
};

window.drawSim2d = function drawSim2d(ctx, width, height, sim) {
	ctx.clearRect(0, 0, width, height);

	const centerX = width/2;
	const centerY = height/2;

	const clap_duration = 2;
	const clap_alpha = 1 - Math.max(0, Math.min(1, sim.t/clap_duration));
	const clap_size = sim.laby.d0/6;
	window.drawClap(ctx, centerX, centerY, clap_size, clap_alpha);

	window.drawLabyrinth2d(ctx, width, height, sim.laby);

	window.drawSoundWave2d(ctx, sim.clap, 0, 2*Math.PI);
	for (let echo of sim.echoes) {
		const angle_start = Math.PI/2 + Math.atan2(echo.originY - centerY, echo.originX - centerX);
		const angle_end = angle_start + Math.PI;
		window.drawSoundWave2d(ctx, echo, angle_start, angle_end);
	}
};

window.stepSim2d = function stepSim2d(sim, dt) {
	const dmax = sim.laby.d0 + sim.laby.b * sim.laby.kmax;
	const new_echoes = [];
	for (let echo of sim.echoes) {
		echo.dist += sim.speedScale*window.SPEED_OF_SOUND * dt;
		echo.amplitude = window.amplitudeDecay(echo.initialAmplitude, echo.dist, dmax);
		if (echo.dist &lt;= Math.hypot(echo.originX - sim.clap.originX, echo.originY - sim.clap.originY)) {
			new_echoes.push(echo);
		}
	}
	const old_clap_dist = sim.clap.dist;
	sim.clap.dist += sim.speedScale*window.SPEED_OF_SOUND * dt;
	sim.clap.amplitude = window.amplitudeDecay(sim.clap.initialAmplitude, sim.clap.dist, dmax);

	const k_old = (old_clap_dist - sim.laby.d0)/sim.laby.b;
	const k_new = (sim.clap.dist - sim.laby.d0)/sim.laby.b;
	const row_crossed = Math.floor(k_new);
	if (row_crossed &lt;= sim.laby.kmax &amp;&amp; Math.floor(k_old) !== Math.floor(k_new)) {
		const k_elapsed = k_new - row_crossed;
		const d_elapsed = sim.laby.b * k_elapsed;
		const dist = sim.laby.d0 + sim.laby.b * row_crossed;
		const num_echoes = 10;
		for (let i = 0; i &lt; num_echoes; ++i) {
			const angle = (i/num_echoes) * 2 * Math.PI
			const x = sim.clap.originX + dist * Math.cos(angle);
			const y = sim.clap.originY + dist * Math.sin(angle);
			new_echoes.push({
				dist: d_elapsed,
				originX: x,
				originY: y,
				initialAmplitude: sim.clap.amplitude,
				amplitude: sim.clap.amplitude
			});
		}
	}
	sim.echoes = new_echoes;
	sim.t += dt;
};

window.playLaby2d = function playLaby2d(canvas, checkCancel, onDone) {
	const ctx = canvas.getContext('2d');
	const width = canvas.width;
	const height = canvas.height;

	const padding = 10;
	const size = Math.min(width, height)/2 - padding;
	const d0 = 0.2 * size;
	const kmax = 4;
	const b = (size - d0) / kmax
	const sim = {
		laby: { d0: d0, b: b, kmax: kmax },
		speedScale: size/1000,
		t: 0,
		clap: {originX: width/2, originY: height/2, dist: 0, initialAmplitude: 1, amplitude: 1},
		echoes: []
	};
	let prevTime = null;
	function go(currTime) {
		if (checkCancel()) {
			ctx.clearRect(0, 0, width, height);
			return;
		} else if (sim.echoes.length === 0 &amp;&amp; sim.clap.dist &gt; d0 + b*kmax) {
			onDone();
			return;
		}
		if (prevTime !== null) {
			const dt = (currTime - prevTime)/1000.0;
			window.stepSim2d(sim, dt);
		}
		window.drawSim2d(ctx, width, height, sim);
		prevTime = currTime;
		window.requestAnimationFrame(go);
	}
	window.requestAnimationFrame(go);
};

window.BRICK_HEIGHT = 5;
window.drawLabyrinth3d = function drawLabyrinth3d(ctx, width, height, laby) {
	const centerX = width/2;
	const centerY = height;
	ctx.fillStyle = 'gray';
	ctx.beginPath();
	for (let k = 0; k &lt;= laby.kmax; ++k) {
		const dist = laby.d0 + laby.b * k;
		const cornerRadius = 2;
		drawRoundRect(ctx, centerX - dist - laby.b, centerY - window.BRICK_HEIGHT, laby.b, window.BRICK_HEIGHT, cornerRadius);
		drawRoundRect(ctx, centerX + dist, centerY - window.BRICK_HEIGHT, laby.b, window.BRICK_HEIGHT, cornerRadius);
	}
	ctx.fill();
};

window.drawSoundWave3d = function drawSoundWave3d(ctx, wave, angle_start, angle_end) {
	ctx.strokeStyle = `rgba(0, 0, 0, ${wave.amplitude})`;
	ctx.beginPath();
	ctx.arc(wave.originX, wave.originY, wave.dist, angle_start, angle_end);
	ctx.stroke();
};

window.drawClap = function drawClap(ctx, originX, originY, size, alpha) {
	ctx.font = `${size}px`;
	ctx.textAlign = 'center';
	ctx.fillStyle = `rgba(0, 0, 0, ${alpha})`;
	ctx.fillText("\u{1f44f}", originX, originY);
};

window.drawSim3d = function drawSim3d(ctx, width, height, sim) {
	ctx.clearRect(0, 0, width, height);

	const clap_duration = 2;
	const clap_alpha = 1 - Math.max(0, Math.min(1, sim.t/clap_duration));
	const clap_size = sim.laby.d0/6;
	window.drawClap(ctx, sim.clap.originX, sim.clap.originY, clap_size, clap_alpha);

	window.drawLabyrinth3d(ctx, width, height, sim.laby);

	window.drawSoundWave3d(ctx, sim.clap, 0, 2*Math.PI);
	for (let echo of sim.echoes) {
		const angle_start = Math.PI/2 + Math.atan2(echo.originY - sim.clap.originY, echo.originX - sim.clap.originX);
		const angle_end = angle_start + Math.PI;
		window.drawSoundWave3d(ctx, echo, angle_start, angle_end);
	}

	function timeX(time) {
		const dmax_par = sim.laby.d0 + sim.laby.b * sim.laby.kmax;
		const dmax = Math.hypot(dmax_par, sim.laby.h);
		const tmax = 2 * dmax / (sim.speedScale * window.SPEED_OF_SOUND);
		const progress = time / tmax;
		return progress * width;
	}

	ctx.beginPath();
	for (let k = 0; k &lt; sim.laby.kmax; ++k) {
		const d_par = sim.laby.d0 + sim.laby.b * k;
		const d = Math.hypot(d_par, sim.laby.h);
		const t = 2 * d / (sim.speedScale * window.SPEED_OF_SOUND);
		if (t &gt; sim.t) break;
		const x = timeX(t);
		ctx.moveTo(x, 0);
		ctx.lineTo(x, 10);
	}
	const x = timeX(sim.t);
	ctx.moveTo(x, 0);
	ctx.lineTo(x, 10);
	ctx.strokeStyle = 'black';
	ctx.stroke();
};

window.stepSim3d = function stepSim3d(sim, dt) {
	const dmax_par = sim.laby.d0 + sim.laby.b * sim.laby.kmax;
	const dmax = Math.sqrt(dmax_par*dmax_par + sim.laby.h*sim.laby.h);
	const new_echoes = [];
	for (let echo of sim.echoes) {
		echo.dist += sim.speedScale*window.SPEED_OF_SOUND * dt;
		echo.amplitude = window.amplitudeDecay(echo.initialAmplitude, echo.dist, dmax);
		if (echo.dist &lt;= Math.hypot(echo.originX - sim.clap.originX, echo.originY - sim.clap.originY)) {
			new_echoes.push(echo);
		}
	}
	const old_clap_dist = sim.clap.dist;
	sim.clap.dist += sim.speedScale*window.SPEED_OF_SOUND * dt;
	sim.clap.amplitude = window.amplitudeDecay(sim.clap.initialAmplitude, sim.clap.dist, dmax);

	if (sim.clap.dist &gt;= Math.hypot(sim.laby.d0, sim.laby.h)) {
		const get_k = (dist) =&gt; (Math.sqrt(dist*dist - sim.laby.h*sim.laby.h) - sim.laby.d0) / sim.laby.b;
		const k_old = get_k(old_clap_dist);
		const k_new = get_k(sim.clap.dist);
		const row_crossed = Math.floor(k_new);
		if (row_crossed &lt;= sim.laby.kmax &amp;&amp; (isNaN(k_old) || Math.floor(k_old) !== Math.floor(k_new))) {
			const dist = sim.laby.d0 + sim.laby.b * row_crossed;
			new_echoes.push({
				dist: 0,
				originX: sim.clap.originX - dist,
				originY: sim.clap.originY + sim.laby.h,
				initialAmplitude: sim.clap.amplitude,
				amplitude: sim.clap.amplitude
			});
			new_echoes.push({
				dist: 0,
				originX: sim.clap.originX + dist,
				originY: sim.clap.originY + sim.laby.h,
				initialAmplitude: sim.clap.amplitude,
				amplitude: sim.clap.amplitude
			});
		}
	}
	sim.echoes = new_echoes;
	sim.t += dt;
};

window.playLaby3d = function playLaby3d(canvas, checkCancel, onDone) {
	const ctx = canvas.getContext('2d');
	const width = canvas.width;
	const height = canvas.height;

	const size = width/2;
	const d0 = 0.2 * size;
	const kmax = 29;
	const b = (size - d0) / (kmax+1)
	const h = 1.5*d0;
	const sim = {
		laby: { d0: d0, b: b, h: h, kmax: kmax },
		speedScale: size/1000,
		t: 0,
		clap: {originX: width/2, originY: height-h-window.BRICK_HEIGHT, dist: 0, initialAmplitude: 1, amplitude: 1},
		echoes: []
	};
	let prevTime = null;
	const dmax_par = d0 + b*kmax;
	const dmax = Math.hypot(dmax_par, h);
	function go(currTime) {
		if (checkCancel()) {
			ctx.clearRect(0, 0, width, height);
			return;
		} else if (sim.echoes.length === 0 &amp;&amp; sim.clap.dist &gt; dmax) {
			onDone();
			return;
		}
		if (prevTime !== null) {
			const dt = (currTime - prevTime)/1000.0;
			window.stepSim3d(sim, dt);
		}
		window.drawSim3d(ctx, width, height, sim);
		prevTime = currTime;
		window.requestAnimationFrame(go);
	}
	window.requestAnimationFrame(go);
};
&lt;/script&gt;
&lt;p&gt;Recently I went to a park that had a brick labyrinth in it. The labyrinth comprised
about 100 concentric rings of bricks. Here&amp;rsquo;s a picture of it taken from above:&lt;/p&gt;</description></item><item><title>About</title><link>https://john.senneker.ca/about/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://john.senneker.ca/about/</guid><description>&lt;p&gt;John Senneker&amp;rsquo;s blog.&lt;/p&gt;</description></item></channel></rss>