| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>MorphoWeb 3D - Real-time Collaboration</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <script src="https://unpkg.com/feather-icons"></script> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> |
| <script src="https://cdn.socket.io/4.5.0/socket.io.min.js"></script> |
| <style> |
| .collab-cursor { |
| position: absolute; |
| width: 16px; |
| height: 16px; |
| border-radius: 50%; |
| pointer-events: none; |
| z-index: 1000; |
| } |
| .user-avatar { |
| width: 32px; |
| height: 32px; |
| border-radius: 50%; |
| border: 2px solid; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| font-size: 12px; |
| font-weight: bold; |
| } |
| #preview-container { |
| background-color: #f3f4f6; |
| border-radius: 0.5rem; |
| height: 500px; |
| position: relative; |
| overflow: hidden; |
| } |
| </style> |
| </head> |
| <body class="bg-gray-100 min-h-screen"> |
| <div class="flex flex-col h-screen"> |
| |
| <div class="bg-white shadow-sm py-3 px-6 flex justify-between items-center"> |
| <div class="flex items-center space-x-4"> |
| <h1 class="font-bold text-xl flex items-center"> |
| <i data-feather="users" class="text-indigo-600 mr-2"></i> |
| Real-time Collaboration |
| </h1> |
| <div class="flex items-center space-x-2 bg-indigo-50 text-indigo-700 px-3 py-1 rounded-full"> |
| <div class="w-2 h-2 bg-green-500 rounded-full"></div> |
| <span class="text-sm">Connected</span> |
| </div> |
| </div> |
| |
| <div class="flex items-center space-x-4"> |
| <div class="flex -space-x-2"> |
| <div class="user-avatar bg-red-100 border-red-400 text-red-600">JD</div> |
| <div class="user-avatar bg-blue-100 border-blue-400 text-blue-600">AM</div> |
| <div class="user-avatar bg-green-100 border-green-400 text-green-600">SK</div> |
| </div> |
| <button class="bg-indigo-600 text-white px-4 py-2 rounded-md flex items-center"> |
| <i data-feather="share-2" class="mr-2"></i> Invite |
| </button> |
| </div> |
| </div> |
| |
| |
| <div class="flex flex-1 overflow-hidden"> |
| |
| <div class="w-64 bg-white shadow-md p-4 overflow-y-auto"> |
| <div class="mb-6"> |
| <h2 class="font-bold mb-3 flex items-center"> |
| <i data-feather="users" class="mr-2"></i> Team Members |
| </h2> |
| <div class="space-y-2"> |
| <div class="flex items-center p-2 bg-indigo-50 rounded-lg"> |
| <div class="user-avatar bg-red-100 border-red-400 text-red-600 mr-3">JD</div> |
| <div> |
| <p class="font-medium">John Doe</p> |
| <p class="text-xs text-gray-500">Editing</p> |
| </div> |
| </div> |
| <div class="flex items-center p-2 hover:bg-gray-50 rounded-lg"> |
| <div class="user-avatar bg-blue-100 border-blue-400 text-blue-600 mr-3">AM</div> |
| <div> |
| <p class="font-medium">Alice Martin</p> |
| <p class="text-xs text-gray-500">Viewing</p> |
| </div> |
| </div> |
| <div class="flex items-center p-2 hover:bg-gray-50 rounded-lg"> |
| <div class="user-avatar bg-green-100 border-green-400 text-green-600 mr-3">SK</div> |
| <div> |
| <p class="font-medium">Sam Kim</p> |
| <p class="text-xs text-gray-500">Viewing</p> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| <div class="mb-6"> |
| <h2 class="font-bold mb-3 flex items-center"> |
| <i data-feather="message-square" class="mr-2"></i> Chat |
| </h2> |
| <div id="chat-messages" class="h-48 overflow-y-auto mb-3 space-y-2"> |
| <div class="chat-message"> |
| <p class="text-sm font-medium">John Doe:</p> |
| <p class="text-sm">Let's work on the header section first</p> |
| </div> |
| <div class="chat-message"> |
| <p class="text-sm font-medium">Alice Martin:</p> |
| <p class="text-sm">Sounds good, I'll handle the styling</p> |
| </div> |
| </div> |
| <div class="flex"> |
| <input type="text" placeholder="Type a message..." class="flex-1 border border-gray-300 rounded-l-lg px-3 py-2 text-sm focus:outline-none"> |
| <button class="bg-indigo-600 text-white px-3 py-2 rounded-r-lg"> |
| <i data-feather="send" class="w-4 h-4"></i> |
| </button> |
| </div> |
| </div> |
| |
| <div> |
| <h2 class="font-bold mb-3 flex items-center"> |
| <i data-feather="clock" class="mr-2"></i> Version History |
| </h2> |
| <div class="space-y-2"> |
| <div class="text-sm p-2 bg-gray-50 rounded-lg cursor-pointer"> |
| <p class="font-medium">Version 1.2</p> |
| <p class="text-xs text-gray-500">Saved 5 minutes ago</p> |
| </div> |
| <div class="text-sm p-2 hover:bg-gray-50 rounded-lg cursor-pointer"> |
| <p class="font-medium">Version 1.1</p> |
| <p class="text-xs text-gray-500">Saved 1 hour ago</p> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="flex-1 flex flex-col overflow-hidden"> |
| |
| <div class="bg-white border-b border-gray-200 p-3 flex items-center justify-between"> |
| <div class="flex items-center space-x-4"> |
| <h2 class="font-medium">Project: My Website</h2> |
| <div class="flex items-center text-sm bg-green-50 text-green-700 px-3 py-1 rounded-full"> |
| <i data-feather="globe" class="w-4 h-4 mr-1"></i> |
| <span>Public Editing</span> |
| </div> |
| </div> |
| |
| <div class="flex space-x-3"> |
| <button class="flex items-center px-3 py-1 bg-gray-100 rounded-md text-sm hover:bg-gray-200"> |
| <i data-feather="save" class="w-4 h-4 mr-1"></i> |
| Save Version |
| </button> |
| <button class="flex items-center px-3 py-1 bg-indigo-600 text-white rounded-md text-sm hover:bg-indigo-700"> |
| <i data-feather="download" class="w-4 h-4 mr-1"></i> |
| Export |
| </button> |
| </div> |
| </div> |
| |
| |
| <div class="flex-1 flex overflow-hidden"> |
| <div id="preview-container"> |
| |
| <div id="collaborative-scene"></div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| document.addEventListener('DOMContentLoaded', () => { |
| feather.replace(); |
| |
| |
| const socket = io('https://collab.morphoweb3d.com'); |
| |
| |
| const scene = new THREE.Scene(); |
| const container = document.getElementById('collaborative-scene'); |
| const width = container.clientWidth; |
| const height = container.clientHeight; |
| const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000); |
| const renderer = new THREE.WebGLRenderer({ antialias: true }); |
| |
| renderer.setSize(width, height); |
| container.appendChild(renderer.domElement); |
| |
| |
| const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); |
| scene.add(ambientLight); |
| |
| const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); |
| directionalLight.position.set(1, 1, 1); |
| scene.add(directionalLight); |
| |
| |
| const gridHelper = new THREE.GridHelper(10, 10); |
| scene.add(gridHelper); |
| |
| |
| camera.position.z = 5; |
| |
| |
| const animate = () => { |
| requestAnimationFrame(animate); |
| renderer.render(scene, camera); |
| }; |
| animate(); |
| |
| |
| window.addEventListener('resize', () => { |
| camera.aspect = container.clientWidth / container.clientHeight; |
| camera.updateProjectionMatrix(); |
| renderer.setSize(container.clientWidth, container.clientHeight); |
| }); |
| |
| |
| const users = {}; |
| const cursors = {}; |
| |
| |
| const currentUserId = 'user-' + Math.random().toString(36).substr(2, 9); |
| const currentUserColor = `hsl(${Math.floor(Math.random() * 360)}, 70%, 60%)`; |
| |
| |
| socket.on('user-update', (data) => { |
| if (data.userId === currentUserId) return; |
| |
| |
| if (!cursors[data.userId]) { |
| const cursor = document.createElement('div'); |
| cursor.className = 'collab-cursor'; |
| cursor.style.backgroundColor = data.color; |
| cursor.style.border = `2px solid ${data.color}`; |
| document.getElementById('preview-container').appendChild(cursor); |
| cursors[data.userId] = cursor; |
| |
| |
| const tooltip = document.createElement('div'); |
| tooltip.className = 'absolute text-xs font-medium whitespace-nowrap -mt-6'; |
| tooltip.style.color = data.color; |
| tooltip.textContent = data.username; |
| cursor.appendChild(tooltip); |
| } |
| |
| |
| const cursor = cursors[data.userId]; |
| if (cursor) { |
| cursor.style.left = `${data.x}px`; |
| cursor.style.top = `${data.y}px`; |
| } |
| |
| |
| if (data.action === 'add-object') { |
| |
| const geometry = new THREE.BoxGeometry(); |
| const material = new THREE.MeshBasicMaterial({ color: new THREE.Color(data.color) }); |
| const cube = new THREE.Mesh(geometry, material); |
| cube.position.set(data.position.x, data.position.y, data.position.z); |
| cube.userData.userId = data.userId; |
| scene.add(cube); |
| } |
| }); |
| |
| |
| document.getElementById('preview-container').addEventListener('mousemove', (e) => { |
| const rect = e.target.getBoundingClientRect(); |
| const x = e.clientX - rect.left; |
| const y = e.clientY - rect.top; |
| |
| |
| socket.emit('user-update', { |
| userId: currentUserId, |
| username: 'You', |
| color: currentUserColor, |
| x: x, |
| y: y |
| }); |
| }); |
| |
| |
| document.getElementById('preview-container').addEventListener('click', (e) => { |
| const rect = e.target.getBoundingClientRect(); |
| const x = (e.clientX - rect.left) / rect.width * 2 - 1; |
| const y = -(e.clientY - rect.top) / rect.height * 2 + 1; |
| |
| |
| const mouse = new THREE.Vector2(x, y); |
| const raycaster = new THREE.Raycaster(); |
| raycaster.setFromCamera(mouse, camera); |
| const intersects = raycaster.intersectObject(gridHelper); |
| |
| if (intersects.length > 0) { |
| const position = intersects[0].point; |
| |
| |
| const geometry = new THREE.BoxGeometry(1, 1, 1); |
| const material = new THREE.MeshBasicMaterial({ color: new THREE.Color(currentUserColor) }); |
| const cube = new THREE.Mesh(geometry, material); |
| cube.position.copy(position); |
| cube.userData.userId = currentUserId; |
| scene.add(cube); |
| |
| |
| socket.emit('user-update', { |
| userId: currentUserId, |
| username: 'You', |
| color: currentUserColor, |
| action: 'add-object', |
| position: { |
| x: position.x, |
| y: position.y, |
| z: position.z |
| } |
| }); |
| } |
| }); |
| |
| |
| const chatInput = document.querySelector('#chat-messages + div input'); |
| const chatButton = document.querySelector('#chat-messages + div button'); |
| |
| chatButton.addEventListener('click', sendMessage); |
| chatInput.addEventListener('keypress', (e) => { |
| if (e.key === 'Enter') sendMessage(); |
| }); |
| |
| function sendMessage() { |
| const message = chatInput.value.trim(); |
| if (message) { |
| socket.emit('chat-message', { |
| userId: currentUserId, |
| username: 'You', |
| message: message, |
| color: currentUserColor |
| }); |
| chatInput.value = ''; |
| } |
| } |
| |
| socket.on('chat-message', (data) => { |
| const chat = document.getElementById('chat-messages'); |
| const messageDiv = document.createElement('div'); |
| messageDiv.className = 'chat-message'; |
| messageDiv.innerHTML = ` |
| <p class="text-sm font-medium" style="color: ${data.color}">${data.username}:</p> |
| <p class="text-sm">${data.message}</p> |
| `; |
| chat.appendChild(messageDiv); |
| chat.scrollTop = chat.scrollHeight; |
| }); |
| }); |
| </script> |
| </body> |
| </html> |