forked from Rockachopa/the-matrix
Compare commits
1 Commits
main
...
claude/iss
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5f407b892 |
39
index.html
39
index.html
@@ -159,6 +159,44 @@
|
||||
@media (max-width: 500px) {
|
||||
#status-panel { top: 100px !important; left: 16px; right: auto; }
|
||||
}
|
||||
|
||||
/* ── Touch controls (mobile) ── */
|
||||
.tap-feedback {
|
||||
position: fixed;
|
||||
z-index: 50;
|
||||
pointer-events: none;
|
||||
color: #00ff41;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
text-shadow: 0 0 8px #00ff41;
|
||||
animation: tapPop 1.2s ease-out forwards;
|
||||
transform: translateX(-50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
@keyframes tapPop {
|
||||
0% { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||||
100% { opacity: 0; transform: translateX(-50%) translateY(-24px); }
|
||||
}
|
||||
|
||||
#touch-hint {
|
||||
position: fixed;
|
||||
bottom: 80px; left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0, 10, 0, 0.88);
|
||||
border: 1px solid #003300;
|
||||
color: #007722;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 11px;
|
||||
letter-spacing: 1.5px;
|
||||
padding: 8px 18px;
|
||||
z-index: 30;
|
||||
pointer-events: none;
|
||||
text-align: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.4s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
#touch-hint.visible { opacity: 1; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -182,6 +220,7 @@
|
||||
<button id="chat-clear-btn" title="Clear chat history" style="position:fixed;bottom:60px;right:16px;background:transparent;border:1px solid #003300;color:#00aa00;font-family:monospace;font-size:.7rem;padding:2px 6px;cursor:pointer;z-index:20;opacity:.6">✕ CLEAR</button>
|
||||
<div id="bark-container"></div>
|
||||
<div id="connection-status">OFFLINE</div>
|
||||
<div id="touch-hint">DRAG · PINCH TO ZOOM · TAP AGENT</div>
|
||||
</div>
|
||||
<div id="chat-input-bar">
|
||||
<input id="chat-input" type="text" placeholder="Say something to the Workshop..." autocomplete="off" />
|
||||
|
||||
@@ -285,6 +285,14 @@ export function applyAgentStates(snapshot) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get core meshes for raycasting (tap-to-interact).
|
||||
* Returns array of { mesh, id, label }.
|
||||
*/
|
||||
export function getAgentMeshes() {
|
||||
return [...agents.values()].map(a => ({ mesh: a.core, id: a.id, label: a.label }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose all agent resources (used on world teardown).
|
||||
*/
|
||||
|
||||
@@ -1,19 +1,90 @@
|
||||
import * as THREE from 'three';
|
||||
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
||||
import { getAgentMeshes } from './agents.js';
|
||||
import { sendVisitorInteraction } from './visitor.js';
|
||||
|
||||
let controls;
|
||||
let _camera, _renderer;
|
||||
const raycaster = new THREE.Raycaster();
|
||||
const _tapStart = { x: 0, y: 0 };
|
||||
const TAP_MAX_MOVE = 10; // px — movement above this = drag, not tap
|
||||
let _touchHintShown = false;
|
||||
|
||||
export function initInteraction(camera, renderer) {
|
||||
_camera = camera;
|
||||
_renderer = renderer;
|
||||
|
||||
controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls.enableDamping = true;
|
||||
controls.dampingFactor = 0.05;
|
||||
controls.enableDamping = true;
|
||||
controls.dampingFactor = 0.05;
|
||||
controls.screenSpacePanning = false;
|
||||
controls.minDistance = 5;
|
||||
controls.maxDistance = 80;
|
||||
controls.maxPolarAngle = Math.PI / 2.1;
|
||||
controls.minDistance = 5;
|
||||
controls.maxDistance = 80;
|
||||
controls.maxPolarAngle = Math.PI / 2.1;
|
||||
controls.target.set(0, 0, 0);
|
||||
|
||||
// Explicit touch mapping: one finger = orbit, two fingers = pinch-zoom + pan
|
||||
controls.touches = {
|
||||
ONE: THREE.TOUCH.ROTATE,
|
||||
TWO: THREE.TOUCH.DOLLY_PAN,
|
||||
};
|
||||
|
||||
controls.update();
|
||||
|
||||
renderer.domElement.addEventListener('contextmenu', e => e.preventDefault());
|
||||
renderer.domElement.addEventListener('pointerdown', _onPointerDown);
|
||||
renderer.domElement.addEventListener('pointerup', _onPointerUp);
|
||||
}
|
||||
|
||||
function _onPointerDown(e) {
|
||||
_tapStart.x = e.clientX;
|
||||
_tapStart.y = e.clientY;
|
||||
|
||||
// Show touch hint on first touch interaction
|
||||
if (!_touchHintShown && e.pointerType === 'touch') {
|
||||
_touchHintShown = true;
|
||||
_showTouchHint();
|
||||
}
|
||||
}
|
||||
|
||||
function _onPointerUp(e) {
|
||||
const dx = e.clientX - _tapStart.x;
|
||||
const dy = e.clientY - _tapStart.y;
|
||||
if (Math.sqrt(dx * dx + dy * dy) > TAP_MAX_MOVE) return; // drag, not tap
|
||||
|
||||
const rect = _renderer.domElement.getBoundingClientRect();
|
||||
const ndc = new THREE.Vector2(
|
||||
((e.clientX - rect.left) / rect.width) * 2 - 1,
|
||||
-((e.clientY - rect.top) / rect.height) * 2 + 1,
|
||||
);
|
||||
raycaster.setFromCamera(ndc, _camera);
|
||||
|
||||
const meshes = getAgentMeshes();
|
||||
const hits = raycaster.intersectObjects(meshes.map(m => m.mesh));
|
||||
if (hits.length > 0) {
|
||||
const hit = meshes.find(m => m.mesh === hits[0].object);
|
||||
if (hit) {
|
||||
sendVisitorInteraction(hit.id, 'tap');
|
||||
_showTapFeedback(hit.label, e.clientX, e.clientY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _showTapFeedback(label, x, y) {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'tap-feedback';
|
||||
el.textContent = `[ ${label} ]`;
|
||||
el.style.left = x + 'px';
|
||||
el.style.top = (y - 40) + 'px';
|
||||
document.body.appendChild(el);
|
||||
setTimeout(() => el.remove(), 1200);
|
||||
}
|
||||
|
||||
function _showTouchHint() {
|
||||
const hint = document.getElementById('touch-hint');
|
||||
if (!hint) return;
|
||||
hint.classList.add('visible');
|
||||
setTimeout(() => hint.classList.remove('visible'), 3500);
|
||||
}
|
||||
|
||||
export function updateControls() {
|
||||
@@ -21,11 +92,17 @@ export function updateControls() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose orbit controls (used on world teardown).
|
||||
* Dispose orbit controls and interaction listeners (used on world teardown).
|
||||
*/
|
||||
export function disposeInteraction() {
|
||||
if (_renderer) {
|
||||
_renderer.domElement.removeEventListener('pointerdown', _onPointerDown);
|
||||
_renderer.domElement.removeEventListener('pointerup', _onPointerUp);
|
||||
}
|
||||
if (controls) {
|
||||
controls.dispose();
|
||||
controls = null;
|
||||
}
|
||||
_camera = null;
|
||||
_renderer = null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user