') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); GitHub - Symbulator/local: Offline browser build of Symbulator Py, running client-side via Pyodide · GitHub
Skip to content

Latest commit

History

263 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Symbulator — browser build

The whole simulator, running as static files. There is no server: Python, SymPy and the symbulator package all run inside the visitor's browser via Pyodide (CPython compiled to WebAssembly). Solving happens in their tab, not on your host.

Two consequences worth having:

  • It uploads anywhere. No "Setup Python App", no subprocess permissions, no memory limits, no PHP-versus-Python question — any host that serves files will do, cPanel included.
  • It works offline and installs. A service worker caches everything on the first visit, so afterwards it runs with no internet, and the page offers an Install bar to give it a desktop or home-screen icon (see the install bar notes in CLAUDE.md).

Putting it online

Upload the whole folder to your web root (or a subfolder) and visit index.html. That's the entire deployment.

Two server details matter:

  1. Serve .wasm as application/wasm. Most hosts already do. If the page reports that the maths engine could not start, this is the usual reason — add to .htaccess:

    AddTypeapplication/wasm .wasm
    AddTypeapplication/json .webmanifest
  2. HTTPS is required for offline mode. Service workers only run on https:// (or localhost). Without it the site still works, it just won't cache or install. Both of your domains have certificates, so this is automatic.

Enabling gzip/deflate is worth it: the first visit transfers about 17 MB compressed rather than 25 MB raw.

Running it locally

You cannot just double-click index.html — browsers block WebAssembly loading from file:// URLs ("Cross origin requests are only supported for http, https"). Serve the folder over HTTP instead:

python -m http.server 8000

then open http://localhost:8000. For everyday offline use, though, prefer installing the hosted version as an app: visit the site once and use the browser's Install option. That needs no Python at all.

What the visitor experiences

momentwhat happens
page appearsinstantly — the form is usable straight away
first ~8 sPython, SymPy and symbulator load in the background, with a small notice; typing a circuit takes about this long anyway
every solve after that20–50 ms for DC, a few hundred ms for AC or transient — faster than the server version, since nothing crosses the network
second visitno download (cached); the ~8 s warm-up runs again, still in the background
offlineidentical, once it has been visited once

Files

filepurpose
index.htmlthe entire interface, and the code that boots Python — generated, do not hand-edit
build_local.pyregenerates index.html from the server version's template
build_zip.pyassembles the downloadable ZIP, and verifies the icons, cache list and head links before writing it
CLAUDE.mdhow all three site variants fit together — read this first
README.txtthe readme that ships inside the ZIP, for end users (this file is for developers)
start.bat, start.sh, start.commandlaunchers shipped in the ZIP; they start python -m http.server and open a browser
bridge.pythin adapter: JSON in, JSON out
symbulator_ui.pyshared with the Flask build — all solving, formatting, units, ordering
circuitbook.pythe [Name] circuit-file format, also shared
examples/the built-in example files, copied from ../server by build_local.py; each carries its own title:
sw.jsservice worker: offline caching. Bump CACHE_VERSION when you change app files, or returning visitors keep the old build
manifest.webmanifestwhat makes it installable. Its icons must include a 192px and a 512px entry, or Chrome on Android will not offer to install
icon.pngmaster artwork; the favicon*, apple-touch-icon.png and icon-*.png files are generated from it. Regenerate them if it changes
LICENSEMIT
vendor/Pyodide runtime plus the sympy, mpmath and symbulator wheels
static/mathjax/typeset maths, served locally

symbulator_ui.py and circuitbook.py are byte-identical to the ones in the server project — deliberately, so the two front ends can never drift apart. If you change one, copy it to the other.

Changing the interface

There is only one interface, and it lives in the server project, at ../server/templates/index.html. This build's index.html is produced from it:

python3 build_local.py # regenerate index.html
python3 build_local.py --check # exit 1 if it is stale

Every substitution the script makes asserts that it matched, so a change to the template that breaks the transformation fails loudly instead of quietly dropping a feature — which is exactly how an earlier build shipped with no service worker registered. Edit the template, re-run the script, bump CACHE_VERSION in sw.js.

Updating the symbulator package

The wheel in vendor/ is pinned. To move to a newer release:

pip download symbulator==X.Y.Z --no-deps -d vendor/

then update the filename in index.html (the loadPackage line) and in sw.js (the ASSETS list), and bump CACHE_VERSION.

About

Offline browser build of Symbulator Py, running client-side via Pyodide

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages