diff --git a/app.js b/app.js index b396094..a098498 100644 --- a/app.js +++ b/app.js @@ -656,6 +656,78 @@ for (let i = 0; i < RUNE_COUNT; i++) { runeSprites.push({ sprite, baseAngle, floatPhase: (i / RUNE_COUNT) * Math.PI * 2 }); } +// === GRAVITY ANOMALY ZONES === +// Areas where ambient particles defy gravity and float upward in spiraling columns. + +const GRAVITY_ZONE_DEFS = [ + { center: new THREE.Vector3( 3.2, 0, 3.2), radius: 1.6, height: 7.0, color: 0x00ccff }, + { center: new THREE.Vector3(-3.2, 0, -3.2), radius: 1.4, height: 6.0, color: 0xcc44ff }, + { center: new THREE.Vector3(-3.2, 0, 3.2), radius: 1.5, height: 5.5, color: 0x44ffaa }, + { center: new THREE.Vector3( 3.2, 0, -3.2), radius: 1.3, height: 6.5, color: 0xff8844 }, +]; + +const ZONE_PARTICLE_COUNT = 35; + +/** + * @type {Array<{points: THREE.Points, geo: THREE.BufferGeometry, states: Array<{r: number, yOff: number, speed: number, driftAngle: number, driftSpeed: number}>, zone: (typeof GRAVITY_ZONE_DEFS)[0]}>} + */ +const gravZoneSystems = []; + +for (const zone of GRAVITY_ZONE_DEFS) { + const N = ZONE_PARTICLE_COUNT; + const positions = new Float32Array(N * 3); + /** @type {Array<{r: number, yOff: number, speed: number, driftAngle: number, driftSpeed: number}>} */ + const states = []; + + for (let p = 0; p < N; p++) { + const angle = Math.random() * Math.PI * 2; + const r = Math.sqrt(Math.random()) * zone.radius; + const yOff = Math.random() * zone.height; + const speed = 0.4 + Math.random() * 0.7; + const driftAngle = Math.random() * Math.PI * 2; + const driftSpeed = (Math.random() - 0.5) * 0.5; + + positions[p * 3] = zone.center.x + Math.cos(angle) * r; + positions[p * 3 + 1] = zone.center.y + yOff; + positions[p * 3 + 2] = zone.center.z + Math.sin(angle) * r; + + states.push({ r, yOff, speed, driftAngle, driftSpeed }); + } + + const geo = new THREE.BufferGeometry(); + geo.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + + const mat = new THREE.PointsMaterial({ + color: zone.color, + size: 0.1, + sizeAttenuation: true, + transparent: true, + opacity: 0.8, + blending: THREE.AdditiveBlending, + depthWrite: false, + }); + + const points = new THREE.Points(geo, mat); + scene.add(points); + gravZoneSystems.push({ points, geo, states, zone }); + + // Faint floor ring marking the zone boundary + const floorRingGeo = new THREE.RingGeometry(zone.radius * 0.82, zone.radius, 32); + const floorRingMat = new THREE.MeshBasicMaterial({ + color: zone.color, + transparent: true, + opacity: 0.13, + side: THREE.DoubleSide, + depthWrite: false, + blending: THREE.AdditiveBlending, + }); + const floorRing = new THREE.Mesh(floorRingGeo, floorRingMat); + floorRing.rotation.x = -Math.PI / 2; + floorRing.position.copy(zone.center); + floorRing.position.y = 0.011; + scene.add(floorRing); +} + // === ANIMATION LOOP === const clock = new THREE.Clock(); @@ -770,6 +842,20 @@ function animate() { rune.sprite.material.opacity = 0.65 + Math.sin(elapsed * 1.2 + rune.floatPhase) * 0.2; } + // Animate gravity anomaly zone particles — float upward, spiral outward + for (const sys of gravZoneSystems) { + const posAttr = sys.geo.attributes.position; + for (let p = 0; p < sys.states.length; p++) { + const s = sys.states[p]; + const yLocal = (s.yOff + s.speed * elapsed) % sys.zone.height; + const swirl = s.driftAngle + elapsed * s.driftSpeed; + posAttr.setX(p, sys.zone.center.x + Math.cos(swirl) * s.r); + posAttr.setY(p, sys.zone.center.y + yLocal); + posAttr.setZ(p, sys.zone.center.z + Math.sin(swirl) * s.r); + } + posAttr.needsUpdate = true; + } + composer.render(); }