Files
2026-10-27 02:24:50 +00:00

982 lines
40 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 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 &nbsp;·&nbsp; tekerlek / iki parmak: yakınlaş &nbsp;·&nbsp; 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>