feat: add device motion interaction

This commit is contained in:
Usu171
2025-12-13 16:02:15 +08:00
parent 86d5c48522
commit a72bfb2e85
4 changed files with 229 additions and 2 deletions

View File

@@ -12,11 +12,26 @@ import Particles from "./Particles.astro";
<div class="halo"></div>
<div class="grain"></div>
<div id="marble-field"></div>
<Footer />
<!-- <div id="debug-overlay">
<div>
Support: <span id="debug-support">?</span>
</div>
<div>
Active: <span id="debug-active">?</span>
</div>
<div>
AX: <span id="debug-ax">0.00</span>
</div>
<div>
AY: <span id="debug-ay">0.00</span>
</div>
<button id="debug-perm-btn">Request Permission</button>
</div> -->
<Footer/>
<div class="content">
<div class="title-card">
<div class="title-svg-container" set:html={titleSvg} />
<div class="title-svg-container" set:html={titleSvg}/>
</div>
</div>
@@ -81,6 +96,28 @@ import Particles from "./Particles.astro";
});
}
// Debug Info Update see also marbleSystem.ts
// const debugSupport = document.getElementById("debug-support");
// const debugActive = document.getElementById("debug-active");
// const debugAx = document.getElementById("debug-ax");
// const debugAy = document.getElementById("debug-ay");
// const debugPermBtn = document.getElementById("debug-perm-btn");
// if (debugPermBtn) {
// debugPermBtn.addEventListener("click", async () => {
// const granted = await marbleSystem.requestDeviceMotionPermission();
// if (debugActive) debugActive.innerText = granted ? "GRANTED" : "DENIED";
// });
// }
// setInterval(() => {
// const info = marbleSystem.getDeviceMotionDebugInfo();
// if (debugSupport) debugSupport.innerText = String(info.supported);
// if (debugActive) debugActive.innerText = String(info.active);
// if (debugAx) debugAx.innerText = info.ax.toFixed(2);
// if (debugAy) debugAy.innerText = info.ay.toFixed(2);
// }, 200);
// Toggle Collisions
window.addEventListener("toggle-collision", ((e: CustomEvent) => {
marbleSystem.setCollisions(e.detail.enabled);
@@ -423,4 +460,18 @@ import Particles from "./Particles.astro";
transform: translate3d(0, 0, 0) scale(1);
}
}
/* #debug-overlay {
position: fixed;
top: 10px;
left: 10px;
background: rgba(0, 0, 0, 0.8);
color: #0f0;
padding: 10px;
z-index: 10000;
font-family: monospace;
font-size: 12px;
pointer-events: auto;
border: 1px solid #0f0;
} */
</style>

View File

@@ -43,6 +43,11 @@ export const MARBLE_CONFIG = {
repelForce: 400, // Repulsion force
attractForce: 600, // Attraction force
},
// Device motion interaction configuration
deviceMotion: {
sensitivity: 600, // Sensitivity
maxForce: 6000, // Maximum force limit
},
} as const;
export const AVATAR_BASE_URL = "https://avatar.awfufu.com/qq/";

View File

@@ -0,0 +1,135 @@
/**
* Device motion interaction system
* Handles the effect of device tilt (acceleration) on marbles
*/
import type { Marble } from "./mouseInteraction";
export interface DeviceMotionConfig {
sensitivity: number;
maxForce: number;
}
export class DeviceMotionInteraction {
private ax: number = 0;
private ay: number = 0;
private isActive: boolean = false;
private config: DeviceMotionConfig;
constructor(config: DeviceMotionConfig) {
this.config = config;
}
/**
* Initialize device motion listener
*/
public init(): void {
if (typeof window === "undefined") return;
// Check if DeviceMotionEvent is supported
if (window.DeviceMotionEvent) {
window.addEventListener("devicemotion", this.handleMotion.bind(this));
this.isActive = true;
}
}
/**
* Handle device motion event
*/
private handleMotion(event: DeviceMotionEvent): void {
// x axis acceleration
// y axis acceleration
const accel = event.acceleration;
if (accel) {
this.ax = -(accel.x || 0);
this.ay = accel.y || 0;
}
}
/**
* Get whether supported and active
*/
public isSupported(): boolean {
return this.isActive;
}
/**
* Request PermissioniOS 13+
*/
public async requestPermission(): Promise<boolean> {
if (typeof (DeviceMotionEvent as any).requestPermission === "function") {
try {
const response = await (DeviceMotionEvent as any).requestPermission();
if (response === "granted") {
this.init();
return true;
}
return false;
} catch (e) {
console.error("DeviceMotion permission error:", e);
return false;
}
}
return true; // Non iOS 13+ devices do not require a request
}
/**
* Apply Force
*/
public applyForce(marbles: Marble[], dt: number): void {
if (!this.isActive) return;
// 阈值过滤,防止抖动
if (Math.abs(this.ax) < 0.5 && Math.abs(this.ay) < 0.5) return;
const { sensitivity, maxForce } = this.config;
// Calculate force
// ax, ay unit is m/s^2
// times sensitivity
let fx = this.ax * sensitivity;
let fy = this.ay * sensitivity;
// Limit max force
const force = Math.hypot(fx, fy);
if (force > maxForce) {
const scale = maxForce / force;
fx *= scale;
fy *= scale;
}
// Apply to all marbles
for (const m of marbles) {
// m.vx += fx;
// m.vy += fy;
m.vx += fx * dt;
m.vy += fy * dt;
// console.log("fx", fx, "fy", fy, "dt", dt);
}
}
/**
* Get Debug Info
*/
public getDebugInfo(): {
supported: boolean;
active: boolean;
ax: number;
ay: number;
permissionState: string;
} {
return {
supported: typeof window !== "undefined" && !!window.DeviceMotionEvent,
active: this.isActive,
ax: this.ax,
ay: this.ay,
permissionState: "unknown", // browser doesn't expose state, unless query
};
}
/**
* Update Config
*/
public updateConfig(config: Partial<DeviceMotionConfig>): void {
this.config = { ...this.config, ...config };
}
}

View File

@@ -2,6 +2,7 @@
import type { UserEntry } from "../config/marbleConfig";
import { MARBLE_CONFIG } from "../config/marbleConfig";
import { DeviceMotionInteraction } from "./deviceMotionInteraction";
import { AnimationLoop } from "./animationLoop";
import { MarbleFactory } from "./marbleFactory";
import { MarblePhysics } from "./marblePhysics";
@@ -18,6 +19,10 @@ export interface MarbleSystemConfig {
repelForce?: number;
attractForce?: number;
};
deviceMotionConfig?: {
sensitivity?: number;
maxForce?: number;
};
}
export class MarbleSystem {
@@ -26,6 +31,7 @@ export class MarbleSystem {
// Subsystems
private mouseInteraction: MouseInteraction;
private deviceMotionInteraction: DeviceMotionInteraction;
private physics: MarblePhysics;
private factory: MarbleFactory;
private animationLoop: AnimationLoop;
@@ -57,6 +63,17 @@ export class MarbleSystem {
this.mouseInteraction = new MouseInteraction(mouseConfig);
this.mouseInteraction.init();
// DeviceMotionInteraction Init
this.deviceMotionInteraction = new DeviceMotionInteraction({
sensitivity:
config.deviceMotionConfig?.sensitivity ??
MARBLE_CONFIG.deviceMotion.sensitivity,
maxForce:
config.deviceMotionConfig?.maxForce ??
MARBLE_CONFIG.deviceMotion.maxForce,
});
this.deviceMotionInteraction.init();
// MarblePhysics Init
this.physics = new MarblePhysics({
fieldWidth: this.fieldWidth,
@@ -95,6 +112,11 @@ export class MarbleSystem {
}
}
// Apply device motion force
if (this.deviceMotionInteraction.isSupported()) {
this.deviceMotionInteraction.applyForce(this.marbles, dt);
}
// Update physics
this.physics.updatePositions(this.marbles, dt);
this.physics.handleCollisions(this.marbles);
@@ -203,6 +225,20 @@ export class MarbleSystem {
return Math.max(min, Math.floor(capped)) * zoomLevel;
}
/**
* Request device motion permission
*/
public async requestDeviceMotionPermission(): Promise<boolean> {
return this.deviceMotionInteraction.requestPermission();
}
/**
* Get device motion debug info see also MainView.astro
*/
// public getDeviceMotionDebugInfo() {
// return this.deviceMotionInteraction.getDebugInfo();
// }
// Destroy system
public destroy(): void {
this.stop();