mirror of
https://github.com/ParkerTenBroeck/automata.git
synced 2026-06-07 05:28:45 -04:00
semi finalized theming
This commit is contained in:
parent
c12f7b325f
commit
620415c824
14 changed files with 728 additions and 224 deletions
116
web/root/src/controls.ts
Normal file
116
web/root/src/controls.ts
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import {nodes, edges, network} from "./visualizer.ts"
|
||||
|
||||
const togglePhysicsBtn = document.getElementById("togglePhysics") as HTMLButtonElement;
|
||||
const resetLayoutBtn = document.getElementById("resetLayout") as HTMLButtonElement;
|
||||
const playPauseBtn = document.getElementById("playPause") as HTMLButtonElement;
|
||||
const stepBtn = document.getElementById("step") as HTMLButtonElement;
|
||||
const speedSlider = document.getElementById("speed") as HTMLInputElement;
|
||||
const speedLabel = document.getElementById("speedLabel") as HTMLSpanElement;
|
||||
const resetSimBtn = document.getElementById("resetSim") as HTMLButtonElement;
|
||||
|
||||
|
||||
function stepSimulation(): void {
|
||||
console.log("step");
|
||||
}
|
||||
|
||||
function resetSimulation(): void {
|
||||
console.log("reset");
|
||||
}
|
||||
|
||||
// ---- Physics toggle (styled label) ----
|
||||
function setPhysicsButtonUI(enabled: boolean) {
|
||||
togglePhysicsBtn.classList.toggle("active", enabled);
|
||||
togglePhysicsBtn.textContent = enabled ? "Physics: ON" : "Physics: OFF";
|
||||
}
|
||||
|
||||
togglePhysicsBtn.onclick = () => {
|
||||
const enabled = !togglePhysicsBtn.classList.contains("active");
|
||||
setPhysicsButtonUI(enabled);
|
||||
network.setOptions({ physics: { enabled } });
|
||||
};
|
||||
|
||||
setPhysicsButtonUI(togglePhysicsBtn.classList.contains("active"));
|
||||
|
||||
resetLayoutBtn.onclick = () => {
|
||||
try {
|
||||
nodes.forEach((n) => {
|
||||
n.physics = true;
|
||||
n.x = undefined;
|
||||
n.y = undefined;
|
||||
});
|
||||
network.setData({ nodes, edges });
|
||||
} catch {
|
||||
// Last resort
|
||||
network.setData({ nodes, edges });
|
||||
}
|
||||
|
||||
// If physics button is OFF, keep it OFF (don’t surprise the user)
|
||||
const physicsEnabled = togglePhysicsBtn.classList.contains("active");
|
||||
network.setOptions({ physics: { enabled: physicsEnabled } });
|
||||
};
|
||||
|
||||
// ---- Play/Pause + Speed ----
|
||||
let running = false;
|
||||
let timer: number | null = null;
|
||||
|
||||
// speed slider is "steps per second"
|
||||
function getStepsPerSecond() {
|
||||
return Math.max(1, Math.min(60, Number(speedSlider.value) || 10));
|
||||
}
|
||||
function updateSpeedUI() {
|
||||
speedLabel.textContent = `${getStepsPerSecond()}×`;
|
||||
}
|
||||
updateSpeedUI();
|
||||
|
||||
speedSlider.addEventListener("input", () => {
|
||||
updateSpeedUI();
|
||||
if (running) restartTimer();
|
||||
});
|
||||
|
||||
function stopTimer() {
|
||||
if (timer !== null) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function restartTimer() {
|
||||
stopTimer();
|
||||
const sps = getStepsPerSecond();
|
||||
const intervalMs = Math.round(1000 / sps);
|
||||
|
||||
timer = globalThis.window.setInterval(() => {
|
||||
// If your step can throw, keep the interval alive:
|
||||
try { stepSimulation(); } catch (e) { console.error(e); }
|
||||
}, intervalMs);
|
||||
}
|
||||
|
||||
function setRunning(on: boolean) {
|
||||
running = on;
|
||||
playPauseBtn.textContent = running ? "⏸ Pause" : "▶ Play";
|
||||
playPauseBtn.classList.toggle("btn-primary", !running);
|
||||
playPauseBtn.classList.toggle("btn-secondary", running);
|
||||
|
||||
// Disable step while running (optional, but feels nice)
|
||||
stepBtn.disabled = running;
|
||||
|
||||
if (running) restartTimer();
|
||||
else stopTimer();
|
||||
}
|
||||
|
||||
playPauseBtn.onclick = () => setRunning(!running);
|
||||
|
||||
stepBtn.onclick = () => {
|
||||
stepSimulation();
|
||||
};
|
||||
|
||||
resetSimBtn.onclick = () => {
|
||||
// Stop if running
|
||||
if (running) setRunning(false);
|
||||
|
||||
// Reset
|
||||
resetSimulation();
|
||||
|
||||
// Optional: re-enable Step after reset
|
||||
stepBtn.disabled = false;
|
||||
};
|
||||
|
|
@ -8,13 +8,13 @@ import {
|
|||
ViewPlugin,
|
||||
lineNumbers,
|
||||
highlightActiveLineGutter,
|
||||
highlightActiveLine
|
||||
} from "npm:@codemirror/view";
|
||||
|
||||
import { EditorState, StateField, Text } from "npm:@codemirror/state";
|
||||
import { defaultKeymap, history, historyKeymap } from "npm:@codemirror/commands";
|
||||
import { bracketMatching, indentOnInput } from "npm:@codemirror/language";
|
||||
import { closeBrackets } from "npm:@codemirror/autocomplete";
|
||||
import { oneDark } from "npm:@codemirror/theme-one-dark";
|
||||
|
||||
|
||||
import wasm from "./wasm.ts"
|
||||
|
|
@ -292,9 +292,9 @@ const state = EditorState.create({
|
|||
history(),
|
||||
indentOnInput(),
|
||||
bracketMatching(),
|
||||
highlightActiveLine(),
|
||||
closeBrackets(),
|
||||
keymap.of([...defaultKeymap, ...historyKeymap]),
|
||||
oneDark,
|
||||
|
||||
analysisField,
|
||||
diagHover,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
|
||||
import "./editor.ts"
|
||||
import "./visualizer.ts"
|
||||
import "./splitters.ts"
|
||||
import "./splitters.ts"
|
||||
import "./controls.ts"
|
||||
import "./theme.ts"
|
||||
|
|
@ -65,7 +65,7 @@ function setFlexFill(pane: HTMLElement) {
|
|||
|
||||
export function enableFlexSplitters() {
|
||||
// Horizontal: A | hSplit | B (top/split/bottom)
|
||||
for (const splitter of document.querySelectorAll<HTMLElement>(".hSplit")) {
|
||||
for (const splitter of document.querySelectorAll<HTMLElement>(".hSplit:not(.styleOnly)")) {
|
||||
const parent = splitter.parentElement as HTMLElement | null;
|
||||
if (!parent) continue;
|
||||
|
||||
|
|
@ -133,7 +133,7 @@ export function enableFlexSplitters() {
|
|||
}
|
||||
|
||||
// Vertical: A | vSplit | B (left/split/right)
|
||||
for (const splitter of document.querySelectorAll<HTMLElement>(".vSplit")) {
|
||||
for (const splitter of document.querySelectorAll<HTMLElement>(".vSplit:not(.styleOnly)")) {
|
||||
const parent = splitter.parentElement as HTMLElement | null;
|
||||
if (!parent) continue;
|
||||
|
||||
|
|
@ -158,7 +158,7 @@ export function enableFlexSplitters() {
|
|||
// --split-default: 30% (right pane width)
|
||||
// --split-min-a: 220px (min left)
|
||||
// --split-min-b: 220px (min right)
|
||||
const defPct = getVarPct(splitter, "--split-default", 30);
|
||||
const defPct = getVarPct(splitter, "--split-default", 50);
|
||||
const minA = getVarPx(splitter, "--split-min-a", 220);
|
||||
const minB = getVarPx(splitter, "--split-min-b", 220);
|
||||
|
||||
|
|
|
|||
75
web/root/src/theme.ts
Normal file
75
web/root/src/theme.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { network } from "./visualizer.ts";
|
||||
|
||||
function cssVar(name: string, fallback = ""): string {
|
||||
return getComputedStyle(document.documentElement)
|
||||
.getPropertyValue(name)
|
||||
.trim() || fallback;
|
||||
}
|
||||
|
||||
const themeBtn = document.getElementById("themeToggle") as HTMLButtonElement;
|
||||
|
||||
type Theme = "dark" | "light";
|
||||
|
||||
function getPreferredTheme(): Theme {
|
||||
// 1) saved preference
|
||||
const saved = localStorage.getItem("theme");
|
||||
if (saved === "dark" || saved === "light") return saved;
|
||||
|
||||
// 2) OS preference
|
||||
const prefersLight = globalThis.window.matchMedia?.(
|
||||
"(prefers-color-scheme: light)",
|
||||
)?.matches;
|
||||
return prefersLight ? "light" : "dark";
|
||||
}
|
||||
|
||||
function setTheme(theme: Theme) {
|
||||
document.documentElement.dataset.theme = theme;
|
||||
localStorage.setItem("theme", theme);
|
||||
|
||||
// update button label
|
||||
themeBtn.textContent = theme === "dark" ? "🌙 Dark" : "☀️ Light";
|
||||
applyGraphTheme();
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
const current = (document.documentElement.dataset.theme as Theme) || "dark";
|
||||
setTheme(current === "dark" ? "light" : "dark");
|
||||
}
|
||||
|
||||
// init
|
||||
setTheme(getPreferredTheme());
|
||||
|
||||
// click handler
|
||||
themeBtn.addEventListener("click", toggleTheme);
|
||||
|
||||
// optional: respond to OS theme changes (only if user hasn't chosen a theme)
|
||||
globalThis.window.matchMedia?.("(prefers-color-scheme: light)")
|
||||
?.addEventListener("change", () => {
|
||||
if (localStorage.getItem("theme")) return; // user has chosen, don't override
|
||||
setTheme(getPreferredTheme());
|
||||
});
|
||||
|
||||
export function applyGraphTheme() {
|
||||
network.setOptions({
|
||||
nodes: {
|
||||
color: {
|
||||
background: cssVar("--graph-node-bg"),
|
||||
border: cssVar("--graph-node-border"),
|
||||
highlight: {
|
||||
background: cssVar("--graph-node-active-bg"),
|
||||
border: cssVar("--graph-node-active-border"),
|
||||
},
|
||||
},
|
||||
font: {
|
||||
color: cssVar("--graph-node-text"),
|
||||
},
|
||||
},
|
||||
edges: {
|
||||
color: {
|
||||
color: cssVar("--graph-edge"),
|
||||
highlight: cssVar("--graph-edge-active"),
|
||||
hover: cssVar("--graph-edge-hover"),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -3,11 +3,8 @@
|
|||
// deno-lint-ignore no-import-prefix
|
||||
import * as vis from "npm:vis-network/standalone";
|
||||
|
||||
|
||||
|
||||
const nodes = new vis.DataSet<vis.Node>();
|
||||
const edges = new vis.DataSet<vis.Edge>();
|
||||
|
||||
export const nodes = new vis.DataSet<vis.Node>();
|
||||
export const edges = new vis.DataSet<vis.Edge>();
|
||||
|
||||
const automaton = {
|
||||
states: ["q0", "q1"],
|
||||
|
|
@ -18,29 +15,29 @@ const automaton = {
|
|||
{
|
||||
from: "q0",
|
||||
to: "q0",
|
||||
label: "ε, z0 → A z0\n"
|
||||
label: "ε, z0 → A z0\n",
|
||||
},
|
||||
{
|
||||
from: "q0",
|
||||
to: "q0",
|
||||
label: "ε, z0 → B z0"
|
||||
label: "ε, z0 → B z0",
|
||||
},
|
||||
{
|
||||
from: "q0",
|
||||
to: "q1",
|
||||
label: "ε, z0 → z0"
|
||||
label: "ε, z0 → z0",
|
||||
},
|
||||
{
|
||||
from: "q1",
|
||||
to: "q1",
|
||||
label: "a, A → ε"
|
||||
label: "a, A → ε",
|
||||
},
|
||||
{
|
||||
from: "q1",
|
||||
to: "q1",
|
||||
label: "b, B → ε"
|
||||
}
|
||||
]
|
||||
label: "b, B → ε",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function renderNode({
|
||||
|
|
@ -57,7 +54,6 @@ function renderNode({
|
|||
ctx.save();
|
||||
const r = style.size;
|
||||
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, r, 0, 2 * Math.PI);
|
||||
ctx.fillStyle = "red";
|
||||
|
|
@ -67,29 +63,27 @@ function renderNode({
|
|||
ctx.stroke();
|
||||
|
||||
ctx.fillStyle = "black";
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillText(label, x, y, r);
|
||||
|
||||
|
||||
ctx.textAlign = 'center';
|
||||
ctx.strokeStyle = 'white';
|
||||
ctx.textAlign = "center";
|
||||
ctx.strokeStyle = "white";
|
||||
ctx.fillStyle = "black";
|
||||
let cy = y - (r + 10);
|
||||
for (const part of "meow[]\nbeeep".split("\n").reverse()) {
|
||||
const metrics = ctx.measureText(part);
|
||||
cy -= metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
|
||||
cy -= metrics.actualBoundingBoxAscent +
|
||||
metrics.actualBoundingBoxDescent;
|
||||
ctx.strokeText(part, x, cy);
|
||||
ctx.fillText(part, x, cy);
|
||||
}
|
||||
|
||||
|
||||
ctx.restore();
|
||||
},
|
||||
nodeDimensions: { width: 20, height: 20 },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// Populate nodes
|
||||
for (const state of automaton.states) {
|
||||
nodes.add({
|
||||
|
|
@ -104,24 +98,31 @@ automaton.transitions.forEach((t, i) => {
|
|||
id: `e${i}`,
|
||||
from: t.from,
|
||||
to: t.to,
|
||||
label: t.label
|
||||
label: t.label,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
function chosen_edge(_: vis.ChosenNodeValues, id: vis.IdType,selected: boolean, hovered: boolean) {
|
||||
console.log("edge", id, selected, hovered)
|
||||
function chosen_edge(
|
||||
_: vis.ChosenNodeValues,
|
||||
id: vis.IdType,
|
||||
selected: boolean,
|
||||
hovered: boolean,
|
||||
) {
|
||||
console.log("edge", id, selected, hovered);
|
||||
}
|
||||
|
||||
function chosen_node(_: vis.ChosenNodeValues, id: vis.IdType,selected: boolean, hovered: boolean) {
|
||||
console.log("node", id, selected, hovered)
|
||||
function chosen_node(
|
||||
_: vis.ChosenNodeValues,
|
||||
id: vis.IdType,
|
||||
selected: boolean,
|
||||
hovered: boolean,
|
||||
) {
|
||||
console.log("node", id, selected, hovered);
|
||||
}
|
||||
|
||||
|
||||
const network: vis.Network = createGraph();
|
||||
export const network: vis.Network = createGraph();
|
||||
|
||||
function createGraph(): vis.Network {
|
||||
|
||||
const container = document.getElementById("graph")!;
|
||||
|
||||
const network = new vis.Network(
|
||||
|
|
@ -129,13 +130,15 @@ function createGraph(): vis.Network {
|
|||
{ nodes, edges },
|
||||
{
|
||||
layout: { improvedLayout: true },
|
||||
autoResize: true,
|
||||
width: "99%",
|
||||
physics: {
|
||||
enabled: true,
|
||||
solver: "barnesHut",
|
||||
barnesHut: { gravitationalConstant: -8000, springLength: 120, springConstant: 0.04 },
|
||||
stabilization: { iterations: 200 }
|
||||
barnesHut: {
|
||||
gravitationalConstant: -8000,
|
||||
springLength: 120,
|
||||
springConstant: 0.04,
|
||||
},
|
||||
stabilization: { iterations: 200 },
|
||||
},
|
||||
interaction: {
|
||||
dragNodes: true,
|
||||
|
|
@ -149,7 +152,7 @@ function createGraph(): vis.Network {
|
|||
color: {
|
||||
background: "#1f6feb",
|
||||
border: "#79c0ff",
|
||||
highlight: { background: "#388bfd", border: "#a5d6ff" }
|
||||
highlight: { background: "#388bfd", border: "#a5d6ff" },
|
||||
},
|
||||
// @ts-expect-error bad library
|
||||
chosen: {
|
||||
|
|
@ -162,7 +165,7 @@ function createGraph(): vis.Network {
|
|||
edges: {
|
||||
chosen: {
|
||||
// @ts-expect-error bad library
|
||||
edge: chosen_edge
|
||||
edge: chosen_edge,
|
||||
},
|
||||
arrowStrikethrough: false,
|
||||
font: { align: "middle", color: "#000000ff" },
|
||||
|
|
@ -170,20 +173,19 @@ function createGraph(): vis.Network {
|
|||
// @ts-expect-error bad library
|
||||
smooth: { type: "dynamic" },
|
||||
arrows: "to",
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
vis.DataSet
|
||||
vis.DataSet;
|
||||
|
||||
network.on("doubleClick", (params: any) => {
|
||||
|
||||
for (const node_id of params.nodes){
|
||||
for (const node_id of params.nodes) {
|
||||
// @ts-expect-error bad library
|
||||
const node: vis.Node = nodes.get(node_id)!;
|
||||
node.physics = !node.physics;
|
||||
nodes.update(node)
|
||||
nodes.update(node);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return network;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue