├── .vscode ├── arduino.json ├── extensions.json └── settings.json ├── CPPLINT.cfg ├── docs ├── image │ └── avatar.gif └── FaceCustomizationGuideline.md ├── examples ├── audio │ ├── data │ │ └── nyaan.mp3 │ └── audio.ino ├── set-position │ └── set-position.ino ├── basics │ └── basics.ino ├── transform │ └── transform.ino ├── PlatformIO_SDL │ ├── src │ │ ├── sdl_main.cpp │ │ └── main.cpp │ └── platformio.ini ├── balloon │ └── balloon.ino ├── talk │ └── talk.ino ├── wink-demo │ └── wink-demo.ino └── face-and-color │ └── face-and-color.ino ├── .editorconfig ├── CONTRIBUTING.md ├── PULL_REQUEST_TEMPLATE.md ├── component.mk ├── src ├── Accessory.h ├── Expression.h ├── Gaze.cpp ├── Gaze.h ├── Drawable.h ├── tasks │ └── LipSync.h ├── Eyeblow.h ├── faces │ ├── OledFace.h │ ├── eye_small.h │ ├── BMPFace.h │ ├── DogFace.h │ └── FaceTemplates.hpp ├── Mouth.h ├── Eye.h ├── DrawingUtils.hpp ├── BoundingRect.h ├── ColorPalette.cpp ├── Mouth.cpp ├── ColorPalette.h ├── BoundingRect.cpp ├── Eyeblow.cpp ├── Eyebrows.hpp ├── Mouths.hpp ├── Eyes.hpp ├── BatteryIcon.h ├── Face.h ├── Balloon.h ├── Eye.cpp ├── Eyebrows.cpp ├── DrawingUtils.cpp ├── DrawContext.h ├── DrawContext.cpp ├── Avatar.h ├── Effect.h ├── Mouths.cpp ├── Face.cpp ├── Avatar.cpp └── Eyes.cpp ├── library.properties ├── .github ├── workflows │ └── doxygen.yml └── ISSUE_TEMPLATE │ └── bug_report.md ├── library.json ├── lib ├── readme.txt └── AquesTalkTTS │ ├── AquesTalkTTS.h │ └── AquesTalkTTS.cpp ├── LICENSE.txt ├── DESIGN.md ├── .travis.yml ├── platformio.ini ├── .gitignore ├── CODE_OF_CONDUCT.md ├── README_ja.md └── README.md /.vscode/arduino.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": "/dev/ttyUSB0" 3 | } -------------------------------------------------------------------------------- /CPPLINT.cfg: -------------------------------------------------------------------------------- 1 | set noparent 2 | linelength=80 3 | root=src 4 | -------------------------------------------------------------------------------- /docs/image/avatar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stack-chan/m5stack-avatar/HEAD/docs/image/avatar.gif -------------------------------------------------------------------------------- /examples/audio/data/nyaan.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stack-chan/m5stack-avatar/HEAD/examples/audio/data/nyaan.mp3 -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = false 9 | insert_final_newline = false -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | * No much rules because this is so personal project. 2 | * You should pass the CI process that runs when your PR is made. It builds all the example using the library. 3 | * Take it easy and stay healthy. Thank you very much. 4 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | ## Type of change 4 | 5 | Please delete options that are not relevant. 6 | 7 | - [ ] Bug fix 8 | - [ ] Feature Enhancement 9 | - [ ] Breaking Change 10 | - [ ] Needs document update 11 | -------------------------------------------------------------------------------- /component.mk: -------------------------------------------------------------------------------- 1 | # 2 | # Main Makefile. This is basically the same as a component makefile. 3 | # 4 | # (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.) 5 | 6 | COMPONENT_SRCDIRS := src src/tasks src/faces 7 | COMPONENT_ADD_INCLUDEDIRS := src 8 | -------------------------------------------------------------------------------- /examples/set-position/set-position.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | using namespace m5avatar; 5 | 6 | Avatar avatar; 7 | 8 | void setup() 9 | { 10 | M5.begin(); 11 | avatar.setPosition(50, 50); 12 | avatar.init(); // start drawing 13 | } 14 | 15 | void loop() 16 | { 17 | } 18 | -------------------------------------------------------------------------------- /src/Accessory.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Shinya Ishikawa. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full 3 | // license information. 4 | 5 | #ifndef ACCESSORY_H_ 6 | #define ACCESSORY_H_ 7 | 8 | // TODO(meganetaaan): Accessory 9 | 10 | #endif // ACCESSORY_H_ 11 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "platformio.platformio-ide" 6 | ], 7 | "unwantedRecommendations": [ 8 | "ms-vscode.cpptools-extension-pack" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /examples/basics/basics.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | using namespace m5avatar; 5 | 6 | Avatar avatar; 7 | 8 | void setup() 9 | { 10 | M5.begin(); 11 | avatar.init(); // start drawing 12 | } 13 | 14 | void loop() 15 | { 16 | // avatar's face updates in another thread 17 | // so no need to loop-by-loop rendering 18 | } 19 | -------------------------------------------------------------------------------- /src/Expression.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Shinya Ishikawa. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full 3 | // license information. 4 | 5 | #ifndef EXPRESSION_H_ 6 | #define EXPRESSION_H_ 7 | 8 | namespace m5avatar { 9 | enum class Expression { Happy, Angry, Sad, Doubt, Sleepy, Neutral }; 10 | } 11 | 12 | #endif // EXPRESSION_H_ 13 | -------------------------------------------------------------------------------- /library.properties: -------------------------------------------------------------------------------- 1 | name=M5Stack_Avatar 2 | version=0.10.0 3 | author=Shinya Ishikawa 4 | maintainer=Shinya Ishikawa 5 | sentence=Yet another avatar module for M5Stack 6 | paragraph=See more on http://M5Stack.com 7 | category=Device Control 8 | url=https://platformio.org/lib/show/4529/M5Stack-Avatar 9 | architectures=esp32 10 | includes=Avatar.h 11 | depends=M5Unified 12 | -------------------------------------------------------------------------------- /src/Gaze.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Shinya Ishikawa. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full 3 | // license information. 4 | 5 | #include "Gaze.h" 6 | 7 | namespace m5avatar { 8 | Gaze::Gaze() : v{0}, h{0} {} 9 | 10 | Gaze::Gaze(float v, float h) : v{v}, h{h} {} 11 | 12 | float Gaze::getVertical() const { return v; } 13 | 14 | float Gaze::getHorizontal() const { return h; } 15 | } // namespace m5avatar 16 | -------------------------------------------------------------------------------- /examples/transform/transform.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | using namespace m5avatar; 5 | 6 | Avatar avatar; 7 | int degrees = 0; 8 | 9 | void setup() 10 | { 11 | M5.begin(); 12 | avatar.init(); 13 | } 14 | 15 | void loop() 16 | { 17 | M5.update(); 18 | degrees = (degrees + 1) % 360; 19 | float radian = degrees / 180.0 * PI; 20 | float scale = sin(radian) / 2.0 + 1.0; 21 | avatar.setRotation(radian); 22 | avatar.setScale(scale); 23 | delay(33); 24 | } 25 | -------------------------------------------------------------------------------- /examples/PlatformIO_SDL/src/sdl_main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #if defined ( SDL_h_ ) 3 | 4 | void setup(void); 5 | void loop(void); 6 | 7 | __attribute__((weak)) 8 | int user_func(bool* running) 9 | { 10 | setup(); 11 | do 12 | { 13 | loop(); 14 | } while (*running); 15 | return 0; 16 | } 17 | 18 | int main(int, char**) 19 | { 20 | // The second argument is effective for step execution with breakpoints. 21 | // You can specify the time in milliseconds to perform slow execution that ensures screen updates. 22 | return lgfx::Panel_sdl::main(user_func, 128); 23 | } 24 | 25 | #endif 26 | -------------------------------------------------------------------------------- /src/Gaze.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Shinya Ishikawa. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full 3 | // license information. 4 | 5 | #ifndef GAZE_H_ 6 | #define GAZE_H_ 7 | 8 | namespace m5avatar { 9 | class Gaze { 10 | private: 11 | float v; 12 | float h; 13 | 14 | public: 15 | Gaze(); 16 | Gaze(float v, float h); 17 | ~Gaze() = default; 18 | Gaze(const Gaze &other) = default; 19 | Gaze &operator=(const Gaze &other) = default; 20 | float getVertical() const; 21 | float getHorizontal() const; 22 | }; 23 | } // namespace m5avatar 24 | 25 | #endif // GAZE_H_ 26 | -------------------------------------------------------------------------------- /src/Drawable.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Shinya Ishikawa. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full 3 | // license information. 4 | 5 | #ifndef DRAWABLE_H_ 6 | #define DRAWABLE_H_ 7 | #define LGFX_USE_V1 8 | #include 9 | #include "BoundingRect.h" 10 | #include "DrawContext.h" 11 | 12 | namespace m5avatar { 13 | class Drawable { 14 | public: 15 | virtual ~Drawable() = default; 16 | virtual void draw(M5Canvas *spi, BoundingRect rect, 17 | DrawContext *drawContext) = 0; 18 | // virtual void draw(TFT_eSPI *spi, DrawContext *drawContext) = 0; 19 | }; 20 | 21 | } // namespace m5avatar 22 | 23 | #endif // DRAWABLE_H_ 24 | -------------------------------------------------------------------------------- /.github/workflows/doxygen.yml: -------------------------------------------------------------------------------- 1 | name: A job to build doxgen document and deploy it on github-pages 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - master 7 | - 'doxygen*' 8 | workflow_dispatch: 9 | 10 | jobs: 11 | doxygen-job: 12 | runs-on: ubuntu-22.04 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: download modern theme 16 | run: | 17 | curl -OL https://raw.githubusercontent.com/jothepro/doxygen-awesome-css/main/doxygen-awesome.css 18 | - uses: mattnotmitt/doxygen-action@v1.9.5 19 | - name: Deploy pages 20 | uses: peaceiris/actions-gh-pages@v3 21 | with: 22 | github_token: ${{ secrets.GITHUB_TOKEN }} 23 | publish_dir: "html" 24 | -------------------------------------------------------------------------------- /library.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "M5Stack-Avatar", 3 | "keywords": "avatar, lcd, cute, face, M5Stack", 4 | "description": "An M5Stack library for rendering avatar faces", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/meganetaaan/m5stack-avatar.git" 8 | }, 9 | "authors": [ 10 | { 11 | "name": "Shinya Ishikawa", 12 | "email": "ishikawa.s.1027@gmail.com", 13 | "url": "https://meganetaaan.github.io/", 14 | "maintainer": true 15 | } 16 | ], 17 | "license": "MIT", 18 | "dependencies": [ 19 | { 20 | "name": "M5Unified", 21 | "version": ">=0.1.11" 22 | } 23 | ], 24 | "version": "0.10.0", 25 | "frameworks": ["arduino", "espidf", "*"], 26 | "platforms": ["espressif32", "native"] 27 | } 28 | -------------------------------------------------------------------------------- /examples/PlatformIO_SDL/src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | using namespace m5avatar; 5 | 6 | Avatar avatar; 7 | 8 | void setup() 9 | { 10 | M5.begin(); 11 | avatar.init(); // start drawing 12 | 13 | // adjust position 14 | const auto r = avatar.getFace()->getBoundingRect(); 15 | const auto scale_w = M5.Display.width() / (float)r->getWidth(); 16 | const auto scale_h = M5.Display.height() / (float)r->getHeight(); 17 | const auto scale = std::min(scale_w, scale_h); 18 | avatar.setScale(scale); 19 | const auto offs_x = (r->getWidth() - M5.Display.width()) / 2; 20 | const auto offs_y = (r->getHeight() - M5.Display.height()) / 2; 21 | avatar.setPosition(-offs_y, -offs_x); 22 | } 23 | 24 | void loop() 25 | { 26 | // avatar's face updates in another thread 27 | // so no need to loop-by-loop rendering 28 | } 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Logs** 21 | If any compile error occurs, add a full compile log. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Environment (please complete the following information):** 27 | - OS: [e.g. Windows, MacOS] 28 | - IDE: [e.g. Arduino IDE, VSCode, Atom] 29 | - Versions 30 | - This library(M5Stack-Avatar) 31 | - M5Stack 32 | 33 | **Additional context** 34 | Add any other context about the problem here. 35 | -------------------------------------------------------------------------------- /src/tasks/LipSync.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Shinya Ishikawa. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full 3 | // license information. 4 | 5 | #ifndef TASKS_LIPSYNC_H_ 6 | #define TASKS_LIPSYNC_H_ 7 | 8 | #include 9 | # if defined(ARDUINO_M5STACK_Core2) || defined(M5AVATAR_CORE2) 10 | #include 11 | # else 12 | #include 13 | # endif 14 | #include 15 | #include "../Avatar.h" 16 | 17 | namespace m5avatar { 18 | extern void lipSync(void *args) { 19 | DriveContext *ctx = reinterpret_cast (args); 20 | Avatar *avatar = ctx->getAvatar(); 21 | for (;;) { 22 | int level = TTS.getLevel(); 23 | float f = level / 12000.0; 24 | float open = min(1.0, f); 25 | avatar->setMouthOpenRatio(open); 26 | delay(33); 27 | } 28 | } 29 | } // namespace m5avatar 30 | 31 | #endif // SRC_TASKS_LIPSYNC_H_ 32 | -------------------------------------------------------------------------------- /src/Eyeblow.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Shinya Ishikawa. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full 3 | // license information. 4 | 5 | #ifndef EYEBLOW_H_ 6 | #define EYEBLOW_H_ 7 | 8 | #define LGFX_USE_V1 9 | #include 10 | #include "BoundingRect.h" 11 | #include "DrawContext.h" 12 | #include "Drawable.h" 13 | 14 | namespace m5avatar { 15 | class Eyeblow final : public Drawable { 16 | private: 17 | uint16_t width; 18 | uint16_t height; 19 | bool isLeft; 20 | 21 | public: 22 | // constructor 23 | Eyeblow() = delete; 24 | Eyeblow(uint16_t w, uint16_t h, bool isLeft); 25 | ~Eyeblow() = default; 26 | Eyeblow(const Eyeblow &other) = default; 27 | Eyeblow &operator=(const Eyeblow &other) = default; 28 | void draw(M5Canvas *spi, BoundingRect rect, 29 | DrawContext *drawContext) override; 30 | }; 31 | 32 | } // namespace m5avatar 33 | 34 | #endif // EYEBLOW_H_ 35 | -------------------------------------------------------------------------------- /src/faces/OledFace.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Shinya Ishikawa. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full 3 | // license information. 4 | 5 | #ifndef FACES_OLEDFACE_H_ 6 | #define FACES_OLEDFACE_H_ 7 | 8 | #include // TODO(meganetaaan): include only the Sprite function not a whole library 9 | #include "../BoundingRect.h" 10 | #include "../DrawContext.h" 11 | #include "../Drawable.h" 12 | 13 | namespace m5avatar { 14 | class OledFace : public Face { 15 | public: 16 | OledFace() 17 | : Face(new Mouth(50, 90, 4, 60), new BoundingRect(168, 163), new Eye(8, false), 18 | new BoundingRect(103, 80), new Eye(8, true), 19 | new BoundingRect(106, 240), new Eyeblow(15, 2, false), 20 | new BoundingRect(67, 96), new Eyeblow(15, 2, true), 21 | new BoundingRect(72, 230)) {} 22 | }; 23 | 24 | } // namespace m5avatar 25 | 26 | #endif // FACES_OLEDFACE_H_ 27 | 28 | -------------------------------------------------------------------------------- /examples/balloon/balloon.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | using namespace m5avatar; 5 | 6 | Avatar avatar; 7 | const char* lyrics[] = {"Hey,", "diddle,", "diddle,", "The cat", 8 | "and", "the fiddle,", "The cow", "jumped", 9 | "over", "the moon.", "The little dog", "laughed", 10 | "to", "see", "such sport,", "And", 11 | "the dish", "ran away", "with the spoon."}; 12 | const int lyricsSize = sizeof(lyrics) / sizeof(char*); 13 | int lyricsIdx = 0; 14 | 15 | void setup() 16 | { 17 | M5.begin(); 18 | avatar.init(); 19 | } 20 | 21 | void loop() 22 | { 23 | M5.update(); 24 | if (M5.BtnA.wasPressed()) 25 | { 26 | const char* l = lyrics[lyricsIdx++ % lyricsSize]; 27 | avatar.setSpeechText(l); 28 | avatar.setMouthOpenRatio(0.7); 29 | delay(200); 30 | avatar.setMouthOpenRatio(0); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Mouth.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Shinya Ishikawa. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full 3 | // license information. 4 | 5 | #ifndef MOUTH_H_ 6 | #define MOUTH_H_ 7 | 8 | #include 9 | #include "BoundingRect.h" 10 | #include "DrawContext.h" 11 | #include "Drawable.h" 12 | 13 | namespace m5avatar { 14 | class Mouth final : public Drawable { 15 | private: 16 | uint16_t minWidth; 17 | uint16_t maxWidth; 18 | uint16_t minHeight; 19 | uint16_t maxHeight; 20 | 21 | public: 22 | // constructor 23 | Mouth() = delete; 24 | ~Mouth() = default; 25 | Mouth(const Mouth &other) = default; 26 | Mouth &operator=(const Mouth &other) = default; 27 | Mouth(uint16_t minWidth, uint16_t maxWidth, uint16_t minHeight, 28 | uint16_t maxHeight); 29 | void draw(M5Canvas *spi, BoundingRect rect, 30 | DrawContext *drawContext) override; 31 | }; 32 | 33 | } // namespace m5avatar 34 | 35 | #endif // MOUTH_H_ 36 | -------------------------------------------------------------------------------- /src/Eye.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Shinya Ishikawa. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full 3 | // license information. 4 | 5 | #ifndef EYE_H_ 6 | #define EYE_H_ 7 | 8 | #define LGFX_USE_V1 9 | #include 10 | #include "DrawContext.h" 11 | #include "Drawable.h" 12 | 13 | namespace m5avatar { 14 | 15 | class Eye final : public Drawable { 16 | private: 17 | uint16_t r; 18 | bool isLeft; 19 | 20 | public: 21 | // constructor 22 | Eye() = delete; 23 | Eye(uint16_t x, uint16_t y, uint16_t r, bool isLeft); // deprecated 24 | Eye(uint16_t r, bool isLeft); 25 | ~Eye() = default; 26 | Eye(const Eye &other) = default; 27 | Eye &operator=(const Eye &other) = default; 28 | void draw(M5Canvas *spi, BoundingRect rect, 29 | DrawContext *drawContext) override; 30 | // void draw(TFT_eSPI *spi, DrawContext *drawContext) override; // deprecated 31 | }; 32 | 33 | } // namespace m5avatar 34 | 35 | #endif // EYE_H_ 36 | -------------------------------------------------------------------------------- /lib/readme.txt: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for the project specific (private) libraries. 3 | PlatformIO will compile them to static libraries and link to executable file. 4 | 5 | The source code of each library should be placed in separate directory, like 6 | "lib/private_lib/[here are source files]". 7 | 8 | For example, see how can be organized `Foo` and `Bar` libraries: 9 | 10 | |--lib 11 | | |--Bar 12 | | | |--docs 13 | | | |--examples 14 | | | |--src 15 | | | |- Bar.c 16 | | | |- Bar.h 17 | | |--Foo 18 | | | |- Foo.c 19 | | | |- Foo.h 20 | | |- readme.txt --> THIS FILE 21 | |- platformio.ini 22 | |--src 23 | |- main.c 24 | 25 | Then in `src/main.c` you should use: 26 | 27 | #include 28 | #include 29 | 30 | // rest H/C/CPP code 31 | 32 | PlatformIO will find your libraries automatically, configure preprocessor's 33 | include paths and build them. 34 | 35 | More information about PlatformIO Library Dependency Finder 36 | - http://docs.platformio.org/page/librarymanager/ldf.html 37 | -------------------------------------------------------------------------------- /examples/talk/talk.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | using namespace m5avatar; 7 | 8 | // AquesTalk License Key 9 | // NULL or wrong value is just ignored 10 | const char* AQUESTALK_KEY = "XXXX-XXXX-XXXX-XXXX"; 11 | 12 | Avatar avatar; 13 | void setup() { 14 | int iret; 15 | M5.begin(); 16 | // For Kanji-to-speech mode (requires dictionary file saved on microSD) 17 | // See http://blog-yama.a-quest.com/?eid=970195 18 | // iret = TTS.createK(AQUESTALK_KEY); 19 | iret = TTS.create(AQUESTALK_KEY); 20 | M5.Lcd.setBrightness(30); 21 | M5.Lcd.clear(); 22 | avatar.init(); 23 | avatar.addTask(lipSync, "lipSync"); 24 | } 25 | 26 | void loop() { 27 | M5.update(); 28 | if (M5.BtnA.wasPressed()) { 29 | // Need to initialize with createK(AQUESTALK_KEY) 30 | // TTS.play("こんにちは。", 80); 31 | TTS.play("konnichiwa", 80); 32 | avatar.setSpeechText("Hello"); 33 | delay(1000); 34 | avatar.setSpeechText(""); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/DrawingUtils.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file DrawingUtils.hpp 3 | * @author botamochi (botamochi6277@gmail.com) 4 | * @brief drawing utils including geometry handling 5 | * @version 0.1 6 | * @date 2024-07-28 7 | * 8 | * @copyright Copyright (c) 2024 9 | * 10 | */ 11 | 12 | #ifndef M5AVATAR_DRAWING_UTILS_HPP_ 13 | #define M5AVATAR_DRAWING_UTILS_HPP_ 14 | 15 | #include 16 | #include 17 | #include 18 | 19 | namespace m5avatar { 20 | void rotatePoint(float &x, float &y, float angle); 21 | 22 | void rotatePointAround(float &x, float &y, float angle, float cx, float cy); 23 | 24 | void fillRotatedRect(M5Canvas *canvas, uint16_t cx, uint16_t cy, uint16_t w, 25 | uint16_t h, float angle, uint16_t color); 26 | 27 | void fillRectRotatedAround(M5Canvas *canvas, float top_left_x, float top_left_y, 28 | float bottom_right_x, float bottom_right_y, 29 | float angle, uint16_t cx, uint16_t cy, 30 | uint16_t color); 31 | 32 | } // namespace m5avatar 33 | 34 | #endif -------------------------------------------------------------------------------- /src/BoundingRect.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Shinya Ishikawa. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full 3 | // license information. 4 | 5 | #ifndef BOUNDINGRECT_H_ 6 | #define BOUNDINGRECT_H_ 7 | #include 8 | 9 | namespace m5avatar { 10 | class BoundingRect { 11 | private: 12 | int16_t top; 13 | int16_t left; 14 | int16_t width; 15 | int16_t height; 16 | 17 | public: 18 | BoundingRect() = default; 19 | ~BoundingRect() = default; 20 | BoundingRect(int16_t top, int16_t left); 21 | BoundingRect(int16_t top, int16_t left, int16_t width, int16_t height); 22 | BoundingRect(const BoundingRect &other) = default; 23 | BoundingRect &operator=(const BoundingRect &other) = default; 24 | int16_t getTop(); 25 | int16_t getLeft(); 26 | int16_t getRight(); 27 | int16_t getBottom(); 28 | int16_t getCenterX(); 29 | int16_t getCenterY(); 30 | int16_t getWidth(); 31 | int16_t getHeight(); 32 | void setPosition(int16_t top, int16_t left); 33 | void setSize(int16_t width, int16_t height); 34 | }; 35 | } // namespace m5avatar 36 | 37 | #endif // BOUNDINGRECT_H_ 38 | -------------------------------------------------------------------------------- /src/ColorPalette.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Shinya Ishikawa. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full 3 | // license information. 4 | 5 | #include "ColorPalette.h" 6 | 7 | namespace m5avatar { 8 | ColorPalette::ColorPalette() 9 | : colors{{COLOR_PRIMARY, TFT_WHITE}, 10 | {COLOR_SECONDARY, TFT_BLACK}, 11 | {COLOR_BACKGROUND, TFT_BLACK}, 12 | {COLOR_BALLOON_FOREGROUND, TFT_BLACK}, 13 | {COLOR_BALLOON_BACKGROUND, TFT_WHITE}} {} 14 | 15 | uint16_t ColorPalette::get(const char* key) const { 16 | auto itr = colors.find(key); 17 | if (itr != colors.end()) { 18 | return itr->second; 19 | } else { 20 | // NOTE: if no value it returns BLACK(0x00) as the default value of the 21 | // type(int) 22 | M5_LOGI("no color with the key %s", key); 23 | return TFT_BLACK; 24 | } 25 | } 26 | 27 | void ColorPalette::set(const char* key, uint16_t value) { 28 | auto itr = colors.find(key); 29 | if (itr != colors.end()) { 30 | M5_LOGI("Overwriting"); 31 | } 32 | itr->second = value; 33 | } 34 | } // namespace m5avatar 35 | -------------------------------------------------------------------------------- /src/Mouth.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Shinya Ishikawa. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full 3 | // license information. 4 | 5 | #include "Mouth.h" 6 | 7 | #ifndef _min 8 | #define _min(a, b) std::min(a, b) 9 | #endif 10 | 11 | namespace m5avatar { 12 | 13 | Mouth::Mouth(uint16_t minWidth, uint16_t maxWidth, uint16_t minHeight, 14 | uint16_t maxHeight) 15 | : minWidth{minWidth}, 16 | maxWidth{maxWidth}, 17 | minHeight{minHeight}, 18 | maxHeight{maxHeight} {} 19 | 20 | void Mouth::draw(M5Canvas *spi, BoundingRect rect, DrawContext *ctx) { 21 | uint16_t primaryColor = ctx->getColorDepth() == 1 ? 1 : ctx->getColorPalette()->get(COLOR_PRIMARY); 22 | float breath = _min(1.0f, ctx->getBreath()); 23 | float openRatio = ctx->getMouthOpenRatio(); 24 | int h = minHeight + (maxHeight - minHeight) * openRatio; 25 | int w = minWidth + (maxWidth - minWidth) * (1 - openRatio); 26 | int x = rect.getLeft() - w / 2; 27 | int y = rect.getTop() - h / 2 + breath * 2; 28 | spi->fillRect(x, y, w, h, primaryColor); 29 | } 30 | 31 | } // namespace m5avatar 32 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Shinya Ishikawa 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 | -------------------------------------------------------------------------------- /examples/wink-demo/wink-demo.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | m5avatar::Avatar avatar; 5 | m5avatar::ColorPalette* palettes[2]; 6 | 7 | void setup() { 8 | M5.begin(); 9 | 10 | palettes[0] = new m5avatar::ColorPalette(); 11 | palettes[1] = new m5avatar::ColorPalette(); 12 | palettes[1]->set(COLOR_PRIMARY, TFT_YELLOW); 13 | palettes[1]->set(COLOR_BACKGROUND, TFT_DARKCYAN); 14 | 15 | avatar.init(); // start drawing 16 | } 17 | 18 | void loop() { 19 | M5.update(); 20 | if (M5.BtnA.wasPressed()) { 21 | // switch right eye 22 | avatar.setRightEyeOpenRatio(avatar.getRightEyeOpenRatio() > 0.5f ? 0.0f 23 | : 1.0f); 24 | } 25 | if (M5.BtnB.wasPressed()) { 26 | avatar.setIsAutoBlink(!avatar.getIsAutoBlink()); 27 | avatar.setColorPalette( 28 | *palettes[static_cast(!avatar.getIsAutoBlink())]); 29 | } 30 | if (M5.BtnC.wasPressed()) { 31 | // switch left eye 32 | avatar.setLeftEyeOpenRatio(avatar.getLeftEyeOpenRatio() > 0.5f ? 0.0f 33 | : 1.0f); 34 | } 35 | delay(10); 36 | } 37 | -------------------------------------------------------------------------------- /src/ColorPalette.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Shinya Ishikawa. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full 3 | // license information. 4 | 5 | #ifndef COLORPALETTE_H_ 6 | #define COLORPALETTE_H_ 7 | #include 8 | #include 9 | #include 10 | #define COLOR_PRIMARY "primary" 11 | #define COLOR_SECONDARY "secondary" 12 | #define COLOR_BACKGROUND "background" 13 | #define COLOR_BALLOON_FOREGROUND "balloon_f" 14 | #define COLOR_BALLOON_BACKGROUND "balloon_b" 15 | 16 | namespace m5avatar { 17 | // enum class ColorType 18 | // { 19 | // ONEBYTE, 20 | // TWOBYTE, 21 | // ONEBIT 22 | // } 23 | /** 24 | * Color palette for drawing face 25 | */ 26 | class ColorPalette { 27 | private: 28 | // ColorType colorType; 29 | // uint16_t colors[2]; 30 | std::map colors; 31 | 32 | public: 33 | // TODO(meganetaaan): constructor with color settings 34 | ColorPalette(); 35 | ~ColorPalette() = default; 36 | ColorPalette(const ColorPalette &other) = default; 37 | ColorPalette &operator=(const ColorPalette &other) = default; 38 | 39 | uint16_t get(const char *key) const; 40 | void set(const char *key, uint16_t value); 41 | void clear(void); 42 | }; 43 | } // namespace m5avatar 44 | 45 | #endif // COLORPALETTE_H_ 46 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "limits": "cpp", 4 | "array": "cpp", 5 | "*.tcc": "cpp", 6 | "cctype": "cpp", 7 | "clocale": "cpp", 8 | "cmath": "cpp", 9 | "cstdarg": "cpp", 10 | "cstdint": "cpp", 11 | "cstdio": "cpp", 12 | "cstdlib": "cpp", 13 | "cstring": "cpp", 14 | "ctime": "cpp", 15 | "cwchar": "cpp", 16 | "cwctype": "cpp", 17 | "deque": "cpp", 18 | "unordered_map": "cpp", 19 | "unordered_set": "cpp", 20 | "vector": "cpp", 21 | "exception": "cpp", 22 | "fstream": "cpp", 23 | "functional": "cpp", 24 | "initializer_list": "cpp", 25 | "iomanip": "cpp", 26 | "iosfwd": "cpp", 27 | "istream": "cpp", 28 | "memory": "cpp", 29 | "new": "cpp", 30 | "ostream": "cpp", 31 | "numeric": "cpp", 32 | "sstream": "cpp", 33 | "stdexcept": "cpp", 34 | "streambuf": "cpp", 35 | "system_error": "cpp", 36 | "cinttypes": "cpp", 37 | "type_traits": "cpp", 38 | "tuple": "cpp", 39 | "typeinfo": "cpp", 40 | "utility": "cpp" 41 | }, 42 | "C_Cpp.clang_format_style": "{ BasedOnStyle: Google, IndentWidth: 2 }", 43 | "cpplint.root": "src" 44 | } -------------------------------------------------------------------------------- /src/BoundingRect.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Shinya Ishikawa. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full 3 | // license information. 4 | 5 | #include "BoundingRect.h" 6 | 7 | namespace m5avatar { 8 | BoundingRect::BoundingRect(int16_t top, int16_t left) 9 | : BoundingRect(top, left, 0, 0) {} 10 | 11 | BoundingRect::BoundingRect(int16_t top, int16_t left, int16_t width, 12 | int16_t height) 13 | : top{top}, left{left}, width{width}, height{height} {} 14 | 15 | int16_t BoundingRect::getTop() { return top; } 16 | 17 | int16_t BoundingRect::getLeft() { return left; } 18 | 19 | int16_t BoundingRect::getRight() { return left + width; } 20 | 21 | int16_t BoundingRect::getBottom() { return top + height; } 22 | 23 | int16_t BoundingRect::getCenterX() { return left + width / 2; } 24 | 25 | int16_t BoundingRect::getCenterY() { return top + height / 2; } 26 | 27 | int16_t BoundingRect::getWidth() { return width; } 28 | 29 | int16_t BoundingRect::getHeight() { return height; } 30 | 31 | void BoundingRect::setPosition(int16_t top, int16_t left) { 32 | this->top = top; 33 | this->left = left; 34 | } 35 | 36 | void BoundingRect::setSize(int16_t width, int16_t height) { 37 | this->width = width; 38 | this->height = height; 39 | } 40 | } // namespace m5avatar 41 | -------------------------------------------------------------------------------- /src/faces/eye_small.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #define eye_small_width 40 4 | #define eye_small_height 40 5 | PROGMEM const unsigned char eye_small[] = { 6 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 7 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 8 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 9 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 10 | 0x01, 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xFC, 0xFF, 0x3F, 0x00, 11 | 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x01, 0xC0, 0x27, 12 | 0xFE, 0xE7, 0x07, 0xF0, 0x13, 0xF8, 0xCF, 0x0F, 0xF8, 0x10, 0xF8, 0x0F, 13 | 0x1F, 0x7C, 0x18, 0xF8, 0x1F, 0x3E, 0x3E, 0x18, 0xF8, 0x1F, 0x78, 0x0F, 14 | 0x38, 0xFC, 0x1F, 0xF8, 0x0F, 0xF8, 0xFF, 0x1F, 0xF0, 0x3E, 0xF8, 0xFF, 15 | 0x1F, 0x7C, 0x7C, 0xF8, 0xFF, 0x1F, 0x3E, 0xF8, 0xF1, 0xFF, 0x8F, 0x1F, 16 | 0xF0, 0xF3, 0xFF, 0xCF, 0x0F, 0xC0, 0xE7, 0xFF, 0xE7, 0x07, 0x80, 0xFF, 17 | 0xFF, 0xFF, 0x01, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFC, 0xFF, 0x3F, 18 | 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0x80, 0xFF, 0x00, 0x00, 0x00, 19 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 20 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 21 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 22 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }; 23 | -------------------------------------------------------------------------------- /DESIGN.md: -------------------------------------------------------------------------------- 1 | # Design drafts 2 | 3 | ## face drawing 4 | 5 | * Face has its size and position 6 | * Face manages color palette 7 | * drawing are cascaded 8 | * Avatar 9 | * EyeR 10 | * EyeL 11 | * Mouth 12 | * drawing has context: common parameters of the face 13 | * transforms 14 | * position 15 | * rotation 16 | * scale 17 | * colors 18 | * color palette (array of 8bit color) 19 | * statuses 20 | * breath 21 | * focus point 22 | * expression 23 | 24 | ```cpp 25 | ColorPalette cp = this->colorPalette; 26 | uint32_t primary = cp.get(PRIMARY_COLOR); 27 | uint32_t secondary = cp.get(SECONDARY_COLOR); 28 | ``` 29 | 30 | ### Colors 31 | 32 | context contains color palette for the face drawing. 33 | it's internally an array of 8bit color. 34 | It should contain at least two colors(primary, secondary) 35 | User can add other colors to the array. 36 | User can use colors from their draw function but not necessary. 37 | 38 | ### discussion 39 | 40 | * Should parents know its children? 41 | * Should renderers be separated from face parts themselves? 42 | * Eye 43 | * setOpenRatio 44 | * setGaze 45 | * Renderer 46 | * draw 47 | 48 | ### tasks 49 | 50 | * removeTask(task) 51 | * addTask(task) 52 | * getTasks() 53 | 54 | task...FreeRTOS like I/F 55 | 56 | task is managed by avatar using its name. 57 | all taskname are unique so that avatar cannot have tasks with the same name. 58 | 59 | Task { 60 | members: 61 | name 62 | run 63 | } 64 | 65 | -------------------------------------------------------------------------------- /lib/AquesTalkTTS/AquesTalkTTS.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \par Copyright (C), 2018, AQUEST 3 | * \class AquesTalkTTS 4 | * \brief Text-to-Sppech library. uses AquesTalk-ESP, I2S, intenal-DAC. 5 | * @file AquesTalkTTS.h 6 | * @author AQUEST 7 | * @version V0.2.0 8 | * @date 2018/08/08 9 | * @brief Header for AquesTalkTTS.cpp module 10 | * 11 | * \par Description 12 | * This file is a TTS class for ESP32. 13 | * 14 | * \par Method List: 15 | * 16 | * 17 | TTS.create(); 18 | TTS.createK(); 19 | TTS.release(); 20 | TTS.play(const char *koe, int speed); 21 | TTS.playK(const char *koe, int speed); 22 | TTS.isPlay(); 23 | TTS.stop(); 24 | TTS.getLavel(); 25 | * 26 | * \par History: 27 | *
28 |  * ``         `
32 | * 33 | */ 34 | 35 | #ifndef _AQUESTALK_TTS_H_ 36 | #define _AQUESTALK_TTS_H_ 37 | 38 | class AquesTalkTTS { 39 | public: 40 | int create(const char *licencekey);//heap:400B 41 | int createK(const char *licencekey);//heap:21KB 42 | void release(); 43 | 44 | int play(const char *koe, int speed);//koe: onsei-kigouretu(roman) 45 | int playK(const char *kanji, int speed);//kanji: kanji-text (UTF8) 46 | 47 | void stop(); 48 | bool isPlay(); 49 | int getLevel(); 50 | }; 51 | extern AquesTalkTTS TTS; 52 | 53 | #endif // !defined(_AQUESTALK_TTS_H_) 54 | -------------------------------------------------------------------------------- /src/Eyeblow.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Shinya Ishikawa. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full 3 | // license information. 4 | 5 | #include "Eyeblow.h" 6 | namespace m5avatar { 7 | 8 | Eyeblow::Eyeblow(uint16_t w, uint16_t h, bool isLeft) 9 | : width{w}, height{h}, isLeft{isLeft} {} 10 | 11 | void Eyeblow::draw(M5Canvas *spi, BoundingRect rect, DrawContext *ctx) { 12 | Expression exp = ctx->getExpression(); 13 | uint32_t x = rect.getLeft(); 14 | uint32_t y = rect.getTop(); 15 | uint16_t primaryColor = ctx->getColorDepth() == 1 ? 1 : ctx->getColorPalette()->get(COLOR_PRIMARY); 16 | if (width == 0 || height == 0) { 17 | return; 18 | } 19 | // draw two triangles to make rectangle 20 | if (exp == Expression::Angry || exp == Expression::Sad) { 21 | int x1, y1, x2, y2, x3, y3, x4, y4; 22 | int a = isLeft ^ (exp == Expression::Sad) ? -1 : 1; 23 | int dx = a * 3; 24 | int dy = a * 5; 25 | x1 = x - width / 2; 26 | x2 = x1 - dx; 27 | x4 = x + width / 2; 28 | x3 = x4 + dx; 29 | y1 = y - height / 2 - dy; 30 | y2 = y + height / 2 - dy; 31 | y3 = y - height / 2 + dy; 32 | y4 = y + height / 2 + dy; 33 | spi->fillTriangle(x1, y1, x2, y2, x3, y3, primaryColor); 34 | spi->fillTriangle(x2, y2, x3, y3, x4, y4, primaryColor); 35 | } else { 36 | int x1 = x - width / 2; 37 | int y1 = y - height / 2; 38 | if (exp == Expression::Happy) { 39 | y1 = y1 - 5; 40 | } 41 | spi->fillRect(x1, y1, width, height, primaryColor); 42 | } 43 | } 44 | 45 | } // namespace m5avatar 46 | -------------------------------------------------------------------------------- /examples/PlatformIO_SDL/platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; https://docs.platformio.org/page/projectconf.html 10 | 11 | [platformio] 12 | default_envs = native 13 | 14 | [env] 15 | lib_extra_dirs=../../../ 16 | lib_deps = m5stack/M5Unified@^0.1.11 17 | 18 | [env:native] 19 | platform = native 20 | build_type = debug 21 | build_flags = -O0 -xc++ -std=c++14 -lSDL2 22 | -I"/usr/local/include/SDL2" ; for intel mac homebrew SDL2 23 | -L"/usr/local/lib" ; for intel mac homebrew SDL2 24 | -I"${sysenv.HOMEBREW_PREFIX}/include/SDL2" ; for arm mac homebrew SDL2 25 | -L"${sysenv.HOMEBREW_PREFIX}/lib" ; for arm mac homebrew SDL2 26 | 27 | [env:native_m5stack] 28 | extends = native 29 | platform = native 30 | build_flags = ${env:native.build_flags} 31 | -DM5GFX_BOARD=board_M5Stack 32 | 33 | [env:native_stickCPlus] 34 | extends = native 35 | platform = native 36 | build_flags = ${env:native.build_flags} 37 | -DM5GFX_SCALE=2 38 | -DM5GFX_ROTATION=0 39 | -DM5GFX_BOARD=board_M5StickCPlus 40 | 41 | [esp32_base] 42 | build_type = debug 43 | platform = espressif32 44 | board = esp32dev 45 | upload_speed = 1500000 46 | monitor_speed = 115200 47 | monitor_filters = esp32_exception_decoder 48 | 49 | [env:esp32_arduino] 50 | extends = esp32_base 51 | framework = arduino 52 | -------------------------------------------------------------------------------- /src/Eyebrows.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Eyebrows.hpp 3 | * @author botamochi (botamochi6277@gmail.com) 4 | * @brief Eyebrow components 5 | * @version 0.1 6 | * @date 2024-07-28 7 | * 8 | * @copyright Copyright (c) 2024 9 | * 10 | */ 11 | 12 | #ifndef M5AVATAR_EYEBROWS_HPP_ 13 | #define M5AVATAR_EYEBROWS_HPP_ 14 | 15 | #include 16 | #include 17 | #include 18 | 19 | #include "DrawingUtils.hpp" 20 | namespace m5avatar { 21 | class BaseEyebrow : public Drawable { 22 | protected: 23 | uint16_t height_; 24 | uint16_t width_; 25 | bool is_left_; 26 | 27 | // caches 28 | uint16_t primary_color_; 29 | uint16_t secondary_color_; 30 | uint16_t background_color_; 31 | int16_t center_x_; 32 | int16_t center_y_; 33 | Expression expression_; 34 | 35 | public: 36 | BaseEyebrow(bool is_left); 37 | BaseEyebrow(uint16_t width, uint16_t height, bool is_left); 38 | void update(M5Canvas *canvas, BoundingRect rect, DrawContext *ctx); 39 | }; 40 | 41 | // Maro Mayu 42 | class EllipseEyebrow : public BaseEyebrow { 43 | public: 44 | using BaseEyebrow::BaseEyebrow; 45 | void draw(M5Canvas *canvas, BoundingRect rect, DrawContext *ctx); 46 | }; 47 | 48 | class BowEyebrow : public BaseEyebrow { 49 | public: 50 | using BaseEyebrow::BaseEyebrow; 51 | void draw(M5Canvas *canvas, BoundingRect rect, DrawContext *ctx); 52 | }; 53 | 54 | class RectEyebrow : public BaseEyebrow { 55 | public: 56 | using BaseEyebrow::BaseEyebrow; 57 | void draw(M5Canvas *canvas, BoundingRect rect, DrawContext *ctx); 58 | }; 59 | 60 | } // namespace m5avatar 61 | 62 | #endif -------------------------------------------------------------------------------- /src/Mouths.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Mouths.hpp 3 | * @author botamochi (botamochi6277@gmail.com) 4 | * @brief Mouth components 5 | * @version 0.1 6 | * @date 2024-07-28 7 | * 8 | * @copyright Copyright (c) 2024 9 | * 10 | */ 11 | #ifndef M5AVATAR_MOUTHS_HPP_ 12 | #define M5AVATAR_MOUTHS_HPP_ 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | namespace m5avatar { 20 | 21 | class BaseMouth : public Drawable { 22 | protected: 23 | uint16_t min_width_; 24 | uint16_t max_width_; 25 | uint16_t min_height_; 26 | uint16_t max_height_; 27 | 28 | // caches for drawing 29 | int16_t center_x_; 30 | int16_t center_y_; 31 | uint16_t primary_color_; 32 | uint16_t secondary_color_; 33 | uint16_t background_color_; 34 | float open_ratio_; 35 | float breath_; 36 | Expression expression_; 37 | 38 | public: 39 | BaseMouth(); 40 | BaseMouth(uint16_t min_width, uint16_t max_width, uint16_t min_height, 41 | uint16_t max_height); 42 | 43 | void update(M5Canvas *canvas, BoundingRect rect, DrawContext *ctx); 44 | }; 45 | 46 | class RectMouth : public BaseMouth { 47 | public: 48 | using BaseMouth::BaseMouth; 49 | void draw(M5Canvas *canvas, BoundingRect rect, DrawContext *ctx); 50 | }; 51 | 52 | class OmegaMouth : public BaseMouth { 53 | public: 54 | using BaseMouth::BaseMouth; 55 | void draw(M5Canvas *canvas, BoundingRect rect, DrawContext *ctx); 56 | }; 57 | 58 | class UShapeMouth : public BaseMouth { 59 | public: 60 | using BaseMouth::BaseMouth; 61 | void draw(M5Canvas *canvas, BoundingRect rect, DrawContext *ctx); 62 | }; 63 | 64 | class DoggyMouth : public BaseMouth { 65 | public: 66 | using BaseMouth::BaseMouth; 67 | void draw(M5Canvas *canvas, BoundingRect rect, DrawContext *ctx); 68 | }; 69 | 70 | } // namespace m5avatar 71 | 72 | #endif -------------------------------------------------------------------------------- /src/faces/BMPFace.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Shinya Ishikawa. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full 3 | // license information. 4 | 5 | #ifndef FACES_BMPFACE_H_ 6 | #define FACES_BMPFACE_H_ 7 | 8 | #include // TODO(meganetaaan): include only the Sprite function not a whole library 9 | 10 | #include "../BoundingRect.h" 11 | #include "../DrawContext.h" 12 | #include "../Drawable.h" 13 | #include "../Face.h" 14 | #include "eye_small.h" 15 | 16 | namespace m5avatar 17 | { 18 | class BMPEye : public Drawable 19 | { 20 | void draw(M5Canvas *spi, BoundingRect rect, DrawContext *ctx) 21 | { 22 | uint16_t color = ctx->getColorDepth() == 1 ? 1 : ctx->getColorPalette()->get(COLOR_PRIMARY); 23 | uint16_t cx = rect.getCenterX(); 24 | uint16_t cy = rect.getCenterY(); 25 | float openRatio = ctx->getEyeOpenRatio(); 26 | Gaze g = ctx->getGaze(); 27 | uint32_t offsetX = g.getHorizontal() * 3; 28 | uint32_t offsetY = g.getVertical() * 3; 29 | if (openRatio == 0) { 30 | // close 31 | int x1 = cx - eye_small_width / 2 + offsetX; 32 | int y1 = cy + offsetY; 33 | int w = eye_small_width; 34 | int h = 4; 35 | spi->fillRect(x1, y1, w, h, color); 36 | return; 37 | } 38 | spi->drawXBitmap( 39 | cx - eye_small_width / 2, 40 | cy - eye_small_height / 2, 41 | eye_small, eye_small_width, eye_small_height, color); 42 | } 43 | }; 44 | 45 | class BMPFace : public Face 46 | { 47 | public: 48 | BMPFace() 49 | : Face(new Mouth(50, 90, 4, 60), new BoundingRect(148, 163), 50 | new BMPEye(), 51 | new BoundingRect(103, 80), new BMPEye(), 52 | new BoundingRect(106, 240), new Eyeblow(15, 2, false), 53 | new BoundingRect(67, 96), new Eyeblow(15, 2, true), 54 | new BoundingRect(72, 230)) {} 55 | }; 56 | 57 | } // namespace m5avatar 58 | 59 | #endif // FACES_BMPFACE_H_ 60 | -------------------------------------------------------------------------------- /src/Eyes.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Eyes.hpp 3 | * @author botamochi (botamochi6277@gmail.com) 4 | * @brief Eye components 5 | * @version 0.1 6 | * @date 2024-07-28 7 | * 8 | * @copyright Copyright (c) 2024 9 | * 10 | */ 11 | #ifndef M5AVATAR_EYES_HPP_ 12 | #define M5AVATAR_EYES_HPP_ 13 | 14 | #include 15 | #include 16 | #include 17 | 18 | #include "DrawingUtils.hpp" 19 | namespace m5avatar { 20 | 21 | class BaseEye : public Drawable { 22 | protected: 23 | uint16_t height_; 24 | uint16_t width_; 25 | bool is_left_; 26 | 27 | // caches for drawing 28 | int16_t center_x_; 29 | int16_t center_y_; 30 | Gaze gaze_; 31 | uint16_t primary_color_; 32 | uint16_t secondary_color_; 33 | uint16_t background_color_; 34 | int16_t shifted_x_; 35 | int16_t shifted_y_; 36 | float open_ratio_; 37 | Expression expression_; 38 | 39 | public: 40 | BaseEye(bool is_left); 41 | BaseEye(uint16_t width, uint16_t height, bool is_left); 42 | void update(M5Canvas *canvas, BoundingRect rect, DrawContext *ctx); 43 | }; 44 | 45 | class EllipseEye : public BaseEye { 46 | public: 47 | using BaseEye::BaseEye; 48 | void draw(M5Canvas *canvas, BoundingRect rect, DrawContext *ctx); 49 | }; 50 | 51 | class GirlyEye : public BaseEye { 52 | public: 53 | using BaseEye::BaseEye; 54 | void drawEyeLid(M5Canvas *canvas); 55 | void overwriteOpenRatio(); 56 | void draw(M5Canvas *canvas, BoundingRect rect, DrawContext *ctx); 57 | }; 58 | 59 | class PinkDemonEye : public BaseEye { 60 | public: 61 | using BaseEye::BaseEye; 62 | void drawEyeLid(M5Canvas *canvas); 63 | void overwriteOpenRatio(); 64 | void draw(M5Canvas *canvas, BoundingRect rect, DrawContext *ctx); 65 | }; 66 | 67 | class DoggyEye : public BaseEye { 68 | public: 69 | using BaseEye::BaseEye; 70 | void draw(M5Canvas *canvas, BoundingRect rect, DrawContext *ctx); 71 | }; 72 | } // namespace m5avatar 73 | 74 | #endif -------------------------------------------------------------------------------- /examples/audio/audio.ino: -------------------------------------------------------------------------------- 1 | #pragma mark - Depend ESP8266Audio and ESP8266_Spiram libraries 2 | /* 3 | cd ~/Arduino/libraries 4 | git clone https://github.com/earlephilhower/ESP8266Audio 5 | git clone https://github.com/Gianbacchio/ESP8266_Spiram 6 | Use the "Tools->ESP32 Sketch Data Upload" menu to write the MP3 to SPIFFS 7 | Then upload the sketch normally. 8 | https://github.com/me-no-dev/arduino-esp32fs-plugin 9 | */ 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include "SPIFFS.h" 16 | #include "AudioFileSourceSPIFFS.h" 17 | #include "AudioFileSourceID3.h" 18 | #include "AudioGeneratorMP3.h" 19 | #include "AudioOutputI2S.h" 20 | #include 21 | 22 | using namespace m5avatar; 23 | 24 | AudioGeneratorMP3 *mp3; 25 | AudioFileSourceSPIFFS *file; 26 | AudioOutputI2S *out; 27 | AudioFileSourceID3 *id3; 28 | 29 | Avatar avatar; 30 | 31 | int levels[10]; 32 | const int levelsSize = sizeof(levels) / sizeof(int); 33 | int levelsIdx = 0; 34 | 35 | int avgLevel() { 36 | int sum = 0; 37 | for (int i = 0; i < levelsSize; i++) { 38 | sum += levels[i]; 39 | } 40 | return sum / levelsSize; 41 | } 42 | 43 | void setup() 44 | { 45 | M5.begin(); 46 | WiFi.mode(WIFI_OFF); 47 | SPIFFS.begin(); 48 | delay(500); 49 | avatar.init(); 50 | } 51 | 52 | void loop() 53 | { 54 | M5.update(); 55 | if (M5.BtnA.wasPressed()) { 56 | Serial.printf("Sample MP3 playback begins...\n"); 57 | file = new AudioFileSourceSPIFFS("/nyaan.mp3"); 58 | id3 = new AudioFileSourceID3(file); 59 | out = new AudioOutputI2S(0, 1); // Output to builtInDAC 60 | out->SetOutputModeMono(true); 61 | out->SetGain(0.16); 62 | mp3 = new AudioGeneratorMP3(); 63 | mp3->begin(id3, out); 64 | while (mp3->isRunning()) { 65 | levels[levelIdx] = abs(out->getLevel()); 66 | levelIdx = (levelIdx + 1) % levelsSize; 67 | float f = avgLevel() / 12000.0; 68 | avatar.setMouthOpenRatio(f); 69 | if (!mp3->loop()) mp3->stop(); 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Continuous Integration (CI) is the practice, in software 2 | # engineering, of merging all developer working copies with a shared mainline 3 | # several times a day < http://docs.platformio.org/page/ci/index.html > 4 | # 5 | # Documentation: 6 | # 7 | # * Travis CI Embedded Builds with PlatformIO 8 | # < https://docs.travis-ci.com/user/integration/platformio/ > 9 | # 10 | # * PlatformIO integration with Travis CI 11 | # < http://docs.platformio.org/page/ci/travis.html > 12 | # 13 | # * User Guide for `platformio ci` command 14 | # < http://docs.platformio.org/page/userguide/cmd_ci.html > 15 | # 16 | # 17 | # Please choice one of the following templates (proposed below) and uncomment 18 | # it (remove "# " before each line) or use own configuration according to the 19 | # Travis CI documentation (see above). 20 | # 21 | 22 | 23 | # 24 | # Template #1: General project. Test it using existing `platformio.ini`. 25 | # 26 | 27 | # language: python 28 | # python: 29 | # - "2.7" 30 | # 31 | # sudo: false 32 | # cache: 33 | # directories: 34 | # - "~/.platformio" 35 | # 36 | # install: 37 | # - pip install -U platformio 38 | # - platformio update 39 | # 40 | # script: 41 | # - platformio run 42 | 43 | 44 | # 45 | # Template #2: The project is intended to by used as a library with examples 46 | # 47 | 48 | language: python 49 | python: 50 | - "3.8" 51 | 52 | sudo: false 53 | cache: 54 | directories: 55 | - "~/.platformio" 56 | 57 | env: 58 | - PLATFORMIO_CI_SRC=examples/face-and-color/face-and-color.ino 59 | - PLATFORMIO_CI_SRC=examples/basics/basics.ino 60 | - PLATFORMIO_CI_SRC=examples/balloon/balloon.ino 61 | - PLATFORMIO_CI_SRC=examples/transform/transform.ino 62 | # - PLATFORMIO_CI_SRC=path/to/test/file.c 63 | # - PLATFORMIO_CI_SRC=examples/file.ino 64 | # - PLATFORMIO_CI_SRC=path/to/test/directory 65 | 66 | install: 67 | - pip install -U platformio 68 | - platformio lib -g install 1851 69 | - platformio update 70 | 71 | script: 72 | - platformio ci --lib="." --board=m5stack-core-esp32 73 | 74 | -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; http://docs.platformio.org/page/projectconf.html 10 | 11 | [platformio] 12 | default_envs = m5stack-core 13 | 14 | ; [env] 15 | ; platform = espressif32 16 | ; framework = arduino 17 | ; monitor_speed = 115200 18 | 19 | [env:m5stack-core] 20 | board = m5stack-core-esp32 21 | platform = espressif32 22 | framework = arduino 23 | monitor_speed = 115200 24 | lib_deps = m5stack/M5Unified 25 | 26 | [env:m5stack-core-with-aquestalk] 27 | board = m5stack-core-esp32 28 | platform = espressif32 29 | framework = arduino 30 | monitor_speed = 115200 31 | lib_deps = m5stack/M5Unified 32 | build_flags = 33 | -laquestalk 34 | -Llib/aquestalk-esp32/src/esp32 35 | 36 | [env:m5stack-fire] 37 | board = m5stack-fire 38 | platform = espressif32 39 | framework = arduino 40 | monitor_speed = 115200 41 | lib_deps = 42 | m5stack/M5Unified 43 | 44 | [env:m5stack-core2] 45 | board = m5stack-core2 46 | platform = espressif32 47 | framework = arduino 48 | monitor_speed = 115200 49 | lib_deps = 50 | m5stack/M5Unified 51 | 52 | [env:native] 53 | platform = native 54 | lib_deps = m5stack/M5Unified 55 | build_type = debug 56 | build_flags = -O0 -xc++ -std=c++14 -lSDL2 57 | -I"/usr/local/include/SDL2" ; for intel mac homebrew SDL2 58 | -L"/usr/local/lib" ; for intel mac homebrew SDL2 59 | -DM5GFX_SHOW_FRAME ; Display frame image. 60 | -DM5GFX_BACK_COLOR=0x222222u ; Color outside the frame image 61 | 62 | [env:native_arm] 63 | platform = native 64 | build_type = debug 65 | lib_deps = m5stack/M5Unified 66 | build_flags = -O0 -xc++ -std=c++14 -lSDL2 67 | -arch arm64 ; for arm mac 68 | -I"${sysenv.HOMEBREW_PREFIX}/include/SDL2" ; for arm mac homebrew SDL2 69 | -L"${sysenv.HOMEBREW_PREFIX}/lib" ; for arm mac homebrew SDL2 70 | -DM5GFX_SHOW_FRAME ; Display frame image. 71 | -DM5GFX_BACK_COLOR=0x222222u ; Color outside the frame image -------------------------------------------------------------------------------- /src/BatteryIcon.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Shinya Ishikawa. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full 3 | // license information. 4 | 5 | #ifndef BATTERYICON_H_ 6 | #define BATTERYICON_H_ 7 | #include 8 | #include 9 | #include "DrawContext.h" 10 | #include "Drawable.h" 11 | 12 | namespace m5avatar { 13 | 14 | class BatteryIcon final : public Drawable { 15 | private: 16 | void drawBatteryIcon(M5Canvas *spi, uint32_t x, uint32_t y, uint16_t fgcolor, uint16_t bgcolor, float offset, BatteryIconStatus batteryIconStatus, int32_t batteryLevel) { 17 | spi->drawRect(x, y + 5, 5, 5, fgcolor); 18 | spi->drawRect(x + 5, y, 30, 15, fgcolor); 19 | int battery_width = 30 * (float)(batteryLevel / 100.0f); 20 | spi->fillRect(x + 5 + 30 - battery_width, y, battery_width, 15, fgcolor); 21 | if (batteryIconStatus == BatteryIconStatus::charging) { 22 | spi->fillTriangle(x + 20, y, x + 15, y + 8, x + 20, y + 8, bgcolor); 23 | spi->fillTriangle(x + 18, y + 7, x + 18, y + 15, x + 23, y + 7, bgcolor); 24 | spi->drawLine(x + 20, y, x + 15, y + 8, fgcolor); 25 | spi->drawLine(x + 20, y, x + 20, y + 7, fgcolor); 26 | spi->drawLine(x + 18, y + 15, x + 23, y + 7, fgcolor); 27 | spi->drawLine(x + 18, y + 8, x + 18, y + 15, fgcolor); 28 | } 29 | } 30 | 31 | public: 32 | // constructor 33 | BatteryIcon() = default; 34 | ~BatteryIcon() = default; 35 | BatteryIcon(const BatteryIcon &other) = default; 36 | BatteryIcon &operator=(const BatteryIcon &other) = default; 37 | void draw(M5Canvas *spi, BoundingRect rect, DrawContext *ctx) override { 38 | if (ctx->getBatteryIconStatus() != BatteryIconStatus::invisible) { 39 | uint16_t primaryColor = ctx->getColorDepth() == 1 ? 1 : ctx->getColorPalette()->get(COLOR_PRIMARY); 40 | uint16_t bgColor = ctx->getColorDepth() == 1 ? ERACER_COLOR : ctx->getColorPalette()->get(COLOR_BACKGROUND); 41 | float offset = ctx->getBreath(); 42 | int32_t batteryLevel = ctx->getBatteryLevel(); 43 | drawBatteryIcon(spi, 285, 5, primaryColor, bgColor, -offset, ctx->getBatteryIconStatus(), batteryLevel); 44 | } 45 | }; 46 | 47 | }; 48 | 49 | } // namespace m5avatar 50 | 51 | #endif // BATTERYICON_H_ 52 | -------------------------------------------------------------------------------- /src/Face.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Shinya Ishikawa. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full 3 | // license information. 4 | 5 | #ifndef FACE_H_ 6 | #define FACE_H_ 7 | 8 | #include "Balloon.h" 9 | #include "BoundingRect.h" 10 | #include "Eye.h" 11 | #include "Eyeblow.h" 12 | #include "Mouth.h" 13 | #include "Effect.h" 14 | #include "BatteryIcon.h" 15 | 16 | namespace m5avatar { 17 | 18 | class Face { 19 | private: 20 | Drawable *mouth; 21 | Drawable *eyeR; 22 | Drawable *eyeL; 23 | Drawable *eyeblowR; 24 | Drawable *eyeblowL; 25 | BoundingRect *mouthPos; 26 | BoundingRect *eyeRPos; 27 | BoundingRect *eyeLPos; 28 | BoundingRect *eyeblowRPos; 29 | BoundingRect *eyeblowLPos; 30 | BoundingRect *boundingRect; 31 | M5Canvas *sprite; 32 | M5Canvas *tmpSprite; 33 | Balloon *b; 34 | Effect *h; 35 | BatteryIcon *battery; 36 | 37 | public: 38 | // constructor 39 | Face(); 40 | Face(Drawable *mouth, Drawable *eyeR, Drawable *eyeL, Drawable *eyeblowR, 41 | Drawable *eyeblowL); 42 | // TODO(meganetaaan): apply builder pattern 43 | Face(Drawable *mouth, BoundingRect *mouthPos, Drawable *eyeR, 44 | BoundingRect *eyeRPos, Drawable *eyeL, BoundingRect *eyeLPos, 45 | Drawable *eyeblowR, BoundingRect *eyeblowRPos, Drawable *eyeblowL, 46 | BoundingRect *eyeblowLPos); 47 | Face(Drawable *mouth, BoundingRect *mouthPos, Drawable *eyeR, 48 | BoundingRect *eyeRPos, Drawable *eyeL, BoundingRect *eyeLPos, 49 | Drawable *eyeblowR, BoundingRect *eyeblowRPos, Drawable *eyeblowL, 50 | BoundingRect *eyeblowLPos, 51 | BoundingRect *boundingRect, M5Canvas *spr, M5Canvas *tmpSpr); 52 | ~Face(); 53 | Face(const Face &other) = default; 54 | Face &operator=(const Face &other) = default; 55 | 56 | Drawable *getLeftEye(); 57 | Drawable *getRightEye(); 58 | 59 | // void setParts(PartsType p, Drawable parts); 60 | // Drawable *getParts(PartsType p); 61 | 62 | Drawable *getMouth(); 63 | BoundingRect *getBoundingRect(); 64 | 65 | void setLeftEye(Drawable *eye); 66 | void setRightEye(Drawable *eye); 67 | void setMouth(Drawable *mouth); 68 | void setLeftEyeblow(); 69 | void setRightEyeblow(); 70 | 71 | void draw(DrawContext *ctx); 72 | }; 73 | } // namespace m5avatar 74 | 75 | #endif // FACE_H_ 76 | -------------------------------------------------------------------------------- /src/Balloon.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Shinya Ishikawa. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full 3 | // license information. 4 | 5 | #ifndef BALLOON_H_ 6 | #define BALLOON_H_ 7 | #define LGFX_USE_V1 8 | #include 9 | #include "DrawContext.h" 10 | #include "Drawable.h" 11 | 12 | #ifndef ARDUINO 13 | #include 14 | typedef std::string String; 15 | #endif // ARDUINO 16 | 17 | const int16_t TEXT_HEIGHT = 8; 18 | const int16_t TEXT_SIZE = 2; 19 | const int16_t MIN_WIDTH = 40; 20 | const int cx = 240; 21 | const int cy = 220; 22 | 23 | namespace m5avatar { 24 | class Balloon final : public Drawable { 25 | public: 26 | // constructor 27 | Balloon() = default; 28 | ~Balloon() = default; 29 | Balloon(const Balloon &other) = default; 30 | Balloon &operator=(const Balloon &other) = default; 31 | void draw(M5Canvas *spi, BoundingRect rect, 32 | DrawContext *drawContext) override { 33 | String text = drawContext->getspeechText(); 34 | const lgfx::IFont *font = drawContext->getSpeechFont(); 35 | if (text.length() == 0) { 36 | return; 37 | } 38 | ColorPalette* cp = drawContext->getColorPalette(); 39 | uint16_t primaryColor = cp->get(COLOR_BALLOON_FOREGROUND); 40 | uint16_t backgroundColor = cp->get(COLOR_BALLOON_BACKGROUND); 41 | M5.Lcd.setTextSize(TEXT_SIZE); 42 | M5.Lcd.setTextDatum(MC_DATUM); 43 | spi->setTextSize(TEXT_SIZE); 44 | spi->setTextColor(primaryColor, backgroundColor); 45 | spi->setTextDatum(MC_DATUM); 46 | M5.Lcd.setFont(font); 47 | int textWidth = M5.Lcd.textWidth(text.c_str()); 48 | int textHeight = TEXT_HEIGHT * TEXT_SIZE; 49 | spi->fillEllipse(cx - 20, cy,textWidth + 2, textHeight * 2 + 2, 50 | primaryColor); 51 | spi->fillTriangle(cx - 62, cy - 42, cx - 8, cy - 10, cx - 41, cy - 8, 52 | primaryColor); 53 | spi->fillEllipse(cx - 20, cy, textWidth, textHeight * 2, 54 | backgroundColor); 55 | spi->fillTriangle(cx - 60, cy - 40, cx - 10, cy - 10, cx - 40, cy - 10, 56 | backgroundColor); 57 | spi->drawString(text.c_str(), cx - textWidth / 6 - 15, cy, font); // Continue printing from new x position 58 | } 59 | }; 60 | 61 | } // namespace m5avatar 62 | 63 | #endif // BALLOON_H_ 64 | -------------------------------------------------------------------------------- /src/Eye.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Shinya Ishikawa. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full 3 | // license information. 4 | 5 | #include "Eye.h" 6 | 7 | namespace m5avatar { 8 | 9 | Eye::Eye(uint16_t x, uint16_t y, uint16_t r, bool isLeft) : Eye(r, isLeft) {} 10 | 11 | Eye::Eye(uint16_t r, bool isLeft) : r{r}, isLeft{isLeft} {} 12 | 13 | void Eye::draw(M5Canvas *spi, BoundingRect rect, DrawContext *ctx) { 14 | Expression exp = ctx->getExpression(); 15 | uint32_t x = rect.getCenterX(); 16 | uint32_t y = rect.getCenterY(); 17 | Gaze g = this->isLeft ? ctx->getLeftGaze() : ctx->getRightGaze(); 18 | float openRatio = 19 | this->isLeft ? ctx->getLeftEyeOpenRatio() : ctx->getRightEyeOpenRatio(); 20 | uint32_t offsetX = g.getHorizontal() * 3; 21 | uint32_t offsetY = g.getVertical() * 3; 22 | uint16_t primaryColor = ctx->getColorDepth() == 1 23 | ? 1 24 | : ctx->getColorPalette()->get(COLOR_PRIMARY); 25 | uint16_t backgroundColor = 26 | ctx->getColorDepth() == 1 ? 0 27 | : ctx->getColorPalette()->get(COLOR_BACKGROUND); 28 | 29 | if (openRatio > 0) { 30 | spi->fillCircle(x + offsetX, y + offsetY, r, primaryColor); 31 | // TODO(meganetaaan): Refactor 32 | if (exp == Expression::Angry || exp == Expression::Sad) { 33 | int x0, y0, x1, y1, x2, y2; 34 | x0 = x + offsetX - r; 35 | y0 = y + offsetY - r; 36 | x1 = x0 + r * 2; 37 | y1 = y0; 38 | x2 = !isLeft != !(exp == Expression::Sad) ? x0 : x1; 39 | y2 = y0 + r; 40 | spi->fillTriangle(x0, y0, x1, y1, x2, y2, backgroundColor); 41 | } 42 | if (exp == Expression::Happy || exp == Expression::Sleepy) { 43 | int x0, y0, w, h; 44 | x0 = x + offsetX - r; 45 | y0 = y + offsetY - r; 46 | w = r * 2 + 4; 47 | h = r + 2; 48 | if (exp == Expression::Happy) { 49 | y0 += r; 50 | spi->fillCircle(x + offsetX, y + offsetY, r / 1.5, backgroundColor); 51 | } 52 | spi->fillRect(x0, y0, w, h, backgroundColor); 53 | } 54 | } else { 55 | int x1 = x - r + offsetX; 56 | int y1 = y - 2 + offsetY; 57 | int w = r * 2; 58 | int h = 4; 59 | spi->fillRect(x1, y1, w, h, primaryColor); 60 | } 61 | } 62 | } // namespace m5avatar 63 | -------------------------------------------------------------------------------- /src/Eyebrows.cpp: -------------------------------------------------------------------------------- 1 | #include "Eyebrows.hpp" 2 | 3 | namespace m5avatar { 4 | 5 | BaseEyebrow::BaseEyebrow(bool is_left) : BaseEyebrow(30, 20, is_left) {} 6 | 7 | BaseEyebrow::BaseEyebrow(uint16_t width, uint16_t height, bool is_left) { 8 | this->width_ = width; 9 | this->height_ = height; 10 | this->is_left_ = is_left; 11 | } 12 | 13 | void BaseEyebrow::update(M5Canvas *canvas, BoundingRect rect, 14 | DrawContext *ctx) { 15 | // common process for all standard eyebrows 16 | // update drawing parameters 17 | ColorPalette *cp = ctx->getColorPalette(); 18 | primary_color_ = ctx->getColorDepth() == 1 ? 1 : cp->get(COLOR_PRIMARY); 19 | secondary_color_ = ctx->getColorDepth() == 1 20 | ? 1 21 | : ctx->getColorPalette()->get(COLOR_SECONDARY); 22 | background_color_ = 23 | ctx->getColorDepth() == 1 ? ERACER_COLOR : cp->get(COLOR_BACKGROUND); 24 | center_x_ = rect.getCenterX(); 25 | center_y_ = rect.getCenterY(); 26 | expression_ = ctx->getExpression(); 27 | } 28 | 29 | void EllipseEyebrow::draw(M5Canvas *canvas, BoundingRect rect, 30 | DrawContext *ctx) { 31 | this->update(canvas, rect, ctx); 32 | if (width_ == 0 || height_ == 0) { 33 | return; // draw nothing 34 | } 35 | 36 | canvas->fillEllipse(center_x_, center_y_, this->width_ / 2, 37 | this->height_ / 2, primary_color_); 38 | } 39 | 40 | void BowEyebrow::draw(M5Canvas *canvas, BoundingRect rect, DrawContext *ctx) { 41 | this->update(canvas, rect, ctx); 42 | uint8_t thickness = 4; 43 | 44 | float angle0 = is_left_ ? 180.0f + 35.0f : 180.0f + 45.0f; 45 | float stroke_angle = 100.0f; 46 | canvas->fillArc(center_x_, center_y_, width_ / 2, width_ / 2 - thickness, 47 | angle0, angle0 + stroke_angle, primary_color_); 48 | } 49 | 50 | void RectEyebrow::draw(M5Canvas *canvas, BoundingRect rect, DrawContext *ctx) { 51 | this->update(canvas, rect, ctx); 52 | 53 | if (width_ == 0 || height_ == 0) { 54 | return; 55 | } 56 | float angle = 0.0f; 57 | if (expression_ == Expression::Angry) { 58 | angle = is_left_ ? -M_PI / 6.0f : M_PI / 6.0f; 59 | } 60 | if (expression_ == Expression::Sad) { 61 | angle = is_left_ ? M_PI / 6.0f : -M_PI / 6.0f; 62 | } 63 | 64 | fillRotatedRect(canvas, center_x_, center_y_, width_, height_, angle, 65 | primary_color_); 66 | } 67 | 68 | } // namespace m5avatar 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Manual ### 2 | lib/aqtkpico_esp32 3 | lib/aquestalk_esp32 4 | lib/aquestalk-esp32 5 | src/*.ino 6 | const.h 7 | /tmp/ 8 | 9 | ### gitignore.io 10 | # Created by https://www.toptal.com/developers/gitignore/api/platformio,visualstudiocode,windows,macos,linux 11 | # Edit at https://www.toptal.com/developers/gitignore?templates=platformio,visualstudiocode,windows,macos,linux 12 | 13 | ### Linux ### 14 | *~ 15 | 16 | # temporary files which can be created if a process still has a handle open of a deleted file 17 | .fuse_hidden* 18 | 19 | # KDE directory preferences 20 | .directory 21 | 22 | # Linux trash folder which might appear on any partition or disk 23 | .Trash-* 24 | 25 | # .nfs files are created when an open file is removed but is still being accessed 26 | .nfs* 27 | 28 | ### macOS ### 29 | # General 30 | .DS_Store 31 | .AppleDouble 32 | .LSOverride 33 | 34 | # Icon must end with two \r 35 | Icon 36 | 37 | 38 | # Thumbnails 39 | ._* 40 | 41 | # Files that might appear in the root of a volume 42 | .DocumentRevisions-V100 43 | .fseventsd 44 | .Spotlight-V100 45 | .TemporaryItems 46 | .Trashes 47 | .VolumeIcon.icns 48 | .com.apple.timemachine.donotpresent 49 | 50 | # Directories potentially created on remote AFP share 51 | .AppleDB 52 | .AppleDesktop 53 | Network Trash Folder 54 | Temporary Items 55 | .apdisk 56 | 57 | ### macOS Patch ### 58 | # iCloud generated files 59 | *.icloud 60 | 61 | ### PlatformIO ### 62 | .pioenvs 63 | .piolibdeps 64 | .clang_complete 65 | .gcc-flags.json 66 | .pio 67 | 68 | ### VisualStudioCode ### 69 | .vscode/* 70 | !.vscode/settings.json 71 | !.vscode/tasks.json 72 | # !.vscode/launch.json 73 | !.vscode/extensions.json 74 | !.vscode/*.code-snippets 75 | 76 | # Local History for Visual Studio Code 77 | .history/ 78 | 79 | # Built Visual Studio Code Extensions 80 | *.vsix 81 | 82 | ### VisualStudioCode Patch ### 83 | # Ignore all local history of files 84 | .history 85 | .ionide 86 | 87 | ### Windows ### 88 | # Windows thumbnail cache files 89 | Thumbs.db 90 | Thumbs.db:encryptable 91 | ehthumbs.db 92 | ehthumbs_vista.db 93 | 94 | # Dump file 95 | *.stackdump 96 | 97 | # Folder config file 98 | [Dd]esktop.ini 99 | 100 | # Recycle Bin used on file shares 101 | $RECYCLE.BIN/ 102 | 103 | # Windows Installer files 104 | *.cab 105 | *.msi 106 | *.msix 107 | *.msm 108 | *.msp 109 | 110 | # Windows shortcuts 111 | *.lnk 112 | 113 | # End of https://www.toptal.com/developers/gitignore/api/platformio,visualstudiocode,windows,macos,linux -------------------------------------------------------------------------------- /src/DrawingUtils.cpp: -------------------------------------------------------------------------------- 1 | #include "DrawingUtils.hpp" 2 | 3 | namespace m5avatar { 4 | void rotatePoint(float &x, float &y, float angle) { 5 | float tmp; 6 | tmp = x * cosf(angle) - y * sinf(angle); 7 | x = tmp; 8 | tmp = x * sinf(angle) + y * cosf(angle); 9 | y = tmp; 10 | } 11 | 12 | void rotatePointAround(float &x, float &y, float angle, float cx, float cy) { 13 | float tmp_x = x - cx; 14 | float tmp_y = y - cy; 15 | rotatePoint(tmp_x, tmp_y, angle); // rotate around origin 16 | x = tmp_x + cx; 17 | y = tmp_y + cy; 18 | } 19 | 20 | void fillRotatedRect(M5Canvas *canvas, uint16_t cx, uint16_t cy, uint16_t w, 21 | uint16_t h, float angle, uint16_t color) { 22 | float top_left_x = cx - w / 2; 23 | float top_left_y = cy - h / 2; 24 | 25 | float top_right_x = cx + w / 2; 26 | float top_right_y = cy - h / 2; 27 | 28 | float bottom_left_x = cx - w / 2; 29 | float bottom_left_y = cy + h / 2; 30 | 31 | float bottom_right_x = cx + w / 2; 32 | float bottom_right_y = cy + h / 2; 33 | 34 | // rotate vertex 35 | rotatePointAround(top_left_x, top_left_y, angle, cx, cy); 36 | rotatePointAround(top_right_x, top_right_y, angle, cx, cy); 37 | rotatePointAround(bottom_left_x, bottom_left_y, angle, cx, cy); 38 | rotatePointAround(bottom_right_x, bottom_right_y, angle, cx, cy); 39 | 40 | canvas->fillTriangle(top_left_x, top_left_y, top_right_x, top_right_y, 41 | bottom_right_x, bottom_right_y, color); 42 | canvas->fillTriangle(top_left_x, top_left_y, bottom_right_x, bottom_right_y, 43 | bottom_left_x, bottom_left_y, color); 44 | } 45 | 46 | void fillRectRotatedAround(M5Canvas *canvas, float top_left_x, float top_left_y, 47 | float bottom_right_x, float bottom_right_y, 48 | float angle, uint16_t cx, uint16_t cy, 49 | uint16_t color) { 50 | float top_right_x = bottom_right_x; 51 | float top_right_y = top_left_y; 52 | 53 | float bottom_left_x = top_left_x; 54 | float bottom_left_y = bottom_right_y; 55 | 56 | rotatePointAround(top_left_x, top_left_y, angle, cx, cy); 57 | rotatePointAround(top_right_x, top_right_y, angle, cx, cy); 58 | rotatePointAround(bottom_left_x, bottom_left_y, angle, cx, cy); 59 | rotatePointAround(bottom_right_x, bottom_right_y, angle, cx, cy); 60 | 61 | canvas->fillTriangle(top_left_x, top_left_y, top_right_x, top_right_y, 62 | bottom_right_x, bottom_right_y, color); 63 | canvas->fillTriangle(top_left_x, top_left_y, bottom_right_x, bottom_right_y, 64 | bottom_left_x, bottom_left_y, color); 65 | } 66 | 67 | } // namespace m5avatar -------------------------------------------------------------------------------- /docs/FaceCustomizationGuideline.md: -------------------------------------------------------------------------------- 1 | # Customization Guideline 2 | 3 | ## Anatomy of Face 4 | 5 | anatomy 6 | 7 | `m5avatar::Face` consists of five components: 8 | 9 | - left eye 10 | - right eye 11 | - mouth 12 | - left eyebrow 13 | - right eyebrow 14 | 15 | You can customize a face with replacing the components. 16 | [examples/face-and-color/face-and-color.ino](../examples/face-and-color/face-and-color.ino) is an example of using and customizing faces. 17 | 18 | ## Face templates 19 | Pre-assembled faces are available and defined in [src/faces/FaceTemplates.hpp](../src/faces/FaceTemplates.hpp) 20 | 21 | |preview|face|eye|mouth|eyebrow|notes| 22 | |:-:|:-|:-|:-|:-|:-| 23 | |SimpleFace|`SimpleFace`| `EllipseEye` | `RectMouth` | -- | Alternative implementation of (Native) `Face` 24 | |DoggyFace|`DoggyFace`|`DoggyEye`|`DoggyMouth`|`RectEyebrow`|Alternative implementation of `DogFace`| 25 | |OmegaFace|`OmegaFace`|`EllipseEye`|`OmegaMouth`|--|| 26 | |GirlyFace|`GirlyFace`|`GirlyEye`|`UShapeMouth`|`EllipseEyebrow`|| 27 | 28 | 29 | 30 | 31 | 32 | ## Eyes 33 | 34 | Predefined eye components are in [src/Eyes.hpp](../src/Eyes.hpp) and [src/Eyes.cpp](../src/Eyes.cpp) 35 | 36 | - `EllipseEye` (Extended implement of `Eye`) 37 | - `GirlyEye` 38 | - `PinkDemonEye` 39 | - `DoggyEye` (Alternative implement of `DogEye`) 40 | 41 | 42 | 43 | ## Mouth 44 | 45 | Predefined mouth components are in [src/Mouths.hpp](../src/Mouths.hpp) and [src/Mouths.cpp](../src/Mouths.cpp) 46 | 47 | - `RectMouth` (Alternative implement of `Mouth`) 48 | - `OmegaMouth` 49 | - `UShapeMouth` 50 | - `DoggyMouth` (Alternative implement of `DogMouth`) 51 | 52 | ## Eyebrows 53 | 54 | Predefined mouth components are in [src/EyeBrow.hpp](../src/EyeBrow.hpp) and [src/EyeBrow.cpp](../src/EyeBrow.cpp) 55 | 56 | - `RectEyebrow` 57 | - `EllipseEyebrow` 58 | - `BowEyebrow` 59 | 60 | 61 | ## Notes 62 | 63 | ### Native files 64 | - [src/Eye.h](../src/Eye.h) and [src/Eye.cpp](../src/Eye.cpp) : Native `Eye` code 65 | - [src/Mouth.h](../src/Mouth.h) and [src/Mouth.cpp](../src/Mouth.cpp) : Native `Mouth` code 66 | - [src/Eyeblow.h](../src/Eyeblow.h) and [src/Eyeblow.cpp](../src/Eyeblow.cpp) : Native `Eyeblow` code for Eyebrow 67 | - [src/faces/DogFace.h](../src/faces/DogFace.h) : Native `DogFace` code 68 | -------------------------------------------------------------------------------- /src/DrawContext.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Shinya Ishikawa. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full 3 | // license information. 4 | 5 | #ifndef DRAWCONTEXT_H_ 6 | #define DRAWCONTEXT_H_ 7 | 8 | #define ERACER_COLOR 0x0000 9 | 10 | #include "ColorPalette.h" 11 | #include "Expression.h" 12 | #include "Gaze.h" 13 | #include "M5GFX.h" 14 | 15 | #ifndef ARDUINO 16 | #include 17 | typedef std::string String; 18 | #endif // ARDUINO 19 | 20 | namespace m5avatar { 21 | enum BatteryIconStatus { discharging, charging, invisible, unknown }; 22 | class DrawContext { 23 | private: 24 | Expression expression; 25 | float breath; 26 | 27 | // left eye 28 | Gaze leftGaze; 29 | float leftEyeOpenRatio; 30 | // right eye 31 | Gaze rightGaze; 32 | float rightEyeOpenRatio; 33 | 34 | float mouthOpenRatio; 35 | 36 | ColorPalette* const palette; 37 | String speechText; 38 | float rotation = 0.0; 39 | float scale = 1.0; 40 | int colorDepth = 1; 41 | BatteryIconStatus batteryIconStatus = BatteryIconStatus::invisible; 42 | int32_t batteryLevel = 0; 43 | const lgfx::IFont* speechFont = 44 | nullptr; // = &fonts::lgfxJapanGothicP_16; // = &fonts::efontCN_10; 45 | 46 | public: 47 | DrawContext() = delete; 48 | DrawContext(Expression expression, float breath, ColorPalette* const palette, 49 | Gaze rightGaze, float rightEyeOpenRatio, Gaze leftGaze, 50 | float leftEyeOpenRatio, float mouthOpenRatio, String speechText, 51 | BatteryIconStatus batteryIconStatus, int32_t batteryLevel, 52 | const lgfx::IFont* speechFont); 53 | DrawContext(Expression expression, float breath, ColorPalette* const palette, 54 | Gaze rightGaze, float rightEyeOpenRatio, Gaze leftGaze, 55 | float leftEyeOpenRatio, float mouthOpenRatio, String speechText, 56 | float rotation, float scale, int colorDepth, 57 | BatteryIconStatus batteryIconStatus, int32_t batteryLevel, 58 | const lgfx::IFont* speechFont); 59 | ~DrawContext() = default; 60 | DrawContext(const DrawContext& other) = delete; 61 | DrawContext& operator=(const DrawContext& other) = delete; 62 | Expression getExpression() const; 63 | float getBreath() const; 64 | float getRightEyeOpenRatio() const; 65 | Gaze getRightGaze() const; 66 | float getLeftEyeOpenRatio() const; 67 | Gaze getLeftGaze() const; 68 | float getMouthOpenRatio() const; 69 | float getScale() const; 70 | float getRotation() const; 71 | ColorPalette* const getColorPalette() const; 72 | String getspeechText() const; 73 | int getColorDepth() const; 74 | BatteryIconStatus getBatteryIconStatus() const; 75 | int32_t getBatteryLevel() const; 76 | const lgfx::IFont* getSpeechFont() const; 77 | }; 78 | } // namespace m5avatar 79 | 80 | #endif // DRAWCONTEXT_H_ 81 | -------------------------------------------------------------------------------- /src/DrawContext.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Shinya Ishikawa. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full 3 | // license information. 4 | 5 | #include "DrawContext.h" 6 | namespace m5avatar { 7 | 8 | // DrawContext 9 | DrawContext::DrawContext(Expression expression, float breath, 10 | ColorPalette* const palette, Gaze rightGaze, 11 | float rightEyeOpenRatio, Gaze leftGaze, 12 | float leftEyeOpenRatio, float mouthOpenRatio, 13 | String speechText, BatteryIconStatus batteryIconStatus, 14 | int32_t batteryLevel, const lgfx::IFont* speechFont) 15 | : DrawContext(expression, breath, palette, rightGaze, rightEyeOpenRatio, 16 | leftGaze, leftEyeOpenRatio, mouthOpenRatio, speechText, 0, 1, 17 | 1, BatteryIconStatus::invisible, 0, speechFont){}; 18 | 19 | DrawContext::DrawContext(Expression expression, float breath, 20 | ColorPalette* const palette, Gaze rightGaze, 21 | float rightEyeOpenRatio, Gaze leftGaze, 22 | float leftEyeOpenRatio, float mouthOpenRatio, 23 | String speechText, float rotation, float scale, 24 | int colorDepth, BatteryIconStatus batteryIconStatus, 25 | int32_t batteryLevel, const lgfx::IFont* speechFont) 26 | : expression{expression}, 27 | breath{breath}, 28 | rightGaze{rightGaze}, 29 | rightEyeOpenRatio{rightEyeOpenRatio}, 30 | leftGaze{leftGaze}, 31 | leftEyeOpenRatio{leftEyeOpenRatio}, 32 | mouthOpenRatio{mouthOpenRatio}, 33 | palette{palette}, 34 | speechText{speechText}, 35 | rotation{rotation}, 36 | scale{scale}, 37 | colorDepth{colorDepth}, 38 | batteryIconStatus(batteryIconStatus), 39 | batteryLevel(batteryLevel), 40 | speechFont{speechFont} {} 41 | 42 | Expression DrawContext::getExpression() const { return expression; } 43 | 44 | float DrawContext::getMouthOpenRatio() const { return mouthOpenRatio; } 45 | 46 | Gaze DrawContext::getLeftGaze() const { return leftGaze; } 47 | 48 | float DrawContext::getLeftEyeOpenRatio() const { return leftEyeOpenRatio; } 49 | 50 | Gaze DrawContext::getRightGaze() const { return rightGaze; } 51 | 52 | float DrawContext::getRightEyeOpenRatio() const { return rightEyeOpenRatio; } 53 | 54 | float DrawContext::getBreath() const { return breath; } 55 | 56 | float DrawContext::getRotation() const { return rotation; } 57 | 58 | float DrawContext::getScale() const { return scale; } 59 | 60 | String DrawContext::getspeechText() const { return speechText; } 61 | 62 | ColorPalette* const DrawContext::getColorPalette() const { return palette; } 63 | 64 | int DrawContext::getColorDepth() const { return colorDepth; } 65 | 66 | const lgfx::IFont* DrawContext::getSpeechFont() const { return speechFont; } 67 | 68 | BatteryIconStatus DrawContext::getBatteryIconStatus() const { 69 | return batteryIconStatus; 70 | } 71 | 72 | int32_t DrawContext::getBatteryLevel() const { return batteryLevel; } 73 | 74 | } // namespace m5avatar 75 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at ishikawa.s.1027@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /README_ja.md: -------------------------------------------------------------------------------- 1 | ## お知らせ 2 | 3 | 本リポジトリは、v0.10.0にて開発を一時凍結します。(管理者不在のため)プルリクエストやISSUEを頂いても対応できません。 4 | 5 | - 今後は関連ライブラリのバージョンアップにより不具合がおきた場合に更新するかもしれません。 6 | - 管理していただける人がいたら是非お願いします。 7 | 8 | # M5Stack-Avatar 9 | 10 | [![Powered By PlatformIO](https://img.shields.io/badge/powered-PlatformIO-brightgreen)](https://platformio.org/) 11 | [![Build Status](https://travis-ci.com/meganetaaan/m5stack-avatar.svg?branch=master)](https://travis-ci.com/meganetaaan/m5stack-avatar) 12 | 13 | ![M5Stack-Avatar](docs/image/avatar.gif) 14 | 15 | 動画: https://www.youtube.com/watch?v=C1Hj9kfY5qc 16 | 17 | [ENGLISH](README.md) 18 | 19 | ## 機能 20 | 21 | * :neutral_face: アバターの顔を表示します。 22 | * :smile: 表情を変えられます(喜び, 怒り, 悲しみ など)。 23 | * :smiley_cat: アバターの顔を独自にカスタマイズできます。 24 | * :kiss: リップシンク(音声に合わせて口を動かす)できます。 25 | * :art: 色を変更できます。 26 | * :arrows_clockwise: 顔を移動、拡大、回転できます。 27 | * :two: M5Stack Core2に対応。 28 | 29 | ## インストール 30 | 31 | ### 前提 32 | 33 | * [Getting Started: Installing the USB Driver](http://www.m5stack.com/assets/docs/)にしたがって、USBドライバがインストールされている 34 | * 何らかの開発環境がセットアップされている 35 | * このライブラリはArduino IDE, Platform IOで動作確認しています。 36 | 37 | ### Arduino IDEを使う場合 38 | 39 | * Arduino IDEのメニューから"Sketch > Include Library > Manage Libraries..."を選択します 40 | * 検索窓("Filter your search...")に"m5stack avatar" と入力します 41 | * 検索結果から"M5Stack_Avatar"を選択し、"Install"ボタンをクリックします。 42 | * M5Stack-Avatarライブラリがインストールされ、使用可能になります。 43 | 44 | ### Platform IOを使う場合 45 | 46 | * cliからPlatform IOプロジェクトを初期化します。 47 | ```sh 48 | mkdir my-avatar 49 | cd my-avatar 50 | platformio init -d . -b m5stack-core-esp32 51 | ``` 52 | * cliからライブラリをインストールします。 53 | ```sh 54 | platformio lib install M5Unified 55 | platformio lib install M5Stack-Avatar 56 | ``` 57 | * ライブラリがプロジェクトの.piolibdeps配下にダウンロードされ、使用可能になります。 58 | 59 | ## 使い方 60 | 61 | ```cpp 62 | 63 | #include 64 | #include 65 | 66 | using namespace m5avatar; 67 | 68 | Avatar avatar; 69 | 70 | void setup() 71 | { 72 | M5.begin(); 73 | avatar.init(); // 描画を開始します。 74 | } 75 | 76 | void loop() 77 | { 78 | // アバターの描画は別のスレッドで行われるので、 79 | // loopループの中で毎回描画をする必要はありません。 80 | } 81 | ``` 82 | 83 | ### リップシンク機能を使う場合 84 | 85 | * AquesTalk-ESP32 をセットアップします(http://blog-yama.a-quest.com/?eid=970195) 86 | * (漢字かな混じり文から音声出力する場合)辞書データをあらかじめmicroSDカードにコピーしてください 87 | * 記事中にあるAquesTalkTTSのソースは本ライブラリに同梱されているため不要です 88 | 89 | * 下記コード例のように記述すると、出力に合わせてアバターの口が動きます 90 | 91 | ```cpp 92 | #include 93 | #include 94 | #include 95 | #include 96 | 97 | using namespace m5avatar; 98 | 99 | // AquesTalk のライセンスキー 100 | // NULLや誤った値を指定すると単に無視されます 101 | const char* AQUESTALK_KEY = "XXXX-XXXX-XXXX-XXXX"; 102 | Avatar avatar; 103 | 104 | void setup() { 105 | int iret; 106 | M5.begin(); 107 | // 漢字かな混じり文から音声出力する場合(辞書ファイルが必要) 108 | // iret = TTS.createK(AQUESTALK_KEY); 109 | iret = TTS.create(AQUESTALK_KEY); 110 | avatar.init(); 111 | avatar.addTask(lipSync, "lipSync"); 112 | } 113 | 114 | void loop() { 115 | M5.update(); 116 | if (M5.BtnA.wasPressed()) { 117 | // 漢字かな混じり文から音声出力する場合 118 | // TTS.play("こんにちは。", 80); 119 | TTS.play("konnichiwa", 80); 120 | } 121 | } 122 | 123 | ``` 124 | 125 | ### その他の使い方 126 | 127 | `examples` ディレクトリを参照ください。 128 | 129 | ### 0.7.x から 0.8.x への移行 130 | 131 | `M5Stack-Avatar` は現在、M5Stackシリーズの機能を統合したライブラリである `M5Unified` に依存しています。 132 | バージョン0.8.0以降では、アバターを使ったスケッチは`M5Stack.h`や`M5Core2.h`ではなく`M5Unified.h`をインクルードするようにしてください。 133 | 134 | ### v0.10.0 の注意事項 135 | 136 | アプリケーションによりaddTask()で追加したタスクのスタックサイズが足りなくなりリブートする場合があります。その場合は、該当タスクのスタックサイズを増やしてください。 -------------------------------------------------------------------------------- /examples/face-and-color/face-and-color.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | 6 | using namespace m5avatar; 7 | 8 | Avatar avatar; 9 | 10 | Face* faces[6]; 11 | const int num_faces = sizeof(faces) / sizeof(Face*); 12 | int face_idx = 0; // face index 13 | 14 | const Expression expressions[] = {Expression::Angry, Expression::Sleepy, 15 | Expression::Happy, Expression::Sad, 16 | Expression::Doubt, Expression::Neutral}; 17 | const int num_expressions = sizeof(expressions) / sizeof(Expression); 18 | int idx = 0; 19 | 20 | ColorPalette* color_palettes[5]; 21 | const int num_palettes = sizeof(color_palettes) / sizeof(ColorPalette*); 22 | int palette_idx = 0; 23 | 24 | bool isShowingQR = false; 25 | 26 | // an example of customizing 27 | class MyCustomFace : public Face { 28 | public: 29 | MyCustomFace() 30 | : Face(new UShapeMouth(44, 44, 0, 16), new BoundingRect(222, 160), 31 | // right eye, second eye arg is center position of eye 32 | new EllipseEye(32, 32, false), new BoundingRect(163, 64), 33 | // left eye 34 | new EllipseEye(32, 32, true), new BoundingRect(163, 256), 35 | // right eyebrow 36 | // BowEyebrow's origin is the center of bow (arc) 37 | new BowEyebrow(64, 20, false), 38 | new BoundingRect(163, 64), // (y,x) 39 | // left eyebrow 40 | new BowEyebrow(64, 20, true), new BoundingRect(163, 256)) {} 41 | }; 42 | 43 | void setup() { 44 | M5.begin(); 45 | M5.Lcd.setBrightness(30); 46 | M5.Lcd.clear(); 47 | 48 | faces[0] = avatar.getFace(); // native face 49 | faces[1] = new DoggyFace(); 50 | faces[2] = new OmegaFace(); 51 | faces[3] = new GirlyFace(); 52 | faces[4] = new PinkDemonFace(); 53 | faces[5] = new MyCustomFace(); 54 | 55 | color_palettes[0] = new ColorPalette(); 56 | color_palettes[1] = new ColorPalette(); 57 | color_palettes[2] = new ColorPalette(); 58 | color_palettes[3] = new ColorPalette(); 59 | color_palettes[4] = new ColorPalette(); 60 | color_palettes[1]->set(COLOR_PRIMARY, 61 | M5.Lcd.color24to16(0x383838)); // eye 62 | color_palettes[1]->set(COLOR_BACKGROUND, 63 | M5.Lcd.color24to16(0xfac2a8)); // skin 64 | color_palettes[1]->set(COLOR_SECONDARY, 65 | TFT_PINK); // cheek 66 | color_palettes[2]->set(COLOR_PRIMARY, TFT_YELLOW); 67 | color_palettes[2]->set(COLOR_BACKGROUND, TFT_DARKCYAN); 68 | color_palettes[3]->set(COLOR_PRIMARY, TFT_DARKGREY); 69 | color_palettes[3]->set(COLOR_BACKGROUND, TFT_WHITE); 70 | color_palettes[4]->set(COLOR_PRIMARY, TFT_RED); 71 | color_palettes[4]->set(COLOR_BACKGROUND, TFT_PINK); 72 | 73 | avatar.init(8); // start drawing w/ 8bit color mode 74 | avatar.setColorPalette(*color_palettes[0]); 75 | } 76 | 77 | void loop() { 78 | M5.update(); 79 | // M5Stack Core's button layout: 80 | // ----------- 81 | // | | 82 | // | | 83 | // ----------- 84 | // [A] [B] [C] 85 | if (M5.BtnA.wasPressed()) { 86 | avatar.setFace(faces[face_idx]); 87 | face_idx = (face_idx + 1) % num_faces; // loop index 88 | } 89 | if (M5.BtnB.wasPressed()) { 90 | avatar.setColorPalette(*color_palettes[palette_idx]); 91 | palette_idx = (palette_idx + 1) % num_palettes; 92 | } 93 | if (M5.BtnC.wasPressed()) { 94 | avatar.setExpression(expressions[idx]); 95 | idx = (idx + 1) % num_expressions; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/faces/DogFace.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Shinya Ishikawa. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full 3 | // license information. 4 | 5 | #ifndef FACES_DOGFACE_H_ 6 | #define FACES_DOGFACE_H_ 7 | 8 | #include // TODO(meganetaaan): include only the Sprite function not a whole library 9 | 10 | #include "../BoundingRect.h" 11 | #include "../DrawContext.h" 12 | #include "../Drawable.h" 13 | #include "../Face.h" 14 | 15 | namespace m5avatar { 16 | class DogEye : public Drawable { 17 | void draw(M5Canvas *spi, BoundingRect rect, DrawContext *ctx) { 18 | uint32_t cx = rect.getCenterX(); 19 | uint32_t cy = rect.getCenterY(); 20 | Gaze g = ctx->getLeftGaze(); 21 | ColorPalette *cp = ctx->getColorPalette(); 22 | uint16_t primaryColor = 23 | ctx->getColorDepth() == 1 ? 1 : cp->get(COLOR_PRIMARY); 24 | uint16_t backgroundColor = ctx->getColorDepth() == 1 25 | ? ERACER_COLOR 26 | : cp->get(COLOR_BACKGROUND); 27 | uint32_t offsetX = g.getHorizontal() * 8; 28 | uint32_t offsetY = g.getVertical() * 5; 29 | float eor = ctx->getLeftEyeOpenRatio(); 30 | 31 | if (eor == 0) { 32 | // eye closed 33 | spi->fillRect(cx - 15, cy - 2, 30, 4, primaryColor); 34 | return; 35 | } 36 | spi->fillEllipse(cx, cy, 30, 25, primaryColor); 37 | spi->fillEllipse(cx, cy, 28, 23, backgroundColor); 38 | 39 | spi->fillEllipse(cx + offsetX, cy + offsetY, 18, 18, primaryColor); 40 | spi->fillEllipse(cx + offsetX - 3, cy + offsetY - 3, 3, 3, 41 | backgroundColor); 42 | } 43 | }; 44 | 45 | class DogMouth : public Drawable { 46 | private: 47 | uint16_t minWidth; 48 | uint16_t maxWidth; 49 | uint16_t minHeight; 50 | uint16_t maxHeight; 51 | 52 | public: 53 | DogMouth() : DogMouth(50, 90, 4, 60) {} 54 | DogMouth(uint16_t minWidth, uint16_t maxWidth, uint16_t minHeight, 55 | uint16_t maxHeight) 56 | : minWidth{minWidth}, 57 | maxWidth{maxWidth}, 58 | minHeight{minHeight}, 59 | maxHeight{maxHeight} {} 60 | void draw(M5Canvas *spi, BoundingRect rect, DrawContext *ctx) { 61 | uint16_t primaryColor = 62 | ctx->getColorDepth() == 1 63 | ? 1 64 | : ctx->getColorPalette()->get(COLOR_PRIMARY); 65 | uint16_t backgroundColor = 66 | ctx->getColorDepth() == 1 67 | ? ERACER_COLOR 68 | : ctx->getColorPalette()->get(COLOR_BACKGROUND); 69 | uint32_t cx = rect.getCenterX(); 70 | uint32_t cy = rect.getCenterY(); 71 | float openRatio = ctx->getMouthOpenRatio(); 72 | uint32_t h = minHeight + (maxHeight - minHeight) * openRatio; 73 | uint32_t w = minWidth + (maxWidth - minWidth) * (1 - openRatio); 74 | if (h > minHeight) { 75 | spi->fillEllipse(cx, cy, w / 2, h / 2, primaryColor); 76 | spi->fillEllipse(cx, cy, w / 2 - 4, h / 2 - 4, TFT_RED); 77 | spi->fillRect(cx - w / 2, cy - h / 2, w, h / 2, backgroundColor); 78 | } 79 | spi->fillEllipse(cx, cy - 15, 10, 6, primaryColor); 80 | spi->fillEllipse(cx - 28, cy, 30, 15, primaryColor); 81 | spi->fillEllipse(cx + 28, cy, 30, 15, primaryColor); 82 | spi->fillEllipse(cx - 29, cy - 4, 27, 15, backgroundColor); 83 | spi->fillEllipse(cx + 29, cy - 4, 27, 15, backgroundColor); 84 | } 85 | }; 86 | 87 | class DogFace : public Face { 88 | public: 89 | DogFace() 90 | : Face(new DogMouth(), new BoundingRect(168, 163), new DogEye(), 91 | new BoundingRect(103, 80), new DogEye(), 92 | new BoundingRect(106, 240), new Eyeblow(15, 2, false), 93 | new BoundingRect(67, 96), new Eyeblow(15, 2, true), 94 | new BoundingRect(72, 230)) {} 95 | }; 96 | 97 | } // namespace m5avatar 98 | 99 | #endif // FACES_DOGFACE_H_ 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Notice 2 | 3 | This repository is temporarily frozen at v0.10.0. (Due to the absence of the administrator) We cannot respond to pull requests or ISSUE received. 4 | 5 | - We may update this repository in the future when we find bugs due to version upgrades of related libraries. 6 | - If there is anyone who can maintain this repository, please let us know. 7 | 8 | # M5Stack-Avatar 9 | 10 | [![Powered By PlatformIO](https://img.shields.io/badge/powered-PlatformIO-brightgreen)](https://platformio.org/) 11 | [![Build Status](https://travis-ci.com/meganetaaan/m5stack-avatar.svg?branch=master)](https://travis-ci.com/meganetaaan/m5stack-avatar) 12 | 13 | ![M5Stack-Avatar](docs/image/avatar.gif) 14 | 15 | Video: https://www.youtube.com/watch?v=C1Hj9kfY5qc 16 | 17 | [日本語](README_ja.md) 18 | 19 | ## Features 20 | 21 | * :neutral_face: Draw avatar face 22 | * :smile: Expression(Happy, Angry, Sad etc.) 23 | * :smiley_cat: Customize face 24 | * :kiss: Lip sync 25 | * :art: Color Palette 26 | * :arrows_clockwise: Move, Zoom and Rotation 27 | * :two: Compatible with M5Stack Core2 28 | 29 | ## Installation 30 | 31 | ### Prerequisites 32 | 33 | * USB Driver is installed throwgh [Getting Started: Installing the USB Driver](http://www.m5stack.com/assets/docs/) 34 | * Any of IDE is set up 35 | * This library is confirmed on __Arduino IDE__ and __Platform IO__ for now 36 | 37 | ### Using Arduino IDE 38 | 39 | * On Arduino IDE, Select "Sketch > Include Library > Manage Libraries..." 40 | * Search "m5stack avatar" 41 | * Select "M5Stack_Avatar" from the results then click "Install" 42 | * The library gets installed 43 | 44 | ### Using Platform IO 45 | 46 | * Initialize your Platform IO project 47 | ```sh 48 | mkdir my-avatar 49 | cd my-avatar 50 | platformio init -d . -b m5stack-core-esp32 51 | ``` 52 | * Install the library and its dependency 53 | ```sh 54 | platformio lib install M5Unified 55 | platformio lib install M5Stack-Avatar 56 | ``` 57 | * The library gets downloaded from repository to .piolibdeps directory 58 | 59 | ## Usage 60 | 61 | ```cpp 62 | 63 | #include 64 | #include 65 | 66 | using namespace m5avatar; 67 | 68 | Avatar avatar; 69 | 70 | void setup() 71 | { 72 | M5.begin(); 73 | avatar.init(); // start drawing 74 | } 75 | 76 | void loop() 77 | { 78 | // avatar's face updates in another thread 79 | // so no need to loop-by-loop rendering 80 | } 81 | ``` 82 | 83 | ### Using LipSync 84 | 85 | * setup AquesTalk-ESP32 (http://blog-yama.a-quest.com/?eid=970195). 86 | * (For parsing Kainji statement) Copy the dictionary file from above link to the microSD card. 87 | * You don't need to copy AquesTalkTTS files. They are included in this library. 88 | 89 | * Write below to open avatar mouth according to the audio output. 90 | 91 | ```cpp 92 | #include 93 | #include 94 | #include 95 | #include 96 | 97 | using namespace m5avatar; 98 | 99 | // AquesTalk License Key 100 | // NULL or wrong value is just ignored 101 | const char* AQUESTALK_KEY = "XXXX-XXXX-XXXX-XXXX"; 102 | Avatar avatar; 103 | 104 | void setup() { 105 | int iret; 106 | M5.begin(); 107 | // For Kanji-to-speech mode (requires dictionary file saved on microSD) 108 | // iret = TTS.createK(AQUESTALK_KEY); 109 | iret = TTS.create(AQUESTALK_KEY); 110 | avatar.init(); 111 | avatar.addTask(lipSync, "lipSync"); 112 | } 113 | 114 | void loop() { 115 | M5.update(); 116 | if (M5.BtnA.wasPressed()) { 117 | // For Kanji-to-speech mode 118 | // TTS.play("こんにちは。", 80); 119 | TTS.play("konnichiwa", 80); 120 | } 121 | } 122 | 123 | ``` 124 | 125 | ### Further usage 126 | 127 | see `examples` directory. 128 | 129 | ### Migration from 0.7.x to 0.8.x 130 | 131 | `M5Stack-Avatar` now depends on `M5Unified`, the integrated library for all devices of M5Stack series. 132 | Since 0.8.0, Sketches with avatar should include `M5Unified.h` instead of `M5Stack.h` or `M5Core2.h` 133 | 134 | ### Notes for v0.10.0 135 | 136 | Some applications may reboot due to insufficient stack size for other tasks. In such cases, increase the stack size of the relevant task. -------------------------------------------------------------------------------- /src/Avatar.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Shinya Ishikawa. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full 3 | // license information. 4 | 5 | #ifndef AVATAR_H_ 6 | #define AVATAR_H_ 7 | #include 8 | 9 | #include "ColorPalette.h" 10 | #include "Face.h" 11 | 12 | #ifdef SDL_h_ 13 | typedef SDL_ThreadFunction TaskFunction_t; 14 | typedef int BaseType_t; 15 | typedef unsigned int UBaseType_t; 16 | typedef SDL_Thread *TaskHandle_t; 17 | typedef int TaskResult_t; 18 | #define APP_CPU_NUM (1) 19 | #else 20 | typedef void TaskResult_t; 21 | #endif 22 | 23 | #ifndef APP_CPU_NUM 24 | #define APP_CPU_NUM PRO_CPU_NUM 25 | #endif 26 | 27 | #ifndef ARDUINO 28 | #include 29 | typedef std::string String; 30 | #endif // ARDUINO 31 | 32 | namespace m5avatar { 33 | class Avatar { 34 | private: 35 | Face *face; 36 | bool _isDrawing; 37 | Expression expression; 38 | float breath; 39 | 40 | // eyes variables 41 | float rightEyeOpenRatio_; 42 | float rightGazeV_; 43 | float rightGazeH_; 44 | 45 | float leftEyeOpenRatio_; 46 | float leftGazeV_; 47 | float leftGazeH_; 48 | 49 | bool isAutoBlink_; 50 | 51 | float mouthOpenRatio; 52 | 53 | float rotation; 54 | float scale; 55 | ColorPalette palette; 56 | String speechText; 57 | int colorDepth; 58 | BatteryIconStatus batteryIconStatus; 59 | int32_t batteryLevel; 60 | const lgfx::IFont *speechFont; 61 | 62 | public: 63 | Avatar(); 64 | explicit Avatar(Face *face); 65 | ~Avatar(); 66 | Avatar(const Avatar &other) = default; 67 | Avatar &operator=(const Avatar &other) = default; 68 | Face *getFace() const; 69 | ColorPalette getColorPalette() const; 70 | void setColorPalette(ColorPalette cp); 71 | void setFace(Face *face); 72 | void init(int colorDepth = 1); 73 | // expression i/o 74 | Expression getExpression(); 75 | void setExpression(Expression exp); 76 | // breath i/o 77 | void setBreath(float f); 78 | float getBreath(); 79 | // gaze i/o 80 | void setRightGaze(float vertical, float horizontal); 81 | void getRightGaze(float *vertical, float *horizontal); 82 | void setLeftGaze(float vertical, float horizontal); 83 | void getLeftGaze(float *vertical, float *horizontal); 84 | 85 | /** 86 | * @brief Get the Gaze of Avatar 87 | * 88 | * [warning] this function return the mean gaze of left gaze & right gaze 89 | * This is not the gaze representing avatar's direction of awareness 90 | * 91 | * @param vertical cache for vertical value. this value will be overwritten 92 | * by this method. 93 | * @param horizontal cache for horizontal value. this value will be 94 | * overwritten by this method. 95 | */ 96 | void getGaze(float *vertical, float *horizontal); 97 | 98 | // eyes open ratio 99 | void setEyeOpenRatio(float ratio); 100 | void setRightEyeOpenRatio(float ratio); 101 | float getRightEyeOpenRatio(); 102 | void setLeftEyeOpenRatio(float ratio); 103 | float getLeftEyeOpenRatio(); 104 | void setIsAutoBlink(bool b); 105 | bool getIsAutoBlink(); 106 | 107 | void setMouthOpenRatio(float ratio); 108 | void setSpeechText(const char *speechText); 109 | void setSpeechFont(const lgfx::IFont *speechFont); 110 | void setRotation(float radian); 111 | void setPosition(int top, int left); 112 | void setScale(float scale); 113 | void draw(void); 114 | bool isDrawing(); 115 | void start(int colorDepth = 1); 116 | void stop(); 117 | void addTask(TaskFunction_t f, const char *name, 118 | const uint32_t stack_size = 2048, UBaseType_t priority = 4, 119 | TaskHandle_t *const task_handle = NULL, 120 | const BaseType_t core_id = APP_CPU_NUM); 121 | void suspend(); 122 | void resume(); 123 | void setBatteryIcon(bool iconStatus); 124 | void setBatteryStatus(bool isCharging, int32_t batteryLevel); 125 | }; 126 | 127 | class DriveContext { 128 | private: 129 | // TODO(meganetaaan): cyclic reference 130 | Avatar *avatar; 131 | 132 | public: 133 | DriveContext() = delete; 134 | explicit DriveContext(Avatar *avatar); 135 | ~DriveContext() = default; 136 | DriveContext(const DriveContext &other) = delete; 137 | DriveContext &operator=(const DriveContext &other) = delete; 138 | Avatar *getAvatar(); 139 | }; 140 | 141 | } // namespace m5avatar 142 | 143 | #endif // AVATAR_H_ 144 | -------------------------------------------------------------------------------- /src/faces/FaceTemplates.hpp: -------------------------------------------------------------------------------- 1 | #ifndef M5AVATAR_FACES_HPP_ 2 | #define M5AVATAR_FACES_HPP_ 3 | 4 | #include "Eyebrows.hpp" 5 | #include "Eyes.hpp" 6 | #include "Face.h" 7 | #include "Mouths.hpp" 8 | 9 | namespace m5avatar { 10 | /** 11 | * @brief face template for "o_o" 12 | * 13 | */ 14 | class SimpleFace : public Face { 15 | public: 16 | SimpleFace() 17 | : Face(new RectMouth(50, 90, 4, 60), new BoundingRect(148, 163), 18 | // right eye, second eye arg is center position of eye in (y,x) 19 | new EllipseEye(16, 16, false), new BoundingRect(93, 90), 20 | // left eye 21 | new EllipseEye(16, 16, true), new BoundingRect(96, 230), 22 | // hide eye brows with setting these height zero 23 | new EllipseEyebrow(0, 0, false), new BoundingRect(67, 96), 24 | new EllipseEyebrow(0, 0, true), new BoundingRect(72, 230)) {} 25 | }; 26 | /** 27 | * @brief face template for "OωO" face 28 | * 29 | */ 30 | class OmegaFace : public Face { 31 | public: 32 | OmegaFace() 33 | : Face(new OmegaMouth(), new BoundingRect(225, 160), 34 | // right eye, second eye arg is center position of eye in (y,x) 35 | new EllipseEye(false), new BoundingRect(165, 84), 36 | // left eye 37 | new EllipseEye(true), new BoundingRect(165, 84 + 154), 38 | // hide eye brows with setting these height zero 39 | new EllipseEyebrow(0, 0, false), new BoundingRect(67, 96), 40 | new EllipseEyebrow(0, 0, true), new BoundingRect(72, 230)) {} 41 | }; 42 | 43 | class GirlyFace : public Face { 44 | public: 45 | GirlyFace() 46 | : Face(new UShapeMouth(44, 44, 0, 16), new BoundingRect(222, 160), 47 | // right eye, second eye arg is center position of eye 48 | new GirlyEye(84, 84, false), new BoundingRect(163, 64), 49 | // left eye 50 | new GirlyEye(84, 84, true), new BoundingRect(163, 256), 51 | 52 | // right eyebrow 53 | new EllipseEyebrow(36, 20, false), 54 | new BoundingRect(97 + 10, 84 + 18), // (y,x) 55 | // left eyebrow 56 | new EllipseEyebrow(36, 20, true), 57 | new BoundingRect(107, 200 + 18)) {} 58 | }; 59 | 60 | class GirlyFace2 : public Face { 61 | public: 62 | GirlyFace2() 63 | : Face(new UShapeMouth(44, 44, 0, 16), new BoundingRect(222, 160), 64 | // right eye, second eye arg is center position of eye 65 | new GirlyEye(84, 84, false), new BoundingRect(163, 64), 66 | // left eye 67 | new GirlyEye(84, 84, true), new BoundingRect(163, 256), 68 | 69 | // right eyebrow 70 | new BowEyebrow(160, 160, false), 71 | new BoundingRect(163, 64), // (y,x) 72 | // left eyebrow 73 | new BowEyebrow(160, 160, true), new BoundingRect(163, 256)) {} 74 | }; 75 | 76 | class PinkDemonFace : public Face { 77 | public: 78 | PinkDemonFace() 79 | : Face(new UShapeMouth(64, 64, 0, 16), new BoundingRect(214, 160), 80 | // right eye, second eye arg is center position of eye 81 | new PinkDemonEye(52, 134, false), new BoundingRect(134, 106), 82 | // left eye 83 | new PinkDemonEye(52, 134, true), new BoundingRect(134, 218), 84 | 85 | // hide eye brows with setting these height zero 86 | new EllipseEyebrow(15, 0, false), new BoundingRect(67, 96), 87 | new EllipseEyebrow(15, 0, true), new BoundingRect(72, 230)) {} 88 | }; 89 | 90 | class DoggyFace : public Face { 91 | public: 92 | DoggyFace() 93 | : Face(new DoggyMouth(50, 90, 4, 60), new BoundingRect(168, 163), 94 | // right eye, second eye arg is center position of eye 95 | new DoggyEye(false), new BoundingRect(103, 80), 96 | // left eye 97 | new DoggyEye(true), new BoundingRect(106, 240), 98 | // hide eye brows with setting these height zero 99 | new RectEyebrow(15, 2, false), new BoundingRect(67, 96), 100 | new RectEyebrow(15, 2, true), new BoundingRect(72, 230)) {} 101 | }; 102 | 103 | } // namespace m5avatar 104 | 105 | #endif // M5AVATAR_FACES_HPP_ 106 | -------------------------------------------------------------------------------- /src/Effect.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Shinya Ishikawa. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full 3 | // license information. 4 | 5 | #ifndef EFFECT_H_ 6 | #define EFFECT_H_ 7 | #define LGFX_USE_V1 8 | #include 9 | #include "DrawContext.h" 10 | #include "Drawable.h" 11 | 12 | namespace m5avatar { 13 | 14 | class Effect final : public Drawable { 15 | private: 16 | void drawBubbleMark(M5Canvas *spi, uint32_t x, uint32_t y, uint32_t r, 17 | uint16_t color) { 18 | drawBubbleMark(spi, x, y, r, color, 0); 19 | } 20 | 21 | void drawBubbleMark(M5Canvas *spi, uint32_t x, uint32_t y, uint32_t r, 22 | uint16_t color, float offset) { 23 | r = r + floor(r * 0.2 * offset); 24 | spi->drawCircle(x, y, r, color); 25 | spi->drawCircle(x - (r / 4), y - (r / 4), r / 4, color); 26 | } 27 | 28 | void drawSweatMark(M5Canvas *spi, uint32_t x, uint32_t y, uint32_t r, 29 | uint16_t color) { 30 | drawSweatMark(spi, x, y, r, color, 0); 31 | } 32 | 33 | void drawSweatMark(M5Canvas *spi, uint32_t x, uint32_t y, uint32_t r, 34 | uint16_t color, float offset) { 35 | y = y + floor(5 * offset); 36 | r = r + floor(r * 0.2 * offset); 37 | spi->fillCircle(x, y, r, color); 38 | uint32_t a = (sqrt(3) * r) / 2; 39 | spi->fillTriangle(x, y - r * 2, x - a, y - r * 0.5, x + a, y - r * 0.5, 40 | color); 41 | } 42 | 43 | void drawChillMark(M5Canvas *spi, uint32_t x, uint32_t y, uint32_t r, 44 | uint16_t color) { 45 | drawChillMark(spi, x, y, r, color, 0); 46 | } 47 | 48 | void drawChillMark(M5Canvas *spi, uint32_t x, uint32_t y, uint32_t r, 49 | uint16_t color, float offset) { 50 | uint32_t h = r + abs(r * 0.2 * offset); 51 | spi->fillRect(x - (r / 2), y, 3, h / 2, color); 52 | spi->fillRect(x, y, 3, h * 3 / 4, color); 53 | spi->fillRect(x + (r / 2), y, 3, h, color); 54 | } 55 | 56 | void drawAngerMark(M5Canvas *spi, uint32_t x, uint32_t y, uint32_t r, 57 | uint16_t color, uint32_t bColor) { 58 | drawAngerMark(spi, x, y, r, color, bColor, 0); 59 | } 60 | 61 | void drawAngerMark(M5Canvas *spi, uint32_t x, uint32_t y, uint32_t r, 62 | uint16_t color, uint16_t bColor, float offset) { 63 | r = r + abs(r * 0.4 * offset); 64 | spi->fillRect(x - (r / 3), y - r, (r * 2) / 3, r * 2, color); 65 | spi->fillRect(x - r, y - (r / 3), r * 2, (r * 2) / 3, color); 66 | spi->fillRect(x - (r / 3) + 2, y - r, ((r * 2) / 3) - 4, r * 2, bColor); 67 | spi->fillRect(x - r, y - (r / 3) + 2, r * 2, ((r * 2) / 3) - 4, bColor); 68 | } 69 | 70 | void drawHeartMark(M5Canvas *spi, uint32_t x, uint32_t y, uint32_t r, 71 | uint16_t color) { 72 | drawHeartMark(spi, x, y, r, color, 0); 73 | } 74 | 75 | void drawHeartMark(M5Canvas *spi, uint32_t x, uint32_t y, uint32_t r, 76 | uint16_t color, float offset) { 77 | r = r + floor(r * 0.4 * offset); 78 | spi->fillCircle(x - r / 2, y, r / 2, color); 79 | spi->fillCircle(x + r / 2, y, r / 2, color); 80 | float a = (sqrt(2) * r) / 4.0; 81 | spi->fillTriangle(x, y, x - r / 2 - a, y + a, x + r / 2 + a, y + a, color); 82 | spi->fillTriangle(x, y + (r / 2) + 2 * a, x - r / 2 - a, y + a, 83 | x + r / 2 + a, y + a, color); 84 | } 85 | 86 | public: 87 | // constructor 88 | Effect() = default; 89 | ~Effect() = default; 90 | Effect(const Effect &other) = default; 91 | Effect &operator=(const Effect &other) = default; 92 | void draw(M5Canvas *spi, BoundingRect rect, DrawContext *ctx) override { 93 | uint16_t primaryColor = ctx->getColorDepth() == 1 ? 1 : ctx->getColorPalette()->get(COLOR_PRIMARY); 94 | uint16_t bgColor = ctx->getColorDepth() == 1 ? ERACER_COLOR : ctx->getColorPalette()->get(COLOR_BACKGROUND); 95 | float offset = ctx->getBreath(); 96 | Expression exp = ctx->getExpression(); 97 | switch (exp) { 98 | case Expression::Doubt: 99 | drawSweatMark(spi, 290, 110, 7, primaryColor, -offset); 100 | break; 101 | case Expression::Angry: 102 | drawAngerMark(spi, 280, 50, 12, primaryColor, bgColor, offset); 103 | break; 104 | case Expression::Happy: 105 | drawHeartMark(spi, 280, 50, 12, primaryColor, offset); 106 | break; 107 | case Expression::Sad: 108 | drawChillMark(spi, 270, 0, 30, primaryColor, offset); 109 | break; 110 | case Expression::Sleepy: 111 | drawBubbleMark(spi, 290, 40, 10, primaryColor, offset); 112 | drawBubbleMark(spi, 270, 52, 6, primaryColor, -offset); 113 | break; 114 | default: 115 | // noop 116 | break; 117 | } 118 | } 119 | }; 120 | 121 | } // namespace m5avatar 122 | 123 | #endif // EFFECT_H_ 124 | -------------------------------------------------------------------------------- /src/Mouths.cpp: -------------------------------------------------------------------------------- 1 | #include "Mouths.hpp" 2 | 3 | #ifndef _min 4 | #define _min(a, b) std::min(a, b) 5 | #endif 6 | namespace m5avatar { 7 | 8 | BaseMouth::BaseMouth() : BaseMouth(80, 80, 15, 30) {} 9 | BaseMouth::BaseMouth(uint16_t min_width, uint16_t max_width, 10 | uint16_t min_height, uint16_t max_height) 11 | : min_width_{min_width}, 12 | max_width_{max_width}, 13 | min_height_{min_height}, 14 | max_height_{max_height} {} 15 | 16 | void BaseMouth::update(M5Canvas *canvas, BoundingRect rect, DrawContext *ctx) { 17 | primary_color_ = ctx->getColorDepth() == 1 18 | ? 1 19 | : ctx->getColorPalette()->get(COLOR_PRIMARY); 20 | background_color_ = ctx->getColorDepth() == 1 21 | ? ERACER_COLOR 22 | : ctx->getColorPalette()->get(COLOR_BACKGROUND); 23 | secondary_color_ = ctx->getColorDepth() == 1 24 | ? 1 25 | : ctx->getColorPalette()->get(COLOR_SECONDARY); 26 | center_x_ = rect.getCenterX(); 27 | center_y_ = rect.getCenterY(); 28 | open_ratio_ = ctx->getMouthOpenRatio(); 29 | breath_ = _min(1.0f, ctx->getBreath()); 30 | } 31 | 32 | void RectMouth::draw(M5Canvas *canvas, BoundingRect rect, DrawContext *ctx) { 33 | this->update(canvas, rect, ctx); // update drawing cache 34 | int16_t h = min_height_ + (max_height_ - min_height_) * open_ratio_; 35 | int16_t w = min_width_ + (max_width_ - min_width_) * (1 - open_ratio_); 36 | int16_t top_left_x = rect.getLeft() - w / 2; 37 | int16_t top_left_y = rect.getTop() - h / 2 + breath_ * 2; 38 | canvas->fillRect(top_left_x, top_left_y, w, h, primary_color_); 39 | } 40 | 41 | void OmegaMouth::draw(M5Canvas *canvas, BoundingRect rect, DrawContext *ctx) { 42 | this->update(canvas, rect, ctx); // update drawing cache 43 | uint32_t h = min_height_ + (max_height_ - min_height_) * open_ratio_; 44 | uint32_t w = min_width_ + (max_width_ - min_width_) * (1 - open_ratio_); 45 | 46 | // inner mouse 47 | canvas->fillEllipse(center_x_, center_y_ - max_height_ / 2, max_width_ / 4, 48 | static_cast(max_height_ * open_ratio_), 49 | primary_color_); 50 | 51 | // omega 52 | canvas->fillEllipse(center_x_ - 16, center_y_ - max_height_ / 2, 20, 15, 53 | primary_color_); // outer 54 | canvas->fillEllipse(center_x_ + 16, center_y_ - max_height_ / 2, 20, 15, 55 | primary_color_); 56 | canvas->fillEllipse(center_x_ - 16, center_y_ - max_height_ / 2, 18, 13, 57 | background_color_); // inner 58 | canvas->fillEllipse(center_x_ + 16, center_y_ - max_height_ / 2, 18, 13, 59 | background_color_); 60 | // mask for omega 61 | canvas->fillRect(center_x_ - max_width_ / 2, center_y_ - max_height_ * 1.5, 62 | max_width_, max_height_, background_color_); 63 | 64 | // cheek 65 | canvas->fillEllipse(center_x_ - 132, center_y_ - 23, 24, 10, 66 | secondary_color_); 67 | canvas->fillEllipse(center_x_ + 132, center_y_ - 23, 24, 10, 68 | secondary_color_); 69 | } 70 | 71 | void UShapeMouth::draw(M5Canvas *canvas, BoundingRect rect, DrawContext *ctx) { 72 | this->update(canvas, rect, ctx); // update drawing cache 73 | uint32_t h = min_height_ + (max_height_ - min_height_) * open_ratio_; 74 | uint32_t w = min_width_ + (max_width_ - min_width_) * (1 - open_ratio_); 75 | 76 | auto ellipse_center_y = center_y_ - max_height_ / 2; 77 | uint16_t thickness = 6; 78 | 79 | // back 80 | canvas->fillEllipse(center_x_, ellipse_center_y, max_width_ / 2, 81 | max_height_, primary_color_); 82 | // rect mask 83 | canvas->fillRect(center_x_ - max_width_ / 2, ellipse_center_y - max_height_, 84 | max_width_ + 1, max_height_, background_color_); 85 | 86 | // inner mouse 87 | canvas->fillEllipse(center_x_, ellipse_center_y, max_width_ / 2 - thickness, 88 | (max_height_ - thickness) * (1.0f - open_ratio_), 89 | background_color_); 90 | 91 | // cheek 92 | canvas->fillEllipse(center_x_ - 132, center_y_ - 23, 24, 10, 93 | secondary_color_); 94 | canvas->fillEllipse(center_x_ + 132, center_y_ - 23, 24, 10, 95 | secondary_color_); 96 | } 97 | 98 | void DoggyMouth::draw(M5Canvas *canvas, BoundingRect rect, DrawContext *ctx) { 99 | this->update(canvas, rect, ctx); 100 | 101 | uint32_t h = min_height_ + (max_height_ - min_height_) * open_ratio_; 102 | uint32_t w = min_width_ + (max_width_ - min_width_) * (1 - open_ratio_); 103 | if (h > min_height_) { 104 | canvas->fillEllipse(center_x_, center_y_, w / 2, h / 2, primary_color_); 105 | canvas->fillEllipse(center_x_, center_y_, w / 2 - 4, h / 2 - 4, 106 | TFT_RED); 107 | canvas->fillRect(center_x_ - w / 2, center_y_ - h / 2, w, h / 2, 108 | background_color_); 109 | } 110 | canvas->fillEllipse(center_x_, center_y_ - 15, 10, 6, primary_color_); 111 | canvas->fillEllipse(center_x_ - 28, center_y_, 30, 15, primary_color_); 112 | canvas->fillEllipse(center_x_ + 28, center_y_, 30, 15, primary_color_); 113 | canvas->fillEllipse(center_x_ - 29, center_y_ - 4, 27, 15, 114 | background_color_); 115 | canvas->fillEllipse(center_x_ + 29, center_y_ - 4, 27, 15, 116 | background_color_); 117 | } 118 | 119 | } // namespace m5avatar -------------------------------------------------------------------------------- /src/Face.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Shinya Ishikawa. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full 3 | // license information. 4 | 5 | #include "Face.h" 6 | 7 | #ifndef _min 8 | #define _min(a, b) std::min(a, b) 9 | #endif 10 | 11 | namespace m5avatar { 12 | BoundingRect br; 13 | 14 | Face::Face() 15 | : Face(new Mouth(50, 90, 4, 60), new BoundingRect(148, 163), 16 | new Eye(8, false), new BoundingRect(93, 90), new Eye(8, true), 17 | new BoundingRect(96, 230), new Eyeblow(32, 0, false), 18 | new BoundingRect(67, 96), new Eyeblow(32, 0, true), 19 | new BoundingRect(72, 230)) {} 20 | 21 | Face::Face(Drawable *mouth, Drawable *eyeR, Drawable *eyeL, Drawable *eyeblowR, 22 | Drawable *eyeblowL) 23 | : Face(mouth, new BoundingRect(148, 163), eyeR, new BoundingRect(93, 90), 24 | eyeL, new BoundingRect(96, 230), eyeblowR, new BoundingRect(67, 96), 25 | eyeblowL, new BoundingRect(72, 230)) {} 26 | 27 | Face::Face(Drawable *mouth, BoundingRect *mouthPos, Drawable *eyeR, 28 | BoundingRect *eyeRPos, Drawable *eyeL, BoundingRect *eyeLPos, 29 | Drawable *eyeblowR, BoundingRect *eyeblowRPos, Drawable *eyeblowL, 30 | BoundingRect *eyeblowLPos) 31 | : Face(mouth, mouthPos, eyeR, eyeRPos, eyeL, eyeLPos, eyeblowR, 32 | eyeblowRPos, eyeblowL, eyeblowLPos, 33 | new BoundingRect(0, 0, 320, 240), 34 | new M5Canvas(&M5.Lcd), new M5Canvas(&M5.Lcd)) {} 35 | 36 | Face::Face(Drawable *mouth, BoundingRect *mouthPos, Drawable *eyeR, 37 | BoundingRect *eyeRPos, Drawable *eyeL, BoundingRect *eyeLPos, 38 | Drawable *eyeblowR, BoundingRect *eyeblowRPos, Drawable *eyeblowL, 39 | BoundingRect *eyeblowLPos, 40 | BoundingRect *boundingRect, M5Canvas *spr, M5Canvas *tmpSpr) 41 | : mouth{mouth}, 42 | eyeR{eyeR}, 43 | eyeL{eyeL}, 44 | eyeblowR{eyeblowR}, 45 | eyeblowL{eyeblowL}, 46 | mouthPos{mouthPos}, 47 | eyeRPos{eyeRPos}, 48 | eyeLPos{eyeLPos}, 49 | eyeblowRPos{eyeblowRPos}, 50 | eyeblowLPos{eyeblowLPos}, 51 | boundingRect{boundingRect}, 52 | sprite{spr}, 53 | tmpSprite{tmpSpr} {} 54 | 55 | Face::~Face() { 56 | delete mouth; 57 | delete mouthPos; 58 | delete eyeR; 59 | delete eyeRPos; 60 | delete eyeL; 61 | delete eyeLPos; 62 | delete eyeblowR; 63 | delete eyeblowRPos; 64 | delete eyeblowL; 65 | delete eyeblowLPos; 66 | delete sprite; 67 | delete boundingRect; 68 | delete b; 69 | delete h; 70 | delete battery; 71 | } 72 | 73 | void Face::setMouth(Drawable *mouth) { this->mouth = mouth; } 74 | 75 | void Face::setLeftEye(Drawable *eyeL) { this->eyeL = eyeL; } 76 | 77 | void Face::setRightEye(Drawable *eyeR) { this->eyeR = eyeR; } 78 | 79 | Drawable *Face::getMouth() { return mouth; } 80 | 81 | Drawable *Face::getLeftEye() { return eyeL; } 82 | 83 | Drawable *Face::getRightEye() { return eyeR; } 84 | 85 | BoundingRect *Face::getBoundingRect() { return boundingRect; } 86 | 87 | void Face::draw(DrawContext *ctx) { 88 | sprite->createSprite(boundingRect->getWidth(), boundingRect->getHeight()); 89 | sprite->setColorDepth(ctx->getColorDepth()); 90 | // NOTE: setting below for 1-bit color depth 91 | sprite->setBitmapColor(ctx->getColorPalette()->get(COLOR_PRIMARY), 92 | ctx->getColorPalette()->get(COLOR_BACKGROUND)); 93 | if (ctx->getColorDepth() != 1) { 94 | sprite->fillSprite(ctx->getColorPalette()->get(COLOR_BACKGROUND)); 95 | } else { 96 | sprite->fillSprite(0); 97 | } 98 | float breath = _min(1.0f, ctx->getBreath()); 99 | 100 | // TODO(meganetaaan): unify drawing process of each parts 101 | BoundingRect rect = *mouthPos; 102 | rect.setPosition(rect.getTop() + breath * 3, rect.getLeft()); 103 | // copy context to each draw function 104 | mouth->draw(sprite, rect, ctx); 105 | 106 | rect = *eyeRPos; 107 | rect.setPosition(rect.getTop() + breath * 3, rect.getLeft()); 108 | eyeR->draw(sprite, rect, ctx); 109 | 110 | rect = *eyeLPos; 111 | rect.setPosition(rect.getTop() + breath * 3, rect.getLeft()); 112 | eyeL->draw(sprite, rect, ctx); 113 | 114 | rect = *eyeblowRPos; 115 | rect.setPosition(rect.getTop() + breath * 3, rect.getLeft()); 116 | eyeblowR->draw(sprite, rect, ctx); 117 | 118 | rect = *eyeblowLPos; 119 | rect.setPosition(rect.getTop() + breath * 3, rect.getLeft()); 120 | eyeblowL->draw(sprite, rect, ctx); 121 | 122 | // TODO(meganetaaan): make balloons and effects selectable 123 | b->draw(sprite, br, ctx); 124 | h->draw(sprite, br, ctx); 125 | battery->draw(sprite, br, ctx); 126 | // drawAccessory(sprite, position, ctx); 127 | 128 | // TODO(meganetaaan): rethink responsibility for transform function 129 | float scale = ctx->getScale(); 130 | float rotation = ctx->getRotation(); 131 | 132 | // ▼▼▼▼ここから▼▼▼▼ 133 | static constexpr uint8_t y_step = 8; 134 | 135 | if (tmpSprite->getBuffer() == nullptr) { 136 | // 出力先と同じcolorDepthを指定することで、DMA転送が可能になる。 137 | // Display自体は16bit or 24bitしか指定できないが、細長なので1bitではなくても大丈夫。 138 | tmpSprite->setColorDepth(M5.Display.getColorDepth()); 139 | 140 | // 確保するメモリは高さ8ピクセルの横長の細長い短冊状とする。 141 | tmpSprite->createSprite(boundingRect->getWidth(), y_step); 142 | } 143 | 144 | // 背景クリア用の色を設定 145 | tmpSprite->setBaseColor(ctx->getColorPalette()->get(COLOR_BACKGROUND)); 146 | int y = 0; 147 | do { 148 | // 背景色で塗り潰し 149 | tmpSprite->clear(); 150 | 151 | // 傾きとズームを反映してspriteからtmpSpriteに転写 152 | sprite->pushRotateZoom(tmpSprite, boundingRect->getWidth()>>1, (boundingRect->getHeight()>>1) - y, rotation, scale, scale); 153 | 154 | // tmpSpriteから画面に転写 155 | M5.Display.startWrite(); 156 | 157 | // 事前にstartWriteしておくことで、pushSprite はDMA転送を開始するとすぐに処理を終えて戻ってくる。 158 | tmpSprite->pushSprite(&M5.Display, boundingRect->getLeft(), boundingRect->getTop() + y); 159 | 160 | // DMA転送中にdelay処理を設けることにより、DMA転送中に他のタスクへCPU処理時間を譲ることができる。 161 | lgfx::delay(1); 162 | 163 | // endWriteによってDMA転送の終了を待つ。 164 | M5.Display.endWrite(); 165 | 166 | } while ((y += y_step) < boundingRect->getHeight()); 167 | 168 | // 削除するのが良いかどうか要検討 (次回メモリ確保できない場合は描画できなくなるので、維持しておいても良いかも?) 169 | // tmpSprite->deleteSprite(); 170 | // ▲▲▲▲ここまで▲▲▲▲ 171 | 172 | sprite->deleteSprite(); 173 | } 174 | } // namespace m5avatar -------------------------------------------------------------------------------- /src/Avatar.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Shinya Ishikawa. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full 3 | // license information. 4 | 5 | #include "Avatar.h" 6 | 7 | #ifndef PI 8 | #define PI 3.1415926535897932384626433832795 9 | #endif 10 | 11 | namespace m5avatar { 12 | 13 | unsigned int seed = 0; 14 | 15 | #ifndef rand_r 16 | #define init_rand() srand(seed) 17 | #define _rand() rand() 18 | #else 19 | #define init_rand() ; 20 | #define _rand() rand_r(&seed) 21 | #endif 22 | 23 | #ifdef SDL_h_ 24 | #define TaskResult() return 0 25 | #define TaskDelay(ms) lgfx::delay(ms) 26 | long random(long howbig) { return std::rand() % howbig; } 27 | #else 28 | #define TaskResult() vTaskDelete(NULL) 29 | #define TaskDelay(ms) vTaskDelay(ms / portTICK_PERIOD_MS) 30 | #endif 31 | 32 | // TODO(meganetaaan): make read-only 33 | DriveContext::DriveContext(Avatar *avatar) : avatar{avatar} {} 34 | 35 | Avatar *DriveContext::getAvatar() { return avatar; } 36 | 37 | TaskHandle_t drawTaskHandle; 38 | 39 | TaskResult_t drawLoop(void *args) { 40 | DriveContext *ctx = reinterpret_cast(args); 41 | Avatar *avatar = ctx->getAvatar(); 42 | // update drawings in the display 43 | while (avatar->isDrawing()) { 44 | if (avatar->isDrawing()) { 45 | avatar->draw(); 46 | } 47 | TaskDelay(10); 48 | } 49 | TaskResult(); 50 | } 51 | 52 | TaskResult_t facialLoop(void *args) { 53 | int count = 0; 54 | DriveContext *ctx = reinterpret_cast(args); 55 | Avatar *avatar = ctx->getAvatar(); 56 | uint32_t saccade_interval = 1000; 57 | uint32_t blink_interval = 1000; 58 | unsigned long last_saccade_millis = 0; 59 | unsigned long last_blink_millis = 0; 60 | bool eye_open = true; 61 | float vertical = 0.0f; 62 | float horizontal = 0.0f; 63 | float breath = 0.0f; 64 | init_rand(); 65 | // update facial internal state 66 | while (avatar->isDrawing()) { 67 | if ((lgfx::millis() - last_saccade_millis) > saccade_interval) { 68 | vertical = _rand() / (RAND_MAX / 2.0) - 1; 69 | horizontal = _rand() / (RAND_MAX / 2.0) - 1; 70 | avatar->setRightGaze(vertical, horizontal); 71 | avatar->setLeftGaze(vertical, horizontal); 72 | saccade_interval = 500 + 100 * random(20); 73 | last_saccade_millis = lgfx::millis(); 74 | } 75 | 76 | if (avatar->getIsAutoBlink()) { 77 | if ((lgfx::millis() - last_blink_millis) > blink_interval) { 78 | if (eye_open) { 79 | avatar->setEyeOpenRatio(1.0f); 80 | blink_interval = 2500 + 100 * random(20); 81 | } else { 82 | avatar->setEyeOpenRatio(0.0f); 83 | blink_interval = 300 + 10 * random(20); 84 | } 85 | eye_open = !eye_open; 86 | last_blink_millis = lgfx::millis(); 87 | } 88 | } 89 | 90 | count = (count + 1) % 100; 91 | breath = sin(count * 2 * PI / 100.0); 92 | avatar->setBreath(breath); 93 | TaskDelay(33); // approx. 30fps 94 | } 95 | TaskResult(); 96 | } 97 | 98 | Avatar::Avatar() : Avatar(new Face()) {} 99 | 100 | Avatar::Avatar(Face *face) 101 | : face{face}, 102 | _isDrawing{false}, 103 | expression{Expression::Neutral}, 104 | breath{0}, 105 | leftEyeOpenRatio_{1.0f}, 106 | leftGazeH_{1.0f}, 107 | leftGazeV_{1.0f}, 108 | rightEyeOpenRatio_{1.0f}, 109 | rightGazeH_{1.0f}, 110 | rightGazeV_{1.0f}, 111 | isAutoBlink_{true}, 112 | mouthOpenRatio{0}, 113 | rotation{0}, 114 | scale{1}, 115 | palette{ColorPalette()}, 116 | speechText{""}, 117 | colorDepth{1}, 118 | batteryIconStatus{BatteryIconStatus::invisible} {} 119 | 120 | Avatar::~Avatar() { delete face; } 121 | 122 | void Avatar::setFace(Face *face) { this->face = face; } 123 | 124 | Face *Avatar::getFace() const { return face; } 125 | 126 | void Avatar::addTask(TaskFunction_t f, const char *name, 127 | const uint32_t stack_size, UBaseType_t priority, 128 | TaskHandle_t *const task_handle, 129 | const BaseType_t core_id) { 130 | DriveContext *ctx = new DriveContext(this); 131 | #ifdef SDL_h_ 132 | if (task_handle == NULL) { 133 | SDL_CreateThreadWithStackSize(f, name, stack_size, ctx); 134 | } else { 135 | *task_handle = SDL_CreateThreadWithStackSize(f, name, stack_size, ctx); 136 | } 137 | #else 138 | // TODO(meganetaaan): set a task handler 139 | xTaskCreateUniversal(f, /* Function to implement the task */ 140 | name, /* Name of the task */ 141 | stack_size, /* Stack size in words */ 142 | ctx, /* Task input parameter */ 143 | priority, /* Priority of the task */ 144 | task_handle, /* Task handle. */ 145 | core_id); /* Core No*/ 146 | #endif 147 | } 148 | 149 | void Avatar::init(int colorDepth) { 150 | // for compatibility with older version 151 | start(colorDepth); 152 | } 153 | 154 | void Avatar::stop() { _isDrawing = false; } 155 | 156 | void Avatar::suspend() { 157 | #ifndef SDL_h_ 158 | vTaskSuspend(drawTaskHandle); 159 | #endif 160 | } 161 | 162 | void Avatar::resume() { 163 | #ifndef SDL_h_ 164 | vTaskResume(drawTaskHandle); 165 | #endif 166 | } 167 | 168 | void Avatar::start(int colorDepth) { 169 | // if the task already started, don't create another task; 170 | if (_isDrawing) return; 171 | _isDrawing = true; 172 | 173 | this->colorDepth = colorDepth; 174 | DriveContext *ctx = new DriveContext(this); 175 | #ifdef SDL_h_ 176 | drawTaskHandle = 177 | SDL_CreateThreadWithStackSize(drawLoop, "drawLoop", 2048, ctx); 178 | SDL_CreateThreadWithStackSize(facialLoop, "facialLoop", 1024, ctx); 179 | #else 180 | // TODO(meganetaaan): keep handle of these tasks 181 | xTaskCreateUniversal(drawLoop, /* Function to implement the task */ 182 | "drawLoop", /* Name of the task */ 183 | 2048, /* Stack size in words */ 184 | ctx, /* Task input parameter */ 185 | 1, /* Priority of the task */ 186 | &drawTaskHandle, /* Task handle. */ 187 | APP_CPU_NUM); 188 | 189 | xTaskCreateUniversal(facialLoop, /* Function to implement the task */ 190 | "facialLoop", /* Name of the task */ 191 | 1024, /* Stack size in words */ 192 | ctx, /* Task input parameter */ 193 | 2, /* Priority of the task */ 194 | NULL, /* Task handle. */ 195 | APP_CPU_NUM); 196 | #endif 197 | } 198 | 199 | void Avatar::draw() { 200 | Gaze rightGaze = Gaze(this->rightGazeV_, this->rightGazeV_); 201 | Gaze leftGaze = Gaze(this->leftGazeV_, this->leftGazeH_); 202 | DrawContext *ctx = new DrawContext( 203 | this->expression, this->breath, &this->palette, rightGaze, 204 | this->rightEyeOpenRatio_, leftGaze, this->leftEyeOpenRatio_, 205 | this->mouthOpenRatio, this->speechText, this->rotation, this->scale, 206 | this->colorDepth, this->batteryIconStatus, this->batteryLevel, 207 | this->speechFont); 208 | face->draw(ctx); 209 | delete ctx; 210 | } 211 | 212 | bool Avatar::isDrawing() { return _isDrawing; } 213 | 214 | void Avatar::setExpression(Expression expression) { 215 | suspend(); 216 | this->expression = expression; 217 | resume(); 218 | } 219 | 220 | Expression Avatar::getExpression() { return this->expression; } 221 | 222 | void Avatar::setBreath(float breath) { this->breath = breath; } 223 | 224 | float Avatar::getBreath() { return this->breath; } 225 | 226 | void Avatar::setRotation(float radian) { this->rotation = radian; } 227 | 228 | void Avatar::setScale(float scale) { this->scale = scale; } 229 | 230 | void Avatar::setPosition(int top, int left) { 231 | this->getFace()->getBoundingRect()->setPosition(top, left); 232 | } 233 | 234 | void Avatar::setColorPalette(ColorPalette cp) { palette = cp; } 235 | 236 | ColorPalette Avatar::getColorPalette(void) const { return this->palette; } 237 | 238 | void Avatar::setMouthOpenRatio(float ratio) { this->mouthOpenRatio = ratio; } 239 | 240 | void Avatar::setEyeOpenRatio(float ratio) { 241 | setRightEyeOpenRatio(ratio); 242 | setLeftEyeOpenRatio(ratio); 243 | } 244 | 245 | void Avatar::setLeftEyeOpenRatio(float ratio) { 246 | this->leftEyeOpenRatio_ = ratio; 247 | } 248 | 249 | float Avatar::getLeftEyeOpenRatio() { return this->leftEyeOpenRatio_; } 250 | 251 | void Avatar::setRightEyeOpenRatio(float ratio) { 252 | this->rightEyeOpenRatio_ = ratio; 253 | } 254 | 255 | float Avatar::getRightEyeOpenRatio() { return this->rightEyeOpenRatio_; } 256 | 257 | void Avatar::setIsAutoBlink(bool b) { this->isAutoBlink_ = b; } 258 | 259 | bool Avatar::getIsAutoBlink() { return this->isAutoBlink_; } 260 | 261 | void Avatar::setRightGaze(float vertical, float horizontal) { 262 | this->rightGazeV_ = vertical; 263 | this->rightGazeH_ = horizontal; 264 | } 265 | 266 | void Avatar::getRightGaze(float *vertical, float *horizontal) { 267 | *vertical = this->rightGazeV_; 268 | *horizontal = this->rightGazeH_; 269 | } 270 | 271 | void Avatar::setLeftGaze(float vertical, float horizontal) { 272 | this->leftGazeV_ = vertical; 273 | this->leftGazeH_ = horizontal; 274 | } 275 | 276 | void Avatar::getLeftGaze(float *vertical, float *horizontal) { 277 | *vertical = this->leftGazeV_; 278 | *horizontal = this->leftGazeH_; 279 | } 280 | 281 | void Avatar::getGaze(float *vertical, float *horizontal){ 282 | *vertical = 0.5f * this->leftGazeV_ + 0.5f * this->rightGazeV_; 283 | *horizontal = 0.5f * this->leftGazeH_ + 0.5f * this->rightGazeH_; 284 | } 285 | 286 | void Avatar::setSpeechText(const char *speechText) { 287 | this->speechText = String(speechText); 288 | } 289 | 290 | void Avatar::setSpeechFont(const lgfx::IFont *speechFont) { 291 | this->speechFont = speechFont; 292 | } 293 | 294 | void Avatar::setBatteryIcon(bool batteryIcon) { 295 | if (!batteryIcon) { 296 | batteryIconStatus = BatteryIconStatus::invisible; 297 | } else { 298 | batteryIconStatus = BatteryIconStatus::unknown; 299 | } 300 | } 301 | 302 | void Avatar::setBatteryStatus(bool isCharging, int32_t batteryLevel) { 303 | if (this->batteryIconStatus != BatteryIconStatus::invisible) { 304 | if (isCharging) { 305 | this->batteryIconStatus = BatteryIconStatus::charging; 306 | } else { 307 | this->batteryIconStatus = BatteryIconStatus::discharging; 308 | } 309 | this->batteryLevel = batteryLevel; 310 | } 311 | } 312 | 313 | } // namespace m5avatar 314 | -------------------------------------------------------------------------------- /lib/AquesTalkTTS/AquesTalkTTS.cpp: -------------------------------------------------------------------------------- 1 | // AquesTalkTTS - AquesTalk ESP32のラッパークラス 2 | // AquesTalk-ESP32 + I2S + internal-DAC 3 | // Copyright (c) 2018 AQUEST (本ソースコードは改変自由) 4 | /* 5 | * 本ソースは、M5Staclkのハードウェア用に記述されています。 6 | お使いのハードウェア環境に応じて変更してください。 7 | 8 | * 9 | DAC_XXX()関数は、D/A部分の関数です。このソースはI2S経由で内蔵DACから音声を出力するものです。 10 | * 11 | I2S-内蔵DACの場合、符号無しのデータを与える必要があるため、DAC_write()で変換しています。 12 | 外付けのI2S-DACを使う場合は注意してください。 13 | 14 | * aqdic_open()/aqdic_close()/aqdic_read()関数は 15 | 辞書データ(aqdic_m.bin)にアクセスする関数で、 16 | libaquestalk.aから呼び出されます。このソースはM5StackのSDにファイルとして辞書データが配置 17 | されている場合のコードです。 18 | 19 | * 20 | ワークバッファをヒープ上に確保しています。create()は400B、createK()は20.4KBを使います。 21 | */ 22 | 23 | #include "AquesTalkTTS.h" 24 | #include 25 | #include 26 | #include 27 | #include 28 | 29 | #define LEN_FRAME 32 30 | #define TASK_PRIORITY 10 31 | #define SAMPLING_FREQ 24000 // 8KHz x 3 32 | #define DMA_BUF_COUNT 3 33 | #define DMA_BUF_LEN (LEN_FRAME * 3) // one buffer size(one channnel samples) 34 | #define DMA_BUF_SIZE (DMA_BUF_COUNT * DMA_BUF_LEN) 35 | #define I2S_FIFO_LEN (64 / 2) 36 | #define TICKS_TO_WAIT (2 * LEN_FRAME / 8 / portTICK_PERIOD_MS) 37 | #define N_BUF_ROMAN 1024 38 | 39 | static uint32_t *workbuf = 0; // work buffer for AquesTalk pico 40 | static uint8_t *workbufK = 0; // work buffer for AqKanji2Roman 41 | static TaskHandle_t taskAquesTalk = 0; 42 | static SemaphoreHandle_t muxAquesTalk = 0; 43 | static int level = 0; 44 | 45 | static void task_TTS_synthe(void *arg); 46 | static void DAC_create(); 47 | static void DAC_release(); 48 | static void DAC_start(); 49 | static void DAC_stop(); 50 | static int DAC_write(int len, int16_t *wav); 51 | static int DAC_write_val(uint16_t val); 52 | 53 | AquesTalkTTS TTS; // the only instance of AquesTalkTTS class 54 | 55 | // 音声記号からの音声合成のみの場合の初期化 56 | int AquesTalkTTS::create(const char *licencekey) { 57 | int iret; 58 | 59 | // Initialize AquesTalk-ESP 60 | if (!workbuf) { 61 | workbuf = (uint32_t *)malloc(AQ_SIZE_WORKBUF * sizeof(uint32_t)); 62 | if (workbuf == 0) return 401; // no heap memory 63 | } 64 | iret = CAqTkPicoF_Init(workbuf, LEN_FRAME, licencekey); 65 | if (iret) return iret; // AquesTalk Init error 66 | 67 | if (!muxAquesTalk) muxAquesTalk = xSemaphoreCreateMutex(); 68 | 69 | return 0; 70 | } 71 | 72 | // 漢字仮名混じり文からの音声合成の場合の初期化 73 | int AquesTalkTTS::createK(const char *licencekey) { 74 | int iret; 75 | 76 | if (!workbufK) { 77 | workbufK = (uint8_t *)malloc(SIZE_AQK2R_MIN_WORK_BUF); 78 | if (workbufK == 0) return 401; // no heap memory 79 | } 80 | iret = CAqK2R_Create(workbufK, SIZE_AQK2R_MIN_WORK_BUF); 81 | if (iret) { 82 | free(workbufK); 83 | workbufK = 0; 84 | return iret; // AqKanji2Roman Init error 85 | } 86 | 87 | return create(licencekey); 88 | } 89 | 90 | void AquesTalkTTS::release() { 91 | stop(); 92 | if (taskAquesTalk) vTaskDelete(taskAquesTalk); 93 | if (muxAquesTalk) vSemaphoreDelete(muxAquesTalk); 94 | if (workbuf) free(workbuf); 95 | if (workbufK) { 96 | CAqK2R_Release(); 97 | free(workbufK); 98 | } 99 | workbuf = 0; 100 | workbufK = 0; 101 | taskAquesTalk = 0; 102 | muxAquesTalk = 0; 103 | } 104 | 105 | int AquesTalkTTS::play(const char *koe, int speed) { 106 | int iret; 107 | if (!muxAquesTalk) return 402; // not initialized. use TTS.create() 108 | 109 | xSemaphoreTake(muxAquesTalk, (portTickType)portMAX_DELAY); 110 | iret = CAqTkPicoF_SetKoe((const uint8_t *)koe, speed, 256); 111 | xSemaphoreGive(muxAquesTalk); 112 | if (iret) return iret; 113 | 114 | if (taskAquesTalk == 0) { 115 | xTaskCreate(task_TTS_synthe, "task_TTS_synthe", 4096, NULL, TASK_PRIORITY, 116 | &taskAquesTalk); 117 | } else { 118 | vTaskResume(taskAquesTalk); 119 | } 120 | return 0; 121 | } 122 | 123 | int AquesTalkTTS::playK(const char *kanji, int speed) { 124 | int iret; 125 | if (workbufK == 0) 126 | return 403; // not initialized ( use TTS.createK() before. ) 127 | if (!muxAquesTalk) return 402; // not initialized use TTS.create() 128 | 129 | char roman[N_BUF_ROMAN]; 130 | iret = CAqK2R_Convert(kanji, roman, N_BUF_ROMAN); 131 | if (iret == 0) { 132 | xSemaphoreTake(muxAquesTalk, (portTickType)portMAX_DELAY); 133 | iret = CAqTkPicoF_SetKoe((const uint8_t *)roman, speed, 256); 134 | xSemaphoreGive(muxAquesTalk); 135 | } else { 136 | iret += 1000; 137 | } 138 | if (iret) return iret; 139 | 140 | if (taskAquesTalk == 0) { 141 | xTaskCreate(task_TTS_synthe, "task_TTS_synthe", 4096, NULL, TASK_PRIORITY, 142 | &taskAquesTalk); 143 | } else { 144 | vTaskResume(taskAquesTalk); 145 | } 146 | return 0; 147 | } 148 | 149 | void AquesTalkTTS::stop() { 150 | if (taskAquesTalk == 0) return; // not playing 151 | if (eTaskGetState(taskAquesTalk) == eSuspended) return; // already suspended. 152 | 153 | xSemaphoreTake(muxAquesTalk, (portTickType)portMAX_DELAY); 154 | CAqTkPicoF_SetKoe((const uint8_t *)"#", 100, 256); // generate error 155 | xSemaphoreGive(muxAquesTalk); 156 | // wait until the task suspend 157 | for (;;) { 158 | if (eTaskGetState(taskAquesTalk) == eSuspended) break; 159 | } 160 | } 161 | 162 | bool AquesTalkTTS::isPlay() { 163 | if (taskAquesTalk == 0) return false; // not playing 164 | if (eTaskGetState(taskAquesTalk) == eSuspended) 165 | return false; // already suspended. 166 | return true; 167 | } 168 | 169 | int AquesTalkTTS::getLevel() { return level; } 170 | 171 | void task_TTS_synthe(void *arg) { 172 | for (;;) { 173 | DAC_create(); 174 | DAC_start(); 175 | for (;;) { 176 | int iret; 177 | uint16_t len; 178 | int16_t wav[LEN_FRAME]; 179 | 180 | xSemaphoreTake(muxAquesTalk, (portTickType)portMAX_DELAY); 181 | iret = CAqTkPicoF_SyntheFrame(wav, &len); 182 | xSemaphoreGive(muxAquesTalk); 183 | 184 | if (iret) break; // EOD or ERROR 185 | DAC_write((int)len, wav); 186 | 187 | { // calc gain for Avatar(lip-sync); 188 | int32_t sum = 0; 189 | for (int i = 0; i < LEN_FRAME; i++) { 190 | sum += abs(wav[i]); 191 | } 192 | level = sum / LEN_FRAME; 193 | } 194 | } 195 | DAC_stop(); 196 | DAC_release(); 197 | level = 0; 198 | vTaskSuspend(NULL); // suspend this task 199 | } 200 | } 201 | 202 | //////////////////////////////// 203 | // I2S DAC 204 | //////////////////////////////// 205 | 206 | // i2s configuration 207 | static const int i2s_num = 0; // i2s port number 208 | static i2s_config_t i2s_config = { 209 | .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX | I2S_MODE_DAC_BUILT_IN), 210 | .sample_rate = SAMPLING_FREQ, 211 | .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, 212 | .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, 213 | .communication_format = (i2s_comm_format_t)I2S_COMM_FORMAT_I2S_MSB, 214 | .intr_alloc_flags = 0, 215 | .dma_buf_count = DMA_BUF_COUNT, 216 | .dma_buf_len = DMA_BUF_LEN, 217 | .use_apll = 0}; 218 | 219 | static void DAC_create() { 220 | i2s_driver_install((i2s_port_t)i2s_num, &i2s_config, 0, NULL); 221 | i2s_set_pin((i2s_port_t)i2s_num, NULL); 222 | i2s_stop((i2s_port_t)i2s_num); // Create時はstop状態 223 | } 224 | 225 | static void DAC_release() { 226 | i2s_driver_uninstall((i2s_port_t)i2s_num); // stop & destroy i2s driver 227 | } 228 | 229 | static void DAC_start() { 230 | int k; 231 | i2s_start((i2s_port_t)i2s_num); 232 | 233 | for (k = 0; k < DMA_BUF_LEN; k++) { 234 | DAC_write_val(0); 235 | } 236 | for (k = 0; k <= 32768; k += 256) { 237 | DAC_write_val((uint16_t)k); 238 | } 239 | AqResample_Reset(); 240 | } 241 | 242 | static void DAC_stop() { 243 | int k; 244 | for (k = 32768; k >= 0; k -= 256) { 245 | DAC_write_val((uint16_t)k); 246 | } 247 | for (k = 0; k < DMA_BUF_SIZE + I2S_FIFO_LEN; k++) { 248 | DAC_write_val(0); 249 | } 250 | i2s_stop((i2s_port_t)i2s_num); 251 | } 252 | 253 | // upsampling & write to I2S 254 | static int DAC_write(int len, int16_t *wav) { 255 | int i; 256 | for (i = 0; i < len; i++) { 257 | // upsampling x3 258 | int16_t wav3[3]; 259 | AqResample_Conv(wav[i], wav3); 260 | 261 | for (int k = 0; k < 3; k++) { 262 | int iret = 263 | DAC_write_val(((uint16_t)wav3[k]) ^ 264 | 0x8000U); // for Internal-DAC (signed -> unsigned data) 265 | // int iret = DAC_write_val((uint16_t)wav3[k]);// for 266 | //External SPI-DAC 267 | if (iret < 0) return 404; // -1:parameter error 268 | if (iret == 0) break; // 0:TIMEOUT 269 | } 270 | } 271 | return i; 272 | } 273 | 274 | // write to I2S DMA buffer 275 | static int DAC_write_val(uint16_t val) { 276 | uint16_t sample[2]; 277 | sample[0] = sample[1] = val; // mono -> stereo 278 | size_t bytes_written; 279 | esp_err_t iret = i2s_write((i2s_port_t)i2s_num, sample, sizeof(uint16_t) * 2, 280 | &bytes_written, TICKS_TO_WAIT); 281 | if (iret != ESP_OK) return -1; 282 | if (bytes_written < sizeof(uint16_t) * 2) return 0; // timeout 283 | return 1; 284 | } 285 | 286 | /***************************************************************************** 287 | 288 | 辞書データ(aqdic_m.bin)のアクセス関数 289 | 290 | ここで定義する関数は、AquesTalk ESP32ライブラリから呼び出されます。 291 | 辞書データを読み込む機能を、使用するハードウェア構成に応じて実装します。 292 | 293 | 漢字仮名混じり文からの音声合成を行う場合に記述が必須で、 294 | さもなければリンク時にエラーとなります。 295 | 音声記号列からの音声合成だけを使用する場合はこれら関数の記述は不要です。 296 | 297 | 辞書データの配置場所は以下などが考えられます。 298 | ・SDカード上のファイル 299 | ・SPIシリアルフラッシュメモリ 300 | ・メモリマップドされたシリアルフラッシュ 301 | ・マイコン内蔵フラッシュメモリ 302 | 303 | 辞書データは大量かつランダムにアクセスされるので、 304 | この関数の処理量が音声合成のパフォーマンスに与える影響は大きいです。 305 | 306 | ******************************************************************************/ 307 | 308 | /*--------------------------------------------------------------------- 309 | 以下のコードはM5StackのSDカードに辞書データファイルを書き込んで使用する場合。 310 | メインプログラムでM5.begin()を呼び出してSD.begin()しておくことを忘れずに 311 | ------------------------------------------------------------------------*/ 312 | #include // ArduinoIDE 313 | 314 | // SDのaq_dicフォルダの下に辞書データファイル(aqdic_m.bin)を配置 315 | #define FILE_DIC "/aq_dic/aqdic_m.bin" 316 | 317 | // 仮想的な辞書データの先頭アドレス 318 | // (NULL以外なら任意で良い。但し4byteアライメント) 319 | #define ADDR_ORG (0x10001000) 320 | 321 | static File fp; 322 | 323 | //----------------------------------------------------------------- 324 | // 辞書データアクセスの初期化 325 | // CAqK2R_Create()内から一度だけ呼び出される 326 | // 戻り値 327 | // 仮想的な辞書データの先頭アドレスを返す(0以外。4byteアライメント)。 328 | // エラーのときは0を返す 329 | extern "C" size_t aqdic_open() { 330 | fp = SD.open(FILE_DIC); 331 | if (!fp) return 0; // err 332 | return ADDR_ORG; // ok 333 | } 334 | 335 | //----------------------------------------------------------------- 336 | // 辞書データアクセスの終了 337 | // CAqK2R_Release()内から一度だけ呼び出される 338 | extern "C" void aqdic_close() { 339 | if (fp) fp.close(); 340 | } 341 | 342 | //----------------------------------------------------------------- 343 | // 辞書データの読み込み 344 | // pos: 先頭アドレス[byte] 345 | // size: 読み込むサイズ[byte] 346 | // buf: 読み込むデータ配列 uint8_t(size) 347 | // 戻り値: 読みこんだバイト数 348 | // CAqK2R_Convert()/CAqK2R_ConvertW()から複数回呼び出される 349 | extern "C" size_t aqdic_read(size_t pos, size_t size, void *buf) { 350 | fp.seek(pos - ADDR_ORG); 351 | return fp.read((uint8_t *)buf, size); 352 | } 353 | -------------------------------------------------------------------------------- /src/Eyes.cpp: -------------------------------------------------------------------------------- 1 | #include "Eyes.hpp" 2 | 3 | namespace m5avatar { 4 | BaseEye::BaseEye(bool is_left) : BaseEye(36, 70, is_left) {} 5 | 6 | BaseEye::BaseEye(uint16_t width, uint16_t height, bool is_left) { 7 | this->width_ = width; 8 | this->height_ = height; 9 | this->is_left_ = is_left; 10 | } 11 | 12 | void BaseEye::update(M5Canvas *canvas, BoundingRect rect, DrawContext *ctx) { 13 | // common process for all standard eyes 14 | // update drawing parameters 15 | center_x_ = rect.getCenterX(); 16 | center_y_ = rect.getCenterY(); 17 | gaze_ = this->is_left_ ? ctx->getLeftGaze() : ctx->getRightGaze(); 18 | ColorPalette *cp = ctx->getColorPalette(); 19 | primary_color_ = ctx->getColorDepth() == 1 ? 1 : cp->get(COLOR_PRIMARY); 20 | secondary_color_ = ctx->getColorDepth() == 1 21 | ? 1 22 | : ctx->getColorPalette()->get(COLOR_SECONDARY); 23 | background_color_ = 24 | ctx->getColorDepth() == 1 ? ERACER_COLOR : cp->get(COLOR_BACKGROUND); 25 | 26 | // offset computed from gaze direction 27 | shifted_x_ = center_x_ + gaze_.getHorizontal() * 8; 28 | shifted_y_ = center_y_ + gaze_.getVertical() * 5; 29 | open_ratio_ = this->is_left_ ? ctx->getLeftEyeOpenRatio() 30 | : ctx->getRightEyeOpenRatio(); 31 | expression_ = ctx->getExpression(); 32 | } 33 | 34 | void EllipseEye::draw(M5Canvas *canvas, BoundingRect rect, DrawContext *ctx) { 35 | this->update(canvas, rect, ctx); 36 | if (open_ratio_ == 0 || expression_ == Expression::Sleepy) { 37 | // eye closed 38 | // NOTE: the center of closed eye is lower than the center of bbox 39 | canvas->fillRect(shifted_x_ - (this->width_ / 2), 40 | shifted_y_ - 2 + this->height_ / 4, this->width_, 4, 41 | primary_color_); 42 | return; 43 | } else if (expression_ == Expression::Happy) { 44 | auto wink_base_y = shifted_y_ + this->height_ / 4; 45 | uint32_t thickness = 4; 46 | canvas->fillEllipse(shifted_x_, wink_base_y + (1 / 8) * this->height_, 47 | this->width_ / 2, this->height_ / 4 + thickness, 48 | primary_color_); 49 | // mask 50 | canvas->fillEllipse(shifted_x_, 51 | wink_base_y + (1 / 8) * this->height_ + thickness, 52 | this->width_ / 2 - thickness, 53 | this->height_ / 4 + thickness, background_color_); 54 | canvas->fillRect(shifted_x_ - this->width_ / 2, 55 | wink_base_y + thickness / 2, this->width_ + 1, 56 | this->height_ / 4 + 1, background_color_); 57 | return; 58 | } 59 | 60 | canvas->fillEllipse(shifted_x_, shifted_y_, this->width_ / 2, 61 | this->height_ / 2, primary_color_); 62 | 63 | // note: you cannot define variable in switch scope 64 | int x0, y0, x1, y1, x2, y2; 65 | switch (expression_) { 66 | case Expression::Angry: 67 | x0 = shifted_x_ - width_ / 2; 68 | y0 = shifted_y_ - height_ / 2; 69 | x1 = shifted_x_ + width_ / 2; 70 | y1 = y0; 71 | x2 = this->is_left_ ? x0 : x1; 72 | y2 = shifted_y_ - height_ / 4; 73 | canvas->fillTriangle(x0, y0, x1, y1, x2, y2, background_color_); 74 | break; 75 | case Expression::Sad: 76 | x0 = shifted_x_ - width_ / 2; 77 | y0 = shifted_y_ - height_ / 2; 78 | x1 = shifted_x_ + width_ / 2; 79 | y1 = y0; 80 | x2 = this->is_left_ ? x1 : x0; 81 | y2 = shifted_y_ - height_ / 4; 82 | canvas->fillTriangle(x0, y0, x1, y1, x2, y2, background_color_); 83 | break; 84 | case Expression::Doubt: 85 | // top left 86 | x0 = shifted_x_ - width_ / 2; 87 | y0 = shifted_y_ - height_ / 2; 88 | // bottom right 89 | x1 = shifted_x_ + width_ / 2; 90 | y1 = shifted_y_ - height_ / 4; 91 | 92 | canvas->fillRect(x0, y0, x1 - x0, y1 - y0, background_color_); 93 | break; 94 | case Expression::Sleepy: 95 | break; 96 | 97 | default: 98 | break; 99 | } 100 | } 101 | 102 | void GirlyEye::drawEyeLid(M5Canvas *canvas) { 103 | // eyelid 104 | auto upper_eyelid_y = shifted_y_ - 0.8f * height_ / 2 + 105 | (1.0f - open_ratio_) * this->height_ * 0.6; 106 | 107 | float eyelash_x0, eyelash_y0, eyelash_x1, eyelash_y1, eyelash_x2, 108 | eyelash_y2; 109 | eyelash_x0 = this->is_left_ ? shifted_x_ + 22 : shifted_x_ - 22; 110 | eyelash_y0 = upper_eyelid_y - 27; 111 | eyelash_x1 = this->is_left_ ? shifted_x_ + 26 : shifted_x_ - 26; 112 | eyelash_y1 = upper_eyelid_y; 113 | eyelash_x2 = this->is_left_ ? shifted_x_ - 10 : shifted_x_ + 10; 114 | eyelash_y2 = upper_eyelid_y; 115 | 116 | float tilt = 0.0f; 117 | float ref_tilt = open_ratio_ * M_PI / 6.0f; 118 | float bias; 119 | if (expression_ == Expression::Angry) { 120 | tilt = this->is_left_ ? -ref_tilt : ref_tilt; 121 | } else if (expression_ == Expression::Sad) { 122 | tilt = this->is_left_ ? ref_tilt : -ref_tilt; 123 | } 124 | bias = 0.2f * width_ * tilt / (M_PI / 6.0f); 125 | 126 | if ((open_ratio_ < 0.99f) || (abs(tilt) > 0.1f)) { 127 | // mask 128 | // top:shifted_y_ - this->height_ / 2 129 | // bottom: upper_eyelid_y 130 | float mask_top_left_x = shifted_x_ - (this->width_ / 2); 131 | float mask_top_left_y = shifted_y_ - 0.75f * this->height_; 132 | float mask_bottom_right_x = shifted_x_ + (this->width_ / 2); 133 | float mask_bottom_right_y = upper_eyelid_y; 134 | 135 | fillRectRotatedAround(canvas, mask_top_left_x, mask_top_left_y, 136 | mask_bottom_right_x, mask_bottom_right_y, tilt, 137 | shifted_x_, upper_eyelid_y, background_color_); 138 | 139 | // eyelid 140 | float eyelid_top_left_x = shifted_x_ - (this->width_ / 2) + bias; 141 | float eyelid_top_left_y = upper_eyelid_y - 4; 142 | float eyelid_bottom_right_x = shifted_x_ + (this->width_ / 2) + bias; 143 | float eyelid_bottom_right_y = upper_eyelid_y; 144 | 145 | fillRectRotatedAround(canvas, eyelid_top_left_x, eyelid_top_left_y, 146 | eyelid_bottom_right_x, eyelid_bottom_right_y, 147 | tilt, shifted_x_, upper_eyelid_y, primary_color_); 148 | 149 | eyelash_x0 += bias; 150 | eyelash_x1 += bias; 151 | eyelash_x2 += bias; 152 | } 153 | 154 | // eyelash 155 | rotatePointAround(eyelash_x0, eyelash_y0, tilt, shifted_x_, upper_eyelid_y); 156 | rotatePointAround(eyelash_x1, eyelash_y1, tilt, shifted_x_, upper_eyelid_y); 157 | rotatePointAround(eyelash_x2, eyelash_y2, tilt, shifted_x_, upper_eyelid_y); 158 | canvas->fillTriangle(eyelash_x0, eyelash_y0, eyelash_x1, eyelash_y1, 159 | eyelash_x2, eyelash_y2, primary_color_); 160 | } 161 | 162 | void GirlyEye::overwriteOpenRatio() { 163 | switch (expression_) { 164 | case Expression::Doubt: 165 | open_ratio_ = 0.6f; 166 | break; 167 | 168 | case Expression::Sleepy: 169 | open_ratio_ = 0.0f; 170 | break; 171 | } 172 | } 173 | 174 | void GirlyEye::draw(M5Canvas *canvas, BoundingRect rect, DrawContext *ctx) { 175 | this->update(canvas, rect, ctx); 176 | this->overwriteOpenRatio(); 177 | auto wink_base_y = shifted_y_ + (1.0f - open_ratio_) * this->height_ / 4; 178 | 179 | uint32_t thickness = 4; 180 | if (expression_ == Expression::Happy) { 181 | canvas->fillEllipse(shifted_x_, wink_base_y + (1 / 8) * this->height_, 182 | this->width_ / 2, this->height_ / 4 + thickness, 183 | primary_color_); 184 | // mask 185 | canvas->fillEllipse(shifted_x_, 186 | wink_base_y + (1 / 8) * this->height_ + thickness, 187 | this->width_ / 2 - thickness, 188 | this->height_ / 4 + thickness, background_color_); 189 | canvas->fillRect(shifted_x_ - this->width_ / 2, 190 | wink_base_y + thickness / 2, this->width_, 191 | this->height_ / 4, background_color_); 192 | // this->drawEyeLid(canvas); 193 | return; 194 | } 195 | // main eye 196 | if (open_ratio_ > 0.1f) { 197 | // bg 198 | canvas->fillEllipse(shifted_x_, shifted_y_, this->width_ / 2, 199 | this->height_ / 2, primary_color_); 200 | 201 | uint16_t accent_color = M5.Lcd.color24to16(0x019E73); 202 | canvas->fillEllipse(shifted_x_, shifted_y_, 203 | this->width_ / 2 - thickness, 204 | this->height_ / 2 - thickness, accent_color); 205 | // upper half moon 206 | canvas->fillArc(shifted_x_, shifted_y_, width_ / 2, 0, 180.0f, 360.0f, 207 | primary_color_); 208 | 209 | canvas->fillEllipse(shifted_x_, shifted_y_, this->width_ / 4, 210 | this->height_ / 4, primary_color_); 211 | // high light 212 | canvas->fillEllipse(shifted_x_ - width_ / 6, shifted_y_ - height_ / 6, 213 | width_ / 8, height_ / 8, 0xffffff); 214 | } 215 | this->drawEyeLid(canvas); 216 | } 217 | 218 | void PinkDemonEye::drawEyeLid(M5Canvas *canvas) { 219 | // eyelid 220 | auto upper_eyelid_y = shifted_y_ - 0.8f * height_ / 2 + 221 | (1.0f - open_ratio_) * this->height_ * 0.6; 222 | 223 | float eyelash_x0, eyelash_y0, eyelash_x1, eyelash_y1, eyelash_x2, 224 | eyelash_y2; 225 | eyelash_x0 = this->is_left_ ? shifted_x_ + 22 : shifted_x_ - 22; 226 | eyelash_y0 = upper_eyelid_y - 27; 227 | eyelash_x1 = this->is_left_ ? shifted_x_ + 26 : shifted_x_ - 26; 228 | eyelash_y1 = upper_eyelid_y; 229 | eyelash_x2 = this->is_left_ ? shifted_x_ - 10 : shifted_x_ + 10; 230 | eyelash_y2 = upper_eyelid_y; 231 | 232 | float tilt = 0.0f; 233 | float ref_tilt = open_ratio_ * M_PI / 6.0f; 234 | if (expression_ == Expression::Angry) { 235 | tilt = this->is_left_ ? -ref_tilt : ref_tilt; 236 | } else if (expression_ == Expression::Sad) { 237 | tilt = this->is_left_ ? ref_tilt : -ref_tilt; 238 | } 239 | 240 | if ((open_ratio_ < 0.99f) || (abs(tilt) > 0.1f)) { 241 | // mask 242 | // top:shifted_y_ - this->height_ / 2 243 | // bottom: upper_eyelid_y 244 | float mask_top_left_x = shifted_x_ - (this->width_ / 2); 245 | float mask_top_left_y = shifted_y_ - 0.75f * this->height_; 246 | float mask_bottom_right_x = shifted_x_ + (this->width_ / 2); 247 | float mask_bottom_right_y = upper_eyelid_y; 248 | 249 | fillRectRotatedAround(canvas, mask_top_left_x, mask_top_left_y, 250 | mask_bottom_right_x, mask_bottom_right_y, tilt, 251 | shifted_x_, upper_eyelid_y, background_color_); 252 | 253 | // eyelid 254 | float eyelid_top_left_x = shifted_x_ - (this->width_ / 2); 255 | float eyelid_top_left_y = upper_eyelid_y - 4; 256 | float eyelid_bottom_right_x = shifted_x_ + (this->width_ / 2); 257 | float eyelid_bottom_right_y = upper_eyelid_y; 258 | 259 | fillRectRotatedAround(canvas, eyelid_top_left_x, eyelid_top_left_y, 260 | eyelid_bottom_right_x, eyelid_bottom_right_y, 261 | tilt, shifted_x_, upper_eyelid_y, primary_color_); 262 | } 263 | 264 | // eyelash 265 | rotatePointAround(eyelash_x0, eyelash_y0, tilt, shifted_x_, upper_eyelid_y); 266 | rotatePointAround(eyelash_x1, eyelash_y1, tilt, shifted_x_, upper_eyelid_y); 267 | rotatePointAround(eyelash_x2, eyelash_y2, tilt, shifted_x_, upper_eyelid_y); 268 | } 269 | 270 | void PinkDemonEye::overwriteOpenRatio() { 271 | switch (expression_) { 272 | case Expression::Doubt: 273 | open_ratio_ = 0.6f; 274 | break; 275 | 276 | case Expression::Sleepy: 277 | open_ratio_ = 0.0f; 278 | break; 279 | } 280 | } 281 | 282 | void PinkDemonEye::draw(M5Canvas *canvas, BoundingRect rect, DrawContext *ctx) { 283 | this->update(canvas, rect, ctx); 284 | this->overwriteOpenRatio(); 285 | uint32_t thickness = 8; 286 | 287 | // main eye 288 | if (open_ratio_ > 0.1f) { 289 | // bg 290 | canvas->fillEllipse(shifted_x_, shifted_y_, this->width_ / 2, 291 | this->height_ / 2, primary_color_); 292 | uint16_t accent_color = M5.Lcd.color24to16(0x00A1FF); 293 | canvas->fillEllipse(shifted_x_, shifted_y_, 294 | this->width_ / 2 - thickness, 295 | this->height_ / 2 - thickness, accent_color); 296 | // upper 297 | uint16_t w1 = width_ * 0.92f; 298 | uint16_t h1 = this->height_ * 0.69f; 299 | uint16_t y1 = shifted_y_ - this->height_ / 2 + h1 / 2; 300 | canvas->fillEllipse(shifted_x_, y1, w1 / 2, h1 / 2, primary_color_); 301 | // high light 302 | uint16_t w2 = width_ * 0.577f; 303 | uint16_t h2 = this->height_ * 0.4f; 304 | uint16_t y2 = shifted_y_ - this->height_ / 2 + thickness + h2 / 2; 305 | 306 | canvas->fillEllipse(shifted_x_, y2, w2 / 2, h2 / 2, 0xffffff); 307 | } 308 | this->drawEyeLid(canvas); 309 | } 310 | 311 | void DoggyEye::draw(M5Canvas *canvas, BoundingRect rect, DrawContext *ctx) { 312 | this->update(canvas, rect, ctx); 313 | 314 | if (this->open_ratio_ == 0) { 315 | // eye closed 316 | canvas->fillRect(center_x_ - 15, center_y_ - 2, 30, 4, primary_color_); 317 | return; 318 | } 319 | canvas->fillEllipse(center_x_, center_y_, 30, 25, primary_color_); 320 | canvas->fillEllipse(center_x_, center_y_, 28, 23, background_color_); 321 | 322 | canvas->fillEllipse(shifted_x_, shifted_y_, 18, 18, primary_color_); 323 | canvas->fillEllipse(shifted_x_ - 3, shifted_y_ - 3, 3, 3, 324 | background_color_); 325 | } 326 | 327 | } // namespace m5avatar 328 | --------------------------------------------------------------------------------