zeroed-some/dougk / 4b1b2b6

Browse files

init. quacking merrily

Authored by espadonne
SHA
4b1b2b6a8a07c3f17a375b18421e87a55785e00c
Tree
955275a

18 changed files

StatusFile+-
A .gitignore 5 0
A DOUGK.md 18 0
A deploy.sh 29 0
A dougk.musicsian.com.conf 20 0
A index.html 29 0
A package-lock.json 958 0
A package.json 18 0
A src/main.js 108 0
A src/renderers/p5/bread.js 86 0
A src/renderers/p5/duck.js 215 0
A src/renderers/p5/index.js 96 0
A src/renderers/p5/pond.js 131 0
A src/renderers/three/bread.js 105 0
A src/renderers/three/duck.js 279 0
A src/renderers/three/index.js 179 0
A src/renderers/three/main.js 167 0
A src/renderers/three/pond.js 211 0
A vite.config.js 9 0
.gitignoreadded
@@ -0,0 +1,5 @@
1
+node_modules/
2
+dist/
3
+.DS_Store
4
+*.log
5
+.vite/
DOUGK.mdadded
@@ -0,0 +1,18 @@
1
+# dougk
2
+
3
+a cozy pond simulator on the web. featuring doug the duck
4
+
5
+the premise is simple: dougk is a simple pond rendering with a happy duck named doug that wanders around the pond and surrounding shores, featuring a preset of seamless fun and quirky animations to engage the user with doug.
6
+
7
+i'm envisioning the pond having a rickety wooden fence on one side, and aside from that we're striving for simplicity and mindfulness-maxxing. it should be quirky though. the duck should be quirky.
8
+
9
+if the user clicks anywhere within the water, it would drop little bits of bread into the pond, and dougk will waddle over to the location and eat the bits. 
10
+if you're not giving any clicks for bread drops. the dougk should just mill about the pond at its leisure. the duck should just be vibing. moving slowly, just chilling. but if you click
11
+if you click. as long as its within the water area of the visual, will drop a few bits of bread and when you do that the duck will after a short lag to emphasize the duck's quirky laziness, meander over to the bread
12
+
13
+we want the whole thing to feel natural, seamless. no hitches.
14
+
15
+Don't go overboard! Simple duck moving about responding to stimuli as described above.
16
+
17
+what stack are we using? I have no idea the best framework/s for animations that are interactive.
18
+
deploy.shadded
@@ -0,0 +1,29 @@
1
+#!/usr/bin/env bash
2
+set -euo pipefail
3
+
4
+STAMP=$(date +%Y-%m-%d-%H%M%S)
5
+OUTDIR=~/builds/$STAMP
6
+SITE=/var/www/dougk.musicsian.com
7
+
8
+echo "▶ npm ci"
9
+npm ci
10
+
11
+echo "▶ npm run build"
12
+npm run build
13
+mv dist "$OUTDIR"
14
+
15
+echo "▶ copy into releases"
16
+sudo mkdir -p "$SITE/releases"
17
+sudo rsync -az --delete "$OUTDIR"/ "$SITE/releases/$STAMP/"
18
+
19
+echo "▶ fix selinux context"
20
+sudo restorecon -Rv "$SITE/releases/$STAMP"
21
+
22
+echo "▶ flip current symlink"
23
+sudo rm -rf "$SITE/current"
24
+sudo ln -s "$SITE/releases/$STAMP" "$SITE/current"
25
+
26
+echo "▶ reload nginx"
27
+sudo systemctl reload nginx
28
+
29
+echo "✓ Deployed dougk $STAMP"
dougk.musicsian.com.confadded
@@ -0,0 +1,20 @@
1
+server {
2
+    server_name dougk.musicsian.com;
3
+    root  /var/www/dougk.musicsian.com/current;
4
+    index index.html;
5
+
6
+    access_log /var/log/nginx/dougk.access.log;
7
+    error_log  /var/log/nginx/dougk.error.log;
8
+
9
+    add_header X-Frame-Options "SAMEORIGIN" always;
10
+    add_header X-Content-Type-Options "nosniff" always;
11
+    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
12
+
13
+    location /assets/ {
14
+        access_log off;
15
+        expires 30d;
16
+        add_header Cache-Control "public";
17
+    }
18
+
19
+    listen 80;
20
+}
index.htmladded
@@ -0,0 +1,29 @@
1
+<!DOCTYPE html>
2
+<html lang="en">
3
+<head>
4
+  <meta charset="UTF-8">
5
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+  <title>dougk - a cozy pond</title>
7
+  <style>
8
+    * {
9
+      margin: 0;
10
+      padding: 0;
11
+      box-sizing: border-box;
12
+    }
13
+    body {
14
+      overflow: hidden;
15
+      background: #2d5a3d;
16
+      display: flex;
17
+      justify-content: center;
18
+      align-items: center;
19
+      min-height: 100vh;
20
+    }
21
+    canvas {
22
+      display: block;
23
+    }
24
+  </style>
25
+</head>
26
+<body>
27
+  <script type="module" src="/src/main.js"></script>
28
+</body>
29
+</html>
package-lock.jsonadded
@@ -0,0 +1,958 @@
1
+{
2
+  "name": "dougk",
3
+  "version": "0.1.0",
4
+  "lockfileVersion": 3,
5
+  "requires": true,
6
+  "packages": {
7
+    "": {
8
+      "name": "dougk",
9
+      "version": "0.1.0",
10
+      "dependencies": {
11
+        "p5": "^1.9.0",
12
+        "three": "^0.170.0"
13
+      },
14
+      "devDependencies": {
15
+        "vite": "^5.0.0"
16
+      }
17
+    },
18
+    "node_modules/@esbuild/aix-ppc64": {
19
+      "version": "0.21.5",
20
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
21
+      "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
22
+      "cpu": [
23
+        "ppc64"
24
+      ],
25
+      "dev": true,
26
+      "license": "MIT",
27
+      "optional": true,
28
+      "os": [
29
+        "aix"
30
+      ],
31
+      "engines": {
32
+        "node": ">=12"
33
+      }
34
+    },
35
+    "node_modules/@esbuild/android-arm": {
36
+      "version": "0.21.5",
37
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
38
+      "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
39
+      "cpu": [
40
+        "arm"
41
+      ],
42
+      "dev": true,
43
+      "license": "MIT",
44
+      "optional": true,
45
+      "os": [
46
+        "android"
47
+      ],
48
+      "engines": {
49
+        "node": ">=12"
50
+      }
51
+    },
52
+    "node_modules/@esbuild/android-arm64": {
53
+      "version": "0.21.5",
54
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
55
+      "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
56
+      "cpu": [
57
+        "arm64"
58
+      ],
59
+      "dev": true,
60
+      "license": "MIT",
61
+      "optional": true,
62
+      "os": [
63
+        "android"
64
+      ],
65
+      "engines": {
66
+        "node": ">=12"
67
+      }
68
+    },
69
+    "node_modules/@esbuild/android-x64": {
70
+      "version": "0.21.5",
71
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
72
+      "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
73
+      "cpu": [
74
+        "x64"
75
+      ],
76
+      "dev": true,
77
+      "license": "MIT",
78
+      "optional": true,
79
+      "os": [
80
+        "android"
81
+      ],
82
+      "engines": {
83
+        "node": ">=12"
84
+      }
85
+    },
86
+    "node_modules/@esbuild/darwin-arm64": {
87
+      "version": "0.21.5",
88
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
89
+      "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
90
+      "cpu": [
91
+        "arm64"
92
+      ],
93
+      "dev": true,
94
+      "license": "MIT",
95
+      "optional": true,
96
+      "os": [
97
+        "darwin"
98
+      ],
99
+      "engines": {
100
+        "node": ">=12"
101
+      }
102
+    },
103
+    "node_modules/@esbuild/darwin-x64": {
104
+      "version": "0.21.5",
105
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
106
+      "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
107
+      "cpu": [
108
+        "x64"
109
+      ],
110
+      "dev": true,
111
+      "license": "MIT",
112
+      "optional": true,
113
+      "os": [
114
+        "darwin"
115
+      ],
116
+      "engines": {
117
+        "node": ">=12"
118
+      }
119
+    },
120
+    "node_modules/@esbuild/freebsd-arm64": {
121
+      "version": "0.21.5",
122
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
123
+      "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
124
+      "cpu": [
125
+        "arm64"
126
+      ],
127
+      "dev": true,
128
+      "license": "MIT",
129
+      "optional": true,
130
+      "os": [
131
+        "freebsd"
132
+      ],
133
+      "engines": {
134
+        "node": ">=12"
135
+      }
136
+    },
137
+    "node_modules/@esbuild/freebsd-x64": {
138
+      "version": "0.21.5",
139
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
140
+      "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
141
+      "cpu": [
142
+        "x64"
143
+      ],
144
+      "dev": true,
145
+      "license": "MIT",
146
+      "optional": true,
147
+      "os": [
148
+        "freebsd"
149
+      ],
150
+      "engines": {
151
+        "node": ">=12"
152
+      }
153
+    },
154
+    "node_modules/@esbuild/linux-arm": {
155
+      "version": "0.21.5",
156
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
157
+      "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
158
+      "cpu": [
159
+        "arm"
160
+      ],
161
+      "dev": true,
162
+      "license": "MIT",
163
+      "optional": true,
164
+      "os": [
165
+        "linux"
166
+      ],
167
+      "engines": {
168
+        "node": ">=12"
169
+      }
170
+    },
171
+    "node_modules/@esbuild/linux-arm64": {
172
+      "version": "0.21.5",
173
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
174
+      "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
175
+      "cpu": [
176
+        "arm64"
177
+      ],
178
+      "dev": true,
179
+      "license": "MIT",
180
+      "optional": true,
181
+      "os": [
182
+        "linux"
183
+      ],
184
+      "engines": {
185
+        "node": ">=12"
186
+      }
187
+    },
188
+    "node_modules/@esbuild/linux-ia32": {
189
+      "version": "0.21.5",
190
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
191
+      "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
192
+      "cpu": [
193
+        "ia32"
194
+      ],
195
+      "dev": true,
196
+      "license": "MIT",
197
+      "optional": true,
198
+      "os": [
199
+        "linux"
200
+      ],
201
+      "engines": {
202
+        "node": ">=12"
203
+      }
204
+    },
205
+    "node_modules/@esbuild/linux-loong64": {
206
+      "version": "0.21.5",
207
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
208
+      "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
209
+      "cpu": [
210
+        "loong64"
211
+      ],
212
+      "dev": true,
213
+      "license": "MIT",
214
+      "optional": true,
215
+      "os": [
216
+        "linux"
217
+      ],
218
+      "engines": {
219
+        "node": ">=12"
220
+      }
221
+    },
222
+    "node_modules/@esbuild/linux-mips64el": {
223
+      "version": "0.21.5",
224
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
225
+      "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
226
+      "cpu": [
227
+        "mips64el"
228
+      ],
229
+      "dev": true,
230
+      "license": "MIT",
231
+      "optional": true,
232
+      "os": [
233
+        "linux"
234
+      ],
235
+      "engines": {
236
+        "node": ">=12"
237
+      }
238
+    },
239
+    "node_modules/@esbuild/linux-ppc64": {
240
+      "version": "0.21.5",
241
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
242
+      "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
243
+      "cpu": [
244
+        "ppc64"
245
+      ],
246
+      "dev": true,
247
+      "license": "MIT",
248
+      "optional": true,
249
+      "os": [
250
+        "linux"
251
+      ],
252
+      "engines": {
253
+        "node": ">=12"
254
+      }
255
+    },
256
+    "node_modules/@esbuild/linux-riscv64": {
257
+      "version": "0.21.5",
258
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
259
+      "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
260
+      "cpu": [
261
+        "riscv64"
262
+      ],
263
+      "dev": true,
264
+      "license": "MIT",
265
+      "optional": true,
266
+      "os": [
267
+        "linux"
268
+      ],
269
+      "engines": {
270
+        "node": ">=12"
271
+      }
272
+    },
273
+    "node_modules/@esbuild/linux-s390x": {
274
+      "version": "0.21.5",
275
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
276
+      "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
277
+      "cpu": [
278
+        "s390x"
279
+      ],
280
+      "dev": true,
281
+      "license": "MIT",
282
+      "optional": true,
283
+      "os": [
284
+        "linux"
285
+      ],
286
+      "engines": {
287
+        "node": ">=12"
288
+      }
289
+    },
290
+    "node_modules/@esbuild/linux-x64": {
291
+      "version": "0.21.5",
292
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
293
+      "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
294
+      "cpu": [
295
+        "x64"
296
+      ],
297
+      "dev": true,
298
+      "license": "MIT",
299
+      "optional": true,
300
+      "os": [
301
+        "linux"
302
+      ],
303
+      "engines": {
304
+        "node": ">=12"
305
+      }
306
+    },
307
+    "node_modules/@esbuild/netbsd-x64": {
308
+      "version": "0.21.5",
309
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
310
+      "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
311
+      "cpu": [
312
+        "x64"
313
+      ],
314
+      "dev": true,
315
+      "license": "MIT",
316
+      "optional": true,
317
+      "os": [
318
+        "netbsd"
319
+      ],
320
+      "engines": {
321
+        "node": ">=12"
322
+      }
323
+    },
324
+    "node_modules/@esbuild/openbsd-x64": {
325
+      "version": "0.21.5",
326
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
327
+      "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
328
+      "cpu": [
329
+        "x64"
330
+      ],
331
+      "dev": true,
332
+      "license": "MIT",
333
+      "optional": true,
334
+      "os": [
335
+        "openbsd"
336
+      ],
337
+      "engines": {
338
+        "node": ">=12"
339
+      }
340
+    },
341
+    "node_modules/@esbuild/sunos-x64": {
342
+      "version": "0.21.5",
343
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
344
+      "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
345
+      "cpu": [
346
+        "x64"
347
+      ],
348
+      "dev": true,
349
+      "license": "MIT",
350
+      "optional": true,
351
+      "os": [
352
+        "sunos"
353
+      ],
354
+      "engines": {
355
+        "node": ">=12"
356
+      }
357
+    },
358
+    "node_modules/@esbuild/win32-arm64": {
359
+      "version": "0.21.5",
360
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
361
+      "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
362
+      "cpu": [
363
+        "arm64"
364
+      ],
365
+      "dev": true,
366
+      "license": "MIT",
367
+      "optional": true,
368
+      "os": [
369
+        "win32"
370
+      ],
371
+      "engines": {
372
+        "node": ">=12"
373
+      }
374
+    },
375
+    "node_modules/@esbuild/win32-ia32": {
376
+      "version": "0.21.5",
377
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
378
+      "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
379
+      "cpu": [
380
+        "ia32"
381
+      ],
382
+      "dev": true,
383
+      "license": "MIT",
384
+      "optional": true,
385
+      "os": [
386
+        "win32"
387
+      ],
388
+      "engines": {
389
+        "node": ">=12"
390
+      }
391
+    },
392
+    "node_modules/@esbuild/win32-x64": {
393
+      "version": "0.21.5",
394
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
395
+      "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
396
+      "cpu": [
397
+        "x64"
398
+      ],
399
+      "dev": true,
400
+      "license": "MIT",
401
+      "optional": true,
402
+      "os": [
403
+        "win32"
404
+      ],
405
+      "engines": {
406
+        "node": ">=12"
407
+      }
408
+    },
409
+    "node_modules/@rollup/rollup-android-arm-eabi": {
410
+      "version": "4.53.5",
411
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.5.tgz",
412
+      "integrity": "sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ==",
413
+      "cpu": [
414
+        "arm"
415
+      ],
416
+      "dev": true,
417
+      "license": "MIT",
418
+      "optional": true,
419
+      "os": [
420
+        "android"
421
+      ]
422
+    },
423
+    "node_modules/@rollup/rollup-android-arm64": {
424
+      "version": "4.53.5",
425
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.5.tgz",
426
+      "integrity": "sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw==",
427
+      "cpu": [
428
+        "arm64"
429
+      ],
430
+      "dev": true,
431
+      "license": "MIT",
432
+      "optional": true,
433
+      "os": [
434
+        "android"
435
+      ]
436
+    },
437
+    "node_modules/@rollup/rollup-darwin-arm64": {
438
+      "version": "4.53.5",
439
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.5.tgz",
440
+      "integrity": "sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ==",
441
+      "cpu": [
442
+        "arm64"
443
+      ],
444
+      "dev": true,
445
+      "license": "MIT",
446
+      "optional": true,
447
+      "os": [
448
+        "darwin"
449
+      ]
450
+    },
451
+    "node_modules/@rollup/rollup-darwin-x64": {
452
+      "version": "4.53.5",
453
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.5.tgz",
454
+      "integrity": "sha512-YTbnsAaHo6VrAczISxgpTva8EkfQus0VPEVJCEaboHtZRIb6h6j0BNxRBOwnDciFTZLDPW5r+ZBmhL/+YpTZgA==",
455
+      "cpu": [
456
+        "x64"
457
+      ],
458
+      "dev": true,
459
+      "license": "MIT",
460
+      "optional": true,
461
+      "os": [
462
+        "darwin"
463
+      ]
464
+    },
465
+    "node_modules/@rollup/rollup-freebsd-arm64": {
466
+      "version": "4.53.5",
467
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.5.tgz",
468
+      "integrity": "sha512-1T8eY2J8rKJWzaznV7zedfdhD1BqVs1iqILhmHDq/bqCUZsrMt+j8VCTHhP0vdfbHK3e1IQ7VYx3jlKqwlf+vw==",
469
+      "cpu": [
470
+        "arm64"
471
+      ],
472
+      "dev": true,
473
+      "license": "MIT",
474
+      "optional": true,
475
+      "os": [
476
+        "freebsd"
477
+      ]
478
+    },
479
+    "node_modules/@rollup/rollup-freebsd-x64": {
480
+      "version": "4.53.5",
481
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.5.tgz",
482
+      "integrity": "sha512-sHTiuXyBJApxRn+VFMaw1U+Qsz4kcNlxQ742snICYPrY+DDL8/ZbaC4DVIB7vgZmp3jiDaKA0WpBdP0aqPJoBQ==",
483
+      "cpu": [
484
+        "x64"
485
+      ],
486
+      "dev": true,
487
+      "license": "MIT",
488
+      "optional": true,
489
+      "os": [
490
+        "freebsd"
491
+      ]
492
+    },
493
+    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
494
+      "version": "4.53.5",
495
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.5.tgz",
496
+      "integrity": "sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==",
497
+      "cpu": [
498
+        "arm"
499
+      ],
500
+      "dev": true,
501
+      "license": "MIT",
502
+      "optional": true,
503
+      "os": [
504
+        "linux"
505
+      ]
506
+    },
507
+    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
508
+      "version": "4.53.5",
509
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.5.tgz",
510
+      "integrity": "sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==",
511
+      "cpu": [
512
+        "arm"
513
+      ],
514
+      "dev": true,
515
+      "license": "MIT",
516
+      "optional": true,
517
+      "os": [
518
+        "linux"
519
+      ]
520
+    },
521
+    "node_modules/@rollup/rollup-linux-arm64-gnu": {
522
+      "version": "4.53.5",
523
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.5.tgz",
524
+      "integrity": "sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==",
525
+      "cpu": [
526
+        "arm64"
527
+      ],
528
+      "dev": true,
529
+      "license": "MIT",
530
+      "optional": true,
531
+      "os": [
532
+        "linux"
533
+      ]
534
+    },
535
+    "node_modules/@rollup/rollup-linux-arm64-musl": {
536
+      "version": "4.53.5",
537
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.5.tgz",
538
+      "integrity": "sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==",
539
+      "cpu": [
540
+        "arm64"
541
+      ],
542
+      "dev": true,
543
+      "license": "MIT",
544
+      "optional": true,
545
+      "os": [
546
+        "linux"
547
+      ]
548
+    },
549
+    "node_modules/@rollup/rollup-linux-loong64-gnu": {
550
+      "version": "4.53.5",
551
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.5.tgz",
552
+      "integrity": "sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==",
553
+      "cpu": [
554
+        "loong64"
555
+      ],
556
+      "dev": true,
557
+      "license": "MIT",
558
+      "optional": true,
559
+      "os": [
560
+        "linux"
561
+      ]
562
+    },
563
+    "node_modules/@rollup/rollup-linux-ppc64-gnu": {
564
+      "version": "4.53.5",
565
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.5.tgz",
566
+      "integrity": "sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==",
567
+      "cpu": [
568
+        "ppc64"
569
+      ],
570
+      "dev": true,
571
+      "license": "MIT",
572
+      "optional": true,
573
+      "os": [
574
+        "linux"
575
+      ]
576
+    },
577
+    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
578
+      "version": "4.53.5",
579
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.5.tgz",
580
+      "integrity": "sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==",
581
+      "cpu": [
582
+        "riscv64"
583
+      ],
584
+      "dev": true,
585
+      "license": "MIT",
586
+      "optional": true,
587
+      "os": [
588
+        "linux"
589
+      ]
590
+    },
591
+    "node_modules/@rollup/rollup-linux-riscv64-musl": {
592
+      "version": "4.53.5",
593
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.5.tgz",
594
+      "integrity": "sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==",
595
+      "cpu": [
596
+        "riscv64"
597
+      ],
598
+      "dev": true,
599
+      "license": "MIT",
600
+      "optional": true,
601
+      "os": [
602
+        "linux"
603
+      ]
604
+    },
605
+    "node_modules/@rollup/rollup-linux-s390x-gnu": {
606
+      "version": "4.53.5",
607
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.5.tgz",
608
+      "integrity": "sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==",
609
+      "cpu": [
610
+        "s390x"
611
+      ],
612
+      "dev": true,
613
+      "license": "MIT",
614
+      "optional": true,
615
+      "os": [
616
+        "linux"
617
+      ]
618
+    },
619
+    "node_modules/@rollup/rollup-linux-x64-gnu": {
620
+      "version": "4.53.5",
621
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.5.tgz",
622
+      "integrity": "sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==",
623
+      "cpu": [
624
+        "x64"
625
+      ],
626
+      "dev": true,
627
+      "license": "MIT",
628
+      "optional": true,
629
+      "os": [
630
+        "linux"
631
+      ]
632
+    },
633
+    "node_modules/@rollup/rollup-linux-x64-musl": {
634
+      "version": "4.53.5",
635
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.5.tgz",
636
+      "integrity": "sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==",
637
+      "cpu": [
638
+        "x64"
639
+      ],
640
+      "dev": true,
641
+      "license": "MIT",
642
+      "optional": true,
643
+      "os": [
644
+        "linux"
645
+      ]
646
+    },
647
+    "node_modules/@rollup/rollup-openharmony-arm64": {
648
+      "version": "4.53.5",
649
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.5.tgz",
650
+      "integrity": "sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==",
651
+      "cpu": [
652
+        "arm64"
653
+      ],
654
+      "dev": true,
655
+      "license": "MIT",
656
+      "optional": true,
657
+      "os": [
658
+        "openharmony"
659
+      ]
660
+    },
661
+    "node_modules/@rollup/rollup-win32-arm64-msvc": {
662
+      "version": "4.53.5",
663
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.5.tgz",
664
+      "integrity": "sha512-nggc/wPpNTgjGg75hu+Q/3i32R00Lq1B6N1DO7MCU340MRKL3WZJMjA9U4K4gzy3dkZPXm9E1Nc81FItBVGRlA==",
665
+      "cpu": [
666
+        "arm64"
667
+      ],
668
+      "dev": true,
669
+      "license": "MIT",
670
+      "optional": true,
671
+      "os": [
672
+        "win32"
673
+      ]
674
+    },
675
+    "node_modules/@rollup/rollup-win32-ia32-msvc": {
676
+      "version": "4.53.5",
677
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.5.tgz",
678
+      "integrity": "sha512-U/54pTbdQpPLBdEzCT6NBCFAfSZMvmjr0twhnD9f4EIvlm9wy3jjQ38yQj1AGznrNO65EWQMgm/QUjuIVrYF9w==",
679
+      "cpu": [
680
+        "ia32"
681
+      ],
682
+      "dev": true,
683
+      "license": "MIT",
684
+      "optional": true,
685
+      "os": [
686
+        "win32"
687
+      ]
688
+    },
689
+    "node_modules/@rollup/rollup-win32-x64-gnu": {
690
+      "version": "4.53.5",
691
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.5.tgz",
692
+      "integrity": "sha512-2NqKgZSuLH9SXBBV2dWNRCZmocgSOx8OJSdpRaEcRlIfX8YrKxUT6z0F1NpvDVhOsl190UFTRh2F2WDWWCYp3A==",
693
+      "cpu": [
694
+        "x64"
695
+      ],
696
+      "dev": true,
697
+      "license": "MIT",
698
+      "optional": true,
699
+      "os": [
700
+        "win32"
701
+      ]
702
+    },
703
+    "node_modules/@rollup/rollup-win32-x64-msvc": {
704
+      "version": "4.53.5",
705
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.5.tgz",
706
+      "integrity": "sha512-JRpZUhCfhZ4keB5v0fe02gQJy05GqboPOaxvjugW04RLSYYoB/9t2lx2u/tMs/Na/1NXfY8QYjgRljRpN+MjTQ==",
707
+      "cpu": [
708
+        "x64"
709
+      ],
710
+      "dev": true,
711
+      "license": "MIT",
712
+      "optional": true,
713
+      "os": [
714
+        "win32"
715
+      ]
716
+    },
717
+    "node_modules/@types/estree": {
718
+      "version": "1.0.8",
719
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
720
+      "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
721
+      "dev": true,
722
+      "license": "MIT"
723
+    },
724
+    "node_modules/esbuild": {
725
+      "version": "0.21.5",
726
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
727
+      "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
728
+      "dev": true,
729
+      "hasInstallScript": true,
730
+      "license": "MIT",
731
+      "bin": {
732
+        "esbuild": "bin/esbuild"
733
+      },
734
+      "engines": {
735
+        "node": ">=12"
736
+      },
737
+      "optionalDependencies": {
738
+        "@esbuild/aix-ppc64": "0.21.5",
739
+        "@esbuild/android-arm": "0.21.5",
740
+        "@esbuild/android-arm64": "0.21.5",
741
+        "@esbuild/android-x64": "0.21.5",
742
+        "@esbuild/darwin-arm64": "0.21.5",
743
+        "@esbuild/darwin-x64": "0.21.5",
744
+        "@esbuild/freebsd-arm64": "0.21.5",
745
+        "@esbuild/freebsd-x64": "0.21.5",
746
+        "@esbuild/linux-arm": "0.21.5",
747
+        "@esbuild/linux-arm64": "0.21.5",
748
+        "@esbuild/linux-ia32": "0.21.5",
749
+        "@esbuild/linux-loong64": "0.21.5",
750
+        "@esbuild/linux-mips64el": "0.21.5",
751
+        "@esbuild/linux-ppc64": "0.21.5",
752
+        "@esbuild/linux-riscv64": "0.21.5",
753
+        "@esbuild/linux-s390x": "0.21.5",
754
+        "@esbuild/linux-x64": "0.21.5",
755
+        "@esbuild/netbsd-x64": "0.21.5",
756
+        "@esbuild/openbsd-x64": "0.21.5",
757
+        "@esbuild/sunos-x64": "0.21.5",
758
+        "@esbuild/win32-arm64": "0.21.5",
759
+        "@esbuild/win32-ia32": "0.21.5",
760
+        "@esbuild/win32-x64": "0.21.5"
761
+      }
762
+    },
763
+    "node_modules/fsevents": {
764
+      "version": "2.3.3",
765
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
766
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
767
+      "dev": true,
768
+      "hasInstallScript": true,
769
+      "license": "MIT",
770
+      "optional": true,
771
+      "os": [
772
+        "darwin"
773
+      ],
774
+      "engines": {
775
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
776
+      }
777
+    },
778
+    "node_modules/nanoid": {
779
+      "version": "3.3.11",
780
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
781
+      "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
782
+      "dev": true,
783
+      "funding": [
784
+        {
785
+          "type": "github",
786
+          "url": "https://github.com/sponsors/ai"
787
+        }
788
+      ],
789
+      "license": "MIT",
790
+      "bin": {
791
+        "nanoid": "bin/nanoid.cjs"
792
+      },
793
+      "engines": {
794
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
795
+      }
796
+    },
797
+    "node_modules/p5": {
798
+      "version": "1.11.11",
799
+      "resolved": "https://registry.npmjs.org/p5/-/p5-1.11.11.tgz",
800
+      "integrity": "sha512-k58mfexvavFb+KNRpi70PbkKE2gCNiWQkzS4kVOyC2F9SKGgYy1jSO+JXZ24ikXV9OvZIAxGusiSVWEijYrmNg==",
801
+      "license": "LGPL-2.1"
802
+    },
803
+    "node_modules/picocolors": {
804
+      "version": "1.1.1",
805
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
806
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
807
+      "dev": true,
808
+      "license": "ISC"
809
+    },
810
+    "node_modules/postcss": {
811
+      "version": "8.5.6",
812
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
813
+      "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
814
+      "dev": true,
815
+      "funding": [
816
+        {
817
+          "type": "opencollective",
818
+          "url": "https://opencollective.com/postcss/"
819
+        },
820
+        {
821
+          "type": "tidelift",
822
+          "url": "https://tidelift.com/funding/github/npm/postcss"
823
+        },
824
+        {
825
+          "type": "github",
826
+          "url": "https://github.com/sponsors/ai"
827
+        }
828
+      ],
829
+      "license": "MIT",
830
+      "dependencies": {
831
+        "nanoid": "^3.3.11",
832
+        "picocolors": "^1.1.1",
833
+        "source-map-js": "^1.2.1"
834
+      },
835
+      "engines": {
836
+        "node": "^10 || ^12 || >=14"
837
+      }
838
+    },
839
+    "node_modules/rollup": {
840
+      "version": "4.53.5",
841
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.5.tgz",
842
+      "integrity": "sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ==",
843
+      "dev": true,
844
+      "license": "MIT",
845
+      "dependencies": {
846
+        "@types/estree": "1.0.8"
847
+      },
848
+      "bin": {
849
+        "rollup": "dist/bin/rollup"
850
+      },
851
+      "engines": {
852
+        "node": ">=18.0.0",
853
+        "npm": ">=8.0.0"
854
+      },
855
+      "optionalDependencies": {
856
+        "@rollup/rollup-android-arm-eabi": "4.53.5",
857
+        "@rollup/rollup-android-arm64": "4.53.5",
858
+        "@rollup/rollup-darwin-arm64": "4.53.5",
859
+        "@rollup/rollup-darwin-x64": "4.53.5",
860
+        "@rollup/rollup-freebsd-arm64": "4.53.5",
861
+        "@rollup/rollup-freebsd-x64": "4.53.5",
862
+        "@rollup/rollup-linux-arm-gnueabihf": "4.53.5",
863
+        "@rollup/rollup-linux-arm-musleabihf": "4.53.5",
864
+        "@rollup/rollup-linux-arm64-gnu": "4.53.5",
865
+        "@rollup/rollup-linux-arm64-musl": "4.53.5",
866
+        "@rollup/rollup-linux-loong64-gnu": "4.53.5",
867
+        "@rollup/rollup-linux-ppc64-gnu": "4.53.5",
868
+        "@rollup/rollup-linux-riscv64-gnu": "4.53.5",
869
+        "@rollup/rollup-linux-riscv64-musl": "4.53.5",
870
+        "@rollup/rollup-linux-s390x-gnu": "4.53.5",
871
+        "@rollup/rollup-linux-x64-gnu": "4.53.5",
872
+        "@rollup/rollup-linux-x64-musl": "4.53.5",
873
+        "@rollup/rollup-openharmony-arm64": "4.53.5",
874
+        "@rollup/rollup-win32-arm64-msvc": "4.53.5",
875
+        "@rollup/rollup-win32-ia32-msvc": "4.53.5",
876
+        "@rollup/rollup-win32-x64-gnu": "4.53.5",
877
+        "@rollup/rollup-win32-x64-msvc": "4.53.5",
878
+        "fsevents": "~2.3.2"
879
+      }
880
+    },
881
+    "node_modules/source-map-js": {
882
+      "version": "1.2.1",
883
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
884
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
885
+      "dev": true,
886
+      "license": "BSD-3-Clause",
887
+      "engines": {
888
+        "node": ">=0.10.0"
889
+      }
890
+    },
891
+    "node_modules/three": {
892
+      "version": "0.170.0",
893
+      "resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz",
894
+      "integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==",
895
+      "license": "MIT"
896
+    },
897
+    "node_modules/vite": {
898
+      "version": "5.4.21",
899
+      "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
900
+      "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
901
+      "dev": true,
902
+      "license": "MIT",
903
+      "dependencies": {
904
+        "esbuild": "^0.21.3",
905
+        "postcss": "^8.4.43",
906
+        "rollup": "^4.20.0"
907
+      },
908
+      "bin": {
909
+        "vite": "bin/vite.js"
910
+      },
911
+      "engines": {
912
+        "node": "^18.0.0 || >=20.0.0"
913
+      },
914
+      "funding": {
915
+        "url": "https://github.com/vitejs/vite?sponsor=1"
916
+      },
917
+      "optionalDependencies": {
918
+        "fsevents": "~2.3.3"
919
+      },
920
+      "peerDependencies": {
921
+        "@types/node": "^18.0.0 || >=20.0.0",
922
+        "less": "*",
923
+        "lightningcss": "^1.21.0",
924
+        "sass": "*",
925
+        "sass-embedded": "*",
926
+        "stylus": "*",
927
+        "sugarss": "*",
928
+        "terser": "^5.4.0"
929
+      },
930
+      "peerDependenciesMeta": {
931
+        "@types/node": {
932
+          "optional": true
933
+        },
934
+        "less": {
935
+          "optional": true
936
+        },
937
+        "lightningcss": {
938
+          "optional": true
939
+        },
940
+        "sass": {
941
+          "optional": true
942
+        },
943
+        "sass-embedded": {
944
+          "optional": true
945
+        },
946
+        "stylus": {
947
+          "optional": true
948
+        },
949
+        "sugarss": {
950
+          "optional": true
951
+        },
952
+        "terser": {
953
+          "optional": true
954
+        }
955
+      }
956
+    }
957
+  }
958
+}
package.jsonadded
@@ -0,0 +1,18 @@
1
+{
2
+  "name": "dougk",
3
+  "version": "0.1.0",
4
+  "description": "A cozy pond simulator featuring Doug the duck",
5
+  "type": "module",
6
+  "scripts": {
7
+    "dev": "vite",
8
+    "build": "vite build",
9
+    "preview": "vite preview"
10
+  },
11
+  "dependencies": {
12
+    "p5": "^1.9.0",
13
+    "three": "^0.170.0"
14
+  },
15
+  "devDependencies": {
16
+    "vite": "^5.0.0"
17
+  }
18
+}
src/main.jsadded
@@ -0,0 +1,108 @@
1
+// dougk - a cozy pond simulator featuring Doug the duck
2
+// Supports both 2D (p5.js) and 3D (Three.js) rendering modes
3
+
4
+import * as p5Renderer from './renderers/p5/index.js'
5
+import * as threeRenderer from './renderers/three/index.js'
6
+
7
+const renderers = {
8
+  '2d': p5Renderer,
9
+  '3d': threeRenderer
10
+}
11
+
12
+let currentMode = localStorage.getItem('dougk-mode') || '3d'
13
+let container = null
14
+
15
+// Create the container
16
+function createContainer() {
17
+  container = document.createElement('div')
18
+  container.id = 'dougk-container'
19
+  container.style.cssText = 'position: fixed; inset: 0; z-index: 0;'
20
+  document.body.appendChild(container)
21
+}
22
+
23
+// Create the toggle UI
24
+function createToggleUI() {
25
+  const toggle = document.createElement('div')
26
+  toggle.id = 'mode-toggle'
27
+  toggle.style.cssText = `
28
+    position: fixed;
29
+    top: 20px;
30
+    right: 20px;
31
+    z-index: 1000;
32
+    display: flex;
33
+    gap: 0;
34
+    font-family: system-ui, -apple-system, sans-serif;
35
+    font-size: 14px;
36
+    font-weight: 600;
37
+    border-radius: 8px;
38
+    overflow: hidden;
39
+    box-shadow: 0 2px 8px rgba(0,0,0,0.2);
40
+    border: 2px solid #191410;
41
+  `
42
+
43
+  const btn2d = createToggleButton('2D', '2d')
44
+  const btn3d = createToggleButton('3D', '3d')
45
+
46
+  toggle.appendChild(btn2d)
47
+  toggle.appendChild(btn3d)
48
+  document.body.appendChild(toggle)
49
+
50
+  updateToggleUI()
51
+}
52
+
53
+function createToggleButton(label, mode) {
54
+  const btn = document.createElement('button')
55
+  btn.textContent = label
56
+  btn.dataset.mode = mode
57
+  btn.style.cssText = `
58
+    padding: 10px 20px;
59
+    border: none;
60
+    cursor: pointer;
61
+    transition: all 0.2s ease;
62
+    font-weight: 600;
63
+    font-size: 14px;
64
+  `
65
+  btn.addEventListener('click', () => switchMode(mode))
66
+  return btn
67
+}
68
+
69
+function updateToggleUI() {
70
+  const buttons = document.querySelectorAll('#mode-toggle button')
71
+  buttons.forEach(btn => {
72
+    const isActive = btn.dataset.mode === currentMode
73
+    btn.style.background = isActive ? '#ffdc50' : '#ffffff'
74
+    btn.style.color = '#191410'
75
+  })
76
+}
77
+
78
+function switchMode(newMode) {
79
+  if (newMode === currentMode) return
80
+
81
+  // Stop current renderer
82
+  renderers[currentMode].stop()
83
+
84
+  // Clear container
85
+  container.innerHTML = ''
86
+
87
+  // Switch mode
88
+  currentMode = newMode
89
+  localStorage.setItem('dougk-mode', currentMode)
90
+
91
+  // Start new renderer
92
+  renderers[currentMode].start(container)
93
+
94
+  updateToggleUI()
95
+}
96
+
97
+// Initialize
98
+createContainer()
99
+createToggleUI()
100
+renderers[currentMode].start(container)
101
+
102
+// Keyboard shortcut: press 'T' to toggle
103
+document.addEventListener('keydown', (e) => {
104
+  if (e.key.toLowerCase() === 't' && !e.ctrlKey && !e.metaKey && !e.altKey) {
105
+    const newMode = currentMode === '2d' ? '3d' : '2d'
106
+    switchMode(newMode)
107
+  }
108
+})
src/renderers/p5/bread.jsadded
@@ -0,0 +1,86 @@
1
+// Bread module - handles bread bit spawning and behavior (p5.js version)
2
+
3
+export class BreadBit {
4
+  constructor(p, x, y) {
5
+    this.p = p
6
+    this.x = x
7
+    this.y = y
8
+    this.size = p.random(4, 8)
9
+    this.eaten = false
10
+    this.bobOffset = p.random(1000)
11
+    this.driftX = p.random(-0.1, 0.1)
12
+    this.driftY = p.random(-0.05, 0.05)
13
+    this.fadeAlpha = 255
14
+  }
15
+
16
+  update() {
17
+    if (this.eaten) {
18
+      this.fadeAlpha -= 15
19
+      return
20
+    }
21
+    this.x += this.driftX
22
+    this.y += this.driftY
23
+  }
24
+
25
+  isDone() {
26
+    return this.eaten && this.fadeAlpha <= 0
27
+  }
28
+
29
+  draw() {
30
+    const p = this.p
31
+    const bob = Math.sin((p.frameCount + this.bobOffset) * 0.08) * 2
32
+
33
+    p.push()
34
+    p.translate(this.x, this.y + bob)
35
+
36
+    const outlineAlpha = Math.min(this.fadeAlpha, 255)
37
+
38
+    p.stroke(25, 20, 15, outlineAlpha)
39
+    p.strokeWeight(1.5)
40
+    p.fill(235, 195, 130, this.fadeAlpha)
41
+    p.ellipse(0, 0, this.size + 2, this.size * 0.75)
42
+
43
+    p.noStroke()
44
+    p.fill(255, 230, 180, this.fadeAlpha * 0.9)
45
+    p.ellipse(-this.size * 0.15, -this.size * 0.15, this.size * 0.5, this.size * 0.35)
46
+
47
+    p.pop()
48
+  }
49
+}
50
+
51
+export class BreadManager {
52
+  constructor(p) {
53
+    this.p = p
54
+    this.bits = []
55
+  }
56
+
57
+  spawnBread(x, y, count = null) {
58
+    const p = this.p
59
+    const numBits = count || Math.floor(p.random(3, 6))
60
+
61
+    for (let i = 0; i < numBits; i++) {
62
+      const offsetX = p.random(-15, 15)
63
+      const offsetY = p.random(-15, 15)
64
+      this.bits.push(new BreadBit(p, x + offsetX, y + offsetY))
65
+    }
66
+  }
67
+
68
+  update() {
69
+    for (let i = this.bits.length - 1; i >= 0; i--) {
70
+      this.bits[i].update()
71
+      if (this.bits[i].isDone()) {
72
+        this.bits.splice(i, 1)
73
+      }
74
+    }
75
+  }
76
+
77
+  draw() {
78
+    for (const bit of this.bits) {
79
+      bit.draw()
80
+    }
81
+  }
82
+
83
+  getActiveBits() {
84
+    return this.bits.filter(b => !b.eaten)
85
+  }
86
+}
src/renderers/p5/duck.jsadded
@@ -0,0 +1,215 @@
1
+// Duck module - Doug's behavior and rendering (p5.js version)
2
+
3
+export class Duck {
4
+  constructor(p, x, y, pond) {
5
+    this.p = p
6
+    this.x = x
7
+    this.y = y
8
+    this.pond = pond
9
+
10
+    this.targetX = x
11
+    this.targetY = y
12
+    this.speed = 0.8
13
+    this.idleSpeed = 0.3
14
+
15
+    this.state = 'idle'
16
+    this.waitTimer = 0
17
+    this.waitDuration = 0
18
+
19
+    this.wobble = 0
20
+    this.headBob = 0
21
+    this.direction = 1
22
+
23
+    this.idleTimer = 0
24
+    this.nextIdleMove = this.randomIdleTime()
25
+  }
26
+
27
+  randomIdleTime() {
28
+    return this.p.random(120, 300)
29
+  }
30
+
31
+  setTarget(x, y, isFood = false) {
32
+    if (isFood) {
33
+      this.state = 'waiting'
34
+      this.waitTimer = 0
35
+      this.waitDuration = this.p.random(30, 60)
36
+      this.pendingTargetX = x
37
+      this.pendingTargetY = y
38
+    } else {
39
+      this.targetX = x
40
+      this.targetY = y
41
+    }
42
+  }
43
+
44
+  pickIdleTarget() {
45
+    const angle = this.p.random(this.p.TWO_PI)
46
+    const radiusX = this.p.random(0.2, 0.8) * (this.pond.width / 2)
47
+    const radiusY = this.p.random(0.2, 0.8) * (this.pond.height / 2)
48
+    this.targetX = this.pond.x + Math.cos(angle) * radiusX
49
+    this.targetY = this.pond.y + Math.sin(angle) * radiusY
50
+  }
51
+
52
+  update(breadBits) {
53
+    const p = this.p
54
+
55
+    let closestBread = null
56
+    let closestDist = Infinity
57
+    for (const bread of breadBits) {
58
+      if (!bread.eaten) {
59
+        const d = p.dist(this.x, this.y, bread.x, bread.y)
60
+        if (d < closestDist) {
61
+          closestDist = d
62
+          closestBread = bread
63
+        }
64
+      }
65
+    }
66
+
67
+    if (closestBread && this.state === 'idle') {
68
+      this.setTarget(closestBread.x, closestBread.y, true)
69
+    }
70
+
71
+    if (this.state === 'waiting') {
72
+      this.waitTimer++
73
+      this.headBob = Math.sin(this.waitTimer * 0.3) * 3
74
+      if (this.waitTimer >= this.waitDuration) {
75
+        this.state = 'swimming'
76
+        this.targetX = this.pendingTargetX
77
+        this.targetY = this.pendingTargetY
78
+      }
79
+    }
80
+
81
+    const dx = this.targetX - this.x
82
+    const dy = this.targetY - this.y
83
+    const dist = Math.sqrt(dx * dx + dy * dy)
84
+
85
+    if (dist > 3) {
86
+      const currentSpeed = this.state === 'swimming' ? this.speed : this.idleSpeed
87
+      const moveX = (dx / dist) * currentSpeed
88
+      const moveY = (dy / dist) * currentSpeed
89
+      this.x += moveX
90
+      this.y += moveY
91
+
92
+      if (Math.abs(dx) > 0.1) {
93
+        this.direction = dx > 0 ? 1 : -1
94
+      }
95
+
96
+      this.wobble += 0.15
97
+    } else {
98
+      if (this.state === 'swimming') {
99
+        this.state = 'idle'
100
+        for (const bread of breadBits) {
101
+          if (!bread.eaten && p.dist(this.x, this.y, bread.x, bread.y) < 20) {
102
+            bread.eaten = true
103
+          }
104
+        }
105
+      }
106
+    }
107
+
108
+    if (this.state === 'idle' && !closestBread) {
109
+      this.idleTimer++
110
+      if (this.idleTimer >= this.nextIdleMove) {
111
+        this.pickIdleTarget()
112
+        this.idleTimer = 0
113
+        this.nextIdleMove = this.randomIdleTime()
114
+      }
115
+    }
116
+
117
+    this.headBob = Math.sin(p.frameCount * 0.05) * 2
118
+  }
119
+
120
+  draw() {
121
+    const p = this.p
122
+
123
+    p.push()
124
+    p.translate(this.x, this.y)
125
+    p.scale(this.direction, 1)
126
+
127
+    const wobbleAmount = Math.sin(this.wobble) * 3
128
+    const outlineWeight = 2.5
129
+    const outlineColor = p.color(25, 20, 15)
130
+
131
+    // Water ripple
132
+    p.noFill()
133
+    p.stroke(255, 255, 255, 60)
134
+    p.strokeWeight(2)
135
+    p.ellipse(0, 12, 50, 16)
136
+
137
+    // Shadow
138
+    p.noStroke()
139
+    p.fill(40, 80, 110, 80)
140
+    p.ellipse(0, 10, 40, 12)
141
+
142
+    // Tail
143
+    p.stroke(outlineColor)
144
+    p.strokeWeight(outlineWeight)
145
+    p.fill(255, 200, 60)
146
+    p.push()
147
+    p.translate(-18, wobbleAmount - 2)
148
+    p.rotate(p.radians(-20))
149
+    p.ellipse(0, 0, 14, 8)
150
+    p.pop()
151
+
152
+    // Body
153
+    p.stroke(outlineColor)
154
+    p.strokeWeight(outlineWeight)
155
+    p.fill(255, 220, 80)
156
+    p.ellipse(0, wobbleAmount, 38, 30)
157
+
158
+    // Body highlight
159
+    p.noStroke()
160
+    p.fill(255, 240, 140)
161
+    p.ellipse(-5, wobbleAmount - 5, 20, 14)
162
+
163
+    // Body shadow
164
+    p.fill(230, 180, 50)
165
+    p.ellipse(5, wobbleAmount + 8, 25, 10)
166
+
167
+    // Wing
168
+    p.stroke(outlineColor)
169
+    p.strokeWeight(outlineWeight)
170
+    p.fill(245, 200, 65)
171
+    p.ellipse(3, wobbleAmount + 2, 22, 18)
172
+
173
+    // Head
174
+    const headY = -14 + this.headBob + wobbleAmount * 0.5
175
+    p.stroke(outlineColor)
176
+    p.strokeWeight(outlineWeight)
177
+    p.fill(255, 220, 80)
178
+    p.ellipse(14, headY, 26, 24)
179
+
180
+    // Head highlight
181
+    p.noStroke()
182
+    p.fill(255, 240, 140)
183
+    p.ellipse(10, headY - 5, 14, 10)
184
+
185
+    // Beak
186
+    p.stroke(outlineColor)
187
+    p.strokeWeight(outlineWeight)
188
+    p.fill(255, 160, 40)
189
+    p.push()
190
+    p.translate(26, headY + 2)
191
+    p.beginShape()
192
+    p.vertex(0, -5)
193
+    p.vertex(14, 0)
194
+    p.vertex(0, 5)
195
+    p.endShape(p.CLOSE)
196
+    p.pop()
197
+
198
+    // Eye
199
+    p.stroke(outlineColor)
200
+    p.strokeWeight(1.5)
201
+    p.fill(255)
202
+    p.ellipse(20, headY - 2, 10, 10)
203
+
204
+    // Pupil
205
+    p.noStroke()
206
+    p.fill(25, 20, 15)
207
+    p.ellipse(21, headY - 1, 5, 6)
208
+
209
+    // Eye shine
210
+    p.fill(255)
211
+    p.ellipse(22, headY - 3, 3, 3)
212
+
213
+    p.pop()
214
+  }
215
+}
src/renderers/p5/index.jsadded
@@ -0,0 +1,96 @@
1
+// p5.js 2D renderer for dougk
2
+import p5 from 'p5'
3
+import { Pond } from './pond.js'
4
+import { Duck } from './duck.js'
5
+import { BreadManager } from './bread.js'
6
+
7
+let sketch = null
8
+let p5Instance = null
9
+
10
+export function start(container) {
11
+  if (p5Instance) return
12
+
13
+  sketch = (p) => {
14
+    let pond
15
+    let doug
16
+    let breadManager
17
+
18
+    p.setup = () => {
19
+      const canvas = p.createCanvas(window.innerWidth, window.innerHeight)
20
+      canvas.parent(container)
21
+      canvas.mousePressed(handleClick)
22
+      p.frameRate(60)
23
+
24
+      pond = new Pond(p, p.width / 2, p.height / 2, 400, 280)
25
+      doug = new Duck(p, p.width / 2, p.height / 2, pond)
26
+      breadManager = new BreadManager(p)
27
+    }
28
+
29
+    p.windowResized = () => {
30
+      p.resizeCanvas(window.innerWidth, window.innerHeight)
31
+      pond.x = p.width / 2
32
+      pond.y = p.height / 2
33
+    }
34
+
35
+    p.draw = () => {
36
+      // Vibrant grass background
37
+      p.background(85, 160, 75)
38
+
39
+      drawGrassDetails()
40
+
41
+      pond.update()
42
+      pond.draw()
43
+
44
+      breadManager.update()
45
+      breadManager.draw()
46
+
47
+      doug.update(breadManager.bits)
48
+      doug.draw()
49
+    }
50
+
51
+    function handleClick() {
52
+      if (pond.contains(p.mouseX, p.mouseY)) {
53
+        breadManager.spawnBread(p.mouseX, p.mouseY)
54
+        pond.addRipple(p.mouseX, p.mouseY)
55
+      }
56
+    }
57
+
58
+    function drawGrassDetails() {
59
+      const seed = 12345
60
+      p.randomSeed(seed)
61
+
62
+      for (let i = 0; i < 40; i++) {
63
+        const gx = p.random(p.width)
64
+        const gy = p.random(p.height)
65
+
66
+        const dx = gx - pond.x
67
+        const dy = gy - pond.y
68
+        if (Math.sqrt(dx * dx + dy * dy) < 250) continue
69
+
70
+        p.stroke(25, 20, 15)
71
+        p.strokeWeight(1)
72
+        p.fill(65, 130, 55)
73
+        p.ellipse(gx, gy, 18, 10)
74
+
75
+        p.noStroke()
76
+        p.fill(110, 180, 95)
77
+        p.ellipse(gx - 3, gy - 2, 10, 5)
78
+      }
79
+
80
+      p.randomSeed(p.millis())
81
+      p.noStroke()
82
+    }
83
+  }
84
+
85
+  p5Instance = new p5(sketch)
86
+}
87
+
88
+export function stop() {
89
+  if (p5Instance) {
90
+    p5Instance.remove()
91
+    p5Instance = null
92
+    sketch = null
93
+  }
94
+}
95
+
96
+export const name = '2D'
src/renderers/p5/pond.jsadded
@@ -0,0 +1,131 @@
1
+// Pond module - handles the water, shore, and fence rendering (p5.js version)
2
+
3
+export class Pond {
4
+  constructor(p, x, y, width, height) {
5
+    this.p = p
6
+    this.x = x
7
+    this.y = y
8
+    this.width = width
9
+    this.height = height
10
+    this.ripples = []
11
+  }
12
+
13
+  contains(px, py) {
14
+    const dx = (px - this.x) / (this.width / 2)
15
+    const dy = (py - this.y) / (this.height / 2)
16
+    return (dx * dx + dy * dy) <= 1
17
+  }
18
+
19
+  addRipple(x, y) {
20
+    this.ripples.push({
21
+      x,
22
+      y,
23
+      radius: 5,
24
+      maxRadius: 40,
25
+      alpha: 150
26
+    })
27
+  }
28
+
29
+  update() {
30
+    for (let i = this.ripples.length - 1; i >= 0; i--) {
31
+      const ripple = this.ripples[i]
32
+      ripple.radius += 0.8
33
+      ripple.alpha -= 3
34
+      if (ripple.alpha <= 0) {
35
+        this.ripples.splice(i, 1)
36
+      }
37
+    }
38
+  }
39
+
40
+  draw() {
41
+    const p = this.p
42
+    const outlineColor = p.color(25, 20, 15)
43
+    const outlineWeight = 2.5
44
+
45
+    // Grassy shore area
46
+    p.stroke(outlineColor)
47
+    p.strokeWeight(outlineWeight)
48
+    p.fill(120, 180, 90)
49
+    p.ellipse(this.x, this.y, this.width + 70, this.height + 55)
50
+
51
+    // Shore highlight
52
+    p.noStroke()
53
+    p.fill(150, 210, 110)
54
+    p.ellipse(this.x - 30, this.y - 20, this.width * 0.4, this.height * 0.25)
55
+
56
+    // Sandy edge
57
+    p.stroke(outlineColor)
58
+    p.strokeWeight(outlineWeight)
59
+    p.fill(190, 160, 110)
60
+    p.ellipse(this.x, this.y, this.width + 25, this.height + 18)
61
+
62
+    // Pond water
63
+    p.stroke(outlineColor)
64
+    p.strokeWeight(outlineWeight)
65
+    p.fill(70, 160, 190)
66
+    p.ellipse(this.x, this.y, this.width, this.height)
67
+
68
+    // Water cel-shaded bands
69
+    p.noStroke()
70
+    p.fill(50, 130, 160)
71
+    p.ellipse(this.x + 20, this.y + 30, this.width * 0.7, this.height * 0.4)
72
+
73
+    p.fill(110, 200, 220)
74
+    p.ellipse(this.x - this.width * 0.18, this.y - this.height * 0.18, this.width * 0.45, this.height * 0.28)
75
+
76
+    p.fill(160, 230, 245)
77
+    p.ellipse(this.x - this.width * 0.22, this.y - this.height * 0.22, this.width * 0.2, this.height * 0.12)
78
+
79
+    // Ripples
80
+    p.noFill()
81
+    for (const ripple of this.ripples) {
82
+      p.stroke(255, 255, 255, ripple.alpha)
83
+      p.strokeWeight(2.5)
84
+      p.ellipse(ripple.x, ripple.y, ripple.radius * 2, ripple.radius * 1.3)
85
+    }
86
+    p.noStroke()
87
+
88
+    this.drawFence()
89
+  }
90
+
91
+  drawFence() {
92
+    const p = this.p
93
+    const fenceX = this.x + this.width / 2 + 50
94
+    const fenceStartY = this.y - 90
95
+    const postCount = 5
96
+    const postSpacing = 38
97
+    const outlineColor = p.color(25, 20, 15)
98
+
99
+    for (let i = 0; i < postCount; i++) {
100
+      const postY = fenceStartY + i * postSpacing
101
+      const wobble = p.sin(i * 1.5) * 5
102
+
103
+      p.stroke(outlineColor)
104
+      p.strokeWeight(2)
105
+      p.fill(180, 130, 70)
106
+      p.push()
107
+      p.translate(fenceX + wobble, postY)
108
+      p.rotate(p.radians(wobble * 0.6))
109
+      p.rect(-5, -22, 10, 44, 2)
110
+
111
+      p.noStroke()
112
+      p.fill(210, 165, 100)
113
+      p.rect(-3, -20, 4, 40, 1)
114
+      p.pop()
115
+    }
116
+
117
+    p.stroke(outlineColor)
118
+    p.strokeWeight(2)
119
+    p.fill(165, 120, 65)
120
+    p.push()
121
+    p.translate(fenceX - 2, fenceStartY - 8)
122
+    p.rotate(p.radians(2))
123
+    p.rect(-6, -4, 12, 8, 2)
124
+    for (let i = 1; i < postCount; i++) {
125
+      p.rect(-6 + i * 2, -4 + i * postSpacing, 12, 8, 2)
126
+    }
127
+    p.pop()
128
+
129
+    p.noStroke()
130
+  }
131
+}
src/renderers/three/bread.jsadded
@@ -0,0 +1,105 @@
1
+// Bread bits - 3D floating bread with Wind Waker styling
2
+import * as THREE from 'three'
3
+
4
+class BreadBit {
5
+  constructor(scene, gradientMap, x, z) {
6
+    this.scene = scene
7
+    this.eaten = false
8
+    this.fadeOut = false
9
+    this.fadeAlpha = 1
10
+
11
+    // Random size
12
+    const size = 0.08 + Math.random() * 0.06
13
+
14
+    // Bread material - warm tan
15
+    const material = new THREE.MeshToonMaterial({
16
+      color: 0xe8c878,
17
+      gradientMap: gradientMap,
18
+      transparent: true,
19
+      opacity: 1
20
+    })
21
+
22
+    // Simple box geometry for bread chunk
23
+    const geometry = new THREE.BoxGeometry(size, size * 0.6, size)
24
+
25
+    this.mesh = new THREE.Mesh(geometry, material)
26
+    this.mesh.position.set(
27
+      x + (Math.random() - 0.5) * 0.4,
28
+      0.05,
29
+      z + (Math.random() - 0.5) * 0.4
30
+    )
31
+    this.mesh.rotation.y = Math.random() * Math.PI * 2
32
+
33
+    // Movement
34
+    this.bobOffset = Math.random() * Math.PI * 2
35
+    this.driftX = (Math.random() - 0.5) * 0.02
36
+    this.driftZ = (Math.random() - 0.5) * 0.02
37
+
38
+    scene.add(this.mesh)
39
+  }
40
+
41
+  get position() {
42
+    return this.mesh.position
43
+  }
44
+
45
+  update(delta, elapsed) {
46
+    if (this.eaten) {
47
+      this.fadeAlpha -= delta * 3
48
+      this.mesh.material.opacity = Math.max(0, this.fadeAlpha)
49
+      this.mesh.position.y -= delta * 0.2 // Sink slightly
50
+      return this.fadeAlpha <= 0
51
+    }
52
+
53
+    // Bob on water
54
+    this.mesh.position.y = 0.05 + Math.sin(elapsed * 2 + this.bobOffset) * 0.02
55
+
56
+    // Gentle drift
57
+    this.mesh.position.x += this.driftX * delta
58
+    this.mesh.position.z += this.driftZ * delta
59
+
60
+    // Slow rotation
61
+    this.mesh.rotation.y += delta * 0.3
62
+
63
+    return false
64
+  }
65
+
66
+  dispose() {
67
+    this.scene.remove(this.mesh)
68
+    this.mesh.geometry.dispose()
69
+    this.mesh.material.dispose()
70
+  }
71
+}
72
+
73
+export class BreadManager {
74
+  constructor(scene, gradientMap) {
75
+    this.scene = scene
76
+    this.gradientMap = gradientMap
77
+    this.bits = []
78
+  }
79
+
80
+  spawnBread(x, z, count = null) {
81
+    const numBits = count || Math.floor(3 + Math.random() * 3)
82
+
83
+    for (let i = 0; i < numBits; i++) {
84
+      this.bits.push(new BreadBit(this.scene, this.gradientMap, x, z))
85
+    }
86
+  }
87
+
88
+  update(delta, elapsed) {
89
+    for (let i = this.bits.length - 1; i >= 0; i--) {
90
+      const done = this.bits[i].update(delta, elapsed)
91
+      if (done) {
92
+        this.bits[i].dispose()
93
+        this.bits.splice(i, 1)
94
+      }
95
+    }
96
+  }
97
+
98
+  getActiveBits() {
99
+    return this.bits.filter(b => !b.eaten)
100
+  }
101
+
102
+  getMeshes() {
103
+    return this.bits.map(b => b.mesh)
104
+  }
105
+}
src/renderers/three/duck.jsadded
@@ -0,0 +1,279 @@
1
+// Doug the Duck - 3D low-poly model with Wind Waker cel-shading
2
+import * as THREE from 'three'
3
+
4
+export function createDoug(scene, gradientMap) {
5
+  const group = new THREE.Group()
6
+
7
+  // Color palette - vibrant Wind Waker yellows
8
+  const bodyColor = 0xffdc50 // Warm yellow
9
+  const bodyHighlight = 0xfff0a0 // Light yellow
10
+  const beakColor = 0xff9020 // Bright orange
11
+  const eyeWhite = 0xffffff
12
+  const eyePupil = 0x191410
13
+
14
+  // Toon materials
15
+  const bodyMaterial = new THREE.MeshToonMaterial({
16
+    color: bodyColor,
17
+    gradientMap: gradientMap
18
+  })
19
+
20
+  const highlightMaterial = new THREE.MeshToonMaterial({
21
+    color: bodyHighlight,
22
+    gradientMap: gradientMap
23
+  })
24
+
25
+  const beakMaterial = new THREE.MeshToonMaterial({
26
+    color: beakColor,
27
+    gradientMap: gradientMap
28
+  })
29
+
30
+  const eyeWhiteMaterial = new THREE.MeshToonMaterial({
31
+    color: eyeWhite,
32
+    gradientMap: gradientMap
33
+  })
34
+
35
+  const eyePupilMaterial = new THREE.MeshBasicMaterial({
36
+    color: eyePupil
37
+  })
38
+
39
+  // Body - stretched sphere
40
+  const bodyGeom = new THREE.SphereGeometry(0.5, 16, 12)
41
+  bodyGeom.scale(1.2, 0.9, 1)
42
+  const body = new THREE.Mesh(bodyGeom, bodyMaterial)
43
+  body.position.y = 0.3
44
+  group.add(body)
45
+
46
+  // Body highlight (chest area)
47
+  const chestGeom = new THREE.SphereGeometry(0.35, 12, 8)
48
+  chestGeom.scale(1, 0.8, 0.8)
49
+  const chest = new THREE.Mesh(chestGeom, highlightMaterial)
50
+  chest.position.set(0.15, 0.35, 0.2)
51
+  group.add(chest)
52
+
53
+  // Tail feathers
54
+  const tailGroup = new THREE.Group()
55
+  for (let i = 0; i < 3; i++) {
56
+    const tailGeom = new THREE.ConeGeometry(0.08, 0.3, 6)
57
+    const tail = new THREE.Mesh(tailGeom, bodyMaterial)
58
+    tail.rotation.x = Math.PI / 2 + (i - 1) * 0.15
59
+    tail.rotation.z = (i - 1) * 0.2
60
+    tail.position.set(-0.55 - i * 0.05, 0.35, (i - 1) * 0.08)
61
+    tailGroup.add(tail)
62
+  }
63
+  group.add(tailGroup)
64
+
65
+  // Wings
66
+  const wingGeom = new THREE.SphereGeometry(0.25, 8, 6)
67
+  wingGeom.scale(0.6, 1, 0.3)
68
+
69
+  const leftWing = new THREE.Mesh(wingGeom, bodyMaterial)
70
+  leftWing.position.set(-0.1, 0.35, 0.45)
71
+  leftWing.rotation.x = 0.2
72
+  group.add(leftWing)
73
+
74
+  const rightWing = new THREE.Mesh(wingGeom, bodyMaterial)
75
+  rightWing.position.set(-0.1, 0.35, -0.45)
76
+  rightWing.rotation.x = -0.2
77
+  group.add(rightWing)
78
+
79
+  // Head
80
+  const headGeom = new THREE.SphereGeometry(0.32, 16, 12)
81
+  const head = new THREE.Mesh(headGeom, bodyMaterial)
82
+  head.position.set(0.45, 0.7, 0)
83
+  group.add(head)
84
+
85
+  // Head tuft (little feather on top)
86
+  const tuftGeom = new THREE.ConeGeometry(0.05, 0.15, 6)
87
+  const tuft = new THREE.Mesh(tuftGeom, bodyMaterial)
88
+  tuft.position.set(0.4, 1.0, 0)
89
+  tuft.rotation.z = -0.3
90
+  group.add(tuft)
91
+
92
+  // Beak - cone pointing forward
93
+  const beakGeom = new THREE.ConeGeometry(0.1, 0.35, 8)
94
+  const beak = new THREE.Mesh(beakGeom, beakMaterial)
95
+  beak.rotation.z = -Math.PI / 2
96
+  beak.position.set(0.8, 0.65, 0)
97
+  group.add(beak)
98
+
99
+  // Eyes - big and expressive Wind Waker style
100
+  const eyeGeom = new THREE.SphereGeometry(0.1, 12, 8)
101
+
102
+  const leftEye = new THREE.Mesh(eyeGeom, eyeWhiteMaterial)
103
+  leftEye.position.set(0.65, 0.78, 0.15)
104
+  leftEye.scale.set(0.8, 1, 0.6)
105
+  group.add(leftEye)
106
+
107
+  const rightEye = new THREE.Mesh(eyeGeom, eyeWhiteMaterial)
108
+  rightEye.position.set(0.65, 0.78, -0.15)
109
+  rightEye.scale.set(0.8, 1, 0.6)
110
+  group.add(rightEye)
111
+
112
+  // Pupils
113
+  const pupilGeom = new THREE.SphereGeometry(0.05, 8, 6)
114
+
115
+  const leftPupil = new THREE.Mesh(pupilGeom, eyePupilMaterial)
116
+  leftPupil.position.set(0.72, 0.78, 0.15)
117
+  group.add(leftPupil)
118
+
119
+  const rightPupil = new THREE.Mesh(pupilGeom, eyePupilMaterial)
120
+  rightPupil.position.set(0.72, 0.78, -0.15)
121
+  group.add(rightPupil)
122
+
123
+  // Eye shine
124
+  const shineGeom = new THREE.SphereGeometry(0.025, 6, 4)
125
+  const shineMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff })
126
+
127
+  const leftShine = new THREE.Mesh(shineGeom, shineMaterial)
128
+  leftShine.position.set(0.73, 0.82, 0.13)
129
+  group.add(leftShine)
130
+
131
+  const rightShine = new THREE.Mesh(shineGeom, shineMaterial)
132
+  rightShine.position.set(0.73, 0.82, -0.17)
133
+  group.add(rightShine)
134
+
135
+  // Add to scene
136
+  scene.add(group)
137
+
138
+  // Duck state
139
+  const state = {
140
+    position: new THREE.Vector3(0, 0, 0),
141
+    targetPosition: new THREE.Vector3(0, 0, 0),
142
+    rotation: 0,
143
+    mode: 'idle', // 'idle', 'waiting', 'swimming'
144
+    waitTimer: 0,
145
+    waitDuration: 0,
146
+    idleTimer: 0,
147
+    nextIdleMove: 3 + Math.random() * 4,
148
+    wobble: 0,
149
+    headBob: 0
150
+  }
151
+
152
+  // Movement speeds
153
+  const idleSpeed = 0.5
154
+  const swimSpeed = 1.2
155
+
156
+  function pickIdleTarget(pond) {
157
+    const angle = Math.random() * Math.PI * 2
158
+    const radius = Math.random() * (pond.radius * 0.7) + pond.radius * 0.1
159
+    state.targetPosition.set(
160
+      Math.cos(angle) * radius,
161
+      0,
162
+      Math.sin(angle) * radius
163
+    )
164
+  }
165
+
166
+  function update(delta, elapsed, breadBits, pond) {
167
+    // Find closest bread
168
+    let closestBread = null
169
+    let closestDist = Infinity
170
+
171
+    for (const bread of breadBits) {
172
+      if (!bread.eaten) {
173
+        const dx = bread.position.x - state.position.x
174
+        const dz = bread.position.z - state.position.z
175
+        const dist = Math.sqrt(dx * dx + dz * dz)
176
+        if (dist < closestDist) {
177
+          closestDist = dist
178
+          closestBread = bread
179
+        }
180
+      }
181
+    }
182
+
183
+    // React to bread
184
+    if (closestBread && state.mode === 'idle') {
185
+      state.mode = 'waiting'
186
+      state.waitTimer = 0
187
+      state.waitDuration = 0.5 + Math.random() * 0.8 // Quirky delay
188
+      state.pendingTarget = closestBread.position.clone()
189
+    }
190
+
191
+    // Handle waiting (the quirky pause)
192
+    if (state.mode === 'waiting') {
193
+      state.waitTimer += delta
194
+      state.headBob = Math.sin(state.waitTimer * 8) * 0.1
195
+      head.position.y = 0.7 + state.headBob
196
+
197
+      if (state.waitTimer >= state.waitDuration) {
198
+        state.mode = 'swimming'
199
+        state.targetPosition.copy(state.pendingTarget)
200
+      }
201
+    }
202
+
203
+    // Movement
204
+    const dx = state.targetPosition.x - state.position.x
205
+    const dz = state.targetPosition.z - state.position.z
206
+    const dist = Math.sqrt(dx * dx + dz * dz)
207
+
208
+    if (dist > 0.1) {
209
+      const speed = state.mode === 'swimming' ? swimSpeed : idleSpeed
210
+      const moveAmount = Math.min(speed * delta, dist)
211
+      const moveX = (dx / dist) * moveAmount
212
+      const moveZ = (dz / dist) * moveAmount
213
+
214
+      state.position.x += moveX
215
+      state.position.z += moveZ
216
+
217
+      // Face movement direction
218
+      state.rotation = Math.atan2(dz, dx)
219
+
220
+      // Wobble animation while moving
221
+      state.wobble += delta * 8
222
+    } else {
223
+      // Arrived
224
+      if (state.mode === 'swimming') {
225
+        state.mode = 'idle'
226
+        // Eat nearby bread
227
+        for (const bread of breadBits) {
228
+          if (!bread.eaten) {
229
+            const bx = bread.position.x - state.position.x
230
+            const bz = bread.position.z - state.position.z
231
+            if (Math.sqrt(bx * bx + bz * bz) < 0.4) {
232
+              bread.eaten = true
233
+            }
234
+          }
235
+        }
236
+      }
237
+    }
238
+
239
+    // Idle wandering
240
+    if (state.mode === 'idle' && !closestBread) {
241
+      state.idleTimer += delta
242
+      if (state.idleTimer >= state.nextIdleMove) {
243
+        pickIdleTarget(pond)
244
+        state.idleTimer = 0
245
+        state.nextIdleMove = 3 + Math.random() * 4
246
+      }
247
+    }
248
+
249
+    // Apply transforms
250
+    group.position.x = state.position.x
251
+    group.position.z = state.position.z
252
+
253
+    // Bobbing on water
254
+    group.position.y = Math.sin(elapsed * 2) * 0.03
255
+
256
+    // Rotation (face direction of movement)
257
+    group.rotation.y = -state.rotation + Math.PI / 2
258
+
259
+    // Body wobble while swimming
260
+    const wobbleAmount = Math.sin(state.wobble) * 0.08
261
+    body.rotation.z = wobbleAmount
262
+    head.position.x = 0.45 + wobbleAmount * 0.2
263
+
264
+    // Wing flap animation
265
+    leftWing.rotation.z = Math.sin(elapsed * 3) * 0.1
266
+    rightWing.rotation.z = -Math.sin(elapsed * 3) * 0.1
267
+
268
+    // Gentle head bob
269
+    if (state.mode !== 'waiting') {
270
+      head.position.y = 0.7 + Math.sin(elapsed * 2) * 0.02
271
+    }
272
+  }
273
+
274
+  return {
275
+    group,
276
+    update,
277
+    getPosition: () => state.position.clone()
278
+  }
279
+}
src/renderers/three/index.jsadded
@@ -0,0 +1,179 @@
1
+// Three.js 3D renderer for dougk
2
+import * as THREE from 'three'
3
+import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'
4
+import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'
5
+import { OutlinePass } from 'three/addons/postprocessing/OutlinePass.js'
6
+import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'
7
+import { createDoug } from './duck.js'
8
+import { createPond } from './pond.js'
9
+import { BreadManager } from './bread.js'
10
+
11
+let scene, camera, renderer, composer, outlinePass
12
+let doug, pond, breadManager
13
+let clock
14
+let animationId = null
15
+
16
+function createToonGradient() {
17
+  const canvas = document.createElement('canvas')
18
+  canvas.width = 4
19
+  canvas.height = 1
20
+  const ctx = canvas.getContext('2d')
21
+
22
+  ctx.fillStyle = '#444444'
23
+  ctx.fillRect(0, 0, 1, 1)
24
+  ctx.fillStyle = '#888888'
25
+  ctx.fillRect(1, 0, 1, 1)
26
+  ctx.fillStyle = '#cccccc'
27
+  ctx.fillRect(2, 0, 1, 1)
28
+  ctx.fillStyle = '#ffffff'
29
+  ctx.fillRect(3, 0, 1, 1)
30
+
31
+  const texture = new THREE.CanvasTexture(canvas)
32
+  texture.minFilter = THREE.NearestFilter
33
+  texture.magFilter = THREE.NearestFilter
34
+  return texture
35
+}
36
+
37
+export function start(container) {
38
+  if (animationId) return
39
+
40
+  // Scene
41
+  scene = new THREE.Scene()
42
+  scene.background = new THREE.Color(0x55a04b)
43
+
44
+  // Camera
45
+  const aspect = window.innerWidth / window.innerHeight
46
+  const frustumSize = 12
47
+  camera = new THREE.OrthographicCamera(
48
+    -frustumSize * aspect / 2,
49
+    frustumSize * aspect / 2,
50
+    frustumSize / 2,
51
+    -frustumSize / 2,
52
+    0.1,
53
+    100
54
+  )
55
+  camera.position.set(10, 10, 10)
56
+  camera.lookAt(0, 0, 0)
57
+
58
+  // Renderer
59
+  renderer = new THREE.WebGLRenderer({ antialias: true })
60
+  renderer.setSize(window.innerWidth, window.innerHeight)
61
+  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
62
+  container.appendChild(renderer.domElement)
63
+
64
+  // Lighting
65
+  const sunLight = new THREE.DirectionalLight(0xffffee, 1.5)
66
+  sunLight.position.set(5, 10, 5)
67
+  scene.add(sunLight)
68
+
69
+  const hemiLight = new THREE.HemisphereLight(0x87ceeb, 0x55a04b, 0.6)
70
+  scene.add(hemiLight)
71
+
72
+  const fillLight = new THREE.DirectionalLight(0xffffff, 0.3)
73
+  fillLight.position.set(-5, 5, -5)
74
+  scene.add(fillLight)
75
+
76
+  const toonGradient = createToonGradient()
77
+
78
+  // Create objects
79
+  pond = createPond(scene, toonGradient)
80
+  doug = createDoug(scene, toonGradient)
81
+  breadManager = new BreadManager(scene, toonGradient)
82
+
83
+  // Post-processing
84
+  composer = new EffectComposer(renderer)
85
+  composer.addPass(new RenderPass(scene, camera))
86
+
87
+  outlinePass = new OutlinePass(
88
+    new THREE.Vector2(window.innerWidth, window.innerHeight),
89
+    scene,
90
+    camera
91
+  )
92
+  outlinePass.edgeStrength = 3
93
+  outlinePass.edgeGlow = 0
94
+  outlinePass.edgeThickness = 1.5
95
+  outlinePass.visibleEdgeColor.set(0x191410)
96
+  outlinePass.hiddenEdgeColor.set(0x191410)
97
+  outlinePass.selectedObjects = [doug.group, pond.group]
98
+  composer.addPass(outlinePass)
99
+  composer.addPass(new OutputPass())
100
+
101
+  // Raycaster
102
+  const raycaster = new THREE.Raycaster()
103
+  const mouse = new THREE.Vector2()
104
+
105
+  renderer.domElement.addEventListener('click', (event) => {
106
+    mouse.x = (event.clientX / window.innerWidth) * 2 - 1
107
+    mouse.y = -(event.clientY / window.innerHeight) * 2 + 1
108
+
109
+    raycaster.setFromCamera(mouse, camera)
110
+    const intersects = raycaster.intersectObject(pond.water)
111
+
112
+    if (intersects.length > 0) {
113
+      const point = intersects[0].point
114
+      breadManager.spawnBread(point.x, point.z)
115
+      pond.addRipple(point.x, point.z)
116
+      outlinePass.selectedObjects = [doug.group, pond.group, ...breadManager.getMeshes()]
117
+    }
118
+  })
119
+
120
+  // Resize handler
121
+  window.addEventListener('resize', onResize)
122
+
123
+  clock = new THREE.Clock()
124
+  animate()
125
+}
126
+
127
+function onResize() {
128
+  const aspect = window.innerWidth / window.innerHeight
129
+  const frustumSize = 12
130
+  camera.left = -frustumSize * aspect / 2
131
+  camera.right = frustumSize * aspect / 2
132
+  camera.top = frustumSize / 2
133
+  camera.bottom = -frustumSize / 2
134
+  camera.updateProjectionMatrix()
135
+
136
+  renderer.setSize(window.innerWidth, window.innerHeight)
137
+  composer.setSize(window.innerWidth, window.innerHeight)
138
+}
139
+
140
+function animate() {
141
+  animationId = requestAnimationFrame(animate)
142
+
143
+  const delta = clock.getDelta()
144
+  const elapsed = clock.getElapsedTime()
145
+
146
+  doug.update(delta, elapsed, breadManager.getActiveBits(), pond)
147
+  breadManager.update(delta, elapsed)
148
+  pond.update(delta, elapsed)
149
+
150
+  composer.render()
151
+}
152
+
153
+export function stop() {
154
+  if (animationId) {
155
+    cancelAnimationFrame(animationId)
156
+    animationId = null
157
+  }
158
+
159
+  window.removeEventListener('resize', onResize)
160
+
161
+  if (renderer) {
162
+    renderer.domElement.remove()
163
+    renderer.dispose()
164
+  }
165
+
166
+  if (composer) {
167
+    composer.dispose()
168
+  }
169
+
170
+  scene = null
171
+  camera = null
172
+  renderer = null
173
+  composer = null
174
+  doug = null
175
+  pond = null
176
+  breadManager = null
177
+}
178
+
179
+export const name = '3D'
src/renderers/three/main.jsadded
@@ -0,0 +1,167 @@
1
+// dougk - a cozy pond simulator featuring Doug the duck
2
+// Three.js version with Wind Waker cel-shading
3
+
4
+import * as THREE from 'three'
5
+import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'
6
+import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'
7
+import { OutlinePass } from 'three/addons/postprocessing/OutlinePass.js'
8
+import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'
9
+import { createDoug } from './duck.js'
10
+import { createPond } from './pond.js'
11
+import { BreadManager } from './bread.js'
12
+
13
+// Scene setup
14
+const scene = new THREE.Scene()
15
+scene.background = new THREE.Color(0x55a04b) // Vibrant grass green
16
+
17
+// Isometric-style orthographic camera
18
+const aspect = window.innerWidth / window.innerHeight
19
+const frustumSize = 12
20
+const camera = new THREE.OrthographicCamera(
21
+  -frustumSize * aspect / 2,
22
+  frustumSize * aspect / 2,
23
+  frustumSize / 2,
24
+  -frustumSize / 2,
25
+  0.1,
26
+  100
27
+)
28
+
29
+// Classic isometric angle
30
+camera.position.set(10, 10, 10)
31
+camera.lookAt(0, 0, 0)
32
+
33
+// Renderer
34
+const renderer = new THREE.WebGLRenderer({ antialias: true })
35
+renderer.setSize(window.innerWidth, window.innerHeight)
36
+renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
37
+document.body.appendChild(renderer.domElement)
38
+
39
+// Cel-shading lighting setup
40
+// Main directional light (sun)
41
+const sunLight = new THREE.DirectionalLight(0xffffee, 1.5)
42
+sunLight.position.set(5, 10, 5)
43
+scene.add(sunLight)
44
+
45
+// Hemisphere light for ambient (sky/ground colors like Wind Waker)
46
+const hemiLight = new THREE.HemisphereLight(0x87ceeb, 0x55a04b, 0.6)
47
+scene.add(hemiLight)
48
+
49
+// Subtle fill light
50
+const fillLight = new THREE.DirectionalLight(0xffffff, 0.3)
51
+fillLight.position.set(-5, 5, -5)
52
+scene.add(fillLight)
53
+
54
+// Create gradient texture for toon shading
55
+function createToonGradient() {
56
+  const canvas = document.createElement('canvas')
57
+  canvas.width = 4
58
+  canvas.height = 1
59
+  const ctx = canvas.getContext('2d')
60
+
61
+  // 3-step gradient for Wind Waker style
62
+  ctx.fillStyle = '#444444'
63
+  ctx.fillRect(0, 0, 1, 1)
64
+  ctx.fillStyle = '#888888'
65
+  ctx.fillRect(1, 0, 1, 1)
66
+  ctx.fillStyle = '#cccccc'
67
+  ctx.fillRect(2, 0, 1, 1)
68
+  ctx.fillStyle = '#ffffff'
69
+  ctx.fillRect(3, 0, 1, 1)
70
+
71
+  const texture = new THREE.CanvasTexture(canvas)
72
+  texture.minFilter = THREE.NearestFilter
73
+  texture.magFilter = THREE.NearestFilter
74
+  return texture
75
+}
76
+
77
+const toonGradient = createToonGradient()
78
+
79
+// Create the pond
80
+const pond = createPond(scene, toonGradient)
81
+
82
+// Create Doug the duck
83
+const doug = createDoug(scene, toonGradient)
84
+
85
+// Bread manager
86
+const breadManager = new BreadManager(scene, toonGradient)
87
+
88
+// Post-processing for outlines
89
+const composer = new EffectComposer(renderer)
90
+const renderPass = new RenderPass(scene, camera)
91
+composer.addPass(renderPass)
92
+
93
+// Outline pass for that bold Wind Waker look
94
+const outlinePass = new OutlinePass(
95
+  new THREE.Vector2(window.innerWidth, window.innerHeight),
96
+  scene,
97
+  camera
98
+)
99
+outlinePass.edgeStrength = 3
100
+outlinePass.edgeGlow = 0
101
+outlinePass.edgeThickness = 1.5
102
+outlinePass.visibleEdgeColor.set(0x191410)
103
+outlinePass.hiddenEdgeColor.set(0x191410)
104
+outlinePass.selectedObjects = [doug.group, pond.group, ...breadManager.getMeshes()]
105
+composer.addPass(outlinePass)
106
+
107
+const outputPass = new OutputPass()
108
+composer.addPass(outputPass)
109
+
110
+// Raycaster for mouse interaction
111
+const raycaster = new THREE.Raycaster()
112
+const mouse = new THREE.Vector2()
113
+
114
+// Handle clicks for bread dropping
115
+renderer.domElement.addEventListener('click', (event) => {
116
+  mouse.x = (event.clientX / window.innerWidth) * 2 - 1
117
+  mouse.y = -(event.clientY / window.innerHeight) * 2 + 1
118
+
119
+  raycaster.setFromCamera(mouse, camera)
120
+  const intersects = raycaster.intersectObject(pond.water)
121
+
122
+  if (intersects.length > 0) {
123
+    const point = intersects[0].point
124
+    breadManager.spawnBread(point.x, point.z)
125
+    pond.addRipple(point.x, point.z)
126
+
127
+    // Update outline objects
128
+    outlinePass.selectedObjects = [doug.group, pond.group, ...breadManager.getMeshes()]
129
+  }
130
+})
131
+
132
+// Handle window resize
133
+window.addEventListener('resize', () => {
134
+  const aspect = window.innerWidth / window.innerHeight
135
+  camera.left = -frustumSize * aspect / 2
136
+  camera.right = frustumSize * aspect / 2
137
+  camera.top = frustumSize / 2
138
+  camera.bottom = -frustumSize / 2
139
+  camera.updateProjectionMatrix()
140
+
141
+  renderer.setSize(window.innerWidth, window.innerHeight)
142
+  composer.setSize(window.innerWidth, window.innerHeight)
143
+})
144
+
145
+// Animation loop
146
+const clock = new THREE.Clock()
147
+
148
+function animate() {
149
+  requestAnimationFrame(animate)
150
+
151
+  const delta = clock.getDelta()
152
+  const elapsed = clock.getElapsedTime()
153
+
154
+  // Update Doug
155
+  doug.update(delta, elapsed, breadManager.getActiveBits(), pond)
156
+
157
+  // Update bread
158
+  breadManager.update(delta, elapsed)
159
+
160
+  // Update pond (ripples, etc)
161
+  pond.update(delta, elapsed)
162
+
163
+  // Render with post-processing
164
+  composer.render()
165
+}
166
+
167
+animate()
src/renderers/three/pond.jsadded
@@ -0,0 +1,211 @@
1
+// Pond environment - 3D water, shore, and fence with Wind Waker styling
2
+import * as THREE from 'three'
3
+
4
+export function createPond(scene, gradientMap) {
5
+  const group = new THREE.Group()
6
+  const radius = 4
7
+
8
+  // Colors
9
+  const waterColor = 0x46a0be // Vibrant teal
10
+  const waterDeep = 0x2d7a94 // Darker teal
11
+  const shoreColor = 0x78b456 // Bright grass green
12
+  const sandColor = 0xc8b080 // Sandy edge
13
+  const fenceColor = 0xb4823c // Warm wood
14
+
15
+  // Materials
16
+  const waterMaterial = new THREE.MeshToonMaterial({
17
+    color: waterColor,
18
+    gradientMap: gradientMap,
19
+    transparent: true,
20
+    opacity: 0.9
21
+  })
22
+
23
+  const shoreMaterial = new THREE.MeshToonMaterial({
24
+    color: shoreColor,
25
+    gradientMap: gradientMap
26
+  })
27
+
28
+  const sandMaterial = new THREE.MeshToonMaterial({
29
+    color: sandColor,
30
+    gradientMap: gradientMap
31
+  })
32
+
33
+  const fenceMaterial = new THREE.MeshToonMaterial({
34
+    color: fenceColor,
35
+    gradientMap: gradientMap
36
+  })
37
+
38
+  // Ground plane (grass)
39
+  const groundGeom = new THREE.CircleGeometry(radius + 2.5, 32)
40
+  groundGeom.rotateX(-Math.PI / 2)
41
+  const ground = new THREE.Mesh(groundGeom, shoreMaterial)
42
+  ground.position.y = -0.05
43
+  group.add(ground)
44
+
45
+  // Sandy shore ring
46
+  const sandGeom = new THREE.RingGeometry(radius - 0.2, radius + 0.5, 32)
47
+  sandGeom.rotateX(-Math.PI / 2)
48
+  const sand = new THREE.Mesh(sandGeom, sandMaterial)
49
+  sand.position.y = -0.02
50
+  group.add(sand)
51
+
52
+  // Water surface
53
+  const waterGeom = new THREE.CircleGeometry(radius, 32)
54
+  waterGeom.rotateX(-Math.PI / 2)
55
+  const water = new THREE.Mesh(waterGeom, waterMaterial)
56
+  water.position.y = 0
57
+  group.add(water)
58
+
59
+  // Water depth visual (darker center)
60
+  const deepGeom = new THREE.CircleGeometry(radius * 0.6, 24)
61
+  deepGeom.rotateX(-Math.PI / 2)
62
+  const deepMaterial = new THREE.MeshToonMaterial({
63
+    color: waterDeep,
64
+    gradientMap: gradientMap,
65
+    transparent: true,
66
+    opacity: 0.5
67
+  })
68
+  const deep = new THREE.Mesh(deepGeom, deepMaterial)
69
+  deep.position.y = -0.01
70
+  group.add(deep)
71
+
72
+  // Water highlight (light reflection)
73
+  const highlightGeom = new THREE.CircleGeometry(radius * 0.3, 16)
74
+  highlightGeom.rotateX(-Math.PI / 2)
75
+  const highlightMaterial = new THREE.MeshBasicMaterial({
76
+    color: 0x88d4e8,
77
+    transparent: true,
78
+    opacity: 0.4
79
+  })
80
+  const highlight = new THREE.Mesh(highlightGeom, highlightMaterial)
81
+  highlight.position.set(-radius * 0.35, 0.02, -radius * 0.35)
82
+  group.add(highlight)
83
+
84
+  // Grass tufts around the pond
85
+  const grassTuftGeom = new THREE.ConeGeometry(0.15, 0.3, 4)
86
+  const grassMaterial = new THREE.MeshToonMaterial({
87
+    color: 0x4a8530,
88
+    gradientMap: gradientMap
89
+  })
90
+
91
+  for (let i = 0; i < 30; i++) {
92
+    const angle = Math.random() * Math.PI * 2
93
+    const dist = radius + 0.8 + Math.random() * 1.5
94
+
95
+    const tuft = new THREE.Mesh(grassTuftGeom, grassMaterial)
96
+    tuft.position.set(
97
+      Math.cos(angle) * dist,
98
+      0.1,
99
+      Math.sin(angle) * dist
100
+    )
101
+    tuft.rotation.x = (Math.random() - 0.5) * 0.3
102
+    tuft.rotation.z = (Math.random() - 0.5) * 0.3
103
+    tuft.scale.setScalar(0.5 + Math.random() * 0.5)
104
+    group.add(tuft)
105
+  }
106
+
107
+  // Rickety fence
108
+  const fenceGroup = new THREE.Group()
109
+  const fenceX = radius + 1
110
+  const postCount = 5
111
+  const postSpacing = 0.8
112
+
113
+  for (let i = 0; i < postCount; i++) {
114
+    const wobble = Math.sin(i * 1.5) * 0.1
115
+
116
+    // Fence post
117
+    const postGeom = new THREE.BoxGeometry(0.12, 0.8, 0.12)
118
+    const post = new THREE.Mesh(postGeom, fenceMaterial)
119
+    post.position.set(
120
+      fenceX + wobble,
121
+      0.35,
122
+      -1.5 + i * postSpacing
123
+    )
124
+    post.rotation.x = wobble * 0.3
125
+    post.rotation.z = wobble * 0.5
126
+    fenceGroup.add(post)
127
+
128
+    // Post cap
129
+    const capGeom = new THREE.BoxGeometry(0.16, 0.06, 0.16)
130
+    const cap = new THREE.Mesh(capGeom, fenceMaterial)
131
+    cap.position.set(
132
+      fenceX + wobble,
133
+      0.78,
134
+      -1.5 + i * postSpacing
135
+    )
136
+    cap.rotation.x = wobble * 0.3
137
+    cap.rotation.z = wobble * 0.5
138
+    fenceGroup.add(cap)
139
+  }
140
+
141
+  // Horizontal rails
142
+  const railGeom = new THREE.BoxGeometry(0.08, 0.08, postSpacing * (postCount - 1) + 0.3)
143
+
144
+  const topRail = new THREE.Mesh(railGeom, fenceMaterial)
145
+  topRail.position.set(fenceX + 0.05, 0.6, -1.5 + (postCount - 1) * postSpacing / 2)
146
+  topRail.rotation.y = 0.02
147
+  fenceGroup.add(topRail)
148
+
149
+  const bottomRail = new THREE.Mesh(railGeom, fenceMaterial)
150
+  bottomRail.position.set(fenceX - 0.03, 0.25, -1.5 + (postCount - 1) * postSpacing / 2)
151
+  bottomRail.rotation.y = -0.03
152
+  fenceGroup.add(bottomRail)
153
+
154
+  group.add(fenceGroup)
155
+
156
+  // Ripple system
157
+  const ripples = []
158
+  const rippleGeom = new THREE.RingGeometry(0.1, 0.15, 16)
159
+  rippleGeom.rotateX(-Math.PI / 2)
160
+
161
+  function addRipple(x, z) {
162
+    const rippleMaterial = new THREE.MeshBasicMaterial({
163
+      color: 0xffffff,
164
+      transparent: true,
165
+      opacity: 0.6,
166
+      side: THREE.DoubleSide
167
+    })
168
+    const ripple = new THREE.Mesh(rippleGeom.clone(), rippleMaterial)
169
+    ripple.position.set(x, 0.02, z)
170
+
171
+    group.add(ripple)
172
+    ripples.push({
173
+      mesh: ripple,
174
+      age: 0,
175
+      maxAge: 1.5
176
+    })
177
+  }
178
+
179
+  function update(delta, elapsed) {
180
+    // Animate water highlight
181
+    highlight.position.x = -radius * 0.35 + Math.sin(elapsed * 0.5) * 0.2
182
+    highlight.position.z = -radius * 0.35 + Math.cos(elapsed * 0.5) * 0.2
183
+
184
+    // Update ripples
185
+    for (let i = ripples.length - 1; i >= 0; i--) {
186
+      const ripple = ripples[i]
187
+      ripple.age += delta
188
+
189
+      const progress = ripple.age / ripple.maxAge
190
+      ripple.mesh.scale.setScalar(1 + progress * 3)
191
+      ripple.mesh.material.opacity = 0.6 * (1 - progress)
192
+
193
+      if (ripple.age >= ripple.maxAge) {
194
+        group.remove(ripple.mesh)
195
+        ripple.mesh.geometry.dispose()
196
+        ripple.mesh.material.dispose()
197
+        ripples.splice(i, 1)
198
+      }
199
+    }
200
+  }
201
+
202
+  scene.add(group)
203
+
204
+  return {
205
+    group,
206
+    water,
207
+    radius,
208
+    addRipple,
209
+    update
210
+  }
211
+}
vite.config.jsadded
@@ -0,0 +1,9 @@
1
+import { defineConfig } from 'vite'
2
+
3
+export default defineConfig({
4
+  root: '.',
5
+  publicDir: 'assets',
6
+  build: {
7
+    outDir: 'dist'
8
+  }
9
+})