mirror of
https://github.com/101island/lolisland.us.git
synced 2026-03-01 11:49:43 +08:00
feat: add device motion interaction
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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/";
|
||||
|
||||
135
src/utils/deviceMotionInteraction.ts
Normal file
135
src/utils/deviceMotionInteraction.ts
Normal 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 Permission(iOS 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 };
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user