dynsim 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +126 -0
- package/dist/dynsim.esm.js +81 -0
- package/dist/dynsim.esm.js.map +7 -0
- package/dist/dynsim.umd.js +81 -0
- package/dist/dynsim.umd.js.map +7 -0
- package/package.json +48 -0
- package/src/controller.js +100 -0
- package/src/editor.js +131 -0
- package/src/index.js +126 -0
- package/src/pybridge.js +67 -0
- package/src/registry.js +102 -0
- package/src/simulation.js +161 -0
- package/src/umd-entry.js +22 -0
- package/src/view.js +235 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure simulation engine — no DOM, no Plotly.
|
|
3
|
+
*
|
|
4
|
+
* Manages state, stepping, and plot data buffering.
|
|
5
|
+
* The step function is resolved via a stepProvider on each tick,
|
|
6
|
+
* enabling live code replacement.
|
|
7
|
+
*/
|
|
8
|
+
export class Simulation {
|
|
9
|
+
/**
|
|
10
|
+
* @param {object} config
|
|
11
|
+
* @param {Array} config.params - Parameter definitions [{id, label, min, max, step, value}]
|
|
12
|
+
* @param {string} config.plotType - 'timeseries' | '3d' | '2d'
|
|
13
|
+
* @param {object} config.plotConfig - Plotly layout config (title, axes, etc.)
|
|
14
|
+
* @param {object} config.initialState - Initial state object
|
|
15
|
+
* @param {number} config.initialX - Initial input/output value
|
|
16
|
+
* @param {number} config.dt - Time step
|
|
17
|
+
* @param {number} [config.maxPoints=1000] - Max buffered points (non-timeseries)
|
|
18
|
+
* @param {number} [config.maxPoints=1000] - Max buffered points (non-timeseries)
|
|
19
|
+
* @param {number} [config.pauseTime=null] - Pause simulation at this time (null = run forever)
|
|
20
|
+
* @param {string} [config.spikes=null] - State variable name to check for spikes (e.g. 'z'). Falsy = no spikes.
|
|
21
|
+
* @param {function} stepProvider - () => stepFunction. Called each tick to get the current step function.
|
|
22
|
+
*/
|
|
23
|
+
constructor(config, stepProvider) {
|
|
24
|
+
this.params = config.params;
|
|
25
|
+
this.plotType = config.plotType || 'timeseries';
|
|
26
|
+
this.plotConfig = config.plotConfig || {};
|
|
27
|
+
this.initialState = config.initialState || { t: 0 };
|
|
28
|
+
this.initialX = config.initialX ?? 0;
|
|
29
|
+
this.dt = config.dt || 0.01;
|
|
30
|
+
this.maxPoints = config.maxPoints || 1000;
|
|
31
|
+
this.pauseTime = config.pauseTime ?? null;
|
|
32
|
+
this.spikes = config.spikes || null;
|
|
33
|
+
|
|
34
|
+
this.stepProvider = stepProvider;
|
|
35
|
+
|
|
36
|
+
this.x = this.initialX;
|
|
37
|
+
this.state = { ...this.initialState };
|
|
38
|
+
this.time = 0;
|
|
39
|
+
this.plotData = [];
|
|
40
|
+
this.spikeTimes = [];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Advance one time step.
|
|
45
|
+
* @param {number} inputValue - Current input from the user (slider)
|
|
46
|
+
* @param {object} paramValues - Parameter values keyed by param ID
|
|
47
|
+
* @returns {{ x: number, state: object, dataPoint: Array }} result of the step
|
|
48
|
+
* @throws if stepProvider returns null or step function throws
|
|
49
|
+
*/
|
|
50
|
+
step(inputValue, paramValues) {
|
|
51
|
+
const stepFn = this.stepProvider();
|
|
52
|
+
if (!stepFn) {
|
|
53
|
+
throw new Error('No step function available');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Add dt to params
|
|
57
|
+
const params = { ...paramValues, dt: this.dt };
|
|
58
|
+
|
|
59
|
+
const result = stepFn(inputValue, this.state, params);
|
|
60
|
+
this.x = result[0];
|
|
61
|
+
this.state = result[1];
|
|
62
|
+
this.time += this.dt;
|
|
63
|
+
|
|
64
|
+
// Record spike if the configured state variable is truthy
|
|
65
|
+
if (this.spikes && this.state[this.spikes]) {
|
|
66
|
+
this.spikeTimes.push(this.time);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const dataPoint = this._collectDataPoint();
|
|
70
|
+
this.plotData.push(dataPoint);
|
|
71
|
+
this._manageBuffer();
|
|
72
|
+
|
|
73
|
+
return { x: this.x, state: this.state, dataPoint };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Whether the simulation has reached its pause time.
|
|
78
|
+
* @returns {boolean}
|
|
79
|
+
*/
|
|
80
|
+
get paused() {
|
|
81
|
+
return this.pauseTime != null && this.time >= this.pauseTime;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Reset simulation to initial conditions.
|
|
86
|
+
*/
|
|
87
|
+
reset() {
|
|
88
|
+
this.x = this.initialX;
|
|
89
|
+
this.state = { ...this.initialState };
|
|
90
|
+
this.time = 0;
|
|
91
|
+
this.plotData = [];
|
|
92
|
+
this.spikeTimes = [];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get current plot data arrays suitable for Plotly.
|
|
97
|
+
* @returns {object} { x, y, z? } arrays
|
|
98
|
+
*/
|
|
99
|
+
getPlotArrays() {
|
|
100
|
+
if (this.plotType === '3d') {
|
|
101
|
+
return {
|
|
102
|
+
x: this.plotData.map(d => d[0]),
|
|
103
|
+
y: this.plotData.map(d => d[1]),
|
|
104
|
+
z: this.plotData.map(d => d[2])
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
x: this.plotData.map(d => d[0]),
|
|
109
|
+
y: this.plotData.map(d => d[1])
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Compute the current x-axis range for timeseries plots.
|
|
115
|
+
* @returns {[number, number]} [min, max]
|
|
116
|
+
*/
|
|
117
|
+
getTimeseriesRange() {
|
|
118
|
+
const windowSize = (this.plotConfig.xaxis?.range?.[1] - this.plotConfig.xaxis?.range?.[0]) || 50;
|
|
119
|
+
const originalEnd = this.plotConfig.xaxis?.range?.[1] || 50;
|
|
120
|
+
|
|
121
|
+
if (this.time > originalEnd) {
|
|
122
|
+
return [this.time - windowSize, this.time];
|
|
123
|
+
}
|
|
124
|
+
return this.plotConfig.xaxis?.range || [0, 50];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// --- Private ---
|
|
128
|
+
|
|
129
|
+
_collectDataPoint() {
|
|
130
|
+
if (this.plotType === '3d') {
|
|
131
|
+
return [this.state.x || this.x, this.state.y || 0, this.state.z || 0];
|
|
132
|
+
} else if (this.plotType === 'timeseries') {
|
|
133
|
+
return [this.time, this.x];
|
|
134
|
+
} else {
|
|
135
|
+
return [this.x, this.state.y || 0];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
_manageBuffer() {
|
|
140
|
+
if (this.plotType === 'timeseries') {
|
|
141
|
+
const windowSize = this.plotConfig.xaxis?.range?.[1] || 50;
|
|
142
|
+
const pointsPerWindow = Math.ceil(windowSize / this.dt);
|
|
143
|
+
const bufferPoints = Math.ceil(pointsPerWindow * 0.5);
|
|
144
|
+
const targetPoints = pointsPerWindow + bufferPoints;
|
|
145
|
+
|
|
146
|
+
if (this.plotData.length > targetPoints * 2) {
|
|
147
|
+
this.plotData = this.plotData.slice(-targetPoints);
|
|
148
|
+
// Trim old spike times outside the buffer
|
|
149
|
+
if (this.spikeTimes.length > 0) {
|
|
150
|
+
const cutoff = this.plotData[0][0]; // earliest time in buffer
|
|
151
|
+
const firstKeep = this.spikeTimes.findIndex(t => t >= cutoff);
|
|
152
|
+
if (firstKeep > 0) this.spikeTimes = this.spikeTimes.slice(firstKeep);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
if (this.plotData.length > this.maxPoints) {
|
|
157
|
+
this.plotData.shift();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
package/src/umd-entry.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UMD entry point — auto-initializes for <script> tag usage.
|
|
3
|
+
* Exposes everything on the global DynSim namespace and calls autoInit().
|
|
4
|
+
*
|
|
5
|
+
* Injects the Python bridge FIRST (synchronously) so that PyScript
|
|
6
|
+
* processes it before any user <script type="py"> tags.
|
|
7
|
+
*/
|
|
8
|
+
export { Simulation } from './simulation.js';
|
|
9
|
+
export { SimulationView } from './view.js';
|
|
10
|
+
export { SimulationController } from './controller.js';
|
|
11
|
+
export { CodeEditor } from './editor.js';
|
|
12
|
+
export * as registry from './registry.js';
|
|
13
|
+
export { autoInit } from './index.js';
|
|
14
|
+
|
|
15
|
+
import { injectPythonBridge } from './pybridge.js';
|
|
16
|
+
import { autoInit } from './index.js';
|
|
17
|
+
|
|
18
|
+
// 1. Inject Python bridge before PyScript runs
|
|
19
|
+
injectPythonBridge();
|
|
20
|
+
|
|
21
|
+
// 2. Set up JS-side registration and auto-init
|
|
22
|
+
autoInit();
|
package/src/view.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SimulationView — DOM + Plotly rendering.
|
|
3
|
+
*
|
|
4
|
+
* Creates the UI (sliders, buttons, plot area), reads user input,
|
|
5
|
+
* and updates the Plotly chart. Emits events via callbacks.
|
|
6
|
+
* Contains no simulation logic.
|
|
7
|
+
*/
|
|
8
|
+
export class SimulationView {
|
|
9
|
+
/**
|
|
10
|
+
* @param {object} options
|
|
11
|
+
* @param {HTMLElement} options.container - DOM element to render into
|
|
12
|
+
* @param {Array} options.params - Parameter definitions [{id, label, min, max, step, value}]
|
|
13
|
+
* @param {number} options.initialX - Initial input value
|
|
14
|
+
* @param {number} options.height - Plot height in pixels
|
|
15
|
+
* @param {string} options.plotType - 'timeseries' | '3d' | '2d'
|
|
16
|
+
* @param {object} options.plotConfig - Plotly layout config
|
|
17
|
+
* @param {object} [options.callbacks] - { onReset, onPauseToggle }
|
|
18
|
+
*/
|
|
19
|
+
constructor({ container, params, initialX, height, plotType, plotConfig, spikeThreshold, callbacks }) {
|
|
20
|
+
this.container = container;
|
|
21
|
+
this.params = params;
|
|
22
|
+
this.initialX = initialX;
|
|
23
|
+
this.height = height || 400;
|
|
24
|
+
this.plotType = plotType || 'timeseries';
|
|
25
|
+
this.plotConfig = plotConfig || {};
|
|
26
|
+
this.spikeThreshold = spikeThreshold;
|
|
27
|
+
this.callbacks = callbacks || {};
|
|
28
|
+
|
|
29
|
+
this.plotDiv = null;
|
|
30
|
+
|
|
31
|
+
this.createHTML();
|
|
32
|
+
this.initPlot();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
createHTML() {
|
|
36
|
+
this.container.innerHTML = `
|
|
37
|
+
<div class="dynsim-container" style="font-family: Arial, sans-serif; font-size: 0.9em;">
|
|
38
|
+
<div class="dynsim-controls" style="background: #f8f9fa; padding: 12px; border-radius: 6px; border: 1px solid #ddd; margin-bottom: 12px; box-sizing: border-box;">
|
|
39
|
+
<div class="dynsim-params"></div>
|
|
40
|
+
</div>
|
|
41
|
+
<div style="width: 100%; height: ${this.height}px; border: 1px solid #ddd; border-radius: 6px; box-sizing: border-box; overflow: hidden;">
|
|
42
|
+
<div class="dynsim-plot" style="width: 100%; height: 100%;"></div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
this._buildControls();
|
|
48
|
+
this.plotDiv = this.container.querySelector('.dynsim-plot');
|
|
49
|
+
|
|
50
|
+
// Typeset LaTeX in labels if MathJax is available
|
|
51
|
+
if (typeof MathJax !== 'undefined' && MathJax.typesetPromise) {
|
|
52
|
+
MathJax.typesetPromise([this.container.querySelector('.dynsim-controls')]);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
_buildControls() {
|
|
57
|
+
const paramsDiv = this.container.querySelector('.dynsim-params');
|
|
58
|
+
|
|
59
|
+
// Input slider row
|
|
60
|
+
let html = `
|
|
61
|
+
<div style="display: flex; gap: 12px; align-items: center; margin-bottom: 8px;">
|
|
62
|
+
<label style="font-weight: 600; font-size: 0.85em; color: #0056b3; white-space: nowrap;">Input (x):</label>
|
|
63
|
+
<input type="range" class="dynsim-input"
|
|
64
|
+
min="-2" max="2" step="0.1" value="${this.initialX}"
|
|
65
|
+
style="flex: 1; height: 6px; min-width: 100px;">
|
|
66
|
+
<span class="dynsim-input-value" style="background: #cfe2ff; padding: 2px 8px; border-radius: 3px; font-size: 0.85em; min-width: 40px; text-align: center; font-family: monospace;">${this.initialX.toFixed(2)}</span>
|
|
67
|
+
<button class="dynsim-reset" style="background: transparent; border: none; cursor: pointer; padding: 4px; display: flex; align-items: center;" title="Reset">
|
|
68
|
+
<svg width="20" height="20" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
|
69
|
+
<path d="M21 12a9 9 0 1 1-9 -9c2.5 0 4.8 1 6.5 2.5l.5 .5"/>
|
|
70
|
+
<path d="M21 3v6h-6"/>
|
|
71
|
+
</svg>
|
|
72
|
+
</button>
|
|
73
|
+
</div>
|
|
74
|
+
`;
|
|
75
|
+
|
|
76
|
+
// Parameter sliders row
|
|
77
|
+
html += `<div style="display: flex; gap: 12px; align-items: center;">`;
|
|
78
|
+
html += this.params.map(param => `
|
|
79
|
+
<label style="font-weight: 600; font-size: 0.85em; white-space: nowrap;">${param.label}:</label>
|
|
80
|
+
<input type="range" class="dynsim-param" data-param="${param.id}"
|
|
81
|
+
min="${param.min}" max="${param.max}" step="${param.step}" value="${param.value}"
|
|
82
|
+
style="flex: 1; height: 6px; min-width: 100px;">
|
|
83
|
+
<span class="dynsim-param-value" style="background: #e9ecef; padding: 2px 8px; border-radius: 3px; font-size: 0.85em; min-width: 40px; text-align: center; font-family: monospace;">${param.value.toFixed(2)}</span>
|
|
84
|
+
`).join('');
|
|
85
|
+
html += `
|
|
86
|
+
<button class="dynsim-pause" style="background: transparent; border: none; cursor: pointer; padding: 4px; display: flex; align-items: center;" title="Pause">
|
|
87
|
+
${SimulationView.PAUSE_ICON}
|
|
88
|
+
</button>
|
|
89
|
+
</div>`;
|
|
90
|
+
|
|
91
|
+
paramsDiv.innerHTML = html;
|
|
92
|
+
|
|
93
|
+
// Wire up slider display updates
|
|
94
|
+
paramsDiv.querySelector('.dynsim-input').addEventListener('input', (e) => {
|
|
95
|
+
e.target.closest('div').querySelector('.dynsim-input-value')
|
|
96
|
+
.textContent = parseFloat(e.target.value).toFixed(2);
|
|
97
|
+
});
|
|
98
|
+
paramsDiv.querySelectorAll('.dynsim-param').forEach(slider => {
|
|
99
|
+
slider.addEventListener('input', (e) => {
|
|
100
|
+
e.target.nextElementSibling.textContent = parseFloat(e.target.value).toFixed(2);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Wire up button callbacks
|
|
105
|
+
this.container.querySelector('.dynsim-reset')
|
|
106
|
+
.addEventListener('click', () => this.callbacks.onReset?.());
|
|
107
|
+
this.container.querySelector('.dynsim-pause')
|
|
108
|
+
.addEventListener('click', () => this.callbacks.onPauseToggle?.());
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
initPlot() {
|
|
112
|
+
if (this.plotType === '3d') {
|
|
113
|
+
Plotly.newPlot(this.plotDiv, [{
|
|
114
|
+
x: [], y: [], z: [],
|
|
115
|
+
mode: 'lines',
|
|
116
|
+
type: 'scatter3d',
|
|
117
|
+
line: { color: '#2196f3', width: 4 }
|
|
118
|
+
}], {
|
|
119
|
+
title: this.plotConfig.title,
|
|
120
|
+
scene: {
|
|
121
|
+
xaxis: { title: this.plotConfig.xaxis?.title || 'X' },
|
|
122
|
+
yaxis: { title: this.plotConfig.yaxis?.title || 'Y' },
|
|
123
|
+
zaxis: { title: this.plotConfig.zaxis?.title || 'Z' }
|
|
124
|
+
},
|
|
125
|
+
margin: { l: 0, r: 0, t: 30, b: 0 }
|
|
126
|
+
});
|
|
127
|
+
} else {
|
|
128
|
+
const layout = { margin: { l: 50, r: 20, t: 40, b: 50 } };
|
|
129
|
+
if (this.plotConfig.title) layout.title = this.plotConfig.title;
|
|
130
|
+
if (this.plotConfig.xaxis) layout.xaxis = this.plotConfig.xaxis;
|
|
131
|
+
if (this.plotConfig.yaxis) layout.yaxis = this.plotConfig.yaxis;
|
|
132
|
+
|
|
133
|
+
Plotly.newPlot(this.plotDiv, [{
|
|
134
|
+
x: [], y: [],
|
|
135
|
+
mode: 'lines',
|
|
136
|
+
line: { color: '#2196f3', width: 2 }
|
|
137
|
+
}], layout);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Read the current input slider value.
|
|
143
|
+
* @returns {number}
|
|
144
|
+
*/
|
|
145
|
+
getInput() {
|
|
146
|
+
return parseFloat(this.container.querySelector('.dynsim-input').value);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Read current parameter slider values as a dict keyed by param ID.
|
|
151
|
+
* @returns {object}
|
|
152
|
+
*/
|
|
153
|
+
getParameters() {
|
|
154
|
+
const result = {};
|
|
155
|
+
this.container.querySelectorAll('.dynsim-param').forEach(slider => {
|
|
156
|
+
result[slider.dataset.param] = parseFloat(slider.value);
|
|
157
|
+
});
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Update the Plotly chart with new data.
|
|
163
|
+
* @param {object} plotArrays - { x, y, z? } from Simulation.getPlotArrays()
|
|
164
|
+
* @param {[number, number]} [xRange] - x-axis range for timeseries
|
|
165
|
+
* @param {number[]} [spikeTimes] - spike times to render as vertical lines
|
|
166
|
+
*/
|
|
167
|
+
updatePlot(plotArrays, xRange, spikeTimes) {
|
|
168
|
+
if (this.plotType === '3d') {
|
|
169
|
+
Plotly.animate(this.plotDiv, {
|
|
170
|
+
data: [{ x: plotArrays.x, y: plotArrays.y, z: plotArrays.z }]
|
|
171
|
+
}, { transition: { duration: 0 }, frame: { duration: 0 } });
|
|
172
|
+
} else if (this.plotType === 'timeseries') {
|
|
173
|
+
const layout = {
|
|
174
|
+
title: this.plotConfig.title,
|
|
175
|
+
xaxis: { title: this.plotConfig.xaxis?.title || 'Time', range: xRange },
|
|
176
|
+
yaxis: this.plotConfig.yaxis,
|
|
177
|
+
margin: { l: 50, r: 20, t: 40, b: 50 }
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Render spike markers and threshold line
|
|
181
|
+
const shapes = [];
|
|
182
|
+
|
|
183
|
+
// Spike threshold: horizontal dashed line
|
|
184
|
+
if (this.spikeThreshold != null) {
|
|
185
|
+
shapes.push({
|
|
186
|
+
type: 'line',
|
|
187
|
+
x0: 0, x1: 1, xref: 'paper',
|
|
188
|
+
y0: this.spikeThreshold, y1: this.spikeThreshold,
|
|
189
|
+
line: { color: 'grey', width: 1, dash: 'dash' }
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Spike times: vertical lines
|
|
194
|
+
if (spikeTimes && spikeTimes.length > 0) {
|
|
195
|
+
for (const t of spikeTimes) {
|
|
196
|
+
shapes.push({
|
|
197
|
+
type: 'line',
|
|
198
|
+
x0: t, x1: t,
|
|
199
|
+
y0: 0, y1: 1, yref: 'paper',
|
|
200
|
+
line: { color: 'rgba(255, 0, 0, 0.4)', width: 1 }
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (shapes.length > 0) layout.shapes = shapes;
|
|
206
|
+
|
|
207
|
+
Plotly.react(this.plotDiv,
|
|
208
|
+
[{ x: plotArrays.x, y: plotArrays.y, mode: 'lines', line: { color: '#2196f3', width: 2 } }],
|
|
209
|
+
layout
|
|
210
|
+
);
|
|
211
|
+
} else {
|
|
212
|
+
Plotly.animate(this.plotDiv, {
|
|
213
|
+
data: [{ x: plotArrays.x, y: plotArrays.y }]
|
|
214
|
+
}, { transition: { duration: 0 }, frame: { duration: 0 } });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Update the pause button icon.
|
|
220
|
+
* @param {boolean} isRunning
|
|
221
|
+
*/
|
|
222
|
+
setPauseState(isRunning) {
|
|
223
|
+
const btn = this.container.querySelector('.dynsim-pause');
|
|
224
|
+
btn.title = isRunning ? 'Pause' : 'Play';
|
|
225
|
+
btn.innerHTML = isRunning ? SimulationView.PAUSE_ICON : SimulationView.PLAY_ICON;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
destroy() {
|
|
229
|
+
this.container.innerHTML = '';
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
SimulationView.PAUSE_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="10" y1="15" x2="10" y2="9"></line><line x1="14" y1="15" x2="14" y2="9"></line></svg>`;
|
|
234
|
+
|
|
235
|
+
SimulationView.PLAY_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polygon points="10 8 16 12 10 16 10 8"></polygon></svg>`;
|