dsh-agent-office 4.6.0 → 4.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/lib/client.js +64 -2
- package/lib/index.js +19 -1
- package/lib/panel.html +239 -191
- package/lib/state.js +36 -0
- package/package.json +1 -1
- package/panel-src/index.html +50 -2
- package/panel-src/main.js +573 -71
package/panel-src/main.js
CHANGED
|
@@ -287,6 +287,12 @@ function applyThemeFrame() {
|
|
|
287
287
|
const neonMaterial = neonSign?.material
|
|
288
288
|
if (neonMaterial) neonMaterial.opacity = NIGHT_THEME.neon + (DAY_THEME.neon - NIGHT_THEME.neon) * mix
|
|
289
289
|
for (const item of [...themeLights, ...annexLights]) item.light.intensity = item.night + (item.day - item.night) * mix
|
|
290
|
+
const weatherDim = weatherMode === 'storm' ? 0.86 : weatherMode === 'rain' || weatherMode === 'snow' ? 0.92 : weatherMode === 'cloudy' ? 0.95 : weatherMode === 'wind' ? 0.98 : 1
|
|
291
|
+
hemi.intensity *= weatherDim
|
|
292
|
+
key.intensity *= weatherDim
|
|
293
|
+
rim.intensity *= weatherDim
|
|
294
|
+
fill.intensity *= weatherDim
|
|
295
|
+
scene.fog.density = 0.0085 * (weatherMode === 'rain' || weatherMode === 'snow' || weatherMode === 'storm' ? 1.06 : 1)
|
|
290
296
|
}
|
|
291
297
|
|
|
292
298
|
function syncThemeByClock(immediate = false) {
|
|
@@ -306,17 +312,17 @@ const neonSign = (() => {
|
|
|
306
312
|
canvas.height = 192
|
|
307
313
|
const ctx = canvas.getContext('2d')
|
|
308
314
|
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
|
309
|
-
ctx.font = '900
|
|
315
|
+
ctx.font = '900 72px Inter, "PingFang SC", system-ui, sans-serif'
|
|
310
316
|
ctx.textAlign = 'center'
|
|
311
317
|
ctx.textBaseline = 'middle'
|
|
312
|
-
ctx.shadowColor = 'rgba(
|
|
313
|
-
ctx.shadowBlur =
|
|
314
|
-
ctx.fillStyle = '#
|
|
315
|
-
ctx.fillText('
|
|
318
|
+
ctx.shadowColor = 'rgba(77,107,254,.95)'
|
|
319
|
+
ctx.shadowBlur = 40
|
|
320
|
+
ctx.fillStyle = '#eaf1ff'
|
|
321
|
+
ctx.fillText('DEEPSEEK HARNESS', 512, 74)
|
|
316
322
|
ctx.shadowBlur = 0
|
|
317
|
-
ctx.font = '500
|
|
318
|
-
ctx.fillStyle = 'rgba(190,
|
|
319
|
-
ctx.fillText('
|
|
323
|
+
ctx.font = '500 26px ui-monospace, Menlo, monospace'
|
|
324
|
+
ctx.fillStyle = 'rgba(160,190,255,.9)'
|
|
325
|
+
ctx.fillText('LIVE SESSION OFFICE', 512, 146)
|
|
320
326
|
const texture = new THREE.CanvasTexture(canvas)
|
|
321
327
|
texture.colorSpace = THREE.SRGBColorSpace
|
|
322
328
|
const sprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: texture, transparent: true, depthWrite: false }))
|
|
@@ -445,6 +451,39 @@ const scribbleTexture = (() => {
|
|
|
445
451
|
})()
|
|
446
452
|
const OVERLOAD_COLOR = new THREE.Color(0xff5a5a)
|
|
447
453
|
|
|
454
|
+
const keyboardTexture = (() => {
|
|
455
|
+
const canvas = document.createElement('canvas')
|
|
456
|
+
canvas.width = 256
|
|
457
|
+
canvas.height = 96
|
|
458
|
+
const ctx = canvas.getContext('2d')
|
|
459
|
+
ctx.fillStyle = '#0a1017'
|
|
460
|
+
ctx.fillRect(0, 0, 256, 96)
|
|
461
|
+
const cols = 14
|
|
462
|
+
const rows = 4
|
|
463
|
+
const keyW = 14
|
|
464
|
+
const keyH = 14
|
|
465
|
+
const gap = 2
|
|
466
|
+
const startX = (256 - (cols * (keyW + gap) - gap)) / 2
|
|
467
|
+
for (let r = 0; r < rows; r++) {
|
|
468
|
+
for (let c = 0; c < cols; c++) {
|
|
469
|
+
const x = startX + c * (keyW + gap)
|
|
470
|
+
const y = 10 + r * (keyH + gap)
|
|
471
|
+
ctx.fillStyle = (r === 2 && c > 4 && c < 9) ? '#24445a' : '#1c2a36'
|
|
472
|
+
ctx.beginPath(); ctx.roundRect(x, y, keyW, keyH, 2); ctx.fill()
|
|
473
|
+
ctx.fillStyle = 'rgba(150,220,255,.22)'
|
|
474
|
+
ctx.fillRect(x + 2, y + 2, keyW - 4, 2)
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
// spacebar + underglow
|
|
478
|
+
ctx.fillStyle = '#1c2a36'
|
|
479
|
+
ctx.beginPath(); ctx.roundRect(62, 74, 132, 13, 3); ctx.fill()
|
|
480
|
+
ctx.fillStyle = 'rgba(80,220,255,.55)'
|
|
481
|
+
ctx.fillRect(4, 91, 248, 2)
|
|
482
|
+
const texture = new THREE.CanvasTexture(canvas)
|
|
483
|
+
texture.colorSpace = THREE.SRGBColorSpace
|
|
484
|
+
return texture
|
|
485
|
+
})()
|
|
486
|
+
|
|
448
487
|
const floorTexture = (() => {
|
|
449
488
|
const canvas = document.createElement('canvas')
|
|
450
489
|
canvas.width = canvas.height = 256
|
|
@@ -464,6 +503,197 @@ const floorTexture = (() => {
|
|
|
464
503
|
return texture
|
|
465
504
|
})()
|
|
466
505
|
|
|
506
|
+
/* ------------------------------------------------------------------ *
|
|
507
|
+
* 天气:云层 / 雨 / 雪 / 风
|
|
508
|
+
* ------------------------------------------------------------------ */
|
|
509
|
+
const weatherGroup = new THREE.Group()
|
|
510
|
+
scene.add(weatherGroup)
|
|
511
|
+
const weatherCloudTexture = (() => {
|
|
512
|
+
const canvas = document.createElement('canvas')
|
|
513
|
+
canvas.width = 512
|
|
514
|
+
canvas.height = 192
|
|
515
|
+
const ctx = canvas.getContext('2d')
|
|
516
|
+
ctx.clearRect(0, 0, 512, 192)
|
|
517
|
+
for (const [x, y, rx, ry, alpha] of [[120, 108, 160, 26, .16], [260, 84, 210, 22, .14], [400, 112, 170, 20, .13], [190, 132, 130, 14, .11], [340, 64, 120, 16, .1]]) {
|
|
518
|
+
const grad = ctx.createRadialGradient(x, y, 0, x, y, rx)
|
|
519
|
+
grad.addColorStop(0, `rgba(230,242,250,${alpha})`)
|
|
520
|
+
grad.addColorStop(.5, `rgba(196,214,228,${alpha * .55})`)
|
|
521
|
+
grad.addColorStop(1, 'rgba(170,195,214,0)')
|
|
522
|
+
ctx.fillStyle = grad
|
|
523
|
+
ctx.beginPath(); ctx.ellipse(x, y, rx, ry, 0, 0, Math.PI * 2); ctx.fill()
|
|
524
|
+
}
|
|
525
|
+
const texture = new THREE.CanvasTexture(canvas)
|
|
526
|
+
texture.colorSpace = THREE.SRGBColorSpace
|
|
527
|
+
return texture
|
|
528
|
+
})()
|
|
529
|
+
const weatherClouds = []
|
|
530
|
+
for (let i = 0; i < 7; i++) {
|
|
531
|
+
const material = new THREE.SpriteMaterial({ map: weatherCloudTexture, color: 0x8d9dab, transparent: true, opacity: 0, depthWrite: false, toneMapped: false })
|
|
532
|
+
const cloud = new THREE.Sprite(material)
|
|
533
|
+
cloud.scale.set(17 + (i % 3) * 3, 2.8 + (i % 2) * .8, 1)
|
|
534
|
+
cloud.position.set(-16 + (i * 5.1) % 32, 6.2 + (i % 4) * .9, 4 + (i * 3.1) % 13)
|
|
535
|
+
cloud.userData = { baseX: cloud.position.x, baseY: cloud.position.y, baseZ: cloud.position.z, phase: i * 0.9 }
|
|
536
|
+
weatherGroup.add(cloud)
|
|
537
|
+
weatherClouds.push(cloud)
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const WEATHER_COUNT = 560
|
|
541
|
+
const weatherPositions = new Float32Array(WEATHER_COUNT * 3)
|
|
542
|
+
const weatherGeometry = new THREE.BufferGeometry()
|
|
543
|
+
weatherGeometry.setAttribute('position', new THREE.BufferAttribute(weatherPositions, 3))
|
|
544
|
+
const weatherPointTexture = (() => {
|
|
545
|
+
const canvas = document.createElement('canvas')
|
|
546
|
+
canvas.width = canvas.height = 64
|
|
547
|
+
const ctx = canvas.getContext('2d')
|
|
548
|
+
const grad = ctx.createRadialGradient(32, 32, 0, 32, 32, 32)
|
|
549
|
+
grad.addColorStop(0, 'rgba(255,255,255,1)')
|
|
550
|
+
grad.addColorStop(0.5, 'rgba(235,250,255,.8)')
|
|
551
|
+
grad.addColorStop(1, 'rgba(235,250,255,0)')
|
|
552
|
+
ctx.fillStyle = grad
|
|
553
|
+
ctx.fillRect(0, 0, 64, 64)
|
|
554
|
+
const texture = new THREE.CanvasTexture(canvas)
|
|
555
|
+
texture.colorSpace = THREE.SRGBColorSpace
|
|
556
|
+
return texture
|
|
557
|
+
})()
|
|
558
|
+
const weatherMaterial = new THREE.PointsMaterial({ map: weatherPointTexture, alphaTest: 0.02, color: 0x9fd4ff, size: 0.055, transparent: true, opacity: 0, depthWrite: false, toneMapped: false, sizeAttenuation: true })
|
|
559
|
+
const weatherPoints = new THREE.Points(weatherGeometry, weatherMaterial)
|
|
560
|
+
weatherPoints.frustumCulled = false
|
|
561
|
+
weatherPoints.visible = false
|
|
562
|
+
weatherGroup.add(weatherPoints)
|
|
563
|
+
const rainPositions = new Float32Array(WEATHER_COUNT * 6)
|
|
564
|
+
const rainGeometry = new THREE.BufferGeometry()
|
|
565
|
+
rainGeometry.setAttribute('position', new THREE.BufferAttribute(rainPositions, 3))
|
|
566
|
+
const rainMaterial = new THREE.LineBasicMaterial({ color: 0x9fd4ff, transparent: true, opacity: 0, depthWrite: false, toneMapped: false })
|
|
567
|
+
const rainLines = new THREE.LineSegments(rainGeometry, rainMaterial)
|
|
568
|
+
rainLines.frustumCulled = false
|
|
569
|
+
rainLines.visible = false
|
|
570
|
+
weatherGroup.add(rainLines)
|
|
571
|
+
const RAIN_COUNT = 80
|
|
572
|
+
rainGeometry.setDrawRange(0, RAIN_COUNT * 2)
|
|
573
|
+
const weatherParticles = []
|
|
574
|
+
let weatherMode = 'clear'
|
|
575
|
+
let weatherWind = 0
|
|
576
|
+
let weatherCloudLevel = 0
|
|
577
|
+
let weatherPointLevel = 0
|
|
578
|
+
|
|
579
|
+
function seedWeatherParticles(mode) {
|
|
580
|
+
const isWind = mode === 'wind'
|
|
581
|
+
const isSnow = mode === 'snow'
|
|
582
|
+
for (let i = 0; i < WEATHER_COUNT; i++) {
|
|
583
|
+
const x = (Math.random() - 0.5) * (isWind ? 34 : 26)
|
|
584
|
+
const y = isWind ? 1.5 + Math.random() * 7 : Math.random() * 18
|
|
585
|
+
const z = (Math.random() - 0.5) * (isWind ? 22 : 24)
|
|
586
|
+
weatherParticles[i] = { x, y, z, speed: isSnow ? 0.035 + Math.random() * 0.035 : 0.16 + Math.random() * 0.2, drift: Math.random() * 2 - 1 }
|
|
587
|
+
weatherPositions[i * 3] = x
|
|
588
|
+
weatherPositions[i * 3 + 1] = y
|
|
589
|
+
weatherPositions[i * 3 + 2] = z
|
|
590
|
+
}
|
|
591
|
+
weatherGeometry.attributes.position.needsUpdate = true
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function setWeather(mode, wind = 0) {
|
|
595
|
+
weatherMode = ['clear', 'cloudy', 'rain', 'snow', 'wind', 'storm'].includes(mode) ? mode : 'clear'
|
|
596
|
+
weatherWind = Number.isFinite(wind) ? wind : 0
|
|
597
|
+
const wet = weatherMode === 'rain' || weatherMode === 'storm'
|
|
598
|
+
const snow = weatherMode === 'snow'
|
|
599
|
+
const windy = weatherMode === 'wind'
|
|
600
|
+
weatherPoints.visible = snow || windy
|
|
601
|
+
rainLines.visible = wet
|
|
602
|
+
weatherMaterial.color.set(snow ? 0xe6f5ff : 0xcfe8ff)
|
|
603
|
+
weatherMaterial.size = snow ? 0.17 : 0.06
|
|
604
|
+
if (wet) rainGeometry.setDrawRange(0, RAIN_COUNT * 2)
|
|
605
|
+
if (wet || snow || windy) seedWeatherParticles(weatherMode)
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function refreshWeather() {
|
|
609
|
+
let location = null
|
|
610
|
+
try {
|
|
611
|
+
const raw = localStorage.getItem('agent-office-weather-location')
|
|
612
|
+
if (raw) {
|
|
613
|
+
const parsed = JSON.parse(raw)
|
|
614
|
+
if (Number.isFinite(parsed?.lat) && Number.isFinite(parsed?.lon)) location = parsed
|
|
615
|
+
}
|
|
616
|
+
} catch {}
|
|
617
|
+
if (!location) {
|
|
618
|
+
setWeather('clear')
|
|
619
|
+
return
|
|
620
|
+
}
|
|
621
|
+
fetch(`/agent-office/api/weather?lat=${encodeURIComponent(location.lat)}&lon=${encodeURIComponent(location.lon)}`, { cache: 'no-store' })
|
|
622
|
+
.then(response => response.ok ? response.json() : null)
|
|
623
|
+
.then(data => {
|
|
624
|
+
if (!data?.ok) {
|
|
625
|
+
setWeather('clear')
|
|
626
|
+
return
|
|
627
|
+
}
|
|
628
|
+
const code = Number(data.code)
|
|
629
|
+
let mode = 'clear'
|
|
630
|
+
if (code >= 95) mode = 'storm'
|
|
631
|
+
else if (code >= 71 && code <= 86) mode = 'snow'
|
|
632
|
+
else if ((code >= 51 && code <= 67) || (code >= 80 && code <= 82)) mode = 'rain'
|
|
633
|
+
else if (code === 2 || code === 3 || code === 45 || code === 48) mode = 'cloudy'
|
|
634
|
+
else if (Number(data.wind) >= 28) mode = 'wind'
|
|
635
|
+
setWeather(mode, Number(data.wind))
|
|
636
|
+
})
|
|
637
|
+
.catch(() => setWeather('clear'))
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function updateWeatherFrame(t) {
|
|
641
|
+
const cloudy = weatherMode === 'cloudy' || weatherMode === 'wind'
|
|
642
|
+
weatherCloudLevel += ((cloudy ? 1 : 0) - weatherCloudLevel) * 0.025
|
|
643
|
+
for (const cloud of weatherClouds) {
|
|
644
|
+
cloud.material.opacity = weatherCloudLevel * 0.22
|
|
645
|
+
cloud.position.x = cloud.userData.baseX + Math.sin(t * 0.035 + cloud.userData.phase) * 2.2
|
|
646
|
+
cloud.position.z = cloud.userData.baseZ + Math.cos(t * 0.03 + cloud.userData.phase) * 1.2
|
|
647
|
+
}
|
|
648
|
+
const active = weatherPoints.visible
|
|
649
|
+
const wet = weatherMode === 'rain' || weatherMode === 'storm'
|
|
650
|
+
weatherPointLevel += (((active || wet) ? 1 : 0) - weatherPointLevel) * 0.06
|
|
651
|
+
weatherMaterial.opacity = weatherPointLevel * (weatherMode === 'snow' ? 0.6 : 0.5)
|
|
652
|
+
rainMaterial.opacity = weatherPointLevel * 0.3
|
|
653
|
+
if (wet) {
|
|
654
|
+
for (let i = 0; i < RAIN_COUNT; i++) {
|
|
655
|
+
const p = weatherParticles[i]
|
|
656
|
+
if (!p) continue
|
|
657
|
+
p.y -= p.speed * 0.95
|
|
658
|
+
p.x += weatherWind * 0.0015 + p.drift * 0.0012
|
|
659
|
+
if (p.y < 0) { p.y = 17; p.x = (Math.random() - 0.5) * 26; p.z = (Math.random() - 0.5) * 24 }
|
|
660
|
+
rainPositions[i * 6] = p.x
|
|
661
|
+
rainPositions[i * 6 + 1] = p.y
|
|
662
|
+
rainPositions[i * 6 + 2] = p.z
|
|
663
|
+
rainPositions[i * 6 + 3] = p.x + weatherWind * 0.0025
|
|
664
|
+
rainPositions[i * 6 + 4] = p.y - 0.22
|
|
665
|
+
rainPositions[i * 6 + 5] = p.z
|
|
666
|
+
}
|
|
667
|
+
rainGeometry.attributes.position.needsUpdate = true
|
|
668
|
+
} else if (active) {
|
|
669
|
+
for (let i = 0; i < WEATHER_COUNT; i++) {
|
|
670
|
+
const p = weatherParticles[i]
|
|
671
|
+
if (!p) continue
|
|
672
|
+
if (weatherMode === 'wind') {
|
|
673
|
+
p.x += 0.11 + Math.abs(weatherWind) * 0.002
|
|
674
|
+
p.y += Math.sin(t * 1.3 + p.drift) * 0.004
|
|
675
|
+
if (p.x > 18) p.x = -18
|
|
676
|
+
} else if (weatherMode === 'snow') {
|
|
677
|
+
p.y -= p.speed * 0.55
|
|
678
|
+
p.x += Math.sin(t * 1.4 + p.drift * 5) * 0.006
|
|
679
|
+
if (p.y < 0) { p.y = 16; p.x = (Math.random() - 0.5) * 26 }
|
|
680
|
+
} else {
|
|
681
|
+
p.y -= p.speed
|
|
682
|
+
p.x += (weatherWind * 0.0015 + p.drift * 0.001)
|
|
683
|
+
if (p.y < 0) { p.y = 17; p.x = (Math.random() - 0.5) * 26; p.z = (Math.random() - 0.5) * 24 }
|
|
684
|
+
}
|
|
685
|
+
weatherPositions[i * 3] = p.x
|
|
686
|
+
weatherPositions[i * 3 + 1] = p.y
|
|
687
|
+
weatherPositions[i * 3 + 2] = p.z
|
|
688
|
+
}
|
|
689
|
+
weatherGeometry.attributes.position.needsUpdate = true
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
window.addEventListener('storage', event => {
|
|
694
|
+
if (event.key === 'agent-office-weather-location') refreshWeather()
|
|
695
|
+
})
|
|
696
|
+
|
|
467
697
|
function disposeGroup(group) {
|
|
468
698
|
group.traverse(object => {
|
|
469
699
|
// CSS2D 标签是独立 DOM,scene 移除后渲染器不会回收,必须手动删掉,否则重建会累积重影
|
|
@@ -475,7 +705,7 @@ function disposeGroup(group) {
|
|
|
475
705
|
if (object.material) {
|
|
476
706
|
const materials = Array.isArray(object.material) ? object.material : [object.material]
|
|
477
707
|
for (const material of materials) {
|
|
478
|
-
if (material.map && material.map !== glowTexture && material.map !== shadowTexture && material.map !== floorTexture && material.map !== sweatTexture && material.map !== scribbleTexture) material.map.dispose()
|
|
708
|
+
if (material.map && material.map !== glowTexture && material.map !== shadowTexture && material.map !== floorTexture && material.map !== sweatTexture && material.map !== scribbleTexture && material.map !== keyboardTexture) material.map.dispose()
|
|
479
709
|
material.dispose()
|
|
480
710
|
}
|
|
481
711
|
}
|
|
@@ -837,16 +1067,65 @@ function workstation(agent, spot) {
|
|
|
837
1067
|
group.add(footrest)
|
|
838
1068
|
}
|
|
839
1069
|
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
1070
|
+
// 键盘:底座 + 键帽贴图 + 底部氛围灯
|
|
1071
|
+
const keyboardBase = new THREE.Mesh(new THREE.BoxGeometry(0.52, 0.03, 0.18), new THREE.MeshStandardMaterial({ color: 0x111923, roughness: 0.45, metalness: 0.4 }))
|
|
1072
|
+
keyboardBase.position.set(spot.x + 0.24, 0.575, spot.z + 0.8)
|
|
1073
|
+
group.add(keyboardBase)
|
|
1074
|
+
const keyboardTop = new THREE.Mesh(
|
|
1075
|
+
new THREE.PlaneGeometry(0.49, 0.16),
|
|
1076
|
+
new THREE.MeshBasicMaterial({ map: keyboardTexture, toneMapped: false }),
|
|
1077
|
+
)
|
|
1078
|
+
keyboardTop.rotation.x = -Math.PI / 2
|
|
1079
|
+
keyboardTop.position.set(spot.x + 0.24, 0.592, spot.z + 0.8)
|
|
1080
|
+
group.add(keyboardTop)
|
|
1081
|
+
const keyboardGlow = new THREE.Mesh(
|
|
1082
|
+
new THREE.BoxGeometry(0.48, 0.006, 0.012),
|
|
1083
|
+
new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.8, toneMapped: false }),
|
|
1084
|
+
)
|
|
1085
|
+
keyboardGlow.position.set(spot.x + 0.24, 0.58, spot.z + 0.9)
|
|
1086
|
+
group.add(keyboardGlow)
|
|
1087
|
+
|
|
1088
|
+
// 鼠标垫 + 鼠标 + 滚轮
|
|
1089
|
+
const mousePad = new THREE.Mesh(new THREE.BoxGeometry(0.26, 0.008, 0.22), new THREE.MeshStandardMaterial({ color: 0x101923, roughness: 0.92, metalness: 0.05 }))
|
|
1090
|
+
mousePad.position.set(spot.x + 0.63, 0.588, spot.z + 0.83)
|
|
1091
|
+
group.add(mousePad)
|
|
1092
|
+
const mouse = new THREE.Mesh(
|
|
1093
|
+
new RoundedBoxGeometry(0.09, 0.04, 0.13, 3, 0.018),
|
|
1094
|
+
new THREE.MeshStandardMaterial({ color: 0x1c2835, roughness: 0.5, metalness: 0.25 }),
|
|
1095
|
+
)
|
|
1096
|
+
mouse.position.set(spot.x + 0.63, 0.61, spot.z + 0.82)
|
|
1097
|
+
mouse.rotation.y = -0.2
|
|
1098
|
+
group.add(mouse)
|
|
1099
|
+
const wheel = new THREE.Mesh(new THREE.BoxGeometry(0.018, 0.018, 0.035), new THREE.MeshBasicMaterial({ color }))
|
|
1100
|
+
wheel.position.set(spot.x + 0.63, 0.64, spot.z + 0.79)
|
|
1101
|
+
group.add(wheel)
|
|
1102
|
+
|
|
1103
|
+
// 键盘 / 鼠标线缆
|
|
1104
|
+
const cableMat = new THREE.MeshStandardMaterial({ color: 0x607385, roughness: 0.38, metalness: 0.3 })
|
|
1105
|
+
const keyboardCable = new THREE.Mesh(
|
|
1106
|
+
new THREE.TubeGeometry(new THREE.CatmullRomCurve3([
|
|
1107
|
+
new THREE.Vector3(spot.x + 0.24, 0.592, spot.z + 0.71),
|
|
1108
|
+
new THREE.Vector3(spot.x + 0.18, 0.592, spot.z + 0.62),
|
|
1109
|
+
new THREE.Vector3(spot.x - 0.06, 0.592, spot.z + 0.54),
|
|
1110
|
+
new THREE.Vector3(spot.x - 0.34, 0.592, spot.z + 0.48),
|
|
1111
|
+
new THREE.Vector3(spot.x - 0.48, 0.6, spot.z + 0.42),
|
|
1112
|
+
], false, 'centripetal'), 18, 0.008, 5, false),
|
|
1113
|
+
cableMat,
|
|
1114
|
+
)
|
|
1115
|
+
group.add(keyboardCable)
|
|
1116
|
+
const mouseCable = new THREE.Mesh(
|
|
1117
|
+
new THREE.TubeGeometry(new THREE.CatmullRomCurve3([
|
|
1118
|
+
new THREE.Vector3(spot.x + 0.63, 0.6, spot.z + 0.76),
|
|
1119
|
+
new THREE.Vector3(spot.x + 0.64, 0.592, spot.z + 0.62),
|
|
1120
|
+
new THREE.Vector3(spot.x + 0.34, 0.592, spot.z + 0.54),
|
|
1121
|
+
new THREE.Vector3(spot.x - 0.08, 0.592, spot.z + 0.48),
|
|
1122
|
+
new THREE.Vector3(spot.x - 0.46, 0.6, spot.z + 0.42),
|
|
1123
|
+
], false, 'centripetal'), 18, 0.007, 5, false),
|
|
1124
|
+
cableMat,
|
|
1125
|
+
)
|
|
1126
|
+
group.add(mouseCable)
|
|
1127
|
+
|
|
843
1128
|
|
|
844
|
-
const plate = new THREE.Mesh(new THREE.BoxGeometry(0.34, 0.16, 0.02), new THREE.MeshBasicMaterial({ color: 0x0e151d }))
|
|
845
|
-
plate.position.set(spot.x + 0.6, 0.63, spot.z + 0.86)
|
|
846
|
-
group.add(plate)
|
|
847
|
-
const plateDot = new THREE.Mesh(new THREE.CircleGeometry(0.035, 12), new THREE.MeshBasicMaterial({ color }))
|
|
848
|
-
plateDot.position.set(spot.x + 0.55, 0.63, spot.z + 0.872)
|
|
849
|
-
group.add(plateDot)
|
|
850
1129
|
|
|
851
1130
|
return group
|
|
852
1131
|
}
|
|
@@ -1347,6 +1626,8 @@ let targetGoal = null
|
|
|
1347
1626
|
let zoomGoal = null
|
|
1348
1627
|
let tower = null
|
|
1349
1628
|
let loading = true
|
|
1629
|
+
let introPlayed = false
|
|
1630
|
+
let introTl = null
|
|
1350
1631
|
|
|
1351
1632
|
function viewScale() {
|
|
1352
1633
|
return clamp(1638 / Math.max(900, window.innerWidth), 0.82, 1.5)
|
|
@@ -1603,6 +1884,68 @@ let mcpDashes = []
|
|
|
1603
1884
|
let mcpGroup = null
|
|
1604
1885
|
let mcpSign = null
|
|
1605
1886
|
let sciFiIcons = []
|
|
1887
|
+
let studyClock = null
|
|
1888
|
+
let annexCats = []
|
|
1889
|
+
|
|
1890
|
+
function drawStudyClock(seconds, alarm) {
|
|
1891
|
+
if (!studyClock) return
|
|
1892
|
+
const ctx = studyClock.ctx
|
|
1893
|
+
const w = 512
|
|
1894
|
+
const h = 512
|
|
1895
|
+
const total = Math.max(0, Math.min(25 * 60, seconds))
|
|
1896
|
+
const mm = String(Math.floor(total / 60)).padStart(2, '0')
|
|
1897
|
+
const ss = String(total % 60).padStart(2, '0')
|
|
1898
|
+
ctx.clearRect(0, 0, w, h)
|
|
1899
|
+
ctx.save()
|
|
1900
|
+
ctx.translate(256, 256)
|
|
1901
|
+
// 背后柔光
|
|
1902
|
+
const glow = ctx.createRadialGradient(0, 0, 40, 0, 0, 240)
|
|
1903
|
+
glow.addColorStop(0, alarm ? 'rgba(255,92,92,.34)' : 'rgba(80,210,255,.24)')
|
|
1904
|
+
glow.addColorStop(1, 'rgba(80,210,255,0)')
|
|
1905
|
+
ctx.fillStyle = glow
|
|
1906
|
+
ctx.beginPath(); ctx.arc(0, 0, 240, 0, Math.PI * 2); ctx.fill()
|
|
1907
|
+
// 铃铛
|
|
1908
|
+
ctx.fillStyle = '#263744'
|
|
1909
|
+
ctx.strokeStyle = alarm ? '#ff6b6b' : '#9fe7ff'
|
|
1910
|
+
ctx.lineWidth = 10
|
|
1911
|
+
for (const x of [-104, 104]) {
|
|
1912
|
+
ctx.beginPath(); ctx.arc(x, -190, 54, Math.PI, 0); ctx.fill(); ctx.stroke()
|
|
1913
|
+
ctx.beginPath(); ctx.moveTo(x - 54, -190); ctx.lineTo(x + 54, -190); ctx.stroke()
|
|
1914
|
+
}
|
|
1915
|
+
// 支脚
|
|
1916
|
+
ctx.lineCap = 'round'
|
|
1917
|
+
ctx.beginPath(); ctx.moveTo(-110, 190); ctx.lineTo(-160, 248); ctx.stroke()
|
|
1918
|
+
ctx.beginPath(); ctx.moveTo(110, 190); ctx.lineTo(160, 248); ctx.stroke()
|
|
1919
|
+
// 表身
|
|
1920
|
+
ctx.fillStyle = '#e9e2d2'
|
|
1921
|
+
ctx.beginPath(); ctx.arc(0, 0, 170, 0, Math.PI * 2); ctx.fill()
|
|
1922
|
+
ctx.lineWidth = 22
|
|
1923
|
+
ctx.strokeStyle = alarm ? '#d84343' : '#294654'
|
|
1924
|
+
ctx.beginPath(); ctx.arc(0, 0, 170, 0, Math.PI * 2); ctx.stroke()
|
|
1925
|
+
// 内圈
|
|
1926
|
+
ctx.fillStyle = alarm ? '#3a1418' : '#112029'
|
|
1927
|
+
ctx.beginPath(); ctx.arc(0, 0, 140, 0, Math.PI * 2); ctx.fill()
|
|
1928
|
+
ctx.strokeStyle = alarm ? '#ff8f8f' : '#8fe7ff'
|
|
1929
|
+
ctx.lineWidth = 3
|
|
1930
|
+
ctx.beginPath(); ctx.arc(0, 0, 140, 0, Math.PI * 2); ctx.stroke()
|
|
1931
|
+
// 数字
|
|
1932
|
+
ctx.textAlign = 'center'
|
|
1933
|
+
ctx.textBaseline = 'middle'
|
|
1934
|
+
ctx.fillStyle = alarm ? '#ffb3b3' : '#9fe7ff'
|
|
1935
|
+
ctx.font = '600 26px ui-monospace, Menlo, monospace'
|
|
1936
|
+
ctx.fillText('FOCUS TIMER', 0, -88)
|
|
1937
|
+
ctx.fillStyle = alarm ? '#ffd0d0' : '#f2fbff'
|
|
1938
|
+
ctx.font = '800 98px ui-monospace, Menlo, monospace'
|
|
1939
|
+
ctx.fillText(mm + ':' + ss, 0, 8)
|
|
1940
|
+
ctx.fillStyle = alarm ? '#ff8f8f' : 'rgba(159,231,255,.75)'
|
|
1941
|
+
ctx.font = '600 22px "PingFang SC", system-ui, sans-serif'
|
|
1942
|
+
ctx.fillText(alarm ? '时间到' : '备考倒计时', 0, 72)
|
|
1943
|
+
// 顶部小灯
|
|
1944
|
+
ctx.fillStyle = alarm ? '#ff6b6b' : '#7dd3a0'
|
|
1945
|
+
ctx.beginPath(); ctx.arc(0, -118, 7, 0, Math.PI * 2); ctx.fill()
|
|
1946
|
+
ctx.restore()
|
|
1947
|
+
studyClock.texture.needsUpdate = true
|
|
1948
|
+
}
|
|
1606
1949
|
|
|
1607
1950
|
function buildAnnex() {
|
|
1608
1951
|
annex = new THREE.Group()
|
|
@@ -1649,6 +1992,12 @@ function buildAnnex() {
|
|
|
1649
1992
|
|
|
1650
1993
|
// 教室布局:讲台在后墙,三张工位面朝讲台
|
|
1651
1994
|
// 1) 通用工位
|
|
1995
|
+
const frosted = new THREE.MeshStandardMaterial({ color: 0xdcefff, transparent: true, opacity: 0.16, roughness: 0.86, metalness: 0.02, depthWrite: false, side: THREE.DoubleSide })
|
|
1996
|
+
function addFrostedHeader(parent, x = 0, width = 1.28, y = 1.16, z = -0.38) {
|
|
1997
|
+
box(width, 0.42, 0.035, frosted, x, y, z, parent)
|
|
1998
|
+
box(width, 0.025, 0.05, metal, x, y + 0.23, z, parent)
|
|
1999
|
+
box(width, 0.025, 0.05, metal, x, y - 0.23, z, parent)
|
|
2000
|
+
}
|
|
1652
2001
|
function addDesk(x, z, matTop = wood) {
|
|
1653
2002
|
const g = new THREE.Group()
|
|
1654
2003
|
g.position.set(x, 0, z)
|
|
@@ -1671,29 +2020,38 @@ function buildAnnex() {
|
|
|
1671
2020
|
base.position.set(0, 0.02, 0.55)
|
|
1672
2021
|
g.add(base)
|
|
1673
2022
|
annex.add(g)
|
|
2023
|
+
addFrostedHeader(g)
|
|
2024
|
+
return g
|
|
1674
2025
|
}
|
|
1675
2026
|
addDesk(-2.35, 0.85)
|
|
1676
2027
|
|
|
1677
|
-
// 2)
|
|
1678
|
-
const
|
|
1679
|
-
|
|
1680
|
-
box(
|
|
1681
|
-
box(0.
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
2028
|
+
// 2) 备考隔间:同一排的学习桌 + 透明磨砂隐私隔板
|
|
2029
|
+
const studyDesk = addDesk(0, 0.85)
|
|
2030
|
+
// 两侧磨砂挡板,形成半包围学习位
|
|
2031
|
+
box(0.035, 1.15, 1.15, frosted, -0.72, 0.85, 0.12, studyDesk)
|
|
2032
|
+
box(0.035, 1.15, 1.15, frosted, 0.72, 0.85, 0.12, studyDesk)
|
|
2033
|
+
|
|
2034
|
+
// 备考倒计时闹钟
|
|
2035
|
+
const clockCanvas = document.createElement('canvas')
|
|
2036
|
+
clockCanvas.width = clockCanvas.height = 512
|
|
2037
|
+
const clockCtx = clockCanvas.getContext('2d')
|
|
2038
|
+
const clockTexture = new THREE.CanvasTexture(clockCanvas)
|
|
2039
|
+
clockTexture.colorSpace = THREE.SRGBColorSpace
|
|
2040
|
+
const clockSprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: clockTexture, transparent: true, depthWrite: false, toneMapped: false }))
|
|
2041
|
+
clockSprite.position.set(0, 2.45, 0.85)
|
|
2042
|
+
clockSprite.scale.set(0.98, 0.98, 1)
|
|
2043
|
+
annex.add(clockSprite)
|
|
2044
|
+
studyClock = {
|
|
2045
|
+
sprite: clockSprite,
|
|
2046
|
+
ctx: clockCtx,
|
|
2047
|
+
texture: clockTexture,
|
|
2048
|
+
endAt: Date.now() + 25 * 60 * 1000,
|
|
2049
|
+
lastSecond: -1,
|
|
2050
|
+
alarmAt: 0,
|
|
2051
|
+
}
|
|
2052
|
+
drawStudyClock(25 * 60, false)
|
|
2053
|
+
box(0.035, 0.025, 1.15, metal, -0.72, 1.51, 0.12, studyDesk)
|
|
2054
|
+
box(0.035, 0.025, 1.15, metal, 0.72, 1.51, 0.12, studyDesk)
|
|
1697
2055
|
|
|
1698
2056
|
// 3) 题库工位:科幻桌面 + 浮动 OCR / 数据库 / 试卷投影
|
|
1699
2057
|
const sci = new THREE.Group()
|
|
@@ -1737,6 +2095,7 @@ function buildAnnex() {
|
|
|
1737
2095
|
sciBase.position.set(0, 0.02, 0.55)
|
|
1738
2096
|
sci.add(sciBase)
|
|
1739
2097
|
annex.add(sci)
|
|
2098
|
+
addFrostedHeader(sci)
|
|
1740
2099
|
const halo = new THREE.Mesh(
|
|
1741
2100
|
new THREE.RingGeometry(0.4, 0.44, 48),
|
|
1742
2101
|
new THREE.MeshBasicMaterial({ color: 0x6fdcff, transparent: true, opacity: 0.35, side: THREE.DoubleSide, depthWrite: false, blending: THREE.AdditiveBlending }),
|
|
@@ -1767,8 +2126,10 @@ function buildAnnex() {
|
|
|
1767
2126
|
const sprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: texture, transparent: true, depthWrite: false, toneMapped: false }))
|
|
1768
2127
|
sprite.scale.set(scale, scale, 1)
|
|
1769
2128
|
sprite.position.set(2.35 + x, y, 0.85 + z)
|
|
2129
|
+
sprite.material.opacity = 0
|
|
1770
2130
|
annex.add(sprite)
|
|
1771
2131
|
sprite.userData.baseY = y
|
|
2132
|
+
sprite.userData.baseScale = scale
|
|
1772
2133
|
sprite.userData.phase = sciFiIcons.length * 0.9
|
|
1773
2134
|
sciFiIcons.push(sprite)
|
|
1774
2135
|
return sprite
|
|
@@ -1781,7 +2142,6 @@ function buildAnnex() {
|
|
|
1781
2142
|
ctx.fillRect(42, 34, 22, 10)
|
|
1782
2143
|
ctx.font = '700 15px ui-monospace, Menlo, monospace'
|
|
1783
2144
|
ctx.textAlign = 'center'
|
|
1784
|
-
ctx.fillText('OCR', 80, 128)
|
|
1785
2145
|
})
|
|
1786
2146
|
const dbTexture = iconCanvas(ctx => {
|
|
1787
2147
|
for (let i = 0; i < 3; i++) {
|
|
@@ -1793,7 +2153,6 @@ function buildAnnex() {
|
|
|
1793
2153
|
}
|
|
1794
2154
|
ctx.font = '700 15px ui-monospace, Menlo, monospace'
|
|
1795
2155
|
ctx.textAlign = 'center'
|
|
1796
|
-
ctx.fillText('DB', 80, 140)
|
|
1797
2156
|
})
|
|
1798
2157
|
const paperTexture = iconCanvas(ctx => {
|
|
1799
2158
|
ctx.beginPath(); ctx.roundRect(46, 28, 68, 92, 5); ctx.stroke()
|
|
@@ -1803,12 +2162,11 @@ function buildAnnex() {
|
|
|
1803
2162
|
}
|
|
1804
2163
|
ctx.font = '700 15px ui-monospace, Menlo, monospace'
|
|
1805
2164
|
ctx.textAlign = 'center'
|
|
1806
|
-
ctx.fillText('EXAM', 80, 142)
|
|
1807
2165
|
})
|
|
1808
2166
|
|
|
1809
|
-
iconSprite(ocrTexture,
|
|
1810
|
-
iconSprite(dbTexture, 0, 2.
|
|
1811
|
-
iconSprite(paperTexture, 0
|
|
2167
|
+
iconSprite(ocrTexture, 0, 2.12, 0.12, 0.95)
|
|
2168
|
+
iconSprite(dbTexture, 0, 2.12, 0.12, 0.95)
|
|
2169
|
+
iconSprite(paperTexture, 0, 2.12, 0.12, 0.95)
|
|
1812
2170
|
|
|
1813
2171
|
// 讲台(教室最前面,背靠后墙,面向三张工位)
|
|
1814
2172
|
const podium = new THREE.Group()
|
|
@@ -1833,6 +2191,46 @@ function buildAnnex() {
|
|
|
1833
2191
|
box(3.3, 1.25, 0.04, dark, 0, 1.55, -2.86)
|
|
1834
2192
|
box(3.1, 0.03, 0.05, cyan, 0, 2.18, -2.83)
|
|
1835
2193
|
|
|
2194
|
+
// 讲台 + 三席的猫咪图片
|
|
2195
|
+
const addAnnexCat = (x, y, z, tint, scale = 0.48, shadowY = null) => {
|
|
2196
|
+
const material = new THREE.SpriteMaterial({ map: faceTextures.happy, color: tint, transparent: true, depthWrite: false, toneMapped: false })
|
|
2197
|
+
const sprite = new THREE.Sprite(material)
|
|
2198
|
+
sprite.scale.set(scale, scale, 1)
|
|
2199
|
+
sprite.position.set(x, y, z)
|
|
2200
|
+
annex.add(sprite)
|
|
2201
|
+
annexCats.push({ sprite, baseY: y, phase: annexCats.length * 1.3 })
|
|
2202
|
+
if (shadowY !== null) {
|
|
2203
|
+
const shadow = new THREE.Mesh(new THREE.PlaneGeometry(scale * 1.5, scale * 1.15), new THREE.MeshBasicMaterial({ map: shadowTexture, transparent: true, opacity: 0.34, depthWrite: false }))
|
|
2204
|
+
shadow.rotation.x = -Math.PI / 2
|
|
2205
|
+
shadow.position.set(x, shadowY, z)
|
|
2206
|
+
annex.add(shadow)
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
addAnnexCat(-2.35, 0.74, 1.28, 0x7fa8ff, 0.46, 0.56)
|
|
2210
|
+
addAnnexCat(0, 0.74, 1.28, 0xff6347, 0.46, 0.56)
|
|
2211
|
+
addAnnexCat(2.35, 0.74, 1.28, 0x5ee7ff, 0.46, 0.56)
|
|
2212
|
+
addAnnexCat(0, 1.5, -1.85, 0xffffff, 0.6, null)
|
|
2213
|
+
|
|
2214
|
+
// Annex 上方 Catora 招牌
|
|
2215
|
+
const annexSignCanvas = document.createElement('canvas')
|
|
2216
|
+
annexSignCanvas.width = 640
|
|
2217
|
+
annexSignCanvas.height = 160
|
|
2218
|
+
const actx = annexSignCanvas.getContext('2d')
|
|
2219
|
+
actx.clearRect(0, 0, 640, 160)
|
|
2220
|
+
actx.font = '900 76px Inter, "PingFang SC", system-ui, sans-serif'
|
|
2221
|
+
actx.textAlign = 'center'
|
|
2222
|
+
actx.textBaseline = 'middle'
|
|
2223
|
+
actx.shadowColor = 'rgba(90,190,255,.85)'
|
|
2224
|
+
actx.shadowBlur = 32
|
|
2225
|
+
actx.fillStyle = '#e9f6ff'
|
|
2226
|
+
actx.fillText('CATORA', 320, 80)
|
|
2227
|
+
const annexSignTexture = new THREE.CanvasTexture(annexSignCanvas)
|
|
2228
|
+
annexSignTexture.colorSpace = THREE.SRGBColorSpace
|
|
2229
|
+
const annexSign = new THREE.Sprite(new THREE.SpriteMaterial({ map: annexSignTexture, transparent: true, depthWrite: false, toneMapped: false }))
|
|
2230
|
+
annexSign.scale.set(2.8, 0.7, 1)
|
|
2231
|
+
annexSign.position.set(0, 3.35, -0.9)
|
|
2232
|
+
annex.add(annexSign)
|
|
2233
|
+
|
|
1836
2234
|
// 室内灯
|
|
1837
2235
|
const lightWarm = new THREE.PointLight(0xffd8b0, 2.6, 11, 2)
|
|
1838
2236
|
lightWarm.position.set(0, 2.15, 0.4)
|
|
@@ -1843,20 +2241,6 @@ function buildAnnex() {
|
|
|
1843
2241
|
annex.add(lightCool)
|
|
1844
2242
|
annexLights.push({ light: lightCool, night: 1.8, day: 0.8 })
|
|
1845
2243
|
|
|
1846
|
-
// 区域标签
|
|
1847
|
-
const tag = (text, x, y, z) => {
|
|
1848
|
-
const el = document.createElement('div')
|
|
1849
|
-
el.className = 'officeTag'
|
|
1850
|
-
el.textContent = text
|
|
1851
|
-
const obj = new CSS2DObject(el)
|
|
1852
|
-
obj.position.set(x, y, z)
|
|
1853
|
-
annex.add(obj)
|
|
1854
|
-
}
|
|
1855
|
-
tag('讲台', 0, 1.9, -1.95)
|
|
1856
|
-
tag('通用', -2.35, 1.45, 0.85)
|
|
1857
|
-
tag('备考', 0, 2.0, 0.85)
|
|
1858
|
-
tag('题库', 2.35, 1.27, 0.85)
|
|
1859
|
-
|
|
1860
2244
|
// MCP 虚线 + 流光 + 暂未接通标牌
|
|
1861
2245
|
mcpGroup = new THREE.Group()
|
|
1862
2246
|
scene.add(mcpGroup)
|
|
@@ -1910,26 +2294,33 @@ function buildAnnex() {
|
|
|
1910
2294
|
sctx.beginPath(); sctx.moveTo(cx - size, cy - size); sctx.lineTo(cx + size, cy + size); sctx.stroke()
|
|
1911
2295
|
sctx.beginPath(); sctx.moveTo(cx + size, cy - size); sctx.lineTo(cx - size, cy + size); sctx.stroke()
|
|
1912
2296
|
}
|
|
1913
|
-
drawX(
|
|
1914
|
-
drawX(
|
|
2297
|
+
drawX(74, 74, 12)
|
|
2298
|
+
drawX(438, 74, 12)
|
|
1915
2299
|
sctx.fillStyle = '#d8efff'
|
|
1916
2300
|
sctx.textAlign = 'center'
|
|
1917
|
-
sctx.font = '700
|
|
1918
|
-
sctx.fillText('MCP', 256,
|
|
1919
|
-
sctx.fillStyle = 'rgba(120,190,255,.82)'
|
|
1920
|
-
sctx.font = '500 22px ui-monospace, Menlo, monospace'
|
|
1921
|
-
sctx.fillText('暂未接通', 256, 128)
|
|
2301
|
+
sctx.font = '700 30px ui-monospace, Menlo, monospace'
|
|
2302
|
+
sctx.fillText('MCP', 256, 86)
|
|
1922
2303
|
const signTex = new THREE.CanvasTexture(signCanvas)
|
|
1923
2304
|
signTex.colorSpace = THREE.SRGBColorSpace
|
|
1924
2305
|
mcpSign = new THREE.Sprite(new THREE.SpriteMaterial({ map: signTex, transparent: true, depthWrite: false, toneMapped: false }))
|
|
1925
|
-
mcpSign.position.set(5.6, 4.
|
|
1926
|
-
mcpSign.scale.set(
|
|
2306
|
+
mcpSign.position.set(5.6, 4.3, 1.18)
|
|
2307
|
+
mcpSign.scale.set(1.8, 0.45, 1)
|
|
2308
|
+
mcpSign.visible = false
|
|
1927
2309
|
mcpGroup.add(mcpSign)
|
|
1928
2310
|
|
|
1929
2311
|
const signGlow = new THREE.Sprite(new THREE.SpriteMaterial({ map: glowTexture, color: 0x3f9dff, transparent: true, opacity: 0.24, blending: THREE.AdditiveBlending, depthWrite: false }))
|
|
1930
2312
|
signGlow.position.copy(mcpSign.position)
|
|
1931
|
-
signGlow.scale.set(
|
|
2313
|
+
signGlow.scale.set(2.5, 0.8, 1)
|
|
2314
|
+
signGlow.visible = false
|
|
1932
2315
|
mcpGroup.add(signGlow)
|
|
2316
|
+
|
|
2317
|
+
// 小号 MCP 标签:固定屏幕字号,和小号说明文字一致
|
|
2318
|
+
const mcpTagEl = document.createElement('div')
|
|
2319
|
+
mcpTagEl.className = 'mcpTag'
|
|
2320
|
+
mcpTagEl.innerHTML = '<span class="x">✕</span><span>MCP</span><span class="x">✕</span>'
|
|
2321
|
+
const mcpTag = new CSS2DObject(mcpTagEl)
|
|
2322
|
+
mcpTag.position.set(5.55, 3.62, 1.02)
|
|
2323
|
+
mcpGroup.add(mcpTag)
|
|
1933
2324
|
}
|
|
1934
2325
|
|
|
1935
2326
|
function rebuild(list) {
|
|
@@ -2067,6 +2458,7 @@ function tick() {
|
|
|
2067
2458
|
const t = clock.getElapsedTime()
|
|
2068
2459
|
requestAnimationFrame(tick)
|
|
2069
2460
|
applyThemeFrame()
|
|
2461
|
+
updateWeatherFrame(t)
|
|
2070
2462
|
if (mcpCurve && mcpBeads.length > 0) {
|
|
2071
2463
|
const flow = clock.getElapsedTime()
|
|
2072
2464
|
for (const bead of mcpBeads) {
|
|
@@ -2084,9 +2476,40 @@ function tick() {
|
|
|
2084
2476
|
dash.mesh.position.copy(point)
|
|
2085
2477
|
dash.mesh.quaternion.setFromUnitVectors(axis, tangent.normalize())
|
|
2086
2478
|
}
|
|
2087
|
-
if (mcpSign) mcpSign.position.y = 4.
|
|
2088
|
-
|
|
2089
|
-
|
|
2479
|
+
if (mcpSign) mcpSign.position.y = 4.3 + Math.sin(flow * 0.9) * 0.025
|
|
2480
|
+
if (studyClock) {
|
|
2481
|
+
const now = Date.now()
|
|
2482
|
+
if (now >= studyClock.endAt) {
|
|
2483
|
+
if (!studyClock.alarmAt) studyClock.alarmAt = now
|
|
2484
|
+
if (now - studyClock.alarmAt > 8000) {
|
|
2485
|
+
studyClock.endAt = now + 25 * 60 * 1000
|
|
2486
|
+
studyClock.alarmAt = 0
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
const remainMs = Math.max(0, studyClock.endAt - now)
|
|
2490
|
+
const remainSec = Math.ceil(remainMs / 1000)
|
|
2491
|
+
if (remainSec !== studyClock.lastSecond) {
|
|
2492
|
+
studyClock.lastSecond = remainSec
|
|
2493
|
+
drawStudyClock(remainSec, remainMs === 0)
|
|
2494
|
+
}
|
|
2495
|
+
studyClock.sprite.position.y = 2.45 + Math.sin(flow * 1.1) * 0.03
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
for (const cat of annexCats) {
|
|
2499
|
+
cat.sprite.position.y = cat.baseY + Math.sin(flow * 1.7 + cat.phase) * 0.065
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
// 三个图标轮流展示:一次只亮一个,淡入淡出
|
|
2503
|
+
const iconCycle = 2.4
|
|
2504
|
+
const activeIcon = Math.floor(flow / iconCycle) % Math.max(1, sciFiIcons.length)
|
|
2505
|
+
for (let i = 0; i < sciFiIcons.length; i++) {
|
|
2506
|
+
const icon = sciFiIcons[i]
|
|
2507
|
+
const on = i === activeIcon
|
|
2508
|
+
icon.material.opacity += ((on ? 1 : 0) - icon.material.opacity) * 0.14
|
|
2509
|
+
const targetScale = icon.userData.baseScale * (on ? 1.12 : 0.86)
|
|
2510
|
+
icon.scale.x += (targetScale - icon.scale.x) * 0.14
|
|
2511
|
+
icon.scale.y += (targetScale - icon.scale.y) * 0.14
|
|
2512
|
+
icon.position.y = icon.userData.baseY + Math.sin(flow * 2.2 + icon.userData.phase) * 0.04 + (on ? 0.04 : 0)
|
|
2090
2513
|
}
|
|
2091
2514
|
}
|
|
2092
2515
|
|
|
@@ -2295,6 +2718,81 @@ async function refreshBalance() {
|
|
|
2295
2718
|
}
|
|
2296
2719
|
}
|
|
2297
2720
|
|
|
2721
|
+
function finishIntro() {
|
|
2722
|
+
if (introTl) {
|
|
2723
|
+
introTl.kill()
|
|
2724
|
+
introTl = null
|
|
2725
|
+
}
|
|
2726
|
+
for (const floor of floors) {
|
|
2727
|
+
floor.group.scale.set(1, 1, 1)
|
|
2728
|
+
floor.group.position.y = floor.offset.y
|
|
2729
|
+
}
|
|
2730
|
+
if (annex) {
|
|
2731
|
+
annex.visible = true
|
|
2732
|
+
annex.scale.setScalar(0.84)
|
|
2733
|
+
annex.position.y = 0
|
|
2734
|
+
}
|
|
2735
|
+
if (mcpGroup) {
|
|
2736
|
+
mcpGroup.visible = true
|
|
2737
|
+
mcpGroup.scale.set(1, 1, 1)
|
|
2738
|
+
}
|
|
2739
|
+
const pose = globalPose(floors.length)
|
|
2740
|
+
controls.target.copy(pose.target)
|
|
2741
|
+
camera.position.copy(pose.position)
|
|
2742
|
+
controls.update()
|
|
2743
|
+
document.body.classList.remove('intro')
|
|
2744
|
+
const intro = $('#intro')
|
|
2745
|
+
if (intro) {
|
|
2746
|
+
intro.classList.add('done')
|
|
2747
|
+
setTimeout(() => intro.remove(), 650)
|
|
2748
|
+
}
|
|
2749
|
+
introPlayed = true
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
function playIntro() {
|
|
2753
|
+
if (introPlayed) return
|
|
2754
|
+
const intro = $('#intro')
|
|
2755
|
+
if (!intro) return
|
|
2756
|
+
introPlayed = true
|
|
2757
|
+
if (reduceMotion) {
|
|
2758
|
+
finishIntro()
|
|
2759
|
+
return
|
|
2760
|
+
}
|
|
2761
|
+
if (annex) {
|
|
2762
|
+
annex.visible = true
|
|
2763
|
+
annex.scale.setScalar(0.02)
|
|
2764
|
+
annex.position.y = -2.4
|
|
2765
|
+
}
|
|
2766
|
+
if (mcpGroup) {
|
|
2767
|
+
mcpGroup.visible = true
|
|
2768
|
+
mcpGroup.scale.setScalar(0.01)
|
|
2769
|
+
}
|
|
2770
|
+
for (const floor of floors) {
|
|
2771
|
+
floor.group.scale.set(0.02, 0.02, 0.02)
|
|
2772
|
+
floor.group.position.y = floor.offset.y - 5.5
|
|
2773
|
+
}
|
|
2774
|
+
const pose = globalPose(floors.length)
|
|
2775
|
+
camera.position.set(pose.position.x + 9, pose.position.y + 11, pose.position.z + 9)
|
|
2776
|
+
controls.target.copy(pose.target)
|
|
2777
|
+
controls.update()
|
|
2778
|
+
|
|
2779
|
+
introTl = gsap.timeline()
|
|
2780
|
+
introTl.to({ value: 0 }, { value: 1, duration: 2.4, ease: 'none' }, 0)
|
|
2781
|
+
introTl.to(camera.position, { x: pose.position.x, y: pose.position.y, z: pose.position.z, duration: 2.2, ease: 'power2.inOut' }, 0.15)
|
|
2782
|
+
introTl.to(controls.target, { x: pose.target.x, y: pose.target.y, z: pose.target.z, duration: 2.2, ease: 'power2.inOut' }, 0.15)
|
|
2783
|
+
floors.forEach((floor, i) => {
|
|
2784
|
+
introTl.to(floor.group.scale, { x: 1, y: 1, z: 1, duration: 0.85, ease: 'back.out(1.35)' }, 0.25 + i * 0.3)
|
|
2785
|
+
introTl.to(floor.group.position, { y: floor.offset.y, duration: 0.85, ease: 'power2.out' }, 0.25 + i * 0.3)
|
|
2786
|
+
})
|
|
2787
|
+
if (annex) {
|
|
2788
|
+
introTl.to(annex.scale, { x: 0.84, y: 0.84, z: 0.84, duration: 0.95, ease: 'back.out(1.25)' }, 1.05)
|
|
2789
|
+
introTl.to(annex.position, { y: 0, duration: 0.95, ease: 'power2.out' }, 1.05)
|
|
2790
|
+
}
|
|
2791
|
+
if (mcpGroup) introTl.to(mcpGroup.scale, { x: 1, y: 1, z: 1, duration: 0.8, ease: 'power2.out' }, 1.45)
|
|
2792
|
+
introTl.call(() => document.body.classList.remove('intro'), null, 2.25)
|
|
2793
|
+
introTl.to(intro, { opacity: 0, duration: 0.5, onComplete: () => intro.remove() }, 2.3)
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2298
2796
|
async function poll() {
|
|
2299
2797
|
try {
|
|
2300
2798
|
const response = await fetch(API, { cache: 'no-store' })
|
|
@@ -2312,6 +2810,7 @@ async function poll() {
|
|
|
2312
2810
|
if (sig !== structureSig) {
|
|
2313
2811
|
structureSig = sig
|
|
2314
2812
|
rebuild(list)
|
|
2813
|
+
if (!introPlayed) playIntro()
|
|
2315
2814
|
} else {
|
|
2316
2815
|
for (const agent of list) {
|
|
2317
2816
|
const entry = agentsById.get(agent.id)
|
|
@@ -2356,6 +2855,7 @@ window.__NS = {
|
|
|
2356
2855
|
get focusFloor() { return focusFloor },
|
|
2357
2856
|
setFloorFocus,
|
|
2358
2857
|
toggleFloorFocus,
|
|
2858
|
+
setWeather,
|
|
2359
2859
|
}
|
|
2360
2860
|
rebuild([])
|
|
2361
2861
|
$('#offline').classList.add('show')
|
|
@@ -2363,9 +2863,11 @@ $('#offline').textContent = '正在读取本机会话…'
|
|
|
2363
2863
|
buildAnnex()
|
|
2364
2864
|
applyGlassSetting()
|
|
2365
2865
|
loadCustomFace()
|
|
2866
|
+
refreshWeather()
|
|
2366
2867
|
poll()
|
|
2367
2868
|
setInterval(poll, 2000)
|
|
2368
2869
|
refreshBalance()
|
|
2369
2870
|
setInterval(refreshBalance, 60_000)
|
|
2370
2871
|
tick()
|
|
2371
2872
|
setTimeout(() => $('#hint').classList.add('hide'), 9000)
|
|
2873
|
+
setTimeout(() => { if (!introPlayed) finishIntro() }, 9000)
|