← Mike Higgins Home
Custom Code Experiment •

Nazar Boncuğu 4: 3D

Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nazar Boncuğu 4: 3D</title>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  body { background: #000; overflow: hidden; width: 100vw; height: 100vh; }
  canvas { display: block; }
  #overlay {
    position: fixed; inset: 0; pointer-events: none;
    background: radial-gradient(ellipse at 50% 50%, transparent 30%, rgba(0,0,0,0.72) 100%);
  }
</style>
</head>
<body>
<div id="overlay"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
(function () {

  const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.shadowMap.enabled = true;
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
  renderer.toneMapping = THREE.ACESFilmicToneMapping;
  renderer.toneMappingExposure = 1.1;
  document.body.appendChild(renderer.domElement);

  const scene = new THREE.Scene();
  scene.background = new THREE.Color(0x04010d);
  scene.fog = new THREE.FogExp2(0x04010d, 0.045);

  const camera = new THREE.PerspectiveCamera(42, window.innerWidth / window.innerHeight, 0.1, 200);
  camera.position.set(0, 0, 7);

  let isDragging = false, prevMouse = { x: 0, y: 0 };
  let rotY = 0, rotX = 0.18;
  let autoRotate = true;
  const pivot = new THREE.Group();
  scene.add(pivot);

  renderer.domElement.addEventListener('mousedown', e => { isDragging = true; autoRotate = false; prevMouse = { x: e.clientX, y: e.clientY }; });
  renderer.domElement.addEventListener('touchstart', e => { isDragging = true; autoRotate = false; prevMouse = { x: e.touches[0].clientX, y: e.touches[0].clientY }; });
  window.addEventListener('mouseup', () => { isDragging = false; });
  window.addEventListener('touchend', () => { isDragging = false; });
  window.addEventListener('mousemove', e => {
    if (!isDragging) return;
    rotY += (e.clientX - prevMouse.x) * 0.008;
    rotX += (e.clientY - prevMouse.y) * 0.008;
    rotX = Math.max(-1.1, Math.min(1.1, rotX));
    prevMouse = { x: e.clientX, y: e.clientY };
  });
  window.addEventListener('touchmove', e => {
    if (!isDragging) return;
    rotY += (e.touches[0].clientX - prevMouse.x) * 0.008;
    rotX += (e.touches[0].clientY - prevMouse.y) * 0.008;
    prevMouse = { x: e.touches[0].clientX, y: e.touches[0].clientY };
  });

  const pmrem = new THREE.PMREMGenerator(renderer);
  pmrem.compileCubemapShader();
  (function buildEnv() {
    const envS = new THREE.Scene();
    envS.background = new THREE.Color(0x08041a);
    const eColors = [0xffd080, 0x3060ff, 0xff4020, 0x20d0a0, 0xc040ff, 0xffffff];
    const ePos = [[5,5,5],[-5,5,-5],[5,-5,-5],[-5,-5,5],[0,8,0],[0,-8,0]];
    eColors.forEach((c, i) => {
      const l = new THREE.PointLight(c, 6, 20);
      l.position.set(...ePos[i]);
      envS.add(l);
    });
    const cubeRT = new THREE.WebGLCubeRenderTarget(256);
    const cubeCamera = new THREE.CubeCamera(0.1, 50, cubeRT);
    envS.add(cubeCamera);
    cubeCamera.update(renderer, envS);
    scene.environment = pmrem.fromCubemap(cubeRT.texture).texture;
  })();

  const lerp = (a, b, t) => a + (b - a) * t;

  function makeRing(innerR, outerR, color, roughness, metalness, emissive = 0x000000, emissiveIntensity = 0) {
    const geo = new THREE.RingGeometry(innerR, outerR, 128, 4);
    const points = [
      new THREE.Vector2(innerR, -0.045),
      new THREE.Vector2(innerR, 0.045),
      new THREE.Vector2(outerR, 0.065),
      new THREE.Vector2(outerR, -0.065),
    ];
    const shape = new THREE.Shape();
    const tGeo = new THREE.TorusGeometry((innerR + outerR) / 2, (outerR - innerR) / 2, 32, 160);
    const mat = new THREE.MeshStandardMaterial({ color, roughness, metalness, envMapIntensity: 1.2 });
    if (emissive) { mat.emissive = new THREE.Color(emissive); mat.emissiveIntensity = emissiveIntensity; }
    const mesh = new THREE.Mesh(tGeo, mat);
    mesh.castShadow = true;
    mesh.receiveShadow = true;
    return mesh;
  }

  const amulet = new THREE.Group();
  pivot.add(amulet);

  const discGeo1 = new THREE.CylinderGeometry(2.0, 2.0, 0.38, 128, 1, false);
  const discMat1 = new THREE.MeshPhysicalMaterial({
    color: 0x0a2070,
    roughness: 0.05,
    metalness: 0.0,
    transmission: 0.55,
    ior: 1.52,
    envMapIntensity: 2.0,
    clearcoat: 1.0,
    clearcoatRoughness: 0.08,
  });
  const disc1 = new THREE.Mesh(discGeo1, discMat1);
  disc1.rotation.x = Math.PI / 2;
  disc1.position.z = -0.18;
  disc1.castShadow = true;
  amulet.add(disc1);

  const bevel1 = makeRing(1.94, 2.04, 0x0e2a90, 0.1, 0.3);
  bevel1.position.z = -0.18;
  amulet.add(bevel1);

  const goldRing = makeRing(1.62, 1.95, 0xc8901a, 0.18, 0.95);
  goldRing.position.z = 0.01;
  amulet.add(goldRing);

  for (let i = 0; i < 6; i++) {
    const r = 1.64 + i * 0.05;
    const fr = makeRing(r, r + 0.012, 0xffd060, 0.22, 0.9);
    fr.position.z = 0.04;
    amulet.add(fr);
  }

  const discGeo2 = new THREE.CylinderGeometry(1.62, 1.62, 0.28, 128);
  const discMat2 = new THREE.MeshPhysicalMaterial({ color: 0xe8dcc4, roughness: 0.3, metalness: 0.0, clearcoat: 0.6, clearcoatRoughness: 0.15 });
  const disc2 = new THREE.Mesh(discGeo2, discMat2);
  disc2.rotation.x = Math.PI / 2;
  disc2.position.z = 0.06;
  amulet.add(disc2);

  const discGeo3 = new THREE.CylinderGeometry(1.28, 1.28, 0.32, 128);
  const discMat3 = new THREE.MeshPhysicalMaterial({ color: 0x1240b8, roughness: 0.06, metalness: 0.0, transmission: 0.4, ior: 1.5, clearcoat: 1.0, clearcoatRoughness: 0.05 });
  const disc3 = new THREE.Mesh(discGeo3, discMat3);
  disc3.rotation.x = Math.PI / 2;
  disc3.position.z = 0.12;
  amulet.add(disc3);

  const discGeo4 = new THREE.CylinderGeometry(0.92, 0.92, 0.28, 128);
  const discMat4 = new THREE.MeshPhysicalMaterial({ color: 0xf0eada, roughness: 0.25, metalness: 0.0, clearcoat: 0.7, clearcoatRoughness: 0.1 });
  const disc4 = new THREE.Mesh(discGeo4, discMat4);
  disc4.rotation.x = Math.PI / 2;
  disc4.position.z = 0.18;
  amulet.add(disc4);

  const discGeo5 = new THREE.CylinderGeometry(0.64, 0.64, 0.34, 128);
  const discMat5 = new THREE.MeshPhysicalMaterial({ color: 0x0a2090, roughness: 0.04, metalness: 0.0, transmission: 0.5, ior: 1.55, clearcoat: 1.0, clearcoatRoughness: 0.04 });
  const disc5 = new THREE.Mesh(discGeo5, discMat5);
  disc5.rotation.x = Math.PI / 2;
  disc5.position.z = 0.22;
  amulet.add(disc5);

  const pupilGeo = new THREE.SphereGeometry(0.44, 64, 64, 0, Math.PI * 2, 0, Math.PI * 0.55);
  const pupilMat = new THREE.MeshPhysicalMaterial({
    color: 0x03020a,
    roughness: 0.0,
    metalness: 0.0,
    transmission: 0.18,
    ior: 1.7,
    clearcoat: 1.0,
    clearcoatRoughness: 0.0,
    envMapIntensity: 3.0,
  });
  const pupil = new THREE.Mesh(pupilGeo, pupilMat);
  pupil.rotation.x = -Math.PI / 2;
  pupil.position.z = 0.28;
  amulet.add(pupil);

  const specGeo = new THREE.SphereGeometry(0.09, 32, 32);
  const specMat = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.7 });
  const spec = new THREE.Mesh(specGeo, specMat);
  spec.position.set(-0.14, 0.14, 0.68);
  amulet.add(spec);

  const spec2 = new THREE.Mesh(new THREE.SphereGeometry(0.04, 16, 16), new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.35 }));
  spec2.position.set(0.18, -0.15, 0.64);
  amulet.add(spec2);

  const rimRing = makeRing(1.98, 2.08, 0xd4a020, 0.12, 0.98);
  rimRing.position.z = 0.0;
  amulet.add(rimRing);

  const beadGroup = new THREE.Group();
  amulet.add(beadGroup);
  const beadCount = 32;
  const beadOrbitR = 2.22;
  for (let i = 0; i < beadCount; i++) {
    const angle = (i / beadCount) * Math.PI * 2;
    const big = i % 3 === 0;
    const r = big ? 0.075 : 0.045;
    const bGeo = new THREE.SphereGeometry(r, 16, 16);
    const bMat = new THREE.MeshStandardMaterial({
      color: big ? 0xd4a020 : 0xe8dcc4,
      roughness: big ? 0.15 : 0.3,
      metalness: big ? 0.9 : 0.1,
    });
    const bead = new THREE.Mesh(bGeo, bMat);
    bead.position.set(
      Math.cos(angle) * beadOrbitR,
      Math.sin(angle) * beadOrbitR,
      0.1
    );
    beadGroup.add(bead);
  }

  const spikeGroup = new THREE.Group();
  amulet.add(spikeGroup);
  const spikeCount = 24;
  for (let i = 0; i < spikeCount; i++) {
    const angle = (i / spikeCount) * Math.PI * 2;
    const isMain = i % 2 === 0;
    const h = isMain ? 0.38 : 0.22;
    const sGeo = new THREE.ConeGeometry(0.04, h, 8);
    const sMat = new THREE.MeshStandardMaterial({ color: isMain ? 0xc8901a : 0x8a6010, roughness: 0.2, metalness: 0.85 });
    const spike = new THREE.Mesh(sGeo, sMat);
    const dist = 2.28 + h / 2;
    spike.position.set(Math.cos(angle) * dist, Math.sin(angle) * dist, 0.0);
    spike.rotation.z = -angle - Math.PI / 2;
    spikeGroup.add(spike);
  }

  const tearPoints = [];
  for (let i = 0; i <= 20; i++) {
    const t = i / 20;
    const y = -t * 1.0;
    const x = Math.sin(t * Math.PI) * 0.22 * (1 - t * 0.4);
    tearPoints.push(new THREE.Vector2(x, y));
  }
  const tearGeo = new THREE.LatheGeometry(tearPoints, 64);
  const tearMat = new THREE.MeshPhysicalMaterial({ color: 0x0e2070, roughness: 0.04, metalness: 0.0, transmission: 0.6, ior: 1.55, clearcoat: 1.0 });
  const tear = new THREE.Mesh(tearGeo, tearMat);
  tear.position.set(0, -2.65, 0.0);
  amulet.add(tear);

  const tdotGeo = new THREE.SphereGeometry(0.07, 16, 16);
  const tdotMat = new THREE.MeshStandardMaterial({ color: 0xd4a020, roughness: 0.1, metalness: 0.95 });
  const tdot = new THREE.Mesh(tdotGeo, tdotMat);
  tdot.position.set(0, -2.9, 0.08);
  amulet.add(tdot);

  for (let i = 0; i < 5; i++) {
    const lGeo = new THREE.TorusGeometry(0.12, 0.035, 12, 32);
    const lMat = new THREE.MeshStandardMaterial({ color: 0xc8901a, roughness: 0.15, metalness: 0.95 });
    const link = new THREE.Mesh(lGeo, lMat);
    link.position.set(0, 2.2 + i * 0.26, 0.0);
    link.rotation.x = i % 2 === 0 ? Math.PI / 2 : 0;
    amulet.add(link);
  }

  const cornerPositions = [
    [-2.7,  2.7], [ 2.7,  2.7],
    [-2.7, -2.7], [ 2.7, -2.7],
  ];
  cornerPositions.forEach(([cx, cy]) => {
    const cGroup = new THREE.Group();
    cGroup.position.set(cx, cy, -0.1);
    const angle = Math.atan2(cy, cx) + Math.PI / 4;
    cGroup.rotation.z = angle;

    const cbGeo = new THREE.CylinderGeometry(0.22, 0.22, 0.12, 32);
    const cbMat = new THREE.MeshPhysicalMaterial({ color: 0x0a2070, roughness: 0.08, clearcoat: 1.0, transmission: 0.3, ior: 1.5 });
    const cb = new THREE.Mesh(cbGeo, cbMat);
    cb.rotation.x = Math.PI / 2;
    cGroup.add(cb);

    const crGeo = new THREE.TorusGeometry(0.21, 0.025, 8, 32);
    const crMat = new THREE.MeshStandardMaterial({ color: 0xd4a020, roughness: 0.15, metalness: 0.95 });
    const cr = new THREE.Mesh(crGeo, crMat);
    cGroup.add(cr);

    const cpGeo = new THREE.SphereGeometry(0.08, 16, 16);
    const cpMat = new THREE.MeshBasicMaterial({ color: 0x101820 });
    const cp = new THREE.Mesh(cpGeo, cpMat);
    cp.position.z = 0.09;
    cGroup.add(cp);

    amulet.add(cGroup);
  });

  const particleCount = 600;
  const pPositions = new Float32Array(particleCount * 3);
  const pColors = new Float32Array(particleCount * 3);
  const pSpeeds = new Float32Array(particleCount);
  const pAngles = new Float32Array(particleCount);
  const pRadii = new Float32Array(particleCount);

  for (let i = 0; i < particleCount; i++) {
    const angle = Math.random() * Math.PI * 2;
    const radius = 3.5 + Math.random() * 5.0;
    const z = (Math.random() - 0.5) * 3.0;
    pPositions[i * 3]     = Math.cos(angle) * radius;
    pPositions[i * 3 + 1] = Math.sin(angle) * radius;
    pPositions[i * 3 + 2] = z;
    pAngles[i] = angle;
    pRadii[i]  = radius;
    pSpeeds[i] = (Math.random() * 0.3 + 0.05) * (Math.random() > 0.5 ? 1 : -1);
    const isGold = Math.random() > 0.5;
    if (isGold) { pColors[i*3]=0.9; pColors[i*3+1]=0.65; pColors[i*3+2]=0.1; }
    else        { pColors[i*3]=0.2; pColors[i*3+1]=0.4;  pColors[i*3+2]=1.0; }
  }

  const pGeo = new THREE.BufferGeometry();
  pGeo.setAttribute('position', new THREE.BufferAttribute(pPositions, 3));
  pGeo.setAttribute('color', new THREE.BufferAttribute(pColors, 3));
  const pMat = new THREE.PointsMaterial({ size: 0.045, vertexColors: true, transparent: true, opacity: 0.75, sizeAttenuation: true });
  const particles = new THREE.Points(pGeo, pMat);
  scene.add(particles);

  const ambient = new THREE.AmbientLight(0x0a0820, 1.2);
  scene.add(ambient);

  const key = new THREE.DirectionalLight(0xffd4a0, 3.5);
  key.position.set(-4, 5, 6);
  key.castShadow = true;
  key.shadow.mapSize.set(2048, 2048);
  key.shadow.camera.near = 0.1;
  key.shadow.camera.far = 40;
  key.shadow.camera.left = key.shadow.camera.bottom = -8;
  key.shadow.camera.right = key.shadow.camera.top = 8;
  scene.add(key);

  const fill = new THREE.DirectionalLight(0x3060ff, 1.4);
  fill.position.set(5, -2, 4);
  scene.add(fill);

  const rim = new THREE.DirectionalLight(0xc08010, 2.2);
  rim.position.set(0, -4, -5);
  scene.add(rim);

  const eyeLight = new THREE.PointLight(0x2050ff, 3.5, 4.0);
  eyeLight.position.set(0, 0, 0.6);
  amulet.add(eyeLight);

  const topLight = new THREE.PointLight(0xffd060, 2.0, 6.0);
  topLight.position.set(0, 3.5, 2.0);
  scene.add(topLight);

  const bgStarCount = 1200;
  const bgPos = new Float32Array(bgStarCount * 3);
  for (let i = 0; i < bgStarCount; i++) {
    const theta = Math.random() * Math.PI * 2;
    const phi   = Math.acos(2 * Math.random() - 1);
    const r     = 60 + Math.random() * 40;
    bgPos[i*3]   = r * Math.sin(phi) * Math.cos(theta);
    bgPos[i*3+1] = r * Math.sin(phi) * Math.sin(theta);
    bgPos[i*3+2] = r * Math.cos(phi);
  }
  const bgGeo = new THREE.BufferGeometry();
  bgGeo.setAttribute('position', new THREE.BufferAttribute(bgPos, 3));
  const bgMat = new THREE.PointsMaterial({ color: 0xffffff, size: 0.18, transparent: true, opacity: 0.6 });
  scene.add(new THREE.Points(bgGeo, bgMat));

  window.addEventListener('resize', () => {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
  });

  let t = 0;
  function animate() {
    requestAnimationFrame(animate);
    t += 0.012;

    if (autoRotate) {
      rotY += 0.004;
    }
    pivot.rotation.y = lerp(pivot.rotation.y, rotY, 0.08);
    pivot.rotation.x = lerp(pivot.rotation.x, rotX, 0.08);

    beadGroup.rotation.z += 0.003;
    spikeGroup.rotation.z -= 0.002;

    eyeLight.intensity = 3.0 + Math.sin(t * 1.8) * 1.5;
    eyeLight.color.setHSL(0.62 + Math.sin(t * 0.4) * 0.04, 1.0, 0.55);

    amulet.position.y = Math.sin(t * 0.7) * 0.08;

    const pos = pGeo.attributes.position.array;
    for (let i = 0; i < particleCount; i++) {
      pAngles[i] += pSpeeds[i] * 0.004;
      pos[i*3]   = Math.cos(pAngles[i]) * pRadii[i];
      pos[i*3+1] = Math.sin(pAngles[i]) * pRadii[i];
    }
    pGeo.attributes.position.needsUpdate = true;

    renderer.render(scene, camera);
  }
  animate();

})();
</script>
</body>
</html>