740 lines
30 KiB
HTML
740 lines
30 KiB
HTML
<!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 · tekerlek: yakınlaş · 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>
|