Auto commit: 2026-03-23T19:22:21.572Z

This commit is contained in:
Flatlogic Bot 2026-03-23 19:22:21 +00:00
parent 16b7d41fc5
commit e713c1471c
4 changed files with 48 additions and 77 deletions

19
app.php
View File

@ -27,7 +27,6 @@ function ensure_recliner_schema(): void
angle_deg TINYINT UNSIGNED NOT NULL,
intensity_pct TINYINT UNSIGNED NOT NULL,
pattern_mode VARCHAR(20) NOT NULL,
duration_ms SMALLINT UNSIGNED NOT NULL,
notes VARCHAR(255) DEFAULT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
@ -41,7 +40,6 @@ function default_preset(): array
'angle_deg' => 112,
'intensity_pct' => 44,
'pattern_mode' => 'continuous',
'duration_ms' => 1400,
'notes' => 'Balanced demo profile for a calm showroom preview.',
];
}
@ -69,13 +67,6 @@ function validate_preset_input(array $input): array
$errors['intensity_pct'] = 'Intensity must be between 0% and 100%.';
}
$duration = filter_var($input['duration_ms'] ?? null, FILTER_VALIDATE_INT, [
'options' => ['min_range' => 100, 'max_range' => 5000],
]);
if ($duration === false) {
$errors['duration_ms'] = 'Duration must be between 100 ms and 5000 ms.';
}
$pattern = (string) ($input['pattern_mode'] ?? 'continuous');
if (!in_array($pattern, RECLINER_PATTERNS, true)) {
$errors['pattern_mode'] = 'Choose a supported vibration pattern.';
@ -93,7 +84,6 @@ function validate_preset_input(array $input): array
'angle_deg' => $angle === false ? 0 : (int) $angle,
'intensity_pct' => $intensity === false ? 0 : (int) $intensity,
'pattern_mode' => $pattern,
'duration_ms' => $duration === false ? 1000 : (int) $duration,
'notes' => $notes,
],
];
@ -102,15 +92,14 @@ function validate_preset_input(array $input): array
function save_preset(array $data): int
{
$stmt = db()->prepare(
'INSERT INTO recliner_presets (name, angle_deg, intensity_pct, pattern_mode, duration_ms, notes)
VALUES (:name, :angle_deg, :intensity_pct, :pattern_mode, :duration_ms, :notes)'
'INSERT INTO recliner_presets (name, angle_deg, intensity_pct, pattern_mode, notes)
VALUES (:name, :angle_deg, :intensity_pct, :pattern_mode, :notes)'
);
$stmt->bindValue(':name', $data['name'], PDO::PARAM_STR);
$stmt->bindValue(':angle_deg', $data['angle_deg'], PDO::PARAM_INT);
$stmt->bindValue(':intensity_pct', $data['intensity_pct'], PDO::PARAM_INT);
$stmt->bindValue(':pattern_mode', $data['pattern_mode'], PDO::PARAM_STR);
$stmt->bindValue(':duration_ms', $data['duration_ms'], PDO::PARAM_INT);
$stmt->bindValue(':notes', $data['notes'] !== '' ? $data['notes'] : null, $data['notes'] !== '' ? PDO::PARAM_STR : PDO::PARAM_NULL);
$stmt->execute();
@ -121,7 +110,7 @@ function get_recent_presets(int $limit = 8): array
{
$limit = max(1, min(24, $limit));
$stmt = db()->query(
'SELECT id, name, angle_deg, intensity_pct, pattern_mode, duration_ms, notes, created_at
'SELECT id, name, angle_deg, intensity_pct, pattern_mode, notes, created_at
FROM recliner_presets
ORDER BY created_at DESC, id DESC
LIMIT ' . $limit
@ -133,7 +122,7 @@ function get_recent_presets(int $limit = 8): array
function get_preset(int $id): ?array
{
$stmt = db()->prepare(
'SELECT id, name, angle_deg, intensity_pct, pattern_mode, duration_ms, notes, created_at
'SELECT id, name, angle_deg, intensity_pct, pattern_mode, notes, created_at
FROM recliner_presets
WHERE id = :id
LIMIT 1'

View File

@ -8,7 +8,6 @@ document.addEventListener('DOMContentLoaded', () => {
angle: document.getElementById('angle_deg'),
intensity: document.getElementById('intensity_pct'),
pattern: document.getElementById('pattern_mode'),
duration: document.getElementById('duration_ms'),
name: document.getElementById('name'),
notes: document.getElementById('notes')
};
@ -18,19 +17,15 @@ document.addEventListener('DOMContentLoaded', () => {
angleValue: document.getElementById('angle-value'),
anglePill: document.getElementById('angle-pill'),
intensityPill: document.getElementById('intensity-pill'),
durationPill: document.getElementById('duration-pill'),
statAngle: document.getElementById('stat-angle'),
statIntensity: document.getElementById('stat-intensity'),
statPattern: document.getElementById('stat-pattern'),
statDuration: document.getElementById('stat-duration'),
saveAngle: document.getElementById('save-angle'),
saveIntensity: document.getElementById('save-intensity'),
savePattern: document.getElementById('save-pattern'),
saveDuration: document.getElementById('save-duration'),
summaryAngle: document.getElementById('summary-angle'),
summaryIntensity: document.getElementById('summary-intensity'),
summaryPattern: document.getElementById('summary-pattern'),
summaryTone: document.getElementById('summary-tone'),
reclineMode: document.getElementById('recline-mode'),
gamepadState: document.getElementById('gamepad-state'),
gamepadSelect: document.getElementById('gamepad-select'),
@ -40,6 +35,7 @@ document.addEventListener('DOMContentLoaded', () => {
let knownGamepads = [];
let scanTimer = null;
let continuousVibrationInterval = null;
const notify = (message) => {
if (!message) {
@ -63,8 +59,7 @@ document.addEventListener('DOMContentLoaded', () => {
const currentState = () => ({
angle: Number(controls.angle?.value || 0),
intensity: Number(controls.intensity?.value || 0),
pattern: controls.pattern?.value || 'continuous',
duration: Number(controls.duration?.value || 1000)
pattern: controls.pattern?.value || 'continuous'
});
const updateVisualization = () => {
@ -79,19 +74,15 @@ document.addEventListener('DOMContentLoaded', () => {
angleValue: `${state.angle}`,
anglePill: `${state.angle}°`,
intensityPill: `${state.intensity}%`,
durationPill: `${state.duration} ms`,
statAngle: `${state.angle}°`,
statIntensity: `${state.intensity}%`,
statPattern: capitalize(state.pattern),
statDuration: `${state.duration} ms`,
saveAngle: `${state.angle}°`,
saveIntensity: `${state.intensity}%`,
savePattern: capitalize(state.pattern),
saveDuration: `${state.duration} ms`,
summaryAngle: `${state.angle}°`,
summaryIntensity: `${state.intensity}%`,
summaryPattern: capitalize(state.pattern),
summaryTone: tone,
reclineMode: tone
};
@ -109,7 +100,6 @@ document.addEventListener('DOMContentLoaded', () => {
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.duration && preset.duration_ms !== undefined) controls.duration.value = preset.duration_ms;
if (controls.name && preset.name !== undefined) controls.name.value = preset.name;
if (controls.notes && preset.notes !== undefined) controls.notes.value = preset.notes;
updateVisualization();
@ -211,32 +201,38 @@ document.addEventListener('DOMContentLoaded', () => {
return;
}
const { intensity, duration, pattern } = currentState();
const { intensity, pattern } = currentState();
const normalizedIntensity = Math.max(0, Math.min(1, intensity / 100));
ui.testButton.disabled = true;
ui.testButton.textContent = 'Testing…';
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') {
let remaining = duration;
while (remaining > 0) {
const burst = Math.min(160, remaining);
await playEffect(actuator, normalizedIntensity, burst);
remaining -= burst;
if (remaining > 0) {
await new Promise((resolve) => window.setTimeout(resolve, 120));
}
}
const pulse = async () => {
await playEffect(actuator, normalizedIntensity, 500);
};
pulse();
continuousVibrationInterval = window.setInterval(pulse, 800);
} else {
await playEffect(actuator, normalizedIntensity, duration);
// 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}% for ${duration} ms.`);
notify(`Running ${pattern} vibration at ${intensity}%.`);
} catch (error) {
console.error(error);
notify('The browser detected the controller, but vibration could not start.');
} finally {
ui.testButton.textContent = 'Test vibration';
refreshGamepads();
ui.testButton.textContent = 'Test vibration (continuous)';
}
};
@ -248,7 +244,6 @@ document.addEventListener('DOMContentLoaded', () => {
angle_deg: button.dataset.angle || 0,
intensity_pct: button.dataset.intensity || 0,
pattern_mode: button.dataset.pattern || 'continuous',
duration_ms: button.dataset.duration || 1000,
notes: button.dataset.notes || ''
});
window.location.hash = 'simulator';
@ -268,7 +263,7 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
[controls.angle, controls.intensity, controls.pattern, controls.duration].forEach((element) => {
[controls.angle, controls.intensity, controls.pattern].forEach((element) => {
if (element) {
element.addEventListener('input', updateVisualization);
element.addEventListener('change', updateVisualization);
@ -306,5 +301,8 @@ document.addEventListener('DOMContentLoaded', () => {
if (scanTimer) {
window.clearInterval(scanTimer);
}
if (continuousVibrationInterval) {
window.clearInterval(continuousVibrationInterval);
}
});
});

View File

@ -36,7 +36,6 @@ if ($presetId) {
'angle_deg' => (int) $selectedPreset['angle_deg'],
'intensity_pct' => (int) $selectedPreset['intensity_pct'],
'pattern_mode' => (string) $selectedPreset['pattern_mode'],
'duration_ms' => (int) $selectedPreset['duration_ms'],
'notes' => (string) ($selectedPreset['notes'] ?? ''),
];
}
@ -125,7 +124,6 @@ if (isset($_GET['saved']) && ctype_digit((string) $_GET['saved'])) {
<div><span>Angle</span><strong id="summary-angle"><?= e((string) $formData['angle_deg']) ?>°</strong></div>
<div><span>Intensity</span><strong id="summary-intensity"><?= e((string) $formData['intensity_pct']) ?>%</strong></div>
<div><span>Pattern</span><strong id="summary-pattern"><?= e(ucfirst($formData['pattern_mode'])) ?></strong></div>
<div><span>Tone</span><strong id="summary-tone"><?= e(preset_tone((int) $formData['angle_deg'])) ?></strong></div>
</div>
<p class="small text-secondary mb-0 mt-3">Tip: the browser requires a user interaction before triggering vibration. Press <strong>Test vibration</strong> after connecting the controller.</p>
</div>
@ -164,18 +162,15 @@ if (isset($_GET['saved']) && ctype_digit((string) $_GET['saved'])) {
</div>
<div class="row g-3 stat-row">
<div class="col-6 col-md-3">
<div class="col-6 col-md-4">
<div class="mini-stat"><span>Recline</span><strong id="stat-angle"><?= e((string) $formData['angle_deg']) ?>°</strong></div>
</div>
<div class="col-6 col-md-3">
<div class="col-6 col-md-4">
<div class="mini-stat"><span>Rumble</span><strong id="stat-intensity"><?= e((string) $formData['intensity_pct']) ?>%</strong></div>
</div>
<div class="col-6 col-md-3">
<div class="col-6 col-md-4">
<div class="mini-stat"><span>Pattern</span><strong id="stat-pattern"><?= e(ucfirst($formData['pattern_mode'])) ?></strong></div>
</div>
<div class="col-6 col-md-3">
<div class="mini-stat"><span>Duration</span><strong id="stat-duration"><?= e((string) $formData['duration_ms']) ?> ms</strong></div>
</div>
</div>
</section>
@ -197,7 +192,7 @@ if (isset($_GET['saved']) && ctype_digit((string) $_GET['saved'])) {
</div>
<div class="col-md-5">
<label class="form-label d-block">Action</label>
<button type="button" class="btn btn-accent w-100" id="test-vibration">Test vibration</button>
<button type="button" class="btn btn-accent w-100" id="test-vibration">Test vibration (continuous)</button>
</div>
</div>
@ -210,19 +205,13 @@ if (isset($_GET['saved']) && ctype_digit((string) $_GET['saved'])) {
<div class="d-flex justify-content-between align-items-center mb-2"><label for="intensity_pct" class="form-label mb-0">Vibration intensity</label><span class="value-pill" id="intensity-pill"><?= e((string) $formData['intensity_pct']) ?>%</span></div>
<input type="range" class="form-range" min="0" max="100" step="1" id="intensity_pct" name="intensity_pct" value="<?= e((string) $formData['intensity_pct']) ?>">
</div>
<div class="row g-3">
<div class="col-md-6">
<div class="control-group">
<label for="pattern_mode" class="form-label">Pattern</label>
<select class="form-select" id="pattern_mode" name="pattern_mode">
<option value="continuous" <?= $formData['pattern_mode'] === 'continuous' ? 'selected' : '' ?>>Continuous</option>
<option value="pulse" <?= $formData['pattern_mode'] === 'pulse' ? 'selected' : '' ?>>Pulse</option>
</select>
</div>
<div class="col-md-6">
<div class="d-flex justify-content-between align-items-center mb-2"><label for="duration_ms" class="form-label mb-0">Duration</label><span class="value-pill" id="duration-pill"><?= e((string) $formData['duration_ms']) ?> ms</span></div>
<input type="range" class="form-range mt-3" min="100" max="5000" step="100" id="duration_ms" name="duration_ms" value="<?= e((string) $formData['duration_ms']) ?>">
</div>
</div>
</div>
</section>
</div>
@ -261,7 +250,6 @@ if (isset($_GET['saved']) && ctype_digit((string) $_GET['saved'])) {
<div><span>Angle</span><strong id="save-angle"><?= e((string) $formData['angle_deg']) ?>°</strong></div>
<div><span>Intensity</span><strong id="save-intensity"><?= e((string) $formData['intensity_pct']) ?>%</strong></div>
<div><span>Pattern</span><strong id="save-pattern"><?= e(ucfirst($formData['pattern_mode'])) ?></strong></div>
<div><span>Duration</span><strong id="save-duration"><?= e((string) $formData['duration_ms']) ?> ms</strong></div>
</div>
</div>
@ -295,7 +283,6 @@ if (isset($_GET['saved']) && ctype_digit((string) $_GET['saved'])) {
<div class="preset-metrics mb-3">
<span><?= e((string) $preset['intensity_pct']) ?>% intensity</span>
<span><?= e(ucfirst((string) $preset['pattern_mode'])) ?></span>
<span><?= e((string) $preset['duration_ms']) ?> ms</span>
</div>
<?php if (!empty($preset['notes'])): ?>
<p class="text-secondary small mb-3"><?= e($preset['notes']) ?></p>
@ -309,7 +296,6 @@ if (isset($_GET['saved']) && ctype_digit((string) $_GET['saved'])) {
data-angle="<?= e((string) $preset['angle_deg']) ?>"
data-intensity="<?= e((string) $preset['intensity_pct']) ?>"
data-pattern="<?= e((string) $preset['pattern_mode']) ?>"
data-duration="<?= e((string) $preset['duration_ms']) ?>"
data-notes="<?= e((string) ($preset['notes'] ?? '')) ?>"
>Apply in simulator</button>
<a class="btn btn-sm btn-link text-decoration-none px-0" href="/preset.php?id=<?= e((string) $preset['id']) ?>">View detail</a>

View File

@ -64,14 +64,13 @@ $pageDescription = $preset
<span class="chip"><?= e((string) $preset['angle_deg']) ?>° angle</span>
<span class="chip"><?= e((string) $preset['intensity_pct']) ?>% intensity</span>
<span class="chip"><?= e(ucfirst((string) $preset['pattern_mode'])) ?> rumble</span>
<span class="chip"><?= e((string) $preset['duration_ms']) ?> ms</span>
</div>
</div>
<div class="col-lg-4">
<div class="panel inset-panel p-3 h-100">
<div class="small-label mb-3">Profile tone</div>
<div class="h4 mb-2"><?= e(preset_tone((int) $preset['angle_deg'])) ?></div>
<p class="text-secondary small mb-0">Saved on <?= e(date('F j, Y  H:i', strtotime((string) $preset['created_at']))) ?> UTC.</p>
<p class="text-secondary small mb-0">Saved on <?= e(date('F j, Y H:i', strtotime((string) $preset['created_at']))) ?> UTC.</p>
</div>
</div>
</div>
@ -85,7 +84,6 @@ $pageDescription = $preset
<div><span>Recline angle</span><strong><?= e((string) $preset['angle_deg']) ?>°</strong></div>
<div><span>Vibration intensity</span><strong><?= e((string) $preset['intensity_pct']) ?>%</strong></div>
<div><span>Pattern</span><strong><?= e(ucfirst((string) $preset['pattern_mode'])) ?></strong></div>
<div><span>Duration</span><strong><?= e((string) $preset['duration_ms']) ?> ms</strong></div>
<div><span>Saved at</span><strong><?= e((string) $preset['created_at']) ?></strong></div>
</div>
<hr class="border-secondary-subtle my-4">