dsh-agent-office 4.5.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 +4 -2
- package/lib/client.js +64 -2
- package/lib/index.js +19 -1
- package/lib/panel.html +235 -187
- package/lib/state.js +36 -0
- package/package.json +1 -1
- package/panel-src/index.html +50 -2
- package/panel-src/main.js +698 -97
- package/screenshot-day.png +0 -0
- package/screenshot.png +0 -0
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)
|
|
@@ -1589,7 +1870,7 @@ function teamList(list) {
|
|
|
1589
1870
|
|
|
1590
1871
|
|
|
1591
1872
|
/* ------------------------------------------------------------------ *
|
|
1592
|
-
* 右侧 Annex
|
|
1873
|
+
* 右侧 Annex:教室布局(讲台 + 通用/备考/题库三席)
|
|
1593
1874
|
* ------------------------------------------------------------------ */
|
|
1594
1875
|
const ANNEX_X = 9.6
|
|
1595
1876
|
const ANNEX_Z = 0.3
|
|
@@ -1602,6 +1883,69 @@ let mcpBeads = []
|
|
|
1602
1883
|
let mcpDashes = []
|
|
1603
1884
|
let mcpGroup = null
|
|
1604
1885
|
let mcpSign = null
|
|
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
|
+
}
|
|
1605
1949
|
|
|
1606
1950
|
function buildAnnex() {
|
|
1607
1951
|
annex = new THREE.Group()
|
|
@@ -1646,11 +1990,18 @@ function buildAnnex() {
|
|
|
1646
1990
|
box(0.06, 0.06, ANNEX_D, metal, ANNEX_W / 2 - 0.08, 2.38, 0)
|
|
1647
1991
|
box(ANNEX_W - 1.0, 0.025, 0.02, cyan, 0, 2.31, -ANNEX_D / 2 + 0.16)
|
|
1648
1992
|
|
|
1649
|
-
//
|
|
1650
|
-
|
|
1993
|
+
// 教室布局:讲台在后墙,三张工位面朝讲台
|
|
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
|
+
}
|
|
2001
|
+
function addDesk(x, z, matTop = wood) {
|
|
1651
2002
|
const g = new THREE.Group()
|
|
1652
2003
|
g.position.set(x, 0, z)
|
|
1653
|
-
box(1.16, 0.055, 0.62,
|
|
2004
|
+
box(1.16, 0.055, 0.62, matTop, 0, 0.72, 0, g)
|
|
1654
2005
|
for (const [lx, lz] of [[-0.5, -0.24], [0.5, -0.24], [-0.5, 0.24], [0.5, 0.24]]) {
|
|
1655
2006
|
box(0.055, 0.7, 0.055, dark, lx, 0.36, lz, g)
|
|
1656
2007
|
}
|
|
@@ -1658,9 +2009,8 @@ function buildAnnex() {
|
|
|
1658
2009
|
const screen = new THREE.Mesh(new THREE.PlaneGeometry(0.54, 0.3), cyan)
|
|
1659
2010
|
screen.position.set(0, 0.95, -0.155)
|
|
1660
2011
|
g.add(screen)
|
|
1661
|
-
box(0.02, 0.06, 0.02, metal, 0, 0.73, 0.12, g)
|
|
1662
2012
|
box(0.5, 0.055, 0.06, warm, 0, 0.76, 0.3, g)
|
|
1663
|
-
//
|
|
2013
|
+
// 椅子(面向讲台 -z)
|
|
1664
2014
|
box(0.42, 0.06, 0.4, dark, 0, 0.45, 0.55, g)
|
|
1665
2015
|
box(0.42, 0.5, 0.06, dark, 0, 0.7, 0.75, g)
|
|
1666
2016
|
const stem = new THREE.Mesh(new THREE.CylinderGeometry(0.025, 0.025, 0.42, 8), metal)
|
|
@@ -1670,14 +2020,157 @@ function buildAnnex() {
|
|
|
1670
2020
|
base.position.set(0, 0.02, 0.55)
|
|
1671
2021
|
g.add(base)
|
|
1672
2022
|
annex.add(g)
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
addDesk(2.35,
|
|
2023
|
+
addFrostedHeader(g)
|
|
2024
|
+
return g
|
|
2025
|
+
}
|
|
2026
|
+
addDesk(-2.35, 0.85)
|
|
2027
|
+
|
|
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)
|
|
2055
|
+
|
|
2056
|
+
// 3) 题库工位:科幻桌面 + 浮动 OCR / 数据库 / 试卷投影
|
|
2057
|
+
const sci = new THREE.Group()
|
|
2058
|
+
sci.position.set(2.35, 0, 0.85)
|
|
2059
|
+
box(1.16, 0.07, 0.62, metal, 0, 0.72, 0, sci)
|
|
2060
|
+
for (const [lx, lz] of [[-0.5, -0.24], [0.5, -0.24], [-0.5, 0.24], [0.5, 0.24]]) {
|
|
2061
|
+
box(0.05, 0.7, 0.05, dark, lx, 0.36, lz, sci)
|
|
2062
|
+
}
|
|
2063
|
+
// 全息桌屏
|
|
2064
|
+
const holoCanvas = document.createElement('canvas')
|
|
2065
|
+
holoCanvas.width = 256
|
|
2066
|
+
holoCanvas.height = 128
|
|
2067
|
+
const hctx = holoCanvas.getContext('2d')
|
|
2068
|
+
hctx.clearRect(0, 0, 256, 128)
|
|
2069
|
+
hctx.strokeStyle = 'rgba(110,220,255,.7)'
|
|
2070
|
+
hctx.lineWidth = 2
|
|
2071
|
+
for (let x = 0; x <= 256; x += 24) {
|
|
2072
|
+
hctx.beginPath(); hctx.moveTo(x, 0); hctx.lineTo(x, 128); hctx.stroke()
|
|
2073
|
+
}
|
|
2074
|
+
for (let y = 0; y <= 128; y += 24) {
|
|
2075
|
+
hctx.beginPath(); hctx.moveTo(0, y); hctx.lineTo(256, y); hctx.stroke()
|
|
2076
|
+
}
|
|
2077
|
+
hctx.fillStyle = 'rgba(120,230,255,.16)'
|
|
2078
|
+
hctx.fillRect(16, 22, 224, 20)
|
|
2079
|
+
hctx.fillRect(16, 62, 150, 10)
|
|
2080
|
+
hctx.fillRect(16, 86, 190, 10)
|
|
2081
|
+
const holoTexture = new THREE.CanvasTexture(holoCanvas)
|
|
2082
|
+
holoTexture.colorSpace = THREE.SRGBColorSpace
|
|
2083
|
+
const holo = new THREE.Mesh(new THREE.PlaneGeometry(0.86, 0.42), new THREE.MeshBasicMaterial({ map: holoTexture, transparent: true, opacity: 0.55, depthWrite: false, toneMapped: false, side: THREE.DoubleSide }))
|
|
2084
|
+
holo.position.set(0, 1.08, -0.08)
|
|
2085
|
+
holo.rotation.x = -0.18
|
|
2086
|
+
sci.add(holo)
|
|
2087
|
+
box(0.5, 0.055, 0.06, cyan, 0, 0.76, 0.3, sci)
|
|
2088
|
+
// 椅子
|
|
2089
|
+
box(0.42, 0.06, 0.4, dark, 0, 0.45, 0.55, sci)
|
|
2090
|
+
box(0.42, 0.5, 0.06, dark, 0, 0.7, 0.75, sci)
|
|
2091
|
+
const sciStem = new THREE.Mesh(new THREE.CylinderGeometry(0.025, 0.025, 0.42, 8), metal)
|
|
2092
|
+
sciStem.position.set(0, 0.22, 0.55)
|
|
2093
|
+
sci.add(sciStem)
|
|
2094
|
+
const sciBase = new THREE.Mesh(new THREE.CylinderGeometry(0.23, 0.23, 0.03, 16), dark)
|
|
2095
|
+
sciBase.position.set(0, 0.02, 0.55)
|
|
2096
|
+
sci.add(sciBase)
|
|
2097
|
+
annex.add(sci)
|
|
2098
|
+
addFrostedHeader(sci)
|
|
2099
|
+
const halo = new THREE.Mesh(
|
|
2100
|
+
new THREE.RingGeometry(0.4, 0.44, 48),
|
|
2101
|
+
new THREE.MeshBasicMaterial({ color: 0x6fdcff, transparent: true, opacity: 0.35, side: THREE.DoubleSide, depthWrite: false, blending: THREE.AdditiveBlending }),
|
|
2102
|
+
)
|
|
2103
|
+
halo.rotation.x = -Math.PI / 2
|
|
2104
|
+
halo.position.set(2.35, 1.58, 0.98)
|
|
2105
|
+
annex.add(halo)
|
|
2106
|
+
|
|
2107
|
+
// 浮动图标:OCR 相机 / 数据库 / 试卷(billboard,远看也清楚)
|
|
2108
|
+
const iconCanvas = (draw) => {
|
|
2109
|
+
const canvas = document.createElement('canvas')
|
|
2110
|
+
canvas.width = canvas.height = 160
|
|
2111
|
+
const ctx = canvas.getContext('2d')
|
|
2112
|
+
ctx.clearRect(0, 0, 160, 160)
|
|
2113
|
+
ctx.strokeStyle = '#9fe7ff'
|
|
2114
|
+
ctx.fillStyle = '#9fe7ff'
|
|
2115
|
+
ctx.lineWidth = 7
|
|
2116
|
+
ctx.lineCap = 'round'
|
|
2117
|
+
ctx.lineJoin = 'round'
|
|
2118
|
+
ctx.shadowColor = 'rgba(80,200,255,.9)'
|
|
2119
|
+
ctx.shadowBlur = 10
|
|
2120
|
+
draw(ctx)
|
|
2121
|
+
const texture = new THREE.CanvasTexture(canvas)
|
|
2122
|
+
texture.colorSpace = THREE.SRGBColorSpace
|
|
2123
|
+
return texture
|
|
2124
|
+
}
|
|
2125
|
+
const iconSprite = (texture, x, y, z, scale = 0.4) => {
|
|
2126
|
+
const sprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: texture, transparent: true, depthWrite: false, toneMapped: false }))
|
|
2127
|
+
sprite.scale.set(scale, scale, 1)
|
|
2128
|
+
sprite.position.set(2.35 + x, y, 0.85 + z)
|
|
2129
|
+
sprite.material.opacity = 0
|
|
2130
|
+
annex.add(sprite)
|
|
2131
|
+
sprite.userData.baseY = y
|
|
2132
|
+
sprite.userData.baseScale = scale
|
|
2133
|
+
sprite.userData.phase = sciFiIcons.length * 0.9
|
|
2134
|
+
sciFiIcons.push(sprite)
|
|
2135
|
+
return sprite
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
const ocrTexture = iconCanvas(ctx => {
|
|
2139
|
+
ctx.beginPath(); ctx.roundRect(32, 44, 78, 58, 10); ctx.stroke()
|
|
2140
|
+
ctx.beginPath(); ctx.arc(73, 73, 17, 0, Math.PI * 2); ctx.stroke()
|
|
2141
|
+
ctx.beginPath(); ctx.arc(73, 73, 6, 0, Math.PI * 2); ctx.fill()
|
|
2142
|
+
ctx.fillRect(42, 34, 22, 10)
|
|
2143
|
+
ctx.font = '700 15px ui-monospace, Menlo, monospace'
|
|
2144
|
+
ctx.textAlign = 'center'
|
|
2145
|
+
})
|
|
2146
|
+
const dbTexture = iconCanvas(ctx => {
|
|
2147
|
+
for (let i = 0; i < 3; i++) {
|
|
2148
|
+
const y = 48 + i * 24
|
|
2149
|
+
ctx.beginPath(); ctx.ellipse(80, y, 28, 9, 0, 0, Math.PI * 2); ctx.stroke()
|
|
2150
|
+
ctx.beginPath(); ctx.moveTo(52, y); ctx.lineTo(52, y + 22); ctx.stroke()
|
|
2151
|
+
ctx.beginPath(); ctx.moveTo(108, y); ctx.lineTo(108, y + 22); ctx.stroke()
|
|
2152
|
+
ctx.beginPath(); ctx.ellipse(80, y + 22, 28, 9, 0, 0, Math.PI * 2); ctx.stroke()
|
|
2153
|
+
}
|
|
2154
|
+
ctx.font = '700 15px ui-monospace, Menlo, monospace'
|
|
2155
|
+
ctx.textAlign = 'center'
|
|
2156
|
+
})
|
|
2157
|
+
const paperTexture = iconCanvas(ctx => {
|
|
2158
|
+
ctx.beginPath(); ctx.roundRect(46, 28, 68, 92, 5); ctx.stroke()
|
|
2159
|
+
ctx.beginPath(); ctx.moveTo(98, 28); ctx.lineTo(114, 44); ctx.lineTo(98, 44); ctx.closePath(); ctx.stroke()
|
|
2160
|
+
for (let i = 0; i < 5; i++) {
|
|
2161
|
+
ctx.beginPath(); ctx.moveTo(60, 54 + i * 13); ctx.lineTo(100, 54 + i * 13); ctx.stroke()
|
|
2162
|
+
}
|
|
2163
|
+
ctx.font = '700 15px ui-monospace, Menlo, monospace'
|
|
2164
|
+
ctx.textAlign = 'center'
|
|
2165
|
+
})
|
|
2166
|
+
|
|
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)
|
|
1677
2170
|
|
|
1678
|
-
//
|
|
2171
|
+
// 讲台(教室最前面,背靠后墙,面向三张工位)
|
|
1679
2172
|
const podium = new THREE.Group()
|
|
1680
|
-
podium.position.set(0, 0, 1.
|
|
2173
|
+
podium.position.set(0, 0, -1.95)
|
|
1681
2174
|
box(1.35, 0.14, 0.95, dark, 0, 0.07, 0, podium)
|
|
1682
2175
|
const lectern = box(0.7, 0.98, 0.5, wood, 0, 0.56, 0, podium)
|
|
1683
2176
|
lectern.rotation.x = -0.04
|
|
@@ -1694,49 +2187,49 @@ function buildAnnex() {
|
|
|
1694
2187
|
micHead.position.set(0.24, 1.4, 0.02)
|
|
1695
2188
|
podium.add(micHead)
|
|
1696
2189
|
annex.add(podium)
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
const
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
annex.add(
|
|
2190
|
+
// 讲台后的黑板/示教板
|
|
2191
|
+
box(3.3, 1.25, 0.04, dark, 0, 1.55, -2.86)
|
|
2192
|
+
box(3.1, 0.03, 0.05, cyan, 0, 2.18, -2.83)
|
|
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)
|
|
1740
2233
|
|
|
1741
2234
|
// 室内灯
|
|
1742
2235
|
const lightWarm = new THREE.PointLight(0xffd8b0, 2.6, 11, 2)
|
|
@@ -1748,19 +2241,6 @@ function buildAnnex() {
|
|
|
1748
2241
|
annex.add(lightCool)
|
|
1749
2242
|
annexLights.push({ light: lightCool, night: 1.8, day: 0.8 })
|
|
1750
2243
|
|
|
1751
|
-
// 区域标签
|
|
1752
|
-
const tag = (text, x, y, z) => {
|
|
1753
|
-
const el = document.createElement('div')
|
|
1754
|
-
el.className = 'officeTag'
|
|
1755
|
-
el.textContent = text
|
|
1756
|
-
const obj = new CSS2DObject(el)
|
|
1757
|
-
obj.position.set(x, y, z)
|
|
1758
|
-
annex.add(obj)
|
|
1759
|
-
}
|
|
1760
|
-
tag('讲台', 0, 1.8, 1.55)
|
|
1761
|
-
tag('健身', 2.45, 1.95, 1.5)
|
|
1762
|
-
tag('备考', -2.55, 2.0, 1.5)
|
|
1763
|
-
|
|
1764
2244
|
// MCP 虚线 + 流光 + 暂未接通标牌
|
|
1765
2245
|
mcpGroup = new THREE.Group()
|
|
1766
2246
|
scene.add(mcpGroup)
|
|
@@ -1814,26 +2294,33 @@ function buildAnnex() {
|
|
|
1814
2294
|
sctx.beginPath(); sctx.moveTo(cx - size, cy - size); sctx.lineTo(cx + size, cy + size); sctx.stroke()
|
|
1815
2295
|
sctx.beginPath(); sctx.moveTo(cx + size, cy - size); sctx.lineTo(cx - size, cy + size); sctx.stroke()
|
|
1816
2296
|
}
|
|
1817
|
-
drawX(
|
|
1818
|
-
drawX(
|
|
2297
|
+
drawX(74, 74, 12)
|
|
2298
|
+
drawX(438, 74, 12)
|
|
1819
2299
|
sctx.fillStyle = '#d8efff'
|
|
1820
2300
|
sctx.textAlign = 'center'
|
|
1821
|
-
sctx.font = '700
|
|
1822
|
-
sctx.fillText('MCP', 256,
|
|
1823
|
-
sctx.fillStyle = 'rgba(120,190,255,.82)'
|
|
1824
|
-
sctx.font = '500 22px ui-monospace, Menlo, monospace'
|
|
1825
|
-
sctx.fillText('暂未接通', 256, 128)
|
|
2301
|
+
sctx.font = '700 30px ui-monospace, Menlo, monospace'
|
|
2302
|
+
sctx.fillText('MCP', 256, 86)
|
|
1826
2303
|
const signTex = new THREE.CanvasTexture(signCanvas)
|
|
1827
2304
|
signTex.colorSpace = THREE.SRGBColorSpace
|
|
1828
2305
|
mcpSign = new THREE.Sprite(new THREE.SpriteMaterial({ map: signTex, transparent: true, depthWrite: false, toneMapped: false }))
|
|
1829
|
-
mcpSign.position.set(5.6, 4.
|
|
1830
|
-
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
|
|
1831
2309
|
mcpGroup.add(mcpSign)
|
|
1832
2310
|
|
|
1833
2311
|
const signGlow = new THREE.Sprite(new THREE.SpriteMaterial({ map: glowTexture, color: 0x3f9dff, transparent: true, opacity: 0.24, blending: THREE.AdditiveBlending, depthWrite: false }))
|
|
1834
2312
|
signGlow.position.copy(mcpSign.position)
|
|
1835
|
-
signGlow.scale.set(
|
|
2313
|
+
signGlow.scale.set(2.5, 0.8, 1)
|
|
2314
|
+
signGlow.visible = false
|
|
1836
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)
|
|
1837
2324
|
}
|
|
1838
2325
|
|
|
1839
2326
|
function rebuild(list) {
|
|
@@ -1971,6 +2458,7 @@ function tick() {
|
|
|
1971
2458
|
const t = clock.getElapsedTime()
|
|
1972
2459
|
requestAnimationFrame(tick)
|
|
1973
2460
|
applyThemeFrame()
|
|
2461
|
+
updateWeatherFrame(t)
|
|
1974
2462
|
if (mcpCurve && mcpBeads.length > 0) {
|
|
1975
2463
|
const flow = clock.getElapsedTime()
|
|
1976
2464
|
for (const bead of mcpBeads) {
|
|
@@ -1988,7 +2476,41 @@ function tick() {
|
|
|
1988
2476
|
dash.mesh.position.copy(point)
|
|
1989
2477
|
dash.mesh.quaternion.setFromUnitVectors(axis, tangent.normalize())
|
|
1990
2478
|
}
|
|
1991
|
-
if (mcpSign) mcpSign.position.y = 4.
|
|
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)
|
|
2513
|
+
}
|
|
1992
2514
|
}
|
|
1993
2515
|
|
|
1994
2516
|
const focusedTeam = focusFloor === null ? null : floors.find(floor => floor.index === focusFloor)?.team
|
|
@@ -2196,6 +2718,81 @@ async function refreshBalance() {
|
|
|
2196
2718
|
}
|
|
2197
2719
|
}
|
|
2198
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
|
+
|
|
2199
2796
|
async function poll() {
|
|
2200
2797
|
try {
|
|
2201
2798
|
const response = await fetch(API, { cache: 'no-store' })
|
|
@@ -2213,6 +2810,7 @@ async function poll() {
|
|
|
2213
2810
|
if (sig !== structureSig) {
|
|
2214
2811
|
structureSig = sig
|
|
2215
2812
|
rebuild(list)
|
|
2813
|
+
if (!introPlayed) playIntro()
|
|
2216
2814
|
} else {
|
|
2217
2815
|
for (const agent of list) {
|
|
2218
2816
|
const entry = agentsById.get(agent.id)
|
|
@@ -2257,6 +2855,7 @@ window.__NS = {
|
|
|
2257
2855
|
get focusFloor() { return focusFloor },
|
|
2258
2856
|
setFloorFocus,
|
|
2259
2857
|
toggleFloorFocus,
|
|
2858
|
+
setWeather,
|
|
2260
2859
|
}
|
|
2261
2860
|
rebuild([])
|
|
2262
2861
|
$('#offline').classList.add('show')
|
|
@@ -2264,9 +2863,11 @@ $('#offline').textContent = '正在读取本机会话…'
|
|
|
2264
2863
|
buildAnnex()
|
|
2265
2864
|
applyGlassSetting()
|
|
2266
2865
|
loadCustomFace()
|
|
2866
|
+
refreshWeather()
|
|
2267
2867
|
poll()
|
|
2268
2868
|
setInterval(poll, 2000)
|
|
2269
2869
|
refreshBalance()
|
|
2270
2870
|
setInterval(refreshBalance, 60_000)
|
|
2271
2871
|
tick()
|
|
2272
2872
|
setTimeout(() => $('#hint').classList.add('hide'), 9000)
|
|
2873
|
+
setTimeout(() => { if (!introPlayed) finishIntro() }, 9000)
|