MarauderMap / templates /index.html
mistpe's picture
Create templates/index.html
9e00bb6 verified
<!DOCTYPE html>
<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 !important;
filter: grayscale(100%) !important;
}
/* .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>