mistpe commited on
Commit
9e00bb6
·
verified ·
1 Parent(s): 9238e07

Create templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +403 -0
templates/index.html ADDED
@@ -0,0 +1,403 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>The Marauder's Map</title>
7
+ <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
8
+ <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
9
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
10
+ <style>
11
+ html, body {
12
+ width: 100%;
13
+ height: 100%;
14
+ margin: 0;
15
+ background: #000;
16
+ }
17
+ .map-container {
18
+ width: 1440px;
19
+ height: 900px;
20
+ position: relative;
21
+ background-image: url('../static/map-bg.png');
22
+ background-size: cover;
23
+ background-position: center;
24
+ }
25
+ #leaflet-map {
26
+ width: 100%;
27
+ height: 100%;
28
+ position: absolute;
29
+ top: 0;
30
+ left: 0;
31
+ background: transparent;
32
+ }
33
+ .leaflet-tile-container img {
34
+ opacity: 0.4 !important;
35
+ filter: grayscale(100%) !important;
36
+ }
37
+ /* .map .mask {
38
+ width: 100%;
39
+ height: 100%;
40
+ background-image: url("../static/cloud.png");
41
+ mix-blend-mode: lighten;
42
+ animation: 4s linear 0s magic forwards;
43
+ position: absolute;
44
+ top: 0;
45
+ left: 0;
46
+ } */
47
+ .map .mask {
48
+ width: 100%;
49
+ height: 100%;
50
+ background-image: url("../static/cloud.png");
51
+ mix-blend-mode: lighten;
52
+ animation: 3s linear 0s magic forwards;
53
+ position: absolute;
54
+ }
55
+ .track {
56
+ position: absolute;
57
+ top: 0;
58
+ width: 1440px;
59
+ height: 900px;
60
+ mix-blend-mode: multiply;
61
+ opacity: .7;
62
+ pointer-events: none;
63
+ }
64
+ .footprint {
65
+ position: absolute;
66
+ pointer-events: none;
67
+ }
68
+ .footprint .foot {
69
+ position: absolute;
70
+ width: 10px;
71
+ height: 22px;
72
+ background-image: url('../static/footprints.png');
73
+ background-size: 40px;
74
+ background-repeat: no-repeat;
75
+ background-position-x: 10px;
76
+ animation: 1s linear 0s footsteps forwards;
77
+ }
78
+ .footprint .foot::after {
79
+ display: block;
80
+ content: '';
81
+ width: 100%;
82
+ height: 100%;
83
+ background-image: url('../static/footprints-cloud.png');
84
+ background-repeat: no-repeat;
85
+ background-size: 20px 22px;
86
+ mix-blend-mode: lighten;
87
+ animation: 8s linear 1s footHide forwards;
88
+ opacity: 0;
89
+ }
90
+ .footprint.last .foot::after {
91
+ animation-play-state: paused;
92
+ }
93
+ @keyframes footHide {
94
+ 0% {
95
+ opacity: 0;
96
+ }
97
+ 25% {
98
+ opacity: 1;
99
+ filter: brightness(1);
100
+ }
101
+ 100% {
102
+ opacity: 1;
103
+ filter: brightness(10);
104
+ }
105
+ }
106
+ .footprint .foot.right::after {
107
+ background-position-x: -10px;
108
+ }
109
+ @keyframes footsteps {
110
+ 0% {
111
+ background-position-x: 0px;
112
+ }
113
+ 25% {
114
+ background-position-x: 0px;
115
+ }
116
+ 25.1% {
117
+ background-position-x: -10px;
118
+ }
119
+ 50% {
120
+ background-position-x: -10px;
121
+ }
122
+ 50.1% {
123
+ background-position-x: -20px;
124
+ }
125
+ 75% {
126
+ background-position-x: -20px;
127
+ }
128
+ 75.1% {
129
+ background-position-x: -30px;
130
+ }
131
+ 100% {
132
+ background-position-x: -30px;
133
+ }
134
+ }
135
+ .footprint .foot.left {
136
+ left: -5px;
137
+ top: 7px;
138
+ }
139
+ .footprint .foot.right {
140
+ left: 5px;
141
+ top: -7px;
142
+ background-position-y: -22px;
143
+ animation-delay: 1s;
144
+ }
145
+ @keyframes magic {
146
+ 0% {
147
+ filter: brightness(10);
148
+ opacity: 1;
149
+ }
150
+ 70% {
151
+ filter: brightness(8);
152
+ opacity: 1;
153
+ }
154
+ 100% {
155
+ filter: brightness(0);
156
+ opacity: 0;
157
+ }
158
+ }
159
+ .name-banner {
160
+ position: absolute;
161
+ width: 160px;
162
+ height: 40px;
163
+ background-image: url('../static/banner.svg');
164
+ background-repeat: no-repeat;
165
+ background-size: 160px 40px;
166
+ text-align: center;
167
+ line-height: 30px;
168
+ font-family: 'Apple Chancery', cursive;
169
+ z-index: 1000;
170
+ animation: 1s linear 0s nameShow forwards;
171
+ color: #3c2a1e;
172
+ text-shadow: 0 0 2px rgba(255, 255, 255, 0.5);
173
+ }
174
+ @keyframes nameShow {
175
+ from {
176
+ opacity: 0;
177
+ }
178
+ to {
179
+ opacity: 1;
180
+ }
181
+ }
182
+ .user-controls {
183
+ position: fixed;
184
+ top: 20px;
185
+ right: 20px;
186
+ z-index: 1000;
187
+ background: rgba(222, 184, 135, 0.9);
188
+ padding: 15px;
189
+ border-radius: 8px;
190
+ border: 2px solid #8b4513;
191
+ }
192
+ .user-controls input {
193
+ padding: 8px 12px;
194
+ margin-right: 10px;
195
+ border: 1px solid #8b4513;
196
+ border-radius: 4px;
197
+ font-family: 'Apple Chancery', cursive;
198
+ background: rgba(255, 255, 255, 0.9);
199
+ }
200
+ .user-controls button {
201
+ padding: 8px 16px;
202
+ background: #8b4513;
203
+ color: #fff;
204
+ border: none;
205
+ border-radius: 4px;
206
+ cursor: pointer;
207
+ font-family: 'Apple Chancery', cursive;
208
+ transition: background-color 0.3s;
209
+ }
210
+ .user-controls button:hover {
211
+ background: #654321;
212
+ }
213
+ .leaflet-control-attribution {
214
+ display: none;
215
+ }
216
+ </style>
217
+ </head>
218
+ <body>
219
+ <div class="map-container">
220
+ <div class="map">
221
+ <div id="mapMask" class="mask"></div>
222
+ </div>
223
+ <div id="leaflet-map"></div>
224
+ <div id="track" class="track"></div>
225
+ <div class="user-controls">
226
+ <input type="text" id="username" placeholder="输入你的名字" maxlength="20">
227
+ <button onclick="updateUsername()">更新名字</button>
228
+ </div>
229
+ </div>
230
+
231
+ <script>
232
+ let map;
233
+ let socket;
234
+ let username = '陌生访客_' + Math.floor(Math.random() * 1000);
235
+ let userMarker;
236
+ let otherUsers = {};
237
+ let lastPosition = null;
238
+ let lastUpdateTime = 0;
239
+ const updateInterval = 1000;
240
+ let isInitialLoad = true;
241
+ function initSocket() {
242
+ socket = io();
243
+ socket.on('connect', () => {
244
+ console.log('Connected to server');
245
+ });
246
+ socket.on('disconnect', () => {
247
+ console.log('Disconnected from server');
248
+ });
249
+ socket.on('users_update', (users) => {
250
+ updateOtherUsers(users);
251
+ });
252
+ socket.on('user_disconnected', (data) => {
253
+ removeUser(data.username);
254
+ });
255
+ }
256
+ function createFootprint() {
257
+ const footprint = document.createElement('div');
258
+ footprint.className = 'footprint';
259
+ const footLeft = document.createElement('div');
260
+ const footRight = document.createElement('div');
261
+ footLeft.className = 'foot left';
262
+ footRight.className = 'foot right';
263
+ footprint.appendChild(footLeft);
264
+ footprint.appendChild(footRight);
265
+ return footprint;
266
+ }
267
+ function getAngle(p0, p1) {
268
+ if (!p0 || !p1) return 0;
269
+ const deltaX = p1.lng - p0.lng;
270
+ const deltaY = p1.lat - p0.lat;
271
+ return Math.atan2(deltaY, deltaX);
272
+ }
273
+ function createUserIcon(name) {
274
+ const icon = document.createElement('div');
275
+ icon.className = 'name-banner';
276
+ icon.textContent = name;
277
+
278
+ return L.divIcon({
279
+ className: 'user-marker',
280
+ html: icon.outerHTML,
281
+ iconSize: [160, 40],
282
+ iconAnchor: [80, 20]
283
+ });
284
+ }
285
+ function placeFootprint(position, angle) {
286
+ const footprint = createFootprint();
287
+ const point = map.latLngToLayerPoint([position.lat, position.lng]);
288
+
289
+ footprint.style.left = `${point.x}px`;
290
+ footprint.style.top = `${point.y}px`;
291
+ footprint.style.transform = `rotate(${angle + Math.PI/2}rad)`;
292
+ document.querySelector('#track').appendChild(footprint);
293
+ setTimeout(() => {
294
+ footprint.remove();
295
+ }, 15000);
296
+ }
297
+ function updateUserPosition(position) {
298
+ const now = Date.now();
299
+ if (now - lastUpdateTime < updateInterval) return;
300
+ lastUpdateTime = now;
301
+ const { latitude, longitude } = position.coords;
302
+ const newPosition = { lat: latitude, lng: longitude };
303
+
304
+ if (!userMarker) {
305
+ userMarker = L.marker([latitude, longitude], {
306
+ icon: createUserIcon(username)
307
+ }).addTo(map);
308
+ map.setView([latitude, longitude], 16);
309
+ } else {
310
+ userMarker.setLatLng([latitude, longitude]);
311
+ // 只在非初次加载且有上一个位置时创建脚印
312
+ if (!isInitialLoad && lastPosition) {
313
+ const angle = getAngle(lastPosition, newPosition);
314
+ placeFootprint(newPosition, angle);
315
+ }
316
+ }
317
+ isInitialLoad = false;
318
+ lastPosition = newPosition;
319
+ socket.emit('update_location', {
320
+ username: username,
321
+ location: newPosition
322
+ });
323
+ }
324
+ function updateOtherUsers(users) {
325
+ for (const [name, data] of Object.entries(users)) {
326
+ if (name === username) continue;
327
+ const newPos = { lat: data.location.lat, lng: data.location.lng };
328
+
329
+ if (!otherUsers[name]) {
330
+ otherUsers[name] = {
331
+ marker: L.marker([newPos.lat, newPos.lng], {
332
+ icon: createUserIcon(name)
333
+ }).addTo(map),
334
+ lastPosition: null
335
+ };
336
+ } else {
337
+ const oldPos = otherUsers[name].lastPosition;
338
+ otherUsers[name].marker.setLatLng([newPos.lat, newPos.lng]);
339
+
340
+ if (oldPos) {
341
+ const angle = getAngle(oldPos, newPos);
342
+ placeFootprint(newPos, angle);
343
+ }
344
+ }
345
+
346
+ otherUsers[name].lastPosition = newPos;
347
+ }
348
+ // 清理断开连接的用户
349
+ for (const name of Object.keys(otherUsers)) {
350
+ if (!users[name]) {
351
+ map.removeLayer(otherUsers[name].marker);
352
+ delete otherUsers[name];
353
+ }
354
+ }
355
+ }
356
+ function removeUser(username) {
357
+ if (otherUsers[username]) {
358
+ map.removeLayer(otherUsers[name].marker);
359
+ delete otherUsers[username];
360
+ }
361
+ }
362
+ function updateUsername() {
363
+ const newName = document.getElementById('username').value.trim();
364
+ if (newName) {
365
+ const oldUsername = username;
366
+ username = newName;
367
+ if (userMarker) {
368
+ userMarker.setIcon(createUserIcon(username));
369
+ }
370
+ socket.emit('update_username', {
371
+ old_username: oldUsername,
372
+ new_username: username
373
+ });
374
+ }
375
+ }
376
+ function initMap() {
377
+ map = L.map('leaflet-map', {
378
+ center: [39.9042, 116.4074],
379
+ zoom: 16,
380
+ zoomControl: false
381
+ });
382
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
383
+ maxZoom: 19
384
+ }).addTo(map);
385
+ const mapMask = document.getElementById('mapMask');
386
+ mapMask.addEventListener('animationend', () => {
387
+ initSocket();
388
+
389
+ if ("geolocation" in navigator) {
390
+ navigator.geolocation.watchPosition(
391
+ updateUserPosition,
392
+ error => console.error("Error getting location:", error),
393
+ { enableHighAccuracy: true }
394
+ );
395
+ } else {
396
+ alert("你的浏览器不支持地理定位功能");
397
+ }
398
+ });
399
+ }
400
+ initMap();
401
+ </script>
402
+ </body>
403
+ </html>