39282-vm/assets/js/main.js
2026-03-23 19:22:21 +00:00

308 lines
12 KiB
JavaScript

document.addEventListener('DOMContentLoaded', () => {
const appConfig = window.appConfig || {};
const toastElement = document.getElementById('app-toast');
const toastMessage = document.getElementById('toast-message');
const toast = toastElement && window.bootstrap ? new window.bootstrap.Toast(toastElement, { delay: 3200 }) : null;
const controls = {
angle: document.getElementById('angle_deg'),
intensity: document.getElementById('intensity_pct'),
pattern: document.getElementById('pattern_mode'),
name: document.getElementById('name'),
notes: document.getElementById('notes')
};
const ui = {
figure: document.getElementById('recliner-figure'),
angleValue: document.getElementById('angle-value'),
anglePill: document.getElementById('angle-pill'),
intensityPill: document.getElementById('intensity-pill'),
statAngle: document.getElementById('stat-angle'),
statIntensity: document.getElementById('stat-intensity'),
statPattern: document.getElementById('stat-pattern'),
saveAngle: document.getElementById('save-angle'),
saveIntensity: document.getElementById('save-intensity'),
savePattern: document.getElementById('save-pattern'),
summaryAngle: document.getElementById('summary-angle'),
summaryIntensity: document.getElementById('summary-intensity'),
summaryPattern: document.getElementById('summary-pattern'),
reclineMode: document.getElementById('recline-mode'),
gamepadState: document.getElementById('gamepad-state'),
gamepadSelect: document.getElementById('gamepad-select'),
testButton: document.getElementById('test-vibration'),
resetButton: document.getElementById('reset-defaults')
};
let knownGamepads = [];
let scanTimer = null;
let continuousVibrationInterval = null;
const notify = (message) => {
if (!message) {
return;
}
if (toastMessage) {
toastMessage.textContent = message;
}
if (toast) {
toast.show();
}
};
const presetTone = (angle) => {
if (angle >= 135) return 'Deep recline';
if (angle >= 90) return 'Lounge';
if (angle >= 45) return 'Reading';
return 'Upright';
};
const currentState = () => ({
angle: Number(controls.angle?.value || 0),
intensity: Number(controls.intensity?.value || 0),
pattern: controls.pattern?.value || 'continuous'
});
const updateVisualization = () => {
const state = currentState();
const tone = presetTone(state.angle);
if (ui.figure) {
ui.figure.style.setProperty('--angle', String(state.angle));
ui.figure.style.setProperty('--intensity', String(state.intensity));
}
const map = {
angleValue: `${state.angle}`,
anglePill: `${state.angle}°`,
intensityPill: `${state.intensity}%`,
statAngle: `${state.angle}°`,
statIntensity: `${state.intensity}%`,
statPattern: capitalize(state.pattern),
saveAngle: `${state.angle}°`,
saveIntensity: `${state.intensity}%`,
savePattern: capitalize(state.pattern),
summaryAngle: `${state.angle}°`,
summaryIntensity: `${state.intensity}%`,
summaryPattern: capitalize(state.pattern),
reclineMode: tone
};
Object.entries(map).forEach(([key, value]) => {
if (ui[key]) {
ui[key].textContent = value;
}
});
};
const capitalize = (value) => value.charAt(0).toUpperCase() + value.slice(1);
const applyPresetState = (preset) => {
if (!preset) return;
if (controls.angle && preset.angle_deg !== undefined) controls.angle.value = preset.angle_deg;
if (controls.intensity && preset.intensity_pct !== undefined) controls.intensity.value = preset.intensity_pct;
if (controls.pattern && preset.pattern_mode) controls.pattern.value = preset.pattern_mode;
if (controls.name && preset.name !== undefined) controls.name.value = preset.name;
if (controls.notes && preset.notes !== undefined) controls.notes.value = preset.notes;
updateVisualization();
};
const getActuator = (gamepad) => {
if (!gamepad) return null;
if (gamepad.vibrationActuator) return gamepad.vibrationActuator;
if (Array.isArray(gamepad.hapticActuators) && gamepad.hapticActuators.length > 0) {
return gamepad.hapticActuators[0];
}
return null;
};
const selectedGamepad = () => {
if (!ui.gamepadSelect || !knownGamepads.length) return null;
const selectedIndex = Number(ui.gamepadSelect.value || knownGamepads[0].index);
return knownGamepads.find((gamepad) => gamepad.index === selectedIndex) || knownGamepads[0];
};
const refreshGamepads = () => {
if (!navigator.getGamepads) {
if (ui.gamepadState) {
ui.gamepadState.textContent = 'Gamepad API unavailable';
}
if (ui.gamepadSelect) {
ui.gamepadSelect.innerHTML = '<option>Unsupported browser</option>';
ui.gamepadSelect.disabled = true;
}
if (ui.testButton) {
ui.testButton.disabled = true;
}
return;
}
knownGamepads = Array.from(navigator.getGamepads()).filter(Boolean);
const previousValue = ui.gamepadSelect ? ui.gamepadSelect.value : '';
if (ui.gamepadSelect) {
ui.gamepadSelect.innerHTML = '';
if (!knownGamepads.length) {
const option = document.createElement('option');
option.textContent = 'No controller detected';
option.value = '';
ui.gamepadSelect.appendChild(option);
ui.gamepadSelect.disabled = true;
} else {
knownGamepads.forEach((gamepad) => {
const option = document.createElement('option');
const supported = getActuator(gamepad) ? 'haptics ready' : 'no haptics';
option.value = String(gamepad.index);
option.textContent = `${gamepad.id} (${supported})`;
ui.gamepadSelect.appendChild(option);
});
ui.gamepadSelect.disabled = false;
const hasPrevious = knownGamepads.some((gamepad) => String(gamepad.index) === previousValue);
ui.gamepadSelect.value = hasPrevious ? previousValue : String(knownGamepads[0].index);
}
}
const active = selectedGamepad();
const actuator = getActuator(active);
if (ui.gamepadState) {
if (!active) {
ui.gamepadState.textContent = 'Connect a controller to enable rumble';
} else if (!actuator) {
ui.gamepadState.textContent = 'Controller found, but haptics are unavailable';
} else {
ui.gamepadState.textContent = 'Controller connected and ready';
}
}
if (ui.testButton) {
ui.testButton.disabled = !active || !actuator;
}
};
const playEffect = async (actuator, intensity, duration) => {
if (typeof actuator.playEffect === 'function') {
await actuator.playEffect('dual-rumble', {
startDelay: 0,
duration,
weakMagnitude: Math.min(1, intensity * 0.8 + 0.1),
strongMagnitude: intensity
});
return;
}
if (typeof actuator.pulse === 'function') {
await actuator.pulse(intensity, duration);
}
};
const runVibration = async () => {
const gamepad = selectedGamepad();
const actuator = getActuator(gamepad);
if (!gamepad || !actuator) {
notify('No compatible haptic controller is currently selected.');
refreshGamepads();
return;
}
const { intensity, pattern } = currentState();
const normalizedIntensity = Math.max(0, Math.min(1, intensity / 100));
if (continuousVibrationInterval) {
window.clearInterval(continuousVibrationInterval);
continuousVibrationInterval = null;
ui.testButton.textContent = 'Test vibration (continuous)';
notify('Vibration stopped.');
return;
}
ui.testButton.textContent = 'Stop vibration';
try {
if (pattern === 'pulse') {
const pulse = async () => {
await playEffect(actuator, normalizedIntensity, 500);
};
pulse();
continuousVibrationInterval = window.setInterval(pulse, 800);
} else {
// For continuous, we re-trigger every 100ms
await playEffect(actuator, normalizedIntensity, 150);
continuousVibrationInterval = window.setInterval(async () => {
await playEffect(actuator, normalizedIntensity, 150);
}, 100);
}
notify(`Running ${pattern} vibration at ${intensity}%.`);
} catch (error) {
console.error(error);
notify('The browser detected the controller, but vibration could not start.');
ui.testButton.textContent = 'Test vibration (continuous)';
}
};
const wirePresetButtons = () => {
document.querySelectorAll('.apply-preset').forEach((button) => {
button.addEventListener('click', () => {
applyPresetState({
name: button.dataset.name || '',
angle_deg: button.dataset.angle || 0,
intensity_pct: button.dataset.intensity || 0,
pattern_mode: button.dataset.pattern || 'continuous',
notes: button.dataset.notes || ''
});
window.location.hash = 'simulator';
notify(`Loaded preset “${button.dataset.name || 'Preset'}” into the simulator.`);
});
});
};
if (ui.testButton) {
ui.testButton.addEventListener('click', runVibration);
}
if (ui.resetButton) {
ui.resetButton.addEventListener('click', () => {
applyPresetState(appConfig.defaults || {});
notify('Demo defaults restored.');
});
}
[controls.angle, controls.intensity, controls.pattern].forEach((element) => {
if (element) {
element.addEventListener('input', updateVisualization);
element.addEventListener('change', updateVisualization);
}
});
if (ui.gamepadSelect) {
ui.gamepadSelect.addEventListener('change', refreshGamepads);
}
window.addEventListener('gamepadconnected', (event) => {
refreshGamepads();
notify(`Connected: ${event.gamepad.id}`);
});
window.addEventListener('gamepaddisconnected', (event) => {
refreshGamepads();
notify(`Disconnected: ${event.gamepad.id}`);
});
applyPresetState(appConfig.initialPreset || {});
wirePresetButtons();
refreshGamepads();
updateVisualization();
if (appConfig.toastMessage) {
notify(appConfig.toastMessage);
}
if (navigator.getGamepads) {
scanTimer = window.setInterval(refreshGamepads, 1500);
}
window.addEventListener('beforeunload', () => {
if (scanTimer) {
window.clearInterval(scanTimer);
}
if (continuousVibrationInterval) {
window.clearInterval(continuousVibrationInterval);
}
});
});