M5stack で画面アニメーションと独自の16セグメント表示の件

先日アップしたM5Stack で RFID を読み込んで mp3 ファイルを鳴らす
を組み込んだヘンシンデバイスを作った話の基盤の件です。

どうせ作るなら、リッチな画面がよかったので次みたいな M5stack の画面をアニメーションで作りました。

本番では、複数の画面を作っていますがそのうちいくつかを紹介します。

では本編

目次

基盤 - 平行四辺形編

画面作成に当たって、欲しいけど M5stack の LCD 関係の標準関数に無いものがあるので(実際は有ったらごめん)、
平行四辺形の作成関数関数を作る。

sample.ino
1
2
3
4
5
6
7
8
//スプライト確保
TFT_eSprite sp = TFT_eSprite(&M5.Lcd);

void Trapezoid(int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4, uint32_t c) {
sp.fillTriangle(x1, y1, x2, y2, x3, y3, c);
sp.fillTriangle(x2, y2, x3, y3, x4, y4, c);
}

なんてことはない 3 角形を2つくっつければ、二等辺三角形は作れる。
スプライトに出力したかったので、Trapezoid()関数の中でスプライトの指定座標に書き込むようにする。
これで次の 16 セグメント表示に必要な斜めの線の作ることができる。

基盤 - 16 セグメント編

16 セグメント表示をする関数群は以下のような方針で作成した。

  • 16 セグメントの各線それぞれを基準の座標に表示する関数を定義 ①
  • 数字,文字を ① で表現する関数を作成 ②
  • 文字列を ② を使って表現する関数を定義
  • 数字列は 4 桁で 9999 通りあり個別定義できないので、関数処理する。
  • 文字列は各画面で出したい文字は決まっているので別個に作成する。
    (今思えばアルファベットなら 26 パターンなので、作ってもよかったかもしれない)
16seg.ino(長いので一部抜粋)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
#include <M5Stack.h>
TFT_eSprite sp = TFT_eSprite(&M5.Lcd);

void Trapezoid(int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4, uint32_t c) {
sp.fillTriangle(x1, y1, x2, y2, x3, y3, c);
sp.fillTriangle(x2, y2, x3, y3, x4, y4, c);
}

void T_1(int p, uint32_t c) {
sp.fillRect(pos(p) + 10, 70, 20, 10, c);
}
void T_2(int p, uint32_t c) {
sp.fillRect(pos(p) + 40, 70, 20, 10, c);
}

void T_3(int p, uint32_t c) {
sp.fillRect(pos(p), 80, 10, 40, c);
}
void T_4(int p, uint32_t c) {
sp.fillRect(pos(p) + 30, 80, 10, 40, c);
}
void T_5(int p, uint32_t c) {
sp.fillRect(pos(p) + 60, 80, 10, 40, c);
}

void T_6(int p, uint32_t c) {
sp.fillRect(pos(p) + 10, 120, 20, 10, c);
}
void T_7(int p, uint32_t c) {
sp.fillRect(pos(p) + 40, 120, 20, 10, c);
}
void T_8(int p, uint32_t c) {
sp.fillRect(pos(p), 130, 10, 40, c);
}
void T_9(int p, uint32_t c) {
sp.fillRect(pos(p) + 30, 130, 10, 40, c);
}
void T_10(int p, uint32_t c) {
sp.fillRect(pos(p) + 60, 130, 10, 40, c);
}

void T_11(int p, uint32_t c) {
sp.fillRect(pos(p) + 10, 170, 20, 10, c);
}
void T_12(int p, uint32_t c) {
sp.fillRect(pos(p) + 40, 170, 20, 10, c);
}
void TF_Nums(char *timestr, uint32_t c) {
TF_Num(0, timestr[0], c);
TF_Num(1, timestr[1], c);
TF_Num(2, timestr[2], c);
TF_Num(3, timestr[3], c);
}

void TF_Num(int p, char num, uint32_t c) {
switch (num) {
case '0': TF_0(p, c);
break;
case '1': TF_1(p, c);
break;
case '2': TF_2(p, c);
break;
case '3': TF_3(p, c);
break;
case '4': TF_4(p, c);
break;
case '5': TF_5(p, c);
break;
case '6': TF_6(p, c);
break;
case '7': TF_7(p, c);
break;
case '8': TF_8(p, c);
break;
case '9': TF_9(p, c);
break;
}
}

void TF_MENU(uint32_t c) {
//M
TF_M(0, c);
//E
TF_E(1, c);
//N
TF_N(2, c);
//U
TF_U(3, c);
}
void TF_BOOT(uint32_t c) {
//B
TF_B(0, c);
//O
TF_O(1, c);
//O
TF_O(2, c);
//T
TF_T(3, c);
}

//座標計算
int pos(int p) {
int offset = 15;
return p * 75 + offset;
}

void T_13(int p, uint32_t c) {
Trapezoid(pos(p) + 11, 82, pos(p) + 21, 82, pos(p) + 19, 118, pos(p) + 29, 118, c);
}
void T_14(int p, uint32_t c) {
Trapezoid(pos(p) + 48, 82, pos(p) + 58, 82, pos(p) + 41, 118, pos(p) + 51, 118, c);
}
void T_15(int p, uint32_t c) {
Trapezoid(pos(p) + 19, 132, pos(p) + 29, 132, pos(p) + 11, 168, pos(p) + 21, 168, c);
}
void T_16(int p, uint32_t c) {
Trapezoid(pos(p) + 41, 132, pos(p) + 51, 132, pos(p) + 49, 168, pos(p) + 59, 168, c);
}

void TF_B(int p, uint32_t c) {
T_1(p, c);
T_2(p, c);
T_3(p, c);
T_6(p, c);
T_8(p, c);
T_11(p, c);
T_12(p, c);
T_14(p, c);
T_16(p, c);

}
void TF_O(int p, uint32_t c) {
T_1(p, c);
T_2(p, c);
T_3(p, c);
T_5(p, c);
T_8(p, c);
T_10(p, c);
T_11(p, c);
T_12(p, c);
}
void TF_T(int p, uint32_t c) {
T_1(p, c);
T_2(p, c);
T_4(p, c);
T_9(p, c);
}

void TF_M(int p, uint32_t c) {
T_3(p, c);
T_5(p, c);
T_8(p, c);
T_9(p, c);
T_10(p, c);
T_13(p, c);
T_14(p, c);
}
void TF_E(int p, uint32_t c) {
T_1(p, c);
T_2(p, c);
T_3(p, c);
T_6(p, c);
T_7(p, c);
T_8(p, c);
T_11(p, c);
T_12(p, c);
}
void TF_N(int p, uint32_t c) {
T_3(p, c);
T_5(p, c);
T_8(p, c);
T_10(p, c);

T_13(p, c);
T_16(p, c);
}
void TF_U(int p, uint32_t c) {
T_3(p, c);
T_5(p, c);
T_8(p, c);
T_10(p, c);
T_11(p, c);
T_12(p, c);
}

void TF_0(int p, uint32_t c) {
T_1(p, c);
T_2(p, c);
T_3(p, c);
T_5(p, c);
T_8(p, c);
T_10(p, c);
T_11(p, c);
T_12(p, c);
}

void TF_1(int p, uint32_t c) {
T_1(p, c);
T_4(p, c);
T_9(p, c);
T_11(p, c);
T_12(p, c);
}
void TF_2(int p, uint32_t c) {
T_1(p, c);
T_2(p, c);
T_3(p, c);
T_11(p, c);
T_12(p, c);
T_14(p, c);
T_15(p, c);
}
void TF_3(int p, uint32_t c) {
T_1(p, c);
T_2(p, c);
T_5(p, c);
T_7(p, c);
T_10(p, c);
T_11(p, c);
T_12(p, c);
}
void TF_4(int p, uint32_t c) {
T_3(p, c);
T_5(p, c);
T_6(p, c);
T_7(p, c);
T_10(p, c);
}
void TF_5(int p, uint32_t c) {
T_1(p, c);
T_2(p, c);
T_3(p, c);
T_6(p, c);
T_7(p, c);
T_10(p, c);
T_11(p, c);
T_12(p, c);
}
void TF_6(int p, uint32_t c) {
T_1(p, c);
T_2(p, c);
T_3(p, c);
T_6(p, c);
T_7(p, c);
T_8(p, c);
T_10(p, c);
T_11(p, c);
T_12(p, c);
}
void TF_7(int p, uint32_t c) {
T_1(p, c);
T_2(p, c);
T_5(p, c);
T_10(p, c);
}
void TF_8(int p, uint32_t c) {
T_1(p, c);
T_2(p, c);
T_3(p, c);
T_5(p, c);
T_6(p, c);
T_7(p, c);
T_8(p, c);
T_10(p, c);
T_11(p, c);
T_12(p, c);
}
void TF_9(int p, uint32_t c) {
T_1(p, c);
T_2(p, c);
T_3(p, c);
T_5(p, c);
T_6(p, c);
T_7(p, c);
T_10(p, c);
T_11(p, c);
T_12(p, c);
}
}```
T_*()関数は個別の線を描画する。
TF_*()関数は文字及び、文字列と数字列を表示する。
これで16セグメントの数字列と文字列を4文字M5stackに表示できる。

## アニメーション
位置を動かす、もしくは一定間隔で表示を変えていけばそれはアニメーションになる。

作成したヘンシンブレスのプログラムの起動画面とメニュー画面は以下のようになっている。
(アニメーションとUIの部分だけ抜粋,そして元が汚かったので一部書き直し・・・。)
```c M5stack-Anim.ino
#include <M5Stack.h>

//menuモード用変数
int menumode = 0;
int menuselect = 0;
int count_m = 0;

//以下各モード用の変数を定義しておく
//
//

void setup() {
M5.begin();
//起動演出
for (int i = 1; i < 31; i++) {
TFT_Boot(i);
delay(20);
}
}

void loop() {
M5.update();

//menu画面操作
if (menumode == 0) {
TFT_Menu(count_m, menuselect);
if (count_m < 33) {
count_m++;
}

if (M5.BtnA.wasPressed() ) {
menuselect--;
if (menuselect < 1) {
menuselect = 5;
}
}
else if (M5.BtnB.wasPressed() ) {
menuselect++;
if (menuselect > 5) {
menuselect = 1;
}
}
else if (M5.BtnC.wasPressed()) {
if (menuselect != 0) {
count_m = 0;
menumode = menuselect;
}
}
}
///以下ほかの画面などを定義する
///
///

delay(20);
}
view.ino
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
#include <M5Stack.h>

void TFT_Boot(int time ) {

//テーマカラー
uint32_t c = 0xfb40;
//初期設定
sp.setColorDepth(8);
sp.createSprite(320, 240);

float m0 = 0;
float m1 = 10.0;
float m2 = 20.0;

float n1 = m1;
float n2 = m2 + n1;

if (m1 >= time && time > m0) {
//背景は黒
sp.fillRect(00, 0, 320, 240, BLACK);
//カーテン上下
//int t = 120 / m1 *(time);
// sp.fillRect(0, 130-t, 320, 2*t, c);
int t = 240 / m1 * (time);
sp.fillRect(0, 0, 320, t, c);
}
else if ( n2 > time && time > n1) {

//上下に開く
int t = 120 / m2 * (time - n1 );

//BOOT
TF_BOOT(c);

//上下帯
sp.fillRect(0, 20, 320, 40, c);
sp.fillRect(0, 190, 320, 40, c);

sp.fillRect(0, 0, 320, 130 - t, BLACK);
sp.fillRect(0, 130 + t, 320, 120 - t, BLACK);

sp.fillRect(0, 0, 320, 130 - t, c);
sp.fillRect(0, 130 + t, 320, 120 - t, c);

}
else {
//BOOT
TF_BOOT(c);
//上下帯
sp.fillRect(0, 20, 320, 40, c);
sp.fillRect(0, 190, 320, 40, c);
}
//表示
sp.pushSprite(0, 0);
}
void TFT_Menu(int time, int select) {
//テーマカラー
uint32_t c = 0x37e4;

//初期設定
sp.setColorDepth(8);
sp.createSprite(320, 240);

if (10 >= time) {
int t = 240 / 10 * (time );
sp.fillRect(0, 0, 320, 240, BLACK);

//MENU
TF_MENU(c);

//上下帯
sp.fillRect(0, 20, 320, 40, c);
sp.fillRect(0, 190, 320, 40, c);

//表示
sp.pushSprite(240 - t, 0);
}
else if (15 >= time) {
int t = 170 / 10 * (time - 10 );
sp.fillRect(0, 0, 320, 240, BLACK);

sp.fillRect(0, 20, 320, 40, c);
sp.fillRect(0, 190 - t, 320, 40, c);

//表示
sp.pushSprite(0, 0);
}
else if (20 >= time) {
int t = 45 / 5 * (time - 15 );
sp.fillRect(0, 0, 320, 240, BLACK);
for (int i = 4; i > 0; i--) {
if (i == 4) {

sp.setTextSize(2);
sp.setTextColor(BLACK);

sp.fillRect(20, t * i + 20, 280, 26, c);

sp.fillRect(40, t * i + 22, 54, 22, BLACK);
sp.fillRect(42, t * i + 23, 50, 20, c);
sp.drawString("BACK", 44, t * i + 25, 1);

sp.fillRect(130, t * i + 22, 54, 22, BLACK);
sp.fillRect(132, t * i + 23, 50, 20, c);
sp.drawString("NEXT", 134, t * i + 25, 1);

sp.fillRect(227, t * i + 22, 70, 22, BLACK);
sp.fillRect(229, t * i + 23, 66, 20, c);
sp.drawString("ENTER", 234, t * i + 25, 1);

}
else {
char *str = " ";
if (select >= 4) {
select -= 2;
if (i == 1) {
str = "VOLUME";
}
if (i == 2) {
str = " BRIGHT";
}
if (i == 3) {
str = "CHANGE";
}
if (select == i) {
sp.setTextSize(5);
sp.setTextColor(BLACK);
//sp.fillRect(20, t*i+20, 280, 40, BLACK);
sp.fillRect(20, t * i + 21, 280, 38, c);
sp.drawString(str, 44, t * i + 23, 1);
}
else {
sp.setTextSize(5);
sp.setTextColor(c);
sp.fillRect(20, t * i + 20, 280, 40, c);
sp.fillRect(24, t * i + 21, 272, 38, BLACK);
sp.drawString(str, 44, t * i + 23, 1);
}
select += 2;
} else {
if (i == 1) {
str = "TIME";
}
if (i == 2) {
str = "CONTROL";
}
if (i == 3) {
str = "VOLUME";
}
if (select == i) {
sp.setTextSize(5);
sp.setTextColor(BLACK);
//sp.fillRect(20, t*i+20, 280, 40, BLACK);
sp.fillRect(20, t * i + 21, 280, 38, c);
sp.drawString(str, 44, t * i + 23, 1);
}
else {
sp.setTextSize(5);
sp.setTextColor(c);
sp.fillRect(20, t * i + 20, 280, 40, c);
sp.fillRect(24, t * i + 21, 272, 38, BLACK);
sp.drawString(str, 44, t * i + 23, 1);
}
}
}
}
sp.fillRect(20, 20, 280, 40, c);
sp.setTextSize(3);
sp.setTextColor(BLACK);
sp.drawString("MENU", 44, 32, 1);

//表示
sp.pushSprite(0, 0);
}
else {
sp.fillRect(0, 0, 320, 240, BLACK);
for (int i = 4; i > 0; i--) {
int h = i * 45;
if (i == 4) {
sp.setTextSize(2);
sp.setTextColor(BLACK);

sp.fillRect(20, h + 20, 280, 26, c);

sp.fillRect(40, h + 22, 54, 22, BLACK);
sp.fillRect(42, h + 23, 50, 20, c);
sp.drawString("BACK", 44, h + 25, 1);

sp.fillRect(130, h + 22, 54, 22, BLACK);
sp.fillRect(132, h + 23, 50, 20, c);
sp.drawString("NEXT", 134, h + 25, 1);

sp.fillRect(227, h + 22, 70, 22, BLACK);
sp.fillRect(229, h + 23, 66, 20, c);
sp.drawString("ENTER", 234, h + 25, 1);
}
else {
char *str = " ";
if (select >= 4) {
select -= 2;
if (i == 1) {
str = "VOLUME";
}
if (i == 2) {
str = "BRIGHT";
}
if (i == 3) {
str = "CHANGE";
}
if (select == i) {
sp.setTextSize(5);
sp.setTextColor(BLACK);
//sp.fillRect(20, h+20, 280, 40, BLACK);
sp.fillRect(20, h + 21, 280, 38, c);
sp.drawString(str, 44, h + 23, 1);
}
else {
sp.setTextSize(5);
sp.setTextColor(c);
sp.fillRect(20, h + 20, 280, 40, c);
sp.fillRect(24, h + 21, 272, 38, BLACK);
sp.drawString(str, 44, h + 23, 1);
}
select += 2;
} else {
if (i == 1) {
str = "TIME";
}
if (i == 2) {
str = "CONTROL";
}
if (i == 3) {
str = "VOLUME";
}
if (select == i) {
sp.setTextSize(5);
sp.setTextColor(BLACK);
//sp.fillRect(20, h+20, 280, 40, BLACK);
sp.fillRect(20, h + 21, 280, 38, c);
sp.drawString(str, 44, h + 23, 1);
}
else {
sp.setTextSize(5);
sp.setTextColor(c);
sp.fillRect(20, h + 20, 280, 40, c);
sp.fillRect(24, h + 21, 272, 38, BLACK);
sp.drawString(str, 44, h + 23, 1);
}
}
}
}
sp.fillRect(20, 20, 280, 40, c);
sp.setTextSize(3);
sp.setTextColor(BLACK);
sp.drawString("MENU", 44, 32, 1);

//表示
sp.pushSprite(0, 0);
}
}

上二つに加えて 16seg.ino を使用する。

これを M5stack に書き込んで動かすと次の動画みたいになる。

とてもいい感じ。
menu 画面で右の c ボタンを押すと次の画面に行くけど、作ってないので固まる。

これを基準に画面をたくさん作ると、
なんかすごいいっぱい機能が乗っているように見えます。(笑)
実際、各画面に毎に M5stack の機能を分解してヘンシンブレスの UI は作っています。
例えば音量調整、照度設定、RFID 読み取り、RFID 読み取りログ表示・・・。

M5Stack 本体はボタンは 3 つしかありませんし、
長押しを多用すると、操作自体が煩わしい。(UX がよくないともいう?)
なので、1 画面当たりの操作の選択肢は少ない方がいいかなと思っています。
右か左か、進むのか前画面に戻すのかみたいな。

今回のアニメーション作成に当たっては、
表示周期(恐らく arduino なら delay()関数で調整)を上げて、
画面下方(M5stack のボタン側)から上方へ絵を動かすと、処理が追いつかないのか左右で線の位置が異なるという現象が起こった。
この時はアニメーションの内容次第で、周期を下げて大きく動かすとか、
画面更新だけ 2 回に 1 回を標準に何かボタンを押した時は更新するとかが必要そう。

連休の件

先日 10 連休が有ったのだけど、今年はもっぱら技術書典での頒布物を順番に手を付けていっただけだった。
機械学習とか、WEB フロントエンドとか PWA とか・・・。

手を付けてゆく中で「ブログを書こう: ブログを書く技術」を読んだ。

最近は本業が忙しかったり、今作っているものの基礎実験のために記事のアップデートもできていないのが心苦しかったのだけど、
継続して書き続けるために記事をもっと気楽にアップデート、アップロードしようと思い直した。

kindleunlimited でも読めるので、会員なら読んでもいいかもと思う。
そんなにページ数も多くないしね。

ではでは。