uex / index.html
Reaperxxxx's picture
Update index.html
276a6d5 verified
Raw
History Blame Contribute Delete
21.9 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
<title>3D Garage Explorer</title>
<style>
body {
margin: 0;
overflow: hidden;
background: #000;
font-family: Arial, sans-serif;
}
canvas {
display: block;
width: 100%;
height: 100%;
}
#loading {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.8);
color: #fff;
padding: 20px 40px;
border-radius: 8px;
text-align: center;
z-index: 100;
font-size: 18px;
}
#orientationWarning {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.95);
color: #fff;
display: none;
justify-content: center;
align-items: center;
flex-direction: column;
z-index: 200;
font-size: 24px;
text-align: center;
padding: 20px;
}
#orientationWarning.show {
display: flex;
}
.rotate-icon {
font-size: 64px;
margin: 20px 0;
animation: rotate 2s infinite ease-in-out;
}
@keyframes rotate {
0%, 100% { transform: rotate(0deg); }
50% { transform: rotate(90deg); }
}
#joystick {
position: fixed;
bottom: 20px;
left: 20px;
width: 140px;
height: 140px;
background: radial-gradient(circle, rgba(50, 50, 50, 0.4), rgba(30, 30, 30, 0.6));
border: 3px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
z-index: 50;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
}
#joystickHandle {
position: absolute;
width: 50px;
height: 50px;
background: radial-gradient(circle, rgba(0, 150, 255, 0.8), rgba(0, 100, 200, 0.6));
border: 3px solid rgba(255, 255, 255, 0.5);
border-radius: 50%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
cursor: grab;
box-shadow: 0 0 15px rgba(0, 150, 255, 0.6);
transition: none;
will-change: transform;
}
#joystickHandle:active {
cursor: grabbing;
box-shadow: 0 0 25px rgba(0, 150, 255, 1);
}
.button-group {
position: fixed;
right: 20px;
bottom: 20px;
display: flex;
flex-direction: column;
gap: 12px;
z-index: 50;
}
.control-btn {
padding: 16px 24px;
background: linear-gradient(135deg, rgba(0, 150, 255, 0.7), rgba(0, 100, 200, 0.7));
border: 2px solid rgba(255, 255, 255, 0.3);
color: white;
border-radius: 12px;
cursor: pointer;
font-weight: bold;
font-size: 18px;
min-width: 90px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
transition: all 0.1s ease;
user-select: none;
}
.control-btn:active {
background: linear-gradient(135deg, rgba(0, 150, 255, 1), rgba(0, 100, 200, 1));
transform: scale(0.95);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
}
#coordinates {
position: fixed;
top: 10px;
right: 10px;
left: auto;
background: linear-gradient(135deg, rgba(0, 0, 0, 0.85), rgba(20, 20, 40, 0.85));
color: #00ff88;
padding: 5px 8px;
border-radius: 6px;
font-family: 'Courier New', monospace;
font-size: 9px;
z-index: 50;
max-width: 120px;
border: 1px solid rgba(0, 255, 136, 0.3);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
}
#fps {
position: fixed;
top: 10px;
left: 10px;
background: rgba(0, 0, 0, 0.7);
color: #00ff00;
padding: 4px 8px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 11px;
z-index: 50;
border: 1px solid rgba(0, 255, 0, 0.3);
}
#coordinates > div {
margin: 1px 0;
text-shadow: 0 0 5px rgba(0, 255, 136, 0.5);
}
.coord-label {
color: #00ffff;
font-weight: bold;
font-size: 10px;
}
.coord-input {
background: rgba(0, 30, 20, 0.9);
border: 1px solid #00ff88;
color: #00ff88;
padding: 4px;
border-radius: 3px;
font-family: 'Courier New', monospace;
margin-top: 4px;
width: calc(100% - 10px);
font-size: 8px;
}
.spawn-btn {
background: linear-gradient(135deg, rgba(0, 180, 100, 0.8), rgba(0, 120, 80, 0.8));
border: 1px solid #00ff88;
color: #fff;
padding: 4px;
border-radius: 3px;
margin-top: 4px;
cursor: pointer;
font-weight: bold;
width: 100%;
transition: all 0.1s ease;
user-select: none;
font-size: 9px;
}
.spawn-btn:active {
transform: scale(0.97);
background: linear-gradient(135deg, rgba(0, 200, 110, 1), rgba(0, 140, 90, 1));
}
#crosshair {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 20px;
height: 20px;
z-index: 100;
pointer-events: none;
}
.crosshair-line {
position: absolute;
background: rgba(255, 255, 255, 0.8);
}
.crosshair-h {
width: 12px;
height: 2px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.crosshair-v {
width: 2px;
height: 12px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.crosshair-dot {
width: 3px;
height: 3px;
background: rgba(255, 255, 255, 0.9);
border-radius: 50%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>
</head>
<body>
<div id="orientationWarning">
<div class="rotate-icon">📱</div>
<div>Please rotate your device to landscape mode</div>
<div style="font-size: 16px; margin-top: 10px; opacity: 0.7;">for the best experience</div>
</div>
<div id="loading">Loading...</div>
<div id="joystick">
<div id="joystickHandle"></div>
</div>
<div class="button-group">
<button class="control-btn" id="upBtn">↑ Up</button>
<button class="control-btn" id="downBtn">↓ Down</button>
</div>
<div id="coordinates">
<div class="coord-label">POS</div>
<div>X: <span id="coordX">0.00</span></div>
<div>Y: <span id="coordY">0.00</span></div>
<div>Z: <span id="coordZ">0.00</span></div>
<input type="text" class="coord-input" id="spawnInput" placeholder="Spawn JSON">
<button class="spawn-btn" onclick="loadSpawn()">Set</button>
</div>
<div id="crosshair">
<div class="crosshair-line crosshair-h"></div>
<div class="crosshair-line crosshair-v"></div>
<div class="crosshair-dot"></div>
</div>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.128.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { FBXLoader } from 'three/addons/loaders/FBXLoader.js';
let scene, camera, renderer, playerModel, garage;
let playerPos = { x: 0, y: 1, z: 0 };
let moveSideways = 0;
let moveForwardBack = 0;
let moveSpeed = 0.06;
let cameraRotation = { x: 0, y: 0 };
let spawnPoint = { x: -3.5719999999999996, y: 1, z: 0.20199999999999968 };
let joystickActive = false;
let lookTouchId = null;
let loadingSteps = { character: false, garage: false };
// Check orientation
function checkOrientation() {
const warning = document.getElementById('orientationWarning');
if (window.innerHeight > window.innerWidth) {
warning.classList.add('show');
} else {
warning.classList.remove('show');
}
}
checkOrientation();
window.addEventListener('resize', checkOrientation);
window.addEventListener('orientationchange', checkOrientation);
function updateLoadingStatus() {
const allLoaded = loadingSteps.character && loadingSteps.garage;
if (allLoaded) {
document.getElementById('loading').style.display = 'none';
}
}
function init() {
scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a2e);
scene.fog = new THREE.Fog(0x1a1a2e, 5, 50);
camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.01,
50
);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);
// Lighting
scene.add(new THREE.AmbientLight(0xffffff, 0.7));
const dirLight1 = new THREE.DirectionalLight(0xffffff, 0.8);
dirLight1.position.set(20, 20, 20);
dirLight1.castShadow = true;
scene.add(dirLight1);
const dirLight2 = new THREE.DirectionalLight(0xffffff, 0.6);
dirLight2.position.set(-20, 15, -20);
scene.add(dirLight2);
const dirLight3 = new THREE.DirectionalLight(0xffffff, 0.5);
dirLight3.position.set(0, 20, -20);
scene.add(dirLight3);
const pointLight1 = new THREE.PointLight(0xffffff, 0.5, 50);
pointLight1.position.set(10, 10, 10);
scene.add(pointLight1);
const pointLight2 = new THREE.PointLight(0xffffff, 0.5, 50);
pointLight2.position.set(-10, 10, -10);
scene.add(pointLight2);
const pointLight3 = new THREE.PointLight(0xffffff, 0.4, 50);
pointLight3.position.set(0, 5, 0);
scene.add(pointLight3);
// Load character model
const fbxLoader = new FBXLoader();
document.getElementById('loading').textContent = 'Loading character...';
fbxLoader.load('./character.fbx', (fbx) => {
playerModel = new THREE.Group();
// Reset FBX position to origin before scaling
fbx.position.set(0, 0, 0);
fbx.scale.set(0.01, 0.01, 0.01);
// Make character visible with proper materials
fbx.traverse((child) => {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
if (child.material) {
child.material.transparent = false;
child.material.opacity = 1;
child.material.side = THREE.DoubleSide;
}
}
});
playerModel.add(fbx);
playerModel.position.set(playerPos.x, playerPos.y, playerPos.z);
scene.add(playerModel);
loadingSteps.character = true;
updateLoadingStatus();
console.log('✅ Character loaded!');
console.log('Character world position:', playerModel.position);
console.log('FBX local position:', fbx.position);
console.log('Camera position:', camera.position);
}, (xhr) => {
const percent = (xhr.loaded / xhr.total * 100).toFixed(1);
document.getElementById('loading').textContent = `Loading character ${percent}%`;
}, (error) => {
console.error('Error loading character:', error);
document.getElementById('loading').textContent = 'Error loading character.fbx';
});
// Load garage
const gltfLoader = new GLTFLoader();
gltfLoader.load('./map.glb', (gltf) => {
garage = gltf.scene;
garage.scale.set(-1, 1, 1);
scene.add(garage);
garage.traverse((child) => {
child.castShadow = true;
child.receiveShadow = true;
if (child.geometry) {
child.userData.isCollider = true;
}
});
playerPos.x = spawnPoint.x;
playerPos.y = spawnPoint.y;
playerPos.z = spawnPoint.z;
loadingSteps.garage = true;
updateLoadingStatus();
console.log('✅ Garage loaded!');
}, undefined, (error) => {
console.error('Error loading garage:', error);
document.getElementById('loading').textContent = 'Error loading map.glb';
});
setupControls();
animate();
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
}
function setupControls() {
const joystick = document.getElementById('joystick');
const joystickHandle = document.getElementById('joystickHandle');
let joystickTouchId = null;
let lastLookPos = { x: 0, y: 0 };
joystick.addEventListener('touchstart', (e) => {
joystickActive = true;
joystickTouchId = e.touches[0].identifier;
e.preventDefault();
e.stopPropagation();
});
document.addEventListener('touchmove', (e) => {
e.preventDefault();
for (let i = 0; i < e.touches.length; i++) {
const touch = e.touches[i];
if (joystickTouchId !== null && touch.identifier === joystickTouchId) {
const rect = joystick.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const dx = touch.clientX - centerX;
const dy = touch.clientY - centerY;
const distance = Math.sqrt(dx * dx + dy * dy);
const maxDistance = 60;
if (distance > 5) {
const limitedDistance = Math.min(distance, maxDistance);
const normalizedDx = (dx / distance) * limitedDistance;
const normalizedDy = (dy / distance) * limitedDistance;
joystickHandle.style.transform = `translate(calc(-50% + ${normalizedDx}px), calc(-50% + ${normalizedDy}px))`;
moveForwardBack = -(dy / maxDistance);
moveSideways = -(dx / maxDistance);
} else {
joystickHandle.style.transform = 'translate(-50%, -50%)';
moveForwardBack = 0;
moveSideways = 0;
}
}
if (touch.identifier !== joystickTouchId) {
if (lookTouchId === null) {
const joystickRect = joystick.getBoundingClientRect();
const coordsRect = document.getElementById('coordinates').getBoundingClientRect();
const notTouchingJoystick = !(touch.clientX >= joystickRect.left - 30 &&
touch.clientX <= joystickRect.right + 30 &&
touch.clientY >= joystickRect.top - 30 &&
touch.clientY <= joystickRect.bottom + 30);
const notTouchingCoords = !(touch.clientX >= coordsRect.left - 10 &&
touch.clientX <= coordsRect.right + 10 &&
touch.clientY >= coordsRect.top - 10 &&
touch.clientY <= coordsRect.bottom + 10);
if (notTouchingJoystick && notTouchingCoords && touch.clientX < window.innerWidth * 0.85) {
lookTouchId = touch.identifier;
lastLookPos = { x: touch.clientX, y: touch.clientY };
}
} else if (touch.identifier === lookTouchId) {
const deltaX = touch.clientX - lastLookPos.x;
const deltaY = touch.clientY - lastLookPos.y;
cameraRotation.y -= deltaX * 0.012;
cameraRotation.x -= deltaY * 0.012;
cameraRotation.x = Math.max(-Math.PI / 2.5, Math.min(Math.PI / 2.5, cameraRotation.x));
lastLookPos = { x: touch.clientX, y: touch.clientY };
}
}
}
}, { passive: false });
document.addEventListener('touchend', (e) => {
const remainingTouchIds = Array.from(e.touches).map(t => t.identifier);
if (joystickTouchId !== null && !remainingTouchIds.includes(joystickTouchId)) {
joystickActive = false;
joystickTouchId = null;
joystickHandle.style.transform = 'translate(-50%, -50%)';
moveForwardBack = 0;
moveSideways = 0;
}
if (lookTouchId !== null && !remainingTouchIds.includes(lookTouchId)) {
lookTouchId = null;
}
});
document.getElementById('upBtn').addEventListener('touchstart', (e) => {
e.preventDefault();
e.stopPropagation();
playerPos.y += 0.5;
});
document.getElementById('downBtn').addEventListener('touchstart', (e) => {
e.preventDefault();
e.stopPropagation();
playerPos.y = Math.max(0.5, playerPos.y - 0.5);
});
}
function updateCoordinates() {
document.getElementById('coordX').textContent = playerPos.x.toFixed(2);
document.getElementById('coordY').textContent = playerPos.y.toFixed(2);
document.getElementById('coordZ').textContent = playerPos.z.toFixed(2);
}
function checkCollisions(newPos) {
if (!garage) return newPos;
const playerRadius = 0.15;
const raycaster = new THREE.Raycaster();
const directions = [
new THREE.Vector3(1, 0, 0),
new THREE.Vector3(-1, 0, 0),
new THREE.Vector3(0, 0, 1),
new THREE.Vector3(0, 0, -1),
];
const origin = new THREE.Vector3(newPos.x, newPos.y + 0.25, newPos.z);
let blocked = false;
for (const dir of directions) {
raycaster.set(origin, dir);
const intersects = raycaster.intersectObjects(garage.children, true);
if (intersects.length > 0 && intersects[0].distance < playerRadius) {
blocked = true;
break;
}
}
if (!blocked) {
return newPos;
}
const tryX = { x: newPos.x, y: playerPos.y, z: playerPos.z };
const tryZ = { x: playerPos.x, y: playerPos.y, z: newPos.z };
let xBlocked = false;
const originX = new THREE.Vector3(tryX.x, tryX.y + 0.25, tryX.z);
for (const dir of directions) {
raycaster.set(originX, dir);
const intersects = raycaster.intersectObjects(garage.children, true);
if (intersects.length > 0 && intersects[0].distance < playerRadius) {
xBlocked = true;
break;
}
}
let zBlocked = false;
const originZ = new THREE.Vector3(tryZ.x, tryZ.y + 0.25, tryZ.z);
for (const dir of directions) {
raycaster.set(originZ, dir);
const intersects = raycaster.intersectObjects(garage.children, true);
if (intersects.length > 0 && intersects[0].distance < playerRadius) {
zBlocked = true;
break;
}
}
if (!xBlocked) return tryX;
if (!zBlocked) return tryZ;
return playerPos;
}
function animate() {
requestAnimationFrame(animate);
const forwardX = -Math.sin(cameraRotation.y) * moveForwardBack * moveSpeed;
const forwardZ = -Math.cos(cameraRotation.y) * moveForwardBack * moveSpeed;
const strafeX = -Math.sin(cameraRotation.y + Math.PI / 2) * moveSideways * moveSpeed;
const strafeZ = -Math.cos(cameraRotation.y + Math.PI / 2) * moveSideways * moveSpeed;
const newPos = {
x: playerPos.x + forwardX + strafeX,
y: playerPos.y,
z: playerPos.z + forwardZ + strafeZ
};
playerPos = checkCollisions(newPos);
if (playerModel) {
playerModel.position.set(playerPos.x, playerPos.y, playerPos.z);
// Character rotation should match camera Y rotation so it faces forward
playerModel.rotation.y = cameraRotation.y;
}
// Position camera at character's eye level (typical human eye height is ~1.6-1.7m above feet)
const eyeHeight = 1.6; // Adjust this if your character model is scaled differently
camera.position.set(
playerPos.x,
playerPos.y + eyeHeight, // Camera at eye level
playerPos.z
);
camera.rotation.order = 'YXZ';
camera.rotation.y = cameraRotation.y;
camera.rotation.x = cameraRotation.x - 0.5; // Look down to see torso/legs below camera
updateCoordinates();
renderer.render(scene, camera);
}
window.loadSpawn = function() {
try {
const input = document.getElementById('spawnInput').value;
const coords = JSON.parse(input);
playerPos.x = coords.x || 0;
playerPos.y = coords.y || 1;
playerPos.z = coords.z || 0;
alert('✅ Spawn point set!');
} catch (e) {
alert('❌ Invalid JSON format');
}
};
window.copySpawn = function() {
const spawnData = JSON.stringify(playerPos);
navigator.clipboard.writeText(spawnData);
alert('✅ Copied to clipboard: ' + spawnData);
};
const coordsDiv = document.getElementById('coordinates');
const copyBtn = document.createElement('button');
copyBtn.className = 'spawn-btn';
copyBtn.textContent = 'Copy Spawn';
copyBtn.onclick = window.copySpawn;
coordsDiv.appendChild(copyBtn);
init();
</script>
</body>
</html>