|
| 1 | +/** |
| 2 | + * ChaosMist — 混沌雾(宪法第一条:遗忘=雾) |
| 3 | + * ChaosMist — the entropy fog (constitution §1: forgetting = mist) |
| 4 | + * |
| 5 | + * @ai-context: 深海场景的遗忘可视化:雾强度由世界信号 mist 驱动 |
| 6 | + * (白化珊瑚占比,宪法锁定 ≤0.4)。语义是"朦胧可拨开"——复习恢复 |
| 7 | + * 珊瑚健康后雾自然消退,全程零负向表达。 |
| 8 | + * 性能预算(宪法第四条):high/medium=径向渐变 sprite 缓慢漂移, |
| 9 | + * low=静态单团、无逐帧动画。 |
| 10 | + * |
| 11 | + * @ai-context: Fog sprites whose opacity tracks the mist world signal. |
| 12 | + * Budget: drifting gradient sprites on high/medium, static on low tier. |
| 13 | + */ |
| 14 | +import { useMemo, useRef } from 'react'; |
| 15 | +import * as THREE from 'three'; |
| 16 | +import { useFrame } from '@react-three/fiber'; |
| 17 | +import { useWorldSignalsSelect } from '@/features/retention/hooks/useWorldSignals'; |
| 18 | +import { useEffectiveTier } from '@/lib/performance/usePerformanceMode'; |
| 19 | + |
| 20 | +/** 雾团落点与尺寸(场景边缘,不遮挡中心导航) / Blob anchors at scene edges */ |
| 21 | +const MIST_BLOBS: Array<{ pos: [number, number, number]; scale: number; speed: number }> = [ |
| 22 | + { pos: [-7, -1.5, -6], scale: 16, speed: 0.05 }, |
| 23 | + { pos: [6.5, 2, -8], scale: 13, speed: 0.07 }, |
| 24 | +]; |
| 25 | + |
| 26 | +/** 程序化径向渐变雾纹理(无外部素材依赖) / Procedural radial mist texture */ |
| 27 | +function createMistTexture(): THREE.CanvasTexture { |
| 28 | + const size = 256; |
| 29 | + const canvas = document.createElement('canvas'); |
| 30 | + canvas.width = size; |
| 31 | + canvas.height = size; |
| 32 | + const ctx = canvas.getContext('2d'); |
| 33 | + if (ctx) { |
| 34 | + const g = ctx.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2); |
| 35 | + g.addColorStop(0, 'rgba(7,12,24,0.9)'); |
| 36 | + g.addColorStop(0.55, 'rgba(7,12,24,0.45)'); |
| 37 | + g.addColorStop(1, 'rgba(7,12,24,0)'); |
| 38 | + ctx.fillStyle = g; |
| 39 | + ctx.fillRect(0, 0, size, size); |
| 40 | + } |
| 41 | + return new THREE.CanvasTexture(canvas); |
| 42 | +} |
| 43 | + |
| 44 | +export function ChaosMist() { |
| 45 | + const tier = useEffectiveTier(); |
| 46 | + const { mist } = useWorldSignalsSelect((s) => ({ mist: s.mist })); |
| 47 | + const texture = useMemo(createMistTexture, []); |
| 48 | + const refs = useRef<Array<THREE.Sprite | null>>([]); |
| 49 | + const elapsed = useRef(0); |
| 50 | + |
| 51 | + // low 档:静态呈现语义(雾仍在),不跑逐帧动画(宪法第四条) |
| 52 | + useFrame((_, delta) => { |
| 53 | + if (tier === 'low' || mist <= 0.001) return; |
| 54 | + elapsed.current += Math.min(delta, 0.1); |
| 55 | + refs.current.forEach((sprite, i) => { |
| 56 | + if (!sprite) return; |
| 57 | + const blob = MIST_BLOBS[i]; |
| 58 | + // 极慢漂移:混沌在渗透,但永不吞噬(可拨开的朦胧) |
| 59 | + sprite.position.x = blob.pos[0] + Math.sin(elapsed.current * blob.speed) * 0.8; |
| 60 | + sprite.position.y = blob.pos[1] + Math.cos(elapsed.current * blob.speed * 0.8) * 0.5; |
| 61 | + }); |
| 62 | + }); |
| 63 | + |
| 64 | + // 雾强度直接取自信号(派生层已封顶 0.4);关闭留存时 mist=0,雾自然隐去 |
| 65 | + if (mist <= 0.001) return null; |
| 66 | + const blobCount = tier === 'low' ? 1 : MIST_BLOBS.length; |
| 67 | + |
| 68 | + return ( |
| 69 | + <group> |
| 70 | + {MIST_BLOBS.slice(0, blobCount).map((blob, i) => ( |
| 71 | + <sprite |
| 72 | + key={i} |
| 73 | + ref={(el) => { refs.current[i] = el; }} |
| 74 | + position={blob.pos} |
| 75 | + scale={[blob.scale, blob.scale, 1]} |
| 76 | + > |
| 77 | + <spriteMaterial |
| 78 | + map={texture} |
| 79 | + transparent |
| 80 | + opacity={mist} |
| 81 | + depthWrite={false} |
| 82 | + color="#8FA3C8" |
| 83 | + /> |
| 84 | + </sprite> |
| 85 | + ))} |
| 86 | + </group> |
| 87 | + ); |
| 88 | +} |
0 commit comments