init. quacking merrily
- SHA
4b1b2b6a8a07c3f17a375b18421e87a55785e00c- Tree
955275a
4b1b2b6
4b1b2b6a8a07c3f17a375b18421e87a55785e00c955275a| Status | File | + | - |
|---|---|---|---|
| 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 | +}) | |