update firmware source code to v0.18 (#9)

This commit is contained in:
Forairaaaaa
2026-03-25 11:11:14 +08:00
committed by GitHub
parent 5001b7081b
commit 605b575fcc
123 changed files with 24590 additions and 1899 deletions
@@ -0,0 +1,99 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
#include "neon_light.h"
#include <hal/hal.h>
using namespace stackchan::addon;
void NeonLight::init()
{
// Setup color animation
_color_anim.duration = 0.3f;
_color_anim.begin();
_is_inited = true;
}
void NeonLight::update()
{
if (!_is_inited) {
init();
}
// Keep update in at most 50Hz
if (GetHAL().millis() - _last_tick < 20) {
return;
}
_last_tick = GetHAL().millis();
// Apply color animation
if (!_color_anim.done()) {
_color_anim.updateWithDelta(0.02f); // Fixed delta time for consistency
for (int i = 0; i < _led_count; i++) {
set_rgb_color_impl(i, _color_anim.r, _color_anim.g, _color_anim.b);
}
refresh_rgb_impl();
}
// Snap to target angle when animation ends
else if (_snap_to_target_on_rest) {
_snap_to_target_on_rest = false;
for (int i = 0; i < _led_count; i++) {
set_rgb_color_impl(i, _color_anim.r, _color_anim.g, _color_anim.b);
}
refresh_rgb_impl();
}
}
void NeonLight::setColor(uint8_t r, uint8_t g, uint8_t b)
{
_color_anim.move(r, g, b);
_snap_to_target_on_rest = true;
}
void NeonLight::setColor(const uitk::color::Rgb_t& rgb)
{
_color_anim.move(rgb);
_snap_to_target_on_rest = true;
}
void NeonLight::setColor(uint32_t hex)
{
_color_anim.move(hex);
_snap_to_target_on_rest = true;
}
void NeonLight::setColor(std::string_view hex)
{
_color_anim.move(hex);
_snap_to_target_on_rest = true;
}
void NeonLight::setDuration(float durationSec)
{
_color_anim.duration = durationSec;
_color_anim.begin();
}
void LeftNeonLight::set_rgb_color_impl(uint8_t index, uint8_t r, uint8_t g, uint8_t b)
{
GetHAL().setRgbColor(index, r, g, b);
}
void LeftNeonLight::refresh_rgb_impl()
{
GetHAL().refreshRgb();
}
void RightNeonLight::set_rgb_color_impl(uint8_t index, uint8_t r, uint8_t g, uint8_t b)
{
GetHAL().setRgbColor(index + 6, r, g, b);
}
void RightNeonLight::refresh_rgb_impl()
{
GetHAL().refreshRgb();
}
@@ -0,0 +1,80 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <cstdint>
#include <smooth_ui_toolkit.hpp>
#include <uitk/short_namespace.hpp>
#include "core/color/color.hpp"
#include <string_view>
namespace stackchan::addon {
/**
* @brief
*
*/
class NeonLight {
public:
NeonLight(int ledCount) : _led_count(ledCount)
{
}
void init();
void update();
void setColor(uint8_t r, uint8_t g, uint8_t b);
void setColor(const uitk::color::Rgb_t& rgb);
void setColor(uint32_t hex);
void setColor(std::string_view hex);
void setDuration(float durationSec);
int getLedCount() const
{
return _led_count;
}
protected:
virtual void set_rgb_color_impl(uint8_t index, uint8_t r, uint8_t g, uint8_t b) = 0;
virtual void refresh_rgb_impl() = 0;
private:
int _led_count;
bool _is_inited = false;
bool _snap_to_target_on_rest = false;
uint32_t _last_tick = 0;
uitk::color::AnimateRgb_t _color_anim;
};
/**
* @brief
*
*/
class LeftNeonLight : public NeonLight {
public:
LeftNeonLight() : NeonLight(6)
{
}
private:
void set_rgb_color_impl(uint8_t index, uint8_t r, uint8_t g, uint8_t b) override;
void refresh_rgb_impl() override;
};
/**
* @brief
*
*/
class RightNeonLight : public NeonLight {
public:
RightNeonLight() : NeonLight(6)
{
}
private:
void set_rgb_color_impl(uint8_t index, uint8_t r, uint8_t g, uint8_t b) override;
void refresh_rgb_impl() override;
};
} // namespace stackchan::addon
@@ -19,6 +19,7 @@ void Keyframe::apply()
feat.setPosition(kf.position);
feat.setRotation(kf.rotation);
feat.setWeight(kf.weight);
feat.setSize(kf.size);
};
apply_feature(leftEye, avatar.leftEye());
apply_feature(rightEye, avatar.rightEye());
@@ -27,6 +28,9 @@ void Keyframe::apply()
auto apply_servo = [&](const ServoKeyframe& kf, motion::Servo& servo) { servo.moveWithSpeed(kf.angle, kf.speed); };
apply_servo(yawServo, motion.yawServo());
apply_servo(pitchServo, motion.pitchServo());
stackchan.leftNeonLight().setColor(leftRgbColor);
stackchan.rightNeonLight().setColor(rightRgbColor);
}
void Timeline::start()
@@ -8,6 +8,7 @@
#include <uitk/short_namespace.hpp>
#include <cstdint>
#include <vector>
#include <string>
namespace stackchan::animation {
@@ -19,6 +20,7 @@ struct FeatureKeyframe {
uitk::Vector2i position;
int rotation;
int weight;
int size;
FeatureKeyframe(int x = 0, int y = 0, int rotation = 0, int weight = 0)
: position(x, y), rotation(rotation), weight(weight)
@@ -49,6 +51,8 @@ struct Keyframe {
FeatureKeyframe mouth;
ServoKeyframe yawServo;
ServoKeyframe pitchServo;
std::string leftRgbColor;
std::string rightRgbColor;
uint32_t durationMs = 0;
Keyframe()
@@ -97,6 +97,16 @@ public:
}
}
void setModifyLock(bool locked)
{
_is_modify_locked = locked;
}
bool isModifyLocked()
{
return _is_modify_locked;
}
/* ---------------------------- Decorator helpers --------------------------- */
int addDecorator(std::unique_ptr<Decorator> decorator)
@@ -120,6 +130,8 @@ protected:
Emotion _emotion = Emotion::Neutral;
KeyElements_t _key_elements;
ObjectPool<Decorator> _decorator_pool;
bool _is_modify_locked = false;
};
} // namespace stackchan::avatar
@@ -19,7 +19,7 @@ public:
virtual ~Element() = default;
/**
* @brief (-100~100, -100~100)
* @brief (-100 ~ 100, -100 ~ 100)
*
* @param position
*/
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: MIT
*/
#include "decorators.h"
#include <hal/hal.h>
#include <vector>
using namespace uitk;
@@ -17,47 +18,77 @@ static const std::vector<int> _angry_rotation_frames = {150, 200};
LV_IMAGE_DECLARE(decorator_angry);
AngryDecorator::AngryDecorator(lv_obj_t* parent, uint32_t destroyAfterMs, uint32_t animationIntervalMs)
: _animation_interval_ms(animationIntervalMs)
{
// 初始化 UI 组件
_angry = std::make_unique<Image>(parent);
_angry->setSrc(&decorator_angry);
_angry->setAlign(LV_ALIGN_CENTER);
_angry->setPos(_angry_default_position.x, _angry_default_position.y);
// 设置旋转中心和初始角度
_angry->setTransformPivot(_angry->getWidth() / 2, _angry->getHeight() / 2);
_angry->setRotation(_angry_rotation_frames[1]);
_angry->setRotation(_angry_rotation_frames[0]);
// 设置颜色偏置
_angry->setImageRecolorOpa(LV_OPA_COVER);
_angry->setImageRecolor(_angry_default_color);
if (destroyAfterMs != 0) {
scheduleDestroy(destroyAfterMs);
uint32_t now = GetHAL().millis();
// 初始化销毁倒计时
if (destroyAfterMs > 0) {
_destroy_at = now + destroyAfterMs;
_has_lifetime = true;
}
if (animationIntervalMs != 0) {
getTimer().addTask(animationIntervalMs, -1, 0, [this]() {
setRotation(_angry_rotation_frames[_animation_index]);
_animation_index++;
if (_animation_index >= _angry_rotation_frames.size()) {
_animation_index = 0;
}
});
// 初始化动画倒计时
if (_animation_interval_ms > 0) {
_next_animation_tick = now + _animation_interval_ms;
}
}
AngryDecorator::~AngryDecorator()
{
_angry.reset();
}
void AngryDecorator::_update()
{
uint32_t now = GetHAL().millis();
// 检查自动销毁
if (_has_lifetime && now >= _destroy_at) {
requestDestroy();
return;
}
// 检查动画跳变
if (_animation_interval_ms > 0 && now >= _next_animation_tick) {
_next_animation_tick = now + _animation_interval_ms;
// 切换帧
_animation_index = (_animation_index + 1) % _angry_rotation_frames.size();
_angry->setRotation(_angry_rotation_frames[_animation_index]);
}
}
void AngryDecorator::setPosition(int x, int y)
{
_angry->setPos(x, y);
if (_angry) {
_angry->setPos(x, y);
}
}
void AngryDecorator::setRotation(int rotation)
{
_angry->setRotation(rotation);
if (_angry) {
_angry->setRotation(rotation);
}
}
void AngryDecorator::setColor(lv_color_t color)
{
_angry->setImageRecolor(color);
if (_angry) {
_angry->setImageRecolor(color);
}
}
@@ -0,0 +1,233 @@
#ifdef __has_include
#if __has_include("lvgl.h")
#ifndef LV_LVGL_H_INCLUDE_SIMPLE
#define LV_LVGL_H_INCLUDE_SIMPLE
#endif
#endif
#endif
#if defined(LV_LVGL_H_INCLUDE_SIMPLE)
#include "lvgl.h"
#else
#include "lvgl/lvgl.h"
#endif
#ifndef LV_ATTRIBUTE_MEM_ALIGN
#define LV_ATTRIBUTE_MEM_ALIGN
#endif
#ifndef LV_ATTRIBUTE_IMAGE_DECORATOR_DIZZY
#define LV_ATTRIBUTE_IMAGE_DECORATOR_DIZZY
#endif
const LV_ATTRIBUTE_MEM_ALIGN LV_ATTRIBUTE_LARGE_CONST LV_ATTRIBUTE_IMAGE_DECORATOR_DIZZY uint8_t
decorator_dizzy_map[] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40,
0x80, 0xaf, 0xdf, 0xff, 0xff, 0xcf, 0xbf, 0x60, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x6f, 0xbf, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xdf, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x20, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbf, 0x8f, 0x5f, 0x40, 0x5f, 0x80, 0xbf, 0xff, 0xff,
0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0xef,
0xff, 0xff, 0xff, 0xff, 0x8f, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xaf, 0xff, 0xff, 0xff,
0xff, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8f, 0xff, 0xff, 0xff, 0xff, 0xbf,
0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xff, 0xff, 0xff, 0xff, 0x40,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0xbf, 0xff, 0xff, 0xff, 0xef, 0x70, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xff, 0xff, 0xff, 0xcf, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x20, 0xdf, 0xff, 0xff, 0xff, 0xcf, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8f, 0xff, 0xff, 0xef, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20,
0xdf, 0xff, 0xff, 0xff, 0x8f, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x5f, 0x80, 0x80, 0x8f, 0x80, 0x80,
0x5f, 0x40, 0x20, 0x00, 0x00, 0x10, 0xaf, 0xef, 0x70, 0x00, 0x00, 0x00, 0x00, 0x20, 0xdf, 0xff, 0xff, 0xff,
0x80, 0x00, 0x00, 0x00, 0x00, 0x40, 0x8f, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x9f, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0xdf, 0xff, 0xff, 0xff, 0x60, 0x00, 0x00, 0x00,
0x20, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xcf, 0x10,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9f, 0xff, 0xff, 0xff, 0x60, 0x00, 0x00, 0x00, 0x30, 0xdf, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xef, 0xdf, 0xdf, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00,
0x00, 0x00, 0x60, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x60, 0xff, 0xff, 0xff, 0xff, 0xef, 0x9f, 0x40,
0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x30, 0xdf, 0xff, 0xff, 0xff, 0x10, 0x00, 0x00, 0x00, 0x20, 0xdf,
0xff, 0xff, 0x9f, 0x00, 0x00, 0x00, 0x20, 0xef, 0xff, 0xff, 0xff, 0x8f, 0x10, 0x00, 0x00, 0x00, 0x30, 0x40,
0x20, 0x00, 0x00, 0x00, 0x00, 0x40, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x80, 0xff, 0xff, 0xdf, 0x20,
0x00, 0x00, 0x20, 0xdf, 0xff, 0xff, 0xef, 0x70, 0x00, 0x00, 0x00, 0x30, 0xbf, 0xff, 0xff, 0xff, 0x50, 0x00,
0x00, 0x00, 0x00, 0xbf, 0xff, 0xff, 0xcf, 0x00, 0x00, 0x00, 0xdf, 0xff, 0xff, 0x60, 0x00, 0x00, 0x00, 0x9f,
0xff, 0xff, 0xff, 0x50, 0x00, 0x00, 0x00, 0x70, 0xff, 0xff, 0xff, 0xff, 0xff, 0xef, 0x10, 0x00, 0x00, 0x00,
0x80, 0xff, 0xff, 0xff, 0x10, 0x00, 0x50, 0xff, 0xff, 0xdf, 0x00, 0x00, 0x00, 0x30, 0xff, 0xff, 0xff, 0x80,
0x00, 0x00, 0x10, 0xaf, 0xff, 0xff, 0xff, 0xef, 0xff, 0xff, 0xff, 0x5f, 0x00, 0x00, 0x00, 0x40, 0xff, 0xff,
0xff, 0x40, 0x00, 0x9f, 0xff, 0xff, 0x9f, 0x00, 0x00, 0x00, 0x9f, 0xff, 0xff, 0xdf, 0x00, 0x00, 0x10, 0xcf,
0xff, 0xff, 0xff, 0xdf, 0x30, 0xdf, 0xff, 0xff, 0x8f, 0x00, 0x00, 0x00, 0x20, 0xff, 0xff, 0xff, 0x5f, 0x00,
0xbf, 0xff, 0xff, 0x50, 0x00, 0x00, 0x00, 0xcf, 0xff, 0xff, 0x6f, 0x00, 0x00, 0xaf, 0xff, 0xff, 0xff, 0xaf,
0x10, 0x00, 0xbf, 0xff, 0xff, 0x9f, 0x00, 0x00, 0x00, 0x10, 0xff, 0xff, 0xff, 0x5f, 0x00, 0xef, 0xff, 0xff,
0x30, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x40, 0x00, 0x70, 0xff, 0xff, 0xff, 0x9f, 0x00, 0x00, 0x00, 0xbf,
0xff, 0xff, 0x9f, 0x00, 0x00, 0x00, 0x10, 0xff, 0xff, 0xff, 0x5f, 0x00, 0xff, 0xff, 0xff, 0x20, 0x00, 0x00,
0x00, 0xff, 0xff, 0xff, 0x40, 0x00, 0xbf, 0xff, 0xff, 0xaf, 0x00, 0x00, 0x00, 0x00, 0xbf, 0xff, 0xff, 0x9f,
0x00, 0x00, 0x00, 0x30, 0xff, 0xff, 0xff, 0x40, 0x00, 0xdf, 0xff, 0xff, 0x30, 0x00, 0x00, 0x00, 0xef, 0xff,
0xff, 0x6f, 0x00, 0x70, 0xff, 0xdf, 0x20, 0x00, 0x00, 0x00, 0x00, 0xbf, 0xff, 0xff, 0x9f, 0x00, 0x00, 0x00,
0x60, 0xff, 0xff, 0xff, 0x20, 0x00, 0xcf, 0xff, 0xff, 0x5f, 0x00, 0x00, 0x00, 0x9f, 0xff, 0xff, 0xdf, 0x10,
0x00, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbf, 0xff, 0xff, 0x9f, 0x00, 0x00, 0x00, 0xbf, 0xff, 0xff,
0xdf, 0x00, 0x00, 0x8f, 0xff, 0xff, 0xaf, 0x00, 0x00, 0x00, 0x40, 0xff, 0xff, 0xff, 0x9f, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x10, 0xef, 0xff, 0xff, 0x80, 0x00, 0x00, 0x20, 0xff, 0xff, 0xff, 0x9f, 0x00, 0x00,
0x40, 0xff, 0xff, 0xef, 0x10, 0x00, 0x00, 0x00, 0xaf, 0xff, 0xff, 0xff, 0xbf, 0x20, 0x00, 0x00, 0x00, 0x00,
0x00, 0x80, 0xff, 0xff, 0xff, 0x50, 0x00, 0x00, 0xdf, 0xff, 0xff, 0xff, 0x20, 0x00, 0x00, 0x00, 0xef, 0xff,
0xff, 0x9f, 0x00, 0x00, 0x00, 0x10, 0xcf, 0xff, 0xff, 0xff, 0xef, 0x9f, 0x40, 0x20, 0x40, 0xaf, 0xff, 0xff,
0xff, 0xdf, 0x00, 0x00, 0x8f, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x60, 0xff, 0xff, 0xff, 0x50,
0x00, 0x00, 0x00, 0x20, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x50, 0x00,
0x80, 0xff, 0xff, 0xff, 0xdf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbf, 0xff, 0xff, 0xef, 0x20, 0x00, 0x00,
0x00, 0x00, 0x80, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xef, 0x70, 0x00, 0x80, 0xff, 0xff, 0xff,
0xdf, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xef, 0xff, 0xff, 0xdf, 0x20, 0x00, 0x00, 0x00, 0x00,
0x20, 0x8f, 0xdf, 0xff, 0xff, 0xff, 0xef, 0x8f, 0x20, 0x00, 0x80, 0xff, 0xff, 0xff, 0xff, 0x30, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0xff, 0xff, 0xff, 0xdf, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x10, 0x30, 0x20, 0x00, 0x00, 0x10, 0x9f, 0xff, 0xff, 0xff, 0xff, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x80, 0xff, 0xff, 0xff, 0xdf, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x40, 0xdf, 0xff, 0xff, 0xff, 0xef, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x80, 0xff, 0xff, 0xff, 0xff, 0x9f, 0x20, 0x00, 0x00, 0x00, 0x00, 0x20, 0x80, 0xdf, 0xff, 0xff,
0xff, 0xff, 0xdf, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x80, 0xff, 0xff, 0xff, 0xff, 0xff, 0xef, 0xbf, 0xcf, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbf, 0x20,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0xdf,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xef, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x8f, 0xdf, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbf, 0x80, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x80, 0x9f, 0x9f,
0x8f, 0x5f, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
const lv_image_dsc_t decorator_dizzy = {
.header.cf = LV_COLOR_FORMAT_RGB565A8,
.header.magic = LV_IMAGE_HEADER_MAGIC,
.header.w = 33,
.header.h = 36,
.data_size = 1188 * 3,
.data = decorator_dizzy_map,
};
@@ -0,0 +1,145 @@
#ifdef __has_include
#if __has_include("lvgl.h")
#ifndef LV_LVGL_H_INCLUDE_SIMPLE
#define LV_LVGL_H_INCLUDE_SIMPLE
#endif
#endif
#endif
#if defined(LV_LVGL_H_INCLUDE_SIMPLE)
#include "lvgl.h"
#else
#include "lvgl/lvgl.h"
#endif
#ifndef LV_ATTRIBUTE_MEM_ALIGN
#define LV_ATTRIBUTE_MEM_ALIGN
#endif
#ifndef LV_ATTRIBUTE_IMAGE_DECORATOR_SHY
#define LV_ATTRIBUTE_IMAGE_DECORATOR_SHY
#endif
const LV_ATTRIBUTE_MEM_ALIGN LV_ATTRIBUTE_LARGE_CONST LV_ATTRIBUTE_IMAGE_DECORATOR_SHY uint8_t decorator_shy_map[] = {
0x54, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x54, 0xfd, 0x14, 0xf5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x54, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x54, 0xfd, 0x54, 0xfd,
0x34, 0xfd, 0x34, 0xfd, 0x14, 0xf5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x54, 0xfd, 0x54, 0xfd, 0x34, 0xfd, 0x54, 0xfd, 0x14, 0xf5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x54, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34,
0xfd, 0x14, 0xf5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x54, 0xfd,
0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x54, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x54, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34,
0xfd, 0x34, 0xfd, 0x33, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0xfd, 0x34, 0xfd, 0x34,
0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x54, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x54, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x33, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd,
0x34, 0xfd, 0x34, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x54, 0xfd, 0x34,
0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x54, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd,
0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x54, 0xfd, 0x14, 0xf5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x33, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0xf5, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x54, 0xfd, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x54, 0xfd, 0x34, 0xfd,
0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x14, 0xf5, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x54, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34,
0xfd, 0x14, 0xf5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x33, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34,
0xfd, 0x00, 0x00, 0x00, 0x00, 0x54, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34,
0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x54, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34,
0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd,
0x34, 0xfd, 0x14, 0xf5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0xf5, 0x34, 0xfd, 0x34,
0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0xf5, 0x54, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34,
0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x54, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd,
0x34, 0xfd, 0x34, 0xfd, 0x54, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x54,
0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0xfd, 0x34, 0xfd, 0x34,
0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0xfd, 0x34, 0xfd,
0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x33, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14,
0xf5, 0x54, 0xfd, 0x54, 0xfd, 0x34, 0xfd, 0x54, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x54, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x14, 0xf5, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x54, 0xfd, 0x54, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x54, 0xfd, 0x34, 0xfd, 0x34, 0xfd, 0x54, 0xfd, 0x14, 0xf5, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x20, 0xcf, 0xef, 0xbf, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xbf, 0xff, 0xff, 0xff, 0x9f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xcf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xdf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xdf, 0xff, 0xff, 0xff, 0xef, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xbf,
0xff, 0xaf, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xbf, 0xff, 0xbf, 0x10, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0xbf, 0xff, 0xff, 0xff, 0xff, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbf,
0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbf, 0xff, 0xff, 0xff, 0xaf, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8f, 0xff, 0xff, 0xff, 0xff, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xef, 0xff, 0xff, 0xff, 0xef, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0xff, 0xff, 0xff, 0xff, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xbf, 0xff, 0xff, 0xff, 0xff, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9f, 0xff, 0xff, 0xff, 0xff,
0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xff, 0xff, 0xff, 0xff, 0xdf, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x6f, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xdf, 0xff, 0xff,
0xff, 0xff, 0xff, 0xbf, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0xcf, 0xff, 0xff, 0xff, 0xff, 0x40, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x10, 0xff, 0xff, 0xff, 0xff, 0xbf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xef,
0xff, 0xff, 0xff, 0xff, 0xff, 0xef, 0x80, 0x10, 0x00, 0x00, 0x00, 0x60, 0xff, 0xff, 0xff, 0xff, 0x9f, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0xbf, 0xff, 0xff, 0xff, 0xff, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x30, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xef, 0x80, 0x00, 0x00, 0x20, 0xff, 0xff, 0xff, 0xff, 0xff, 0x30,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6f, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x20, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x9f, 0xff, 0xff, 0xff, 0xff,
0xdf, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0xff, 0xff, 0xff, 0xff, 0xdf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x10, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xaf, 0x00, 0x00, 0x20, 0xef, 0xff, 0xff,
0xff, 0xff, 0xbf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbf, 0xff, 0xff, 0xff, 0xff, 0x60, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0xdf, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x80, 0xff,
0xff, 0xff, 0xff, 0xff, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xff, 0xff, 0xff, 0xff, 0x9f, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x50, 0xbf, 0xcf, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00,
0xaf, 0xff, 0xff, 0xff, 0xff, 0xbf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xcf, 0xff, 0xff, 0xff, 0x60, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x10, 0xdf, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xbf, 0xdf, 0x70, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x20, 0xaf, 0xcf, 0x8f, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
const lv_image_dsc_t decorator_shy = {
.header.cf = LV_COLOR_FORMAT_RGB565A8,
.header.magic = LV_IMAGE_HEADER_MAGIC,
.header.w = 39,
.header.h = 18,
.data_size = 702 * 3,
.data = decorator_shy_map,
};
@@ -4,7 +4,6 @@
* SPDX-License-Identifier: MIT
*/
#pragma once
#include "../../utils/timer.h"
#include "../avatar/decorator.h"
#include <lvgl.h>
#include <smooth_lvgl.hpp>
@@ -13,36 +12,11 @@
namespace stackchan::avatar {
/**
* @brief Decorator with a timer
*
*/
class TimerDecorator : public Decorator {
public:
void scheduleDestroy(uint32_t ms)
{
_timer.addTask(ms, 1, 0, [this]() { requestDestroy(); });
}
Timer& getTimer()
{
return _timer;
}
virtual void _update() override
{
_timer.update();
}
private:
Timer _timer;
};
/**
* @brief
*
*/
class HeartDecorator : public TimerDecorator {
class HeartDecorator : public Decorator {
public:
/**
* @brief
@@ -54,14 +28,22 @@ public:
HeartDecorator(lv_obj_t* parent, uint32_t destroyAfterMs = 0, uint32_t animationIntervalMs = 500);
~HeartDecorator();
void _update() override;
using Element::setPosition;
void setPosition(int x, int y);
void setRotation(int rotation);
void setRotation(int rotation) override;
void setColor(lv_color_t color);
private:
std::unique_ptr<uitk::lvgl_cpp::Image> _heart;
uint32_t _destroy_at = 0;
uint32_t _next_animation_tick = 0;
uint32_t _animation_interval_ms = 0;
bool _has_lifetime = false;
int _animation_index = 0;
};
@@ -69,7 +51,7 @@ private:
* @brief
*
*/
class AngryDecorator : public TimerDecorator {
class AngryDecorator : public Decorator {
public:
/**
* @brief
@@ -81,14 +63,22 @@ public:
AngryDecorator(lv_obj_t* parent, uint32_t destroyAfterMs = 0, uint32_t animationIntervalMs = 500);
~AngryDecorator();
void _update() override;
using Element::setPosition;
void setPosition(int x, int y);
void setRotation(int rotation);
void setRotation(int rotation) override;
void setColor(lv_color_t color);
private:
std::unique_ptr<uitk::lvgl_cpp::Image> _angry;
uint32_t _destroy_at = 0;
uint32_t _next_animation_tick = 0;
uint32_t _animation_interval_ms = 0;
bool _has_lifetime = false;
int _animation_index = 0;
};
@@ -96,7 +86,7 @@ private:
* @brief
*
*/
class SweatDecorator : public TimerDecorator {
class SweatDecorator : public Decorator {
public:
/**
* @brief
@@ -108,6 +98,8 @@ public:
SweatDecorator(lv_obj_t* parent, uint32_t destroyAfterMs = 0, uint32_t animationIntervalMs = 700);
~SweatDecorator();
void _update() override;
using Element::setPosition;
void setPosition(int x, int y);
@@ -117,6 +109,81 @@ public:
private:
std::unique_ptr<uitk::lvgl_cpp::Image> _sweat;
uint32_t _destroy_at = 0;
uint32_t _next_animation_tick = 0;
uint32_t _animation_interval_ms = 0;
bool _has_lifetime = false;
int _animation_index = 0;
};
/**
* @brief
*
*/
class ShyDecorator : public Decorator {
public:
/**
* @brief
*
* @param parent
* @param destroyAfterMs Destroy after milliseconds, 0 for infinite
*/
ShyDecorator(lv_obj_t* parent, uint32_t destroyAfterMs = 0);
~ShyDecorator();
void _update() override;
using Element::setPosition;
void setPosition(int x, int y);
void setRotation(int rotation) override;
void setColor(lv_color_t color);
void setVisible(bool visible) override;
private:
std::unique_ptr<uitk::lvgl_cpp::Image> _left;
std::unique_ptr<uitk::lvgl_cpp::Image> _right;
uint32_t _destroy_at = 0;
bool _has_lifetime = false;
};
/**
* @brief
*
*/
class DizzyDecorator : public Decorator {
public:
/**
* @brief
*
* @param parent
* @param destroyAfterMs Destroy after milliseconds, 0 for infinite
* @param animationIntervalMs Animation update interval in milliseconds, 0 for none
*/
DizzyDecorator(lv_obj_t* parent, uint32_t destroyAfterMs = 0, uint32_t animationIntervalMs = 500);
~DizzyDecorator();
void _update() override;
using Element::setPosition;
void setPosition(int x, int y);
void setRotation(int rotation) override;
void setColor(lv_color_t color);
void setVisible(bool visible) override;
private:
std::unique_ptr<uitk::lvgl_cpp::Image> _left;
std::unique_ptr<uitk::lvgl_cpp::Image> _right;
uint32_t _destroy_at = 0;
uint32_t _next_animation_tick = 0;
uint32_t _animation_interval_ms = 0;
bool _has_lifetime = false;
int _animation_index = 0;
};
@@ -0,0 +1,127 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
#include "decorators.h"
#include <hal/hal.h>
#include <vector>
using namespace uitk;
using namespace uitk::lvgl_cpp;
using namespace stackchan::avatar;
static const Vector2i _dizzy_left_default_position = Vector2i(-70, -16);
static const Vector2i _dizzy_right_default_position = Vector2i(70, -16);
static const lv_color_t _dizzy_default_color = lv_color_hex(0xFFFFFF);
LV_IMAGE_DECLARE(decorator_dizzy);
DizzyDecorator::DizzyDecorator(lv_obj_t* parent, uint32_t destroyAfterMs, uint32_t animationIntervalMs)
: _animation_interval_ms(animationIntervalMs)
{
// Initialize Left Image
_left = std::make_unique<Image>(parent);
_left->setSrc(&decorator_dizzy);
_left->setAlign(LV_ALIGN_CENTER);
_left->setPos(_dizzy_left_default_position.x, _dizzy_left_default_position.y);
_left->setTransformPivot(_left->getWidth() / 2, _left->getHeight() / 2);
_left->setRotation(0);
_left->setImageRecolorOpa(LV_OPA_COVER);
_left->setImageRecolor(_dizzy_default_color);
// Initialize Right Image
_right = std::make_unique<Image>(parent);
_right->setSrc(&decorator_dizzy);
_right->setAlign(LV_ALIGN_CENTER);
_right->setPos(_dizzy_right_default_position.x, _dizzy_right_default_position.y);
_right->setTransformPivot(_right->getWidth() / 2, _right->getHeight() / 2);
_right->setRotation(450);
_right->setImageRecolorOpa(LV_OPA_COVER);
_right->setImageRecolor(_dizzy_default_color);
uint32_t now = GetHAL().millis();
if (destroyAfterMs > 0) {
_destroy_at = now + destroyAfterMs;
_has_lifetime = true;
}
if (_animation_interval_ms > 0) {
_next_animation_tick = now + _animation_interval_ms;
}
_animation_index = 0; // 用作当前旋转角度
}
DizzyDecorator::~DizzyDecorator()
{
}
void DizzyDecorator::_update()
{
uint32_t now = GetHAL().millis();
if (_has_lifetime && now >= _destroy_at) {
requestDestroy();
return;
}
if (_animation_interval_ms > 0 && now >= _next_animation_tick) {
_next_animation_tick = now + _animation_interval_ms;
// 自加30度 (300 unit)
_animation_index = (_animation_index + 300) % 3600;
int rotation = -_animation_index;
if (_left) {
_left->setRotation(rotation);
}
if (_right) {
_right->setRotation((rotation + 450) % 3600);
}
}
}
void DizzyDecorator::setPosition(int x, int y)
{
if (_left) {
_left->setPos(x + _dizzy_left_default_position.x, y + _dizzy_left_default_position.y);
}
if (_right) {
_right->setPos(x + _dizzy_right_default_position.x, y + _dizzy_right_default_position.y);
}
}
void DizzyDecorator::setRotation(int rotation)
{
// 这里要注意,手动设置的 rotation 可能会被 update 中的动画覆盖
// 但按照 HeartDecorator 的逻辑,setRotation 也是直接设置的
if (_left) {
_left->setRotation(rotation);
}
if (_right) {
_right->setRotation(rotation);
}
}
void DizzyDecorator::setColor(lv_color_t color)
{
if (_left) {
_left->setImageRecolor(color);
}
if (_right) {
_right->setImageRecolor(color);
}
}
void DizzyDecorator::setVisible(bool visible)
{
if (_left) {
_left->setHidden(!visible);
}
if (_right) {
_right->setHidden(!visible);
}
}
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: MIT
*/
#include "decorators.h"
#include <hal/hal.h>
#include <vector>
using namespace uitk;
@@ -17,47 +18,77 @@ static const std::vector<int> _heart_rotation_frames = {150, 200};
LV_IMAGE_DECLARE(decorator_heart);
HeartDecorator::HeartDecorator(lv_obj_t* parent, uint32_t destroyAfterMs, uint32_t animationIntervalMs)
: _animation_interval_ms(animationIntervalMs)
{
// 初始化图像
_heart = std::make_unique<Image>(parent);
_heart->setSrc(&decorator_heart);
_heart->setAlign(LV_ALIGN_CENTER);
_heart->setPos(_heart_default_position.x, _heart_default_position.y);
// 设置旋转中心为中心点
_heart->setTransformPivot(_heart->getWidth() / 2, _heart->getHeight() / 2);
_heart->setRotation(_heart_rotation_frames[1]);
_heart->setRotation(_heart_rotation_frames[0]);
// 设置颜色偏向
_heart->setImageRecolorOpa(LV_OPA_COVER);
_heart->setImageRecolor(_heart_default_color);
if (destroyAfterMs != 0) {
scheduleDestroy(destroyAfterMs);
uint32_t now = GetHAL().millis();
// 设置销毁计时
if (destroyAfterMs > 0) {
_destroy_at = now + destroyAfterMs;
_has_lifetime = true;
}
if (animationIntervalMs != 0) {
getTimer().addTask(animationIntervalMs, -1, 0, [this]() {
setRotation(_heart_rotation_frames[_animation_index]);
_animation_index++;
if (_animation_index >= _heart_rotation_frames.size()) {
_animation_index = 0;
}
});
// 设置动画计时
if (_animation_interval_ms > 0) {
_next_animation_tick = now + _animation_interval_ms;
}
}
HeartDecorator::~HeartDecorator()
{
_heart.reset();
}
void HeartDecorator::_update()
{
uint32_t now = GetHAL().millis();
// 1. 处理销毁
if (_has_lifetime && now >= _destroy_at) {
requestDestroy();
return;
}
// 2. 处理动画跳变(心跳效果)
if (_animation_interval_ms > 0 && now >= _next_animation_tick) {
_next_animation_tick = now + _animation_interval_ms;
// 切换旋转角度
_animation_index = (_animation_index + 1) % _heart_rotation_frames.size();
_heart->setRotation(_heart_rotation_frames[_animation_index]);
}
}
void HeartDecorator::setPosition(int x, int y)
{
_heart->setPos(x, y);
if (_heart) {
_heart->setPos(x, y);
}
}
void HeartDecorator::setRotation(int rotation)
{
_heart->setRotation(rotation);
if (_heart) {
_heart->setRotation(rotation);
}
}
void HeartDecorator::setColor(lv_color_t color)
{
_heart->setImageRecolor(color);
if (_heart) {
_heart->setImageRecolor(color);
}
}
@@ -0,0 +1,100 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
#include "decorators.h"
#include <hal/hal.h>
#include <vector>
using namespace uitk;
using namespace uitk::lvgl_cpp;
using namespace stackchan::avatar;
static const Vector2i _shy_left_default_position = Vector2i(-108, 28);
static const Vector2i _shy_right_default_position = Vector2i(108, 28);
static const lv_color_t _shy_default_color = lv_color_hex(0xF7A59E);
LV_IMAGE_DECLARE(decorator_shy);
ShyDecorator::ShyDecorator(lv_obj_t* parent, uint32_t destroyAfterMs)
{
// Initialize Left Image
_left = std::make_unique<Image>(parent);
_left->setSrc(&decorator_shy);
_left->setAlign(LV_ALIGN_CENTER);
_left->setPos(_shy_left_default_position.x, _shy_left_default_position.y);
_left->setTransformPivot(_left->getWidth() / 2, _left->getHeight() / 2);
_left->setImageRecolorOpa(LV_OPA_COVER);
_left->setImageRecolor(_shy_default_color);
// Initialize Right Image
_right = std::make_unique<Image>(parent);
_right->setSrc(&decorator_shy);
_right->setAlign(LV_ALIGN_CENTER);
_right->setPos(_shy_right_default_position.x, _shy_right_default_position.y);
_right->setTransformPivot(_right->getWidth() / 2, _right->getHeight() / 2);
_right->setImageRecolorOpa(LV_OPA_COVER);
_right->setImageRecolor(_shy_default_color);
uint32_t now = GetHAL().millis();
if (destroyAfterMs > 0) {
_destroy_at = now + destroyAfterMs;
_has_lifetime = true;
}
}
ShyDecorator::~ShyDecorator()
{
}
void ShyDecorator::_update()
{
uint32_t now = GetHAL().millis();
if (_has_lifetime && now >= _destroy_at) {
requestDestroy();
return;
}
}
void ShyDecorator::setPosition(int x, int y)
{
if (_left) {
_left->setPos(x + _shy_left_default_position.x, y + _shy_left_default_position.y);
}
if (_right) {
_right->setPos(x + _shy_right_default_position.x, y + _shy_right_default_position.y);
}
}
void ShyDecorator::setRotation(int rotation)
{
if (_left) {
_left->setRotation(rotation);
}
if (_right) {
_right->setRotation(rotation);
}
}
void ShyDecorator::setColor(lv_color_t color)
{
if (_left) {
_left->setImageRecolor(color);
}
if (_right) {
_right->setImageRecolor(color);
}
}
void ShyDecorator::setVisible(bool visible)
{
if (_left) {
_left->setHidden(!visible);
}
if (_right) {
_right->setHidden(!visible);
}
}
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: MIT
*/
#include "decorators.h"
#include <hal/hal.h>
#include <vector>
using namespace uitk;
@@ -17,59 +18,89 @@ static const std::vector<int> _sweat_pos_y_frames = {-72, -68, -62, -58, 0};
LV_IMAGE_DECLARE(decorator_sweat);
SweatDecorator::SweatDecorator(lv_obj_t* parent, uint32_t destroyAfterMs, uint32_t animationIntervalMs)
: _animation_interval_ms(animationIntervalMs)
{
// 初始化图像
_sweat = std::make_unique<Image>(parent);
_sweat->setSrc(&decorator_sweat);
_sweat->setAlign(LV_ALIGN_CENTER);
_sweat->setPos(_sweat_default_position.x, _sweat_default_position.y);
_sweat->setTransformPivot(_sweat->getWidth() / 2, _sweat->getHeight() / 2);
_sweat->setImageRecolorOpa(LV_OPA_COVER);
_sweat->setImageRecolor(_sweat_default_color);
if (destroyAfterMs != 0) {
scheduleDestroy(destroyAfterMs);
uint32_t now = GetHAL().millis();
if (destroyAfterMs > 0) {
_destroy_at = now + destroyAfterMs;
_has_lifetime = true;
}
if (animationIntervalMs != 0) {
getTimer().addTask(animationIntervalMs, -1, 0, [this]() {
if (_sweat_pos_y_frames[_animation_index] == 0) {
setVisible(false);
} else {
setVisible(true);
setPosition(_sweat_default_position.x, _sweat_pos_y_frames[_animation_index]);
}
_animation_index++;
if (_animation_index >= _sweat_pos_y_frames.size()) {
_animation_index = 0;
}
});
if (_animation_interval_ms > 0) {
_next_animation_tick = now + _animation_interval_ms;
}
}
SweatDecorator::~SweatDecorator()
{
_sweat.reset();
}
void SweatDecorator::_update()
{
uint32_t now = GetHAL().millis();
// 检查自动销毁
if (_has_lifetime && now >= _destroy_at) {
requestDestroy();
return;
}
// 检查动画帧更新
if (_animation_interval_ms > 0 && now >= _next_animation_tick) {
_next_animation_tick = now + _animation_interval_ms;
int current_y_frame = _sweat_pos_y_frames[_animation_index];
if (current_y_frame == 0) {
// 特殊帧:隐藏图像
setVisible(false);
} else {
// 普通帧:移动位置并显示
setVisible(true);
_sweat->setPos(_sweat_default_position.x, current_y_frame);
}
// 步进索引
_animation_index = (_animation_index + 1) % _sweat_pos_y_frames.size();
}
}
void SweatDecorator::setPosition(int x, int y)
{
Element::setPosition({x, y});
_sweat->setPos(x, y);
// 注意:这里的 setPosition 会覆盖动画中的 x 坐标
if (_sweat) {
_sweat->setPos(x, y);
}
}
void SweatDecorator::setRotation(int rotation)
{
Element::setRotation(rotation);
_sweat->setRotation(rotation);
if (_sweat) {
_sweat->setRotation(rotation);
}
}
void SweatDecorator::setColor(lv_color_t color)
{
_sweat->setImageRecolor(color);
if (_sweat) {
_sweat->setImageRecolor(color);
}
}
void SweatDecorator::setVisible(bool visible)
{
Element::setVisible(visible);
_sweat->setHidden(!visible);
if (_sweat) {
_sweat->setHidden(!visible);
}
}
@@ -157,6 +157,9 @@ static FeatureKeyframe parse_feature(ArduinoJson::JsonObject jsonObject)
if (jsonObject["weight"].is<int>()) {
kf.weight = jsonObject["weight"];
}
if (jsonObject["size"].is<int>()) {
kf.size = jsonObject["size"];
}
return kf;
}
@@ -210,6 +213,12 @@ KeyframeSequence parse_sequence_from_json(const char* jsonContent)
if (kfObj["pitchServo"].is<ArduinoJson::JsonObject>()) {
kf.pitchServo = parse_servo(kfObj["pitchServo"]);
}
if (kfObj["leftRgbColor"].is<const char*>()) {
kf.leftRgbColor = kfObj["leftRgbColor"].as<std::string>();
}
if (kfObj["rightRgbColor"].is<const char*>()) {
kf.rightRgbColor = kfObj["rightRgbColor"].as<std::string>();
}
if (kfObj["durationMs"].is<int>()) {
kf.durationMs = kfObj["durationMs"];
}
@@ -221,3 +230,35 @@ KeyframeSequence parse_sequence_from_json(const char* jsonContent)
}
} // namespace stackchan::animation
namespace stackchan::addon {
void update_neon_light_from_json(NeonLight* left, NeonLight* right, const char* jsonContent)
{
if (!left || !right || !jsonContent) {
return;
}
ArduinoJson::JsonDocument doc;
auto error = ArduinoJson::deserializeJson(doc, jsonContent);
if (error) {
mclog::tagError(_tag, "deserializeJson failed: {}", error.c_str());
return;
}
if (doc["leftRgbDuration"].is<float>()) {
left->setDuration(doc["leftRgbDuration"].as<float>());
}
if (doc["leftRgbColor"].is<std::string>()) {
left->setColor(doc["leftRgbColor"].as<std::string>());
}
if (doc["rightRgbDuration"].is<float>()) {
right->setDuration(doc["rightRgbDuration"].as<float>());
}
if (doc["rightRgbColor"].is<std::string>()) {
right->setColor(doc["rightRgbColor"].as<std::string>());
}
}
} // namespace stackchan::addon
@@ -7,6 +7,7 @@
#include "../avatar/avatar.h"
#include "../motion/motion.h"
#include "../animation/animation.h"
#include "../addons/neon_light/neon_light.h"
namespace stackchan {
@@ -22,4 +23,8 @@ namespace animation {
KeyframeSequence parse_sequence_from_json(const char* jsonContent);
}
namespace addon {
void update_neon_light_from_json(NeonLight* left, NeonLight* right, const char* jsonContent);
}
} // namespace stackchan
+7
View File
@@ -6,6 +6,7 @@
#pragma once
#include "avatar/avatar.h"
#include "motion/motion.h"
#include "addons/neon_light/neon_light.h"
#include "utils/object_pool.h"
namespace stackchan {
@@ -23,6 +24,12 @@ public:
virtual avatar::Avatar& avatar() = 0;
virtual bool hasAvatar() = 0;
// virtual bool hasMotion() = 0; // Motion must be always present
virtual addon::NeonLight& leftNeonLight() = 0;
virtual addon::NeonLight& rightNeonLight() = 0;
};
/**
+65 -28
View File
@@ -4,7 +4,9 @@
* SPDX-License-Identifier: MIT
*/
#pragma once
#include "timed.h"
#include "../modifiable.h"
#include "../utils/random.h"
#include <hal/hal.h>
#include <cstdint>
namespace stackchan {
@@ -13,16 +15,27 @@ namespace stackchan {
* @brief
*
*/
class BlinkModifier : public TimerModifier {
class BlinkModifier : public Modifier {
public:
/**
* @param destroyAfterMs 持续多久后停止眨眼并销毁(0 为永久)
* @param openIntervalMs 睁眼持续时间
* @param closeIntervalMs 闭眼持续时间(瞬间)
*/
BlinkModifier(uint32_t destroyAfterMs = 0, uint32_t openIntervalMs = 5200, uint32_t closeIntervalMs = 200)
: _open_interval_ms(openIntervalMs), _close_interval_ms(closeIntervalMs)
{
if (destroyAfterMs != 0) {
scheduleDestroy(destroyAfterMs);
uint32_t now = GetHAL().millis();
// 处理销毁计时
if (destroyAfterMs > 0) {
_destroy_at = now + destroyAfterMs;
_has_lifetime = true;
}
_should_close = true;
// 初始化:从睁眼状态开始,立即准备闭眼
_state = State::OPEN;
_next_state_tick = now + _open_interval_ms;
}
void resyncEyeWeights()
@@ -32,49 +45,73 @@ public:
void _update(Modifiable& stackchan) override
{
TimerModifier::_update(stackchan);
if (!stackchan.hasAvatar()) {
if (!stackchan.hasAvatar() || stackchan.avatar().isModifyLocked()) {
return;
}
uint32_t now = GetHAL().millis();
// 1. 处理销毁逻辑
if (_has_lifetime && now >= _destroy_at) {
// 销毁前确保眼睛是睁开的
if (_state == State::CLOSED) {
apply_eye_weights(stackchan, _left_eye_weight, _right_eye_weight);
}
requestDestroy();
return;
}
// 2. 处理权重同步请求
// 如果眼睛正闭着,我们只记录权重,等睁眼时再应用
if (_needs_resync) {
_needs_resync = false;
_left_eye_weight = stackchan.avatar().leftEye().getWeight();
_right_eye_weight = stackchan.avatar().rightEye().getWeight();
}
if (_should_close) {
_should_close = false;
// 3. 状态机切换逻辑
if (now >= _next_state_tick) {
if (_state == State::OPEN) {
// 睁眼 -> 闭眼
_state = State::CLOSED;
_next_state_tick = now + _close_interval_ms;
_left_eye_weight = stackchan.avatar().leftEye().getWeight();
_right_eye_weight = stackchan.avatar().rightEye().getWeight();
// 闭眼瞬间,先备份当前权重(以防外部中途修改了权重)
_left_eye_weight = stackchan.avatar().leftEye().getWeight();
_right_eye_weight = stackchan.avatar().rightEye().getWeight();
stackchan.avatar().leftEye().setWeight(25);
stackchan.avatar().rightEye().setWeight(25);
apply_eye_weights(stackchan, 25, 25);
} else {
// 闭眼 -> 睁眼
_state = State::OPEN;
// 睁眼时间可以加一点随机抖动,看起来更自然
uint32_t jitter = Random::getInstance().getInt(0, 500);
_next_state_tick = now + _open_interval_ms + jitter;
getTimer().addTask(_close_interval_ms, 1, 0, [this]() { _should_open = true; });
}
if (_should_open) {
_should_open = false;
stackchan.avatar().leftEye().setWeight(_left_eye_weight);
stackchan.avatar().rightEye().setWeight(_right_eye_weight);
getTimer().addTask(_open_interval_ms, 1, 0, [this]() { _should_close = true; });
apply_eye_weights(stackchan, _left_eye_weight, _right_eye_weight);
}
}
}
private:
enum class State { OPEN, CLOSED };
void apply_eye_weights(Modifiable& stackchan, int left, int right)
{
stackchan.avatar().leftEye().setWeight(left);
stackchan.avatar().rightEye().setWeight(right);
}
State _state;
uint32_t _next_state_tick = 0;
uint32_t _open_interval_ms;
uint32_t _close_interval_ms;
bool _should_close = false;
bool _should_open = false;
uint32_t _destroy_at = 0;
bool _has_lifetime = false;
bool _needs_resync = false;
int _left_eye_weight = 0;
int _right_eye_weight = 0;
int _left_eye_weight = 100;
int _right_eye_weight = 100;
};
} // namespace stackchan
+68 -57
View File
@@ -4,8 +4,10 @@
* SPDX-License-Identifier: MIT
*/
#pragma once
#include "timed.h"
#include "../modifiable.h"
#include <hal/hal.h>
#include <cstdint>
#include <cmath>
namespace stackchan {
@@ -13,83 +15,92 @@ namespace stackchan {
* @brief
*
*/
class BreathModifier : public TimerModifier {
class BreathModifier : public Modifier {
public:
BreathModifier(uint32_t destroyAfterMs = 0, uint32_t updateIntervalMs = 600)
/**
* @param destroyAfterMs 持续时间(0 为永久)
* @param amplitude 呼吸幅度,单位像素
* @param breathCycleMs 呼吸一次的周期(吸+呼)
* @param updateIntervalMs 更新间隔
*/
BreathModifier(uint32_t destroyAfterMs = 0, int amplitude = 16, uint32_t breathCycleMs = 6600,
uint32_t updateIntervalMs = 600)
: _amplitude(amplitude), _breath_cycle_ms(breathCycleMs), _update_interval_ms(updateIntervalMs)
{
if (destroyAfterMs != 0) {
scheduleDestroy(destroyAfterMs);
_start_tick = GetHAL().millis();
if (destroyAfterMs > 0) {
_destroy_at = _start_tick + destroyAfterMs;
_has_lifetime = true;
}
if (updateIntervalMs == 0) {
requestDestroy();
}
getTimer().addTask(updateIntervalMs, -1, 0, [this]() { _update_flag = true; });
}
void _update(Modifiable& stackchan) override
{
TimerModifier::_update(stackchan);
if (!stackchan.hasAvatar()) return;
if (!stackchan.hasAvatar()) {
uint32_t now = GetHAL().millis();
// 销毁逻辑
if (_has_lifetime && now >= _destroy_at) {
reset_position(stackchan.avatar()); // 销毁前复位
requestDestroy();
return;
}
if (!_update_flag) {
if (now - _last_update_tick < _update_interval_ms) {
return;
}
_update_flag = false;
_last_update_tick = now;
// Check state switch
_breathe_count++;
if (_is_breathing_in) {
if (_breathe_count >= _breathe_in_count) {
_is_breathing_in = false;
_breathe_count = 0;
}
} else {
if (_breathe_count >= _breathe_out_count) {
_is_breathing_in = true;
_breathe_count = 0;
}
}
// 使用正弦波计算偏移量
// (now - _start_tick) / cycle 得到进度,乘以 2PI 传给 sin
float phase = (float)((now - _start_tick) % _breath_cycle_ms) / _breath_cycle_ms;
float sin_val = sinf(phase * 2.0f * M_PI);
// Update breath
auto& avatar = stackchan.avatar();
_left_eye_position = avatar.leftEye().getPosition();
_right_eye_position = avatar.rightEye().getPosition();
_mouth_position = avatar.mouth().getPosition();
// 计算当前偏移
int current_offset = static_cast<int>(sin_val * _amplitude);
if (_is_breathing_in) {
_left_eye_position.y += _breathe_in_offset;
_right_eye_position.y += _breathe_in_offset;
_mouth_position.y += _breathe_in_offset;
} else {
_left_eye_position.y += _breathe_out_offset;
_right_eye_position.y += _breathe_out_offset;
_mouth_position.y += _breathe_out_offset;
}
avatar.leftEye().setPosition(_left_eye_position);
avatar.rightEye().setPosition(_right_eye_position);
avatar.mouth().setPosition(_mouth_position);
// 应用增量偏移
apply_relative_offset(stackchan.avatar(), current_offset);
}
private:
// Keep in and out total offset added up to 0
const int _breathe_in_count = 7;
const int _breathe_in_offset = -4;
const int _breathe_out_count = 7;
const int _breathe_out_offset = 4;
void apply_relative_offset(avatar::Avatar& avatar, int new_offset)
{
// 计算本次需要移动的差值
int delta = new_offset - _last_applied_offset;
if (delta == 0) return;
bool _update_flag = false;
bool _is_breathing_in = true;
int _breathe_count = 0;
// 批量移动五官
move_component(avatar.leftEye(), delta);
move_component(avatar.rightEye(), delta);
move_component(avatar.mouth(), delta);
uitk::Vector2i _left_eye_position;
uitk::Vector2i _right_eye_position;
uitk::Vector2i _mouth_position;
_last_applied_offset = new_offset;
}
void move_component(avatar::Feature& comp, int delta_y)
{
auto pos = comp.getPosition();
pos.y += delta_y;
comp.setPosition(pos);
}
void reset_position(avatar::Avatar& avatar)
{
// 将偏移归零
apply_relative_offset(avatar, 0);
}
int _amplitude;
uint32_t _breath_cycle_ms;
uint32_t _update_interval_ms;
uint32_t _start_tick = 0;
uint32_t _last_update_tick = 0;
uint32_t _destroy_at = 0;
bool _has_lifetime = false;
int _last_applied_offset = 0; // 记录上一次应用的偏移量,用于增量更新
};
} // namespace stackchan
+93 -76
View File
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: MIT
*/
#pragma once
#include "timed.h"
#include "../modifiable.h"
#include "../avatar/decorators/decorators.h"
#include "../utils/random.h"
#include <smooth_ui_toolkit.hpp>
@@ -14,22 +14,20 @@
namespace stackchan {
class HeadPetModifier : public TimerModifier {
/**
* @brief
*
*/
class HeadPetModifier : public Modifier {
public:
HeadPetModifier(uint32_t restoreDelayMs = 3000) : _restore_delay_ms(restoreDelayMs)
{
// 绑定信号
_signal_connection = GetHAL().onHeadPetGesture.connect([this](HeadPetGesture gesture) {
LvglLockGuard lock;
if (gesture == HeadPetGesture::SwipeForward || gesture == HeadPetGesture::SwipeBackward) {
_trigger_happy = true;
_trigger_release = false;
// 每次抚摸都打断恢复倒计时
_should_restore = false;
_restore_timer_active = false;
_event_swipe = true;
} else if (gesture == HeadPetGesture::Release) {
_trigger_release = true;
_event_release = true;
}
});
}
@@ -41,105 +39,124 @@ public:
void _update(Modifiable& stackchan) override
{
TimerModifier::_update(stackchan);
uint32_t now = GetHAL().millis();
if (_trigger_happy) {
_trigger_happy = false;
// 1. 首次进入记录当前位置
if (!_in_happy_state) {
_prev_emotion = stackchan.avatar().getEmotion();
_prev_pitch = stackchan.motion().getCurrentPitchAngle();
_prev_yaw = stackchan.motion().getCurrentYawAngle();
_in_happy_state = true;
}
// 2. 表情反馈
stackchan.avatar().setEmotion(avatar::Emotion::Happy);
int duration = Random::getInstance().getInt(1500, 2500);
stackchan.avatar().addDecorator(
std::make_unique<avatar::HeartDecorator>(lv_screen_active(), duration, 500));
// 3. 执行随机动作
perform_random_motion(stackchan);
// 处理“被抚摸中”事件
if (_event_swipe) {
_event_swipe = false;
handle_swipe(stackchan);
// 只要在摸,就推迟恢复时间
_is_waiting_restore = false;
}
if (_trigger_release) {
_trigger_release = false;
// 处理“手松开”事件
if (_event_release) {
_event_release = false;
if (_in_happy_state) {
// 启动恢复计时
_restore_timer_active = true;
getTimer().addTask(_restore_delay_ms, 1, 0, [this]() { _should_restore = true; });
_is_waiting_restore = true;
_restore_tick = now + _restore_delay_ms;
}
}
if (_should_restore) {
_should_restore = false;
if (_restore_timer_active && _in_happy_state) {
// 恢复原状
stackchan.avatar().setEmotion(_prev_emotion);
auto& motion = stackchan.motion();
motion.moveWithSpeed(_prev_yaw, _prev_pitch, 200);
_in_happy_state = false;
_restore_timer_active = false;
}
// 处理恢复逻辑
if (_is_waiting_restore && now >= _restore_tick) {
_is_waiting_restore = false;
restore_original_state(stackchan);
}
}
private:
void perform_random_motion(Modifiable& stackchan)
void handle_swipe(Modifiable& stackchan)
{
auto& avatar = stackchan.avatar();
// 首次进入开心状态,记录原始信息
if (!_in_happy_state) {
_in_happy_state = true;
_prev_emotion = avatar.getEmotion();
auto angles = stackchan.motion().getCurrentAngles();
_prev_yaw = angles.x;
_prev_pitch = angles.y;
}
// 视觉反馈
avatar.setEmotion(avatar::Emotion::Happy);
// 添加爱心装饰
int duration = Random::getInstance().getInt(1500, 2500);
avatar.removeDecorator(_heart_decorator_id);
avatar.removeDecorator(_shy_decorator_id);
_heart_decorator_id =
avatar.addDecorator(std::make_unique<avatar::HeartDecorator>(lv_screen_active(), duration, 500));
_shy_decorator_id = avatar.addDecorator(std::make_unique<avatar::ShyDecorator>(lv_screen_active(), duration));
// 动作反馈
perform_pet_motion(stackchan);
}
void restore_original_state(Modifiable& stackchan)
{
if (!_in_happy_state) {
return;
}
stackchan.avatar().setEmotion(_prev_emotion);
stackchan.motion().moveWithSpeed(_prev_yaw, _prev_pitch, 200);
_in_happy_state = false;
}
void perform_pet_motion(Modifiable& stackchan)
{
int action = Random::getInstance().getInt(0, 2);
auto& motion = stackchan.motion();
if (motion.isModifyLocked() || motion.isMoving()) {
return;
}
int speed = Random::getInstance().getInt(400, 800);
int action = Random::getInstance().getInt(0, 2);
int speed = Random::getInstance().getInt(300, 500);
int32_t target_pitch = _prev_pitch;
int32_t target_yaw = _prev_yaw;
int32_t target_pitch = _prev_pitch;
// 角度限制: 10 units = 1度
switch (action) {
case 0: // 舒服地蹭手/抬头
case 0: // 抬头
target_pitch += Random::getInstance().getInt(150, 250);
target_yaw += Random::getInstance().getInt(-50, 50); // 微调 Yaw
target_yaw += Random::getInstance().getInt(-50, 50);
break;
case 1: // 开心歪头/摇晃
target_pitch -= Random::getInstance().getInt(0, 50); // 略微低头
if (Random::getInstance().getInt(0, 1) == 0) {
target_yaw += Random::getInstance().getInt(100, 200);
} else {
target_yaw -= Random::getInstance().getInt(100, 200);
}
case 1: // 歪头
target_pitch -= Random::getInstance().getInt(0, 50);
target_yaw += (Random::getInstance().getInt(0, 1) == 0 ? 150 : -150);
break;
case 2: // 兴奋大幅度抬头
case 2: // 大幅度开心
target_pitch += Random::getInstance().getInt(250, 400);
break;
}
target_pitch = uitk::clamp(target_pitch, 0, 900);
target_yaw = uitk::clamp(target_yaw, -1280, 1280);
// 自然范围限制
target_pitch = uitk::clamp(target_pitch, 0, 540);
target_yaw = uitk::clamp(target_yaw, -512, 512);
motion.moveWithSpeed(target_yaw, target_pitch, speed);
}
// 信号相关
int _signal_connection;
volatile bool _event_swipe = false;
volatile bool _event_release = false;
// 状态机相关
bool _in_happy_state = false;
bool _is_waiting_restore = false;
uint32_t _restore_tick = 0;
uint32_t _restore_delay_ms;
int _heart_decorator_id = -1;
int _shy_decorator_id = -1;
bool _trigger_happy = false;
bool _trigger_release = false;
bool _in_happy_state = false;
bool _restore_timer_active = false;
bool _should_restore = false;
// 记忆相关
avatar::Emotion _prev_emotion = avatar::Emotion::Neutral;
int32_t _prev_pitch = 0;
int32_t _prev_yaw = 0;
int32_t _prev_pitch = 0;
};
} // namespace stackchan
@@ -0,0 +1,93 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include "../modifiable.h"
#include "../utils/random.h"
#include <smooth_ui_toolkit.hpp>
#include <hal/hal.h>
#include <cstdint>
namespace stackchan {
/**
* @brief
*
*/
class IdleExpressionModifier : public Modifier {
public:
IdleExpressionModifier(uint32_t interval_min = 2000, uint32_t interval_max = 6000)
: _interval_min(interval_min), _interval_max(interval_max)
{
_next_tick = GetHAL().millis() + 500;
}
void _update(Modifiable& stackchan) override
{
if (!stackchan.hasAvatar() || stackchan.avatar().isModifyLocked()) {
return;
}
uint32_t now = GetHAL().millis();
if (now < _next_tick) {
return;
}
// 执行随机微表情
perform_idle_emotion(stackchan.avatar());
// 计算下一次触发时间
uint32_t delay = Random::getInstance().getInt(_interval_min, _interval_max);
_next_tick = now + delay;
}
private:
void perform_idle_emotion(avatar::Avatar& avatar)
{
int action = Random::getInstance().getInt(0, 100);
if (action < 70) {
// 【动作 1:眼神游离】双眼协同移动一个小范围
int offsetX = Random::getInstance().getInt(-20, 20);
int offsetY = Random::getInstance().getInt(-15, 15);
avatar.leftEye().setPosition({offsetX, offsetY});
avatar.rightEye().setPosition({offsetX, offsetY});
// 嘴巴也配合动一下
avatar.mouth().setPosition({0, Random::getInstance().getInt(0, 10)});
} else if (action < 80) {
// 【动作 3:嘴巴歪一下】旋转角度
// Rotation: 0~3600
int rotation = Random::getInstance().getInt(-30, 30);
// 加上基准值
avatar.mouth().setRotation(rotation < 0 ? 3600 + rotation : rotation);
} else {
// 【动作 4:回归中性】
reset_to_neutral(avatar);
}
}
void reset_to_neutral(avatar::Avatar& avatar)
{
// 位置回归中心
avatar.leftEye().setPosition({0, 0});
avatar.rightEye().setPosition({0, 0});
avatar.mouth().setPosition({0, 0});
// 缩放回归正常
avatar.leftEye().setSize(0);
avatar.rightEye().setSize(0);
// 旋转和权重回归
avatar.mouth().setRotation(0);
avatar.mouth().setWeight(0);
}
uint32_t _interval_min;
uint32_t _interval_max;
uint32_t _next_tick = 0;
};
} // namespace stackchan
+74 -64
View File
@@ -4,108 +4,118 @@
* SPDX-License-Identifier: MIT
*/
#pragma once
#include "timed.h"
#include "../modifiable.h"
#include "../utils/random.h"
#include <smooth_ui_toolkit.hpp>
// #include <mooncake_log.h>
#include <hal/hal.h>
#include <cstdint>
namespace stackchan {
class IdleMotionModifier : public TimerModifier {
/**
* @brief
*
*/
class IdleMotionModifier : public Modifier {
public:
IdleMotionModifier(uint32_t interval_min = 2000, uint32_t interval_max = 8000)
IdleMotionModifier(uint32_t interval_min = 4000, uint32_t interval_max = 8000)
: _interval_min(interval_min), _interval_max(interval_max)
{
schedule_next_move();
_next_tick = GetHAL().millis() + 1000; // 启动 1 秒后开始第一次动作
}
void pause()
{
_paused = true;
}
void resume()
{
if (_paused) {
_paused = false;
if (!_timer_active) {
schedule_next_move();
}
_paused = false;
_next_tick = GetHAL().millis() + 500;
}
}
void _update(Modifiable& stackchan) override
{
TimerModifier::_update(stackchan);
if (_paused || !stackchan.hasAvatar()) return;
if (_trigger_move) {
_trigger_move = false;
_timer_active = false;
if (!_paused) {
perform_idle_motion(stackchan);
schedule_next_move();
}
uint32_t now = GetHAL().millis();
// 如果时间没到,直接跳过
if (now < _next_tick) {
return;
}
// 如果上次动作还没做完,就把下一次尝试推迟 500ms,避免指令堆积
if (stackchan.motion().isMoving()) {
_next_tick = now + 500;
return;
}
// 执行动作
perform_idle_motion(stackchan);
// 算下一次的时间间隔
uint32_t delay = Random::getInstance().getInt(_interval_min, _interval_max);
_next_tick = now + delay;
// mclog::info("next idle motion in {} ms", delay);
}
private:
void schedule_next_move()
{
if (_paused) {
_timer_active = false;
return;
}
_timer_active = true;
uint32_t delay = Random::getInstance().getInt(_interval_min, _interval_max);
getTimer().addTask(delay, 1, 0, [this]() { _trigger_move = true; });
}
void perform_idle_motion(Modifiable& stackchan)
{
auto& motion = stackchan.motion();
int action = Random::getInstance().getInt(0, 10); // Probabilistic choice
int32_t current_yaw = motion.getCurrentYawAngle();
int32_t current_pitch = motion.getCurrentPitchAngle();
int32_t target_yaw = current_yaw;
int32_t target_pitch = current_pitch;
int speed = 400;
if (action < 5) {
// 50% chance: Casual look around (Look at a random point in front)
target_yaw = Random::getInstance().getInt(-300, 300);
target_pitch = Random::getInstance().getInt(100, 250);
speed = Random::getInstance().getInt(200, 500); // Slow to medium speed
} else if (action < 7) {
// 20% chance: Small adjustment / "Breathing" (Very small movement from current)
target_yaw += Random::getInstance().getInt(-100, 100);
target_pitch += Random::getInstance().getInt(-50, 50);
speed = Random::getInstance().getInt(100, 300); // Very slow
} else if (action < 9) {
// 20% chance: Quick glance (Faster, smaller range)
target_yaw += Random::getInstance().getInt(-100, 100);
target_pitch += Random::getInstance().getInt(-30, 30);
speed = Random::getInstance().getInt(300, 600); // Fast
} else {
// 10% chance: Return to center / Neutral
target_yaw = 0;
target_pitch = 200; // Slightly looking up/forward
speed = 100;
if (motion.isModifyLocked()) {
return;
}
target_yaw = uitk::clamp(target_yaw, -1280, 1280);
target_pitch = uitk::clamp(target_pitch, 0, 900);
speed = uitk::clamp(speed, 0, 1000);
int action = Random::getInstance().getInt(0, 100);
motion.moveWithSpeed(target_yaw, target_pitch, speed);
if (action < 50) {
// 【动作 1:随意环视】使用归一化坐标 (-1.0 ~ 1.0)
float target_x = Random::getInstance().getFloat(-0.4f, 0.4f); // 左右看
float target_y = Random::getInstance().getFloat(-0.95f, 0.2f); // 上下看
int speed = Random::getInstance().getInt(150, 300);
// mclog::info("action 1: look at normalized ({}, {}) in speed {}", target_x, target_y, speed);
motion.lookAtNormalized(target_x, target_y, speed);
} else if (action < 80) {
// 【动作 2:微小的观察动作】基于当前位置的小偏移
auto current = motion.getCurrentAngles(); // Vector2i(yaw, pitch)
int diff_yaw = Random::getInstance().getInt(-150, 150);
int diff_pitch = Random::getInstance().getInt(-80, 80);
int target_yaw = uitk::clamp(current.x + diff_yaw, -800, 800);
int target_pitch = uitk::clamp(current.y + diff_pitch, 0, 600);
int speed = Random::getInstance().getInt(100, 250);
// mclog::info("action 2: small move to ({}, {}) in speed {}", target_yaw, target_pitch, speed);
motion.moveWithSpeed(target_yaw, target_pitch, speed);
} else if (action < 90) {
// 【动作 3:快速撇一眼】速度快,跨度中等
int target_yaw = Random::getInstance().getInt(-500, 500);
int target_pitch = Random::getInstance().getInt(100, 400);
int speed = Random::getInstance().getInt(250, 400);
// mclog::info("action 3: quick glance to ({}, {}) in speed {}", target_yaw, target_pitch, speed);
motion.moveWithSpeed(target_yaw, target_pitch, speed);
} else {
// 【动作 4yaw 回正】
int target_pitch = Random::getInstance().getInt(50, 400);
int speed = Random::getInstance().getInt(100, 300);
// mclog::info("action 4: go home to (0, {}) in speed {}", target_pitch, speed);
motion.moveWithSpeed(0, target_pitch, speed);
}
}
uint32_t _interval_min;
uint32_t _interval_max;
bool _trigger_move = false;
bool _paused = false;
bool _timer_active = false;
uint32_t _next_tick = 0;
bool _paused = false;
};
} // namespace stackchan
+104 -41
View File
@@ -4,8 +4,8 @@
* SPDX-License-Identifier: MIT
*/
#pragma once
#include "timed.h"
#
#include "../modifiable.h"
#include <hal/hal.h>
#include <cstdint>
namespace stackchan {
@@ -14,61 +14,124 @@ namespace stackchan {
* @brief
*
*/
class IMUMotionModifier : public TimerModifier {
class ImuEventModifier : public Modifier {
public:
IMUMotionModifier()
ImuEventModifier(uint32_t reactionDurationMs = 4000) : _reaction_duration_ms(reactionDurationMs)
{
GetHAL().onImuMotionEvent.connect([&](ImuMotionEvent event) {
if (_is_applied) {
return;
_signal_connection = GetHAL().onImuMotionEvent.connect([this](ImuMotionEvent event) {
if (event == ImuMotionEvent::Shake) {
_event_shake = true;
}
_new_event = event;
});
}
~ImuEventModifier()
{
GetHAL().onImuMotionEvent.disconnect(_signal_connection);
}
void _update(Modifiable& stackchan) override
{
TimerModifier::_update(stackchan);
uint32_t now = GetHAL().millis();
if (_new_event != ImuMotionEvent::None) {
_new_event = ImuMotionEvent::None;
apply(stackchan, false);
_is_applied = true;
_restore_flag = false;
getTimer().addTask(2000, 0, 0, [&]() { _restore_flag = true; });
// 收到晃动事件
if (_event_shake) {
_event_shake = false;
handle_shake_start(stackchan, now);
}
if (_restore_flag) {
apply(stackchan, true);
_restore_flag = false;
_is_applied = false;
}
}
// 如果处于晃动反应状态
if (_is_reacting) {
if (now >= _next_toggle_tick) {
_next_toggle_tick = now + 600;
void apply(Modifiable& stackchan, bool isRestore)
{
auto& avatar = stackchan.avatar();
if (isRestore) {
avatar.leftEye().setPosition({0, 0});
avatar.rightEye().setPosition({0, 0});
avatar.leftEye().setSize(0);
avatar.rightEye().setSize(0);
avatar.mouth().setPosition({0, 0});
avatar.mouth().setWeight(0);
} else {
avatar.leftEye().setPosition({-20, 0});
avatar.rightEye().setPosition({20, 0});
avatar.leftEye().setSize(60);
avatar.rightEye().setSize(60);
avatar.mouth().setPosition({0, -40});
avatar.mouth().setWeight(58);
_toggle_phase = !_toggle_phase;
int mouth_rotation = _toggle_phase ? -25 : 25;
auto& avatar = stackchan.avatar();
avatar.mouth().setRotation(mouth_rotation);
avatar.mouth().setWeight(65);
}
// Lock motion modify and move home
auto& motion = stackchan.motion();
if (!motion.isModifyLocked()) {
motion.setModifyLock(true);
motion.goHome(300);
}
// 检查是否结束反应
if (now >= _restore_at) {
restore_state(stackchan);
}
}
}
private:
ImuMotionEvent _new_event = ImuMotionEvent::None;
bool _is_applied = false;
bool _restore_flag = false;
void handle_shake_start(Modifiable& stackchan, uint32_t now)
{
if (!_is_reacting) {
// 首次触发时,记录状态以便恢复
_is_reacting = true;
auto& avatar = stackchan.avatar();
avatar.setModifyLock(true);
avatar.leftEye().setVisible(false);
avatar.rightEye().setVisible(false);
stackchan.avatar().removeDecorator(_dizzy_decorator_id);
stackchan.avatar().removeDecorator(_shy_decorator_id);
_dizzy_decorator_id =
stackchan.avatar().addDecorator(std::make_unique<avatar::DizzyDecorator>(lv_screen_active(), 0, 300));
_shy_decorator_id =
stackchan.avatar().addDecorator(std::make_unique<avatar::ShyDecorator>(lv_screen_active(), 0));
}
// 刷新恢复时间和切换时间
_restore_at = now + _reaction_duration_ms;
if (_next_toggle_tick <= now) {
_next_toggle_tick = now; // 立即触发第一次嘴巴动作
}
}
void restore_state(Modifiable& stackchan)
{
if (!_is_reacting) {
return;
}
auto& avatar = stackchan.avatar();
avatar.setModifyLock(false);
avatar.leftEye().setVisible(true);
avatar.rightEye().setVisible(true);
avatar.mouth().setWeight(0);
avatar.mouth().setRotation(0);
stackchan.avatar().removeDecorator(_dizzy_decorator_id);
stackchan.avatar().removeDecorator(_shy_decorator_id);
_dizzy_decorator_id = -1;
_shy_decorator_id = -1;
auto& motion = stackchan.motion();
motion.setModifyLock(false);
_is_reacting = false;
}
// 信号相关
int _signal_connection;
volatile bool _event_shake = false;
// 状态控制
bool _is_reacting = false;
bool _toggle_phase = false;
uint32_t _restore_at = 0;
uint32_t _next_toggle_tick = 0;
uint32_t _reaction_duration_ms;
int _dizzy_decorator_id = -1;
int _shy_decorator_id = -1;
};
} // namespace stackchan
@@ -10,5 +10,6 @@
#include "speaking.h"
#include "head_pet.h"
#include "idle_motion.h"
#include "idle_expression.h"
#include "dance.h"
#include "imu.h"
+93 -72
View File
@@ -4,123 +4,144 @@
* SPDX-License-Identifier: MIT
*/
#pragma once
#include "timed.h"
#include "../modifiable.h"
#include "../utils/random.h"
#include <smooth_ui_toolkit.hpp>
#include <hal/hal.h>
#include <cstdint>
namespace stackchan {
/**
* @brief
*
*/
class SpeakingModifier : public TimerModifier {
class SpeakingModifier : public Modifier {
public:
SpeakingModifier(uint32_t destroyAfterMs = 0, uint32_t updateIntervalMs = 180, bool enableMotion = true)
: _enable_motion(enableMotion)
/**
* @param destroyAfterMs 持续说话时间(0 为永久,直到手动移除)
* @param mouthIntervalMs 嘴巴开合频率(默认 180ms)
* @param enableMotion 是否在说话时伴随头部微动
*/
SpeakingModifier(uint32_t destroyAfterMs = 0, uint32_t mouthIntervalMs = 180, bool enableMotion = true)
: _mouth_interval_ms(mouthIntervalMs), _enable_motion(enableMotion)
{
if (destroyAfterMs != 0) {
getTimer().addTask(destroyAfterMs, -1, 0, [this]() { _destroy_flag = true; });
uint32_t now = GetHAL().millis();
// 销毁计时
if (destroyAfterMs > 0) {
_destroy_at = now + destroyAfterMs;
_has_lifetime = true;
}
if (updateIntervalMs == 0) {
requestDestroy();
}
getTimer().addTask(updateIntervalMs, -1, 0, [this]() { _update_flag = true; });
// 嘴巴计时
_next_mouth_tick = now + _mouth_interval_ms;
// 动作计时
if (_enable_motion) {
schedule_next_motion();
_next_motion_tick = now + Random::getInstance().getInt(1000, 2000);
}
_need_get_prev_angles = true;
}
void _update(Modifiable& stackchan) override
{
TimerModifier::_update(stackchan);
if (!stackchan.hasAvatar()) {
return;
}
if (_motion_flag) {
_motion_flag = false;
perform_speaking_motion(stackchan);
schedule_next_motion();
}
uint32_t now = GetHAL().millis();
if (!_update_flag) {
return;
}
_update_flag = false;
if (_destroy_flag) {
stackchan.avatar().mouth().setWeight(0);
// 检查销毁逻辑
if (_has_lifetime && now >= _destroy_at) {
stackchan.avatar().mouth().setWeight(0); // 闭嘴
requestDestroy();
return;
}
auto& random = Random::getInstance();
// 嘴巴开合动画
if (now >= _next_mouth_tick) {
_next_mouth_tick = now + _mouth_interval_ms;
animate_mouth(stackchan.avatar());
}
_is_opened = !_is_opened;
if (_is_opened) {
stackchan.avatar().mouth().setWeight(random.getInt(_open_min_weight, _open_max_weight));
} else {
stackchan.avatar().mouth().setWeight(random.getInt(_close_min_weight, _close_max_weight));
// 身体微动动作
if (_enable_motion && now >= _next_motion_tick) {
// 随机下一个动作的时间 (1.5s ~ 2.5s)
_next_motion_tick = now + Random::getInstance().getInt(1500, 2500);
perform_subtle_speaking_motion(stackchan);
}
}
private:
void schedule_next_motion()
void animate_mouth(avatar::Avatar& avatar)
{
uint32_t delay = Random::getInstance().getInt(1600, 2200);
getTimer().addTask(delay, 1, 0, [this]() { _motion_flag = true; });
_is_mouth_open = !_is_mouth_open;
auto& random = Random::getInstance();
int weight = _is_mouth_open ? random.getInt(_open_min_weight, _open_max_weight)
: random.getInt(_close_min_weight, _close_max_weight);
avatar.mouth().setWeight(weight);
}
void perform_speaking_motion(Modifiable& stackchan)
void perform_subtle_speaking_motion(Modifiable& stackchan)
{
auto& motion = stackchan.motion();
int action = Random::getInstance().getInt(0, 10);
int32_t current_yaw = motion.getCurrentYawAngle();
int32_t current_pitch = motion.getCurrentPitchAngle();
int32_t target_yaw = current_yaw;
int32_t target_pitch = current_pitch;
int speed = 120;
if (action < 4) {
// Emphasize: Nod / Head bob
target_pitch += Random::getInstance().getInt(-30, 60);
speed = Random::getInstance().getInt(100, 160);
} else if (action < 7) {
// Look at audience: Small yaw changes
target_yaw += Random::getInstance().getInt(-60, 60);
target_pitch += Random::getInstance().getInt(-30, 30);
speed = Random::getInstance().getInt(160, 200);
} else if (action < 9) {
// Reset / Center attention
target_yaw = Random::getInstance().getInt(-60, 60);
target_pitch = 200 + Random::getInstance().getInt(-50, 50);
speed = 120;
if (motion.isMoving()) {
return;
}
target_yaw = uitk::clamp(target_yaw, -1280, 1280);
target_pitch = uitk::clamp(target_pitch, 0, 900);
uitk::Vector2i current_actual_angles = motion.getCurrentAngles();
if (_need_get_prev_angles) {
_prev_angles = current_actual_angles;
_need_get_prev_angles = false;
} else {
// If there is a large external movement
// sync the baseline angles to preventsnapping back to old position
const int32_t threshold = 300;
int32_t diff_x = std::abs(current_actual_angles.x - _prev_angles.x);
int32_t diff_y = std::abs(current_actual_angles.y - _prev_angles.y);
if (diff_x > threshold || diff_y > threshold) {
_prev_angles = current_actual_angles;
}
}
int32_t target_yaw = _prev_angles.x;
int32_t target_pitch = _prev_angles.y;
int action = Random::getInstance().getInt(0, 10);
int speed = Random::getInstance().getInt(100, 200); // 说话时的动作都很慢
if (action < 5) {
// 动作 A:轻微点头 (Nod)
target_pitch += Random::getInstance().getInt(-20, 50);
} else {
// 动作 B:轻微摆头 (Yaw drift)
target_yaw += Random::getInstance().getInt(-40, 40);
target_pitch += Random::getInstance().getInt(-20, 20);
}
motion.moveWithSpeed(target_yaw, target_pitch, speed);
}
// 配置常量
const int _open_min_weight = 40;
const int _open_max_weight = 70;
const int _open_max_weight = 80;
const int _close_min_weight = 0;
const int _close_max_weight = 30;
const int _close_max_weight = 20;
bool _enable_motion = false;
bool _motion_flag = false;
bool _update_flag = false;
bool _destroy_flag = false;
bool _is_opened = false;
// 计时状态
uint32_t _destroy_at = 0;
uint32_t _next_mouth_tick = 0;
uint32_t _next_motion_tick = 0;
uint32_t _mouth_interval_ms;
bool _has_lifetime = false;
bool _enable_motion = false;
bool _is_mouth_open = false;
bool _need_get_prev_angles = true;
uitk::Vector2i _prev_angles;
};
} // namespace stackchan
+16 -46
View File
@@ -4,73 +4,42 @@
* SPDX-License-Identifier: MIT
*/
#pragma once
#include "../utils/timer.h"
#include "../modifiable.h"
#include <string_view>
#include <functional>
#include <hal/hal.h>
#include <cstdint>
#include <memory>
namespace stackchan {
/**
* @brief
*
*/
class TimerModifier : public Modifier {
public:
void scheduleDestroy(uint32_t ms)
{
getTimer().addTask(ms, 1, 0, [this]() { requestDestroy(); });
}
Timer& getTimer()
{
if (!_timer) {
_timer = std::make_unique<Timer>();
}
return *_timer;
}
virtual void _update(Modifiable& stackchan) override
{
if (_timer) {
_timer->update();
}
}
private:
std::unique_ptr<Timer> _timer;
};
/**
* @brief A timed event modifier base, which will be destroyed after the given duration
*
*/
class TimedEventModifier : public TimerModifier {
class TimedEventModifier : public Modifier {
public:
TimedEventModifier(uint32_t durationMs)
TimedEventModifier(uint32_t durationMs) : _duration_ms(durationMs), _start_time(0), _is_started(false)
{
if (durationMs == 0) {
requestDestroy();
}
getTimer().addTask(durationMs, -1, 0, [this]() { _destroy_flag = true; });
_is_first_in = true;
}
void _update(Modifiable& stackchan) override
{
TimerModifier::_update(stackchan);
uint32_t now = GetHAL().millis();
if (_is_first_in) {
_is_first_in = false;
if (!_is_started) {
_is_started = true;
_start_time = now;
_on_start(stackchan);
if (_duration_ms == 0) {
_on_end(stackchan);
requestDestroy();
}
return;
}
if (_destroy_flag) {
if (now - _start_time >= _duration_ms) {
_on_end(stackchan);
requestDestroy();
}
@@ -85,8 +54,9 @@ public:
}
private:
bool _is_first_in = true;
bool _destroy_flag = false;
uint32_t _duration_ms;
uint32_t _start_time;
bool _is_started;
};
/**
+10
View File
@@ -137,3 +137,13 @@ void Motion::setAutoAngleSyncEnabled(bool enabled)
_yaw_servo->setAutoAngleSyncEnabled(enabled);
_pitch_servo->setAutoAngleSyncEnabled(enabled);
}
void Motion::setModifyLock(bool locked)
{
_is_modify_locked = locked;
}
bool Motion::isModifyLocked()
{
return _is_modify_locked;
}
+4
View File
@@ -156,9 +156,13 @@ public:
void setAutoTorqueReleaseEnabled(bool enabled);
void setAutoAngleSyncEnabled(bool enabled);
void setModifyLock(bool locked);
bool isModifyLocked();
private:
std::unique_ptr<Servo> _yaw_servo;
std::unique_ptr<Servo> _pitch_servo;
bool _is_modify_locked = false;
static constexpr float RAD_TO_DEG = 180.0f / M_PI;
+9 -2
View File
@@ -37,7 +37,7 @@ void Servo::init()
void Servo::update()
{
// Update at 50Hz
// Keep update in at most 50Hz
if (GetHAL().millis() - _last_tick < 20) {
return;
}
@@ -45,7 +45,7 @@ void Servo::update()
// Apply animation
if (!_angle_anim.done()) {
_angle_anim.update();
_angle_anim.updateWithDelta(0.02f); // Fixed delta time for consistency
set_angle_impl(static_cast<int>(_angle_anim.directValue()));
}
@@ -92,6 +92,11 @@ int Servo::getCurrentAngle()
return _angle_anim.directValue();
}
bool Servo::isMoving()
{
return _angle_anim.done() == false || is_moving_impl();
}
void Servo::apply_default_spring_options()
{
auto& options = _angle_anim.springOptions();
@@ -102,6 +107,8 @@ void Servo::apply_default_spring_options()
void Servo::update_angle_anim_target(int angle)
{
angle = uitk::clamp(angle, _angle_limit.x, _angle_limit.y);
if (_auto_angle_sync_enabled) {
_angle_anim.teleport(getCurrentAngle()); // Use current angle as start
}
+14 -5
View File
@@ -86,10 +86,7 @@ public:
* @return true
* @return false
*/
virtual bool isMoving()
{
return false;
}
bool isMoving();
/**
* @brief
@@ -139,6 +136,14 @@ public:
{
}
/**
* @brief
*
*/
virtual void resetZeroCalibration()
{
}
protected:
Servo()
{
@@ -155,8 +160,12 @@ protected:
* @param angle
*/
virtual void set_angle_impl(int angle) = 0;
virtual bool is_moving_impl()
{
return false;
}
private:
protected:
uitk::Vector2i _angle_limit;
uitk::AnimateValue _angle_anim;
+22 -11
View File
@@ -90,18 +90,14 @@ public:
return false;
}
/**
* @brief Check if motion is attached
*
* @return true
* @return false
*/
bool hasMotion()
addon::NeonLight& leftNeonLight() override
{
if (_motion) {
return true;
}
return false;
return _left_neon_light;
}
addon::NeonLight& rightNeonLight() override
{
return _right_neon_light;
}
/**
@@ -143,6 +139,9 @@ public:
if (_motion) {
_motion->update();
}
_left_neon_light.update();
_right_neon_light.update();
}
/**
@@ -169,9 +168,21 @@ public:
}
}
/**
* @brief
*
* @param jsonContent
*/
void updateNeonLightFromJson(const char* jsonContent)
{
addon::update_neon_light_from_json(&_left_neon_light, &_right_neon_light, jsonContent);
}
private:
std::unique_ptr<avatar::Avatar> _avatar;
std::unique_ptr<motion::Motion> _motion;
addon::LeftNeonLight _left_neon_light;
addon::RightNeonLight _right_neon_light;
ObjectPool<Modifier> _modifier_pool;
};
-165
View File
@@ -1,165 +0,0 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <hal/hal.h>
#include <functional>
#include <vector>
namespace stackchan {
class Timer {
public:
using Callback = std::function<void(void)>;
struct Task {
uint32_t intervalMs; // Time interval between executions (ms)
uint32_t lastTick; // Last tick recorded
uint32_t delayMs; // Delay before first execution (ms)
int repeatCount; // Remaining repeat count (-1 = infinite)
Callback callback; // Task callback function
bool enabled; // Enabled or disabled
bool started; // Has delay finished and started interval execution?
};
/**
* @brief Register a new task
* @param intervalMs Execution interval in milliseconds
* @param repeatCount Times to execute (-1 = infinite loop)
* @param delayMs Delay before the first execution (ms)
* @param cb Callback function
* @param start Whether to start immediately
* @return Task ID
*/
int addTask(uint32_t intervalMs, int repeatCount, uint32_t delayMs, Callback cb, bool start = true)
{
int id = -1;
if (!_free_indices.empty()) {
id = _free_indices.back();
_free_indices.pop_back();
} else {
id = (int)_tasks.size();
_tasks.push_back(Task());
}
Task& t = _tasks[id];
t.intervalMs = intervalMs;
t.repeatCount = repeatCount;
t.delayMs = delayMs;
t.callback = cb;
t.enabled = start;
t.started = false;
t.lastTick = GetHAL().millis();
return id;
}
void setInterval(int id, uint32_t intervalMs)
{
if (is_valid(id)) {
_tasks[id].intervalMs = intervalMs;
}
}
void setRepeat(int id, int repeatCount)
{
if (is_valid(id)) {
_tasks[id].repeatCount = repeatCount;
}
}
void setDelay(int id, uint32_t delayMs)
{
if (is_valid(id)) {
_tasks[id].delayMs = delayMs;
}
}
void enable(int id)
{
if (is_valid(id)) {
_tasks[id].enabled = true;
}
}
void disable(int id)
{
if (is_valid(id)) {
_tasks[id].enabled = false;
}
}
/**
* @brief Remove task and recycle its ID
*/
void remove(int id)
{
if (is_valid(id)) {
_tasks[id] = Task(); // Reset content
_tasks[id].enabled = false;
_tasks[id].callback = nullptr;
_free_indices.push_back(id); // Recycle index
}
}
/**
* @brief MUST be called periodically in your main loop
*/
void update()
{
uint32_t now = GetHAL().millis();
for (int i = 0; i < (int)_tasks.size(); i++) {
Task& t = _tasks[i];
if (!t.enabled) {
continue;
}
if (!t.callback) {
continue;
}
// Handle delay start
if (!t.started) {
if ((now - t.lastTick) >= t.delayMs) {
t.started = true;
t.lastTick = now;
} else {
continue;
}
}
// Handle interval execution
if ((now - t.lastTick) >= t.intervalMs) {
t.lastTick = now;
t.callback();
if (t.repeatCount > 0) {
t.repeatCount--;
if (t.repeatCount == 0) {
t.enabled = false;
}
}
}
}
}
private:
bool is_valid(int id)
{
if (id < 0) {
return false;
}
if (id >= (int)_tasks.size()) {
return false;
}
return true;
}
std::vector<Task> _tasks;
std::vector<int> _free_indices;
};
} // namespace stackchan