diff --git a/README.md b/README.md index 1006fe7..7b513a1 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Open Free Books +[![DeepWiki](https://img.shields.io/badge/DeepWiki-HananoshikaYomaru%2Fopenfreebooks-blue.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAyCAYAAAAnWDnqAAAAAXNSR0IArs4c6QAAA05JREFUaEPtmUtyEzEQhtWTQyQLHNak2AB7ZnyXZMEjXMGeK/AIi+QuHrMnbChYY7MIh8g01fJoopFb0uhhEqqcbWTp06/uv1saEDv4O3n3dV60RfP947Mm9/SQc0ICFQgzfc4CYZoTPAswgSJCCUJUnAAoRHOAUOcATwbmVLWdGoH//PB8mnKqScAhsD0kYP3j/Yt5LPQe2KvcXmGvRHcDnpxfL2zOYJ1mFwrryWTz0advv1Ut4CJgf5uhDuDj5eUcAUoahrdY/56ebRWeraTjMt/00Sh3UDtjgHtQNHwcRGOC98BJEAEymycmYcWwOprTgcB6VZ5JK5TAJ+fXGLBm3FDAmn6oPPjR4rKCAoJCal2eAiQp2x0vxTPB3ALO2CRkwmDy5WohzBDwSEFKRwPbknEggCPB/imwrycgxX2NzoMCHhPkDwqYMr9tRcP5qNrMZHkVnOjRMWwLCcr8ohBVb1OMjxLwGCvjTikrsBOiA6fNyCrm8V1rP93iVPpwaE+gO0SsWmPiXB+jikdf6SizrT5qKasx5j8ABbHpFTx+vFXp9EnYQmLx02h1QTTrl6eDqxLnGjporxl3NL3agEvXdT0WmEost648sQOYAeJS9Q7bfUVoMGnjo4AZdUMQku50McDcMWcBPvr0SzbTAFDfvJqwLzgxwATnCgnp4wDl6Aa+Ax283gghmj+vj7feE2KBBRMW3FzOpLOADl0Isb5587h/U4gGvkt5v60Z1VLG8BhYjbzRwyQZemwAd6cCR5/XFWLYZRIMpX39AR0tjaGGiGzLVyhse5C9RKC6ai42ppWPKiBagOvaYk8lO7DajerabOZP46Lby5wKjw1HCRx7p9sVMOWGzb/vA1hwiWc6jm3MvQDTogQkiqIhJV0nBQBTU+3okKCFDy9WwferkHjtxib7t3xIUQtHxnIwtx4mpg26/HfwVNVDb4oI9RHmx5WGelRVlrtiw43zboCLaxv46AZeB3IlTkwouebTr1y2NjSpHz68WNFjHvupy3q8TFn3Hos2IAk4Ju5dCo8B3wP7VPr/FGaKiG+T+v+TQqIrOqMTL1VdWV1DdmcbO8KXBz6esmYWYKPwDL5b5FA1a0hwapHiom0r/cKaoqr+27/XcrS5UwSMbQAAAABJRU5ErkJggg==)](https://deepwiki.com/HananoshikaYomaru/openfreebooks) + > Work in Progress. I am looking for contributors to help build this project. Educators and engineer welcome. Free, open-source textbooks for every learner — from elementary school through university. The site is fully static, open on GitHub, and deployed to Cloudflare. diff --git a/content/math/counting-and-numbers/widgets/big-numbers-scale.tsx b/content/math/counting-and-numbers/widgets/big-numbers-scale.tsx index 829bbfd..36570cf 100644 --- a/content/math/counting-and-numbers/widgets/big-numbers-scale.tsx +++ b/content/math/counting-and-numbers/widgets/big-numbers-scale.tsx @@ -54,10 +54,33 @@ function BigNumbersScale() { return; } - const ctx = canvas.getContext("2d"); - if (!ctx) return; + const canvasCtx = canvas.getContext("2d"); + if (!canvasCtx) return; + const ctx: CanvasRenderingContext2D = canvasCtx; - const STEPS = [ + type CoreTone = "green" | "purple" | "orange" | "yellow" | "cyan"; + type CoreColorInfo = { + hex: string; + rgb: [number, number, number]; + glow: string; + }; + type StepMeta = { + id: string; + label: string; + value: number; + power: string; + tone: CoreTone; + example: string; + rawTime: number; + }; + type TierMeta = { + index: number; + unitValue: number; + colorName: CoreTone; + label: string; + }; + + const STEPS: StepMeta[] = [ { id: "one", label: "1", value: 1, power: "10⁰", tone: "green", example: "A single heartbeat. The primary unit of all structures.", rawTime: 1 }, { id: "ten", label: "10", value: 10, power: "10¹", tone: "green", example: "The fingers on two hands. The basic structural unit of decimal arithmetic.", rawTime: 10 }, { id: "hundred", label: "100", value: 100, power: "10²", tone: "purple", example: "Grains of salt in a pinch. Page count of a short story.", rawTime: 100 }, @@ -68,23 +91,23 @@ function BigNumbersScale() { { id: "ten_million", label: "10 Million", value: 10_000_000, power: "10⁷", tone: "yellow", example: "Population of a major mega-city like Tokyo, Seoul, or London.", rawTime: 10_000_000 }, { id: "hundred_million", label: "100 Million", value: 100_000_000, power: "10⁸", tone: "cyan", example: "Total books published in human history. Heartbeats of a cat over its lifetime.", rawTime: 100_000_000 }, { id: "billion", label: "1 Billion", value: 1_000_000_000, power: "10⁹", tone: "cyan", example: "Grains of sand in a typical children's playground. Light travel distance in meters in 3.3 seconds.", rawTime: 1_000_000_000 }, - ] as const; + ]; - const CORES = { + const CORES: Record = { green: { hex: "#14b8a6", rgb: [20, 184, 166] as [number, number, number], glow: "rgba(20, 184, 166, 0.4)" }, purple: { hex: "#8b5cf6", rgb: [139, 92, 246] as [number, number, number], glow: "rgba(139, 92, 246, 0.4)" }, orange: { hex: "#f97316", rgb: [249, 115, 22] as [number, number, number], glow: "rgba(249, 115, 22, 0.4)" }, yellow: { hex: "#f59e0b", rgb: [245, 158, 11] as [number, number, number], glow: "rgba(245, 158, 11, 0.4)" }, cyan: { hex: "#06b6d4", rgb: [6, 182, 212] as [number, number, number], glow: "rgba(6, 182, 212, 0.4)" }, - } as const; + }; - const TIERS = [ + const TIERS: TierMeta[] = [ { index: 0, unitValue: 1, colorName: "green", label: "Units (Green)" }, { index: 1, unitValue: 100, colorName: "purple", label: "Hundreds (Purple)" }, { index: 2, unitValue: 10_000, colorName: "orange", label: "Ten-Thousands (Orange)" }, { index: 3, unitValue: 1_000_000, colorName: "yellow", label: "Millions (Yellow)" }, { index: 4, unitValue: 100_000_000, colorName: "cyan", label: "Hundred-Millions (Cyan)" }, - ] as const; + ]; type VisualMode = "NORMAL" | "COMPRESSING" | "EXPANDING"; type Speed = "slow" | "normal" | "fast"; @@ -230,7 +253,7 @@ function BigNumbersScale() { y = 0; radius = 5; alpha = 1; - colorInfo = CORES.green; + colorInfo: CoreColorInfo = CORES.green; constructor(index: number, tierIdx: number) { this.index = index; @@ -652,7 +675,7 @@ function BigNumbersScale() { }); btnPrevStep.addEventListener("click", () => { - let closestSmallerStep = STEPS[0]!.value; + let closestSmallerStep: number = STEPS[0]!.value; for (let i = STEPS.length - 1; i >= 0; i -= 1) { if (STEPS[i]!.value < state.targetValue) { closestSmallerStep = STEPS[i]!.value; @@ -664,7 +687,7 @@ function BigNumbersScale() { }); btnNextStep.addEventListener("click", () => { - let closestLargerStep = STEPS[STEPS.length - 1]!.value; + let closestLargerStep: number = STEPS[STEPS.length - 1]!.value; for (let i = 0; i < STEPS.length; i += 1) { if (STEPS[i]!.value > state.targetValue) { closestLargerStep = STEPS[i]!.value; diff --git a/content/math/linear-equations-two-unknowns/widgets/simultaneous-solver-workbench.tsx b/content/math/linear-equations-two-unknowns/widgets/simultaneous-solver-workbench.tsx index 22dac17..0d54eb0 100644 --- a/content/math/linear-equations-two-unknowns/widgets/simultaneous-solver-workbench.tsx +++ b/content/math/linear-equations-two-unknowns/widgets/simultaneous-solver-workbench.tsx @@ -496,12 +496,10 @@ function SimultaneousSolverWorkbench() { - {(step) => ( -
-

{step().title}

-
-
- )} +
+

{current()!.title}

+
+
diff --git a/content/math/subject.ts b/content/math/subject.ts index 5da7910..bb0102e 100644 --- a/content/math/subject.ts +++ b/content/math/subject.ts @@ -10,3 +10,5 @@ export function initMathSubject(root: HTMLElement): void { panel.classList.add("book-question__solution"); } } + +export const initSubject = initMathSubject; diff --git a/content/math/written-calculation/chapter.scss b/content/math/written-calculation/chapter.scss index 291cf7d..56a974a 100644 --- a/content/math/written-calculation/chapter.scss +++ b/content/math/written-calculation/chapter.scss @@ -76,6 +76,7 @@ .va-workbench__brand { display: flex; align-items: center; + flex-wrap: wrap; gap: 0.75rem; margin-bottom: 0.25rem; } @@ -105,6 +106,24 @@ font-weight: 600; } +.va-workbench__sound-btn { + margin-left: auto; + font: inherit; + font-size: 0.72rem; + font-weight: 600; + padding: 0.35rem 0.55rem; + border: 1px solid var(--line); + border-radius: 999px; + background: color-mix(in srgb, var(--paper) 55%, var(--card-bg)); + color: var(--soft-ink); + cursor: pointer; +} + +.va-workbench__sound-btn:hover { + border-color: var(--accent); + color: var(--ink); +} + .va-workbench__field { display: flex; flex-direction: column; diff --git a/content/math/written-calculation/widgets/vertical-arithmetic-workbench.tsx b/content/math/written-calculation/widgets/vertical-arithmetic-workbench.tsx index 482c148..a38a9e8 100644 --- a/content/math/written-calculation/widgets/vertical-arithmetic-workbench.tsx +++ b/content/math/written-calculation/widgets/vertical-arithmetic-workbench.tsx @@ -5,6 +5,82 @@ 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; @@ -19,6 +95,11 @@ function VerticalArithmeticWorkbench() { 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; @@ -32,12 +113,23 @@ function VerticalArithmeticWorkbench() { 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; }; - visualizer.onPlayState = setPlaying; const resize = () => visualizer?.resizeCanvas(); resize(); @@ -72,6 +164,7 @@ function VerticalArithmeticWorkbench() { const generate = () => { visualizer?.setOperation(operation()); visualizer?.generateFromInput(numbersInput()); + sound.playGenerate(); }; return ( @@ -93,6 +186,18 @@ function VerticalArithmeticWorkbench() { Arithmetic Visualizer +
@@ -112,7 +217,10 @@ function VerticalArithmeticWorkbench() { classList={{ "va-workbench__op-btn--active": operation() === op }} aria-pressed={operation() === op} aria-label={label} - onClick={() => setOperation(op)} + onClick={() => { + setOperation(op); + sound.playTick(); + }} > {op === "*" ? "×" : op === "/" ? "÷" : op} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index de27647..5499cf7 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -66,7 +66,7 @@ function mountChapterWidgets() { } loadPromises.push( loader().then((mod) => { - render(() => mod.default(), el); + render(() => mod.default({}), el); }) ); } diff --git a/package.json b/package.json index bc30f33..6677565 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,8 @@ "scripts": { "sync:chapters": "bun scripts/sync-chapters.ts", "build:js": "bun run sync:chapters && vite build", - "build": "rm -f themes/openfreebooks/static/main.css public/main.css && bun run build:js && zola build && bun run index:search", - "index:search": "pagefind --site public && rm -rf static/pagefind && cp -R public/pagefind static/pagefind", + "build": "find themes/openfreebooks/static -name '*.html' -delete && rm -f themes/openfreebooks/static/main.css public/main.css && bun run build:js && zola build && bun run index:search", + "index:search": "pagefind --site public --glob \"**/index.html\" && rm -rf static/pagefind && cp -R public/pagefind static/pagefind", "dev:js": "bun run sync:chapters && vite build --watch", "scan:size": "bun scripts/scan-output-size.ts", "test": "bun test frontend/src/lib" diff --git a/scripts/sync-chapters.ts b/scripts/sync-chapters.ts index e287bfa..1cbf913 100644 --- a/scripts/sync-chapters.ts +++ b/scripts/sync-chapters.ts @@ -172,7 +172,7 @@ function buildSubjectModuleRegistry( ]; for (const { id, dir } of withJs) { const relFromGenerated = relative(dirname(GENERATED_SUBJECT_MODULES), dir).replace(/\\/g, "/"); - lines.push(` "${id}": () => import("${relFromGenerated}/subject.ts"),`); + lines.push(` "${id}": () => import("${relFromGenerated}/subject"),`); } lines.push("};", ""); return lines.join("\n"); @@ -314,7 +314,7 @@ function buildWidgetRegistry( for (const { key, widgets, dir } of entries) { const relFromGenerated = relative(dirname(GENERATED_WIDGETS), dir).replace(/\\/g, "/"); for (const widget of widgets) { - const importPath = `${relFromGenerated}/widgets/${widget}.tsx`; + const importPath = `${relFromGenerated}/widgets/${widget}`; lines.push(` "${key}/${widget}": () => import("${importPath}"),`); } } diff --git a/spec.md b/spec.md index 0bea313..47593ae 100644 --- a/spec.md +++ b/spec.md @@ -18,6 +18,8 @@ we need to have a question bank feature. since this is purely static sites, ques question bank not only show answer but also steps, explanation, calcultion. +best practice for creating chapters: correctness > completeness > order and structure > accessibility > optimization + ## Chapters requirements ### Maths diff --git a/static/og.jpg b/static/og.jpg new file mode 100644 index 0000000..5503cd0 Binary files /dev/null and b/static/og.jpg differ diff --git a/static/og.png b/static/og.png deleted file mode 100644 index 743be9a..0000000 Binary files a/static/og.png and /dev/null differ diff --git a/themes/openfreebooks/templates/partials/social-meta.html b/themes/openfreebooks/templates/partials/social-meta.html index e01f4c5..4223d6c 100644 --- a/themes/openfreebooks/templates/partials/social-meta.html +++ b/themes/openfreebooks/templates/partials/social-meta.html @@ -1,5 +1,5 @@ {# Open Graph + Twitter Card — shared site image on every page #} -{% set og_image = get_url(path='og.png', trailing_slash=false) %} +{% set og_image = get_url(path='og.jpg', trailing_slash=false) %} diff --git a/vite.config.ts b/vite.config.ts index 6a31d97..a4d079a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -7,6 +7,7 @@ const rootDir = path.dirname(fileURLToPath(import.meta.url)); export default defineConfig({ plugins: [solid()], + publicDir: false, resolve: { alias: { "@ofb/katex": path.resolve(rootDir, "frontend/src/lib/katex.ts"),