Idle animations. Closes #14
This commit is contained in:
@@ -126,6 +126,7 @@
|
|||||||
import BoardControlSection from './web/js/sections/board-control.js';
|
import BoardControlSection from './web/js/sections/board-control.js';
|
||||||
import InputTextSection from './web/js/sections/input-text.js';
|
import InputTextSection from './web/js/sections/input-text.js';
|
||||||
import SequenceEditorSection from './web/js/sections/sequence-editor.js';
|
import SequenceEditorSection from './web/js/sections/sequence-editor.js';
|
||||||
|
import './web/js/idle-effects.js';
|
||||||
|
|
||||||
// ── Register routes ────────────────────────────────────────────
|
// ── Register routes ────────────────────────────────────────────
|
||||||
Router.register('home', HomeSection);
|
Router.register('home', HomeSection);
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
import IK from './kinematics.js';
|
||||||
|
import Motion from './motion.js';
|
||||||
|
|
||||||
|
let activeMode = 'none'; // 'none', 'circles', 'erratic', 'park-bounce'
|
||||||
|
let parkPosition = { x: 0, y: 80 };
|
||||||
|
let anchorPos = { x: 0, y: 80 };
|
||||||
|
let timerId = null;
|
||||||
|
let currentAngle = 0;
|
||||||
|
let bounceState = 'to-park'; // 'to-park' or 'to-anchor'
|
||||||
|
|
||||||
|
const IDLE_WAIT_MS = 5000;
|
||||||
|
const CONTINUOUS_WAIT_MS = 500; // 0.5s pause between idle movements
|
||||||
|
const RADIUS = 20;
|
||||||
|
|
||||||
|
export const IdleEffects = {
|
||||||
|
setMode(mode) {
|
||||||
|
activeMode = mode || 'none';
|
||||||
|
localStorage.setItem('wiji_idle_mode', activeMode);
|
||||||
|
|
||||||
|
// Update all dropdowns in UI to keep them in sync
|
||||||
|
document.querySelectorAll('.idle-mode-select').forEach(el => {
|
||||||
|
if (el.value !== activeMode) el.value = activeMode;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (activeMode === 'none') {
|
||||||
|
this.stopTimer();
|
||||||
|
} else {
|
||||||
|
if (!Motion.isMoving && localStorage.getItem('wiji_homed') === 'true') {
|
||||||
|
this.updateAnchorPos();
|
||||||
|
this.startTimer(IDLE_WAIT_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getMode() {
|
||||||
|
return activeMode;
|
||||||
|
},
|
||||||
|
|
||||||
|
setParkPosition(x, y) {
|
||||||
|
parkPosition = { x, y };
|
||||||
|
},
|
||||||
|
|
||||||
|
updateAnchorPos() {
|
||||||
|
const { steps1, steps2 } = Motion.getSteps();
|
||||||
|
const pos = IK.forward(IK.stepsToRad(steps1), IK.stepsToRad(steps2));
|
||||||
|
if (pos.valid) {
|
||||||
|
anchorPos = { x: pos.endX, y: pos.endY };
|
||||||
|
currentAngle = 0;
|
||||||
|
bounceState = 'to-park';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
startTimer(ms) {
|
||||||
|
this.stopTimer();
|
||||||
|
if (activeMode === 'none') return;
|
||||||
|
|
||||||
|
timerId = setTimeout(() => {
|
||||||
|
this.triggerNextMove();
|
||||||
|
}, ms);
|
||||||
|
},
|
||||||
|
|
||||||
|
stopTimer() {
|
||||||
|
if (timerId) {
|
||||||
|
clearTimeout(timerId);
|
||||||
|
timerId = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async triggerNextMove() {
|
||||||
|
if (activeMode === 'none' || localStorage.getItem('wiji_homed') !== 'true') return;
|
||||||
|
|
||||||
|
let targetX = anchorPos.x;
|
||||||
|
let targetY = anchorPos.y;
|
||||||
|
let motionMode = 'direct';
|
||||||
|
|
||||||
|
if (activeMode === 'circles') {
|
||||||
|
currentAngle += Math.PI / 4; // 45 degrees
|
||||||
|
targetX = anchorPos.x + RADIUS * Math.cos(currentAngle);
|
||||||
|
targetY = anchorPos.y + RADIUS * Math.sin(currentAngle);
|
||||||
|
motionMode = 'direct';
|
||||||
|
} else if (activeMode === 'erratic') {
|
||||||
|
const randAngle = Math.random() * Math.PI * 2;
|
||||||
|
const randR = Math.random() * RADIUS;
|
||||||
|
targetX = anchorPos.x + randR * Math.cos(randAngle);
|
||||||
|
targetY = anchorPos.y + randR * Math.sin(randAngle);
|
||||||
|
motionMode = 'erratic';
|
||||||
|
} else if (activeMode === 'park-bounce') {
|
||||||
|
if (bounceState === 'to-park') {
|
||||||
|
targetX = parkPosition.x;
|
||||||
|
targetY = parkPosition.y;
|
||||||
|
bounceState = 'to-anchor';
|
||||||
|
} else {
|
||||||
|
targetX = anchorPos.x;
|
||||||
|
targetY = anchorPos.y;
|
||||||
|
bounceState = 'to-park';
|
||||||
|
}
|
||||||
|
motionMode = 'direct';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Workspace check
|
||||||
|
const check = IK.checkWorkspace(targetX, targetY);
|
||||||
|
if (!check.ok) {
|
||||||
|
// If out of bounds, skip this move and wait for next
|
||||||
|
this.startTimer(CONTINUOUS_WAIT_MS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fire the idle move!
|
||||||
|
try {
|
||||||
|
await Motion.goto(targetX, targetY, motionMode, true);
|
||||||
|
} catch(e) {
|
||||||
|
// If aborted, it's fine.
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
init() {
|
||||||
|
const saved = localStorage.getItem('wiji_idle_mode');
|
||||||
|
if (saved) activeMode = saved;
|
||||||
|
|
||||||
|
document.addEventListener('wiji:motion-start', (e) => {
|
||||||
|
const isIdleMove = e.detail && e.detail.isIdleMove;
|
||||||
|
if (!isIdleMove) {
|
||||||
|
this.stopTimer();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('wiji:motion-end', (e) => {
|
||||||
|
if (activeMode === 'none' || localStorage.getItem('wiji_homed') !== 'true') return;
|
||||||
|
|
||||||
|
const isIdleMove = e.detail && e.detail.isIdleMove;
|
||||||
|
if (!isIdleMove) {
|
||||||
|
this.updateAnchorPos();
|
||||||
|
this.startTimer(IDLE_WAIT_MS);
|
||||||
|
} else {
|
||||||
|
this.startTimer(CONTINUOUS_WAIT_MS);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('wiji:unhomed', () => {
|
||||||
|
this.setMode('none');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-init globally
|
||||||
|
IdleEffects.init();
|
||||||
+32
-12
@@ -52,21 +52,39 @@ function getSteps() {
|
|||||||
|
|
||||||
let motionQueue = [];
|
let motionQueue = [];
|
||||||
let isMoving = false;
|
let isMoving = false;
|
||||||
|
let currentIsIdleMove = false;
|
||||||
let currentResolve = null;
|
let currentResolve = null;
|
||||||
let queueCheckInterval = null;
|
let queueCheckInterval = null;
|
||||||
let currentMotionMode = 'direct';
|
let currentMotionMode = 'direct';
|
||||||
let hasHesitated = false;
|
let hasHesitated = false;
|
||||||
|
|
||||||
|
function stopCurrentMove() {
|
||||||
|
motionQueue = [];
|
||||||
|
isMoving = false;
|
||||||
|
if (queueCheckInterval) {
|
||||||
|
clearInterval(queueCheckInterval);
|
||||||
|
queueCheckInterval = null;
|
||||||
|
}
|
||||||
|
if (currentResolve) {
|
||||||
|
currentResolve(false); // resolve false because aborted
|
||||||
|
currentResolve = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function executeQueue() {
|
async function executeQueue() {
|
||||||
if (motionQueue.length === 0) {
|
if (motionQueue.length === 0) {
|
||||||
isMoving = false;
|
isMoving = false;
|
||||||
if (queueCheckInterval) clearInterval(queueCheckInterval);
|
if (queueCheckInterval) {
|
||||||
|
clearInterval(queueCheckInterval);
|
||||||
|
queueCheckInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
// Restore speed if it was altered
|
// Restore speed if it was altered
|
||||||
await MotionEffects.onSequenceEnd(currentMotionMode);
|
await MotionEffects.onSequenceEnd(currentMotionMode);
|
||||||
|
|
||||||
if (currentResolve) currentResolve(true);
|
if (currentResolve) currentResolve(true);
|
||||||
currentResolve = null;
|
currentResolve = null;
|
||||||
|
document.dispatchEvent(new CustomEvent('wiji:motion-end', { detail: { isIdleMove: currentIsIdleMove } }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,11 +101,8 @@ async function executeQueue() {
|
|||||||
await BLE.write(cmd);
|
await BLE.write(cmd);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
UI.log(`BLE error: ${e.message}`, 'error');
|
UI.log(`BLE error: ${e.message}`, 'error');
|
||||||
motionQueue = []; // clear queue on error
|
stopCurrentMove();
|
||||||
isMoving = false;
|
document.dispatchEvent(new CustomEvent('wiji:motion-end', { detail: { isIdleMove: currentIsIdleMove } }));
|
||||||
if (queueCheckInterval) clearInterval(queueCheckInterval);
|
|
||||||
if (currentResolve) currentResolve(false);
|
|
||||||
currentResolve = null;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,12 +117,9 @@ async function executeQueue() {
|
|||||||
|
|
||||||
// Check for timeout stall
|
// Check for timeout stall
|
||||||
if (Date.now() - waypointStartTime > CONFIG.WAYPOINT_TIMEOUT_MS) {
|
if (Date.now() - waypointStartTime > CONFIG.WAYPOINT_TIMEOUT_MS) {
|
||||||
clearInterval(queueCheckInterval);
|
stopCurrentMove();
|
||||||
motionQueue = [];
|
|
||||||
isMoving = false;
|
|
||||||
forceUnhome('Motion timeout! Arms may be stuck. Rehoming required.');
|
forceUnhome('Motion timeout! Arms may be stuck. Rehoming required.');
|
||||||
if (currentResolve) currentResolve(false);
|
document.dispatchEvent(new CustomEvent('wiji:motion-end', { detail: { isIdleMove: currentIsIdleMove } }));
|
||||||
currentResolve = null;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,12 +234,18 @@ function buildSegments(startX, startY, endX, endY, mode) {
|
|||||||
* Dynamically slices the path into segments to prevent out-of-bounds arcs.
|
* Dynamically slices the path into segments to prevent out-of-bounds arcs.
|
||||||
* @returns {Promise<boolean>} Resolves to true if successful, false if aborted/error.
|
* @returns {Promise<boolean>} Resolves to true if successful, false if aborted/error.
|
||||||
*/
|
*/
|
||||||
async function goto(targetX, targetY, mode = 'direct') {
|
async function goto(targetX, targetY, mode = 'direct', isIdleMove = false) {
|
||||||
if (isMoving) {
|
if (isMoving) {
|
||||||
|
// If the currently executing move is an idle move, and this is a user move, we can interrupt it.
|
||||||
|
if (currentIsIdleMove && !isIdleMove) {
|
||||||
|
stopCurrentMove(); // Abort the idle move
|
||||||
|
} else {
|
||||||
UI.log('Arm is currently moving! Wait for it to finish.', 'warn');
|
UI.log('Arm is currently moving! Wait for it to finish.', 'warn');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentIsIdleMove = isIdleMove;
|
||||||
mode = MotionEffects.resolveMode(mode);
|
mode = MotionEffects.resolveMode(mode);
|
||||||
|
|
||||||
currentMotionMode = mode;
|
currentMotionMode = mode;
|
||||||
@@ -283,6 +301,8 @@ async function goto(targetX, targetY, mode = 'direct') {
|
|||||||
|
|
||||||
await MotionEffects.onSequenceStart(mode);
|
await MotionEffects.onSequenceStart(mode);
|
||||||
|
|
||||||
|
document.dispatchEvent(new CustomEvent('wiji:motion-start', { detail: { isIdleMove: currentIsIdleMove } }));
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
currentResolve = resolve;
|
currentResolve = resolve;
|
||||||
executeQueue();
|
executeQueue();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import IK from '../kinematics.js';
|
|||||||
import Settings from '../settings.js';
|
import Settings from '../settings.js';
|
||||||
import Motion from '../motion.js';
|
import Motion from '../motion.js';
|
||||||
import { buildQuickSequencesHTML, initQuickSequences } from '../quick-sequences.js';
|
import { buildQuickSequencesHTML, initQuickSequences } from '../quick-sequences.js';
|
||||||
|
import { IdleEffects } from '../idle-effects.js';
|
||||||
|
|
||||||
let eventCleanup = [];
|
let eventCleanup = [];
|
||||||
let isHomed = false;
|
let isHomed = false;
|
||||||
@@ -161,6 +162,16 @@ function buildHTML() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="margin-bottom: 12px;">
|
||||||
|
<label class="form-label">Idle Animation</label>
|
||||||
|
<select id="bc-idle-mode-select" class="form-input idle-mode-select" style="width:100%;">
|
||||||
|
<option value="none">None</option>
|
||||||
|
<option value="circles">Circles</option>
|
||||||
|
<option value="erratic">Erratic</option>
|
||||||
|
<option value="park-bounce">Park Bounce</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group" style="flex:1; display:flex; flex-direction:column;">
|
<div class="form-group" style="flex:1; display:flex; flex-direction:column;">
|
||||||
<label class="form-label">Available Spots</label>
|
<label class="form-label">Available Spots</label>
|
||||||
<select id="spot-list" size="10" class="form-input" style="flex:1; width: 100%; margin-bottom: 12px; font-family: monospace; font-size: 1rem; padding: 8px;">
|
<select id="spot-list" size="10" class="form-input" style="flex:1; width: 100%; margin-bottom: 12px; font-family: monospace; font-size: 1rem; padding: 8px;">
|
||||||
@@ -248,6 +259,7 @@ async function loadSpots() {
|
|||||||
if (!parsedSpots.find(s => s.id === 'PARK')) {
|
if (!parsedSpots.find(s => s.id === 'PARK')) {
|
||||||
parsedSpots.push({ id: 'PARK', label: 'Park Position', x: parkPosition.x, y: parkPosition.y });
|
parsedSpots.push({ id: 'PARK', label: 'Park Position', x: parkPosition.x, y: parkPosition.y });
|
||||||
}
|
}
|
||||||
|
IdleEffects.setParkPosition(parkPosition.x, parkPosition.y);
|
||||||
spots = parsedSpots;
|
spots = parsedSpots;
|
||||||
document.getElementById('bg-image').src = `web/assets/backgrounds/${currentBg}/bg.svg?v=${Date.now()}`;
|
document.getElementById('bg-image').src = `web/assets/backgrounds/${currentBg}/bg.svg?v=${Date.now()}`;
|
||||||
renderSpots();
|
renderSpots();
|
||||||
@@ -325,6 +337,15 @@ export default {
|
|||||||
updateHomingUI();
|
updateHomingUI();
|
||||||
loadSpots();
|
loadSpots();
|
||||||
|
|
||||||
|
// ── Idle Effects Selection ──────────────────────────────────
|
||||||
|
const idleSelect = document.getElementById('bc-idle-mode-select');
|
||||||
|
if (idleSelect) {
|
||||||
|
idleSelect.value = IdleEffects.getMode();
|
||||||
|
idleSelect.addEventListener('change', () => {
|
||||||
|
IdleEffects.setMode(idleSelect.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ── Background Selection ──────────────────────────────────
|
// ── Background Selection ──────────────────────────────────
|
||||||
const bgSelect = document.getElementById('bg-select');
|
const bgSelect = document.getElementById('bg-select');
|
||||||
if (bgSelect) {
|
if (bgSelect) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import IK from '../kinematics.js';
|
|||||||
import Settings from '../settings.js';
|
import Settings from '../settings.js';
|
||||||
import Motion from '../motion.js';
|
import Motion from '../motion.js';
|
||||||
import { buildQuickSequencesHTML, initQuickSequences } from '../quick-sequences.js';
|
import { buildQuickSequencesHTML, initQuickSequences } from '../quick-sequences.js';
|
||||||
|
import { IdleEffects } from '../idle-effects.js';
|
||||||
|
|
||||||
let eventCleanup = [];
|
let eventCleanup = [];
|
||||||
let isHomed = false;
|
let isHomed = false;
|
||||||
@@ -149,6 +150,16 @@ function buildHTML() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="margin-bottom: 12px;">
|
||||||
|
<label class="form-label">Idle Animation</label>
|
||||||
|
<select id="it-idle-mode-select" class="form-input idle-mode-select" style="width:100%;">
|
||||||
|
<option value="none">None</option>
|
||||||
|
<option value="circles">Circles</option>
|
||||||
|
<option value="erratic">Erratic</option>
|
||||||
|
<option value="park-bounce">Park Bounce</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Settings -->
|
<!-- Settings -->
|
||||||
<div style="background: var(--surface-2); padding: 10px; border-radius: var(--radius-sm); margin-bottom: 12px;">
|
<div style="background: var(--surface-2); padding: 10px; border-radius: var(--radius-sm); margin-bottom: 12px;">
|
||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
@@ -266,6 +277,7 @@ async function loadSpots() {
|
|||||||
if (!parsedSpots.find(s => s.id === 'PARK')) {
|
if (!parsedSpots.find(s => s.id === 'PARK')) {
|
||||||
parsedSpots.push({ id: 'PARK', label: 'Park Position', x: parkPosition.x, y: parkPosition.y });
|
parsedSpots.push({ id: 'PARK', label: 'Park Position', x: parkPosition.x, y: parkPosition.y });
|
||||||
}
|
}
|
||||||
|
IdleEffects.setParkPosition(parkPosition.x, parkPosition.y);
|
||||||
spots = parsedSpots;
|
spots = parsedSpots;
|
||||||
|
|
||||||
document.getElementById('bg-image').src = `web/assets/backgrounds/${currentBg}/bg.svg?v=${Date.now()}`;
|
document.getElementById('bg-image').src = `web/assets/backgrounds/${currentBg}/bg.svg?v=${Date.now()}`;
|
||||||
@@ -355,6 +367,15 @@ export default {
|
|||||||
updateHomingUI();
|
updateHomingUI();
|
||||||
loadSpots();
|
loadSpots();
|
||||||
|
|
||||||
|
// ── Idle Effects Selection ──────────────────────────────────
|
||||||
|
const idleSelect = document.getElementById('it-idle-mode-select');
|
||||||
|
if (idleSelect) {
|
||||||
|
idleSelect.value = IdleEffects.getMode();
|
||||||
|
idleSelect.addEventListener('change', () => {
|
||||||
|
IdleEffects.setMode(idleSelect.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ── Background Selection ──────────────────────────────────
|
// ── Background Selection ──────────────────────────────────
|
||||||
const bgSelect = document.getElementById('bg-select');
|
const bgSelect = document.getElementById('bg-select');
|
||||||
if (bgSelect) {
|
if (bgSelect) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import IK from '../kinematics.js';
|
|||||||
import Settings from '../settings.js';
|
import Settings from '../settings.js';
|
||||||
import SequenceRunner from '../sequence-runner.js';
|
import SequenceRunner from '../sequence-runner.js';
|
||||||
import Motion from '../motion.js';
|
import Motion from '../motion.js';
|
||||||
|
import { IdleEffects } from '../idle-effects.js';
|
||||||
|
|
||||||
let eventCleanup = [];
|
let eventCleanup = [];
|
||||||
let isHomed = false;
|
let isHomed = false;
|
||||||
@@ -282,6 +283,16 @@ function buildHTML() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="margin-bottom: 12px;">
|
||||||
|
<label class="form-label">Idle Animation</label>
|
||||||
|
<select id="se-idle-mode-select" class="form-input idle-mode-select" style="width:100%;">
|
||||||
|
<option value="none">None</option>
|
||||||
|
<option value="circles">Circles</option>
|
||||||
|
<option value="erratic">Erratic</option>
|
||||||
|
<option value="park-bounce">Park Bounce</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style="background: var(--surface-2); padding: 10px; border-radius: var(--radius-sm);">
|
<div style="background: var(--surface-2); padding: 10px; border-radius: var(--radius-sm);">
|
||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
<label class="form-label" style="margin:0;">Speed</label>
|
<label class="form-label" style="margin:0;">Speed</label>
|
||||||
@@ -432,6 +443,7 @@ async function loadSpots() {
|
|||||||
if (!parsedSpots.find(s => s.id === 'PARK')) {
|
if (!parsedSpots.find(s => s.id === 'PARK')) {
|
||||||
parsedSpots.push({ id: 'PARK', label: 'Park Position', x: parkPosition.x, y: parkPosition.y });
|
parsedSpots.push({ id: 'PARK', label: 'Park Position', x: parkPosition.x, y: parkPosition.y });
|
||||||
}
|
}
|
||||||
|
IdleEffects.setParkPosition(parkPosition.x, parkPosition.y);
|
||||||
spots = parsedSpots;
|
spots = parsedSpots;
|
||||||
|
|
||||||
document.getElementById('text-input-row').style.display = hasAlphabet ? 'grid' : 'none';
|
document.getElementById('text-input-row').style.display = hasAlphabet ? 'grid' : 'none';
|
||||||
@@ -578,6 +590,15 @@ export default {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Idle Effects Selection ──────────────────────────────────
|
||||||
|
const idleSelect = document.getElementById('se-idle-mode-select');
|
||||||
|
if (idleSelect) {
|
||||||
|
idleSelect.value = IdleEffects.getMode();
|
||||||
|
idleSelect.addEventListener('change', () => {
|
||||||
|
IdleEffects.setMode(idleSelect.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ── Settings Sliders ──────────────────────────────────────
|
// ── Settings Sliders ──────────────────────────────────────
|
||||||
const sSpeed = document.getElementById('slider-speed');
|
const sSpeed = document.getElementById('slider-speed');
|
||||||
const sAccel = document.getElementById('slider-accel');
|
const sAccel = document.getElementById('slider-accel');
|
||||||
|
|||||||
Reference in New Issue
Block a user