@@ -89,33 +89,36 @@ export function createOllie(scene, gradientMap) { |
| 89 | 89 | rightShine.position.set(0.35, 0.4, -0.38) |
| 90 | 90 | group.add(rightShine) |
| 91 | 91 | |
| 92 | | - // Create 8 tentacles |
| 92 | + // Create 8 tentacles - splayed outward evenly around the body like \./ |
| 93 | 93 | const tentacles = [] |
| 94 | 94 | const tentacleGroup = new THREE.Group() |
| 95 | | - tentacleGroup.position.y = -0.3 |
| 95 | + tentacleGroup.position.y = -0.05 // Raised up so tentacles emerge above water |
| 96 | 96 | |
| 97 | 97 | for (let i = 0; i < 8; i++) { |
| 98 | 98 | const angle = (i / 8) * Math.PI * 2 |
| 99 | 99 | const tentacle = createTentacle(bodyMaterial, suckerMaterial, gradientMap) |
| 100 | + |
| 101 | + // Position at edge of lower body |
| 100 | 102 | tentacle.position.x = Math.cos(angle) * 0.35 |
| 101 | 103 | tentacle.position.z = Math.sin(angle) * 0.35 |
| 102 | | - tentacle.rotation.y = -angle + Math.PI / 2 |
| 103 | | - // Splay outward slightly |
| 104 | | - tentacle.rotation.z = 0.3 |
| 104 | + |
| 105 | + // Simply rotate around Y to spread evenly - tentacles extend in +Z and curve up |
| 106 | + tentacle.rotation.y = angle |
| 107 | + |
| 105 | 108 | tentacles.push(tentacle) |
| 106 | 109 | tentacleGroup.add(tentacle) |
| 107 | 110 | } |
| 108 | 111 | |
| 109 | 112 | group.add(tentacleGroup) |
| 110 | 113 | |
| 111 | | - // Magnifying glass - attached to front-right tentacle (index 1) |
| 114 | + // Magnifying glass - will be attached to front tentacle (index 0) |
| 112 | 115 | const magGlassGroup = new THREE.Group() |
| 113 | 116 | |
| 114 | | - // Handle |
| 115 | | - const handleGeom = new THREE.CylinderGeometry(0.02, 0.025, 0.3, 6) |
| 117 | + // Handle - longer and thicker for visibility |
| 118 | + const handleGeom = new THREE.CylinderGeometry(0.03, 0.035, 0.25, 6) |
| 116 | 119 | const handle = new THREE.Mesh(handleGeom, glassRimMaterial) |
| 117 | 120 | handle.rotation.z = Math.PI / 2 |
| 118 | | - handle.position.x = -0.15 |
| 121 | + handle.position.x = -0.18 |
| 119 | 122 | magGlassGroup.add(handle) |
| 120 | 123 | |
| 121 | 124 | // Rim |
@@ -129,10 +132,30 @@ export function createOllie(scene, gradientMap) { |
| 129 | 132 | lens.position.z = 0.01 |
| 130 | 133 | magGlassGroup.add(lens) |
| 131 | 134 | |
| 132 | | - // Position magnifying glass at end of front-right tentacle |
| 133 | | - magGlassGroup.position.set(0.9, -0.5, -0.3) |
| 134 | | - magGlassGroup.rotation.y = -0.5 |
| 135 | | - group.add(magGlassGroup) |
| 135 | + // Back of lens for visibility from other side |
| 136 | + const lensBack = new THREE.Mesh(lensGeom, glassMaterial) |
| 137 | + lensBack.position.z = -0.01 |
| 138 | + lensBack.rotation.y = Math.PI |
| 139 | + magGlassGroup.add(lensBack) |
| 140 | + |
| 141 | + // Attach magnifying glass to the TIP of tentacle 2 (side tentacle, away from body) |
| 142 | + // Navigate to the last joint of that tentacle |
| 143 | + let magTentacle = tentacles[2] |
| 144 | + let lastJoint = magTentacle.children[0] // First joint |
| 145 | + while (lastJoint.children.length > 1 || (lastJoint.children[0] && lastJoint.children[0].type === 'Group')) { |
| 146 | + // Find the child that is a Group (the next joint) |
| 147 | + const nextJoint = lastJoint.children.find(c => c.type === 'Group') |
| 148 | + if (nextJoint) { |
| 149 | + lastJoint = nextJoint |
| 150 | + } else { |
| 151 | + break |
| 152 | + } |
| 153 | + } |
| 154 | + |
| 155 | + // Position at the tip of the last segment |
| 156 | + magGlassGroup.position.set(0, 0.05, 0.15) |
| 157 | + magGlassGroup.rotation.x = 0.5 // Angle it forward/up for visibility |
| 158 | + lastJoint.add(magGlassGroup) |
| 136 | 159 | |
| 137 | 160 | // Ollie starts hidden below the water |
| 138 | 161 | group.position.y = -3 |
@@ -150,42 +173,54 @@ export function createOllie(scene, gradientMap) { |
| 150 | 173 | } |
| 151 | 174 | |
| 152 | 175 | function createTentacle(bodyMat, suckerMat, gradient) { |
| 176 | + // Build tentacle as a chain of segments, each one a child of the previous |
| 177 | + // This creates a smooth curve by rotating each joint |
| 153 | 178 | const tentacleObj = new THREE.Group() |
| 154 | 179 | |
| 155 | | - // 3 segments, getting smaller |
| 156 | | - const segments = [ |
| 157 | | - { radius: 0.08, length: 0.35 }, |
| 158 | | - { radius: 0.06, length: 0.3 }, |
| 159 | | - { radius: 0.04, length: 0.25 } |
| 160 | | - ] |
| 180 | + const numSegments = 5 |
| 181 | + let currentParent = tentacleObj |
| 182 | + |
| 183 | + for (let idx = 0; idx < numSegments; idx++) { |
| 184 | + const radius = 0.065 - idx * 0.01 |
| 185 | + const length = 0.18 - idx * 0.015 |
| 186 | + |
| 187 | + // Create a joint group for this segment |
| 188 | + const joint = new THREE.Group() |
| 189 | + |
| 190 | + // Position joint at end of parent (except first one at origin) |
| 191 | + if (idx > 0) { |
| 192 | + joint.position.z = 0.16 - (idx - 1) * 0.012 // Length of previous segment |
| 193 | + } |
| 194 | + |
| 195 | + // Rotate joint to curve upward - more curve toward the tip |
| 196 | + joint.rotation.x = -0.28 - idx * 0.08 |
| 197 | + |
| 198 | + // Create the segment mesh |
| 199 | + const segGeom = new THREE.CylinderGeometry(radius * 0.7, radius, length, 6) |
| 200 | + segGeom.rotateX(Math.PI / 2) // Lay along Z axis |
| 201 | + segGeom.translate(0, 0, length / 2) // Move so base is at origin |
| 161 | 202 | |
| 162 | | - let yOffset = 0 |
| 163 | | - segments.forEach((seg, idx) => { |
| 164 | | - const segGeom = new THREE.CylinderGeometry(seg.radius * 0.7, seg.radius, seg.length, 6) |
| 165 | 203 | const segMesh = new THREE.Mesh(segGeom, bodyMat) |
| 166 | | - segMesh.position.y = yOffset - seg.length / 2 |
| 167 | | - tentacleObj.add(segMesh) |
| 168 | | - |
| 169 | | - // Add suckers on underside (only first two segments) |
| 170 | | - if (idx < 2) { |
| 171 | | - for (let s = 0; s < 2; s++) { |
| 172 | | - const suckerGeom = new THREE.SphereGeometry(0.015, 4, 4) |
| 173 | | - const sucker = new THREE.Mesh(suckerGeom, suckerMat) |
| 174 | | - sucker.position.set(-seg.radius * 0.8, yOffset - seg.length * 0.3 - s * 0.12, 0) |
| 175 | | - sucker.scale.set(1, 0.5, 1) |
| 176 | | - tentacleObj.add(sucker) |
| 177 | | - } |
| 204 | + joint.add(segMesh) |
| 205 | + |
| 206 | + // Add suckers on underside |
| 207 | + if (idx < 4) { |
| 208 | + const suckerGeom = new THREE.SphereGeometry(0.015, 4, 4) |
| 209 | + const sucker = new THREE.Mesh(suckerGeom, suckerMat) |
| 210 | + sucker.position.set(0, -radius * 0.85, length * 0.5) |
| 211 | + sucker.scale.set(1, 0.5, 1) |
| 212 | + joint.add(sucker) |
| 178 | 213 | } |
| 179 | 214 | |
| 180 | | - yOffset -= seg.length |
| 181 | | - }) |
| 215 | + currentParent.add(joint) |
| 216 | + currentParent = joint |
| 217 | + } |
| 182 | 218 | |
| 183 | | - // Curly tip |
| 184 | | - const tipGeom = new THREE.SphereGeometry(0.03, 6, 4) |
| 185 | | - tipGeom.scale(1, 1.5, 1) |
| 219 | + // Curly tip at the end |
| 220 | + const tipGeom = new THREE.SphereGeometry(0.02, 6, 4) |
| 186 | 221 | const tip = new THREE.Mesh(tipGeom, bodyMat) |
| 187 | | - tip.position.y = yOffset - 0.03 |
| 188 | | - tentacleObj.add(tip) |
| 222 | + tip.position.z = 0.12 |
| 223 | + currentParent.add(tip) |
| 189 | 224 | |
| 190 | 225 | return tentacleObj |
| 191 | 226 | } |
@@ -228,15 +263,17 @@ export function createOllie(scene, gradientMap) { |
| 228 | 263 | // Animate tentacles (always, when visible) |
| 229 | 264 | if (group.visible) { |
| 230 | 265 | tentacles.forEach((t, i) => { |
| 266 | + const baseAngle = (i / 8) * Math.PI * 2 |
| 231 | 267 | const phase = i * (Math.PI / 4) |
| 232 | | - // Wave motion |
| 233 | | - t.rotation.x = 0.3 + Math.sin(elapsed * 2 + phase) * 0.25 |
| 234 | | - t.rotation.z = 0.3 + Math.cos(elapsed * 1.5 + phase) * 0.15 |
| 268 | + // Gentle swaying - each tentacle waves side to side |
| 269 | + t.rotation.y = baseAngle + Math.sin(elapsed * 1.5 + phase) * 0.12 |
| 270 | + // Slight up/down bob |
| 271 | + t.rotation.x = Math.sin(elapsed * 2 + phase) * 0.08 |
| 235 | 272 | }) |
| 236 | 273 | |
| 237 | | - // Magnifying glass sway |
| 238 | | - magGlassGroup.rotation.z = Math.sin(elapsed * 3) * 0.15 |
| 239 | | - magGlassGroup.rotation.x = Math.sin(elapsed * 2.5) * 0.1 |
| 274 | + // Magnifying glass gets a little extra wobble for curious inspection look |
| 275 | + magGlassGroup.rotation.z = Math.sin(elapsed * 3) * 0.1 |
| 276 | + magGlassGroup.rotation.y = Math.sin(elapsed * 2) * 0.15 |
| 240 | 277 | } |
| 241 | 278 | |
| 242 | 279 | switch (state.mode) { |
@@ -315,10 +352,13 @@ export function createOllie(scene, gradientMap) { |
| 315 | 352 | const easeIn = Math.pow(submergeProgress, 2) |
| 316 | 353 | group.position.y = 0.2 - easeIn * 2.5 |
| 317 | 354 | |
| 318 | | - // Tentacles curl inward as submerging |
| 355 | + // Tentacles curl as submerging |
| 319 | 356 | tentacles.forEach((t, i) => { |
| 357 | + const baseAngle = (i / 8) * Math.PI * 2 |
| 320 | 358 | const phase = i * (Math.PI / 4) |
| 321 | | - t.rotation.x = 0.3 + easeIn * 0.5 + Math.sin(elapsed * 2 + phase) * 0.15 |
| 359 | + t.rotation.y = baseAngle + Math.sin(elapsed * 2 + phase) * 0.05 |
| 360 | + // Curl downward as sinking |
| 361 | + t.rotation.x = easeIn * 0.4 + Math.sin(elapsed * 2 + phase) * 0.05 |
| 322 | 362 | }) |
| 323 | 363 | |
| 324 | 364 | // Add ripples as submerging |