controls = function({running = false, speed = 25, min = 0, max = 1000, run = true, skip = false, random = false} = {}) {
const newSeed = 'Generate';
const startLabel = htl.html`<div class=content>
<i class="bi bi-play-fill"></i>
<span>Start</span>
</div>`;
const stopLabel = htl.html`<div class=content>
<i class="bi bi-stop-fill"></i>
<span>Stop</span>
</div>`;
const form = htl.html`<form class="controls ${run ? 'run' : ''} ${skip ? 'skip' : ''}">
${icons.cloneNode(true)}
${controlsStyle.cloneNode(true)}
<div class=container>
<div class=buttons>
<button type=button name=reset value=reset>
${run
? htl.html`<div class=content>
<i class="bi bi-skip-backward-fill"></i>
<span>Reset</span>
</div>`
: htl.html`<div class=content>
<i class="bi bi-arrow-clockwise"></i>
<span>Resample</span>
</div>`}
</button>
${run
? htl.html`<button type=button name=step value=step>
<div class=content>
<i class="bi bi-skip-end-fill" ></i>
<span>Step</span>
</div>
</button>`
: ``
}
${(run && skip)
? htl.html`<button type=button name=skip value=skip>
<div class=content>
<i class="bi bi-skip-forward-fill" ></i>
<span>Skip</span>
</div>
</button>`
: ``
}
${run
? htl.html`<button type=button name=startStop value=startStop>
${startLabel}
</button>`
: ``
}
</div>
${run
? htl.html`<label class=speed>
<div class=content>
<input type=range name=speed min=0 max=100 step=1 value=${speed}>
<span>Speed</span>
</div>
</label>`
: ``
}
${random
? htl.html`<div class=seeds>
<label>
<div class=content>
<select name=nextSeedSelect>
<option value=${newSeed} selected>${newSeed}</option>
</select>
<input type=text name=nextSeed value=${newSeed}>
<span>Next Seed</span>
</div>
</label>
<label>
<div class=content>
<input type=text name=currentSeed readonly>
<span>Current Seed</span>
</div>
</label>
</div>`
: ``
}
</div>
</form>`;
// Initialize state
let state = {
running,
skipping: false,
};
// Stop skip mode function
const stopSkip = () => {
if (state.running && state.skipping) {
form.update({stop: true});
}
};
// Event handlers
form.reset.onclick = (event) => {
form.update({reset: true});
event.stopImmediatePropagation();
}
if (run) {
form.step.onclick = (event) => {
form.update({step: true});
event.stopImmediatePropagation();
}
form.startStop.onclick = (event) => {
if (state.running) {
form.update({stop: true});
} else {
form.update({start: true});
}
event.stopImmediatePropagation();
}
form.speed.oninput = (event) => {
event.stopImmediatePropagation();
}
}
if (run && skip) {
form.skip.onclick = (event) => {
form.update({skip: true});
event.stopImmediatePropagation();
}
}
if (random) {
form.nextSeedSelect.oninput = (event) => {
form.nextSeed.value = form.nextSeedSelect.value;
event.stopImmediatePropagation();
}
form.nextSeed.oninput = (event) => {
event.stopImmediatePropagation();
}
form.currentSeed.onclick = (event) => {
navigator.clipboard.writeText(form.currentSeed.value);
form.currentSeed.nextElementSibling.classList.remove('highlight');
void form.currentSeed.nextElementSibling.offsetWidth;
form.currentSeed.nextElementSibling.classList.add('highlight');
event.stopImmediatePropagation();
}
}
// Random seed handling
const seedScale = 1000000000;
const createSeed = () => {
return Math.floor(Math.random() * seedScale).toString().padStart(9, '0');
}
const createRNG = (seed) => {
return d3.randomLcg(seed / seedScale)
}
// Process input
form.update = (action = {}) => {
// Update random seed
if (random && action.reset) {
form.currentSeed.value = (form.nextSeed.value === newSeed)
? createSeed()
: form.nextSeed.value;
if (!form.nextSeedSelect.querySelector(`[value="${form.currentSeed.value}"]`)) {
form.nextSeedSelect.children[0].after(htl.html`<option value=${form.currentSeed.value}>${form.currentSeed.value}</option>`);
}
}
// Update internal state
state = {
running: (action.start || action.skip)
? true
: action.stop
? false
: state.running,
skipping: action.skip
? true
: (action.start || action.stop)
? false
: state.skipping,
delay: run
? max - (form.speed.valueAsNumber * ((max - min) / 100))
: NaN,
random: random
? action.reset
? createRNG(form.currentSeed.value)
: state.random
: null,
seed: random
? action.reset
? form.currentSeed.value
: state.seed
:null,
};
// Update exposed value
form.value = {
...(run && skip && state.skipping ? {stopSkip} : {}),
...(run && skip ? {skip: state.skipping} : {}),
reset: action.reset
? true
: false,
...(random ? {random: state.random, seed: state.seed} : {}),
};
// Update Start/Stop button
if (action.stop) {
form.startStop.replaceChild(startLabel, stopLabel);
} else if (action.start || action.skip) {
form.startStop.replaceChild(stopLabel, startLabel);
}
// Short circuit the timer on Reset, Step, Skip, Start, and Stop
if (form.timeout && (action.reset || action.step || action.skip || action.start || action.stop)) {
clearTimeout(form.timeout);
form.timeout = null;
}
// Step the simulation on Reset, Step, or when running
if (action.reset || action.step || state.running) {
form.dispatchEvent(new CustomEvent('input', {bubbles: true}));
}
// Keep running
const resetSafety = 20;
if (state.running) {
form.timeout = setTimeout(
form.update,
(action.reset && (state.delay < resetSafety)) ? resetSafety : state.delay,
);
} else {
form.timeout = null;
}
}
// Init
Inputs.disposal(form).then(() => {
clearTimeout(form.timeout);
form.timeout = null;
});
form.update({reset: true});
return form;
}