Skip to content

Commit 57e2127

Browse files
zlyfunctionclaude
andauthored
Add gyroscope tilt and shake interaction to cloth sim (#1)
Tilting the phone redirects gravity (baseline captured on enable, so it works from whatever angle the phone is held); shaking it injects a decaying impulse across the cloth. Uses deviceorientation/devicemotion with the iOS 13+ permission-request gate, feature-detected and gated behind a new "gyro" toggle button. Co-authored-by: Claude <noreply@anthropic.com>
1 parent 005b277 commit 57e2127

1 file changed

Lines changed: 103 additions & 2 deletions

File tree

cloth.html

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
<button id="btnReset">reset</button>
5454
<button id="btnGravity" class="on">gravity</button>
5555
<button id="btnWind">wind</button>
56+
<button id="btnGyro">gyro</button>
5657
<button id="btnColor">mono</button>
5758
<hr class="sep">
5859
<button id="btnMat">linen</button>
@@ -115,6 +116,23 @@
115116
let touchMode = 0; // 0=grab, 1=cut
116117
let time = 0;
117118

119+
// ─── Gyroscope / motion (mobile tilt-to-tip, shake-to-gust) ──────────────────
120+
let gyroOn = false;
121+
let gyroBaseline = null; // { beta, gamma } captured when gyro is enabled
122+
let gravDirX = 0; // smoothed gravity direction, unit-ish vector
123+
let gravDirY = 1; // default: straight down
124+
let gravTargetX = 0;
125+
let gravTargetY = 1;
126+
let lastAccel = null;
127+
let shakeImpulseX = 0;
128+
let shakeImpulseY = 0;
129+
const GYRO_MAX_TILT = 40; // degrees of tilt for full gravity redirect
130+
const GYRO_SMOOTH = 0.12; // per-frame lerp toward target (damps sensor jitter)
131+
const SHAKE_THRESHOLD = 12; // m/s^2 of jerk that counts as a shake
132+
const SHAKE_GAIN = 2.2; // px of impulse per m/s^2 of jerk
133+
const SHAKE_MAX = 40; // px, impulse cap
134+
const SHAKE_DECAY = 0.85; // per physics step
135+
118136
// ─── Mouse / touch ───────────────────────────────────────────────────────────
119137
const mouse = { x: 0, y: 0, lx: 0, ly: 0, down: false, right: false, shift: false };
120138
let grabbed = -1;
@@ -189,15 +207,25 @@
189207
Math.sin(time*1.86)*W*0.15) * stepScale * stepScale;
190208
}
191209

210+
// Smooth the sensor-driven gravity direction to damp raw sensor jitter.
211+
gravDirX += (gravTargetX - gravDirX) * GYRO_SMOOTH;
212+
gravDirY += (gravTargetY - gravDirY) * GYRO_SMOOTH;
213+
214+
// Shake impulse (devicemotion) decays each physics step after a spike.
215+
const shakeX = shakeImpulseX * stepScale;
216+
const shakeY = shakeImpulseY * stepScale;
217+
shakeImpulseX *= SHAKE_DECAY;
218+
shakeImpulseY *= SHAKE_DECAY;
219+
192220
// 1. Verlet integrate
193221
for (let i = 0; i < nP; i++) {
194222
if (pinned[i] || i === held) continue;
195223
const vx = (px[i] - ppx[i]) * damp;
196224
const vy = (py[i] - ppy[i]) * damp;
197225
ppx[i] = px[i];
198226
ppy[i] = py[i];
199-
px[i] += vx + wx;
200-
py[i] += vy + grav;
227+
px[i] += vx + wx + grav * gravDirX + shakeX;
228+
py[i] += vy + grav * gravDirY + shakeY;
201229
}
202230

203231
// 2. Pin the grabbed particle before solving so its pull propagates now.
@@ -567,6 +595,79 @@
567595
mouse.down = false; grabbed = -1;
568596
});
569597

598+
// ─── Gyroscope / motion ───────────────────────────────────────────────────────
599+
function flashWarn(el) {
600+
el.classList.add('warn');
601+
setTimeout(() => el.classList.remove('warn'), 300);
602+
}
603+
604+
function onDeviceOrientation(e) {
605+
if (e.beta === null || e.gamma === null) return;
606+
if (!gyroBaseline) gyroBaseline = { beta: e.beta, gamma: e.gamma };
607+
const dGamma = e.gamma - gyroBaseline.gamma;
608+
const dBeta = e.beta - gyroBaseline.beta;
609+
const gx = dGamma / GYRO_MAX_TILT;
610+
const gy = 1 - dBeta / GYRO_MAX_TILT;
611+
const len = Math.sqrt(gx*gx + gy*gy) || 1;
612+
gravTargetX = gx / len;
613+
gravTargetY = gy / len;
614+
}
615+
616+
function onDeviceMotion(e) {
617+
const acc = e.accelerationIncludingGravity;
618+
if (!acc || acc.x === null) return;
619+
if (lastAccel) {
620+
const dx = acc.x - lastAccel.x;
621+
const dy = acc.y - lastAccel.y;
622+
const dz = acc.z - lastAccel.z;
623+
const jerk = Math.sqrt(dx*dx + dy*dy + dz*dz);
624+
if (jerk > SHAKE_THRESHOLD) {
625+
shakeImpulseX = Math.max(-SHAKE_MAX, Math.min(SHAKE_MAX, shakeImpulseX - dx * SHAKE_GAIN));
626+
shakeImpulseY = Math.max(-SHAKE_MAX, Math.min(SHAKE_MAX, shakeImpulseY + dy * SHAKE_GAIN));
627+
}
628+
}
629+
lastAccel = { x: acc.x, y: acc.y, z: acc.z };
630+
}
631+
632+
const btnGyro = document.getElementById('btnGyro');
633+
634+
async function enableGyro() {
635+
const hasOrientation = typeof DeviceOrientationEvent !== 'undefined';
636+
const hasMotion = typeof DeviceMotionEvent !== 'undefined';
637+
if (!hasOrientation && !hasMotion) { flashWarn(btnGyro); return; }
638+
try {
639+
if (hasOrientation && typeof DeviceOrientationEvent.requestPermission === 'function') {
640+
if (await DeviceOrientationEvent.requestPermission() !== 'granted') { flashWarn(btnGyro); return; }
641+
}
642+
if (hasMotion && typeof DeviceMotionEvent.requestPermission === 'function') {
643+
if (await DeviceMotionEvent.requestPermission() !== 'granted') { flashWarn(btnGyro); return; }
644+
}
645+
} catch (err) {
646+
flashWarn(btnGyro);
647+
return;
648+
}
649+
gyroOn = true;
650+
gyroBaseline = null;
651+
lastAccel = null;
652+
shakeImpulseX = 0; shakeImpulseY = 0;
653+
window.addEventListener('deviceorientation', onDeviceOrientation);
654+
window.addEventListener('devicemotion', onDeviceMotion);
655+
btnGyro.classList.add('on');
656+
btnGyro.textContent = 'gyro on';
657+
}
658+
659+
function disableGyro() {
660+
gyroOn = false;
661+
gravTargetX = 0; gravTargetY = 1;
662+
shakeImpulseX = 0; shakeImpulseY = 0;
663+
window.removeEventListener('deviceorientation', onDeviceOrientation);
664+
window.removeEventListener('devicemotion', onDeviceMotion);
665+
btnGyro.classList.remove('on');
666+
btnGyro.textContent = 'gyro';
667+
}
668+
669+
btnGyro.addEventListener('click', () => { gyroOn ? disableGyro() : enableGyro(); });
670+
570671
// ─── Boot ─────────────────────────────────────────────────────────────────────
571672
window.addEventListener('resize', resize);
572673
resize();

0 commit comments

Comments
 (0)