Files
timmy-tower/the-matrix/js/interaction.js
alexpaynex a0df576ed6 Add touchstart fallback and adjust interaction lockout
Introduce `touchstart` event listener as a fallback for older browsers lacking Pointer Events, and reduce the interaction lockout timer from 220ms to 150ms to prevent accidental orbit drags after a slap.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: b1d20c43-904b-495f-9262-401975d950d3
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9f85e954-647c-46a5-90a7-396e495a805a/418bf6f8-212b-4bb0-a7a5-8231a061da4e/Q83Uqvu
Replit-Helium-Checkpoint-Created: true
2026-03-19 03:33:48 +00:00

111 lines
3.5 KiB
JavaScript

import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
let controls;
let _canvas;
let _camera = null;
let _timmyGroup = null;
let _applySlap = null;
const _raycaster = new THREE.Raycaster();
const _pointer = new THREE.Vector2();
const _noCtxMenu = e => e.preventDefault();
// ── registerSlapTarget ────────────────────────────────────────────────────────
// Call after initAgents() with Timmy's group and the applySlap function.
// The capture-phase pointerdown listener will hit-test against the group and
// call applySlap(hitPoint) — also suppressing the OrbitControls drag.
export function registerSlapTarget(timmyGroup, applyFn) {
_timmyGroup = timmyGroup;
_applySlap = applyFn;
}
export function initInteraction(camera, renderer) {
_camera = camera;
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.screenSpacePanning = false;
controls.minDistance = 5;
controls.maxDistance = 80;
controls.maxPolarAngle = Math.PI / 2.1;
controls.target.set(0, 0, 0);
controls.update();
_canvas = renderer.domElement;
_canvas.addEventListener('contextmenu', _noCtxMenu);
// Capture phase so we intercept before OrbitControls' bubble-phase handler.
// If Timmy is hit we call stopImmediatePropagation() to suppress the orbit drag.
_canvas.addEventListener('pointerdown', _onPointerDown, { capture: true });
// touchstart fallback for older mobile browsers that lack Pointer Events
if (!window.PointerEvent) {
_canvas.addEventListener('touchstart', _onTouchStart, { capture: true, passive: false });
}
}
function _hitTest(clientX, clientY) {
if (!_timmyGroup || !_applySlap || !_camera) return false;
const rect = _canvas.getBoundingClientRect();
_pointer.x = ((clientX - rect.left) / rect.width) * 2 - 1;
_pointer.y = ((clientY - rect.top) / rect.height) * -2 + 1;
_raycaster.setFromCamera(_pointer, _camera);
const hits = _raycaster.intersectObject(_timmyGroup, true);
if (hits.length > 0) {
_applySlap(hits[0].point);
// 150 ms lockout to avoid accidental orbit drag immediately after a slap
if (controls) {
controls.enabled = false;
setTimeout(() => { if (controls) controls.enabled = true; }, 150);
}
return true;
}
return false;
}
function _onPointerDown(event) {
if (_hitTest(event.clientX, event.clientY)) {
event.stopImmediatePropagation(); // block OrbitControls drag
}
}
function _onTouchStart(event) {
if (!event.touches || event.touches.length === 0) return;
const t = event.touches[0];
if (_hitTest(t.clientX, t.clientY)) {
event.stopImmediatePropagation();
event.preventDefault(); // suppress subsequent mouse events (ghost click)
}
}
export function updateControls() {
if (controls) controls.update();
}
/**
* Dispose OrbitControls event listeners.
* Called before context-loss teardown.
*/
export function disposeInteraction() {
if (_canvas) {
_canvas.removeEventListener('contextmenu', _noCtxMenu);
_canvas.removeEventListener('pointerdown', _onPointerDown, { capture: true });
if (!window.PointerEvent) {
_canvas.removeEventListener('touchstart', _onTouchStart, { capture: true });
}
_canvas = null;
}
if (controls) {
controls.dispose();
controls = null;
}
_camera = null;
_timmyGroup = null;
_applySlap = null;
}