diff --git a/src/components/MainView.astro b/src/components/MainView.astro
index 43780aa..1dc20e2 100644
--- a/src/components/MainView.astro
+++ b/src/components/MainView.astro
@@ -12,11 +12,26 @@ import Particles from "./Particles.astro";
@@ -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;
+ } */
diff --git a/src/config/marbleConfig.ts b/src/config/marbleConfig.ts
index 468a56e..1e897dc 100644
--- a/src/config/marbleConfig.ts
+++ b/src/config/marbleConfig.ts
@@ -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/";
diff --git a/src/utils/deviceMotionInteraction.ts b/src/utils/deviceMotionInteraction.ts
new file mode 100644
index 0000000..84e66d6
--- /dev/null
+++ b/src/utils/deviceMotionInteraction.ts
@@ -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 Permission(iOS 13+)
+ */
+ public async requestPermission(): Promise
{
+ 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): void {
+ this.config = { ...this.config, ...config };
+ }
+}
diff --git a/src/utils/marbleSystem.ts b/src/utils/marbleSystem.ts
index a9537c5..d6bae15 100644
--- a/src/utils/marbleSystem.ts
+++ b/src/utils/marbleSystem.ts
@@ -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 {
+ 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();