Files

112 lines
4.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* haptics.js — navigator.vibrate 震动反馈封装。
*
* 预定义震型:
* | 名称 | 震型 (ms) | 建议用途 |
* |-----------|----------------------|------------------------|
* | confirm | [20] | 点按确认 |
* | error | [60,40,60] | 操作错误 |
* | success | [100] | 过关/成功 |
* | warning | [30,50,30,50,30] | 警告/危险逼近 |
* | swipe | [10] | 滑动识别成功(最轻) |
* | heartbeat | [40,100,40] | 心跳/倒计时紧张感 |
* | pickup | [15] | 拾取/获得物品 |
* | hit | [80] | 受击/终结性失败 |
* | beat | [8] | 节拍锚点(最轻) |
* | danger | [25,40,25,40,25] | 危险逼近 |
*
* 特性检测:'vibrate' in navigator。支持的平台正常震动;
* **不支持的平台(iOS Safari 等)降级为视觉反馈**——全屏边框闪光覆盖层
* (#haptics-flash,样式在 style.css),不同震型对应不同颜色与闪烁次数:
* confirm 白·1 次 / error 红·2 次 / success 绿·1 次 / warning 橙·3 次 /
* swipe 白·极淡 1 次 / heartbeat 粉·2 次 / pickup 青·1 次 / hit 红·强 1 次 /
* beat 白·极淡 1 次 / danger 红·快 3 次。两者都不行的环境静默 no-op。
*
* 用法:
* import { vibrate } from '../engine/haptics.js';
* vibrate('confirm'); // 预定义名称
* vibrate([50, 30, 50]); // 自定义震型
*
* 注意:gesture.js 已内置基础反馈(滑动→swipe,tap/doubletap→confirm),
* 游戏逻辑层只需在语义事件(错误/成功/警告等)处自行调用。
*/
/** @type {Record<string, number[]>} */
export const HAPTIC_PATTERNS = {
confirm: [20],
error: [60, 40, 60],
success: [100],
warning: [30, 50, 30, 50, 30],
swipe: [10],
heartbeat: [40, 100, 40],
pickup: [15],
hit: [80],
beat: [8],
danger: [25, 40, 25, 40, 25],
};
/** 当前环境是否支持震动(iOS Safari 为 false,此时 vibrate() 走视觉降级)。 */
export const hapticsSupported =
typeof navigator !== 'undefined' && 'vibrate' in navigator;
/** 震型 → 视觉降级闪光参数:[颜色, 动画 class, 亮度] */
const FLASH_MAP = {
confirm: ['#ffffff', 'hp-once', 0.7],
error: ['#ff5252', 'hp-twice', 0.9],
success: ['#69f0ae', 'hp-once', 0.9],
warning: ['#ffb74d', 'hp-thrice', 0.9],
swipe: ['#ffffff', 'hp-once', 0.25],
heartbeat: ['#f48fb1', 'hp-twice', 0.7],
pickup: ['#80deea', 'hp-once', 0.7],
hit: ['#ff1744', 'hp-once', 1],
beat: ['#ffffff', 'hp-once', 0.15],
danger: ['#ff5252', 'hp-thrice', 0.9],
};
let flashEl = null;
/** 视觉降级:全屏边框闪光(覆盖层常驻 body,改 class 重触发动画)。 */
function _flash(patternName) {
if (typeof document === 'undefined') return;
const [color, anim, alpha] = FLASH_MAP[patternName] || FLASH_MAP.confirm;
if (!flashEl) {
flashEl = document.getElementById('haptics-flash');
if (!flashEl) {
flashEl = document.createElement('div');
flashEl.id = 'haptics-flash';
document.body.appendChild(flashEl);
}
}
flashEl.style.borderColor = color;
flashEl.style.boxShadow = `inset 0 0 32px ${color}`;
flashEl.style.setProperty('--hp-alpha', alpha);
flashEl.style.opacity = '';
flashEl.className = '';
void flashEl.offsetWidth; // 强制 reflow,重触发动画
flashEl.className = anim;
}
/**
* 触发震动;不支持 vibrate 的平台自动降级为边框闪光。
* @param {keyof typeof HAPTIC_PATTERNS | number[]} pattern 预定义名称或毫秒数组
*/
export function vibrate(pattern) {
const name = typeof pattern === 'string' ? pattern : null;
const p = name ? HAPTIC_PATTERNS[name] : pattern;
if (!p) {
console.warn('[haptics] 未知震型:', pattern);
return;
}
if (hapticsSupported) {
try {
navigator.vibrate(p);
return;
} catch (_) {
// 某些平台调用即抛(极少见)→ 落入视觉降级
}
}
_flash(name || 'confirm'); // 自定义数组无语义名,按 confirm 闪
}
export default vibrate;