| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773 |
- <script setup lang="ts">
- import { onMounted, onUnmounted, ref, watch } from 'vue'
- import * as THREE from 'three'
- import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
- import { Sky } from 'three/examples/jsm/objects/Sky.js'
- import { Water, createWaterNormalTexture } from './Water'
- import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
- const containerRef = ref<HTMLDivElement>()
- const pickedPosition = ref<{ x: number; y: number; z: number } | null>(null)
- const cameraInfo = ref({
- position: { x: 0, y: 0, z: 0 },
- target: { x: 0, y: 0, z: 0 },
- distance: 0,
- minDistance: 0,
- maxDistance: 0,
- })
- const waterParams = ref({
- alpha: 0.65,
- sunColor: '#ffffff',
- waterColor: '#78a6a0',
- distortionScale: 30.0,
- noiseScale: 1.0,
- fresnelBias: 0.15,
- fresnelPower: 4.0,
- fresnelStrength: 1.0,
- flowSpeed: 0.5,
- flowDirectionX: 1.0,
- flowDirectionY: 1.0,
- })
- const showCoordinatePanel = ref(false)
- const showCameraPanel = ref(false)
- const showWaterPanel = ref(false)
- let scene: THREE.Scene
- let camera: THREE.PerspectiveCamera
- let renderer: THREE.WebGLRenderer
- let controls: OrbitControls
- let sky: Sky
- let water: Water
- let waterMesh: THREE.Mesh
- let sunDirection: THREE.Vector3
- let animationId: number
- let tilesetGroup: THREE.Group | null = null
- let raycaster: THREE.Raycaster
- let mouse: THREE.Vector2
- function createSky() {
- sky = new Sky()
- sky.scale.setScalar(50000)
- scene.add(sky)
- const uniforms = sky.material.uniforms
- uniforms.turbidity.value = 6
- uniforms.rayleigh.value = 0.1
- uniforms.mieCoefficient.value = 0.005
- uniforms.mieDirectionalG.value = 0.7
- uniforms.sunPosition.value.set(20, 30, 10)
- uniforms.cloudScale.value = 0.0008
- uniforms.cloudSpeed.value = 0.00015
- uniforms.cloudCoverage.value = 0.6
- uniforms.cloudDensity.value = 0.6
- uniforms.cloudElevation.value = 0.9
- }
- function createWaterSurface() {
- const textureLoader = new THREE.TextureLoader()
- const waterNormals = createWaterNormalTexture(textureLoader)
- water = new Water(renderer, camera, scene, {
- textureWidth: 512,
- textureHeight: 512,
- waterNormals,
- alpha: waterParams.value.alpha,
- sunDirection,
- sunColor: new THREE.Color(waterParams.value.sunColor),
- waterColor: new THREE.Color(waterParams.value.waterColor),
- distortionScale: waterParams.value.distortionScale,
- noiseScale: waterParams.value.noiseScale,
- fresnelBias: waterParams.value.fresnelBias,
- fresnelPower: waterParams.value.fresnelPower,
- fresnelStrength: waterParams.value.fresnelStrength,
- flowDirection: new THREE.Vector2(waterParams.value.flowDirectionX, waterParams.value.flowDirectionY),
- side: THREE.DoubleSide,
- fog: true,
- })
- waterMesh = new THREE.Mesh(
- new THREE.PlaneGeometry(200, 200, 60, 60),
- water.material
- )
- waterMesh.add(water)
- waterMesh.rotation.x = -Math.PI / 2
- waterMesh.position.set(840.85714, 7.47851, 2179.50782)
- waterMesh.receiveShadow = true
- scene.add(waterMesh)
- }
- async function load3DTiles() {
- const tilesetUrl = 'http://localhost:9003/model/scence/tileset.json'
- const gltfLoader = new GLTFLoader()
-
- try {
- const response = await fetch(tilesetUrl)
- const tileset = await response.json()
-
- console.log('Tileset loaded:', tileset)
- console.log('Root tile:', tileset.root)
- console.log('Root tile content:', tileset.root?.content)
- console.log('Root tile children count:', tileset.root?.children?.length)
-
- tilesetGroup = new THREE.Group()
-
- async function loadTile(tile: any, parentGroup: THREE.Group, depth: number = 0): Promise<void> {
- console.log(`${' '.repeat(depth)}Processing tile at depth ${depth}`)
- console.log(`${' '.repeat(depth)}Tile content:`, tile.content)
- console.log(`${' '.repeat(depth)}Tile has transform:`, !!tile.transform)
-
- if (tile.content && tile.content.uri) {
- const tileUrl = new URL(tile.content.uri, tilesetUrl).href
- console.log(`${' '.repeat(depth)}Loading tile from: ${tileUrl}`)
-
- try {
- const response = await fetch(tileUrl, { method: 'HEAD' })
- if (!response.ok) {
- throw new Error(`HTTP ${response.status}: ${response.statusText}`)
- }
- console.log(`${' '.repeat(depth)}File exists, content-type: ${response.headers.get('content-type')}`)
-
- const gltf = await new Promise((resolve, reject) => {
- gltfLoader.load(
- tileUrl,
- (gltf) => resolve(gltf),
- (progress) => {
- if (progress.lengthComputable) {
- const percentComplete = (progress.loaded / progress.total) * 100
- console.log(`${' '.repeat(depth)}Loading progress: ${percentComplete.toFixed(2)}%`)
- }
- },
- (error) => {
- console.error(`${' '.repeat(depth)}GLTFLoader error:`, error)
- reject(error)
- }
- )
- }) as { scene: THREE.Group }
-
- if (gltf && gltf.scene) {
- const mesh = gltf.scene.clone()
-
- console.log(`${' '.repeat(depth)}GLB scene children count:`, mesh.children.length)
- console.log(`${' '.repeat(depth)}GLB scene:`, mesh)
-
- mesh.traverse((child) => {
- if (child instanceof THREE.Mesh) {
- console.log(`${' '.repeat(depth)}Found mesh:`, child.name)
- console.log(`${' '.repeat(depth)}Mesh geometry:`, child.geometry)
- console.log(`${' '.repeat(depth)}Mesh material:`, child.material)
- console.log(`${' '.repeat(depth)}Mesh position:`, child.position)
- console.log(`${' '.repeat(depth)}Mesh visible:`, child.visible)
- }
- })
-
- if (tile.transform) {
- const matrix = new THREE.Matrix4().fromArray(tile.transform)
- mesh.applyMatrix4(matrix)
- }
-
- parentGroup.add(mesh)
- console.log(`${' '.repeat(depth)}Successfully loaded tile: ${tile.content.uri}`)
- }
- } catch (error) {
- console.warn(`${' '.repeat(depth)}Error loading tile:`, tile.content.uri, error)
- }
- }
-
- if (tile.children && tile.children.length > 0) {
- console.log(`${' '.repeat(depth)}Tile has ${tile.children.length} children`)
- for (const child of tile.children) {
- await loadTile(child, parentGroup, depth + 1)
- }
- }
- }
-
- if (tileset.root) {
- await loadTile(tileset.root, tilesetGroup)
- }
-
- console.log('Tileset group children count:', tilesetGroup.children.length)
-
- scene.add(tilesetGroup)
-
- const boundingBox = new THREE.Box3().setFromObject(tilesetGroup)
- const center = boundingBox.getCenter(new THREE.Vector3())
- const size = boundingBox.getSize(new THREE.Vector3())
- const maxDim = Math.max(size.x, size.y, size.z)
-
- console.log('Tileset bounding box center:', center)
- console.log('Tileset size:', size)
-
- if (maxDim > 0) {
- tilesetGroup.position.copy(center).multiplyScalar(-1)
- waterMesh.position.set(840.85714, 7.47851, 2179.50782)
- controls.target.set(842.3117, 9.27789, 2178.09268)
- controls.update()
- console.log('Tileset positioned')
- }
-
- } catch (error) {
- console.error('Error loading 3D Tiles:', error)
- console.error('Error details:', (error as Error).message)
- }
- }
- function initScene() {
- const container = containerRef.value!
- scene = new THREE.Scene()
- scene.fog = new THREE.FogExp2(0xd4d4d4, 0.004)
- camera = new THREE.PerspectiveCamera(60, container.clientWidth / container.clientHeight, 0.1, 100000)
- camera.position.set(838.15445, 22.22044, 2209.96548)
- renderer = new THREE.WebGLRenderer({ antialias: true })
- renderer.setSize(container.clientWidth, container.clientHeight)
- renderer.domElement.style.width = '100%'
- renderer.domElement.style.height = '100%'
- renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
- renderer.toneMapping = THREE.ACESFilmicToneMapping
- renderer.toneMappingExposure = 1.0
- renderer.shadowMap.enabled = true
- renderer.shadowMap.type = THREE.PCFShadowMap
- container.appendChild(renderer.domElement)
- controls = new OrbitControls(camera, renderer.domElement)
- controls.enableDamping = true
- controls.dampingFactor = 0.03
- controls.screenSpacePanning = false
- controls.minDistance = 0
- controls.maxDistance = Infinity
- controls.mouseButtons = {
- LEFT: THREE.MOUSE.PAN,
- MIDDLE: THREE.MOUSE.DOLLY,
- RIGHT: THREE.MOUSE.ROTATE,
- }
- controls.target.set(842.3117, 9.27789, 2178.09268)
- controls.maxPolarAngle = Math.PI / 2.1
- controls.minDistance = 20
- controls.maxDistance = 100
- createSky()
- const hemisphereLight = new THREE.HemisphereLight(0xd4d4d4, 0x3d6b4a, 0.6)
- scene.add(hemisphereLight)
- sunDirection = new THREE.Vector3(20, 30, 10).normalize()
- const sunLight = new THREE.DirectionalLight(0xffeedd, 2.0)
- sunLight.position.set(20, 30, 10)
- sunLight.castShadow = true
- sunLight.shadow.mapSize.width = 2048
- sunLight.shadow.mapSize.height = 2048
- sunLight.shadow.camera.near = 0.5
- sunLight.shadow.camera.far = 60
- sunLight.shadow.camera.left = -20
- sunLight.shadow.camera.right = 20
- sunLight.shadow.camera.top = 20
- sunLight.shadow.camera.bottom = -20
- scene.add(sunLight)
- createWaterSurface()
- load3DTiles()
- initRaycaster()
- animate()
- }
- function initRaycaster() {
- raycaster = new THREE.Raycaster()
- mouse = new THREE.Vector2()
-
- const container = containerRef.value!
-
- container.addEventListener('click', onMouseClick)
- }
- function onMouseClick(event: MouseEvent) {
- const container = containerRef.value!
- const rect = container.getBoundingClientRect()
-
- mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1
- mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1
-
- raycaster.setFromCamera(mouse, camera)
-
- const intersectObjects: THREE.Object3D[] = []
-
- if (tilesetGroup) {
- intersectObjects.push(tilesetGroup)
- }
- if (waterMesh) {
- intersectObjects.push(waterMesh)
- }
-
- const intersects = raycaster.intersectObjects(intersectObjects, true)
-
- if (intersects.length > 0) {
- const point = intersects[0].point
- pickedPosition.value = {
- x: Number((point.x * 100).toFixed(3)),
- y: Number((point.y * 100).toFixed(3)),
- z: Number((point.z * 100).toFixed(3))
- }
-
- console.log('Picked position (cm):', pickedPosition.value)
- }
- }
- function animate() {
- animationId = requestAnimationFrame(animate)
- sky.material.uniforms.time.value += 0.001
- water.material.uniforms.time.value += 0.005 * waterParams.value.flowSpeed
- water.render()
- controls.update()
- cameraInfo.value.position = {
- x: Number((camera.position.x * 100).toFixed(3)),
- y: Number((camera.position.y * 100).toFixed(3)),
- z: Number((camera.position.z * 100).toFixed(3)),
- }
- cameraInfo.value.target = {
- x: Number((controls.target.x * 100).toFixed(3)),
- y: Number((controls.target.y * 100).toFixed(3)),
- z: Number((controls.target.z * 100).toFixed(3)),
- }
- cameraInfo.value.distance = Number(camera.position.distanceTo(controls.target).toFixed(3))
- cameraInfo.value.minDistance = controls.minDistance
- cameraInfo.value.maxDistance = controls.maxDistance
- renderer.render(scene, camera)
- }
- function onResize() {
- if (!containerRef.value) return
- const width = containerRef.value.clientWidth
- const height = containerRef.value.clientHeight
- camera.aspect = width / height
- camera.updateProjectionMatrix()
- renderer.setSize(width, height)
- }
- onMounted(() => {
- initScene()
- window.addEventListener('resize', onResize)
- })
- onUnmounted(() => {
- cancelAnimationFrame(animationId)
- window.removeEventListener('resize', onResize)
-
- if (containerRef.value) {
- containerRef.value.removeEventListener('click', onMouseClick)
- }
-
- if (tilesetGroup) {
- scene.remove(tilesetGroup)
- }
-
- renderer.dispose()
- controls.dispose()
- })
- watch(() => waterParams.value, (newParams) => {
- if (water && water.material && water.material.uniforms) {
- water.material.uniforms.alpha.value = newParams.alpha
- water.material.uniforms.sunColor.value.set(newParams.sunColor)
- water.material.uniforms.waterColor.value.set(newParams.waterColor)
- water.material.uniforms.distortionScale.value = newParams.distortionScale
- water.material.uniforms.noiseScale.value = newParams.noiseScale
- water.material.uniforms.fresnelBias.value = newParams.fresnelBias
- water.material.uniforms.fresnelPower.value = newParams.fresnelPower
- water.material.uniforms.fresnelStrength.value = newParams.fresnelStrength
- water.material.uniforms.flowDirection.value.set(newParams.flowDirectionX, newParams.flowDirectionY)
- }
- }, { deep: true })
- </script>
- <template>
- <div ref="containerRef" class="scene-container" />
- <div class="toolbar">
- <button class="toolbar-btn" :class="{ active: showCoordinatePanel }" @click="showCoordinatePanel = !showCoordinatePanel">坐标</button>
- <button class="toolbar-btn" :class="{ active: showCameraPanel }" @click="showCameraPanel = !showCameraPanel">相机</button>
- <button class="toolbar-btn" :class="{ active: showWaterPanel }" @click="showWaterPanel = !showWaterPanel">水面</button>
- </div>
- <div v-if="showCoordinatePanel && pickedPosition" class="panel coordinate-panel">
- <div class="panel-header">
- <span class="panel-title">拾取坐标 (cm)</span>
- <button class="toggle-btn" @click="showCoordinatePanel = false">×</button>
- </div>
- <div class="coordinate-item">
- <span class="coordinate-label">X:</span>
- <span class="coordinate-value">{{ pickedPosition.x }}</span>
- </div>
- <div class="coordinate-item">
- <span class="coordinate-label">Y:</span>
- <span class="coordinate-value">{{ pickedPosition.y }}</span>
- </div>
- <div class="coordinate-item">
- <span class="coordinate-label">Z:</span>
- <span class="coordinate-value">{{ pickedPosition.z }}</span>
- </div>
- </div>
- <div v-if="showCameraPanel" class="panel camera-panel">
- <div class="panel-header">
- <span class="panel-title">相机信息</span>
- <button class="toggle-btn" @click="showCameraPanel = false">×</button>
- </div>
- <div class="camera-section">
- <div class="section-label">位置 (cm)</div>
- <div class="camera-item">
- <span class="camera-label">X:</span>
- <span class="camera-value">{{ cameraInfo.position.x }}</span>
- </div>
- <div class="camera-item">
- <span class="camera-label">Y:</span>
- <span class="camera-value">{{ cameraInfo.position.y }}</span>
- </div>
- <div class="camera-item">
- <span class="camera-label">Z:</span>
- <span class="camera-value">{{ cameraInfo.position.z }}</span>
- </div>
- </div>
- <div class="camera-section">
- <div class="section-label">目标点 (cm)</div>
- <div class="camera-item">
- <span class="camera-label">X:</span>
- <span class="camera-value">{{ cameraInfo.target.x }}</span>
- </div>
- <div class="camera-item">
- <span class="camera-label">Y:</span>
- <span class="camera-value">{{ cameraInfo.target.y }}</span>
- </div>
- <div class="camera-item">
- <span class="camera-label">Z:</span>
- <span class="camera-value">{{ cameraInfo.target.z }}</span>
- </div>
- </div>
- <div class="camera-section">
- <div class="section-label">缩放距离 (m)</div>
- <div class="camera-item">
- <span class="camera-label">当前:</span>
- <span class="camera-value">{{ cameraInfo.distance }}</span>
- </div>
- <div class="camera-item">
- <span class="camera-label">最近:</span>
- <span class="camera-value">{{ cameraInfo.minDistance }}m</span>
- </div>
- <div class="camera-item">
- <span class="camera-label">最远:</span>
- <span class="camera-value">{{ cameraInfo.maxDistance }}m</span>
- </div>
- </div>
- </div>
- <div v-if="showWaterPanel" class="panel water-panel">
- <div class="panel-header">
- <span class="panel-title">水面参数调节</span>
- <button class="toggle-btn" @click="showWaterPanel = false">×</button>
- </div>
- <div class="water-section">
- <div class="section-label">透明度</div>
- <div class="slider-item">
- <input type="range" v-model.number="waterParams.alpha" min="0" max="1" step="0.01" />
- <span class="slider-value">{{ waterParams.alpha.toFixed(2) }}</span>
- </div>
- </div>
- <div class="water-section">
- <div class="section-label">水流速度</div>
- <div class="slider-item">
- <input type="range" v-model.number="waterParams.flowSpeed" min="0" max="3" step="0.1" />
- <span class="slider-value">{{ waterParams.flowSpeed.toFixed(1) }}</span>
- </div>
- </div>
- <div class="water-section">
- <div class="section-label">流向 X</div>
- <div class="slider-item">
- <input type="range" v-model.number="waterParams.flowDirectionX" min="-2" max="2" step="0.1" />
- <span class="slider-value">{{ waterParams.flowDirectionX.toFixed(1) }}</span>
- </div>
- </div>
- <div class="water-section">
- <div class="section-label">流向 Z</div>
- <div class="slider-item">
- <input type="range" v-model.number="waterParams.flowDirectionY" min="-2" max="2" step="0.1" />
- <span class="slider-value">{{ waterParams.flowDirectionY.toFixed(1) }}</span>
- </div>
- </div>
- <div class="water-section">
- <div class="section-label">水颜色</div>
- <div class="color-item">
- <input type="color" v-model="waterParams.waterColor" />
- <span class="color-value">{{ waterParams.waterColor }}</span>
- </div>
- </div>
- <div class="water-section">
- <div class="section-label">太阳光颜色</div>
- <div class="color-item">
- <input type="color" v-model="waterParams.sunColor" />
- <span class="color-value">{{ waterParams.sunColor }}</span>
- </div>
- </div>
- <div class="water-section">
- <div class="section-label">波纹强度</div>
- <div class="slider-item">
- <input type="range" v-model.number="waterParams.distortionScale" min="0" max="100" step="1" />
- <span class="slider-value">{{ waterParams.distortionScale.toFixed(0) }}</span>
- </div>
- </div>
- <div class="water-section">
- <div class="section-label">噪声缩放</div>
- <div class="slider-item">
- <input type="range" v-model.number="waterParams.noiseScale" min="0.1" max="5" step="0.1" />
- <span class="slider-value">{{ waterParams.noiseScale.toFixed(1) }}</span>
- </div>
- </div>
- <div class="water-section">
- <div class="section-label">菲涅尔偏移</div>
- <div class="slider-item">
- <input type="range" v-model.number="waterParams.fresnelBias" min="0" max="1" step="0.01" />
- <span class="slider-value">{{ waterParams.fresnelBias.toFixed(2) }}</span>
- </div>
- </div>
- <div class="water-section">
- <div class="section-label">菲涅尔功率</div>
- <div class="slider-item">
- <input type="range" v-model.number="waterParams.fresnelPower" min="0.1" max="10" step="0.1" />
- <span class="slider-value">{{ waterParams.fresnelPower.toFixed(1) }}</span>
- </div>
- </div>
- <div class="water-section">
- <div class="section-label">菲涅尔强度</div>
- <div class="slider-item">
- <input type="range" v-model.number="waterParams.fresnelStrength" min="0" max="3" step="0.1" />
- <span class="slider-value">{{ waterParams.fresnelStrength.toFixed(1) }}</span>
- </div>
- </div>
- </div>
- </template>
- <style scoped>
- .scene-container {
- position: fixed;
- top: 0;
- left: 0;
- width: 100vw;
- height: 100vh;
- overflow: hidden;
- }
- .toolbar {
- position: fixed;
- top: 20px;
- right: 20px;
- display: flex;
- flex-direction: column;
- gap: 8px;
- z-index: 1001;
- }
- .toolbar-btn {
- padding: 10px 16px;
- border: none;
- border-radius: 8px;
- background: rgba(0, 0, 0, 0.85);
- color: #888;
- font-size: 13px;
- font-weight: bold;
- cursor: pointer;
- transition: all 0.2s;
- min-width: 70px;
- }
- .toolbar-btn:hover {
- background: rgba(30, 30, 30, 0.9);
- color: #fff;
- }
- .toolbar-btn.active {
- color: #fff;
- box-shadow: 0 0 10px rgba(255, 255, 255, 0.2);
- }
- .toolbar-btn:nth-child(1).active { color: #4fc3f7; }
- .toolbar-btn:nth-child(2).active { color: #ff9800; }
- .toolbar-btn:nth-child(3).active { color: #4dd0e1; }
- .panel {
- position: fixed;
- top: 20px;
- left: 20px;
- background: rgba(0, 0, 0, 0.85);
- color: white;
- padding: 15px;
- border-radius: 8px;
- font-family: 'Courier New', monospace;
- z-index: 1000;
- max-height: calc(100vh - 40px);
- overflow-y: auto;
- }
- .coordinate-panel { min-width: 200px; }
- .camera-panel { min-width: 200px; }
- .water-panel { min-width: 240px; }
- .panel-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 12px;
- padding-bottom: 8px;
- border-bottom: 1px solid rgba(255, 255, 255, 0.3);
- }
- .panel-title {
- font-weight: bold;
- font-size: 14px;
- }
- .coordinate-panel .panel-title { color: #4fc3f7; }
- .camera-panel .panel-title { color: #ff9800; }
- .water-panel .panel-title { color: #4dd0e1; }
- .toggle-btn {
- width: 22px;
- height: 22px;
- border: none;
- border-radius: 4px;
- background: rgba(255, 255, 255, 0.15);
- color: white;
- font-size: 14px;
- line-height: 1;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: background 0.2s;
- }
- .toggle-btn:hover {
- background: rgba(255, 255, 255, 0.3);
- }
- .coordinate-item {
- display: flex;
- justify-content: space-between;
- margin: 5px 0;
- }
- .coordinate-label {
- color: #81c784;
- font-weight: bold;
- }
- .coordinate-value {
- color: #fff;
- }
- .camera-section {
- margin-bottom: 10px;
- }
- .section-label {
- color: #81c784;
- font-size: 11px;
- margin-bottom: 5px;
- font-weight: bold;
- }
- .camera-item {
- display: flex;
- justify-content: space-between;
- margin: 3px 0;
- }
- .camera-label {
- color: #4fc3f7;
- }
- .camera-value {
- color: #fff;
- }
- .water-section {
- margin-bottom: 12px;
- }
- .water-section .section-label {
- color: #81c784;
- font-size: 11px;
- margin-bottom: 6px;
- font-weight: bold;
- }
- .slider-item {
- display: flex;
- align-items: center;
- gap: 10px;
- }
- .slider-item input[type="range"] {
- flex: 1;
- height: 6px;
- -webkit-appearance: none;
- background: #444;
- border-radius: 3px;
- outline: none;
- }
- .slider-item input[type="range"]::-webkit-slider-thumb {
- -webkit-appearance: none;
- width: 14px;
- height: 14px;
- background: #4dd0e1;
- border-radius: 50%;
- cursor: pointer;
- }
- .slider-value {
- min-width: 45px;
- text-align: right;
- color: #fff;
- font-size: 11px;
- }
- .color-item {
- display: flex;
- align-items: center;
- gap: 10px;
- }
- .color-item input[type="color"] {
- width: 40px;
- height: 28px;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- background: transparent;
- }
- .color-item input[type="color"]::-webkit-color-swatch-wrapper {
- padding: 0;
- }
- .color-item input[type="color"]::-webkit-color-swatch {
- border: 1px solid rgba(255, 255, 255, 0.3);
- border-radius: 4px;
- }
- .color-value {
- color: #fff;
- font-size: 11px;
- }
- </style>
|