import { createEffect, createSignal, onCleanup, onMount } from "solid-js"; import { MathVisualizer } from "./lib/math-visualizer"; type Op = "+" | "-" | "*" | "/"; const DEFAULT_NUMBERS = "27\n59"; class WorkbenchSound { private ctx: AudioContext | null = null; muted = true; private lastTick = 0; private ensureCtx() { if (!this.ctx) { this.ctx = new ( window.AudioContext || (window as typeof window & { webkitAudioContext?: typeof AudioContext }).webkitAudioContext )(); } if (this.ctx.state === "suspended") { void this.ctx.resume(); } } toggleMute() { this.ensureCtx(); this.muted = !this.muted; return this.muted; } private play( type: OscillatorType, fromHz: number, toHz: number, duration: number, gainStart: number ) { if (this.muted) return; this.ensureCtx(); if (!this.ctx) return; const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.connect(gain); gain.connect(this.ctx.destination); osc.type = type; osc.frequency.setValueAtTime(fromHz, this.ctx.currentTime); if (toHz > fromHz) { osc.frequency.exponentialRampToValueAtTime(toHz, this.ctx.currentTime + duration); } else { osc.frequency.linearRampToValueAtTime(toHz, this.ctx.currentTime + duration); } gain.gain.setValueAtTime(gainStart, this.ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + duration); osc.start(); osc.stop(this.ctx.currentTime + duration + 0.01); } playTick() { if (!this.ctx) return; const now = this.ctx.currentTime; if (now - this.lastTick < 0.04) return; this.lastTick = now; this.play("sine", 520, 220, 0.07, 0.07); } playGenerate() { this.play("triangle", 200, 420, 0.22, 0.11); } playTogglePlay(playing: boolean) { if (playing) { this.play("triangle", 260, 600, 0.15, 0.09); } else { this.play("sawtooth", 440, 220, 0.12, 0.06); } } playError() { this.play("square", 180, 120, 0.16, 0.08); } } function VerticalArithmeticWorkbench() { let rootRef!: HTMLElement; let canvasRef!: HTMLCanvasElement; let canvasWrapRef!: HTMLDivElement; let visualizer: MathVisualizer | undefined; const [operation, setOperation] = createSignal("+"); const [numbersInput, setNumbersInput] = createSignal(DEFAULT_NUMBERS); const [message, setMessage] = createSignal("Enter numbers and click Generate"); const [isError, setIsError] = createSignal(false); const [stepCurrent, setStepCurrent] = createSignal(0); const [stepTotal, setStepTotal] = createSignal(0); const [playing, setPlaying] = createSignal(false); const [speed, setSpeed] = createSignal(800); const [soundMuted, setSoundMuted] = createSignal(true); const sound = new WorkbenchSound(); let lastStep = 0; let lastPlaying = false; const progress = () => stepTotal() > 0 ? (stepCurrent() / stepTotal()) * 100 : 0; onMount(() => { visualizer = new MathVisualizer({ canvas: canvasRef, wrapper: canvasWrapRef, themeRoot: rootRef, }); visualizer.onMessage = (msg, err) => { setMessage(msg); setIsError(!!err); if (err) sound.playError(); }; visualizer.onStep = (current, total) => { setStepCurrent(current); setStepTotal(total); if (current !== lastStep) { sound.playTick(); } lastStep = current; }; visualizer.onPlayState = (nextPlaying) => { setPlaying(nextPlaying); if (nextPlaying !== lastPlaying) { sound.playTogglePlay(nextPlaying); } lastPlaying = nextPlaying; }; const resize = () => visualizer?.resizeCanvas(); resize(); const ro = new ResizeObserver(resize); ro.observe(canvasWrapRef); window.addEventListener("resize", resize); const themeObserver = new MutationObserver(() => { visualizer?.refreshColors(); visualizer?.render(); }); themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"], }); onCleanup(() => { ro.disconnect(); window.removeEventListener("resize", resize); themeObserver.disconnect(); visualizer?.destroy(); }); setTimeout(() => generate(), 100); }); createEffect(() => { visualizer?.setOperation(operation()); visualizer?.setSpeed(speed()); }); const generate = () => { visualizer?.setOperation(operation()); visualizer?.generateFromInput(numbersInput()); sound.playGenerate(); }; return (

Carry & borrow arithmetic visualizer