JavaScript · 8309 bytes Raw Blame History
1 // Donny the Narwhal - distinguished gentleman of the deep
2 import * as THREE from 'three'
3
4 export function createDonny(scene, gradientMap) {
5 const group = new THREE.Group()
6
7 // Color palette
8 const bodyColor = 0x7a9eb8 // Dusty blue-grey
9 const bellyColor = 0xc8d8e4 // Pale belly
10 const tuskColor = 0xf5f0e6 // Ivory
11 const monocleColor = 0xd4af37 // Gold
12
13 // Materials
14 const bodyMaterial = new THREE.MeshToonMaterial({
15 color: bodyColor,
16 gradientMap: gradientMap
17 })
18
19 const bellyMaterial = new THREE.MeshToonMaterial({
20 color: bellyColor,
21 gradientMap: gradientMap
22 })
23
24 const tuskMaterial = new THREE.MeshToonMaterial({
25 color: tuskColor,
26 gradientMap: gradientMap
27 })
28
29 const monocleMaterial = new THREE.MeshToonMaterial({
30 color: monocleColor,
31 gradientMap: gradientMap
32 })
33
34 const glassMaterial = new THREE.MeshBasicMaterial({
35 color: 0x88ccff,
36 transparent: true,
37 opacity: 0.3
38 })
39
40 // Main body - elongated oval
41 const bodyGeom = new THREE.SphereGeometry(0.5, 8, 6)
42 bodyGeom.scale(2.2, 0.7, 0.8)
43 const body = new THREE.Mesh(bodyGeom, bodyMaterial)
44 body.position.y = 0.1
45 group.add(body)
46
47 // Belly
48 const bellyGeom = new THREE.SphereGeometry(0.4, 8, 6)
49 bellyGeom.scale(1.8, 0.5, 0.7)
50 const belly = new THREE.Mesh(bellyGeom, bellyMaterial)
51 belly.position.set(0, -0.05, 0)
52 group.add(belly)
53
54 // Head bump
55 const headGeom = new THREE.SphereGeometry(0.35, 8, 6)
56 headGeom.scale(1.2, 1, 1)
57 const head = new THREE.Mesh(headGeom, bodyMaterial)
58 head.position.set(1.0, 0.25, 0)
59 group.add(head)
60
61 // The magnificent tusk!
62 const tuskGeom = new THREE.ConeGeometry(0.06, 1.8, 6)
63 const tusk = new THREE.Mesh(tuskGeom, tuskMaterial)
64 tusk.position.set(1.6, 0.35, 0)
65 tusk.rotation.z = -Math.PI / 2 + 0.15 // Pointing forward, slightly up
66 // Add spiral ridges (simplified with rotation)
67 tusk.rotation.y = 0.3
68 group.add(tusk)
69
70 // Eyes
71 const eyeGeom = new THREE.SphereGeometry(0.08, 8, 6)
72 const eyeMaterial = new THREE.MeshBasicMaterial({ color: 0x1a1a1a })
73
74 const leftEye = new THREE.Mesh(eyeGeom, eyeMaterial)
75 leftEye.position.set(1.15, 0.38, 0.28)
76 group.add(leftEye)
77
78 const rightEye = new THREE.Mesh(eyeGeom, eyeMaterial)
79 rightEye.position.set(1.15, 0.38, -0.28)
80 group.add(rightEye)
81
82 // Eye shines
83 const shineGeom = new THREE.SphereGeometry(0.025, 6, 4)
84 const shineMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff })
85
86 const leftShine = new THREE.Mesh(shineGeom, shineMaterial)
87 leftShine.position.set(1.18, 0.42, 0.3)
88 group.add(leftShine)
89
90 const rightShine = new THREE.Mesh(shineGeom, shineMaterial)
91 rightShine.position.set(1.18, 0.42, -0.26)
92 group.add(rightShine)
93
94 // THE MONOCLE - on right eye (our left when facing)
95 const monocleGroup = new THREE.Group()
96
97 // Monocle rim
98 const rimGeom = new THREE.TorusGeometry(0.12, 0.015, 8, 16)
99 const rim = new THREE.Mesh(rimGeom, monocleMaterial)
100 monocleGroup.add(rim)
101
102 // Monocle glass
103 const glassGeom = new THREE.CircleGeometry(0.11, 16)
104 const glass = new THREE.Mesh(glassGeom, glassMaterial)
105 glass.position.z = 0.01
106 monocleGroup.add(glass)
107
108 // Monocle chain attachment
109 const chainStartGeom = new THREE.SphereGeometry(0.02, 6, 4)
110 const chainStart = new THREE.Mesh(chainStartGeom, monocleMaterial)
111 chainStart.position.set(0, -0.12, 0)
112 monocleGroup.add(chainStart)
113
114 // Chain (simple dangling segments)
115 const chainMaterial = new THREE.MeshToonMaterial({
116 color: monocleColor,
117 gradientMap: gradientMap
118 })
119 for (let i = 0; i < 4; i++) {
120 const linkGeom = new THREE.TorusGeometry(0.018, 0.005, 4, 8)
121 const link = new THREE.Mesh(linkGeom, chainMaterial)
122 link.position.set(0, -0.16 - i * 0.05, 0)
123 link.rotation.x = i % 2 === 0 ? 0 : Math.PI / 2
124 monocleGroup.add(link)
125 }
126
127 monocleGroup.position.set(1.22, 0.38, -0.32)
128 monocleGroup.rotation.y = -0.3
129 group.add(monocleGroup)
130
131 // Flippers
132 const flipperGeom = new THREE.ConeGeometry(0.15, 0.5, 4)
133
134 const leftFlipper = new THREE.Mesh(flipperGeom, bodyMaterial)
135 leftFlipper.position.set(0.3, -0.1, 0.45)
136 leftFlipper.rotation.x = 0.5
137 leftFlipper.rotation.z = 2.2
138 group.add(leftFlipper)
139
140 const rightFlipper = new THREE.Mesh(flipperGeom, bodyMaterial)
141 rightFlipper.position.set(0.3, -0.1, -0.45)
142 rightFlipper.rotation.x = -0.5
143 rightFlipper.rotation.z = 2.2
144 group.add(rightFlipper)
145
146 // Tail flukes
147 const flukeGeom = new THREE.ConeGeometry(0.2, 0.4, 4)
148
149 const leftFluke = new THREE.Mesh(flukeGeom, bodyMaterial)
150 leftFluke.position.set(-1.2, 0.15, 0.15)
151 leftFluke.rotation.z = 1.8
152 leftFluke.rotation.y = 0.3
153 group.add(leftFluke)
154
155 const rightFluke = new THREE.Mesh(flukeGeom, bodyMaterial)
156 rightFluke.position.set(-1.2, 0.15, -0.15)
157 rightFluke.rotation.z = 1.8
158 rightFluke.rotation.y = -0.3
159 group.add(rightFluke)
160
161 // Donny starts hidden below the water
162 group.position.y = -3
163 group.visible = false
164
165 scene.add(group)
166
167 // State
168 const state = {
169 mode: 'waiting', // 'waiting', 'rumbling', 'emerging', 'surfaced', 'submerging'
170 timer: 30 + Math.random() * 30, // First appearance in 30-60 seconds
171 emergeX: 0,
172 emergeZ: 0,
173 surfaceTime: 0
174 }
175
176 function startRumble(pond) {
177 state.mode = 'rumbling'
178 state.timer = 0
179
180 // Pick random spot in the pond
181 const angle = Math.random() * Math.PI * 2
182 const dist = Math.random() * pond.radius * 0.5 + pond.radius * 0.2
183 state.emergeX = Math.cos(angle) * dist
184 state.emergeZ = Math.sin(angle) * dist
185
186 group.position.x = state.emergeX
187 group.position.z = state.emergeZ
188 group.rotation.y = angle + Math.PI / 2 // Face outward-ish
189 }
190
191 function update(delta, elapsed, pond) {
192 state.timer += delta
193
194 switch (state.mode) {
195 case 'waiting':
196 if (state.timer >= 60) {
197 startRumble(pond)
198 }
199 break
200
201 case 'rumbling':
202 // Create rumble ripples
203 if (state.timer < 2) {
204 if (Math.random() < delta * 8) {
205 const rx = state.emergeX + (Math.random() - 0.5) * 0.8
206 const rz = state.emergeZ + (Math.random() - 0.5) * 0.8
207 pond.addRipple(rx, rz)
208 }
209 } else {
210 state.mode = 'emerging'
211 state.timer = 0
212 group.visible = true
213 group.position.y = -2
214 }
215 break
216
217 case 'emerging':
218 // Rise from the water
219 const emergeProgress = Math.min(state.timer / 1.5, 1)
220 const easeOut = 1 - Math.pow(1 - emergeProgress, 3)
221 group.position.y = -2 + easeOut * 2.3 // Rise to 0.3 above water
222
223 // Gentle rocking as emerging
224 group.rotation.z = Math.sin(state.timer * 4) * 0.1
225
226 if (emergeProgress >= 1) {
227 state.mode = 'surfaced'
228 state.timer = 0
229 state.surfaceTime = 4 + Math.random() * 3 // Stay 4-7 seconds
230 }
231 break
232
233 case 'surfaced':
234 // Bob gently on the surface
235 group.position.y = 0.3 + Math.sin(elapsed * 2) * 0.08
236 group.rotation.z = Math.sin(elapsed * 1.5) * 0.05
237
238 // Gentle flipper animation
239 leftFlipper.rotation.z = 2.2 + Math.sin(elapsed * 3) * 0.15
240 rightFlipper.rotation.z = 2.2 + Math.sin(elapsed * 3 + 0.5) * 0.15
241
242 // Occasional ripples
243 if (Math.random() < delta * 0.5) {
244 pond.addRipple(
245 group.position.x + (Math.random() - 0.5) * 0.5,
246 group.position.z + (Math.random() - 0.5) * 0.5
247 )
248 }
249
250 if (state.timer >= state.surfaceTime) {
251 state.mode = 'submerging'
252 state.timer = 0
253 }
254 break
255
256 case 'submerging':
257 // Sink back down
258 const submergeProgress = Math.min(state.timer / 1.2, 1)
259 const easeIn = Math.pow(submergeProgress, 2)
260 group.position.y = 0.3 - easeIn * 2.5
261
262 // Add bubbles/ripples as submerging
263 if (Math.random() < delta * 4) {
264 pond.addRipple(
265 group.position.x + (Math.random() - 0.5) * 0.6,
266 group.position.z + (Math.random() - 0.5) * 0.6
267 )
268 }
269
270 if (submergeProgress >= 1) {
271 state.mode = 'waiting'
272 state.timer = 0
273 group.visible = false
274 group.position.y = -3
275 }
276 break
277 }
278 }
279
280 return {
281 group,
282 update
283 }
284 }