From bd3f5d304aee814462542e0791def87255668c7d Mon Sep 17 00:00:00 2001 From: osiu97 Date: Wed, 8 Jul 2026 21:16:47 +0200 Subject: [PATCH] some updates to effects --- web/js/motion-effects.js | 120 +++++++++++++++++++++++++++++++++++++++ web/js/motion.js | 103 +++++++-------------------------- 2 files changed, 142 insertions(+), 81 deletions(-) create mode 100644 web/js/motion-effects.js diff --git a/web/js/motion-effects.js b/web/js/motion-effects.js new file mode 100644 index 0000000..f0965fd --- /dev/null +++ b/web/js/motion-effects.js @@ -0,0 +1,120 @@ +import IK from './kinematics.js'; +import Settings from './settings.js'; +import BLE from './ble.js'; + +export const MotionEffects = { + // Returns true if the mode should randomize + resolveMode(mode) { + if (mode === 'random') { + const modes = ['direct', 'snaky', 'erratic', 'jumpy', 'hesitant', 'overshoot']; + return modes[Math.floor(Math.random() * modes.length)]; + } + return mode || 'direct'; + }, + + // Called before a movement sequence starts + async onSequenceStart(mode) { + if (!BLE.isConnected()) return; + if (mode === 'jumpy' || mode === 'overshoot') { + const fastSpeed = Math.min(2000, Settings.speed * 1.5); + try { await BLE.write(`SPD:${Math.floor(fastSpeed)}`); } catch(e){} + } + }, + + // Called after a movement sequence finishes + async onSequenceEnd(mode) { + if (!BLE.isConnected()) return; + if (mode !== 'direct') { + try { await BLE.write(`SPD:${Settings.speed}`); } catch(e){} + } + }, + + // Called right before sending a waypoint to BLE + async onWaypointSend(target, hasHesitated) { + let delay = 0; + let newHasHesitated = hasHesitated; + + if (target.mode === 'hesitant' && target.fraction >= 0.45 && !hasHesitated) { + delay = 800; // Noticeably pause + newHasHesitated = true; + } else if (target.mode === 'overshoot_settle') { + delay = 200; // Brief pause before returning + if (BLE.isConnected()) { + try { await BLE.write(`SPD:${Math.floor(Math.max(100, Settings.speed * 0.4))}`); } catch(e){} + } + } + return { delay, hasHesitated: newHasHesitated }; + }, + + // Called to determine blending behaviour during queue checks + getQueueAction(mode, maxDist, blendThreshold) { + if (mode === 'erratic') { + if (maxDist <= 5) return { action: 'wait', delay: 100 }; + } else if (mode === 'jumpy') { + if (maxDist <= 5) return { action: 'wait', delay: 100 }; + } else { + if (maxDist <= blendThreshold) return { action: 'blend' }; + } + return { action: 'continue' }; + }, + + // Called to modify the final destination before slicing (e.g. overshoot) + modifyDestination(startX, startY, endX, endY, mode) { + if (mode === 'overshoot') { + const dx = endX - startX; + const dy = endY - startY; + const dist = Math.sqrt(dx * dx + dy * dy); + if (dist > 0) { + let nx = endX + (dx / dist) * 20; // Overshoot 20mm + let ny = endY + (dy / dist) * 20; + if (IK.checkWorkspace(nx, ny).ok) { + return { endX: nx, endY: ny }; + } + } + } + return { endX, endY }; + }, + + // Called to determine segment count + getSegmentCount(distance, baseSegmentSize, mode) { + let count = Math.ceil(distance / baseSegmentSize); + if (mode === 'snaky') { + count = Math.max(count, Math.ceil(distance / 10)); // fine resolution for sine + } else if (mode === 'erratic') { + count = Math.max(count, Math.ceil(distance / 20)); // fewer, larger jumps + } + return Math.max(1, count); + }, + + // Called to apply per-waypoint visual perturbations + applyPerturbation(px, py, nx, ny, fraction, distance, mode) { + if (mode === 'snaky') { + const cycles = Math.max(1, Math.floor(distance / 50)); + const offset = Math.sin(fraction * Math.PI * 2 * cycles) * 15; + px += nx * offset; + py += ny * offset; + } else if (mode === 'erratic') { + // Meaningful jumps + px += (Math.random() - 0.5) * 20; + py += (Math.random() - 0.5) * 20; + } + return { px, py }; + }, + + // Generate extra waypoints at the end (e.g. settle from overshoot) + getSettlingWaypoints(endX, endY, mode) { + const segs = []; + if (mode === 'overshoot') { + const res = IK.solve(endX, endY); + if (res.reachable && !IK.armsCrossed(res.theta1, res.theta2)) { + segs.push({ + s1: IK.radToStepsAbsolute(res.theta1, 1), + s2: IK.radToStepsAbsolute(res.theta2, 2), + mode: 'overshoot_settle', + fraction: 1.1 + }); + } + } + return segs; + } +}; diff --git a/web/js/motion.js b/web/js/motion.js index afa556a..1053c7a 100644 --- a/web/js/motion.js +++ b/web/js/motion.js @@ -2,6 +2,7 @@ import BLE from './ble.js'; import IK from './kinematics.js'; import UI from './ui.js'; import Settings from './settings.js'; +import { MotionEffects } from './motion-effects.js'; const CONFIG = { SEGMENT_SIZE_MM: 25, // Maximum distance between waypoints to prevent out-of-bounds joint-space arcs @@ -57,9 +58,7 @@ async function executeQueue() { if (queueCheckInterval) clearInterval(queueCheckInterval); // Restore speed if it was altered - if (BLE.isConnected() && ['jumpy', 'hesitant', 'overshoot'].includes(currentMotionMode)) { - try { await BLE.write(`SPD:${Settings.speed}`); } catch(e){} - } + await MotionEffects.onSequenceEnd(currentMotionMode); if (currentResolve) currentResolve(true); currentResolve = null; @@ -69,20 +68,12 @@ async function executeQueue() { isMoving = true; let target = motionQueue.shift(); - let delayBeforeSend = 0; - if (target.mode === 'hesitant' && target.fraction >= 0.45 && !hasHesitated) { - hasHesitated = true; - delayBeforeSend = 400; - } else if (target.mode === 'overshoot_settle') { - delayBeforeSend = 200; // brief pause before returning - } + const { delay: delayBeforeSend, hasHesitated: newHasHesitated } = await MotionEffects.onWaypointSend(target, hasHesitated); + hasHesitated = newHasHesitated; const doSend = async () => { const cmd = `X:${target.s1},${target.s2}`; if (BLE.isConnected()) { - if (target.mode === 'overshoot_settle') { - try { await BLE.write(`SPD:${Math.floor(Math.max(100, Settings.speed * 0.4))}`); } catch(e){} - } try { await BLE.write(cmd); } catch (e) { @@ -104,23 +95,13 @@ async function executeQueue() { const maxDist = Math.max(dist1, dist2); if (motionQueue.length > 0) { - // We have more waypoints. Blend or wait depending on mode. - if (target.mode === 'erratic') { - if (maxDist <= 5) { // wait for full stop - clearInterval(queueCheckInterval); - setTimeout(executeQueue, 50 + Math.random() * 100); - } - } else if (target.mode === 'jumpy') { - if (maxDist <= 5) { // stop and go rhythm - clearInterval(queueCheckInterval); - setTimeout(executeQueue, 100); - } - } else { - // direct, snaky, hesitant, etc default to direct blending - if (maxDist <= CONFIG.BLEND_THRESHOLD_STEPS) { - clearInterval(queueCheckInterval); - executeQueue(); - } + const queueAction = MotionEffects.getQueueAction(target.mode, maxDist, CONFIG.BLEND_THRESHOLD_STEPS); + if (queueAction.action === 'wait') { + clearInterval(queueCheckInterval); + setTimeout(executeQueue, queueAction.delay); + } else if (queueAction.action === 'blend') { + clearInterval(queueCheckInterval); + executeQueue(); } } else { // Last waypoint. Wait until closely reached. @@ -159,36 +140,16 @@ async function executeQueue() { } function buildSegments(startX, startY, endX, endY, mode) { - let actualEndX = endX; - let actualEndY = endY; - - if (mode === 'overshoot') { - const totalDx = endX - startX; - const totalDy = endY - startY; - const totalDist = Math.sqrt(totalDx * totalDx + totalDy * totalDy); - if (totalDist > 0) { - actualEndX = endX + (totalDx / totalDist) * 15; - actualEndY = endY + (totalDy / totalDist) * 15; - - const wsCheck = IK.checkWorkspace(actualEndX, actualEndY); - if (!wsCheck.ok) { - actualEndX = endX; - actualEndY = endY; - } - } - } + const modDest = MotionEffects.modifyDestination(startX, startY, endX, endY, mode); + const actualEndX = modDest.endX; + const actualEndY = modDest.endY; const dx = actualEndX - startX; const dy = actualEndY - startY; const distance = Math.sqrt(dx * dx + dy * dy); - let numSegments = Math.ceil(distance / CONFIG.SEGMENT_SIZE_MM); + let numSegments = MotionEffects.getSegmentCount(distance, CONFIG.SEGMENT_SIZE_MM, mode); - if (mode === 'snaky' || mode === 'erratic') { - numSegments = Math.max(numSegments, Math.ceil(distance / 10)); // 10mm segments max for finer resolution - } - if (numSegments === 0) numSegments = 1; - let segs = []; const nx = -dy / distance || 0; @@ -199,15 +160,9 @@ function buildSegments(startX, startY, endX, endY, mode) { let px = startX + dx * fraction; let py = startY + dy * fraction; - if (mode === 'snaky') { - const cycles = Math.max(1, Math.floor(distance / 50)); - const offset = Math.sin(fraction * Math.PI * 2 * cycles) * 15; - px += nx * offset; - py += ny * offset; - } else if (mode === 'erratic') { - px += (Math.random() - 0.5) * 10; - py += (Math.random() - 0.5) * 10; - } + const perturbed = MotionEffects.applyPerturbation(px, py, nx, ny, fraction, distance, mode); + px = perturbed.px; + py = perturbed.py; const wsCheck = IK.checkWorkspace(px, py); if (!wsCheck.ok) { @@ -238,14 +193,8 @@ function buildSegments(startX, startY, endX, endY, mode) { segs.push({ s1, s2, mode, fraction }); } - if (mode === 'overshoot') { - const res = IK.solve(endX, endY); - if (res.reachable && !IK.armsCrossed(res.theta1, res.theta2)) { - const s1 = IK.radToStepsAbsolute(res.theta1, 1); - const s2 = IK.radToStepsAbsolute(res.theta2, 2); - segs.push({ s1, s2, mode: 'overshoot_settle', fraction: 1.1 }); - } - } + const settlingSegs = MotionEffects.getSettlingWaypoints(endX, endY, mode); + segs = segs.concat(settlingSegs); return segs; } @@ -261,10 +210,7 @@ async function goto(targetX, targetY, mode = 'direct') { return false; } - if (mode === 'random') { - const modes = ['direct', 'snaky', 'erratic', 'jumpy', 'hesitant', 'overshoot']; - mode = modes[Math.floor(Math.random() * modes.length)]; - } + mode = MotionEffects.resolveMode(mode); currentMotionMode = mode; hasHesitated = false; @@ -317,12 +263,7 @@ async function goto(targetX, targetY, mode = 'direct') { motionQueue = segments; - if (BLE.isConnected()) { - if (mode === 'jumpy' || mode === 'hesitant') { - const fastSpeed = Math.min(2000, Settings.speed * 1.5); - try { await BLE.write(`SPD:${Math.floor(fastSpeed)}`); } catch(e){} - } - } + await MotionEffects.onSequenceStart(mode); return new Promise((resolve) => { currentResolve = resolve;