Files
2026-10-27 02:22:09 +00:00

740 lines
30 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>Numex Deep Blue — Okyanusun karanlığında yaşayan ışıklar</title>
<meta name="description" content="Tamamen kodla üretilmiş interaktif denizanası akvaryumu: biyolüminesans denizanaları, derinlik, sinematik tur." />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@300;400&family=Inter:wght@300;400;500&display=swap" rel="stylesheet" />
<style>
:root{
--ink:#e8f4ff;
--dim:rgba(214,236,255,.58);
--panel:rgba(6,18,38,.46);
--brd:rgba(140,200,255,.20);
--accent:#6fe3ff;
}
*{ box-sizing:border-box; -webkit-tap-highlight-color:transparent; }
html,body{ height:100%; margin:0; }
body{
background:#01060f; color:var(--ink); overflow:hidden; touch-action:none;
font-family:'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif;
}
#scene{ position:fixed; inset:0; display:block; width:100%; height:100%; }
#veil{
position:fixed; inset:0; background:#01060f; z-index:60;
display:flex; align-items:center; justify-content:center; flex-direction:column; gap:16px;
letter-spacing:.30em; font-size:11px; color:var(--dim); transition:opacity 1.1s ease;
}
#veil.gone{ opacity:0; pointer-events:none; }
#veil .bar{ width:130px; height:1px; background:rgba(140,200,255,.18); overflow:hidden; }
#veil .bar i{ display:block; height:100%; width:40%; background:var(--accent); box-shadow:0 0 12px var(--accent); animation:slide 1.5s ease-in-out infinite; }
@keyframes slide{ 0%{transform:translateX(-100%)} 100%{transform:translateX(300%)} }
#title{
position:fixed; top:clamp(14px,3.2vh,34px); left:0; right:0; text-align:center;
z-index:10; pointer-events:none; transition:opacity .8s ease;
}
#title h1{
font-family:'Cormorant Garamond', serif; font-weight:400; margin:0;
font-size:clamp(20px,3.6vw,44px); letter-spacing:.20em; white-space:nowrap;
color:#eafafd;
text-shadow:0 0 24px rgba(90,220,255,.55), 0 0 60px rgba(120,140,255,.35), 0 2px 6px rgba(0,0,0,.8);
}
#title p{
margin:clamp(4px,.9vh,8px) 0 0; padding:0 16px; line-height:1.6;
font-size:clamp(8.5px,1.15vw,11px); letter-spacing:.34em; text-transform:uppercase;
color:var(--dim); font-weight:300;
}
#hint{
position:fixed; left:50%; transform:translateX(-50%);
top:clamp(84px,15vh,140px); z-index:9; padding:0 18px;
font-size:10.5px; letter-spacing:.14em; text-align:center; line-height:1.7;
color:rgba(200,232,255,.48); pointer-events:none; transition:opacity 1.4s ease;
}
#hint.gone{ opacity:0; }
#panel{
position:fixed; z-index:20; left:50%; transform:translateX(-50%);
bottom:clamp(12px,2.6vh,26px);
width:min(640px, calc(100vw - 24px));
display:flex; flex-wrap:wrap; align-items:center; justify-content:center; gap:6px 8px;
padding:10px clamp(10px,1.6vw,16px);
background:var(--panel); border:1px solid var(--brd); border-radius:18px;
backdrop-filter:blur(18px) saturate(1.25); -webkit-backdrop-filter:blur(18px) saturate(1.25);
box-shadow:0 18px 54px rgba(0,10,30,.55), inset 0 1px 0 rgba(180,225,255,.10);
transition:opacity .5s ease, transform .5s ease;
}
#panel.hidden{ opacity:0; pointer-events:none; transform:translate(-50%,130%); }
.grp{ display:flex; align-items:center; gap:5px; padding:0 6px; }
.grp + .grp{ border-left:1px solid rgba(140,200,255,.14); }
.lbl{ font-size:9px; letter-spacing:.18em; text-transform:uppercase; color:var(--dim); margin-right:2px; white-space:nowrap; }
button{
font-family:inherit; font-size:11px; font-weight:500; color:var(--ink);
background:rgba(120,190,255,.09); border:1px solid rgba(140,200,255,.20);
border-radius:11px; padding:0 11px; height:34px; min-height:34px; min-width:34px;
cursor:pointer; display:inline-flex; align-items:center; justify-content:center; gap:5px;
transition:background .22s, border-color .22s, color .22s, transform .12s; white-space:nowrap;
}
button:hover{ background:rgba(120,200,255,.20); border-color:rgba(140,220,255,.40); }
button:active{ transform:scale(.96); }
button.on{ background:rgba(111,227,255,.24); border-color:rgba(111,227,255,.65); color:#dff8ff; box-shadow:0 0 18px rgba(111,227,255,.35); }
button svg{ width:14px; height:14px; }
input[type=range]{
-webkit-appearance:none; appearance:none; width:clamp(56px,8.5vw,90px); height:30px;
background:transparent; cursor:pointer;
}
input[type=range]::-webkit-slider-runnable-track{ height:3px; border-radius:3px; background:rgba(160,215,255,.24); }
input[type=range]::-webkit-slider-thumb{
-webkit-appearance:none; width:14px; height:14px; border-radius:50%; background:var(--accent);
margin-top:-5.5px; border:none; box-shadow:0 0 12px rgba(111,227,255,.85);
}
input[type=range]::-moz-range-track{ height:3px; border-radius:3px; background:rgba(160,215,255,.24); }
input[type=range]::-moz-range-thumb{ width:14px; height:14px; border:none; border-radius:50%; background:var(--accent); }
#toggle{
position:fixed; z-index:25; right:clamp(10px,2vw,20px); bottom:clamp(12px,2.6vh,26px);
width:44px; height:44px; min-width:44px; min-height:44px; border-radius:50%; padding:0;
background:rgba(6,18,38,.55); backdrop-filter:blur(14px); -webkit-backdrop-filter:blur(14px);
}
@media (max-width:640px){
.lbl{ display:none; }
.grp{ padding:0 3px; gap:4px; }
.grp + .grp{ border-left:none; }
button{ font-size:10px; padding:0 9px; height:36px; min-height:36px; }
#panel{ gap:5px 4px; padding:8px; border-radius:16px; justify-content:flex-start; }
#title h1{ letter-spacing:.12em; font-size:clamp(17px,5.4vw,26px); }
#title p{ letter-spacing:.20em; font-size:8px; padding:0 22px; }
#hint{ font-size:9px; top:clamp(70px,12vh,110px); line-height:1.8; }
}
@media (prefers-reduced-motion: reduce){
#veil .bar i{ animation:none; width:100%; }
}
</style>
</head>
<body>
<canvas id="scene"></canvas>
<div id="veil"><span>NUMEX DEEP BLUE</span><div class="bar"><i></i></div></div>
<div id="title">
<h1>DEEP BLUE</h1>
<p>Okyanusun karanlığında yaşayan ışıklar</p>
</div>
<div id="hint">sürükle: bak &nbsp;·&nbsp; tekerlek: yakınlaş &nbsp;·&nbsp; denizanasına dokun</div>
<div id="panel">
<div class="grp">
<span class="lbl">Işık</span>
<input id="glow" type="range" min="10" max="100" value="60" aria-label="Işık şiddeti" />
</div>
<div class="grp">
<span class="lbl">Hız</span>
<input id="speed" type="range" min="0" max="100" value="40" aria-label="Yüzme hızı" />
</div>
<div class="grp">
<span class="lbl">Sayı</span>
<input id="count" type="range" min="8" max="24" value="12" aria-label="Denizanası sayısı" />
</div>
<div class="grp">
<button id="tourBtn" title="Sinematik tur">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M4 12a8 8 0 1 1 2.34 5.66"/><path d="M4 20v-6h6"/></svg>
Tur
</button>
<button id="resetBtn" title="Kamerayı sıfırla" aria-label="Kamerayı sıfırla">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><circle cx="12" cy="12" r="3"/><path d="M12 3v3M12 18v3M3 12h3M18 12h3"/></svg>
</button>
</div>
</div>
<button id="toggle" title="Paneli gizle" aria-label="Paneli gizle">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" width="17" height="17"><path d="M4 7h16M4 12h16M4 17h16"/></svg>
</button>
<script type="importmap">
{ "imports": { "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/" } }
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
/* ═════════ sahne ═════════ */
const canvas = document.getElementById('scene');
const isMobile = matchMedia('(max-width: 820px)').matches || /Android|iPhone|iPad/i.test(navigator.userAgent);
const renderer = new THREE.WebGLRenderer({ canvas, antialias:true, powerPreference:'high-performance' });
renderer.setPixelRatio(Math.min(devicePixelRatio, isMobile ? 1.6 : 2));
renderer.setSize(innerWidth, innerHeight);
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.05;
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x020a18, 0.038);
const camera = new THREE.PerspectiveCamera(52, innerWidth/innerHeight, 0.1, 300);
// NUMEX-DÜZELTME (2026-10-27): dikey telefonda dar görüş alanı sahneyi kırpıyordu (kenardaki denizanası yarısı dışarıda).
// Mobilde kamera geri alınır ve görüş açısı genişletilir; sahne bütün olarak sığar.
const CAM_START = isMobile ? new THREE.Vector3(0, 2.6, 38) : new THREE.Vector3(0, 2.0, 27);
if (isMobile) camera.fov = 62;
camera.updateProjectionMatrix();
camera.position.copy(CAM_START);
const controls = new OrbitControls(camera, canvas);
controls.target.set(0, 0.4, 0);
controls.enableDamping = true;
controls.dampingFactor = 0.06;
controls.minDistance = 4;
controls.maxDistance = 30;
controls.maxPolarAngle = Math.PI * 0.78;
controls.minPolarAngle = 0.25;
controls.enablePan = false;
controls.rotateSpeed = 0.55;
controls.zoomSpeed = 0.7;
controls.update();
/* ═════════ derinlik — arka plan tonu ve ışık huzmeleri ═════════ */
const bgGeo = new THREE.SphereGeometry(150, 32, 20);
const bgMat = new THREE.ShaderMaterial({
side: THREE.BackSide, depthWrite:false,
uniforms:{ uTop:{value:new THREE.Color(0x0a3a63)}, uBot:{value:new THREE.Color(0x01040c)} },
vertexShader:`varying vec3 vP; void main(){ vP = position; gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0); }`,
fragmentShader:`
varying vec3 vP; uniform vec3 uTop, uBot;
void main(){
float h = clamp(normalize(vP).y * 0.5 + 0.5, 0.0, 1.0);
vec3 c = mix(uBot, uTop, pow(h, 1.5));
gl_FragColor = vec4(c, 1.0);
}`
});
scene.add(new THREE.Mesh(bgGeo, bgMat));
// yukarıdan sızan ışık huzmeleri — yarı saydam koniler
const beamGroup = new THREE.Group();
scene.add(beamGroup);
// NUMEX-DÜZELTME (2026-10-27): katı koni geometrisi keskin üçgen blok ve tabanda koyu "disk" gölgesi bırakıyordu.
// Işık huzmesi artık gölgelendiriciyle çizilir: kenarı yumuşakça sönen, tepeden tabana zayıflayan hacim.
const beamVert = `
varying float vY;
varying vec3 vView;
void main(){
vY = uv.y;
vec4 mv = modelViewMatrix * vec4(position, 1.0);
vView = normalize(-mv.xyz);
gl_Position = projectionMatrix * mv;
}`;
const beamFrag = `
varying float vY;
varying vec3 vView;
uniform vec3 uColor;
uniform float uOpacity;
void main(){
// uv.y: 0 taban, 1 tepe → tepede parlak, tabanda tamamen söner
float vert = pow(vY, 3.0);
// silindirin yanlarına bakıldıkça (yüzeye dik) daha parlak: hacim hissi
float edge = 1.0;
vec3 n = normalize(cross(dFdx(vView), dFdy(vView)));
gl_FragColor = vec4(uColor, uOpacity * vert * edge);
}`;
for (let i=0;i<7;i++){
const h = 26 + Math.random()*8;
const geo = new THREE.CylinderGeometry(0.8 + Math.random()*1.2, 4 + Math.random()*3, h, 24, 8, true);
const mat = new THREE.ShaderMaterial({
uniforms:{ uColor:{value:new THREE.Color(0x8fd8ff)}, uOpacity:{value: 0.10} },
vertexShader: beamVert, fragmentShader: beamFrag,
transparent:true, depthWrite:false, side:THREE.DoubleSide, blending:THREE.AdditiveBlending
});
const m = new THREE.Mesh(geo, mat);
m.position.set((Math.random()-0.5)*36, h*0.35, (Math.random()-0.5)*36);
m.rotation.z = (Math.random()-0.5)*0.16;
beamGroup.add(m);
}
/* ═════════ ışıklar ═════════ */
const hemi = new THREE.HemisphereLight(0x7fd4ff, 0x030814, 0.55);
scene.add(hemi);
const key = new THREE.DirectionalLight(0x9fdcff, 0.75);
key.position.set(4, 18, 8);
scene.add(key);
const rim = new THREE.PointLight(0x8a6bff, 22, 42, 2);
rim.position.set(-10, 6, -8);
scene.add(rim);
const ambientFill = new THREE.AmbientLight(0x1b3a68, 0.7);
scene.add(ambientFill);
/* ═════════ denizanası ═════════ */
const PALETTE = [0x6fe3ff, 0xa77bff, 0xff8fd0, 0x64ffd8, 0x7ea8ff, 0xffb0e0];
const glowState = { intensity: 0.6 };
// gövde şemsiyesi: yarı saydam, kenarı hafif dalgalı
// NUMEX-DÜZELTME (2026-10-27): İlk sürüm yarım kürenin alt kısmını kesiyordu; sonuç düz "kase" gibi duruyordu.
// Şemsiye formu: üstte kubbe, kenarda hafif içe kıvrılan çan ağzı, ağız kenarında 8 dilimli dalgalanma.
// NUMEX-DÜZELTME (2026-10-27): küre kesiti (SphereGeometry + phiLength) çanın eteğini düz bir çizgiyle kapatıyordu;
// sonuç "pervane/mantar şapkası" gibi duruyordu. Çan artık profil eğrisinden sıfırdan kurulur:
// tepede kapalı kubbe → gövde genişler → etek dışa taşar ve aşağı kıvrılır (gerçek medüza formu).
function makeBellGeometry(){
const RADIAL = 56, PROFILE = 22;
const verts = [], norms = [], uvs = [], idx = [];
// profil: t∈[0,1] → yarıçap (r) ve yükseklik (y)
function profile(t){
// t=0 tepe (kapalı kubbe), t=1 çan ağzı (etek)
const r = Math.sin(t * Math.PI * 0.5) * (0.78 + t * 0.34);
// y: tepeden aşağı inerken önce dik, sonra eteğe doğru aşağı kıvrılır
const y = Math.cos(t * Math.PI * 0.5) * 0.92 - Math.pow(t, 3.2) * 0.44;
return { r, y };
}
for (let j=0;j<=PROFILE;j++){
const t = j / PROFILE;
const p = profile(t);
for (let i=0;i<=RADIAL;i++){
const a = (i / RADIAL) * Math.PI * 2;
// etek kenarında 10 dilimli dalgalanma (canlı çan ağzı)
const wob = Math.sin(a * 10) * Math.pow(t, 3) * 0.055;
const rr = p.r * (1 + wob);
const yy = p.y - Math.pow(t, 4) * Math.abs(Math.sin(a * 10)) * 0.05;
verts.push(Math.cos(a) * rr, yy, Math.sin(a) * rr);
norms.push(Math.cos(a), 0, Math.sin(a)); // geçici; computeVertexNormals düzeltecek
uvs.push(i / RADIAL, t);
}
}
for (let j=0;j<PROFILE;j++){
for (let i=0;i<RADIAL;i++){
const a0 = j*(RADIAL+1) + i;
const b0 = a0 + RADIAL + 1;
idx.push(a0, b0, a0+1, a0+1, b0, b0+1);
}
}
const g = new THREE.BufferGeometry();
g.setAttribute('position', new THREE.Float32BufferAttribute(verts, 3));
g.setAttribute('normal', new THREE.Float32BufferAttribute(norms, 3));
g.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
g.setIndex(idx);
g.computeVertexNormals();
return g;
}
const bellGeo = makeBellGeometry();
// iç ışık çekirdeği
const coreGeo = new THREE.SphereGeometry(0.42, 16, 12);
// dokunaç + kol eğrileri
function makeTentacleGeo(len, segs, loops){
const pts = [];
for (let i=0;i<=segs;i++){
const t = i/segs;
pts.push(new THREE.Vector3(
Math.sin(t*6.0)*0.10,
-t*len,
Math.cos(t*5.0)*0.10
));
}
const curve = new THREE.CatmullRomCurve3(pts);
return new THREE.TubeGeometry(curve, segs, 0.017, 5, false);
}
const jellies = [];
const MAX_JELLIES = 24;
function createJelly(i){
const group = new THREE.Group();
const color = PALETTE[i % PALETTE.length];
const bellMat = new THREE.MeshPhysicalMaterial({
color, transparent:true, opacity:0.26,
roughness:0.18, metalness:0.0,
transmission:0.72, thickness:0.5,
emissive:new THREE.Color(color), emissiveIntensity:1.1,
side:THREE.DoubleSide, depthWrite:false
});
const bell = new THREE.Mesh(bellGeo, bellMat);
bell.scale.setScalar(1);
group.add(bell);
const coreMat = new THREE.MeshBasicMaterial({
color:new THREE.Color(color).lerp(new THREE.Color(0xffffff), 0.45),
transparent:true, opacity:0.55, blending:THREE.AdditiveBlending, depthWrite:false
});
const core = new THREE.Mesh(coreGeo, coreMat);
core.position.y = -0.05;
core.scale.set(1, 0.7, 1);
group.add(core);
// uzun dokunaçlar
const tentacles = [];
const tentCount = 10;
for (let t=0;t<tentCount;t++){
const a = (t/tentCount) * Math.PI*2;
const len = 2.2 + Math.random()*2.4;
const geo = makeTentacleGeo(len, 26);
const mat = new THREE.MeshBasicMaterial({
color, transparent:true, opacity:0.30, depthWrite:false
});
const m = new THREE.Mesh(geo, mat);
m.position.set(Math.cos(a)*0.55, -0.15, Math.sin(a)*0.55);
m.userData = { phase: Math.random()*Math.PI*2, baseX: Math.cos(a)*0.55, baseZ: Math.sin(a)*0.55 };
group.add(m);
tentacles.push(m);
}
// kısa, kalın kollar
for (let t=0;t<4;t++){
const a = (t/4)*Math.PI*2 + Math.PI/4;
const geo = makeTentacleGeo(0.75 + Math.random()*0.4, 12);
const mat = new THREE.MeshBasicMaterial({ color, transparent:true, opacity:0.42, depthWrite:false });
const m = new THREE.Mesh(geo, mat);
m.position.set(Math.cos(a)*0.3, -0.12, Math.sin(a)*0.3);
m.userData = { phase: Math.random()*Math.PI*2, baseX: Math.cos(a)*0.3, baseZ: Math.sin(a)*0.3 };
group.add(m);
tentacles.push(m);
}
const scale = 0.55 + Math.random()*0.95;
group.scale.setScalar(scale);
// altın oran dağıtımı: sahne her yöne dengeli yayılır (sağa yığılma olmaz)
const ang = i * 2.399963 + Math.random()*0.55;
const rad = 5.5 + Math.random()*11;
// yükseklik katmanlara bölünür: altta ağır, ortada yoğun, üstte seyrek
// NUMEX-DÜZELTME (2026-10-27): alt katman ekranın dibinden kesiliyordu; taban yükseltildi.
const yLayer = Math.random();
const yPos = yLayer < 0.35 ? -1.6 + Math.random()*1.8
: yLayer < 0.85 ? 0.4 + Math.random()*3.8
: 3.6 + Math.random()*2.8;
group.position.set(Math.cos(ang)*rad, yPos, Math.sin(ang)*rad);
scene.add(group);
const j = {
group, bell, bellMat, core, coreMat, tentacles, scale,
colorIdx: i % PALETTE.length,
pulse: 0, pulseSpeed: 0.9 + Math.random()*0.7,
phase: Math.random()*Math.PI*2,
driftAngle: ang, driftRad: rad,
driftSpeed: 0.05 + Math.random()*0.10,
yBase: group.position.y,
yAmp: 0.5 + Math.random()*0.9,
rotSpeed: (Math.random()-0.5)*0.25,
active: true
};
jellies.push(j);
return j;
}
for (let i=0;i<MAX_JELLIES;i++) createJelly(i);
let activeCount = 12;
jellies.forEach((j,i) => {
j.active = i < activeCount;
j.group.visible = j.active;
});
/* ═════════ biyolüminesans parçacıklar ═════════ */
const SPARK_COUNT = 1800;
const sparkGeo = new THREE.BufferGeometry();
const sparkPos = new Float32Array(SPARK_COUNT*3);
const sparkSeed = new Float32Array(SPARK_COUNT);
const sparkSpeed = new Float32Array(SPARK_COUNT);
for (let i=0;i<SPARK_COUNT;i++){
sparkPos[i*3] = (Math.random()-0.5)*54;
sparkPos[i*3+1] = (Math.random()-0.5)*30;
sparkPos[i*3+2] = (Math.random()-0.5)*54;
sparkSeed[i] = Math.random()*Math.PI*2;
sparkSpeed[i] = 0.08 + Math.random()*0.22;
}
sparkGeo.setAttribute('position', new THREE.BufferAttribute(sparkPos, 3));
// yumuşak yuvarlak nokta dokusu
function makeDotTexture(){
const s = 64, c = document.createElement('canvas'); c.width = c.height = s;
const ctx = c.getContext('2d');
const grd = ctx.createRadialGradient(s/2, s/2, 0, s/2, s/2, s/2);
grd.addColorStop(0, 'rgba(255,255,255,1)');
grd.addColorStop(0.35, 'rgba(180,240,255,.55)');
grd.addColorStop(1, 'rgba(120,200,255,0)');
ctx.fillStyle = grd; ctx.fillRect(0,0,s,s);
return new THREE.CanvasTexture(c);
}
const sparkMat = new THREE.PointsMaterial({
size: 0.16, map: makeDotTexture(), transparent:true, opacity:0.85,
blending: THREE.AdditiveBlending, depthWrite:false, sizeAttenuation:true,
color: 0x9fe8ff, fog:false
});
const sparks = new THREE.Points(sparkGeo, sparkMat);
scene.add(sparks);
/* ═════════ deniz tabanı — kum ve kayalar ═════════ */
// NUMEX-DÜZELTME (2026-10-27): düz daire zemin, sisin de yardımıyla ekranın altında dev siyah bir kubbe gibi
// görünüyordu. Zemin artık kenara doğru koyulaşıp sise karışan yumuşak bir gölgelendiriciyle çizilir.
const floorMat = new THREE.ShaderMaterial({
uniforms:{ uCenter:{value:new THREE.Color(0x0d2136)}, uEdge:{value:new THREE.Color(0x01040c)} },
vertexShader:`varying vec2 vP; void main(){ vP = position.xy; gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0); }`,
fragmentShader:`
varying vec2 vP; uniform vec3 uCenter, uEdge;
void main(){
float d = length(vP) / 40.0;
gl_FragColor = vec4(mix(uCenter, uEdge, smoothstep(0.05, 0.85, d)), 1.0);
}`
});
const floor = new THREE.Mesh(new THREE.CircleGeometry(40, 72), floorMat);
floor.rotation.x = -Math.PI/2;
floor.position.y = -12;
scene.add(floor);
const rockMat = new THREE.MeshStandardMaterial({ color:0x0a1626, roughness:0.95, flatShading:true });
for (let i=0;i<26;i++){
const s = 0.5 + Math.random()*2.2;
const r = new THREE.Mesh(new THREE.DodecahedronGeometry(s, 0), rockMat);
const a = Math.random()*Math.PI*2, d = 6 + Math.random()*26;
r.position.set(Math.cos(a)*d, -12 + s*0.35, Math.sin(a)*d);
r.rotation.set(Math.random(), Math.random()*3, Math.random());
r.scale.y = 0.55;
scene.add(r);
}
/* ═════════ efektler ═════════ */
// NUMEX-DÜZELTME (2026-10-27): mobilde bloom kapatılmıştı; yarı saydam çanlar ve ışık huzmeleri parlamasız
// keskin/köşeli görünüyordu. Mobilde de açılır, yalnız daha hafif ayarla.
let composer = null, bloomPass = null;
{
composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
bloomPass = new UnrealBloomPass(
new THREE.Vector2(innerWidth, innerHeight),
isMobile ? 0.34 : 0.85, // güç
isMobile ? 0.55 : 0.85, // yarıçap
isMobile ? 0.82 : 0.62 // eşik (mobilde yüksek: sadece gerçekten parlak yerler ışısın)
);
composer.addPass(bloomPass);
composer.addPass(new OutputPass());
}
/* ═════════ durum & etkileşim ═════════ */
let swimSpeed = 0.4;
let tourActive = false, tourTime = 0;
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();
let downPos = null;
function ndc(e){
pointer.x = (e.clientX / innerWidth) * 2 - 1;
pointer.y = -(e.clientY / innerHeight) * 2 + 1;
}
// tıklanan denizanasının rengi değişir
function tryJellyPick(e){
ndc(e);
raycaster.setFromCamera(pointer, camera);
const targets = jellies.filter(j => j.active).map(j => j.bell);
const hit = raycaster.intersectObjects(targets, false)[0];
if (!hit) return;
const j = jellies.find(x => x.bell === hit.object);
if (!j) return;
j.colorIdx = (j.colorIdx + 1 + Math.floor(Math.random()*3)) % PALETTE.length;
const c = new THREE.Color(PALETTE[j.colorIdx]);
j.bellMat.color.copy(c);
j.bellMat.emissive.copy(c);
j.coreMat.color.copy(c).lerp(new THREE.Color(0xffffff), 0.45);
j.tentacles.forEach(t => t.material.color.copy(c));
j.pulse = 1.15; // dokununca bir kez güçlü atış
}
canvas.addEventListener('pointerdown', e => { downPos = { x:e.clientX, y:e.clientY, t:performance.now() }; });
canvas.addEventListener('pointerup', e => {
if (!downPos) return;
const moved = Math.hypot(e.clientX-downPos.x, e.clientY-downPos.y);
const dur = performance.now() - downPos.t;
downPos = null;
if (moved < 8 && dur < 450) tryJellyPick(e);
});
const $ = s => document.querySelector(s);
const veil = $('#veil');
const hint = $('#hint');
$('#glow').addEventListener('input', e => { glowState.intensity = e.target.value / 100; });
$('#speed').addEventListener('input', e => { swimSpeed = e.target.value / 100; });
$('#count').addEventListener('input', e => {
activeCount = parseInt(e.target.value, 10);
jellies.forEach((j, i) => { j.active = i < activeCount; j.group.visible = j.active; });
});
$('#tourBtn').addEventListener('click', () => {
tourActive = !tourActive;
$('#tourBtn').classList.toggle('on', tourActive);
document.querySelector('#tourBtn').lastChild.textContent = tourActive ? 'Durdur' : 'Tur';
if (tourActive) tourTime = 0;
});
$('#resetBtn').addEventListener('click', () => {
tourActive = false;
const b = $('#tourBtn');
b.classList.remove('on');
b.lastChild.textContent = 'Tur';
camera.position.copy(CAM_START);
controls.target.set(0, 0.4, 0);
controls.update();
});
$('#toggle').addEventListener('click', () => $('#panel').classList.toggle('hidden'));
setTimeout(() => hint.classList.add('gone'), 9000);
/* ═════════ sinematik tur yolu ═════════ */
const TOUR = [
new THREE.Vector3(0, 1.2, 15),
new THREE.Vector3(-9, 4.5, 9),
new THREE.Vector3(-3, -3.5, 10),
new THREE.Vector3(8, 2.0, 9),
new THREE.Vector3(4, 6.5, -8),
new THREE.Vector3(-6, -1.5, -9),
new THREE.Vector3(0, 2.5, 13)
];
const tourCurve = new THREE.CatmullRomCurve3(TOUR, true, 'catmullrom', 0.4);
/* ═════════ döngü ═════════ */
const clock = new THREE.Clock();
const tmpV = new THREE.Vector3();
function animate(){
requestAnimationFrame(animate);
const dt = Math.min(clock.getDelta(), 0.05);
const t = clock.elapsedTime;
const glow = glowState.intensity;
// sinematik tur
if (tourActive){
tourTime += dt * 0.035;
const pos = tourCurve.getPointAt(tourTime % 1);
camera.position.lerp(pos, 1 - Math.pow(0.002, dt));
// bakış hedefi yumuşak süzülür
tmpV.set(Math.sin(t*0.35)*1.6, Math.sin(t*0.27)*0.9, Math.cos(t*0.31)*1.6);
controls.target.lerp(tmpV, 1 - Math.pow(0.02, dt));
}
controls.update();
// denizanaları
for (let i=0;i<jellies.length;i++){
const j = jellies[i];
if (!j.active) continue;
j.phase += dt * j.pulseSpeed * (0.5 + swimSpeed * 1.6);
// atış: 0..1 arası yumuşak nabız
const beat = Math.sin(j.phase);
const beatSharp = Math.pow(Math.max(0, beat), 2.0);
j.pulse = Math.max(0, j.pulse - dt * 1.6);
const sx = 1 + beatSharp * 0.16 + j.pulse * 0.12;
const sy = 1 - beatSharp * 0.20 - j.pulse * 0.10;
j.bell.scale.set(sx, sy, sx);
j.core.scale.set(1 + beatSharp*0.2, 0.7 * (1 - beatSharp*0.15), 1 + beatSharp*0.2);
j.coreMat.opacity = 0.35 + beatSharp * 0.45 + j.pulse * 0.3;
j.bellMat.emissiveIntensity = (0.55 + beatSharp * 0.85 + j.pulse * 0.8) * (0.35 + glow * 1.4);
// süzülme
j.driftAngle += dt * j.driftSpeed * (0.3 + swimSpeed * 2.0);
j.group.position.x = Math.cos(j.driftAngle) * j.driftRad;
j.group.position.z = Math.sin(j.driftAngle) * j.driftRad;
j.group.position.y = j.yBase + Math.sin(t * 0.55 + j.phase * 0.6) * j.yAmp;
j.group.rotation.y += dt * j.rotSpeed;
j.group.rotation.z = Math.sin(t * 0.4 + j.phase) * 0.10;
// dokunaç dalgalanması — gövde atışıyla senkron
const sway = beatSharp * 0.22;
for (let k=0;k<j.tentacles.length;k++){
const m = j.tentacles[k];
const ud = m.userData;
m.rotation.x = Math.sin(t * 1.4 + ud.phase) * 0.22 + sway;
m.rotation.z = Math.cos(t * 1.15 + ud.phase * 1.3) * 0.20;
m.position.x = ud.baseX * (1 + beatSharp * 0.08);
m.position.z = ud.baseZ * (1 + beatSharp * 0.08);
m.material.opacity = (0.18 + beatSharp * 0.22) * (0.5 + glow);
}
}
// biyolüminesans parçacıklar
const sp = sparkGeo.attributes.position.array;
for (let i=0;i<SPARK_COUNT;i++){
const iy = i*3+1;
sp[iy] += sparkSpeed[i] * dt * (0.4 + swimSpeed * 2.2);
if (sp[iy] > 15){ sp[iy] = -15; sp[i*3] = (Math.random()-0.5)*54; sp[i*3+2] = (Math.random()-0.5)*54; }
sp[i*3] += Math.sin(t*0.6 + sparkSeed[i]) * dt * 0.10;
sp[i*3+2] += Math.cos(t*0.5 + sparkSeed[i]) * dt * 0.10;
}
sparkGeo.attributes.position.needsUpdate = true;
sparkMat.opacity = (0.25 + glow * 0.75) * 0.95;
sparkMat.size = 0.10 + glow * 0.14;
// ışık huzmeleri hafifçe salınır
beamGroup.children.forEach((m, i) => {
m.material.opacity = (0.02 + glow * 0.05) * (0.7 + Math.sin(t * 0.4 + i) * 0.3);
m.rotation.y += dt * 0.015;
});
// ışık şiddeti
rim.intensity = 10 + glow * 30;
key.intensity = 0.30 + glow * 0.9;
hemi.intensity = 0.25 + glow * 0.45;
ambientFill.intensity = 0.35 + glow * 0.6;
renderer.toneMappingExposure = 0.85 + glow * 0.5;
if (bloomPass) bloomPass.strength = 0.45 + glow * 0.85;
// panel/başlık kararması — tur sırasında sadeleş
document.querySelectorAll('#title,#hint').forEach(el => {
el.style.opacity = tourActive ? '0.25' : '';
});
if (composer) composer.render(); else renderer.render(scene, camera);
}
addEventListener('resize', () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
if (composer) composer.setSize(innerWidth, innerHeight);
});
// perdеу kaldır
requestAnimationFrame(() => {
animate();
setTimeout(() => veil.classList.add('gone'), 450);
});
// ── ölçüm köprüsü: denizanası renginin tıklamayla gerçekten değiştiğini dışarıdan okumak için ──
// (yalnız okuma; sahneye/etkileşime karışmaz)
window.__deepblue = {
jellyState(i){
const j = jellies[i];
if (!j) return null;
return { colorIdx: j.colorIdx, hex: '#' + j.bellMat.color.getHexString() };
},
// en büyük görünür denizanasının ekran koordinatı (tıklama testi için)
screenOf(i){
const j = jellies[i];
if (!j) return null;
const v = j.bell.getWorldPosition(new THREE.Vector3()).project(camera);
return {
x: (v.x * 0.5 + 0.5) * innerWidth,
y: (-v.y * 0.5 + 0.5) * innerHeight,
onScreen: Math.abs(v.x) < 1 && Math.abs(v.y) < 1
};
}
};
</script>
</body>
</html>