982 lines
40 KiB
HTML
982 lines
40 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 Zen Garden — Biraz huzur, biraz teknoloji</title>
|
||
<meta name="description" content="Kodla üretilmiş interaktif Japon bahçesi: sakura, koi göleti, sinematik kamera turu." />
|
||
<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=Shippori+Mincho:wght@400;600&family=Inter:wght@300;400;500&display=swap" rel="stylesheet" />
|
||
<style>
|
||
:root{
|
||
--ink: #f6efe8;
|
||
--ink-dim: rgba(246,239,232,.62);
|
||
--panel: rgba(22,18,24,.42);
|
||
--panel-brd: rgba(255,255,255,.14);
|
||
--accent: #ffb7c5;
|
||
--accent-2: #8fd3c7;
|
||
}
|
||
*{ box-sizing:border-box; -webkit-tap-highlight-color:transparent; }
|
||
html,body{ height:100%; margin:0; }
|
||
body{
|
||
background:#0b0a10; color:var(--ink);
|
||
font-family:'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif;
|
||
overflow:hidden;
|
||
touch-action:none;
|
||
}
|
||
#scene{ position:fixed; inset:0; display:block; width:100%; height:100%; }
|
||
#veil{
|
||
position:fixed; inset:0; background:#0b0a10;
|
||
display:flex; align-items:center; justify-content:center; flex-direction:column; gap:14px;
|
||
z-index:40; transition:opacity .9s ease; letter-spacing:.28em; font-size:12px; color:var(--ink-dim);
|
||
}
|
||
#veil.gone{ opacity:0; pointer-events:none; }
|
||
#veil .bar{ width:120px; height:1px; background:rgba(255,255,255,.15); overflow:hidden; }
|
||
#veil .bar i{ display:block; height:100%; width:40%; background:var(--accent); animation:slide 1.2s ease-in-out infinite; }
|
||
@keyframes slide{ 0%{transform:translateX(-100%)} 100%{transform:translateX(300%)} }
|
||
|
||
/* ---- Başlık ---- */
|
||
#title{
|
||
position:fixed; top:clamp(14px,3vh,32px); left:0; right:0; text-align:center;
|
||
pointer-events:none; z-index:10; transition:opacity .6s ease;
|
||
}
|
||
#title h1{
|
||
font-family:'Shippori Mincho', serif; font-weight:400;
|
||
font-size:clamp(19px, 3.4vw, 40px); margin:0; letter-spacing:.18em;
|
||
text-shadow:0 1px 2px rgba(0,0,0,.85), 0 2px 12px rgba(0,0,0,.75), 0 0 46px rgba(0,0,0,.6);
|
||
}
|
||
#title p{
|
||
margin:6px 0 0; font-size:clamp(9px,1.3vw,11px); letter-spacing:.42em;
|
||
text-transform:uppercase; color:var(--ink-dim); font-weight:300;
|
||
}
|
||
|
||
/* ---- Kontrol paneli ---- */
|
||
#panel{
|
||
position:fixed; z-index:20;
|
||
left:clamp(12px, 1.8vw, 26px); bottom:clamp(12px, 2.6vh, 26px);
|
||
width:min(560px, calc(100vw - 24px));
|
||
background:var(--panel); border:1px solid var(--panel-brd); border-radius:18px;
|
||
backdrop-filter:blur(18px) saturate(1.2); -webkit-backdrop-filter:blur(18px) saturate(1.2);
|
||
box-shadow:0 18px 50px rgba(0,0,0,.42);
|
||
padding:10px clamp(10px,1.6vw,16px);
|
||
display:flex; flex-wrap:wrap; align-items:center; justify-content:center;
|
||
gap:6px 8px;
|
||
transition:opacity .5s ease, transform .5s ease;
|
||
}
|
||
/* NUMEX-DÜZELTME (2026-10-27): panel left ile konumlanıyor ama gizlenirken translate(-50%) uygulanıyordu;
|
||
konumlandırma ile transform uyumsuzdu, taşma hâlinde ilk öğe ("AN") panelin dışında kalıyordu. */
|
||
#panel.hidden{ opacity:0; pointer-events:none; transform:translateY(120%); }
|
||
.grp{ display:flex; align-items:center; gap:5px; padding:0 6px; }
|
||
.grp + .grp{ border-left:1px solid rgba(255,255,255,.10); }
|
||
.lbl{ font-size:9px; letter-spacing:.18em; text-transform:uppercase; color:var(--ink-dim); margin-right:2px; }
|
||
button{
|
||
font-family:inherit; font-size:11px; font-weight:500; color:var(--ink);
|
||
background:rgba(255,255,255,.055); border:1px solid rgba(255,255,255,.10);
|
||
border-radius:11px; padding:0 11px; 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(255,255,255,.12); border-color:rgba(255,255,255,.22); }
|
||
button:active{ transform:scale(.96); }
|
||
button.on{ background:rgba(255,183,197,.20); border-color:rgba(255,183,197,.55); color:#ffe3e9; }
|
||
button svg{ width:14px; height:14px; }
|
||
input[type=range]{
|
||
-webkit-appearance:none; appearance:none; width:clamp(58px,9vw,92px); height:22px; background:transparent; cursor:pointer;
|
||
}
|
||
input[type=range]::-webkit-slider-runnable-track{ height:3px; border-radius:3px; background:rgba(255,255,255,.20); }
|
||
input[type=range]::-webkit-slider-thumb{
|
||
-webkit-appearance:none; width:13px; height:13px; border-radius:50%; background:var(--accent);
|
||
margin-top:-5px; box-shadow:0 0 12px rgba(255,183,197,.7); border:none;
|
||
}
|
||
input[type=range]::-moz-range-track{ height:3px; border-radius:3px; background:rgba(255,255,255,.20); }
|
||
input[type=range]::-moz-range-thumb{ width:13px; height:13px; border:none; border-radius:50%; background:var(--accent); }
|
||
|
||
/* ---- İpucu ---- */
|
||
#hint{
|
||
position:fixed; z-index:15; left:50%; transform:translateX(-50%);
|
||
top:clamp(90px, 16vh, 150px);
|
||
font-size:10.5px; letter-spacing:.14em; color:rgba(255,255,255,.5);
|
||
text-align:center; pointer-events:none; transition:opacity 1.2s ease; padding:0 12px;
|
||
}
|
||
#hint.gone{ opacity:0; }
|
||
|
||
#toggle{
|
||
position:fixed; z-index:25; right:clamp(10px,2vw,20px); bottom:clamp(12px,2.6vh,26px);
|
||
width:40px; height:40px; border-radius:50%; padding:0;
|
||
background:rgba(22,18,24,.5); backdrop-filter:blur(14px);
|
||
}
|
||
@media (max-width: 620px){
|
||
.lbl{ display:none; }
|
||
.grp{ padding:0 3px; gap:4px; flex:0 0 auto; }
|
||
.grp:first-child{ flex-basis:100%; justify-content:center; }
|
||
input[type=range]{ width:clamp(48px,17vw,90px); }
|
||
button{ font-size:10px; padding:0 8px; height:32px; border-radius:10px; }
|
||
#panel{ gap:5px 4px; padding:8px 8px; border-radius:16px; }
|
||
#hint{ font-size:9px; top:clamp(78px, 13vh, 120px); padding:0 20px; line-height:1.6; }
|
||
#title p{ letter-spacing:.22em; font-size:8.5px; padding:0 18px; line-height:1.7; }
|
||
#panel{ justify-content:flex-start; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<canvas id="scene"></canvas>
|
||
|
||
<div id="veil"><span>NUMEX ZEN GARDEN</span><div class="bar"><i></i></div></div>
|
||
|
||
<div id="title">
|
||
<h1>ZEN BAHÇESİ</h1>
|
||
<p>Numex Zen Garden — biraz huzur, biraz teknoloji</p>
|
||
</div>
|
||
|
||
<div id="hint">sürükle: bak · tekerlek / iki parmak: yakınlaş · yapraklara dokun</div>
|
||
|
||
<div id="panel">
|
||
<div class="grp" id="timeGrp">
|
||
<span class="lbl">An</span>
|
||
<button data-time="morning">Sabah</button>
|
||
<button data-time="sunset" class="on">Gün Batımı</button>
|
||
<button data-time="night">Gece</button>
|
||
</div>
|
||
<div class="grp">
|
||
<span class="lbl">Rüzgâr</span>
|
||
<input id="wind" type="range" min="0" max="100" value="45" aria-label="Rüzgâr" />
|
||
</div>
|
||
<div class="grp">
|
||
<span class="lbl">Yaprak</span>
|
||
<input id="density" type="range" min="10" max="100" value="60" aria-label="Yaprak yoğunluğu" />
|
||
</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">
|
||
<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">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" width="16" height="16"><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';
|
||
|
||
/* ═══════════════ TEMEL ═══════════════ */
|
||
const canvas = document.getElementById('scene');
|
||
const renderer = new THREE.WebGLRenderer({ canvas, antialias:true, powerPreference:'high-performance' });
|
||
const isMobile = matchMedia('(max-width: 820px)').matches || /Android|iPhone|iPad/i.test(navigator.userAgent);
|
||
renderer.setPixelRatio(Math.min(devicePixelRatio, isMobile ? 1.6 : 2));
|
||
renderer.setSize(innerWidth, innerHeight);
|
||
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||
renderer.toneMappingExposure = 1.05;
|
||
renderer.shadowMap.enabled = true;
|
||
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||
|
||
const scene = new THREE.Scene();
|
||
scene.fog = new THREE.FogExp2(0x1b1b2b, 0.022);
|
||
|
||
const camera = new THREE.PerspectiveCamera(48, innerWidth/innerHeight, 0.1, 400);
|
||
camera.position.set(9.4, 6.6, 14.8);
|
||
|
||
const controls = new OrbitControls(camera, canvas);
|
||
controls.target.set(0, 3.4, -0.4);
|
||
controls.enableDamping = true;
|
||
controls.dampingFactor = 0.055;
|
||
controls.minDistance = 5;
|
||
controls.maxDistance = 26;
|
||
controls.maxPolarAngle = Math.PI * 0.495;
|
||
controls.minPolarAngle = 0.35;
|
||
controls.enablePan = false;
|
||
controls.rotateSpeed = 0.55;
|
||
controls.zoomSpeed = 0.7;
|
||
controls.autoRotateSpeed = 0.28;
|
||
controls.update();
|
||
|
||
/* ═══════════════ GÖKYÜZÜ (shader) ═══════════════ */
|
||
const skyUniforms = {
|
||
topColor: { value: new THREE.Color(0x2a5c8a) },
|
||
midColor: { value: new THREE.Color(0xd98a6a) },
|
||
botColor: { value: new THREE.Color(0xf6c88a) },
|
||
sunDir: { value: new THREE.Vector3(-0.5, 0.18, -0.85).normalize() },
|
||
sunColor: { value: new THREE.Color(0xffd9a0) },
|
||
sunI: { value: 1.0 }
|
||
};
|
||
const skyGeo = new THREE.SphereGeometry(160, 32, 20);
|
||
const skyMat = new THREE.ShaderMaterial({
|
||
side: THREE.BackSide, depthWrite:false, uniforms: skyUniforms,
|
||
vertexShader:`varying vec3 vP; void main(){ vP = position; gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0); }`,
|
||
fragmentShader:`
|
||
varying vec3 vP;
|
||
uniform vec3 topColor, midColor, botColor, sunDir, sunColor; uniform float sunI;
|
||
void main(){
|
||
vec3 d = normalize(vP);
|
||
float h = clamp(d.y * 0.5 + 0.5, 0.0, 1.0);
|
||
vec3 c = mix(botColor, midColor, smoothstep(0.35, 0.56, h));
|
||
c = mix(c, topColor, smoothstep(0.52, 0.95, h));
|
||
float s = max(dot(d, normalize(sunDir)), 0.0);
|
||
c += sunColor * pow(s, 26.0) * 2.2 * sunI;
|
||
c += sunColor * pow(s, 3.0) * 0.20 * sunI;
|
||
gl_FragColor = vec4(c, 1.0);
|
||
}`
|
||
});
|
||
const sky = new THREE.Mesh(skyGeo, skyMat);
|
||
scene.add(sky);
|
||
|
||
/* ═══════════════ IŞIKLAR ═══════════════ */
|
||
const hemi = new THREE.HemisphereLight(0xbfd8ff, 0x3a2f28, 0.85);
|
||
scene.add(hemi);
|
||
|
||
const sun = new THREE.DirectionalLight(0xffd9a8, 2.4);
|
||
sun.position.set(-14, 12, -16);
|
||
sun.castShadow = true;
|
||
sun.shadow.mapSize.set(isMobile ? 1024 : 2048, isMobile ? 1024 : 2048);
|
||
sun.shadow.camera.near = 1; sun.shadow.camera.far = 60;
|
||
sun.shadow.camera.left = -18; sun.shadow.camera.right = 18;
|
||
sun.shadow.camera.top = 18; sun.shadow.camera.bottom = -18;
|
||
sun.shadow.bias = -0.0009;
|
||
sun.shadow.radius = 3;
|
||
scene.add(sun);
|
||
|
||
const fillLight = new THREE.DirectionalLight(0x7fa8ff, 0.25);
|
||
fillLight.position.set(8, 6, 10);
|
||
scene.add(fillLight);
|
||
|
||
/* ═══════════════ ZEMİN ═══════════════ */
|
||
const groundMat = new THREE.MeshStandardMaterial({ color:0x5c6b45, roughness:1, metalness:0 });
|
||
const ground = new THREE.Mesh(new THREE.CircleGeometry(46, 64), groundMat);
|
||
ground.rotation.x = -Math.PI/2;
|
||
ground.receiveShadow = true;
|
||
scene.add(ground);
|
||
|
||
// çakıl / kum doku — prosedürel
|
||
function makeNoiseTexture(size, base, spread){
|
||
const c = document.createElement('canvas'); c.width = c.height = size;
|
||
const ctx = c.getContext('2d');
|
||
const img = ctx.createImageData(size, size);
|
||
for (let i=0;i<size*size;i++){
|
||
const n = (Math.random()-0.5)*spread;
|
||
img.data[i*4] = Math.max(0, Math.min(255, base[0]+n));
|
||
img.data[i*4+1] = Math.max(0, Math.min(255, base[1]+n));
|
||
img.data[i*4+2] = Math.max(0, Math.min(255, base[2]+n));
|
||
img.data[i*4+3] = 255;
|
||
}
|
||
ctx.putImageData(img,0,0);
|
||
const t = new THREE.CanvasTexture(c);
|
||
t.wrapS = t.wrapT = THREE.RepeatWrapping;
|
||
return t;
|
||
}
|
||
const mossTex = makeNoiseTexture(256,[96,112,72],46);
|
||
mossTex.repeat.set(26,26);
|
||
groundMat.map = mossTex;
|
||
groundMat.needsUpdate = true;
|
||
|
||
/* ═══════════════ GÖLET ═══════════════ */
|
||
const POND = { x: 0, z: 3.4, r: 4.6 };
|
||
const waterUniforms = {
|
||
uTime:{ value:0 },
|
||
uDeep:{ value:new THREE.Color(0x0c2c3a) },
|
||
uShallow:{ value:new THREE.Color(0x3f8f9c) },
|
||
uSky:{ value:new THREE.Color(0xd9a07a) },
|
||
uSun:{ value:new THREE.Color(0xffd9a0) },
|
||
uRefl:{ value:0.55 }
|
||
};
|
||
const waterGeo = new THREE.CircleGeometry(POND.r, 96);
|
||
const waterMat = new THREE.ShaderMaterial({
|
||
transparent:true, uniforms: waterUniforms,
|
||
vertexShader:`
|
||
varying vec2 vUv; varying vec3 vPos;
|
||
void main(){ vUv = uv; vPos = position; gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0); }`,
|
||
fragmentShader:`
|
||
varying vec2 vUv; varying vec3 vPos;
|
||
uniform float uTime, uRefl; uniform vec3 uDeep, uShallow, uSky, uSun;
|
||
void main(){
|
||
vec2 p = vPos.xy;
|
||
float w = sin(p.x*1.7 + uTime*0.9)*0.5 + sin(p.y*2.3 - uTime*1.1)*0.5
|
||
+ sin((p.x+p.y)*3.1 + uTime*1.7)*0.28 + sin(length(p)*3.4 - uTime*1.4)*0.35;
|
||
vec3 col = mix(uDeep, uShallow, 0.45 + 0.35*w);
|
||
// gökyüzü yansıması — merkezden uzağa doğru artar (fresnel benzeri)
|
||
float d = length(vUv - 0.5) * 2.0;
|
||
col = mix(col, uSky, clamp(d*0.55, 0.0, 1.0) * uRefl);
|
||
// güneş parıltısı
|
||
float glint = pow(max(0.0, w), 8.0);
|
||
col += uSun * glint * 0.55;
|
||
float a = 0.80 + 0.14*w;
|
||
gl_FragColor = vec4(col, a);
|
||
}`
|
||
});
|
||
// yıldızlar — gece görünür, gündüz söner
|
||
const starGeo = new THREE.BufferGeometry();
|
||
{
|
||
const N = 900, arr = new Float32Array(N*3);
|
||
for (let i=0;i<N;i++){
|
||
const r = 150 + Math.random()*90;
|
||
const th = Math.random()*Math.PI*2;
|
||
const ph = Math.acos(Math.random()*0.92); // ufuk üstü ağırlıklı
|
||
arr[i*3] = Math.sin(ph)*Math.cos(th)*r;
|
||
arr[i*3+1] = Math.cos(ph)*r*0.85 + 14;
|
||
arr[i*3+2] = Math.sin(ph)*Math.sin(th)*r;
|
||
}
|
||
starGeo.setAttribute('position', new THREE.BufferAttribute(arr, 3));
|
||
}
|
||
const starMat = new THREE.PointsMaterial({
|
||
color:0xdfe9ff, size:1.5, sizeAttenuation:true,
|
||
transparent:true, opacity:0, depthWrite:false, fog:false
|
||
});
|
||
const stars = new THREE.Points(starGeo, starMat);
|
||
scene.add(stars);
|
||
|
||
// uzak tepe siluetleri — ufka derinlik
|
||
(function addHills(){
|
||
// NUMEX-DÜZELTME (2026-10-27): tepeler çok soluktu ve ufuk sert düz bir çizgi gibi görünüyordu;
|
||
// yükseklik artırıldı, opaklık yükseltildi, en yakın katman hizalandı.
|
||
const layers = [
|
||
{ r:44, h:9.0, col:0x6d8471, op:0.72, seg:11 },
|
||
{ r:58, h:14.5, col:0x4e6759, op:0.60, seg:9 },
|
||
{ r:76, h:21.0, col:0x3a4f46, op:0.46, seg:7 }
|
||
];
|
||
// NUMEX-DÜZELTME (2026-10-27): Ayrık düzlem dilimleri keskin dikdörtgen ufuk yaratıyordu; kesintisiz dalgalı silindir kuşakla değiştirildi.
|
||
layers.forEach((L, li) => {
|
||
// her katman: kesintisiz, yumuşak dalgalanan bir silindir kuşak
|
||
const segs = 160, rings = 6;
|
||
const g = new THREE.CylinderGeometry(L.r, L.r, 1, segs, rings, true);
|
||
const pos = g.attributes.position;
|
||
const ph1 = Math.random()*10, ph2 = Math.random()*10;
|
||
for (let i=0;i<pos.count;i++){
|
||
const x = pos.getX(i), y = pos.getY(i), z = pos.getZ(i);
|
||
const ang = Math.atan2(z, x);
|
||
// tepe profili: birkaç sinüsün toplamı — ancak yumuşak, keskin ek yok
|
||
const prof = 0.55
|
||
+ Math.sin(ang*2.0 + ph1)*0.26
|
||
+ Math.sin(ang*5.0 + ph2)*0.13
|
||
+ Math.sin(ang*9.0 + ph1*2.0)*0.06;
|
||
const h = L.h * Math.max(0.12, prof);
|
||
// y ∈ [-0.5, 0.5]; üst yarı tepe, alt yarı yere gömülü
|
||
pos.setY(i, (y + 0.5) * h - 1.2);
|
||
}
|
||
g.computeVertexNormals();
|
||
const m = new THREE.Mesh(g, new THREE.MeshBasicMaterial({
|
||
color:L.col, transparent:true, opacity:L.op, depthWrite:false,
|
||
side:THREE.DoubleSide, fog:true
|
||
}));
|
||
m.position.set(0, 0, 0);
|
||
m.rotation.y = li * 0.7;
|
||
scene.add(m);
|
||
});
|
||
})();
|
||
|
||
const water = new THREE.Mesh(waterGeo, waterMat);
|
||
water.rotation.x = -Math.PI/2;
|
||
water.position.set(POND.x, 0.055, POND.z);
|
||
scene.add(water);
|
||
|
||
// gölet kenarı (taş halka)
|
||
const bankMat = new THREE.MeshStandardMaterial({ color:0x6b6459, roughness:0.95, flatShading:true });
|
||
for (let i=0;i<46;i++){
|
||
const a = (i/46)*Math.PI*2;
|
||
const rr = POND.r + 0.10 + Math.random()*0.22;
|
||
const s = 0.34 + Math.random()*0.3;
|
||
const rock = new THREE.Mesh(new THREE.DodecahedronGeometry(s, 0), bankMat);
|
||
rock.position.set(POND.x + Math.cos(a)*rr, s*0.30, POND.z + Math.sin(a)*rr);
|
||
rock.rotation.set(Math.random(), Math.random()*3, Math.random());
|
||
rock.scale.y = 0.6;
|
||
rock.castShadow = true; rock.receiveShadow = true;
|
||
scene.add(rock);
|
||
}
|
||
|
||
/* ═══════════════ SAKURA AĞACI ═══════════════ */
|
||
const tree = new THREE.Group();
|
||
tree.position.set(-0.3, 0, -3.2);
|
||
scene.add(tree);
|
||
|
||
const barkMat = new THREE.MeshStandardMaterial({ color:0x4a3a34, roughness:0.94, flatShading:true });
|
||
const blossomMats = [0xffc4d4, 0xffb0c6, 0xffd9e4, 0xf9a7bd].map(c =>
|
||
new THREE.MeshStandardMaterial({ color:c, roughness:0.82, flatShading:true, emissive:new THREE.Color(c).multiplyScalar(0.06) })
|
||
);
|
||
|
||
// gövde — hafif kıvrılan segmentler
|
||
function branch(from, dir, len, radius, depth){
|
||
const to = from.clone().add(dir.clone().multiplyScalar(len));
|
||
const mid = from.clone().add(dir.clone().multiplyScalar(len*0.5));
|
||
const curve = new THREE.QuadraticBezierCurve3(
|
||
from,
|
||
mid.clone().add(new THREE.Vector3((Math.random()-0.5)*len*0.28, len*0.08, (Math.random()-0.5)*len*0.28)),
|
||
to
|
||
);
|
||
const geo = new THREE.TubeGeometry(curve, 8, radius, 7, false);
|
||
const m = new THREE.Mesh(geo, barkMat);
|
||
m.castShadow = true; m.receiveShadow = true;
|
||
tree.add(m);
|
||
|
||
if (depth === 0){
|
||
// ince uç dalı çiçeklerin arasından görünsün
|
||
const tip = new THREE.Mesh(
|
||
new THREE.TubeGeometry(curve, 5, radius*0.85, 5, false), barkMat);
|
||
tree.add(tip);
|
||
// çiçekler dala yayılır: küme değil, dizi hâlinde
|
||
const n = 3 + Math.floor(Math.random()*3);
|
||
for (let i=0;i<n;i++){
|
||
const puff = new THREE.Mesh(
|
||
new THREE.IcosahedronGeometry(0.2 + Math.random()*0.26, 1),
|
||
blossomMats[Math.floor(Math.random()*blossomMats.length)]
|
||
);
|
||
const along = curve.getPoint(0.45 + Math.random()*0.55);
|
||
puff.position.copy(along).add(new THREE.Vector3(
|
||
(Math.random()-0.5)*0.95, (Math.random()-0.5)*0.72, (Math.random()-0.5)*0.95
|
||
));
|
||
puff.scale.set(1, 0.8, 1);
|
||
puff.rotation.set(Math.random()*3, Math.random()*3, Math.random()*3);
|
||
puff.castShadow = true;
|
||
tree.add(puff);
|
||
}
|
||
return;
|
||
}
|
||
const kids = depth > 2 ? 2 : (Math.random() > 0.45 ? 3 : 2);
|
||
for (let i=0;i<kids;i++){
|
||
// dallar dışa ve yukarı açılır — taç top gibi toplanmaz
|
||
const spread = 1.15 + depth * 0.28;
|
||
const nd = dir.clone()
|
||
.add(new THREE.Vector3((Math.random()-0.5)*spread, (Math.random()-0.2)*0.55, (Math.random()-0.5)*spread))
|
||
.normalize();
|
||
branch(to, nd, len*(0.66 + Math.random()*0.18), radius*0.62, depth-1);
|
||
}
|
||
}
|
||
branch(new THREE.Vector3(0,0,0), new THREE.Vector3(0.06,1,0.03).normalize(), 2.1, 0.34, 4);
|
||
|
||
// gövde dibi kök yayılımı
|
||
for (let i=0;i<5;i++){
|
||
const a = (i/5)*Math.PI*2 + Math.random();
|
||
const root = new THREE.Mesh(new THREE.CylinderGeometry(0.07, 0.19, 1.1, 6), barkMat);
|
||
root.position.set(Math.cos(a)*0.42, 0.16, Math.sin(a)*0.42);
|
||
root.rotation.z = Math.cos(a)*0.85;
|
||
root.rotation.x = -Math.sin(a)*0.85;
|
||
root.castShadow = true;
|
||
tree.add(root);
|
||
}
|
||
|
||
/* ═══════════════ JAPON KÖPRÜSÜ ═══════════════ */
|
||
const bridge = new THREE.Group();
|
||
// kıyıya oturur: gölet kenarını aşan iki ucu kuru zemine iner
|
||
// kıyıya oturmuş, dışa doğru inen geçiş (iki ucu toprağa basar)
|
||
bridge.position.set(POND.x - 6.2, -0.35, POND.z + 2.4);
|
||
bridge.rotation.y = -0.55;
|
||
bridge.scale.set(1.0, 0.72, 1.0);
|
||
scene.add(bridge);
|
||
|
||
const woodMat = new THREE.MeshStandardMaterial({ color:0x8a4a35, roughness:0.85 });
|
||
const woodDark = new THREE.MeshStandardMaterial({ color:0x63301f, roughness:0.9 });
|
||
|
||
// kavisli güverte — dilim dilim
|
||
const BR_W = 2.0, BR_L = 8.4, seg = 26;
|
||
for (let i=0;i<seg;i++){
|
||
const t = i/(seg-1);
|
||
const x = (t-0.5)*BR_L;
|
||
const y = 0.55 + Math.sin(t*Math.PI)*1.05;
|
||
const plank = new THREE.Mesh(new THREE.BoxGeometry(BR_L/seg*0.92, 0.13, BR_W), i%2 ? woodMat : woodDark);
|
||
plank.position.set(x, y, 0);
|
||
plank.rotation.z = -Math.cos(t*Math.PI)*0.24;
|
||
plank.castShadow = true; plank.receiveShadow = true;
|
||
bridge.add(plank);
|
||
|
||
if (i%5 === 0){
|
||
for (const zSide of [-1, 1]){
|
||
const post = new THREE.Mesh(new THREE.CylinderGeometry(0.075,0.075,1.0,6), woodDark);
|
||
post.position.set(x, y + 0.5, zSide*(BR_W/2 - 0.08));
|
||
post.castShadow = true;
|
||
bridge.add(post);
|
||
const cap = new THREE.Mesh(new THREE.SphereGeometry(0.10, 8, 6), woodMat);
|
||
cap.position.set(x, y + 1.02, zSide*(BR_W/2 - 0.08));
|
||
bridge.add(cap);
|
||
}
|
||
}
|
||
}
|
||
// korkuluk çubukları
|
||
for (const zSide of [-1, 1]){
|
||
for (let i=0;i<seg-1;i++){
|
||
const t0 = i/(seg-1), t1 = (i+1)/(seg-1);
|
||
const y0 = 0.55 + Math.sin(t0*Math.PI)*1.05, y1 = 0.55 + Math.sin(t1*Math.PI)*1.05;
|
||
const rail = new THREE.Mesh(new THREE.BoxGeometry(BR_L/seg*1.02, 0.07, 0.07), woodMat);
|
||
rail.position.set((t0-0.5)*BR_L + BR_L/seg/2, (y0+y1)/2 + 0.95, zSide*(BR_W/2 - 0.08));
|
||
rail.rotation.z = Math.atan2(y1-y0, BR_L/seg);
|
||
bridge.add(rail);
|
||
}
|
||
}
|
||
|
||
/* ═══════════════ TAŞ YÜRÜYÜŞ YOLU ═══════════════ */
|
||
const stoneMat = new THREE.MeshStandardMaterial({ color:0x8e8a80, roughness:0.96, flatShading:true });
|
||
const pathPts = [
|
||
[-9, 8.5], [-7.2, 7.0], [-5.4, 6.2], [-3.8, 5.4],
|
||
[3.4, 7.6], [5.2, 8.6], [7.0, 9.2], [9.2, 9.4]
|
||
];
|
||
pathPts.forEach(([x,z], i) => {
|
||
const s = 0.30 + Math.random()*0.16;
|
||
const st = new THREE.Mesh(new THREE.CylinderGeometry(s, s*1.15, 0.12, 7), stoneMat);
|
||
st.position.set(x, 0.045, z);
|
||
st.rotation.y = Math.random()*3;
|
||
st.scale.z = 0.82;
|
||
st.receiveShadow = true; st.castShadow = true;
|
||
scene.add(st);
|
||
});
|
||
|
||
/* ═══════════════ FENERLER ═══════════════ */
|
||
const lanternLights = [];
|
||
function makeLantern(x, z, scale = 1, lit = true){
|
||
const g = new THREE.Group();
|
||
g.position.set(x, 0, z);
|
||
g.scale.setScalar(scale);
|
||
const stone = new THREE.MeshStandardMaterial({ color:0x9a9689, roughness:0.95, flatShading:true });
|
||
const base = new THREE.Mesh(new THREE.CylinderGeometry(0.28,0.36,0.22,6), stone);
|
||
base.position.y = 0.11; g.add(base);
|
||
const stem = new THREE.Mesh(new THREE.CylinderGeometry(0.10,0.13,0.72,6), stone);
|
||
stem.position.y = 0.58; g.add(stem);
|
||
const plat = new THREE.Mesh(new THREE.CylinderGeometry(0.34,0.26,0.14,6), stone);
|
||
plat.position.y = 1.0; g.add(plat);
|
||
const box = new THREE.Mesh(new THREE.BoxGeometry(0.42,0.42,0.42),
|
||
new THREE.MeshStandardMaterial({ color:0xffe6bf, emissive:0xffbb66, emissiveIntensity: lit?2.2:0.15, roughness:0.6 }));
|
||
box.position.y = 1.29; g.add(box);
|
||
const roof = new THREE.Mesh(new THREE.ConeGeometry(0.44,0.3,6), stone);
|
||
roof.position.y = 1.65; g.add(roof);
|
||
const cap = new THREE.Mesh(new THREE.SphereGeometry(0.09,6,5), stone);
|
||
cap.position.y = 1.85; g.add(cap);
|
||
|
||
g.traverse(o => { if (o.isMesh){ o.castShadow = true; o.receiveShadow = true; } });
|
||
|
||
const light = new THREE.PointLight(0xffb35c, lit ? 9 : 0, 7, 2);
|
||
light.position.set(0, 1.3, 0);
|
||
g.add(light);
|
||
lanternLights.push({ light, box, base: 9 });
|
||
|
||
scene.add(g);
|
||
return g;
|
||
}
|
||
makeLantern(-4.4, 5.6, 1.0);
|
||
makeLantern(4.6, 6.4, 1.0);
|
||
makeLantern(7.4, 0.4, 0.85);
|
||
makeLantern(-6.4, -1.6, 0.85);
|
||
|
||
/* ═══════════════ KOI BALIKLARI ═══════════════ */
|
||
const koi = [];
|
||
const koiColors = [0xff7a2f, 0xfff3e0, 0xe23c2a, 0xffc93c, 0xff9e6b];
|
||
for (let i=0;i<6;i++){
|
||
const body = new THREE.Mesh(
|
||
new THREE.SphereGeometry(0.19, 12, 10),
|
||
new THREE.MeshStandardMaterial({ color: koiColors[i % koiColors.length], roughness:0.55, metalness:0.05 })
|
||
);
|
||
body.scale.set(1, 0.55, 2.1);
|
||
const tail = new THREE.Mesh(
|
||
new THREE.ConeGeometry(0.17, 0.34, 8, 1, true),
|
||
new THREE.MeshStandardMaterial({ color: koiColors[i % koiColors.length], roughness:0.6, side:THREE.DoubleSide })
|
||
);
|
||
tail.rotation.x = Math.PI/2;
|
||
tail.position.z = -0.38;
|
||
tail.scale.set(1, 1, 0.35);
|
||
const fish = new THREE.Group();
|
||
fish.add(body); fish.add(tail);
|
||
fish.position.y = 0.02;
|
||
scene.add(fish);
|
||
koi.push({
|
||
mesh: fish, tail,
|
||
ang: Math.random()*Math.PI*2,
|
||
rad: 0.9 + Math.random()*2.5,
|
||
spd: 0.20 + Math.random()*0.28,
|
||
cy: 0.03 + Math.random()*0.35,
|
||
ph: Math.random()*10
|
||
});
|
||
}
|
||
|
||
/* ═══════════════ SAKURA YAPRAKLARI ═══════════════ */
|
||
const MAX_PETALS = 900;
|
||
const petalGeo = new THREE.PlaneGeometry(0.14, 0.10);
|
||
const petalMat = new THREE.MeshStandardMaterial({
|
||
color:0xffc0d2, roughness:0.75, side:THREE.DoubleSide,
|
||
emissive:0xff9ab5, emissiveIntensity:0.18, transparent:true, opacity:0.96
|
||
});
|
||
const petals = new THREE.InstancedMesh(petalGeo, petalMat, MAX_PETALS);
|
||
petals.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
|
||
petals.castShadow = false;
|
||
scene.add(petals);
|
||
|
||
const P = {
|
||
pos: new Float32Array(MAX_PETALS*3),
|
||
rot: new Float32Array(MAX_PETALS*3),
|
||
vel: new Float32Array(MAX_PETALS*3),
|
||
spin: new Float32Array(MAX_PETALS*3),
|
||
size: new Float32Array(MAX_PETALS),
|
||
alive: new Uint8Array(MAX_PETALS)
|
||
};
|
||
const dummy = new THREE.Object3D();
|
||
const TREE_CROWN = new THREE.Vector3(-0.3, 4.6, -3.2);
|
||
|
||
function spawnPetal(i, burst=false){
|
||
const a = Math.random()*Math.PI*2;
|
||
const r = burst ? 0.4 + Math.random()*4 : Math.random()*3.2;
|
||
P.pos[i*3] = TREE_CROWN.x + Math.cos(a)*r*0.85;
|
||
P.pos[i*3+1] = burst ? 1.6 + Math.random()*4.2 : TREE_CROWN.y + (Math.random()-0.5)*2.2;
|
||
P.pos[i*3+2] = TREE_CROWN.z + Math.sin(a)*r*0.85;
|
||
P.vel[i*3] = (Math.random()-0.5)*0.6;
|
||
P.vel[i*3+1] = -0.15 - Math.random()*0.35;
|
||
P.vel[i*3+2] = (Math.random()-0.5)*0.6;
|
||
P.rot[i*3] = Math.random()*6.28; P.rot[i*3+1] = Math.random()*6.28; P.rot[i*3+2] = Math.random()*6.28;
|
||
P.spin[i*3] = (Math.random()-0.5)*2.4;
|
||
P.spin[i*3+1]= (Math.random()-0.5)*2.0;
|
||
P.spin[i*3+2]= (Math.random()-0.5)*2.8;
|
||
P.size[i] = 0.7 + Math.random()*0.7;
|
||
P.alive[i] = 1;
|
||
dummy.position.set(1e4,1e4,1e4); dummy.updateMatrix();
|
||
petals.setMatrixAt(i, dummy.matrix);
|
||
}
|
||
for (let i=0;i<MAX_PETALS;i++) spawnPetal(i);
|
||
petals.instanceMatrix.needsUpdate = true;
|
||
|
||
/* ═══════════════ DURUM ═══════════════ */
|
||
let windStrength = 0.45;
|
||
let density = 0.6;
|
||
let tourActive = false;
|
||
let tourTime = 0;
|
||
|
||
let currentTime = 'sunset';
|
||
|
||
const $ = s => document.querySelector(s);
|
||
const veil = $('#veil');
|
||
const hint = $('#hint');
|
||
|
||
const PRESETS = {
|
||
morning: {
|
||
top:0x2f6fae, mid:0xa8cbe0, bot:0xf2e6d8, sky:0xcfe3ef,
|
||
hemiSky:0xd8ecff, hemiGround:0x6a6152, hemiI:1.5,
|
||
sunCol:0xfff2dd, sunI:2.9, sunPos:new THREE.Vector3(-18,16,10),
|
||
fog:0xbccfdc, fogD:0.021,
|
||
waterDeep:0x1d5a6b, waterShallow:0x74bcc6, waterSky:0xcfe3ef, refl:0.62,
|
||
lantern:0.0, exposure:1.0, sunDisc:1.0
|
||
},
|
||
sunset: {
|
||
top:0x2a5c8a, mid:0xd98a6a, bot:0xf6c88a, sky:0xd9a07a,
|
||
hemiSky:0xffd0b0, hemiGround:0x3a2f28, hemiI:0.85,
|
||
sunCol:0xffd9a8, sunI:2.4, sunPos:new THREE.Vector3(-14,12,-16),
|
||
fog:0x9c7a70, fogD:0.027,
|
||
waterDeep:0x0c2c3a, waterShallow:0x3f8f9c, waterSky:0xd9a07a, refl:0.55,
|
||
lantern:0.55, exposure:1.05, sunDisc:1.2
|
||
},
|
||
night: {
|
||
top:0x050a18, mid:0x0d1b33, bot:0x1b2b45, sky:0x14243d,
|
||
hemiSky:0x2b4a7a, hemiGround:0x14161f, hemiI:0.34,
|
||
sunCol:0x9db8ff, sunI:0.55, sunPos:new THREE.Vector3(-10,20,-14),
|
||
fog:0x0e1828, fogD:0.030,
|
||
waterDeep:0x04121c, waterShallow:0x123a48, waterSky:0x1b3350, refl:0.45,
|
||
lantern:2.4, exposure:1.15, sunDisc:0.35
|
||
}
|
||
};
|
||
|
||
const cur = {
|
||
top:new THREE.Color(), mid:new THREE.Color(), bot:new THREE.Color(), sky:new THREE.Color(),
|
||
hemiSky:new THREE.Color(), hemiGround:new THREE.Color(), hemiI:1,
|
||
sunCol:new THREE.Color(), sunI:2, sunPos:new THREE.Vector3(),
|
||
fog:new THREE.Color(), fogD:0.02,
|
||
waterDeep:new THREE.Color(), waterShallow:new THREE.Color(), waterSky:new THREE.Color(), refl:0.5,
|
||
lantern:0, exposure:1, sunDisc:1
|
||
};
|
||
function snapPreset(p){
|
||
cur.top.setHex(p.top); cur.mid.setHex(p.mid); cur.bot.setHex(p.bot); cur.sky.setHex(p.sky);
|
||
cur.hemiSky.setHex(p.hemiSky); cur.hemiGround.setHex(p.hemiGround); cur.hemiI = p.hemiI;
|
||
cur.sunCol.setHex(p.sunCol); cur.sunI = p.sunI; cur.sunPos.copy(p.sunPos);
|
||
cur.fog.setHex(p.fog); cur.fogD = p.fogD;
|
||
cur.waterDeep.setHex(p.waterDeep); cur.waterShallow.setHex(p.waterShallow);
|
||
cur.waterSky.setHex(p.waterSky); cur.refl = p.refl;
|
||
cur.lantern = p.lantern; cur.exposure = p.exposure; cur.sunDisc = p.sunDisc;
|
||
applyCur();
|
||
}
|
||
const TARGET = { ...cur };
|
||
function applyCur(){
|
||
skyUniforms.topColor.value.copy(cur.top);
|
||
skyUniforms.midColor.value.copy(cur.mid);
|
||
skyUniforms.botColor.value.copy(cur.bot);
|
||
skyUniforms.sunColor.value.copy(cur.sunCol);
|
||
skyUniforms.sunI.value = cur.sunDisc;
|
||
skyUniforms.sunDir.value.copy(cur.sunPos).normalize();
|
||
|
||
hemi.color.copy(cur.hemiSky); hemi.groundColor.copy(cur.hemiGround); hemi.intensity = cur.hemiI;
|
||
sun.color.copy(cur.sunCol); sun.intensity = cur.sunI; sun.position.copy(cur.sunPos);
|
||
scene.fog.color.copy(cur.fog); scene.fog.density = cur.fogD;
|
||
// gece yıldızları: yalnız karanlıkta görünür
|
||
const nightness = THREE.MathUtils.clamp(1 - cur.hemiI * 1.35, 0, 1);
|
||
starMat.opacity = nightness * 0.9;
|
||
// NUMEX-DÜZELTME (2026-10-27): applyCur() parametresiz bir fonksiyon; içine yazdığım dt referansı her karede ReferenceError atıp animasyon döngüsünü durduruyordu (perde de bu yüzden kalkmıyordu). Yıldız dönüşü sabit ve kare tabanlı yapıldı.
|
||
stars.rotation.y += 0.00025;
|
||
groundMat.color.setHex(0x5c6b45);
|
||
|
||
waterUniforms.uDeep.value.copy(cur.waterDeep);
|
||
waterUniforms.uShallow.value.copy(cur.waterShallow);
|
||
waterUniforms.uSky.value.copy(cur.waterSky);
|
||
waterUniforms.uRefl.value = cur.refl;
|
||
renderer.toneMappingExposure = cur.exposure;
|
||
|
||
lanternLights.forEach(l => { l.light.intensity = l.base * cur.lantern; });
|
||
ambientGlow = cur.lantern;
|
||
}
|
||
let ambientGlow = 0;
|
||
let targetPreset = PRESETS.sunset;
|
||
snapPreset(PRESETS.sunset);
|
||
|
||
/* ═══════════════ SAHNE EFEKTLERİ (bloom) ═══════════════ */
|
||
let composer = null;
|
||
if (!isMobile){
|
||
composer = new EffectComposer(renderer);
|
||
composer.addPass(new RenderPass(scene, camera));
|
||
const bloom = new UnrealBloomPass(new THREE.Vector2(innerWidth, innerHeight), 0.55, 0.75, 0.88);
|
||
composer.addPass(bloom);
|
||
composer.addPass(new OutputPass());
|
||
}
|
||
|
||
/* ═══════════════ ETKİLEŞİM ═══════════════ */
|
||
const raycaster = new THREE.Raycaster();
|
||
const pointer = new THREE.Vector2();
|
||
let burstQueue = 0;
|
||
|
||
function pointerToNDC(e){
|
||
pointer.x = (e.clientX / innerWidth) * 2 - 1;
|
||
pointer.y = -(e.clientY / innerHeight) * 2 + 1;
|
||
}
|
||
|
||
// yaprak instance'ına tıklama
|
||
function tryPetalPick(e){
|
||
pointerToNDC(e);
|
||
raycaster.setFromCamera(pointer, camera);
|
||
const hit = raycaster.intersectObject(petals, false)[0];
|
||
if (!hit) return false;
|
||
const idx = hit.instanceId;
|
||
if (idx === undefined || idx === null) return false;
|
||
// tıklanan yaprağı savrul: yukarı + dışa hız ver
|
||
const dx = P.pos[idx*3] - TREE_CROWN.x;
|
||
const dz = P.pos[idx*3+2] - TREE_CROWN.z;
|
||
const len = Math.hypot(dx,dz) || 1;
|
||
P.vel[idx*3] = (dx/len)*1.6 + (Math.random()-0.5)*1.2;
|
||
P.vel[idx*3+1] = 1.5 + Math.random()*1.6;
|
||
P.vel[idx*3+2] = (dz/len)*1.6 + (Math.random()-0.5)*1.2;
|
||
P.spin[idx*3] = (Math.random()-0.5)*7;
|
||
P.spin[idx*3+1]= (Math.random()-0.5)*7;
|
||
P.spin[idx*3+2]= (Math.random()-0.5)*7;
|
||
burstQueue = Math.min(26, burstQueue + 10);
|
||
return true;
|
||
}
|
||
|
||
let downPos = null;
|
||
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 dt = performance.now() - downPos.t;
|
||
downPos = null;
|
||
if (moved < 8 && dt < 420) tryPetalPick(e);
|
||
});
|
||
|
||
// panel
|
||
document.querySelectorAll('[data-time]').forEach(btn => {
|
||
btn.addEventListener('click', () => setTime(btn.dataset.time));
|
||
});
|
||
|
||
function setTime(name){
|
||
currentTime = name;
|
||
targetPreset = PRESETS[name];
|
||
document.querySelectorAll('[data-time]').forEach(b => b.classList.toggle('on', b.dataset.time === name));
|
||
}
|
||
|
||
$('#wind').addEventListener('input', e => { windStrength = e.target.value / 100; });
|
||
$('#density').addEventListener('input', e => { density = e.target.value / 100; });
|
||
|
||
$('#tourBtn').addEventListener('click', () => {
|
||
tourActive = !tourActive;
|
||
$('#tourBtn').classList.toggle('on', tourActive);
|
||
controls.autoRotate = false;
|
||
if (tourActive) tourTime = 0;
|
||
});
|
||
$('#resetBtn').addEventListener('click', () => {
|
||
tourActive = false;
|
||
$('#tourBtn').classList.remove('on');
|
||
controls.target.set(0, 3.1, 0);
|
||
camera.position.set(9.4, 6.6, 14.8);
|
||
controls.update();
|
||
});
|
||
$('#toggle').addEventListener('click', () => {
|
||
$('#panel').classList.toggle('hidden');
|
||
});
|
||
|
||
// ipucu kaybol
|
||
setTimeout(() => hint.classList.add('gone'), 9000);
|
||
|
||
/* ═══════════════ ANİMASYON DÖNGÜSÜ ═══════════════ */
|
||
const clock = new THREE.Clock();
|
||
// NUMEX-DÜZELTME (2026-10-27): Animasyonun atmosfer geçişinde kullandığı tmpColor hiç tanımlanmamıştı; her karede ReferenceError atıp döngüyü durduruyordu.
|
||
const tmpColor = new THREE.Color();
|
||
const tmpMat = new THREE.Matrix4();
|
||
const tmpQ = new THREE.Quaternion();
|
||
const tmpE = new THREE.Euler();
|
||
const tmpV = new THREE.Vector3();
|
||
|
||
// tur kamera yolu
|
||
const TOUR = [
|
||
{ p:new THREE.Vector3(11, 6.5, 12), t:new THREE.Vector3(0, 2.6, 0) },
|
||
{ p:new THREE.Vector3(2, 2.4, 9.5), t:new THREE.Vector3(-0.3, 4.2, -2.5) },
|
||
{ p:new THREE.Vector3(-9.5, 5.0, 6.5), t:new THREE.Vector3(0, 1.6, 2.0) },
|
||
{ p:new THREE.Vector3(-6.2, 2.0, 8.6), t:new THREE.Vector3(0.5, 0.4, 3.4) },
|
||
{ p:new THREE.Vector3(0.6, 8.6, 9.0), t:new THREE.Vector3(-0.3, 3.4, -2.4) },
|
||
{ p:new THREE.Vector3(9.2, 3.4, 7.4), t:new THREE.Vector3(1.5, 1.8, 2.2) }
|
||
];
|
||
const tourCurve = new THREE.CatmullRomCurve3(TOUR.map(s => s.p), true, 'catmullrom', 0.35);
|
||
const tourLook = new THREE.CatmullRomCurve3(TOUR.map(s => s.t), true, 'catmullrom', 0.35);
|
||
|
||
function animate(){
|
||
requestAnimationFrame(animate);
|
||
const dt = Math.min(clock.getDelta(), 0.05);
|
||
const t = clock.elapsedTime;
|
||
|
||
// atmosfer geçişi
|
||
const p = targetPreset;
|
||
const k = 1 - Math.pow(0.0018, dt);
|
||
cur.top.lerp(tmpColor.setHex(p.top), k);
|
||
cur.mid.lerp(tmpColor.setHex(p.mid), k);
|
||
cur.bot.lerp(tmpColor.setHex(p.bot), k);
|
||
cur.sky.lerp(tmpColor.setHex(p.sky), k);
|
||
cur.hemiSky.lerp(tmpColor.setHex(p.hemiSky), k);
|
||
cur.hemiGround.lerp(tmpColor.setHex(p.hemiGround), k);
|
||
cur.sunCol.lerp(tmpColor.setHex(p.sunCol), k);
|
||
cur.fog.lerp(tmpColor.setHex(p.fog), k);
|
||
cur.waterDeep.lerp(tmpColor.setHex(p.waterDeep), k);
|
||
cur.waterShallow.lerp(tmpColor.setHex(p.waterShallow), k);
|
||
cur.waterSky.lerp(tmpColor.setHex(p.waterSky), k);
|
||
cur.sunPos.lerp(p.sunPos, k);
|
||
cur.hemiI += (p.hemiI - cur.hemiI) * k;
|
||
cur.sunI += (p.sunI - cur.sunI) * k;
|
||
cur.fogD += (p.fogD - cur.fogD) * k;
|
||
cur.refl += (p.refl - cur.refl) * k;
|
||
cur.lantern += (p.lantern - cur.lantern) * k;
|
||
cur.exposure += (p.exposure - cur.exposure) * k;
|
||
cur.sunDisc += (p.sunDisc - cur.sunDisc) * k;
|
||
applyCur();
|
||
|
||
// su
|
||
waterUniforms.uTime.value = t;
|
||
|
||
// koi
|
||
for (const f of koi){
|
||
f.ang += f.spd * dt * 0.9;
|
||
const wob = Math.sin(t*0.7 + f.ph) * 0.5;
|
||
const px = POND.x + Math.cos(f.ang) * (f.rad + wob*0.35);
|
||
const pz = POND.z + Math.sin(f.ang) * (f.rad + wob*0.35);
|
||
const py = -0.04 + Math.sin(t*1.3 + f.ph) * 0.12 + f.cy*0.25;
|
||
f.mesh.position.set(px, Math.max(-0.16, py), pz);
|
||
f.mesh.rotation.y = -f.ang + Math.PI/2;
|
||
f.mesh.rotation.z = Math.sin(t*2.2 + f.ph) * 0.12;
|
||
f.tail.rotation.y = Math.sin(t*7 + f.ph) * 0.35;
|
||
}
|
||
|
||
// fener titreşimi
|
||
for (let i=0;i<lanternLights.length;i++){
|
||
const l = lanternLights[i];
|
||
const flick = 0.9 + Math.sin(t*3.1 + i*1.7)*0.05 + Math.sin(t*7.3 + i)*0.03;
|
||
l.light.intensity = l.base * cur.lantern * flick;
|
||
l.box.material.emissiveIntensity = 0.2 + cur.lantern * 1.5 * flick;
|
||
}
|
||
|
||
// yaprak fiziği
|
||
// NUMEX-DÜZELTME (2026-10-27): Yaprak animasyonu P.pos/vel/rot dizilerini P.x/P.vx gibi var olmayan alanlarla okuyordu; ilk tanıma (P.pos[], P.vel[], P.rot[]) uyarlandı.
|
||
const activeCount = Math.floor(MAX_PETALS * density);
|
||
if (burstQueue > 0){
|
||
let spawned = 0;
|
||
for (let i=0;i<MAX_PETALS && spawned<burstQueue;i++){
|
||
if (!P.alive[i]) { spawnPetal(i, true); spawned++; }
|
||
}
|
||
burstQueue = 0;
|
||
}
|
||
const windBase = windStrength * 2.4;
|
||
for (let i=0;i<MAX_PETALS;i++){
|
||
const ix = i*3;
|
||
if (i >= activeCount){
|
||
// yoğunluk dışı: sahne dışında tut
|
||
P.pos[ix+1] = -50;
|
||
dummy.position.set(0,-50,0); dummy.updateMatrix();
|
||
petals.setMatrixAt(i, dummy.matrix);
|
||
continue;
|
||
}
|
||
const px = P.pos[ix], py = P.pos[ix+1], pz = P.pos[ix+2];
|
||
const wind = windBase * (0.65 + 0.35*Math.sin(t*0.7 + i*0.37));
|
||
P.vel[ix] += (wind - P.vel[ix]) * dt * 0.9;
|
||
P.vel[ix+2] += (Math.sin(t*0.9 + i*1.3) * windStrength * 0.9 - P.vel[ix+2]) * dt * 0.8;
|
||
P.vel[ix+1] += (-0.55 - P.vel[ix+1]*0.35) * dt;
|
||
P.pos[ix] += P.vel[ix]*dt;
|
||
P.pos[ix+1] += P.vel[ix+1]*dt;
|
||
P.pos[ix+2] += P.vel[ix+2]*dt;
|
||
P.rot[ix] += P.spin[ix]*dt;
|
||
P.rot[ix+1] += P.spin[ix+1]*dt;
|
||
P.rot[ix+2] += P.spin[ix+2]*dt;
|
||
// su yüzeyine düşenler kaybolur
|
||
const overPond = Math.hypot(P.pos[ix]-POND.x, P.pos[ix+2]-POND.z) < POND.r;
|
||
const floorY = overPond ? 0.02 : 0.05;
|
||
if (P.pos[ix+1] <= floorY || Math.abs(P.pos[ix]) > 32 || Math.abs(P.pos[ix+2]) > 32){
|
||
spawnPetal(i);
|
||
continue;
|
||
}
|
||
dummy.position.set(P.pos[ix], P.pos[ix+1], P.pos[ix+2]);
|
||
dummy.rotation.set(P.rot[ix], P.rot[ix+1], P.rot[ix+2]);
|
||
const s = P.size[i];
|
||
dummy.scale.set(s, s, s);
|
||
dummy.updateMatrix();
|
||
petals.setMatrixAt(i, dummy.matrix);
|
||
}
|
||
petals.instanceMatrix.needsUpdate = true;
|
||
|
||
petals.instanceMatrix.needsUpdate = true;
|
||
|
||
// yansıma dokusu güncelle
|
||
|
||
// NUMEX-DÜZELTME (2026-10-27): Tur kamerayı sert ışınlıyordu ve hazır CatmullRom tur yolunu (tourCurve/tourLook) hiç kullanmıyordu; yumuşak yörüngeye bağlandı.
|
||
// sinematik tur (hazır kamera yolu üzerinde)
|
||
if (tourActive){
|
||
tourTime = (tourTime + dt * 0.022) % 1;
|
||
tourCurve.getPointAt(tourTime, tmpV);
|
||
camera.position.lerp(tmpV, 1 - Math.pow(0.002, dt));
|
||
tourLook.getPointAt(tourTime, tmpV);
|
||
controls.target.lerp(tmpV, 1 - Math.pow(0.004, dt));
|
||
camera.lookAt(controls.target);
|
||
if (tourTime < 0.01){ /* döngü başı */ }
|
||
} else {
|
||
controls.update();
|
||
}
|
||
|
||
|
||
renderer.render(scene, camera);
|
||
}
|
||
|
||
/* ═══════════════ BAŞLAT ═══════════════ */
|
||
animate();
|
||
|
||
// perdeyi kaldır
|
||
setTimeout(() => {
|
||
veil.classList.add('gone');
|
||
setTimeout(() => { veil.style.display = 'none'; }, 900);
|
||
}, 700);
|
||
|
||
// yeniden boyutlandırma
|
||
addEventListener('resize', () => {
|
||
camera.aspect = innerWidth / innerHeight;
|
||
camera.updateProjectionMatrix();
|
||
renderer.setSize(innerWidth, innerHeight);
|
||
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|