Skip to content

Commit a32a41f

Browse files
authored
Merge pull request #93 from botamochi6277/face_custom
Add Face templates, custom eyes, mouths, and eyebrows
2 parents 9ced351 + b3df43c commit a32a41f

12 files changed

Lines changed: 1145 additions & 115 deletions

File tree

docs/FaceCustomizationGuideline.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Customization Guideline
2+
3+
## Anatomy of Face
4+
5+
<img src="https://github.com/user-attachments/assets/0f9da4df-f5f6-48ce-adc6-ac0405041b6f" width=50% alt="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+
|<img src="https://github.com/user-attachments/assets/5908e69f-9674-43df-a933-9f8d24fa488c/" width=20% alt="SimpleFace">|`SimpleFace`| `EllipseEye` | `RectMouth` | -- | Alternative implementation of (Native) `Face`
24+
|<img src="https://github.com/user-attachments/assets/5457eaec-a774-46f5-90dd-7ee1a3e13c03" width=20% alt="DoggyFace">|`DoggyFace`|`DoggyEye`|`DoggyMouth`|`RectEyebrow`|Alternative implementation of `DogFace`|
25+
|<img src="https://github.com/user-attachments/assets/0ddd047a-76bf-450f-8a32-d245fdc40380" width=20% alt="OmegaFace">|`OmegaFace`|`EllipseEye`|`OmegaMouth`|--||
26+
|<img src="https://github.com/user-attachments/assets/fc0a5d3f-bf0d-4563-aa19-1e2d565e83aa" width=20% alt="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
Lines changed: 79 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,97 @@
1-
#include <M5Unified.h>
21
#include <Avatar.h>
3-
#include <faces/DogFace.h>
2+
#include <M5Unified.h>
3+
4+
#include <faces/FaceTemplates.hpp>
45

56
using namespace m5avatar;
67

78
Avatar avatar;
89

9-
Face* faces[2];
10-
const int facesSize = sizeof(faces) / sizeof(Face*);
11-
int faceIdx = 0;
10+
Face* faces[6];
11+
const int num_faces = sizeof(faces) / sizeof(Face*);
12+
int face_idx = 0; // face index
1213

13-
const Expression expressions[] = {
14-
Expression::Angry,
15-
Expression::Sleepy,
16-
Expression::Happy,
17-
Expression::Sad,
18-
Expression::Doubt,
19-
Expression::Neutral
20-
};
21-
const int expressionsSize = sizeof(expressions) / sizeof(Expression);
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);
2218
int idx = 0;
2319

24-
ColorPalette* cps[4];
25-
const int cpsSize = sizeof(cps) / sizeof(ColorPalette*);
26-
int cpsIdx = 0;
20+
ColorPalette* color_palettes[5];
21+
const int num_palettes = sizeof(color_palettes) / sizeof(ColorPalette*);
22+
int palette_idx = 0;
2723

2824
bool isShowingQR = false;
2925

30-
void setup()
31-
{
32-
M5.begin();
33-
M5.Lcd.setBrightness(30);
34-
M5.Lcd.clear();
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();
3547

36-
faces[0] = avatar.getFace();
37-
faces[1] = new DogFace();
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();
3854

39-
cps[0] = new ColorPalette();
40-
cps[1] = new ColorPalette();
41-
cps[2] = new ColorPalette();
42-
cps[3] = new ColorPalette();
43-
cps[1]->set(COLOR_PRIMARY, TFT_YELLOW);
44-
cps[1]->set(COLOR_BACKGROUND, TFT_DARKCYAN);
45-
cps[2]->set(COLOR_PRIMARY, TFT_DARKGREY);
46-
cps[2]->set(COLOR_BACKGROUND, TFT_WHITE);
47-
cps[3]->set(COLOR_PRIMARY, TFT_RED);
48-
cps[3]->set(COLOR_BACKGROUND, TFT_PINK);
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);
4972

50-
avatar.init();
51-
avatar.setColorPalette(*cps[0]);
73+
avatar.init(8); // start drawing w/ 8bit color mode
74+
avatar.setColorPalette(*color_palettes[0]);
5275
}
5376

54-
void loop()
55-
{
56-
M5.update();
57-
if (M5.BtnA.wasPressed())
58-
{
59-
avatar.setFace(faces[faceIdx]);
60-
faceIdx = (faceIdx + 1) % facesSize;
61-
}
62-
if (M5.BtnB.wasPressed())
63-
{
64-
avatar.setColorPalette(*cps[cpsIdx]);
65-
cpsIdx = (cpsIdx + 1) % cpsSize;
66-
}
67-
if (M5.BtnC.wasPressed())
68-
{
69-
avatar.setExpression(expressions[idx]);
70-
idx = (idx + 1) % expressionsSize;
71-
}
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+
}
7297
}

src/DrawingUtils.cpp

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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

src/DrawingUtils.hpp

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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 <BoundingRect.h>
16+
#include <DrawContext.h>
17+
#include <Drawable.h>
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/Eyebrows.cpp

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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

0 commit comments

Comments
 (0)