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