├── .DS_Store ├── .gitignore ├── LICENSE ├── README.md ├── audio ├── space.mp3 └── underwater.mp3 ├── cat.html ├── eyes.html ├── images ├── device1.png ├── device1_cat.png ├── device1_eyes.png ├── device2_notion.png └── device2_plant.png ├── index.html ├── notion.html ├── plant.html └── resource ├── eyes.css ├── eyes.js ├── notion.css ├── notion.js ├── plant.css └── plant.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Barqawiz/Tamagotchi/260f5236103834a6734a4d5c59201bdc91341fe6/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 github.com/Barqawiz/ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tamagotchi 2 | 3 | This project is inspired by the classic Tamagotchi device, featuring a virtual character drawn with JavaScript. The character can be controlled through various buttons, or it can dynamically change based on interactions with an AI assistant. 4 | 5 | 6 | ## How to Use: 7 | 8 | 1. **Manual Mode**: Click on the buttons to perform actions. 9 | 2. **AI-Controlled Mode**: Connect this with an AI assistant to automatically update the character. 10 | 11 | 12 | Feel free to customize the project, or expand it for AI assistant interaction. 13 | 14 | ## AI Characters: 15 | 16 | ### Eyes 17 | 18 | 19 | 20 | ### Notion 21 | 22 | 23 | 24 | ### Cat 25 | 26 | 27 | ## Plant 28 | 29 | 30 | ## How to Connect to Chatbot: 31 | 32 | Call `setAction` based on the AI model’s status or reactions. For example: 33 | 34 | - `setAction('curious')` if the AI’s response includes a question. 35 | - `setAction('excited')` if the response shows excitement. 36 | - `setAction('speaking')` for general replies. 37 | 38 | Check the [demo](https://barqawiz.github.io/Tamagotchi/) for more action details. 39 | 40 | -------------------------------------------------------------------------------- /audio/space.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Barqawiz/Tamagotchi/260f5236103834a6734a4d5c59201bdc91341fe6/audio/space.mp3 -------------------------------------------------------------------------------- /audio/underwater.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Barqawiz/Tamagotchi/260f5236103834a6734a4d5c59201bdc91341fe6/audio/underwater.mp3 -------------------------------------------------------------------------------- /cat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Interactive Pixel Cat 6 | 35 | 36 | 37 | 38 | 39 | 40 |
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 |
59 | 60 | 588 | 589 | 590 | 591 | 592 | -------------------------------------------------------------------------------- /eyes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Interactive Eye Character 6 | 7 | 8 | 9 |
10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /images/device1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Barqawiz/Tamagotchi/260f5236103834a6734a4d5c59201bdc91341fe6/images/device1.png -------------------------------------------------------------------------------- /images/device1_cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Barqawiz/Tamagotchi/260f5236103834a6734a4d5c59201bdc91341fe6/images/device1_cat.png -------------------------------------------------------------------------------- /images/device1_eyes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Barqawiz/Tamagotchi/260f5236103834a6734a4d5c59201bdc91341fe6/images/device1_eyes.png -------------------------------------------------------------------------------- /images/device2_notion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Barqawiz/Tamagotchi/260f5236103834a6734a4d5c59201bdc91341fe6/images/device2_notion.png -------------------------------------------------------------------------------- /images/device2_plant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Barqawiz/Tamagotchi/260f5236103834a6734a4d5c59201bdc91341fe6/images/device2_plant.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Give Identity to Your AI 7 | 89 | 90 | 91 | 92 | 93 |
94 | Star 95 |
96 | 97 |

Tamagotchi

98 |

Give Identity to Your AI

99 | 100 |
101 | 102 | 103 | 104 | 105 | 106 |
107 | 108 | 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /notion.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Notion AI Assistant 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 | 32 |
33 | Star 34 |
35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /plant.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Interactive Plant Character 6 | 7 | 8 | 9 |
10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 |
28 |
29 |
30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /resource/eyes.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | background-color: #ffffff; 5 | color: #000000; 6 | font-family: Arial, sans-serif; 7 | } 8 | 9 | #container { 10 | text-align: center; 11 | padding-top: 50px; 12 | } 13 | 14 | #eyesCanvas { 15 | background-color: #eeeeee; 16 | border: 2px solid #000; 17 | border-radius: 10px; 18 | } 19 | 20 | #controls { 21 | margin-top: 20px; 22 | } 23 | 24 | button { 25 | background-color: #ff66cc; 26 | border: none; 27 | color: white; 28 | padding: 10px 15px; 29 | margin: 5px; 30 | border-radius: 10px; 31 | font-size: 14px; 32 | cursor: pointer; 33 | transition: background-color 0.3s; 34 | } 35 | 36 | button:hover { 37 | background-color: #ff85d5; 38 | } 39 | -------------------------------------------------------------------------------- /resource/eyes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * MIT License 3 | * Copyright (c) 2024 github.com/Barqawiz/ 4 | * **/ 5 | 6 | // get the canvas and context 7 | const canvas = document.getElementById('eyesCanvas'); 8 | const ctx = canvas.getContext('2d'); 9 | 10 | // eye parameters 11 | const eyeRadius = 80; 12 | const irisRadiusDefault = 30; 13 | const pupilRadiusDefault = 12; 14 | const eyeOffsetX = 120; // distance from center for each eye 15 | const eyeCenterY = canvas.height / 2; 16 | const leftEyeCenterX = canvas.width / 2 - eyeOffsetX; 17 | const rightEyeCenterX = canvas.width / 2 + eyeOffsetX; 18 | 19 | // mouse position 20 | let mouseX = canvas.width / 2; 21 | let mouseY = canvas.height / 2; 22 | 23 | // state variables 24 | let isBlinking = false; 25 | let blinkProgress = 0; 26 | let isSurprised = false; 27 | let isSleepy = false; 28 | let isBored = false; 29 | let isFollowingMouse = false; 30 | let isIdle = true; 31 | let isLookingAround = false; 32 | let isWTF = false; 33 | 34 | // idle blink timer 35 | let idleBlinkInterval; 36 | 37 | // look around variables 38 | let lookPause = false; 39 | let lookDirection = 1; // 1 for right, -1 for left 40 | let lookAngle = 0; 41 | 42 | // animation parameters 43 | let lastTime = 0; 44 | 45 | function draw(timestamp) { 46 | const deltaTime = timestamp - lastTime; 47 | lastTime = timestamp; 48 | 49 | ctx.clearRect(0, 0, canvas.width, canvas.height); 50 | 51 | // update lookAngle globally 52 | if (isLookingAround && !lookPause) { 53 | // speed 54 | lookAngle += 0.01 * lookDirection; 55 | if (Math.abs(lookAngle) > Math.PI / 4) { 56 | // limit angle to ±45 degrees 57 | lookPause = true; 58 | setTimeout(() => { 59 | lookPause = false; 60 | // change direction after pause 61 | lookDirection *= -1; 62 | }, 500 /*shorter pause for smoother movement*/); 63 | } 64 | } 65 | 66 | // draw left eye 67 | drawEye(leftEyeCenterX, eyeCenterY); 68 | 69 | // draw right eye 70 | drawEye(rightEyeCenterX, eyeCenterY); 71 | 72 | requestAnimationFrame(draw); 73 | } 74 | 75 | function drawEye(centerX, centerY) { 76 | ctx.save(); 77 | 78 | let currentEyeRadius = eyeRadius; 79 | let currentIrisRadius = irisRadiusDefault; 80 | let currentPupilRadius = pupilRadiusDefault; 81 | 82 | // adjust eye size if surprised 83 | if (isSurprised) { 84 | currentEyeRadius = eyeRadius * 1.2; 85 | currentIrisRadius = irisRadiusDefault * 1.2; 86 | currentPupilRadius = pupilRadiusDefault * 1.2; 87 | } 88 | 89 | // draw eye outline 90 | ctx.beginPath(); 91 | ctx.arc(centerX, centerY, currentEyeRadius, 0, Math.PI * 2); 92 | ctx.fillStyle = '#FFFFFF'; 93 | ctx.fill(); 94 | ctx.strokeStyle = '#000000'; 95 | ctx.lineWidth = 5; 96 | ctx.stroke(); 97 | 98 | // calculate iris position 99 | let irisX = centerX; 100 | let irisY = centerY; 101 | if (isFollowingMouse) { 102 | let dx = mouseX - centerX; 103 | let dy = mouseY - centerY; 104 | let distance = Math.sqrt(dx * dx + dy * dy); 105 | let maxDistance = currentEyeRadius - currentIrisRadius - 5; 106 | if (distance > maxDistance) { 107 | let ratio = maxDistance / distance; 108 | dx *= ratio; 109 | dy *= ratio; 110 | } 111 | irisX = centerX + dx; 112 | irisY = centerY + dy; 113 | } else if (isLookingAround) { 114 | let maxOffset = currentEyeRadius - currentIrisRadius - 10; 115 | irisX = centerX + Math.sin(lookAngle) * maxOffset; 116 | } else if (isIdle) { 117 | // slightly offset iris position for a natural gaze 118 | irisX = centerX + (currentEyeRadius - currentIrisRadius - 15) * 0.1; 119 | } 120 | 121 | // draw iris 122 | ctx.beginPath(); 123 | ctx.arc(irisX, irisY, currentIrisRadius, 0, Math.PI * 2); 124 | ctx.fillStyle = '#1E90FF'; // blue iris 125 | ctx.fill(); 126 | 127 | // draw pupil 128 | ctx.beginPath(); 129 | ctx.arc(irisX, irisY, currentPupilRadius, 0, Math.PI * 2); 130 | ctx.fillStyle = '#000000'; 131 | ctx.fill(); 132 | 133 | // add light reflection 134 | ctx.beginPath(); 135 | ctx.arc(irisX - currentPupilRadius / 2.5, irisY - currentPupilRadius / 2.5, currentPupilRadius / 3, 0, Math.PI * 2); 136 | ctx.fillStyle = '#FFFFFF'; 137 | ctx.fill(); 138 | 139 | // draw expressions 140 | if (isBlinking) { 141 | drawBlink(centerX, centerY, currentEyeRadius); 142 | } else if (isSleepy) { 143 | drawSleepyEyelid(centerX, centerY, currentEyeRadius); 144 | } else if (isBored) { 145 | drawBoredEyelid(centerX, centerY, currentEyeRadius); 146 | } else if (isWTF) { 147 | drawwtfEyebrows(centerX, centerY, currentEyeRadius); 148 | } 149 | 150 | ctx.restore(); 151 | } 152 | 153 | function drawBlink(centerX, centerY, radius) { 154 | let blinkAmount = blinkProgress / 100; 155 | 156 | ctx.save(); 157 | 158 | // create clipping region within the eye circle 159 | ctx.beginPath(); 160 | ctx.arc(centerX, centerY, radius, 0, Math.PI * 2); 161 | ctx.closePath(); 162 | ctx.clip(); 163 | 164 | // eyelid color (light gray) 165 | ctx.fillStyle = '#cccccc'; 166 | 167 | // upper eyelid moving down 168 | ctx.beginPath(); 169 | ctx.rect(centerX - radius, centerY - radius, radius * 2, radius * blinkAmount * 2); 170 | ctx.fill(); 171 | 172 | ctx.restore(); 173 | } 174 | 175 | function drawSleepyEyelid(centerX, centerY, radius) { 176 | ctx.save(); 177 | 178 | // create clipping region within the eye circle 179 | ctx.beginPath(); 180 | ctx.arc(centerX, centerY, radius, 0, Math.PI * 2); 181 | ctx.closePath(); 182 | ctx.clip(); 183 | 184 | // eyelid color (light gray) 185 | ctx.fillStyle = '#cccccc'; 186 | 187 | // sleepy upper eyelid 188 | ctx.beginPath(); 189 | ctx.moveTo(centerX - radius, centerY + radius / 3); 190 | ctx.quadraticCurveTo(centerX, centerY + radius / 2, centerX + radius, centerY + radius / 3); 191 | ctx.lineTo(centerX + radius, centerY + radius + 1); 192 | ctx.lineTo(centerX - radius, centerY + radius + 1); 193 | ctx.closePath(); 194 | ctx.fill(); 195 | 196 | ctx.restore(); 197 | } 198 | 199 | function drawBoredEyelid(centerX, centerY, radius) { 200 | ctx.save(); 201 | 202 | // create clipping region within the eye circle 203 | ctx.beginPath(); 204 | ctx.arc(centerX, centerY, radius, 0, Math.PI * 2); 205 | ctx.closePath(); 206 | ctx.clip(); 207 | 208 | // eyelid color (light gray) 209 | ctx.fillStyle = '#cccccc'; 210 | 211 | // bored upper eyelid 212 | ctx.beginPath(); 213 | ctx.moveTo(centerX - radius, centerY + radius / 5); 214 | ctx.quadraticCurveTo(centerX, centerY + radius / 4, centerX + radius, centerY + radius / 5); 215 | ctx.lineTo(centerX + radius, centerY + radius + 1); 216 | ctx.lineTo(centerX - radius, centerY + radius + 1); 217 | ctx.closePath(); 218 | ctx.fill(); 219 | 220 | ctx.restore(); 221 | } 222 | 223 | function drawwtfEyebrows(centerX, centerY, radius) { 224 | ctx.save(); 225 | 226 | // draw wtf eyebrow above the eye 227 | ctx.strokeStyle = '#000000'; 228 | ctx.lineWidth = 5; 229 | 230 | ctx.beginPath(); 231 | // eyebrow slanting down towards center for wtf expression 232 | ctx.moveTo(centerX - radius / 1.5, centerY - radius * 1.1); 233 | ctx.lineTo(centerX + radius / 1.5, centerY - radius * 0.9); 234 | ctx.stroke(); 235 | 236 | ctx.restore(); 237 | } 238 | 239 | function startBlink() { 240 | if (isBlinking) return; 241 | isBlinking = true; 242 | blinkProgress = 0; 243 | let closing = true; 244 | let blinkInterval = setInterval(() => { 245 | if (closing) { 246 | blinkProgress += 10; 247 | if (blinkProgress >= 100) { 248 | blinkProgress = 100; 249 | closing = false; 250 | } 251 | } else { 252 | blinkProgress -= 10; 253 | if (blinkProgress <= 0) { 254 | blinkProgress = 0; 255 | isBlinking = false; 256 | clearInterval(blinkInterval); 257 | } 258 | } 259 | }, 30); // adjusted speed for smoother blink 260 | } 261 | 262 | canvas.addEventListener('mousemove', function (event) { 263 | if (!isFollowingMouse) return; 264 | let rect = canvas.getBoundingClientRect(); 265 | mouseX = event.clientX - rect.left; 266 | mouseY = event.clientY - rect.top; 267 | }); 268 | 269 | function setMode(mode) { 270 | // reset all modes 271 | isFollowingMouse = false; 272 | isSurprised = false; 273 | isSleepy = false; 274 | isBored = false; 275 | isIdle = false; 276 | isLookingAround = false; 277 | isWTF = false; 278 | lookPause = false; 279 | 280 | clearInterval(idleBlinkInterval); 281 | 282 | switch (mode) { 283 | case 'idle': 284 | isIdle = true; 285 | idleBlink(); 286 | idleBlinkInterval = setInterval(idleBlink, 4000 + Math.random() * 2000); 287 | break; 288 | case 'followMouse': 289 | isFollowingMouse = true; 290 | break; 291 | case 'surprised': 292 | isSurprised = true; 293 | break; 294 | case 'sleepy': 295 | isSleepy = true; 296 | break; 297 | case 'bored': 298 | isBored = true; 299 | break; 300 | case 'wtf': 301 | isWTF = true; 302 | break; 303 | case 'lookAround': 304 | isLookingAround = true; 305 | lookAngle = 0; // reset angle 306 | break; 307 | default: 308 | break; 309 | } 310 | } 311 | 312 | function idleBlink() { 313 | startBlink(); 314 | } 315 | 316 | document.getElementById('idle').addEventListener('click', function () { 317 | setMode('idle'); 318 | }); 319 | 320 | document.getElementById('followMouse').addEventListener('click', function () { 321 | setMode('followMouse'); 322 | }); 323 | 324 | document.getElementById('blink').addEventListener('click', function () { 325 | startBlink(); 326 | }); 327 | 328 | document.getElementById('surprised').addEventListener('click', function () { 329 | setMode('surprised'); 330 | }); 331 | 332 | document.getElementById('sleepy').addEventListener('click', function () { 333 | setMode('sleepy'); 334 | }); 335 | 336 | document.getElementById('bored').addEventListener('click', function () { 337 | setMode('bored'); 338 | }); 339 | 340 | document.getElementById('wtf').addEventListener('click', function () { 341 | setMode('wtf'); 342 | }); 343 | 344 | document.getElementById('lookAround').addEventListener('click', function () { 345 | setMode('lookAround'); 346 | }); 347 | 348 | requestAnimationFrame(draw); 349 | setMode('idle'); 350 | -------------------------------------------------------------------------------- /resource/notion.css: -------------------------------------------------------------------------------- 1 | body { 2 | text-align: center; 3 | background-color: #ffffff; 4 | font-family: Arial, sans-serif; 5 | margin: 0; 6 | overflow: hidden; 7 | } 8 | 9 | #controls { 10 | position: absolute; 11 | bottom: 20px; 12 | width: 100%; 13 | text-align: center; 14 | z-index: 1; 15 | } 16 | 17 | #controls button { 18 | margin: 5px; 19 | padding: 10px 20px; 20 | font-size: 16px; 21 | background-color: #000000; 22 | color: #ffffff; 23 | border: none; 24 | cursor: pointer; 25 | border-radius: 5px; 26 | } 27 | 28 | #controls button:hover { 29 | background-color: #333333; 30 | } 31 | -------------------------------------------------------------------------------- /resource/notion.js: -------------------------------------------------------------------------------- 1 | /** 2 | * MIT License 3 | * Copyright (c) 2024 github.com/Barqawiz/ 4 | * **/ 5 | let notionAI; 6 | let sketchStarted = false; 7 | 8 | // Whisper font 9 | WebFont.load({ 10 | google: { 11 | families: ['Whisper'] 12 | }, 13 | active: function() { 14 | if (!sketchStarted) { 15 | new p5(sketch); 16 | sketchStarted = true; 17 | } 18 | } 19 | }); 20 | 21 | function sketch(p) { 22 | p.setup = function() { 23 | p.createCanvas(400, 400).parent(document.body); 24 | p.frameRate(24); 25 | notionAI = new NotionAI(p); 26 | }; 27 | 28 | p.draw = function() { 29 | p.background(255); 30 | notionAI.update(); 31 | notionAI.draw(); 32 | }; 33 | 34 | // expose setAction to the global scope 35 | window.setAction = function(action) { 36 | notionAI.setAction(action); 37 | }; 38 | 39 | class NotionAI { 40 | constructor(p) { 41 | this.p = p; 42 | this.x = p.width / 2; 43 | this.y = p.height / 2; 44 | this.scaleFactor = 2; 45 | this.frameCount = 0; 46 | this.blinkTimer = 0; 47 | this.blinkInterval = p.random(100, 200); 48 | this.currentAction = 'idle'; 49 | this.browWaveOffset = 0; 50 | this.browWaveDirection = 1; 51 | this.eyeBlink = false; 52 | this.typingText = ''; 53 | this.typingIndex = 0; 54 | this.typingTimer = 0; 55 | this.faceTilt = 0; 56 | this.errorTranslateY = 0; 57 | this.errorRotation = 0; 58 | } 59 | 60 | setAction(action) { 61 | this.currentAction = action; 62 | this.frameCount = 0; 63 | this.browWaveOffset = 0; 64 | this.blinkTimer = 0; 65 | this.eyeBlink = false; 66 | this.typingText = ''; 67 | this.typingIndex = 0; 68 | this.typingTimer = 0; 69 | this.faceTilt = 0; 70 | this.errorTranslateY = 0; 71 | this.errorRotation = 0; 72 | } 73 | 74 | update() { 75 | this.frameCount++; 76 | 77 | if (this.currentAction === 'idle') { 78 | this.blinkTimer++; 79 | if (this.blinkTimer > this.blinkInterval) { 80 | this.eyeBlink = true; 81 | if (this.blinkTimer > this.blinkInterval + 5) { 82 | this.eyeBlink = false; 83 | this.blinkTimer = 0; 84 | this.blinkInterval = this.p.random(100, 200); 85 | } 86 | } 87 | } 88 | 89 | if (this.currentAction === 'thinking') { 90 | this.browWaveOffset += 0.1 * this.browWaveDirection; 91 | if (this.browWaveOffset > this.p.PI / 4 || this.browWaveOffset < -this.p.PI / 4) { 92 | this.browWaveDirection *= -1; 93 | } 94 | } 95 | 96 | if (this.currentAction === 'typing') { 97 | this.typingTimer++; 98 | if (this.typingTimer % 5 === 0 && this.typingIndex < 10) { 99 | this.typingText += String.fromCharCode(97 + Math.floor(this.p.random(0, 26))); 100 | this.typingIndex++; 101 | } 102 | if (this.typingIndex >= 10) { 103 | this.typingText = ''; 104 | this.typingIndex = 0; 105 | } 106 | } 107 | 108 | if (this.currentAction === 'uncertain') { 109 | if (this.frameCount < 30) { 110 | this.faceTilt = this.p.map(this.frameCount, 0, 30, 0, -0.2); 111 | } else { 112 | this.faceTilt = -0.2; 113 | } 114 | } else { 115 | this.faceTilt = 0; 116 | } 117 | 118 | if (this.currentAction === 'error') { 119 | if (this.frameCount < 30) { 120 | this.errorTranslateY = this.frameCount; 121 | this.errorRotation = this.p.radians(this.frameCount * 5); 122 | } else if (this.frameCount < 60) { 123 | this.errorTranslateY = 60 - this.frameCount; 124 | this.errorRotation = this.p.radians((60 - this.frameCount) * 5); 125 | } else { 126 | this.errorTranslateY = 0; 127 | this.errorRotation = 0; 128 | this.setAction('idle'); 129 | } 130 | } else { 131 | this.errorTranslateY = 0; 132 | this.errorRotation = 0; 133 | } 134 | } 135 | 136 | draw() { 137 | this.p.push(); 138 | this.p.translate(this.x, this.y); 139 | this.p.scale(this.scaleFactor); 140 | this.p.rotate(this.faceTilt); 141 | 142 | this.p.translate(0, this.errorTranslateY); 143 | this.p.rotate(this.errorRotation); 144 | 145 | this.p.stroke(0); 146 | this.p.strokeWeight(2); 147 | this.p.noFill(); 148 | 149 | this.drawNose(); 150 | this.drawEyes(); 151 | this.drawEyebrows(); 152 | this.drawMouth(); 153 | 154 | if (this.currentAction === 'typing') { 155 | this.drawTypingAnimation(); 156 | } 157 | 158 | this.p.pop(); 159 | } 160 | 161 | drawNose() { 162 | this.p.push(); 163 | this.p.strokeWeight(4); 164 | this.p.beginShape(); 165 | this.p.vertex(0, -10); 166 | this.p.quadraticVertex(10, -5, 0, 10); 167 | this.p.endShape(); 168 | this.p.pop(); 169 | } 170 | 171 | drawEyes() { 172 | this.p.fill(0); 173 | this.p.noStroke(); 174 | let eyeOffsetX = 20; 175 | let eyeOffsetY = -15; 176 | 177 | if (this.eyeBlink) { 178 | this.p.rect(-eyeOffsetX - 5, eyeOffsetY, 10, 2); 179 | this.p.rect(eyeOffsetX - 5, eyeOffsetY, 10, 2); 180 | } else { 181 | this.p.ellipse(-eyeOffsetX, eyeOffsetY, 5, 5); 182 | this.p.ellipse(eyeOffsetX, eyeOffsetY, 5, 5); 183 | } 184 | } 185 | 186 | drawEyebrows() { 187 | this.p.stroke(0); 188 | this.p.strokeWeight(2); 189 | let browOffsetX = 18; 190 | let browOffsetY = -25; 191 | 192 | if (this.currentAction === 'thinking') { 193 | let offset = this.p.sin(this.frameCount * 0.2) * 5; 194 | this.drawCurvedEyebrow(-browOffsetX, browOffsetY + offset, -10, -35 + offset); 195 | this.drawCurvedEyebrow(browOffsetX, browOffsetY - offset, 10, -35 - offset); 196 | } else if (this.currentAction === 'curious') { 197 | this.drawCurvedEyebrow(-browOffsetX, browOffsetY - 5, -10, browOffsetY - 15); 198 | this.drawCurvedEyebrow(browOffsetX, browOffsetY + 5, 10, browOffsetY + 5); 199 | } else if (this.currentAction === 'surprised') { 200 | this.drawCurvedEyebrow(-browOffsetX, browOffsetY - 10, -10, browOffsetY - 10); 201 | this.drawCurvedEyebrow(browOffsetX, browOffsetY - 10, 10, browOffsetY - 10); 202 | } else if (this.currentAction === 'uncertain') { 203 | this.drawCurvedEyebrow(-browOffsetX, browOffsetY + 5, -10, browOffsetY); 204 | this.drawCurvedEyebrow(browOffsetX, browOffsetY + 5, 10, browOffsetY); 205 | } else if (this.currentAction === 'confident') { 206 | this.drawCurvedEyebrow(-browOffsetX, browOffsetY - 5, -10, browOffsetY - 15); 207 | this.drawCurvedEyebrow(browOffsetX, browOffsetY - 5, 10, browOffsetY - 15); 208 | } else if (this.currentAction === 'excited') { 209 | this.drawCurvedEyebrow(-browOffsetX, browOffsetY - 15, -10, browOffsetY - 25); 210 | this.drawCurvedEyebrow(browOffsetX, browOffsetY - 15, 10, browOffsetY - 25); 211 | } else { 212 | this.drawCurvedEyebrow(-browOffsetX, browOffsetY, -10, browOffsetY); 213 | this.drawCurvedEyebrow(browOffsetX, browOffsetY, 10, browOffsetY); 214 | } 215 | } 216 | 217 | drawCurvedEyebrow(startX, startY, endX, endY) { 218 | this.p.beginShape(); 219 | this.p.curveVertex(startX - 5, startY); 220 | this.p.curveVertex(startX, startY); 221 | this.p.curveVertex(endX, endY); 222 | this.p.curveVertex(endX + 5, endY); 223 | this.p.endShape(); 224 | } 225 | 226 | drawMouth() { 227 | this.p.noFill(); 228 | this.p.stroke(0); 229 | this.p.strokeWeight(2); 230 | 231 | if (this.currentAction === 'satisfaction') { 232 | this.p.arc(0, 20, 20, 10, 0, this.p.PI); 233 | } else if (this.currentAction === 'surprised') { 234 | this.p.ellipse(0, 20, 5, 5); 235 | } else if (this.currentAction === 'uncertain') { 236 | this.p.arc(0, 25, 20, 10, this.p.PI, this.p.TWO_PI); 237 | } else if (this.currentAction === 'confident') { 238 | // No mouth 239 | } else if (this.currentAction === 'excited') { 240 | this.p.arc(0, 20, 20, 15, 0, this.p.PI); 241 | } else { 242 | // No mouth for other states 243 | } 244 | } 245 | 246 | drawTypingAnimation() { 247 | this.p.push(); 248 | this.p.translate(0, 30); // reduced vertical offset 249 | this.p.fill(0); 250 | this.p.noStroke(); 251 | this.p.textSize(16); // increased font size 252 | this.p.textAlign(this.p.CENTER); 253 | 254 | this.p.textFont('Whisper'); 255 | 256 | this.p.text(this.typingText + '_', 0, 0); 257 | this.p.pop(); 258 | } 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /resource/plant.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | background-color: #ffffff; /* start with day background */ 5 | color: #000000; 6 | font-family: Arial, sans-serif; 7 | transition: background-color 0.5s, color 0.5s; 8 | } 9 | 10 | #container { 11 | text-align: center; 12 | padding-top: 50px; 13 | } 14 | 15 | #plantCanvas { 16 | background-color: #87CEEB; /* sky blue for day */ 17 | border: 2px solid #000; 18 | border-radius: 10px; 19 | } 20 | 21 | #controls { 22 | margin-top: 20px; 23 | } 24 | 25 | button { 26 | background-color: #ff66cc; /* cute button color */ 27 | border: none; 28 | color: white; 29 | padding: 10px 15px; 30 | margin: 5px; 31 | border-radius: 10px; 32 | font-size: 14px; 33 | cursor: pointer; 34 | transition: background-color 0.3s; 35 | } 36 | 37 | button:hover { 38 | background-color: #ff85d5; 39 | } 40 | 41 | #message { 42 | margin-top: 20px; 43 | font-size: 18px; 44 | } 45 | 46 | #textDisplay { 47 | margin-top: 20px; 48 | font-size: 18px; 49 | color: #ffcc00; /* yellowish color for text */ 50 | } 51 | -------------------------------------------------------------------------------- /resource/plant.js: -------------------------------------------------------------------------------- 1 | /** 2 | * MIT License 3 | * Copyright (c) 2024 github.com/Barqawiz/ 4 | * **/ 5 | const canvas = document.getElementById('plantCanvas'); 6 | const ctx = canvas.getContext('2d'); 7 | 8 | let isDayMode = true; // start with day mode 9 | let growthStage = 1; // 0: seed, 1: seedling, 2: growing, 3: mature 10 | let needsWater = false; 11 | let needsSun = false; 12 | 13 | let textTimeout; 14 | let animationAngle = 0; 15 | 16 | function drawPlant() { 17 | ctx.clearRect(0, 0, canvas.width, canvas.height); 18 | 19 | // draw background 20 | if (isDayMode) { 21 | drawDayBackground(); 22 | } else { 23 | drawNightBackground(); 24 | } 25 | 26 | // save the context 27 | ctx.save(); 28 | // move to the bottom center of the canvas 29 | ctx.translate(canvas.width / 2, canvas.height); 30 | 31 | // apply idle animation sway 32 | ctx.rotate(Math.sin(animationAngle) * 0.05); // sway angle 33 | 34 | // draw plant based on growth stage 35 | if (growthStage >= 0) { 36 | // seed stage 37 | drawSeed(); 38 | } 39 | 40 | if (growthStage >= 1) { 41 | // seedling stage 42 | drawStem(1); 43 | drawLeaves(1); 44 | } 45 | 46 | if (growthStage >= 2) { 47 | // growing stage 48 | drawStem(2); // taller stem 49 | drawLeaves(2); // more leaves 50 | } 51 | 52 | if (growthStage >= 3) { 53 | // mature stage 54 | drawStem(3); 55 | drawLeaves(3); 56 | drawFlower(); 57 | } 58 | 59 | ctx.restore(); 60 | 61 | // display needs 62 | if (needsWater && needsSun) { 63 | document.getElementById('message').innerText = 'I need water and sun!'; 64 | } else if (needsWater) { 65 | document.getElementById('message').innerText = 'I need water!'; 66 | // draw "I need water" text next to plant 67 | ctx.save(); 68 | ctx.translate(canvas.width / 2 + 50, canvas.height - 100); 69 | ctx.fillStyle = '#0000ff'; 70 | ctx.font = '16px Arial'; 71 | ctx.fillText('I need water!', 0, 0); 72 | ctx.restore(); 73 | } else if (needsSun) { 74 | document.getElementById('message').innerText = 'I need sun!'; 75 | // draw "I need sun" text next to plant 76 | ctx.save(); 77 | ctx.translate(canvas.width / 2 + 50, canvas.height - 130); 78 | ctx.fillStyle = '#FFA500'; 79 | ctx.font = '16px Arial'; 80 | ctx.fillText('I need sun!', 0, 0); 81 | ctx.restore(); 82 | 83 | // draw cloud over sun 84 | if (isDayMode) { 85 | drawCloud(30, 50); // adjusted position 86 | } 87 | } else { 88 | document.getElementById('message').innerText = ''; 89 | } 90 | } 91 | 92 | function drawSeed() { 93 | ctx.fillStyle = '#8B4513'; // brown color for seed 94 | ctx.beginPath(); 95 | ctx.ellipse(0, 0, 10, 10, 0, 0, Math.PI * 2); 96 | ctx.fill(); 97 | } 98 | 99 | function drawStem(stage) { 100 | ctx.strokeStyle = '#228B22'; // forestGreen color 101 | ctx.lineWidth = 5; 102 | 103 | ctx.beginPath(); 104 | ctx.moveTo(0, 0); 105 | 106 | if (stage === 1) { 107 | // short stem 108 | ctx.bezierCurveTo(0, -20, 10, -40, 0, -60); 109 | } else if (stage === 2) { 110 | // medium stem 111 | ctx.bezierCurveTo(0, -50, 20, -100, 0, -150); 112 | } else if (stage === 3) { 113 | // tall stem 114 | ctx.bezierCurveTo(0, -70, 30, -140, 0, -210); 115 | } 116 | 117 | ctx.stroke(); 118 | } 119 | 120 | function drawLeaves(stage) { 121 | if (stage >= 1) { 122 | // left leaf 123 | drawLeaf(ctx, -15, -60, 1, -Math.PI / 4); 124 | // right leaf 125 | drawLeaf(ctx, 15, -80, 1, Math.PI / 4); 126 | } 127 | if (stage >= 2) { 128 | // additional leaves 129 | drawLeaf(ctx, -20, -110, 0.8, -Math.PI / 3); 130 | drawLeaf(ctx, 20, -130, 0.8, Math.PI / 3); 131 | } 132 | if (stage >= 3) { 133 | // more leaves 134 | drawLeaf(ctx, -25, -160, 0.6, -Math.PI / 2.5); 135 | drawLeaf(ctx, 25, -180, 0.6, Math.PI / 2.5); 136 | } 137 | } 138 | 139 | function drawLeaf(ctx, x, y, scale, angle) { 140 | ctx.save(); 141 | ctx.translate(x, y); 142 | ctx.rotate(angle); 143 | ctx.scale(scale, scale); 144 | ctx.fillStyle = '#32CD32'; // limeGreen color 145 | ctx.beginPath(); 146 | ctx.moveTo(0, 0); 147 | ctx.bezierCurveTo(15, -10, 15, -30, 0, -40); 148 | ctx.bezierCurveTo(-15, -30, -15, -10, 0, 0); 149 | ctx.fill(); 150 | ctx.restore(); 151 | } 152 | 153 | function drawFlower() { 154 | ctx.save(); 155 | ctx.translate(0, -210); // move to the top of the stem 156 | // draw petals 157 | for (let i = 0; i < 5; i++) { 158 | ctx.fillStyle = '#FF69B4'; // hot pink 159 | ctx.beginPath(); 160 | ctx.rotate((Math.PI * 2) / 5); 161 | ctx.moveTo(0, 0); 162 | ctx.lineTo(0, -20); 163 | ctx.arc(0, -30, 10, 0, Math.PI); 164 | ctx.lineTo(0, -20); 165 | ctx.fill(); 166 | } 167 | // draw center of the flower 168 | ctx.fillStyle = '#FFD700'; // gold 169 | ctx.beginPath(); 170 | ctx.arc(0, 0, 7, 0, Math.PI * 2); 171 | ctx.fill(); 172 | ctx.restore(); 173 | } 174 | 175 | function drawDayBackground() { 176 | // sky blue background 177 | ctx.fillStyle = '#87CEEB'; 178 | ctx.fillRect(0, 0, canvas.width, canvas.height); 179 | 180 | // draw sun 181 | ctx.fillStyle = '#FFD700'; 182 | ctx.beginPath(); 183 | ctx.arc(50, 50, 30, 0, Math.PI * 2); 184 | ctx.fill(); 185 | 186 | // if needs sun, draw cloud over part of sun 187 | if (needsSun) { 188 | drawCloud(30, 50); 189 | } 190 | } 191 | 192 | function drawCloud(x, y) { 193 | ctx.fillStyle = '#ffffff'; 194 | ctx.beginPath(); 195 | ctx.arc(x, y, 20, Math.PI * 0.5, Math.PI * 1.5); 196 | ctx.arc(x + 20, y - 20, 20, Math.PI, Math.PI * 2); 197 | ctx.arc(x + 40, y - 20, 20, Math.PI, Math.PI * 2); 198 | ctx.arc(x + 60, y, 20, Math.PI * 1.5, Math.PI * 0.5); 199 | ctx.moveTo(x + 60, y + 20); 200 | ctx.lineTo(x, y + 20); 201 | ctx.fill(); 202 | } 203 | 204 | function drawNightBackground() { 205 | ctx.fillStyle = '#0a0a2a'; // dark blue 206 | ctx.fillRect(0, 0, canvas.width, canvas.height); 207 | 208 | // draw stars 209 | for (let i = 0; i < 50; i++) { 210 | ctx.fillStyle = '#ffffff'; 211 | ctx.beginPath(); 212 | const x = Math.random() * canvas.width; 213 | const y = Math.random() * canvas.height; 214 | ctx.arc(x, y, 1, 0, Math.PI * 2); 215 | ctx.fill(); 216 | } 217 | } 218 | 219 | function toggleDayNightMode() { 220 | isDayMode = !isDayMode; 221 | 222 | // change the background color of the body 223 | if (isDayMode) { 224 | document.body.style.backgroundColor = '#ffffff'; 225 | document.body.style.color = '#000000'; 226 | document.getElementById('plantCanvas').style.backgroundColor = '#87CEEB'; 227 | document.getElementById('plantCanvas').style.borderColor = '#000'; 228 | } else { 229 | document.body.style.backgroundColor = '#0a0a2a'; 230 | document.body.style.color = '#ffffff'; 231 | document.getElementById('plantCanvas').style.backgroundColor = '#1a1a3d'; 232 | document.getElementById('plantCanvas').style.borderColor = '#fff'; 233 | } 234 | 235 | drawPlant(); 236 | } 237 | 238 | function waterPlant() { 239 | // keep the animation as it with drops 240 | playWateringAnimation(); 241 | 242 | needsWater = false; 243 | drawPlant(); 244 | } 245 | 246 | function giveSun() { 247 | if (needsSun) { 248 | needsSun = false; 249 | // play sun animation 250 | playSunAnimation(); 251 | } else { 252 | playSunAnimation(); 253 | } 254 | } 255 | 256 | function changeGrowthStage() { 257 | growthStage = (growthStage + 1) % 4; // now 4 stages 258 | drawPlant(); 259 | } 260 | 261 | function showRandomText() { 262 | const messages = [ 263 | "Plants make people happy!", 264 | "Don't forget to water me!", 265 | "I'm growing strong!", 266 | "I love sunlight!", 267 | "Thank you for taking care of me!" 268 | ]; 269 | const randomMessage = messages[Math.floor(Math.random() * messages.length)]; 270 | // display the message in the textDisplay div 271 | document.getElementById('textDisplay').innerText = randomMessage; 272 | 273 | // hide the text after some time 274 | clearTimeout(textTimeout); 275 | textTimeout = setTimeout(() => { 276 | document.getElementById('textDisplay').innerText = ''; 277 | }, 5000); // hide after 5 seconds 278 | } 279 | 280 | function playWateringAnimation() { 281 | let dropletY = -150; 282 | function animateDroplet() { 283 | drawPlant(); 284 | ctx.save(); 285 | ctx.translate(canvas.width / 2, dropletY); 286 | ctx.fillStyle = '#0000ff'; 287 | ctx.beginPath(); 288 | ctx.arc(0, 0, 5, 0, Math.PI * 2); 289 | ctx.fill(); 290 | ctx.restore(); 291 | dropletY += 5; 292 | if (dropletY <= canvas.height) { 293 | requestAnimationFrame(animateDroplet); 294 | } else { 295 | drawPlant(); 296 | } 297 | } 298 | animateDroplet(); 299 | } 300 | 301 | function playSunAnimation() { 302 | // show the cloud moving away 303 | let cloudX = 30; 304 | function animateSun() { 305 | drawPlant(); 306 | ctx.save(); 307 | ctx.translate(cloudX, 50); 308 | ctx.fillStyle = '#ffffff'; 309 | ctx.beginPath(); 310 | ctx.arc(0, 0, 20, Math.PI * 0.5, Math.PI * 1.5); 311 | ctx.arc(20, -20, 20, Math.PI, Math.PI * 2); 312 | ctx.arc(40, -20, 20, Math.PI, Math.PI * 2); 313 | ctx.arc(60, 0, 20, Math.PI * 1.5, Math.PI * 0.5); 314 | ctx.moveTo(60, 20); 315 | ctx.lineTo(0, 20); 316 | ctx.fill(); 317 | ctx.restore(); 318 | 319 | cloudX += 2; 320 | if (cloudX < canvas.width) { 321 | requestAnimationFrame(animateSun); 322 | } else { 323 | drawPlant(); 324 | } 325 | } 326 | animateSun(); 327 | } 328 | 329 | function showCuteReaction() { 330 | let heartY = canvas.height / 2; 331 | function animateHeart() { 332 | drawPlant(); 333 | ctx.save(); 334 | ctx.translate(canvas.width / 2, heartY); 335 | ctx.fillStyle = '#ff69b4'; // hot pink color 336 | ctx.beginPath(); 337 | ctx.moveTo(0, -10); 338 | ctx.bezierCurveTo(-25, -35, -50, 0, 0, 30); 339 | ctx.bezierCurveTo(50, 0, 25, -35, 0, -10); 340 | ctx.fill(); 341 | ctx.restore(); 342 | heartY -= 5; 343 | if (heartY > 0) { 344 | requestAnimationFrame(animateHeart); 345 | } else { 346 | drawPlant(); 347 | } 348 | } 349 | animateHeart(); 350 | } 351 | 352 | function fertilizePlant() { 353 | // improved fertilize animation without bag moving down 354 | let particles = []; 355 | // start sprinkling particles 356 | for (let i = 0; i < 20; i++) { 357 | particles.push({ 358 | x: canvas.width / 2 - 10 + Math.random() * 20, 359 | y: canvas.height / 2, 360 | vy: 2 + Math.random() * 2 361 | }); 362 | } 363 | function animateParticles() { 364 | drawPlant(); 365 | 366 | // draw particles 367 | ctx.fillStyle = '#8B4513'; // brown color for particles 368 | particles.forEach((p, index) => { 369 | ctx.beginPath(); 370 | ctx.arc(p.x, p.y, 2, 0, Math.PI * 2); 371 | ctx.fill(); 372 | p.y += p.vy; 373 | if (p.y > canvas.height) { 374 | particles.splice(index, 1); 375 | } 376 | }); 377 | 378 | if (particles.length > 0) { 379 | requestAnimationFrame(animateParticles); 380 | } else { 381 | drawPlant(); 382 | } 383 | } 384 | animateParticles(); 385 | } 386 | 387 | function playMusic() { 388 | // show moving musical notes 389 | let notes = []; 390 | for (let i = 0; i < 5; i++) { 391 | notes.push({ 392 | x: canvas.width / 2 + Math.random() * 100 - 50, 393 | y: canvas.height - 100, 394 | vy: -2 - Math.random() * 2 395 | }); 396 | } 397 | function animateNotes() { 398 | drawPlant(); 399 | ctx.save(); 400 | // set music note color based on day/night mode 401 | if (isDayMode) { 402 | ctx.fillStyle = '#000000'; // black in day 403 | } else { 404 | ctx.fillStyle = '#ffffff'; // white in night 405 | } 406 | notes.forEach(note => { 407 | ctx.font = '20px Arial'; 408 | ctx.fillText('♪', note.x, note.y); 409 | note.y += note.vy; 410 | }); 411 | ctx.restore(); 412 | if (notes[0].y > 0) { 413 | requestAnimationFrame(animateNotes); 414 | } else { 415 | drawPlant(); 416 | } 417 | } 418 | animateNotes(); 419 | } 420 | 421 | function talkToPlant() { 422 | let opacity = 1; 423 | function animateSpeechBubble() { 424 | drawPlant(); 425 | ctx.save(); 426 | ctx.globalAlpha = opacity; 427 | ctx.fillStyle = '#ffffff'; 428 | ctx.fillRect(canvas.width / 2 - 60, canvas.height / 2 - 150, 120, 50); 429 | ctx.fillStyle = '#000000'; 430 | ctx.font = '14px Arial'; 431 | ctx.fillText('Hello, friend!', canvas.width / 2 - 50, canvas.height / 2 - 120); 432 | ctx.restore(); 433 | opacity -= 0.02; 434 | if (opacity > 0) { 435 | requestAnimationFrame(animateSpeechBubble); 436 | } else { 437 | drawPlant(); 438 | } 439 | } 440 | animateSpeechBubble(); 441 | } 442 | 443 | function showButterfly() { 444 | // show butterfly animation 445 | let butterflyX = 0; 446 | let butterflyY = canvas.height / 2; 447 | 448 | function animateButterfly() { 449 | drawPlant(); 450 | ctx.save(); 451 | ctx.translate(butterflyX, butterflyY); 452 | ctx.fillStyle = '#ff00ff'; // magenta color for butterfly 453 | // draw simple butterfly wings 454 | ctx.beginPath(); 455 | ctx.ellipse(0, 0, 10, 5, 0, 0, Math.PI * 2); 456 | ctx.fill(); 457 | ctx.beginPath(); 458 | ctx.ellipse(15, 0, 10, 5, 0, 0, Math.PI * 2); 459 | ctx.fill(); 460 | // draw body 461 | ctx.fillStyle = '#000000'; 462 | ctx.fillRect(5, -5, 5, 10); 463 | ctx.restore(); 464 | 465 | butterflyX += 5; 466 | butterflyY += Math.sin(butterflyX / 20) * 5; 467 | 468 | if (butterflyX < canvas.width) { 469 | requestAnimationFrame(animateButterfly); 470 | } else { 471 | drawPlant(); 472 | } 473 | } 474 | animateButterfly(); 475 | } 476 | 477 | function sleepingPlant() { 478 | // show animated Z's next to the plant 479 | let zY = canvas.height - 150; 480 | function animateZ() { 481 | drawPlant(); 482 | ctx.save(); 483 | ctx.fillStyle = '#0000ff'; 484 | ctx.font = '30px Arial'; 485 | ctx.fillText('Z', canvas.width / 2 + 20, zY); 486 | ctx.restore(); 487 | zY -= 2; 488 | if (zY > 0) { 489 | requestAnimationFrame(animateZ); 490 | } else { 491 | drawPlant(); 492 | } 493 | } 494 | animateZ(); 495 | } 496 | 497 | function showNeedWater() { 498 | needsWater = true; 499 | drawPlant(); 500 | } 501 | 502 | function showNeedSun() { 503 | needsSun = true; 504 | drawPlant(); 505 | } 506 | 507 | function resetToIdeal() { 508 | // clear any messages or flags 509 | document.getElementById('message').innerText = ''; 510 | document.getElementById('textDisplay').innerText = ''; 511 | needsWater = false; 512 | needsSun = false; 513 | drawPlant(); 514 | } 515 | 516 | function animate() { 517 | animationAngle += 0.02; // increase the angle 518 | drawPlant(); 519 | requestAnimationFrame(animate); 520 | } 521 | 522 | // event listeners for buttons 523 | document.getElementById('dayNightMode').addEventListener('click', toggleDayNightMode); 524 | document.getElementById('growthStage').addEventListener('click', changeGrowthStage); 525 | document.getElementById('waterPlant').addEventListener('click', waterPlant); 526 | document.getElementById('giveSun').addEventListener('click', giveSun); 527 | document.getElementById('fertilizePlant').addEventListener('click', fertilizePlant); 528 | document.getElementById('playMusic').addEventListener('click', playMusic); 529 | document.getElementById('talkPlant').addEventListener('click', talkToPlant); 530 | document.getElementById('showText').addEventListener('click', showRandomText); 531 | document.getElementById('cuteButton').addEventListener('click', showCuteReaction); 532 | document.getElementById('showButterfly').addEventListener('click', showButterfly); 533 | document.getElementById('sleeping').addEventListener('click', sleepingPlant); 534 | document.getElementById('showNeedWater').addEventListener('click', showNeedWater); 535 | document.getElementById('showNeedSun').addEventListener('click', showNeedSun); 536 | document.getElementById('idealButton').addEventListener('click', resetToIdeal); 537 | 538 | // initial draw and start animation 539 | animate(); 540 | --------------------------------------------------------------------------------