{"id":37,"date":"2026-06-13T08:10:11","date_gmt":"2026-06-13T08:10:11","guid":{"rendered":"https:\/\/poonaki.ir\/?page_id=37"},"modified":"2026-06-13T08:51:10","modified_gmt":"2026-06-13T08:51:10","slug":"%d8%b3%d9%87%db%8c%d9%84-%d9%be%d9%88%d9%86%da%a9%db%8c-soheil-pounaki-2","status":"publish","type":"page","link":"https:\/\/poonaki.ir\/","title":{"rendered":"\u0633\u0647\u06cc\u0644 \u067e\u0648\u0646\u06a9\u06cc | Soheil Pounaki"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-post\" data-elementor-id=\"37\" class=\"elementor elementor-37\" data-elementor-post-type=\"page\">\n\t\t\t\t<div class=\"elementor-element elementor-element-6b905872 e-con e-atomic-element e-flexbox-base e-6b905872-0e307db \" data-id=\"6b905872\" data-element_type=\"e-flexbox\" data-e-type=\"e-flexbox\" data-interaction-id=\"6b905872\" data-e-type=\"e-flexbox\" data-id=\"6b905872\">\n    \t\t<div class=\"elementor-element elementor-element-1a396aeb elementor-widget elementor-widget-html\" data-id=\"1a396aeb\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t\t<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<title>Letter Cubes<\/title>\n<style>\n* { margin: 0; padding: 0; box-sizing: border-box; }\nbody { background: #0d0d0d; overflow: hidden; }\ncanvas { display: block; }\n#hint {\n  position: absolute;\n  bottom: 18px; left: 50%;\n  transform: translateX(-50%);\n  font-family: sans-serif; font-size: 12px;\n  color: rgba(255,255,255,0.28);\n  pointer-events: none; letter-spacing: 0.05em; white-space: nowrap;\n  transition: opacity 0.5s;\n}\n<\/style>\n<\/head>\n<body>\n<div id=\"hint\">move mouse over the shape to scatter \u00b7 cubes return on their own<\/div>\n<script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/three.js\/r128\/three.min.js\"><\/script>\n<script>\n\/\/ \u2500\u2500 Scene \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst W = window.innerWidth, H = window.innerHeight;\nconst scene = new THREE.Scene();\nconst camera = new THREE.PerspectiveCamera(80, W \/ H, 0.1, 1000);\ncamera.position.z = 30;\n\nconst renderer = new THREE.WebGLRenderer({ antialias: true });\nrenderer.setSize(W, H);\nrenderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\nrenderer.setClearColor(0x0d0d0d, 1);\ndocument.body.appendChild(renderer.domElement);\n\n\/\/ \u2500\u2500 Lighting \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nscene.add(new THREE.AmbientLight(0xffd0a0, 0.55));\nconst key = new THREE.DirectionalLight(0xffffff, 1.1);\nkey.position.set(-6, 10, 12); scene.add(key);\nconst fill = new THREE.DirectionalLight(0xff6600, 0.7);\nfill.position.set(8, -4, 6); scene.add(fill);\nconst rim = new THREE.DirectionalLight(0xff9933, 0.5);\nrim.position.set(0, -8, -10); scene.add(rim);\n\n\/\/ \u2500\u2500 Shape definitions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\/\/ Helper: convert a 2D grid array into {x,y} target positions (centered, 40 cubes max)\nfunction gridToPositions(grid, spacing) {\n  const rows = grid.length;\n  const cols = grid[0].length;\n  const pts = [];\n  for (let r = 0; r < rows; r++)\n    for (let c = 0; c < cols; c++)\n      if (grid[r][c]) pts.push({ r, c });\n  \/\/ Center\n  return pts.map(p => ({\n    x: (p.c - (cols - 1) \/ 2) * spacing,\n    y: ((rows - 1) \/ 2 - p.r) * spacing,\n    z: 0\n  }));\n}\n\nconst SPACING = 1.75;\n\n\/\/ Step 1: 5\u00d78 block (40 cubes)\nconst BLOCK_ROWS = 8, BLOCK_COLS = 5;\nconst blockGrid = [];\nfor (let r = 0; r < BLOCK_ROWS; r++) { blockGrid.push([]); for (let c = 0; c < BLOCK_COLS; c++) blockGrid[r].push(1); }\nconst step0_positions = gridToPositions(blockGrid, SPACING);\n\n\/\/ Step 2: Pen (7 cols \u00d7 15 rows) \u2014 trimmed to 40 cubes\nconst PEN_GRID = [\n  [0, 0, 0, 1, 0, 0, 0],\n  [0, 0, 0, 1, 0, 0, 0],\n  [0, 0, 1, 0, 1, 0, 0],\n  [0, 0, 1, 0, 1, 0, 0],\n  [0, 1, 0, 1, 0, 1, 0],\n  [0, 1, 0, 1, 0, 1, 0],\n  [1, 0, 0, 1, 0, 0, 1],\n  [1, 0, 0, 0, 0, 0, 1],\n  [1, 0, 0, 0, 0, 0, 1],\n  [1, 0, 0, 1, 0, 0, 1],\n  [0, 1, 0, 0, 0, 1, 0],\n  [1, 1, 1, 1, 1, 1, 1],\n  [1, 0, 1, 1, 1, 0, 1],\n  [1, 0, 0, 0, 0, 0, 1],\n  [1, 0, 0, 0, 0, 0, 1],\n];\nconst step1_positions = gridToPositions(PEN_GRID, SPACING);\n\n\/\/ Step 3: Cursor (10 cols \u00d7 16 rows)\nconst CURSOR_GRID = [\n  [1, 1, 0, 0, 0, 0, 0, 0, 0, 0],\n  [1, 0, 1, 0, 0, 0, 0, 0, 0, 0],\n  [1, 0, 0, 1, 0, 0, 0, 0, 0, 0],\n  [1, 0, 0, 0, 1, 0, 0, 0, 0, 0],\n  [1, 0, 0, 0, 0, 1, 0, 0, 0, 0],\n  [1, 0, 0, 0, 0, 0, 1, 0, 0, 0],\n  [1, 0, 0, 0, 0, 0, 0, 1, 0, 0],\n  [1, 0, 0, 0, 0, 0, 0, 0, 1, 0],\n  [1, 0, 0, 0, 0, 0, 0, 0, 0, 1],\n  [1, 0, 0, 0, 0, 0, 1, 1, 1, 1],\n  [1, 0, 0, 1, 0, 0, 1, 0, 0, 0],\n  [1, 0, 1, 0, 1, 0, 0, 1, 0, 0],\n  [1, 1, 0, 0, 1, 0, 0, 1, 0, 0],\n  [0, 0, 0, 0, 0, 1, 0, 0, 1, 0],\n  [0, 0, 0, 0, 0, 1, 0, 0, 1, 0],\n  [0, 0, 0, 0, 0, 0, 1, 1, 0, 0],\n];\nconst step2_positions = gridToPositions(CURSOR_GRID, SPACING);\n\n\/\/ Step 4: Light Bulb (11 cols \u00d7 13 rows)\nconst BULB_GRID = [\n  [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],\n  [0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0],\n  [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0],\n  [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],\n  [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],\n  [1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1],\n  [1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1],\n  [0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0],\n  [0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0],\n  [0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0],\n  [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],\n  [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0],\n  [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0],\n];\nconst step3_positions = gridToPositions(BULB_GRID, SPACING);\n\n\/\/ Step 5: Soheil (11 cols \u00d7 7 rows)\nconst SOHEIL_GRID = [\n  [0,0,1,0,0,0,1,0,1,0,1],\n  [0,0,1,0,1,0,1,1,1,1,1],\n  [1,0,1,0,1,0,1,0,0,0,0],\n  [1,0,1,1,1,1,1,1,1,1,1],\n  [1,0,1,0,0,0,1,0,1,0,1],\n  [1,1,1,0,1,0,1,1,1,1,1],\n  [0,0,0,0,1,0,0,0,0,0,0],\n];\nconst step4_positions = gridToPositions(SOHEIL_GRID, SPACING);\n\n\/\/ All steps in order\nconst STEPS = [step0_positions, step1_positions, step2_positions, step3_positions, step4_positions];\nconst NUM_CUBES = 40;\n\n\/\/ \u2500\u2500 Camera zoom: compute ideal Z for each step \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction computeZoomZ(positions, fovDeg, aspect, margin) {\n  let maxX = 0, maxY = 0;\n  positions.forEach(p => {\n    maxX = Math.max(maxX, Math.abs(p.x));\n    maxY = Math.max(maxY, Math.abs(p.y));\n  });\n  const halfW = maxX + margin;\n  const halfH = maxY + margin;\n  const fovRad = (fovDeg * Math.PI) \/ 180;\n  const zFromH = halfH \/ Math.tan(fovRad \/ 2);\n  const zFromW = halfW \/ (aspect * Math.tan(fovRad \/ 2));\n  return Math.max(zFromH, zFromW) * 1.05;\n}\n\n\/\/ \u2500\u2500 Create cubes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst ORANGES = [0xff6600,0xff7722,0xff8833,0xff5500,0xff7700,0xee5500];\nconst GEO = new THREE.BoxGeometry(1.3, 1.3, 1.3);\nconst CUBE_SIZE = 1.3;\nconst HALF = CUBE_SIZE \/ 2;\n\nconst cubes = [];\nfor (let i = 0; i < NUM_CUBES; i++) {\n  const mat = new THREE.MeshPhongMaterial({\n    color: ORANGES[i % ORANGES.length],\n    shininess: 90,\n    specular: new THREE.Color(0xffcc99),\n  });\n  const mesh = new THREE.Mesh(GEO, mat);\n  \/\/ Start at step0 positions\n  const p = step0_positions[i];\n  mesh.position.set(p.x, p.y, p.z);\n  mesh.rotation.set(0, 0, 0);\n  scene.add(mesh);\n\n  cubes.push({\n    mesh,\n    currentTarget: { ...p },  \/\/ where this cube is heading right now\n    vx: 0, vy: 0, vz: 0,\n    ax: 0, ay: 0, az: 0,\n    state: 'formed',  \/\/ formed | morphing | scattered | returning\n    scatterStartTime: 0,\n    scatterDuration: 400,\n    returnTimer: 0,\n    returnStartTime: 0,\n    returnDuration: 700,\n    returnStartPos: { x: 0, y: 0, z: 0 },\n    returnStartRot: { x: 0, y: 0, z: 0 },\n  });\n}\n\n\/\/ \u2500\u2500 Step sequence controller \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst MORPH_DURATION = 2000;  \/\/ 2s to animate\nconst HOLD_DURATION  = 2000;  \/\/ 2s to hold (except last step holds forever)\n\nlet currentStep  = 0;\nlet seqPhase     = 'holding';  \/\/ 'holding' | 'morphing'\nlet seqPhaseStart = performance.now();\nlet morphing = false;\n\nfunction startMorphToStep(stepIdx) {\n  const targets = STEPS[stepIdx];\n  cubes.forEach((c, i) => {\n    \/\/ Only morph if not scattered\/returning\n    c.currentTarget = { ...targets[i] };\n    if (c.state === 'formed' || c.state === 'morphing') {\n      c.state = 'morphing';\n      c._morphStart = { x: c.mesh.position.x, y: c.mesh.position.y, z: c.mesh.position.z };\n      c._morphT = 0;\n    }\n  });\n  morphing = true;\n}\n\n\/\/ \u2500\u2500 Mouse \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst mouseWorld = new THREE.Vector3(99999, 99999, 0);\nlet mouseOverShape = false;\nconst _rv = new THREE.Vector3();\n\n\/\/ Camera orbit on X mouse movement\nconst CAM_RADIUS = 30;\nconst MAX_ORBIT_DEG = 8;\nlet camTargetAngle = 0;\nlet camCurrentAngle = 0;\nlet lastMouseCX = W \/ 2;\nlet lastMouseCY = H \/ 2;\n\nfunction updateMouseWorld(cx, cy) {\n  const nx = (cx \/ W) * 2 - 1;\n  const ny = -(cy \/ H) * 2 + 1;\n  const ray = new THREE.Vector3(nx, ny, 0.5).unproject(camera);\n  ray.sub(camera.position).normalize();\n  \/\/ Intersect ray with z=0 plane\n  const t = -camera.position.z \/ ray.z;\n  mouseWorld.copy(camera.position).addScaledVector(ray, t);\n  const shapeW = 12 + CUBE_SIZE;\n  const shapeH = 16 + CUBE_SIZE;\n  mouseOverShape = Math.abs(mouseWorld.x) < shapeW && Math.abs(mouseWorld.y) < shapeH;\n}\n\nfunction updateMouse(cx, cy) {\n  lastMouseCX = cx;\n  lastMouseCY = cy;\n  const nx = (cx \/ W) * 2 - 1;\n  camTargetAngle = nx * (MAX_ORBIT_DEG * Math.PI \/ 180);\n  updateMouseWorld(cx, cy);\n}\nwindow.addEventListener('mousemove', e => updateMouse(e.clientX, e.clientY));\nwindow.addEventListener('mouseleave', () => {\n  mouseOverShape = false;\n  mouseWorld.set(99999, 99999, 0);\n  camTargetAngle = 0;\n});\n\n\/\/ \u2500\u2500 Easing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); }\nfunction easeOutQuart(t) { return 1 - Math.pow(1 - t, 4); }\nfunction easeOutQuint(t) { return 1 - Math.pow(1 - t, 5); }\n\n\/\/ \u2500\u2500 Physics constants \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst GRAVITY      = 9.8;\nconst FLOOR_Y      = -14;\nconst WALL_X       = 17;\nconst REPULSE_R    = 5.5;\nconst REPULSE_F    = 22;\nconst SCATTER_DRAG = 0.993;\nconst BOUNCE_DAMP  = 0.15;\nconst RETURN_DELAY = 1600;\n\n\/\/ \u2500\u2500 Cube\u2013cube collision \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction resolveCubeCollisions() {\n  for (let i = 0; i < cubes.length; i++) {\n    const a = cubes[i];\n    if (a.state !== 'scattered') continue;\n    const pa = a.mesh.position;\n    for (let j = i + 1; j < cubes.length; j++) {\n      const b = cubes[j];\n      if (b.state !== 'scattered') continue;\n      const pb = b.mesh.position;\n      const dx = pb.x - pa.x, dy = pb.y - pa.y, dz = pb.z - pa.z;\n      const ox = CUBE_SIZE - Math.abs(dx);\n      const oy = CUBE_SIZE - Math.abs(dy);\n      const oz = CUBE_SIZE - Math.abs(dz);\n      if (ox > 0 && oy > 0 && oz > 0) {\n        const mo = Math.min(ox, oy, oz);\n        const push = mo \/ 2 + 0.001;\n        if (mo === ox) {\n          const s = dx >= 0 ? 1 : -1;\n          pa.x -= s*push; pb.x += s*push;\n          const imp = (a.vx - b.vx) * 0.5 * 0.4;\n          a.vx -= imp; b.vx += imp;\n        } else if (mo === oy) {\n          const s = dy >= 0 ? 1 : -1;\n          pa.y -= s*push; pb.y += s*push;\n          const imp = (a.vy - b.vy) * 0.5 * 0.4;\n          a.vy -= imp; b.vy += imp;\n        } else {\n          const s = dz >= 0 ? 1 : -1;\n          pa.z -= s*push; pb.z += s*push;\n          const imp = (a.vz - b.vz) * 0.5 * 0.4;\n          a.vz -= imp; b.vz += imp;\n        }\n      }\n    }\n  }\n}\n\n\/\/ \u2500\u2500 Main loop \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst clock = new THREE.Clock();\n\nfunction animate() {\n  requestAnimationFrame(animate);\n  const dt  = Math.min(clock.getDelta(), 0.033);\n  const now = performance.now();\n\n  \/\/ \u2500\u2500 Sequence controller \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  \/\/ Determine if any cube is currently morphing\n  const anyMorphing = cubes.some(c => c.state === 'morphing');\n\n  if (seqPhase === 'holding') {\n    const elapsed = now - seqPhaseStart;\n    \/\/ Only advance if: not the last step, hold duration passed, no cube is morphing\n    const isLastStep = (currentStep === STEPS.length - 1);\n    if (!isLastStep && elapsed >= HOLD_DURATION && !anyMorphing) {\n      \/\/ Advance to next step\n      const nextStep = currentStep + 1;\n      \/\/ Set all cubes to morphing state; scattered\/returning ones keep their scatter physics\n      \/\/ but will morph once they return\n      cubes.forEach((c, i) => {\n        const tgt = STEPS[nextStep][i];\n        c.currentTarget = { ...tgt };\n        \/\/ Only kick off morph on 'formed' cubes \u2014 scattered cubes will use currentTarget when returning\n        if (c.state === 'formed') {\n          c.state = 'morphing';\n          c._morphStart = { x: c.mesh.position.x, y: c.mesh.position.y, z: c.mesh.position.z };\n          c._morphT = 0;\n        }\n      });\n      currentStep = nextStep;\n      seqPhase = 'morphing';\n      seqPhaseStart = now;\n    }\n  }\n\n  if (seqPhase === 'morphing') {\n    \/\/ Check if all cubes are done morphing\n    const doneMorphing = cubes.every(c => c.state !== 'morphing');\n    if (doneMorphing) {\n      seqPhase = 'holding';\n      seqPhaseStart = now;\n    }\n  }\n\n  \/\/ \u2500\u2500 Per-cube update \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  cubes.forEach((c, i) => {\n    const m = c.mesh;\n\n    \/\/ \u2500 Morphing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n    if (c.state === 'morphing') {\n      c._morphT = Math.min(c._morphT + dt \/ (MORPH_DURATION \/ 1000), 1);\n      const e = easeOutCubic(c._morphT);\n      m.position.x = c._morphStart.x + (c.currentTarget.x - c._morphStart.x) * e;\n      m.position.y = c._morphStart.y + (c.currentTarget.y - c._morphStart.y) * e;\n      m.position.z = c._morphStart.z + (c.currentTarget.z - c._morphStart.z) * e;\n      m.rotation.set(0, 0, 0);\n      if (c._morphT >= 1) {\n        m.position.set(c.currentTarget.x, c.currentTarget.y, c.currentTarget.z);\n        c.state = 'formed';\n      }\n      return; \/\/ no scatter during morph\n    }\n\n    \/\/ \u2500 Formed idle \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n    if (c.state === 'formed') {\n      m.position.set(c.currentTarget.x, c.currentTarget.y, c.currentTarget.z);\n      m.rotation.set(0, 0, 0);\n      \/\/ Mouse repulsion \u2014 only on final step\n      if (currentStep === STEPS.length - 1 && mouseOverShape) {\n        const mdx = m.position.x - mouseWorld.x;\n        const mdy = m.position.y - mouseWorld.y;\n        const mdist = Math.sqrt(mdx*mdx + mdy*mdy);\n        if (mdist < REPULSE_R) {\n          c.state = 'scattered';\n          c.returnTimer = now + RETURN_DELAY;\n          c.scatterStartTime = now;\n          const f = ((REPULSE_R - mdist) \/ REPULSE_R) * REPULSE_F;\n          const nx = mdx \/ (mdist + 0.001);\n          const ny = mdy \/ (mdist + 0.001);\n          c.vx = nx * f;\n          c.vy = ny * f;\n          c.vz = (Math.random() - 0.5) * f * 0.3;\n          c.ax = (Math.random() - 0.5) * 0.18;\n          c.ay = (Math.random() - 0.5) * 0.18;\n          c.az = (Math.random() - 0.5) * 0.08;\n        }\n      }\n      return;\n    }\n\n    \/\/ \u2500 Scattered \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n    if (c.state === 'scattered') {\n      if (now >= c.returnTimer) {\n        c.state = 'returning';\n        c.returnStartTime = now;\n        c.returnStartPos = { x: m.position.x, y: m.position.y, z: m.position.z };\n        c.returnStartRot = { x: m.rotation.x, y: m.rotation.y, z: m.rotation.z };\n        return;\n      }\n      const launchT = Math.min((now - c.scatterStartTime) \/ c.scatterDuration, 1);\n      const launchEase = easeOutQuart(launchT);\n      const activeDrag = SCATTER_DRAG - (1 - launchEase) * (SCATTER_DRAG - 0.970);\n      c.vy -= GRAVITY * dt;\n      c.vx *= Math.pow(activeDrag, dt * 60);\n      c.vy *= Math.pow(activeDrag, dt * 60);\n      c.vz *= Math.pow(activeDrag, dt * 60);\n      m.position.x += c.vx * dt;\n      m.position.y += c.vy * dt;\n      m.position.z += c.vz * dt;\n      m.position.z = Math.max(-8, Math.min(8, m.position.z));\n      if (m.position.y < FLOOR_Y + HALF) {\n        m.position.y = FLOOR_Y + HALF;\n        c.vy = -c.vy * BOUNCE_DAMP;\n        c.vx *= 0.72; c.vz *= 0.72;\n        c.ax *= 0.55; c.ay *= 0.55; c.az *= 0.55;\n        if (Math.abs(c.vy) < 0.12) c.vy = 0;\n      }\n      if (m.position.x >  WALL_X) { m.position.x =  WALL_X; c.vx = -Math.abs(c.vx)*0.4; }\n      if (m.position.x < -WALL_X) { m.position.x = -WALL_X; c.vx =  Math.abs(c.vx)*0.4; }\n      m.rotation.x += c.ax; m.rotation.y += c.ay; m.rotation.z += c.az;\n      c.ax *= 0.987; c.ay *= 0.987; c.az *= 0.987;\n      return;\n    }\n\n    \/\/ \u2500 Returning \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n    if (c.state === 'returning') {\n      \/\/ Allow re-scatter only on last step\n      if (currentStep === STEPS.length - 1 && mouseOverShape) {\n        const mdx = m.position.x - mouseWorld.x;\n        const mdy = m.position.y - mouseWorld.y;\n        const mdist = Math.sqrt(mdx*mdx + mdy*mdy);\n        if (mdist < REPULSE_R) {\n          c.state = 'scattered';\n          c.scatterStartTime = now;\n          c.returnTimer = now + RETURN_DELAY;\n          const f = ((REPULSE_R - mdist) \/ REPULSE_R) * REPULSE_F;\n          c.vx = (mdx \/ (mdist+0.001)) * f;\n          c.vy = (mdy \/ (mdist+0.001)) * f;\n          c.vz = (Math.random() - 0.5) * f * 0.3;\n          c.ax = (Math.random() - 0.5) * 0.18;\n          c.ay = (Math.random() - 0.5) * 0.18;\n          c.az = (Math.random() - 0.5) * 0.08;\n          return;\n        }\n      }\n      const t = Math.min((now - c.returnStartTime) \/ 700, 1);\n      const e = easeOutQuint(t);\n      m.position.x = c.returnStartPos.x + (c.currentTarget.x - c.returnStartPos.x) * e;\n      m.position.y = c.returnStartPos.y + (c.currentTarget.y - c.returnStartPos.y) * e;\n      m.position.z = c.returnStartPos.z + (c.currentTarget.z - c.returnStartPos.z) * e;\n      m.rotation.x = c.returnStartRot.x * (1 - e);\n      m.rotation.y = c.returnStartRot.y * (1 - e);\n      m.rotation.z = c.returnStartRot.z * (1 - e);\n      if (t >= 1) {\n        m.position.set(c.currentTarget.x, c.currentTarget.y, c.currentTarget.z);\n        m.rotation.set(0, 0, 0);\n        c.vx = c.vy = c.vz = 0;\n        c.ax = c.ay = c.az = 0;\n        c.state = 'formed';\n      }\n    }\n  });\n\n  \/\/ Smooth camera orbit\n  camCurrentAngle += (camTargetAngle - camCurrentAngle) * 0.06;\n  camera.position.x = Math.sin(camCurrentAngle) * CAM_RADIUS;\n  camera.position.z = Math.cos(camCurrentAngle) * CAM_RADIUS;\n  camera.lookAt(0, 0, 0);\n  camera.updateMatrixWorld();\n  \/\/ Re-project mouse after camera moved\n  updateMouseWorld(lastMouseCX, lastMouseCY);\n\n  resolveCubeCollisions();\n  renderer.render(scene, camera);\n}\n\nanimate();\n\nwindow.addEventListener('resize', () => {\n  const w = window.innerWidth, h = window.innerHeight;\n  camera.aspect = w \/ h;\n  camera.updateProjectionMatrix();\n  renderer.setSize(w, h);\n});\n<\/script>\n<\/body>\n<\/html>\n\t\t\t\t<\/div>\n\t\t\n<\/div>\n<div class=\"elementor-element elementor-element-7b1cbb9e e-con e-atomic-element e-flexbox-base e-7b1cbb9e-cc7217a \" data-id=\"7b1cbb9e\" data-element_type=\"e-flexbox\" data-e-type=\"e-flexbox\" data-interaction-id=\"7b1cbb9e\" data-e-type=\"e-flexbox\" data-id=\"7b1cbb9e\">\n    <div class=\"elementor-element elementor-element-2276ae74 e-con e-atomic-element e-div-block-base e-2276ae74-872f57d \" data-id=\"2276ae74\" data-element_type=\"e-div-block\" data-e-type=\"e-div-block\" data-interaction-id=\"2276ae74\" data-e-type=\"e-div-block\" data-id=\"2276ae74\">\n    \n<\/div>\n<div class=\"elementor-element elementor-element-9ffa24e e-con e-atomic-element e-div-block-base e-9ffa24e-1b2b3b8 \" data-id=\"9ffa24e\" data-element_type=\"e-div-block\" data-e-type=\"e-div-block\" data-interaction-id=\"9ffa24e\" data-e-type=\"e-div-block\" data-id=\"9ffa24e\">\n    \t\t\t<h1 data-interaction-id=\"3445bb38\" class=\"e-3445bb38-7189cf2 e-heading-base\" data-e-type=\"widget\" data-id=\"3445bb38\"><strong>\u0633\u0647\u06cc\u0644 \u067e\u0648\u0646\u06a9\u06cc <\/strong><\/h1>\n\t\t\t\t\t<h1 data-interaction-id=\"7796f961\" class=\"e-7796f961-d742a03 e-heading-base\" data-e-type=\"widget\" data-id=\"7796f961\"><strong>\u0637\u0631\u0627\u062d \u06af\u0631\u0627\u0641\u06cc\u06a9 <\/strong><\/h1>\n\t\t\n<\/div>\n<div class=\"elementor-element elementor-element-1015ebdb e-con e-atomic-element e-div-block-base e-1015ebdb-72d27e8 \" data-id=\"1015ebdb\" data-element_type=\"e-div-block\" data-e-type=\"e-div-block\" data-interaction-id=\"1015ebdb\" data-e-type=\"e-div-block\" data-id=\"1015ebdb\">\n    \n<\/div>\n<div class=\"elementor-element elementor-element-45becf63 e-con e-atomic-element e-div-block-base e-45becf63-c6bcb46 \" data-id=\"45becf63\" data-element_type=\"e-div-block\" data-e-type=\"e-div-block\" data-interaction-id=\"45becf63\" data-e-type=\"e-div-block\" data-id=\"45becf63\">\n    \t\t\t<h3 data-interaction-id=\"3543eb4c\" class=\"e-3543eb4c-6211914 e-heading-base\" data-e-type=\"widget\" data-id=\"3543eb4c\">\u0631\u0632\u0648\u0645\u0647<\/h3>\n\t\t\n<\/div>\n<div class=\"elementor-element elementor-element-730f4a3d e-con e-atomic-element e-div-block-base e-730f4a3d-66adb8c \" data-id=\"730f4a3d\" data-element_type=\"e-div-block\" data-e-type=\"e-div-block\" data-interaction-id=\"730f4a3d\" data-e-type=\"e-div-block\" data-id=\"730f4a3d\">\n    \t\t\t<h3 data-interaction-id=\"6e40b200\" class=\"e-6e40b200-804f83f e-heading-base\" data-e-type=\"widget\" data-id=\"6e40b200\">\u062a\u0645\u0627\u0633<\/h3>\n\t\t\n<\/div>\n<div class=\"elementor-element elementor-element-4b43930a e-con e-atomic-element e-div-block-base e-4b43930a-4334c68 \" data-id=\"4b43930a\" data-element_type=\"e-div-block\" data-e-type=\"e-div-block\" data-interaction-id=\"4b43930a\" data-e-type=\"e-div-block\" data-id=\"4b43930a\">\n    \t\t\t<h3 data-interaction-id=\"69e60516\" class=\"e-69e60516-f390b0d e-heading-base\" data-e-type=\"widget\" data-id=\"69e60516\">\u062e\u0627\u0646\u0647<\/h3>\n\t\t\n<\/div>\n<div class=\"elementor-element elementor-element-22422988 e-con e-atomic-element e-div-block-base e-22422988-934f56d \" data-id=\"22422988\" data-element_type=\"e-div-block\" data-e-type=\"e-div-block\" data-interaction-id=\"22422988\" data-e-type=\"e-div-block\" data-id=\"22422988\">\n    \t\t\t<h3 data-interaction-id=\"3a7bfff1\" class=\"e-3a7bfff1-a0b9b8b e-heading-base\" data-e-type=\"widget\" data-id=\"3a7bfff1\">\u0628\u0644\u0627\u06af<\/h3>\n\t\t\n<\/div>\n\n<\/div>\n\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>Letter Cubes move mouse over the shape to scatter \u00b7 cubes return on their own \u0633\u0647\u06cc\u0644 \u067e\u0648\u0646\u06a9\u06cc \u0637\u0631\u0627\u062d \u06af\u0631\u0627\u0641\u06cc\u06a9 \u0631\u0632\u0648\u0645\u0647 \u062a\u0645\u0627\u0633 \u062e\u0627\u0646\u0647 \u0628\u0644\u0627\u06af<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"elementor_canvas","meta":{"footnotes":""},"class_list":["post-37","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/poonaki.ir\/index.php?rest_route=\/wp\/v2\/pages\/37","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/poonaki.ir\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/poonaki.ir\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/poonaki.ir\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/poonaki.ir\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=37"}],"version-history":[{"count":13,"href":"https:\/\/poonaki.ir\/index.php?rest_route=\/wp\/v2\/pages\/37\/revisions"}],"predecessor-version":[{"id":70,"href":"https:\/\/poonaki.ir\/index.php?rest_route=\/wp\/v2\/pages\/37\/revisions\/70"}],"wp:attachment":[{"href":"https:\/\/poonaki.ir\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=37"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}