Idle animations. Closes #14

This commit is contained in:
PROFERIS - Mi³osz Stocki
2026-07-09 14:01:00 +02:00
parent 79e0be007e
commit dd8f6b2277
6 changed files with 244 additions and 14 deletions
+1
View File
@@ -126,6 +126,7 @@
import BoardControlSection from './web/js/sections/board-control.js';
import InputTextSection from './web/js/sections/input-text.js';
import SequenceEditorSection from './web/js/sections/sequence-editor.js';
import './web/js/idle-effects.js';
// ── Register routes ────────────────────────────────────────────
Router.register('home', HomeSection);
+146
View File
@@ -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
View File
@@ -52,21 +52,39 @@ function getSteps() {
let motionQueue = [];
let isMoving = false;
let currentIsIdleMove = false;
let currentResolve = null;
let queueCheckInterval = null;
let currentMotionMode = 'direct';
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() {
if (motionQueue.length === 0) {
isMoving = false;
if (queueCheckInterval) clearInterval(queueCheckInterval);
if (queueCheckInterval) {
clearInterval(queueCheckInterval);
queueCheckInterval = null;
}
// Restore speed if it was altered
await MotionEffects.onSequenceEnd(currentMotionMode);
if (currentResolve) currentResolve(true);
currentResolve = null;
document.dispatchEvent(new CustomEvent('wiji:motion-end', { detail: { isIdleMove: currentIsIdleMove } }));
return;
}
@@ -83,11 +101,8 @@ async function executeQueue() {
await BLE.write(cmd);
} catch (e) {
UI.log(`BLE error: ${e.message}`, 'error');
motionQueue = []; // clear queue on error
isMoving = false;
if (queueCheckInterval) clearInterval(queueCheckInterval);
if (currentResolve) currentResolve(false);
currentResolve = null;
stopCurrentMove();
document.dispatchEvent(new CustomEvent('wiji:motion-end', { detail: { isIdleMove: currentIsIdleMove } }));
return;
}
@@ -102,12 +117,9 @@ async function executeQueue() {
// Check for timeout stall
if (Date.now() - waypointStartTime > CONFIG.WAYPOINT_TIMEOUT_MS) {
clearInterval(queueCheckInterval);
motionQueue = [];
isMoving = false;
stopCurrentMove();
forceUnhome('Motion timeout! Arms may be stuck. Rehoming required.');
if (currentResolve) currentResolve(false);
currentResolve = null;
document.dispatchEvent(new CustomEvent('wiji:motion-end', { detail: { isIdleMove: currentIsIdleMove } }));
return;
}
@@ -222,12 +234,18 @@ function buildSegments(startX, startY, endX, endY, mode) {
* Dynamically slices the path into segments to prevent out-of-bounds arcs.
* @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 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');
return false;
}
}
currentIsIdleMove = isIdleMove;
mode = MotionEffects.resolveMode(mode);
currentMotionMode = mode;
@@ -283,6 +301,8 @@ async function goto(targetX, targetY, mode = 'direct') {
await MotionEffects.onSequenceStart(mode);
document.dispatchEvent(new CustomEvent('wiji:motion-start', { detail: { isIdleMove: currentIsIdleMove } }));
return new Promise((resolve) => {
currentResolve = resolve;
executeQueue();
+21
View File
@@ -4,6 +4,7 @@ import IK from '../kinematics.js';
import Settings from '../settings.js';
import Motion from '../motion.js';
import { buildQuickSequencesHTML, initQuickSequences } from '../quick-sequences.js';
import { IdleEffects } from '../idle-effects.js';
let eventCleanup = [];
let isHomed = false;
@@ -161,6 +162,16 @@ function buildHTML() {
</select>
</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;">
<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;">
@@ -248,6 +259,7 @@ async function loadSpots() {
if (!parsedSpots.find(s => s.id === 'PARK')) {
parsedSpots.push({ id: 'PARK', label: 'Park Position', x: parkPosition.x, y: parkPosition.y });
}
IdleEffects.setParkPosition(parkPosition.x, parkPosition.y);
spots = parsedSpots;
document.getElementById('bg-image').src = `web/assets/backgrounds/${currentBg}/bg.svg?v=${Date.now()}`;
renderSpots();
@@ -325,6 +337,15 @@ export default {
updateHomingUI();
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 ──────────────────────────────────
const bgSelect = document.getElementById('bg-select');
if (bgSelect) {
+21
View File
@@ -4,6 +4,7 @@ import IK from '../kinematics.js';
import Settings from '../settings.js';
import Motion from '../motion.js';
import { buildQuickSequencesHTML, initQuickSequences } from '../quick-sequences.js';
import { IdleEffects } from '../idle-effects.js';
let eventCleanup = [];
let isHomed = false;
@@ -149,6 +150,16 @@ function buildHTML() {
</select>
</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 -->
<div style="background: var(--surface-2); padding: 10px; border-radius: var(--radius-sm); margin-bottom: 12px;">
<div class="setting-row">
@@ -266,6 +277,7 @@ async function loadSpots() {
if (!parsedSpots.find(s => s.id === 'PARK')) {
parsedSpots.push({ id: 'PARK', label: 'Park Position', x: parkPosition.x, y: parkPosition.y });
}
IdleEffects.setParkPosition(parkPosition.x, parkPosition.y);
spots = parsedSpots;
document.getElementById('bg-image').src = `web/assets/backgrounds/${currentBg}/bg.svg?v=${Date.now()}`;
@@ -355,6 +367,15 @@ export default {
updateHomingUI();
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 ──────────────────────────────────
const bgSelect = document.getElementById('bg-select');
if (bgSelect) {
+21
View File
@@ -4,6 +4,7 @@ import IK from '../kinematics.js';
import Settings from '../settings.js';
import SequenceRunner from '../sequence-runner.js';
import Motion from '../motion.js';
import { IdleEffects } from '../idle-effects.js';
let eventCleanup = [];
let isHomed = false;
@@ -282,6 +283,16 @@ function buildHTML() {
</select>
</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 class="setting-row">
<label class="form-label" style="margin:0;">Speed</label>
@@ -432,6 +443,7 @@ async function loadSpots() {
if (!parsedSpots.find(s => s.id === 'PARK')) {
parsedSpots.push({ id: 'PARK', label: 'Park Position', x: parkPosition.x, y: parkPosition.y });
}
IdleEffects.setParkPosition(parkPosition.x, parkPosition.y);
spots = parsedSpots;
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 ──────────────────────────────────────
const sSpeed = document.getElementById('slider-speed');
const sAccel = document.getElementById('slider-accel');