|
53 | 53 | <button id="btnReset">reset</button> |
54 | 54 | <button id="btnGravity" class="on">gravity</button> |
55 | 55 | <button id="btnWind">wind</button> |
| 56 | + <button id="btnGyro">gyro</button> |
56 | 57 | <button id="btnColor">mono</button> |
57 | 58 | <hr class="sep"> |
58 | 59 | <button id="btnMat">linen</button> |
|
115 | 116 | let touchMode = 0; // 0=grab, 1=cut |
116 | 117 | let time = 0; |
117 | 118 |
|
| 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 | + |
118 | 136 | // ─── Mouse / touch ─────────────────────────────────────────────────────────── |
119 | 137 | const mouse = { x: 0, y: 0, lx: 0, ly: 0, down: false, right: false, shift: false }; |
120 | 138 | let grabbed = -1; |
|
189 | 207 | Math.sin(time*1.86)*W*0.15) * stepScale * stepScale; |
190 | 208 | } |
191 | 209 |
|
| 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 | + |
192 | 220 | // 1. Verlet integrate |
193 | 221 | for (let i = 0; i < nP; i++) { |
194 | 222 | if (pinned[i] || i === held) continue; |
195 | 223 | const vx = (px[i] - ppx[i]) * damp; |
196 | 224 | const vy = (py[i] - ppy[i]) * damp; |
197 | 225 | ppx[i] = px[i]; |
198 | 226 | 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; |
201 | 229 | } |
202 | 230 |
|
203 | 231 | // 2. Pin the grabbed particle before solving so its pull propagates now. |
|
567 | 595 | mouse.down = false; grabbed = -1; |
568 | 596 | }); |
569 | 597 |
|
| 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 | + |
570 | 671 | // ─── Boot ───────────────────────────────────────────────────────────────────── |
571 | 672 | window.addEventListener('resize', resize); |
572 | 673 | resize(); |
|
0 commit comments