Spaces:
Runtime error
Runtime error
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>The Marauder's Map</title> | |
| <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" /> | |
| <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script> | |
| <style> | |
| html, body { | |
| width: 100%; | |
| height: 100%; | |
| margin: 0; | |
| background: #000; | |
| } | |
| .map-container { | |
| width: 1440px; | |
| height: 900px; | |
| position: relative; | |
| background-image: url('../static/map-bg.png'); | |
| background-size: cover; | |
| background-position: center; | |
| } | |
| #leaflet-map { | |
| width: 100%; | |
| height: 100%; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| background: transparent; | |
| } | |
| .leaflet-tile-container img { | |
| opacity: 0.4 ; | |
| filter: grayscale(100%) ; | |
| } | |
| /* .map .mask { | |
| width: 100%; | |
| height: 100%; | |
| background-image: url("../static/cloud.png"); | |
| mix-blend-mode: lighten; | |
| animation: 4s linear 0s magic forwards; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| } */ | |
| .map .mask { | |
| width: 100%; | |
| height: 100%; | |
| background-image: url("../static/cloud.png"); | |
| mix-blend-mode: lighten; | |
| animation: 3s linear 0s magic forwards; | |
| position: absolute; | |
| } | |
| .track { | |
| position: absolute; | |
| top: 0; | |
| width: 1440px; | |
| height: 900px; | |
| mix-blend-mode: multiply; | |
| opacity: .7; | |
| pointer-events: none; | |
| } | |
| .footprint { | |
| position: absolute; | |
| pointer-events: none; | |
| } | |
| .footprint .foot { | |
| position: absolute; | |
| width: 10px; | |
| height: 22px; | |
| background-image: url('../static/footprints.png'); | |
| background-size: 40px; | |
| background-repeat: no-repeat; | |
| background-position-x: 10px; | |
| animation: 1s linear 0s footsteps forwards; | |
| } | |
| .footprint .foot::after { | |
| display: block; | |
| content: ''; | |
| width: 100%; | |
| height: 100%; | |
| background-image: url('../static/footprints-cloud.png'); | |
| background-repeat: no-repeat; | |
| background-size: 20px 22px; | |
| mix-blend-mode: lighten; | |
| animation: 8s linear 1s footHide forwards; | |
| opacity: 0; | |
| } | |
| .footprint.last .foot::after { | |
| animation-play-state: paused; | |
| } | |
| @keyframes footHide { | |
| 0% { | |
| opacity: 0; | |
| } | |
| 25% { | |
| opacity: 1; | |
| filter: brightness(1); | |
| } | |
| 100% { | |
| opacity: 1; | |
| filter: brightness(10); | |
| } | |
| } | |
| .footprint .foot.right::after { | |
| background-position-x: -10px; | |
| } | |
| @keyframes footsteps { | |
| 0% { | |
| background-position-x: 0px; | |
| } | |
| 25% { | |
| background-position-x: 0px; | |
| } | |
| 25.1% { | |
| background-position-x: -10px; | |
| } | |
| 50% { | |
| background-position-x: -10px; | |
| } | |
| 50.1% { | |
| background-position-x: -20px; | |
| } | |
| 75% { | |
| background-position-x: -20px; | |
| } | |
| 75.1% { | |
| background-position-x: -30px; | |
| } | |
| 100% { | |
| background-position-x: -30px; | |
| } | |
| } | |
| .footprint .foot.left { | |
| left: -5px; | |
| top: 7px; | |
| } | |
| .footprint .foot.right { | |
| left: 5px; | |
| top: -7px; | |
| background-position-y: -22px; | |
| animation-delay: 1s; | |
| } | |
| @keyframes magic { | |
| 0% { | |
| filter: brightness(10); | |
| opacity: 1; | |
| } | |
| 70% { | |
| filter: brightness(8); | |
| opacity: 1; | |
| } | |
| 100% { | |
| filter: brightness(0); | |
| opacity: 0; | |
| } | |
| } | |
| .name-banner { | |
| position: absolute; | |
| width: 160px; | |
| height: 40px; | |
| background-image: url('../static/banner.svg'); | |
| background-repeat: no-repeat; | |
| background-size: 160px 40px; | |
| text-align: center; | |
| line-height: 30px; | |
| font-family: 'Apple Chancery', cursive; | |
| z-index: 1000; | |
| animation: 1s linear 0s nameShow forwards; | |
| color: #3c2a1e; | |
| text-shadow: 0 0 2px rgba(255, 255, 255, 0.5); | |
| } | |
| @keyframes nameShow { | |
| from { | |
| opacity: 0; | |
| } | |
| to { | |
| opacity: 1; | |
| } | |
| } | |
| .user-controls { | |
| position: fixed; | |
| top: 20px; | |
| right: 20px; | |
| z-index: 1000; | |
| background: rgba(222, 184, 135, 0.9); | |
| padding: 15px; | |
| border-radius: 8px; | |
| border: 2px solid #8b4513; | |
| } | |
| .user-controls input { | |
| padding: 8px 12px; | |
| margin-right: 10px; | |
| border: 1px solid #8b4513; | |
| border-radius: 4px; | |
| font-family: 'Apple Chancery', cursive; | |
| background: rgba(255, 255, 255, 0.9); | |
| } | |
| .user-controls button { | |
| padding: 8px 16px; | |
| background: #8b4513; | |
| color: #fff; | |
| border: none; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-family: 'Apple Chancery', cursive; | |
| transition: background-color 0.3s; | |
| } | |
| .user-controls button:hover { | |
| background: #654321; | |
| } | |
| .leaflet-control-attribution { | |
| display: none; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="map-container"> | |
| <div class="map"> | |
| <div id="mapMask" class="mask"></div> | |
| </div> | |
| <div id="leaflet-map"></div> | |
| <div id="track" class="track"></div> | |
| <div class="user-controls"> | |
| <input type="text" id="username" placeholder="输入你的名字" maxlength="20"> | |
| <button onclick="updateUsername()">更新名字</button> | |
| </div> | |
| </div> | |
| <script> | |
| let map; | |
| let socket; | |
| let username = '陌生访客_' + Math.floor(Math.random() * 1000); | |
| let userMarker; | |
| let otherUsers = {}; | |
| let lastPosition = null; | |
| let lastUpdateTime = 0; | |
| const updateInterval = 1000; | |
| let isInitialLoad = true; | |
| function initSocket() { | |
| socket = io(); | |
| socket.on('connect', () => { | |
| console.log('Connected to server'); | |
| }); | |
| socket.on('disconnect', () => { | |
| console.log('Disconnected from server'); | |
| }); | |
| socket.on('users_update', (users) => { | |
| updateOtherUsers(users); | |
| }); | |
| socket.on('user_disconnected', (data) => { | |
| removeUser(data.username); | |
| }); | |
| } | |
| function createFootprint() { | |
| const footprint = document.createElement('div'); | |
| footprint.className = 'footprint'; | |
| const footLeft = document.createElement('div'); | |
| const footRight = document.createElement('div'); | |
| footLeft.className = 'foot left'; | |
| footRight.className = 'foot right'; | |
| footprint.appendChild(footLeft); | |
| footprint.appendChild(footRight); | |
| return footprint; | |
| } | |
| function getAngle(p0, p1) { | |
| if (!p0 || !p1) return 0; | |
| const deltaX = p1.lng - p0.lng; | |
| const deltaY = p1.lat - p0.lat; | |
| return Math.atan2(deltaY, deltaX); | |
| } | |
| function createUserIcon(name) { | |
| const icon = document.createElement('div'); | |
| icon.className = 'name-banner'; | |
| icon.textContent = name; | |
| return L.divIcon({ | |
| className: 'user-marker', | |
| html: icon.outerHTML, | |
| iconSize: [160, 40], | |
| iconAnchor: [80, 20] | |
| }); | |
| } | |
| function placeFootprint(position, angle) { | |
| const footprint = createFootprint(); | |
| const point = map.latLngToLayerPoint([position.lat, position.lng]); | |
| footprint.style.left = `${point.x}px`; | |
| footprint.style.top = `${point.y}px`; | |
| footprint.style.transform = `rotate(${angle + Math.PI/2}rad)`; | |
| document.querySelector('#track').appendChild(footprint); | |
| setTimeout(() => { | |
| footprint.remove(); | |
| }, 15000); | |
| } | |
| function updateUserPosition(position) { | |
| const now = Date.now(); | |
| if (now - lastUpdateTime < updateInterval) return; | |
| lastUpdateTime = now; | |
| const { latitude, longitude } = position.coords; | |
| const newPosition = { lat: latitude, lng: longitude }; | |
| if (!userMarker) { | |
| userMarker = L.marker([latitude, longitude], { | |
| icon: createUserIcon(username) | |
| }).addTo(map); | |
| map.setView([latitude, longitude], 16); | |
| } else { | |
| userMarker.setLatLng([latitude, longitude]); | |
| // 只在非初次加载且有上一个位置时创建脚印 | |
| if (!isInitialLoad && lastPosition) { | |
| const angle = getAngle(lastPosition, newPosition); | |
| placeFootprint(newPosition, angle); | |
| } | |
| } | |
| isInitialLoad = false; | |
| lastPosition = newPosition; | |
| socket.emit('update_location', { | |
| username: username, | |
| location: newPosition | |
| }); | |
| } | |
| function updateOtherUsers(users) { | |
| for (const [name, data] of Object.entries(users)) { | |
| if (name === username) continue; | |
| const newPos = { lat: data.location.lat, lng: data.location.lng }; | |
| if (!otherUsers[name]) { | |
| otherUsers[name] = { | |
| marker: L.marker([newPos.lat, newPos.lng], { | |
| icon: createUserIcon(name) | |
| }).addTo(map), | |
| lastPosition: null | |
| }; | |
| } else { | |
| const oldPos = otherUsers[name].lastPosition; | |
| otherUsers[name].marker.setLatLng([newPos.lat, newPos.lng]); | |
| if (oldPos) { | |
| const angle = getAngle(oldPos, newPos); | |
| placeFootprint(newPos, angle); | |
| } | |
| } | |
| otherUsers[name].lastPosition = newPos; | |
| } | |
| // 清理断开连接的用户 | |
| for (const name of Object.keys(otherUsers)) { | |
| if (!users[name]) { | |
| map.removeLayer(otherUsers[name].marker); | |
| delete otherUsers[name]; | |
| } | |
| } | |
| } | |
| function removeUser(username) { | |
| if (otherUsers[username]) { | |
| map.removeLayer(otherUsers[name].marker); | |
| delete otherUsers[username]; | |
| } | |
| } | |
| function updateUsername() { | |
| const newName = document.getElementById('username').value.trim(); | |
| if (newName) { | |
| const oldUsername = username; | |
| username = newName; | |
| if (userMarker) { | |
| userMarker.setIcon(createUserIcon(username)); | |
| } | |
| socket.emit('update_username', { | |
| old_username: oldUsername, | |
| new_username: username | |
| }); | |
| } | |
| } | |
| function initMap() { | |
| map = L.map('leaflet-map', { | |
| center: [39.9042, 116.4074], | |
| zoom: 16, | |
| zoomControl: false | |
| }); | |
| L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { | |
| maxZoom: 19 | |
| }).addTo(map); | |
| const mapMask = document.getElementById('mapMask'); | |
| mapMask.addEventListener('animationend', () => { | |
| initSocket(); | |
| if ("geolocation" in navigator) { | |
| navigator.geolocation.watchPosition( | |
| updateUserPosition, | |
| error => console.error("Error getting location:", error), | |
| { enableHighAccuracy: true } | |
| ); | |
| } else { | |
| alert("你的浏览器不支持地理定位功能"); | |
| } | |
| }); | |
| } | |
| initMap(); | |
| </script> | |
| </body> | |
| </html> |